diff --git a/src/main/java/org/jabref/gui/BasePanel.java b/src/main/java/org/jabref/gui/BasePanel.java index 932491c4800..d64f2226ff0 100644 --- a/src/main/java/org/jabref/gui/BasePanel.java +++ b/src/main/java/org/jabref/gui/BasePanel.java @@ -182,10 +182,9 @@ public BasePanel(JabRefFrame frame, BasePanelPreferences preferences, BibDatabas this.getDatabase().registerListener(new UpdateTimestampListener(Globals.prefs)); - this.entryEditor = new EntryEditor(this, preferences.getEntryEditorPreferences(), Globals.getFileUpdateMonitor(), dialogService, externalFileTypes, Globals.TASK_EXECUTOR); + this.entryEditor = new EntryEditor(this, preferences.getEntryEditorPreferences(), Globals.getFileUpdateMonitor(), dialogService, externalFileTypes, Globals.TASK_EXECUTOR, Globals.stateManager); this.preview = new PreviewPanel(getBibDatabaseContext(), this, dialogService, externalFileTypes, Globals.getKeyPrefs(), preferences.getPreviewPreferences()); - frame().getGlobalSearchBar().getSearchQueryHighlightObservable().addSearchListener(preview); } @Subscribe diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index 630d5511eb9..f9a0076d70f 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -6,6 +6,7 @@ import java.util.stream.Collectors; import javafx.beans.binding.Bindings; +import javafx.beans.property.IntegerProperty; import javafx.beans.property.ReadOnlyListProperty; import javafx.beans.property.ReadOnlyListWrapper; import javafx.collections.FXCollections; @@ -23,9 +24,8 @@ * This class manages the GUI-state of JabRef, including: * - currently selected database * - currently selected group - * Coming soon: - * - open databases * - active search + * - active number of search results */ public class StateManager { @@ -34,6 +34,7 @@ public class StateManager { private final ObservableList selectedEntries = FXCollections.observableArrayList(); private final ObservableMap> selectedGroups = FXCollections.observableHashMap(); private final OptionalObjectProperty activeSearchQuery = OptionalObjectProperty.empty(); + private final ObservableMap searchResultMap = FXCollections.observableHashMap(); public StateManager() { activeGroups.bind(Bindings.valueAt(selectedGroups, activeDatabase.orElse(null))); @@ -47,6 +48,14 @@ public OptionalObjectProperty activeSearchQueryProperty() { return activeSearchQuery; } + public void setActiveSearchResultSize(BibDatabaseContext database, IntegerProperty resultSize) { + searchResultMap.put(database, resultSize); + } + + public IntegerProperty getSearchResultSize() { + return searchResultMap.get(activeDatabase.getValue().orElse(new BibDatabaseContext())); + } + public ReadOnlyListProperty activeGroupProperty() { return activeGroups.getReadOnlyProperty(); } @@ -79,7 +88,7 @@ public Optional getActiveDatabase() { public List getEntriesInCurrentDatabase() { return OptionalUtil.flatMap(activeDatabase.get(), BibDatabaseContext::getEntries) - .collect(Collectors.toList()); + .collect(Collectors.toList()); } public void clearSearchQuery() { diff --git a/src/main/java/org/jabref/gui/collab/EntryAddChangeViewModel.java b/src/main/java/org/jabref/gui/collab/EntryAddChangeViewModel.java index 4fa50ccca9c..45832985b5b 100644 --- a/src/main/java/org/jabref/gui/collab/EntryAddChangeViewModel.java +++ b/src/main/java/org/jabref/gui/collab/EntryAddChangeViewModel.java @@ -2,6 +2,7 @@ import javafx.scene.Node; +import org.jabref.Globals; import org.jabref.JabRefGUI; import org.jabref.gui.preview.PreviewViewer; import org.jabref.gui.undo.NamedCompound; @@ -27,7 +28,7 @@ public void makeChange(BibDatabaseContext database, NamedCompound undoEdit) { @Override public Node description() { - PreviewViewer previewViewer = new PreviewViewer(new BibDatabaseContext(), JabRefGUI.getMainFrame().getDialogService()); + PreviewViewer previewViewer = new PreviewViewer(new BibDatabaseContext(), JabRefGUI.getMainFrame().getDialogService(), Globals.stateManager); previewViewer.setEntry(diskEntry); return previewViewer; } diff --git a/src/main/java/org/jabref/gui/collab/EntryDeleteChangeViewModel.java b/src/main/java/org/jabref/gui/collab/EntryDeleteChangeViewModel.java index f8e2b88a02c..f07dcbe28c9 100644 --- a/src/main/java/org/jabref/gui/collab/EntryDeleteChangeViewModel.java +++ b/src/main/java/org/jabref/gui/collab/EntryDeleteChangeViewModel.java @@ -2,6 +2,7 @@ import javafx.scene.Node; +import org.jabref.Globals; import org.jabref.JabRefGUI; import org.jabref.gui.preview.PreviewViewer; import org.jabref.gui.undo.NamedCompound; @@ -44,7 +45,7 @@ public void makeChange(BibDatabaseContext database, NamedCompound undoEdit) { @Override public Node description() { - PreviewViewer previewViewer = new PreviewViewer(new BibDatabaseContext(), JabRefGUI.getMainFrame().getDialogService()); + PreviewViewer previewViewer = new PreviewViewer(new BibDatabaseContext(), JabRefGUI.getMainFrame().getDialogService(), Globals.stateManager); previewViewer.setEntry(memEntry); return previewViewer; } diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css index 1a1be9d2870..d0a97309716 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.css @@ -98,4 +98,10 @@ -fx-padding: 20 20 20 20; } +#bibtexSourceCodeArea .search { + -fx-fill: red; +} +.bibtexSourceCodeArea .text { + -fx-fill: -fx-text-background-color; +} \ No newline at end of file diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 2629b4d3234..72fdf53f104 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -2,7 +2,6 @@ import java.io.File; import java.nio.file.Path; -import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -27,6 +26,7 @@ import org.jabref.gui.BasePanel; import org.jabref.gui.DialogService; import org.jabref.gui.GUIGlobals; +import org.jabref.gui.StateManager; import org.jabref.gui.bibtexkeypattern.GenerateBibtexKeySingleAction; import org.jabref.gui.entryeditor.fileannotationtab.FileAnnotationTab; import org.jabref.gui.externalfiles.ExternalFilesEntryLinker; @@ -43,7 +43,6 @@ import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.EntryBasedFetcher; import org.jabref.logic.importer.WebFetchers; -import org.jabref.logic.search.SearchQueryHighlightListener; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.util.FileUpdateMonitor; @@ -71,7 +70,6 @@ public class EntryEditor extends BorderPane { private final BibDatabaseContext databaseContext; private final CountingUndoManager undoManager; private final BasePanel panel; - private final List searchListeners = new ArrayList<>(); private Subscription typeSubscription; private final List tabs; private final FileUpdateMonitor fileMonitor; @@ -90,8 +88,9 @@ public class EntryEditor extends BorderPane { private final DialogService dialogService; private final ExternalFilesEntryLinker fileLinker; private final TaskExecutor taskExecutor; + private final StateManager stateManager; - public EntryEditor(BasePanel panel, EntryEditorPreferences preferences, FileUpdateMonitor fileMonitor, DialogService dialogService, ExternalFileTypes externalFileTypes, TaskExecutor taskExecutor) { + public EntryEditor(BasePanel panel, EntryEditorPreferences preferences, FileUpdateMonitor fileMonitor, DialogService dialogService, ExternalFileTypes externalFileTypes, TaskExecutor taskExecutor, StateManager stateManager) { this.panel = panel; this.databaseContext = panel.getBibDatabaseContext(); this.undoManager = panel.getUndoManager(); @@ -99,6 +98,7 @@ public EntryEditor(BasePanel panel, EntryEditorPreferences preferences, FileUpda this.fileMonitor = fileMonitor; this.dialogService = dialogService; this.taskExecutor = taskExecutor; + this.stateManager = stateManager; fileLinker = new ExternalFilesEntryLinker(externalFileTypes, Globals.prefs.getFilePreferences(), databaseContext); @@ -108,9 +108,9 @@ public EntryEditor(BasePanel panel, EntryEditorPreferences preferences, FileUpda if (GUIGlobals.currentFont != null) { setStyle( - "text-area-background: " + ColorUtil.toHex(GUIGlobals.validFieldBackgroundColor) + ";" - + "text-area-foreground: " + ColorUtil.toHex(GUIGlobals.editorTextColor) + ";" - + "text-area-highlight: " + ColorUtil.toHex(GUIGlobals.activeBackgroundColor) + ";"); + "text-area-background: " + ColorUtil.toHex(GUIGlobals.validFieldBackgroundColor) + ";" + + "text-area-foreground: " + ColorUtil.toHex(GUIGlobals.editorTextColor) + ";" + + "text-area-highlight: " + ColorUtil.toHex(GUIGlobals.activeBackgroundColor) + ";"); } EasyBind.subscribe(tabbed.getSelectionModel().selectedItemProperty(), tab -> { @@ -139,58 +139,46 @@ public EntryEditor(BasePanel panel, EntryEditorPreferences preferences, FileUpda FileDragDropPreferenceType dragDropPreferencesType = Globals.prefs.getEntryEditorFileLinkPreference(); - if (dragDropPreferencesType == FileDragDropPreferenceType.MOVE) - { + if (dragDropPreferencesType == FileDragDropPreferenceType.MOVE) { if (event.getTransferMode() == TransferMode.LINK) //alt on win { LOGGER.debug("Mode LINK"); fileLinker.addFilesToEntry(entry, files); - } - else if (event.getTransferMode() == TransferMode.COPY) //ctrl on win, no modifier on Xubuntu + } else if (event.getTransferMode() == TransferMode.COPY) //ctrl on win, no modifier on Xubuntu { LOGGER.debug("Mode COPY"); fileLinker.copyFilesToFileDirAndAddToEntry(entry, files); - } - else - { + } else { LOGGER.debug("Mode MOVE"); //shift on win or no modifier fileLinker.moveFilesToFileDirAndAddToEntry(entry, files); } } - if (dragDropPreferencesType == FileDragDropPreferenceType.COPY) - { + if (dragDropPreferencesType == FileDragDropPreferenceType.COPY) { if (event.getTransferMode() == TransferMode.COPY) //ctrl on win, no modifier on Xubuntu { LOGGER.debug("Mode MOVE"); fileLinker.moveFilesToFileDirAndAddToEntry(entry, files); - } - else if (event.getTransferMode() == TransferMode.LINK) //alt on win + } else if (event.getTransferMode() == TransferMode.LINK) //alt on win { LOGGER.debug("Mode LINK"); fileLinker.addFilesToEntry(entry, files); - } - else - { + } else { LOGGER.debug("Mode COPY"); //shift on win or no modifier fileLinker.copyFilesToFileDirAndAddToEntry(entry, files); } } - if (dragDropPreferencesType == FileDragDropPreferenceType.LINK) - { + if (dragDropPreferencesType == FileDragDropPreferenceType.LINK) { if (event.getTransferMode() == TransferMode.COPY) //ctrl on win, no modifier on Xubuntu { LOGGER.debug("Mode COPY"); fileLinker.copyFilesToFileDirAndAddToEntry(entry, files); - } - else if (event.getTransferMode() == TransferMode.LINK) //alt on win + } else if (event.getTransferMode() == TransferMode.LINK) //alt on win { LOGGER.debug("Mode MOVE"); fileLinker.moveFilesToFileDirAndAddToEntry(entry, files); - } - else - { + } else { LOGGER.debug("Mode LINK"); //shift on win or no modifier fileLinker.addFilesToEntry(entry, files); } @@ -299,7 +287,7 @@ private List createTabs() { tabs.add(new RelatedArticlesTab(preferences, dialogService)); // Source tab - sourceTab = new SourceTab(databaseContext, undoManager, preferences.getLatexFieldFormatterPreferences(), preferences.getImportFormatPreferences(), fileMonitor, dialogService); + sourceTab = new SourceTab(databaseContext, undoManager, preferences.getLatexFieldFormatterPreferences(), preferences.getImportFormatPreferences(), fileMonitor, dialogService, stateManager); tabs.add(sourceTab); return tabs; } @@ -396,12 +384,6 @@ private void fetchAndMerge(EntryBasedFetcher fetcher) { new FetchAndMergeEntry(panel, taskExecutor).fetchAndMerge(entry, fetcher); } - void addSearchListener(SearchQueryHighlightListener listener) { - // TODO: Highlight search text in entry editors - searchListeners.add(listener); - panel.frame().getGlobalSearchBar().getSearchQueryHighlightObservable().addSearchListener(listener); - } - public void setFocusToField(String fieldName) { DefaultTaskExecutor.runInJavaFXThread(() -> { for (Tab tab : tabbed.getTabs()) { diff --git a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index 2752103316d..c8fa3c87b37 100644 --- a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -5,6 +5,9 @@ import java.io.StringWriter; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.swing.undo.UndoManager; @@ -16,6 +19,7 @@ import javafx.scene.input.InputMethodRequests; import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.undo.NamedCompound; @@ -31,6 +35,7 @@ import org.jabref.logic.importer.ParserResult; import org.jabref.logic.importer.fileformat.BibtexParser; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.search.SearchQuery; import org.jabref.model.database.BibDatabase; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; @@ -53,12 +58,16 @@ public class SourceTab extends EntryEditorTab { private final BibDatabaseMode mode; private final UndoManager undoManager; private final ObjectProperty sourceIsValid = new SimpleObjectProperty<>(); - private final ObservableRuleBasedValidator sourceValidator = new ObservableRuleBasedValidator(sourceIsValid); + @SuppressWarnings("unchecked") private final ObservableRuleBasedValidator sourceValidator = new ObservableRuleBasedValidator(sourceIsValid); private final ImportFormatPreferences importFormatPreferences; private final FileUpdateMonitor fileMonitor; private final DialogService dialogService; + private final StateManager stateManager; - public SourceTab(BibDatabaseContext bibDatabaseContext, CountingUndoManager undoManager, LatexFieldFormatterPreferences fieldFormatterPreferences, ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileMonitor, DialogService dialogService) { + private Optional searchHighlightPattern = Optional.empty(); + private CodeArea codeArea; + + public SourceTab(BibDatabaseContext bibDatabaseContext, CountingUndoManager undoManager, LatexFieldFormatterPreferences fieldFormatterPreferences, ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileMonitor, DialogService dialogService, StateManager stateManager) { this.mode = bibDatabaseContext.getMode(); this.setText(Localization.lang("%0 source", mode.getFormattedName())); this.setTooltip(new Tooltip(Localization.lang("Show/edit %0 source", mode.getFormattedName()))); @@ -68,9 +77,27 @@ public SourceTab(BibDatabaseContext bibDatabaseContext, CountingUndoManager undo this.importFormatPreferences = importFormatPreferences; this.fileMonitor = fileMonitor; this.dialogService = dialogService; + this.stateManager = stateManager; + + stateManager.activeSearchQueryProperty().addListener((observable, oldValue, newValue) -> { + searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); + highlightSearchPattern(); + }); } + private void highlightSearchPattern() { + if (searchHighlightPattern.isPresent() && codeArea != null) { + codeArea.setStyleClass(0, codeArea.getLength(), "text"); + Matcher matcher = searchHighlightPattern.get().matcher(codeArea.getText()); + while (matcher.find()) { + for (int i = 0; i <= matcher.groupCount(); i++) { + codeArea.setStyleClass(matcher.start(), matcher.end(), "search"); + } + } + } + } + private static String getSourceString(BibEntry entry, BibDatabaseMode type, LatexFieldFormatterPreferences fieldFormatterPreferences) throws IOException { StringWriter stringWriter = new StringWriter(200); LatexFieldFormatter formatter = LatexFieldFormatter.buildIgnoreHashes(fieldFormatterPreferences); @@ -115,6 +142,7 @@ private CodeArea createSourceEditor() { codeArea.insertText(codeArea.getCaretPosition(), committed); } }); + codeArea.setId("bibtexSourceCodeArea"); return codeArea; } @@ -140,7 +168,7 @@ protected void bindToEntry(BibEntry entry) { } }); this.setContent(codeArea); - + this.codeArea = codeArea; // Store source for on focus out event in the source code (within its text area) // and update source code for every change of entry field values BindingsHelper.bindContentBidirectional(entry.getFieldsObservable(), codeArea.focusedProperty(), onFocus -> { @@ -152,10 +180,12 @@ protected void bindToEntry(BibEntry entry) { codeArea.clear(); try { codeArea.appendText(getSourceString(entry, mode, fieldFormatterPreferences)); + highlightSearchPattern(); + } catch (IOException ex) { codeArea.setEditable(false); codeArea.appendText(ex.getMessage() + "\n\n" + - Localization.lang("Correct the entry, and reopen editor to display/edit source.")); + Localization.lang("Correct the entry, and reopen editor to display/edit source.")); LOGGER.debug("Incorrect entry", ex); } }); @@ -208,8 +238,7 @@ private void storeSource(BibEntry outOfFocusEntry, String text) { String fieldValue = field.getValue(); if (InternalBibtexFields.isDisplayableField(fieldName) && !newEntry.hasField(fieldName)) { - compound.addEdit( - new UndoableFieldChange(outOfFocusEntry, fieldName, fieldValue, null)); + compound.addEdit(new UndoableFieldChange(outOfFocusEntry, fieldName, fieldValue, null)); outOfFocusEntry.clearField(fieldName); } } @@ -221,8 +250,7 @@ private void storeSource(BibEntry outOfFocusEntry, String text) { String newValue = field.getValue(); if (!Objects.equals(oldValue, newValue)) { // Test if the field is legally set. - new LatexFieldFormatter(fieldFormatterPreferences) - .format(newValue, fieldName); + new LatexFieldFormatter(fieldFormatterPreferences).format(newValue, fieldName); compound.addEdit(new UndoableFieldChange(outOfFocusEntry, fieldName, oldValue, newValue)); outOfFocusEntry.setField(fieldName, newValue); @@ -243,4 +271,5 @@ private void storeSource(BibEntry outOfFocusEntry, String text) { LOGGER.debug("Incorrect source", ex); } } + } diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java index 5ef91744ea9..da3c0724790 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java @@ -4,6 +4,8 @@ import java.util.Optional; import javafx.beans.binding.Bindings; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -30,8 +32,12 @@ public MainTableDataModel(BibDatabaseContext context) { entriesFiltered.predicateProperty().bind( Bindings.createObjectBinding(() -> this::isMatched, Globals.stateManager.activeGroupProperty(), Globals.stateManager.activeSearchQueryProperty()) + ); + IntegerProperty resultSize = new SimpleIntegerProperty(); + resultSize.bind(Bindings.size(entriesFiltered)); + Globals.stateManager.setActiveSearchResultSize(context, resultSize); // We need to wrap the list since otherwise sorting in the table does not work entriesSorted = new SortedList<>(entriesFiltered); } diff --git a/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java b/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java index cec9e6a9048..778758961f3 100644 --- a/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java +++ b/src/main/java/org/jabref/gui/openoffice/StyleSelectDialogView.java @@ -11,6 +11,7 @@ import javafx.scene.control.TableView; import javafx.scene.layout.VBox; +import org.jabref.Globals; import org.jabref.gui.DialogService; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.preview.PreviewViewer; @@ -68,11 +69,11 @@ public StyleSelectDialogView(StyleLoader loader) { private void initialize() { viewModel = new StyleSelectDialogViewModel(dialogService, loader, preferencesService); - previewArticle = new PreviewViewer(new BibDatabaseContext(), dialogService); + previewArticle = new PreviewViewer(new BibDatabaseContext(), dialogService, Globals.stateManager); previewArticle.setEntry(TestEntry.getTestEntry()); vbox.getChildren().add(previewArticle); - previewBook = new PreviewViewer(new BibDatabaseContext(), dialogService); + previewBook = new PreviewViewer(new BibDatabaseContext(), dialogService, Globals.stateManager); previewBook.setEntry(TestEntry.getTestEntryBook()); vbox.getChildren().add(previewBook); diff --git a/src/main/java/org/jabref/gui/preferences/PreviewPreferencesTab.java b/src/main/java/org/jabref/gui/preferences/PreviewPreferencesTab.java index da23285e2e8..ec4c6348a23 100644 --- a/src/main/java/org/jabref/gui/preferences/PreviewPreferencesTab.java +++ b/src/main/java/org/jabref/gui/preferences/PreviewPreferencesTab.java @@ -118,7 +118,7 @@ private void setupLogic() { btnTest.setOnAction(event -> { try { - PreviewViewer testPane = new PreviewViewer(new BibDatabaseContext(), dialogService); + PreviewViewer testPane = new PreviewViewer(new BibDatabaseContext(), dialogService, Globals.stateManager); testPane.setEntry(TestEntry.getTestEntry()); PreviewLayout layout = chosen.getSelectionModel().getSelectedItem(); diff --git a/src/main/java/org/jabref/gui/preview/PreviewPanel.java b/src/main/java/org/jabref/gui/preview/PreviewPanel.java index 08db01a8a88..790aae4c1d6 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewPanel.java +++ b/src/main/java/org/jabref/gui/preview/PreviewPanel.java @@ -4,7 +4,6 @@ import java.nio.file.Path; import java.util.List; import java.util.Optional; -import java.util.regex.Pattern; import java.util.stream.Collectors; import javafx.scene.control.ContextMenu; @@ -27,7 +26,6 @@ import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.logic.citationstyle.PreviewLayout; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.search.SearchQueryHighlightListener; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.preferences.PreviewPreferences; @@ -35,7 +33,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class PreviewPanel extends VBox implements SearchQueryHighlightListener { +public class PreviewPanel extends VBox { private static final Logger LOGGER = LoggerFactory.getLogger(PreviewPanel.class); @@ -52,7 +50,7 @@ public PreviewPanel(BibDatabaseContext database, BasePanel basePanel, DialogServ this.dialogService = dialogService; fileLinker = new ExternalFilesEntryLinker(externalFileTypes, Globals.prefs.getFilePreferences(), database); - previewView = new PreviewViewer(database, dialogService); + previewView = new PreviewViewer(database, dialogService, Globals.stateManager); previewView.setLayout(preferences.getCurrentPreviewStyle()); previewView.setContextMenu(createPopupMenu()); previewView.setOnDragDetected(event -> { @@ -107,11 +105,6 @@ public void close() { basePanel.closeBottomPane(); } - @Override - public void highlightPattern(Optional newPattern) { - // TODO: Implement that search phrases are highlighted - } - public void updateLayout(PreviewPreferences previewPreferences) { updateLayout(previewPreferences, false); } diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 6432ef4cc3b..8811d398b88 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -2,9 +2,12 @@ import java.util.Objects; import java.util.Optional; +import java.util.regex.Pattern; import javafx.beans.InvalidationListener; import javafx.beans.Observable; +import javafx.beans.value.ChangeListener; +import javafx.concurrent.Worker; import javafx.print.PrinterJob; import javafx.scene.control.ScrollPane; import javafx.scene.input.ClipboardContent; @@ -13,11 +16,13 @@ import org.jabref.Globals; import org.jabref.gui.ClipBoardManager; import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.citationstyle.PreviewLayout; import org.jabref.logic.exporter.ExporterFactory; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.search.SearchQuery; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -34,22 +39,39 @@ public class PreviewViewer extends ScrollPane implements InvalidationListener { private static final Logger LOGGER = LoggerFactory.getLogger(PreviewViewer.class); + private static final String JS_HIGHLIGHT_FUNCTION = " "; + private final ClipBoardManager clipBoardManager; private final DialogService dialogService; private final TaskExecutor taskExecutor = Globals.TASK_EXECUTOR; private final WebView previewView; private PreviewLayout layout; + /** * The entry currently shown */ private Optional entry = Optional.empty(); + private Optional searchHighlightPattern = Optional.empty(); + private BibDatabaseContext database; + private boolean registered; + + private ChangeListener> listener = (queryObservable, queryOldValue, queryNewValue) -> { + searchHighlightPattern = queryNewValue.flatMap(SearchQuery::getPatternForWords); + highlightSearchPattern(); + }; /** * @param database Used for resolving strings and pdf directories for links. */ - public PreviewViewer(BibDatabaseContext database, DialogService dialogService) { + public PreviewViewer(BibDatabaseContext database, DialogService dialogService, StateManager stateManager) { this.database = Objects.requireNonNull(database); this.dialogService = dialogService; this.clipBoardManager = Globals.clipboardManager; @@ -59,6 +81,27 @@ public PreviewViewer(BibDatabaseContext database, DialogService dialogService) { previewView = new WebView(); setContent(previewView); previewView.setContextMenuEnabled(false); + + previewView.getEngine().getLoadWorker().stateProperty().addListener((observable, oldValue, newValue) -> { + + if (newValue != Worker.State.SUCCEEDED) { + return; + } + if (!registered) { + stateManager.activeSearchQueryProperty().addListener(listener); + registered = true; + } + highlightSearchPattern(); + }); + + } + + private void highlightSearchPattern() { + if (searchHighlightPattern.isPresent()) { + String pattern = searchHighlightPattern.get().pattern().replace("\\Q", "").replace("\\E", ""); + + previewView.getEngine().executeScript("highlight('" + pattern + "');"); + } } public void setLayout(PreviewLayout newLayout) { @@ -80,8 +123,8 @@ public void setEntry(BibEntry newEntry) { for (Observable observable : newEntry.getObservables()) { observable.addListener(this); } - update(); + } private void update() { @@ -93,18 +136,21 @@ private void update() { ExporterFactory.entryNumber = 1; // Set entry number in case that is included in the preview layout. BackgroundTask - .wrap(() -> layout.generatePreview(entry.get(), database.getDatabase())) - .onRunning(() -> setPreviewText("" + Localization.lang("Processing %0", Localization.lang("Citation Style")) + ": " + layout.getName() + " ..." + "")) - .onSuccess(this::setPreviewText) - .onFailure(exception -> { - LOGGER.error("Error while generating citation style", exception); - setPreviewText(Localization.lang("Error while generating citation style")); - }) - .executeWith(taskExecutor); + .wrap(() -> layout.generatePreview(entry.get(), database.getDatabase())) + .onRunning(() -> setPreviewText("" + Localization.lang("Processing %0", Localization.lang("Citation Style")) + ": " + layout.getName() + " ..." + "")) + .onSuccess(this::setPreviewText) + .onFailure(exception -> { + LOGGER.error("Error while generating citation style", exception); + setPreviewText(Localization.lang("Error while generating citation style")); + }) + .executeWith(taskExecutor); } private void setPreviewText(String text) { - previewView.getEngine().loadContent(text); + String myText = JS_HIGHLIGHT_FUNCTION + "
"; + previewView.getEngine().setJavaScriptEnabled(true); + previewView.getEngine().loadContent(myText); + this.setHvalue(0); } @@ -116,13 +162,13 @@ public void print() { } BackgroundTask - .wrap(() -> { - job.getJobSettings().setJobName(entry.flatMap(BibEntry::getCiteKeyOptional).orElse("NO ENTRY")); - previewView.getEngine().print(job); - job.endJob(); - }) - .onFailure(exception -> dialogService.showErrorDialogAndWait(Localization.lang("Could not print preview"), exception)) - .executeWith(taskExecutor); + .wrap(() -> { + job.getJobSettings().setJobName(entry.flatMap(BibEntry::getCiteKeyOptional).orElse("NO ENTRY")); + previewView.getEngine().print(job); + job.endJob(); + }) + .onFailure(exception -> dialogService.showErrorDialogAndWait(Localization.lang("Could not print preview"), exception)) + .executeWith(taskExecutor); } public void copyPreviewToClipBoard() { diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 60a46a783ca..efd2aab5258 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -44,11 +44,11 @@ import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.maintable.MainTable; +import org.jabref.gui.search.rules.describer.SearchDescribers; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; import org.jabref.logic.search.SearchQuery; -import org.jabref.logic.search.SearchQueryHighlightObservable; import org.jabref.model.entry.Author; import org.jabref.preferences.JabRefPreferences; import org.jabref.preferences.SearchPreferences; @@ -77,9 +77,6 @@ public class GlobalSearchBar extends HBox { private final Button searchModeButton = new Button(); private final Label currentResults = new Label(""); private final Tooltip tooltip = new Tooltip(); - private final SearchQueryHighlightObservable searchQueryHighlightObservable = new SearchQueryHighlightObservable(); - private SearchWorker searchWorker; - private SearchDisplayMode searchDisplayMode; public GlobalSearchBar(JabRefFrame frame) { @@ -155,11 +152,16 @@ public GlobalSearchBar(JabRefFrame frame) { } }); - this.getChildren().addAll( - searchField, - currentResults); + this.getChildren().addAll(searchField, currentResults); this.setAlignment(Pos.CENTER_LEFT); + + EasyBind.subscribe(Globals.stateManager.activeSearchQueryProperty(), searchQuery -> { + searchQuery.ifPresent(query -> { + updateResults(Globals.stateManager.getSearchResultSize().intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), + query.isGrammarBasedSearch()); + }); + }); } private void toggleSearchModeAndSearch() { @@ -181,7 +183,6 @@ public void endSearch() { clearSearch(); MainTable mainTable = frame.getCurrentBasePanel().getMainTable(); mainTable.requestFocus(); - //SwingUtilities.invokeLater(() -> mainTable.ensureVisible(mainTable.getSelectedRow())); } } @@ -199,8 +200,6 @@ private void clearSearch() { currentResults.setText(""); searchField.setText(""); setHintTooltip(null); - searchQueryHighlightObservable.reset(); - Globals.stateManager.clearSearchQuery(); } @@ -209,11 +208,6 @@ public void performSearch() { if (currentBasePanel == null) { return; } - - if (searchWorker != null) { - searchWorker.cancel(true); - } - // An empty search field should cause the search to be cleared. if (searchField.getText().isEmpty()) { clearSearch(); @@ -228,16 +222,11 @@ public void performSearch() { Globals.stateManager.setSearchQuery(searchQuery); - // TODO: Remove search worker as this is doing the work twice now - searchWorker = new SearchWorker(currentBasePanel, searchQuery, searchDisplayMode); - searchWorker.execute(); } private void informUserAboutInvalidSearchQuery() { searchField.pseudoClassStateChanged(CLASS_NO_RESULTS, true); - searchQueryHighlightObservable.reset(); - Globals.stateManager.clearSearchQuery(); String illegalSearch = Localization.lang("Search failed: illegal search expression"); @@ -270,10 +259,6 @@ private AutoCompletePopup getPopup(AutoCompletionBinding autoCompletio } } - public SearchQueryHighlightObservable getSearchQueryHighlightObservable() { - return searchQueryHighlightObservable; - } - private SearchQuery getSearchQuery() { SearchQuery searchQuery = new SearchQuery(this.searchField.getText(), this.caseSensitive.isSelected(), this.regularExp.isSelected()); this.frame.getCurrentBasePanel().setCurrentSearchQuery(searchQuery); @@ -401,6 +386,7 @@ public AutoCompletePopup getSkinnable() { @Override public void dispose() { + //empty } } } diff --git a/src/main/java/org/jabref/gui/search/SearchWorker.java b/src/main/java/org/jabref/gui/search/SearchWorker.java deleted file mode 100644 index 5e916e63ad6..00000000000 --- a/src/main/java/org/jabref/gui/search/SearchWorker.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.jabref.gui.search; - -import java.util.List; -import java.util.Objects; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; - -import javax.swing.SwingWorker; - -import org.jabref.JabRefGUI; -import org.jabref.gui.BasePanel; -import org.jabref.gui.search.rules.describer.SearchDescribers; -import org.jabref.gui.util.DefaultTaskExecutor; -import org.jabref.logic.search.SearchQuery; -import org.jabref.model.database.BibDatabase; -import org.jabref.model.entry.BibEntry; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Not reusable. Always create a new instance for each search! - */ -class SearchWorker extends SwingWorker, Void> { - - private static final Logger LOGGER = LoggerFactory.getLogger(SearchWorker.class); - - private final BibDatabase database; - - private final SearchQuery searchQuery; - - public SearchWorker(BasePanel basePanel, SearchQuery searchQuery, SearchDisplayMode searchDisplayMode) { - this.database = Objects.requireNonNull(basePanel.getDatabase()); - this.searchQuery = Objects.requireNonNull(searchQuery); - LOGGER.debug("Search (" + searchDisplayMode.getDisplayName() + "): " + this.searchQuery); - } - - @Override - protected List doInBackground() throws Exception { - return database.getEntries().parallelStream() - .filter(searchQuery::isMatch) - .collect(Collectors.toList()); - } - - @Override - protected void done() { - if (isCancelled()) { - return; - } - - try { - updateUIWithSearchResult(get()); - } catch (InterruptedException | ExecutionException e) { - LOGGER.error("something went wrong during the search", e); - } - } - - private void updateUIWithSearchResult(List matchedEntries) { - GlobalSearchBar globalSearchBar = JabRefGUI.getMainFrame().getGlobalSearchBar(); - - DefaultTaskExecutor.runInJavaFXThread(() -> - globalSearchBar.updateResults(matchedEntries.size(), - SearchDescribers.getSearchDescriberFor(searchQuery).getDescription(), - searchQuery.isGrammarBasedSearch())); - globalSearchBar.getSearchQueryHighlightObservable().fireSearchlistenerEvent(searchQuery); - } - -} diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index d33fa4f90ff..e6e35486767 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -3,6 +3,9 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.regex.Pattern; import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; @@ -63,9 +66,9 @@ private String getRegularExpressionDescription() { public String localize() { return String.format("\"%s\" (%s, %s)", - getQuery(), - getLocalizedCaseSensitiveDescription(), - getLocalizedRegularExpressionDescription()); + getQuery(), + getLocalizedCaseSensitiveDescription(), + getLocalizedRegularExpressionDescription()); } private String getLocalizedCaseSensitiveDescription() { @@ -119,6 +122,28 @@ public List getSearchWords() { } } + // Returns a regular expression pattern in the form (w1)|(w2)| ... wi are escaped if no regular expression search is enabled + public Optional getPatternForWords() { + List words = getSearchWords(); + + if ((words == null) || words.isEmpty() || words.get(0).isEmpty()) { + return Optional.empty(); + } + + // compile the words to a regular expression in the form (w1)|(w2)|(w3) + StringJoiner joiner = new StringJoiner(")|(", "(", ")"); + for (String word : words) { + joiner.add(regularExpression ? word : Pattern.quote(word)); + } + String searchPattern = joiner.toString(); + + if (caseSensitive) { + return Optional.of(Pattern.compile(searchPattern)); + } else { + return Optional.of(Pattern.compile(searchPattern, Pattern.CASE_INSENSITIVE)); + } + } + public SearchRule getRule() { return rule; } diff --git a/src/main/java/org/jabref/logic/search/SearchQueryHighlightListener.java b/src/main/java/org/jabref/logic/search/SearchQueryHighlightListener.java deleted file mode 100644 index 5f88be0ae0a..00000000000 --- a/src/main/java/org/jabref/logic/search/SearchQueryHighlightListener.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.jabref.logic.search; - -import java.util.Optional; -import java.util.regex.Pattern; - -import com.google.common.eventbus.Subscribe; - -/** - * Every Listener that wants to receive events from a search needs to - * implement this interface - */ -@FunctionalInterface -public interface SearchQueryHighlightListener { - - /** - * Pattern with which one can determine what to highlight - */ - @Subscribe - void highlightPattern(Optional highlightPattern); -} diff --git a/src/main/java/org/jabref/logic/search/SearchQueryHighlightObservable.java b/src/main/java/org/jabref/logic/search/SearchQueryHighlightObservable.java deleted file mode 100644 index 8046f442446..00000000000 --- a/src/main/java/org/jabref/logic/search/SearchQueryHighlightObservable.java +++ /dev/null @@ -1,86 +0,0 @@ -package org.jabref.logic.search; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.StringJoiner; -import java.util.regex.Pattern; - -import com.google.common.eventbus.EventBus; - -public class SearchQueryHighlightObservable { - - private final EventBus eventBus = new EventBus(); - - private Optional pattern = Optional.empty(); - - /** - * Adds a SearchQueryHighlightListener to the search bar. The added listener is immediately informed about the current search. - * Subscribers will be notified about searches. - * - * @param newListener SearchQueryHighlightListener to be added - */ - public void addSearchListener(SearchQueryHighlightListener newListener) { - Objects.requireNonNull(newListener); - - eventBus.register(newListener); - newListener.highlightPattern(pattern); - - } - - public void removeSearchListener(SearchQueryHighlightListener listener) { - Objects.requireNonNull(listener); - - try { - eventBus.unregister(listener); - } catch (IllegalArgumentException e) { - // occurs if the event source has not been registered, should not prevent shutdown - } - } - /** - * Fires an event if a search was started (or cleared) - * - * @param searchQuery the search query - */ - - public void fireSearchlistenerEvent(SearchQuery searchQuery) { - Objects.requireNonNull(searchQuery); - - // Parse the search string to words - pattern = getPatternForWords(searchQuery.getSearchWords(), searchQuery.isRegularExpression(), - searchQuery.isCaseSensitive()); - - update(); - } - - public void reset() { - pattern = Optional.empty(); - update(); - } - - private void update() { - // Fire an event for every listener - eventBus.post(pattern); - } - - // Returns a regular expression pattern in the form (w1)|(w2)| ... wi are escaped if no regular expression search is enabled - public static Optional getPatternForWords(List words, boolean useRegex, boolean isCaseSensitive) { - if ((words == null) || words.isEmpty() || words.get(0).isEmpty()) { - return Optional.empty(); - } - - // compile the words to a regular expression in the form (w1)|(w2)|(w3) - StringJoiner joiner = new StringJoiner(")|(", "(", ")"); - for (String word : words) { - joiner.add(useRegex ? word : Pattern.quote(word)); - } - String searchPattern = joiner.toString(); - - if (isCaseSensitive) { - return Optional.of(Pattern.compile(searchPattern)); - } else { - return Optional.of(Pattern.compile(searchPattern, Pattern.CASE_INSENSITIVE)); - } - } - -} diff --git a/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java b/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java index c3f50e3c853..20d11612483 100644 --- a/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java @@ -7,6 +7,7 @@ import javafx.stage.Stage; import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.logic.bibtex.LatexFieldFormatterPreferences; import org.jabref.logic.importer.ImportFormatPreferences; @@ -38,7 +39,7 @@ public class SourceTabTest { public void onStart(Stage stage) { area = new CodeArea(); area.appendText("some example\n text to go here\n across a couple of \n lines...."); - sourceTab = new SourceTab(new BibDatabaseContext(), new CountingUndoManager(), new LatexFieldFormatterPreferences(), mock(ImportFormatPreferences.class), new DummyFileUpdateMonitor(), mock(DialogService.class)); + sourceTab = new SourceTab(new BibDatabaseContext(), new CountingUndoManager(), new LatexFieldFormatterPreferences(), mock(ImportFormatPreferences.class), new DummyFileUpdateMonitor(), mock(DialogService.class), mock(StateManager.class)); pane = new TabPane( new Tab("main area", area), new Tab("other tab", new Label("some text")), diff --git a/src/test/java/org/jabref/logic/search/SearchQueryHighlightObservableTest.java b/src/test/java/org/jabref/logic/search/SearchQueryHighlightObservableTest.java deleted file mode 100644 index fbe998e8a85..00000000000 --- a/src/test/java/org/jabref/logic/search/SearchQueryHighlightObservableTest.java +++ /dev/null @@ -1,59 +0,0 @@ -package org.jabref.logic.search; - -import java.util.Optional; -import java.util.regex.Pattern; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.atLeastOnce; -import static org.mockito.Mockito.verify; - -class SearchQueryHighlightObservableTest { - - @Captor ArgumentCaptor> captor; - @Mock private SearchQueryHighlightListener listener; - private SearchQueryHighlightObservable observable; - - @BeforeEach - void setUp() throws Exception { - observable = new SearchQueryHighlightObservable(); - MockitoAnnotations.initMocks(this); - } - - @Test - void addSearchListenerNotifiesListenerAboutPreviousPattern() throws Exception { - observable.fireSearchlistenerEvent(new SearchQuery("test", false, false)); - - observable.addSearchListener(listener); - - verify(listener).highlightPattern(captor.capture()); - assertEquals("(\\Qtest\\E)", captor.getValue().get().pattern()); - } - - @Test - void addSearchListenerNotifiesRegisteredListener() throws Exception { - observable.addSearchListener(listener); - - observable.fireSearchlistenerEvent(new SearchQuery("test", false, false)); - - verify(listener, atLeastOnce()).highlightPattern(captor.capture()); - assertEquals("(\\Qtest\\E)", captor.getValue().get().pattern()); - } - - @Test - void addSearchListenerNotifiesRegisteredListenerAboutGrammarBasedSearches() throws Exception { - observable.addSearchListener(listener); - - observable.fireSearchlistenerEvent(new SearchQuery("author=harrer", false, false)); - - verify(listener, atLeastOnce()).highlightPattern(captor.capture()); - // TODO: We would expect "harrer" here - assertEquals("(\\Qauthor=harrer\\E)", captor.getValue().get().pattern()); - } -} diff --git a/src/test/java/org/jabref/logic/search/SearchQueryTest.java b/src/test/java/org/jabref/logic/search/SearchQueryTest.java index e4463f75155..34593c55d85 100644 --- a/src/test/java/org/jabref/logic/search/SearchQueryTest.java +++ b/src/test/java/org/jabref/logic/search/SearchQueryTest.java @@ -1,5 +1,8 @@ package org.jabref.logic.search; +import java.util.Optional; +import java.util.regex.Pattern; + import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibtexEntryTypes; import org.jabref.model.entry.FieldName; @@ -189,7 +192,15 @@ public void testSimpleTerm() { String query = "progress"; SearchQuery result = new SearchQuery(query, false, false); - assertFalse(result.isGrammarBasedSearch()); } + + @Test + public void testGetPattern() { + String query = "progress"; + SearchQuery result = new SearchQuery(query, false, false); + Pattern pattern = Pattern.compile("(\\Qprogress\\E)"); + //We can't directly compare the pattern objects + assertEquals(Optional.of(pattern.toString()), result.getPatternForWords().map(Pattern::toString)); + } }