Skip to content

Commit

Permalink
Improve presentation of fulltext search results (#7989)
Browse files Browse the repository at this point in the history
  • Loading branch information
btut authored Aug 21, 2021
1 parent fd1cab0 commit 09c0f59
Show file tree
Hide file tree
Showing 17 changed files with 347 additions and 125 deletions.
6 changes: 3 additions & 3 deletions src/main/java/org/jabref/gui/Base.css
Original file line number Diff line number Diff line change
Expand Up @@ -287,15 +287,15 @@
-fx-font-size: 1em;
}

.tooltip > TextFlow > .tooltip-text-bold {
TextFlow > .tooltip-text-bold {
-fx-font-weight: bold;
}

.tooltip > TextFlow > .tooltip-text-italic {
TextFlow > .tooltip-text-italic {
-fx-font-style: italic;
}

.tooltip > TextFlow > .tooltip-text-monospaced {
TextFlow > .tooltip-text-monospaced {
-fx-font-family: monospace;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
<Tooltip text="%Show the document of the currently selected entry."/>
</tooltip>
</ToggleButton>
<ToggleButton mnemonicParsing="false" styleClass="icon-button" text="%Locked"
<ToggleButton fx:id="modeLock" mnemonicParsing="false" styleClass="icon-button" text="%Locked"
toggleGroup="$toggleGroupMode">
<tooltip>
<Tooltip text="%Show this document until unlocked."/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public class DocumentViewerView extends BaseDialog<Void> {
@FXML private ComboBox<LinkedFile> fileChoice;
@FXML private BorderPane mainPane;
@FXML private ToggleButton modeLive;
@FXML private ToggleButton modeLock;
@FXML private TextField currentPage;
@FXML private Label maxPages;

Expand Down Expand Up @@ -64,6 +65,7 @@ private void initialize() {

private void setupModeButtons() {
viewModel.liveModeProperty().bind(modeLive.selectedProperty());
modeLock.selectedProperty().bind(modeLive.selectedProperty().not());
}

private void setupScrollbar() {
Expand Down Expand Up @@ -103,6 +105,14 @@ private void setupViewer() {
mainPane.setCenter(viewer);
}

public void setLiveMode(boolean liveMode) {
modeLive.setSelected(liveMode);
}

public void gotoPage(int pageNumber) {
viewModel.showPage(pageNumber);
}

public void nextPage(ActionEvent actionEvent) {
viewModel.showNextPage();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ public BooleanProperty liveModeProperty() {
return liveMode;
}

public void showPage(int pageNumber) {
currentPage.set(pageNumber);
}

public void showNextPage() {
currentPage.set(getCurrentPage() + 1);
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ private List<EntryEditorTab> createTabs() {
// LaTeX citations tab
entryEditorTabs.add(new LatexCitationsTab(databaseContext, preferencesService, taskExecutor, dialogService));

entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService.getTheme(), preferencesService.getFilePreferences()));
entryEditorTabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService));

return entryEditorTabs;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,69 @@
package org.jabref.gui.entryeditor.fileannotationtab;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;

import javafx.scene.web.WebView;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Separator;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;

import org.jabref.gui.DialogService;
import org.jabref.gui.StateManager;
import org.jabref.gui.actions.ActionFactory;
import org.jabref.gui.actions.StandardActions;
import org.jabref.gui.desktop.JabRefDesktop;
import org.jabref.gui.documentviewer.DocumentViewerView;
import org.jabref.gui.entryeditor.EntryEditorTab;
import org.jabref.gui.util.OpenHyperlinksInExternalBrowser;
import org.jabref.gui.util.Theme;
import org.jabref.gui.maintable.OpenExternalFileAction;
import org.jabref.gui.maintable.OpenFolderAction;
import org.jabref.gui.util.TooltipTextUtil;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.LinkedFile;
import org.jabref.model.pdf.search.PdfSearchResults;
import org.jabref.model.pdf.search.SearchResult;
import org.jabref.model.search.rules.SearchRules;
import org.jabref.preferences.FilePreferences;
import org.jabref.preferences.PreferencesService;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class FulltextSearchResultsTab extends EntryEditorTab {

private static final Logger LOGGER = LoggerFactory.getLogger(FulltextSearchResultsTab.class);

private final StateManager stateManager;
private final FilePreferences filePreferences;
private final PreferencesService preferencesService;
private final DialogService dialogService;
private final ActionFactory actionFactory;

private final TextFlow content;

private BibEntry entry;

private final WebView webView;
private DocumentViewerView documentViewerView;

public FulltextSearchResultsTab(StateManager stateManager, Theme theme, FilePreferences filePreferences) {
public FulltextSearchResultsTab(StateManager stateManager, PreferencesService preferencesService, DialogService dialogService) {
this.stateManager = stateManager;
this.filePreferences = filePreferences;
webView = new WebView();
setTheme(theme);
webView.getEngine().loadContent(wrapHTML("<p>" + Localization.lang("Search results") + "</p>"));
setContent(webView);
webView.getEngine().getLoadWorker().stateProperty().addListener(new OpenHyperlinksInExternalBrowser(webView));
this.preferencesService = preferencesService;
this.dialogService = dialogService;
this.actionFactory = new ActionFactory(preferencesService.getKeyBindingRepository());

content = new TextFlow();
ScrollPane scrollPane = new ScrollPane(content);
scrollPane.setFitToWidth(true);
content.setPadding(new Insets(10));
setContent(scrollPane);
setText(Localization.lang("Search results"));
this.stateManager.activeSearchQueryProperty().addListener((observable, oldValue, newValue) -> bindToEntry(entry));
}

@Override
Expand All @@ -44,39 +76,96 @@ public boolean shouldShow(BibEntry entry) {

@Override
protected void bindToEntry(BibEntry entry) {
if (!shouldShow(entry)) {
if (entry == null || !shouldShow(entry)) {
return;
}
if (!entry.equals(this.entry)) {
documentViewerView = new DocumentViewerView();
}
this.entry = entry;
PdfSearchResults searchResults = stateManager.activeSearchQueryProperty().get().get().getRule().getFulltextResults(stateManager.activeSearchQueryProperty().get().get().getQuery(), entry);
StringBuilder content = new StringBuilder();

content.append("<p>");
content.getChildren().clear();

if (searchResults.numSearchResults() == 0) {
content.append(Localization.lang("No search matches."));
} else {
content.append(Localization.lang("Search results"));
content.getChildren().add(new Text(Localization.lang("No search matches.")));
}
content.append("</p>");

for (SearchResult searchResult : searchResults.getSearchResults()) {
content.append("<p>");
LinkedFile linkedFile = new LinkedFile("just for link", Path.of(searchResult.getPath()), "pdf");
Path resolvedPath = linkedFile.findIn(stateManager.getActiveDatabase().get(), filePreferences).orElse(Path.of(searchResult.getPath()));
String link = "<a href=" + resolvedPath.toAbsolutePath().toString() + ">" + searchResult.getPath() + "</a>";
content.append(Localization.lang("Found match in %0", link));
content.append("</p><p>");
content.append(searchResult.getHtml());
content.append("</p>");

// Iterate through files with search hits
for (Map.Entry<String, List<SearchResult>> resultsForPath : searchResults.getSearchResultsByPath().entrySet()) {
content.getChildren().addAll(createFileLink(resultsForPath.getKey()), lineSeparator());

// Iterate through pages (within file) with search hits
for (SearchResult searchResult : resultsForPath.getValue()) {
for (String resultTextHtml : searchResult.getContentResultStringsHtml()) {
content.getChildren().addAll(TooltipTextUtil.createTextsFromHtml(resultTextHtml.replaceAll("</b> <b>", " ")));
content.getChildren().addAll(new Text(System.lineSeparator()), lineSeparator(0.8), createPageLink(searchResult.getPageNumber()));
}
if (!searchResult.getAnnotationsResultStringsHtml().isEmpty()) {
Text annotationsText = new Text(System.lineSeparator() + Localization.lang("Found matches in Annotations:") + System.lineSeparator() + System.lineSeparator());
annotationsText.setStyle("-fx-font-style: italic;");
content.getChildren().add(annotationsText);
}
for (String resultTextHtml : searchResult.getAnnotationsResultStringsHtml()) {
content.getChildren().addAll(TooltipTextUtil.createTextsFromHtml(resultTextHtml.replaceAll("</b> <b>", " ")));
content.getChildren().addAll(new Text(System.lineSeparator()), lineSeparator(0.8), createPageLink(searchResult.getPageNumber()));
}
}
}
}

private Text createFileLink(String pathToFile) {
LinkedFile linkedFile = new LinkedFile("", Path.of(pathToFile), "pdf");
Text fileLinkText = new Text(Localization.lang("Found match in %0", pathToFile) + System.lineSeparator() + System.lineSeparator());
fileLinkText.setStyle("-fx-font-weight: bold;");

ContextMenu fileContextMenu = getFileContextMenu(linkedFile);
Path resolvedPath = linkedFile.findIn(stateManager.getActiveDatabase().get(), preferencesService.getFilePreferences()).orElse(Path.of(pathToFile));
Tooltip fileLinkTooltip = new Tooltip(resolvedPath.toAbsolutePath().toString());
Tooltip.install(fileLinkText, fileLinkTooltip);
fileLinkText.setOnMouseClicked(event -> {
if (MouseButton.PRIMARY.equals(event.getButton())) {
try {
JabRefDesktop.openBrowser(resolvedPath.toUri());
} catch (IOException e) {
LOGGER.error("Cannot open {}.", resolvedPath.toString(), e);
}
} else {
fileContextMenu.show(fileLinkText, event.getScreenX(), event.getScreenY());
}
});
return fileLinkText;
}

private Text createPageLink(int pageNumber) {
Text pageLink = new Text(Localization.lang("On page %0", pageNumber) + System.lineSeparator() + System.lineSeparator());
pageLink.setStyle("-fx-font-style: italic; -fx-font-weight: bold;");

pageLink.setOnMouseClicked(event -> {
if (MouseButton.PRIMARY.equals(event.getButton())) {
documentViewerView.gotoPage(pageNumber);
documentViewerView.setLiveMode(false);
documentViewerView.show();
}
});
return pageLink;
}

webView.getEngine().loadContent(wrapHTML(content.toString()));
private ContextMenu getFileContextMenu(LinkedFile file) {
ContextMenu fileContextMenu = new ContextMenu();
fileContextMenu.getItems().add(actionFactory.createMenuItem(StandardActions.OPEN_FOLDER, new OpenFolderAction(dialogService, stateManager, preferencesService, entry, file)));
fileContextMenu.getItems().add(actionFactory.createMenuItem(StandardActions.OPEN_EXTERNAL_FILE, new OpenExternalFileAction(dialogService, stateManager, preferencesService)));
return fileContextMenu;
}

private String wrapHTML(String content) {
return "<html><body id=\"previewBody\"><div id=\"content\">" + content + "</div></body></html>";
private Separator lineSeparator() {
return lineSeparator(1.0);
}

public void setTheme(Theme theme) {
theme.getAdditionalStylesheet().ifPresent(location -> webView.getEngine().setUserStyleSheetLocation(location));
private Separator lineSeparator(double widthMultiplier) {
Separator lineSeparator = new Separator(Orientation.HORIZONTAL);
lineSeparator.prefWidthProperty().bind(content.widthProperty().multiply(widthMultiplier));
lineSeparator.setPrefHeight(15);
return lineSeparator;
}
}
79 changes: 52 additions & 27 deletions src/main/java/org/jabref/gui/maintable/OpenExternalFileAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,27 @@ public class OpenExternalFileAction extends SimpleCommand {
private final StateManager stateManager;
private final PreferencesService preferencesService;

private final BibEntry entry;
private final LinkedFile linkedFile;

public OpenExternalFileAction(DialogService dialogService, StateManager stateManager, PreferencesService preferencesService) {
this(dialogService, stateManager, preferencesService, null, null);
}

public OpenExternalFileAction(DialogService dialogService, StateManager stateManager, PreferencesService preferencesService, BibEntry entry, LinkedFile linkedFile) {
this.dialogService = dialogService;
this.stateManager = stateManager;
this.preferencesService = preferencesService;
this.entry = entry;
this.linkedFile = linkedFile;

this.executable.bind(ActionHelper.hasLinkedFileForSelectedEntries(stateManager)
.and(ActionHelper.needsEntriesSelected(stateManager))
);
if (this.linkedFile == null) {
this.executable.bind(ActionHelper.hasLinkedFileForSelectedEntries(stateManager)
.and(ActionHelper.needsEntriesSelected(stateManager))
);
} else {
this.setExecutable(true);
}
}

/**
Expand All @@ -41,37 +54,49 @@ public OpenExternalFileAction(DialogService dialogService, StateManager stateMan
@Override
public void execute() {
stateManager.getActiveDatabase().ifPresent(databaseContext -> {
final List<BibEntry> selectedEntries = stateManager.getSelectedEntries();
if (entry == null) {
final List<BibEntry> selectedEntries = stateManager.getSelectedEntries();

List<LinkedFileViewModel> linkedFileViewModelList = new LinkedList<>();
LinkedFileViewModel linkedFileViewModel;
List<LinkedFileViewModel> linkedFileViewModelList = new LinkedList<>();
LinkedFileViewModel linkedFileViewModel;

for (BibEntry entry:selectedEntries) {
for (LinkedFile linkedFile:entry.getFiles()) {
linkedFileViewModel = new LinkedFileViewModel(
linkedFile,
entry,
databaseContext,
Globals.TASK_EXECUTOR,
dialogService,
preferencesService,
ExternalFileTypes.getInstance());
for (BibEntry entry : selectedEntries) {
for (LinkedFile linkedFile : entry.getFiles()) {
linkedFileViewModel = new LinkedFileViewModel(
linkedFile,
entry,
databaseContext,
Globals.TASK_EXECUTOR,
dialogService,
preferencesService,
ExternalFileTypes.getInstance());

linkedFileViewModelList.add(linkedFileViewModel);
linkedFileViewModelList.add(linkedFileViewModel);
}
}
}

// ask the user when detecting # of files > FILES_LIMIT
if (linkedFileViewModelList.size() > FILES_LIMIT) {
boolean continueOpening = dialogService.showConfirmationDialogAndWait(Localization.lang("Opening large number of files"),
Localization.lang("You are about to open %0 files. Continue?", Integer.toString(linkedFileViewModelList.size())),
Localization.lang("Continue"), Localization.lang("Cancel"));
if (!continueOpening) {
return;
// ask the user when detecting # of files > FILES_LIMIT
if (linkedFileViewModelList.size() > FILES_LIMIT) {
boolean continueOpening = dialogService.showConfirmationDialogAndWait(Localization.lang("Opening large number of files"),
Localization.lang("You are about to open %0 files. Continue?", linkedFileViewModelList.size()),
Localization.lang("Continue"), Localization.lang("Cancel"));
if (!continueOpening) {
return;
}
}
}

linkedFileViewModelList.forEach(LinkedFileViewModel::open);
linkedFileViewModelList.forEach(LinkedFileViewModel::open);
} else {
LinkedFileViewModel linkedFileViewModel = new LinkedFileViewModel(
linkedFile,
entry,
databaseContext,
Globals.TASK_EXECUTOR,
dialogService,
preferencesService,
ExternalFileTypes.getInstance());
linkedFileViewModel.open();
}
});
}
}
Loading

0 comments on commit 09c0f59

Please sign in to comment.