diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 6f66180350b..714a6c92e01 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -55,6 +55,7 @@ import org.jabref.gui.menus.FileHistoryMenu; import org.jabref.gui.push.PushToApplicationCommand; import org.jabref.gui.search.GlobalSearchBar; +import org.jabref.gui.search.SearchType; import org.jabref.gui.sidepane.SidePane; import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.undo.CountingUndoManager; @@ -136,7 +137,7 @@ public JabRefFrame(Stage mainStage, this.entryTypesManager = Globals.entryTypesManager; this.taskExecutor = Globals.TASK_EXECUTOR; - this.globalSearchBar = new GlobalSearchBar(this, stateManager, prefs, undoManager, dialogService); + this.globalSearchBar = new GlobalSearchBar(this, stateManager, prefs, undoManager, dialogService, SearchType.NORMAL_SEARCH); this.pushToApplicationCommand = new PushToApplicationCommand(stateManager, dialogService, prefs, taskExecutor); this.fileHistory = new FileHistoryMenu(prefs.getGuiPreferences().getFileHistory(), dialogService, getOpenDatabaseAction()); diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index bdc1659ac75..2c842cd213d 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -59,6 +59,8 @@ public class StateManager { private final ObservableList selectedEntries = FXCollections.observableArrayList(); private final ObservableMap> selectedGroups = FXCollections.observableHashMap(); private final OptionalObjectProperty activeSearchQuery = OptionalObjectProperty.empty(); + private final OptionalObjectProperty activeGlobalSearchQuery = OptionalObjectProperty.empty(); + private final IntegerProperty globalSearchResultSize = new SimpleIntegerProperty(0); private final ObservableMap searchResultMap = FXCollections.observableHashMap(); private final OptionalObjectProperty focusOwner = OptionalObjectProperty.empty(); private final ObservableList, Task>> backgroundTasks = FXCollections.observableArrayList(task -> new Observable[]{task.getValue().progressProperty(), task.getValue().runningProperty()}); @@ -108,6 +110,22 @@ public IntegerProperty getSearchResultSize() { return searchResultMap.getOrDefault(activeDatabase.getValue().orElse(new BibDatabaseContext()), new SimpleIntegerProperty(0)); } + public OptionalObjectProperty activeGlobalSearchQueryProperty() { + return activeGlobalSearchQuery; + } + + public IntegerProperty getGlobalSearchResultSize() { + return globalSearchResultSize; + } + + public IntegerProperty getSearchResultSize(OptionalObjectProperty searchQueryProperty) { + if (searchQueryProperty.equals(activeSearchQuery)) { + return getSearchResultSize(); + } else { + return getGlobalSearchResultSize(); + } + } + public ReadOnlyListProperty activeGroupProperty() { return activeGroups.getReadOnlyProperty(); } @@ -151,8 +169,12 @@ public void clearSearchQuery() { activeSearchQuery.setValue(Optional.empty()); } - public void setSearchQuery(SearchQuery searchQuery) { - activeSearchQuery.setValue(Optional.of(searchQuery)); + public void setSearchQuery(OptionalObjectProperty searchQueryProperty, SearchQuery query) { + searchQueryProperty.setValue(Optional.of(query)); + } + + public void clearSearchQuery(OptionalObjectProperty searchQueryProperty) { + searchQueryProperty.setValue(Optional.empty()); } public OptionalObjectProperty focusOwnerProperty() { diff --git a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index bd39e442b1d..7cdc06c0531 100644 --- a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -125,6 +125,11 @@ public SourceTab(BibDatabaseContext bibDatabaseContext, searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); highlightSearchPattern(); }); + + stateManager.activeGlobalSearchQueryProperty().addListener((observable, oldValue, newValue) -> { + searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); + highlightSearchPattern(); + }); } private void highlightSearchPattern() { diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 379e45af226..6df1c946292 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -170,6 +170,7 @@ public PreviewViewer(BibDatabaseContext database, } if (!registered) { stateManager.activeSearchQueryProperty().addListener(listener); + stateManager.activeGlobalSearchQueryProperty().addListener(listener); registered = true; } highlightSearchPattern(); diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 8f0ce5a4a17..1170ebeb975 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -54,10 +54,10 @@ import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.search.rules.describer.SearchDescribers; -import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.util.BindingsHelper; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.IconValidationDecorator; +import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; import org.jabref.logic.search.SearchQuery; @@ -94,7 +94,6 @@ public class GlobalSearchBar extends HBox { private final ToggleButton fulltextButton; private final Button openGlobalSearchButton; private final ToggleButton keepSearchString; - // private final Button searchModeButton; private final Tooltip searchFieldTooltip = new Tooltip(); private final Label currentResults = new Label(""); @@ -102,24 +101,34 @@ public class GlobalSearchBar extends HBox { private final PreferencesService preferencesService; private final Validator regexValidator; private final UndoManager undoManager; + private final LibraryTabContainer tabContainer; private final SearchPreferences searchPreferences; private final DialogService dialogService; private final BooleanProperty globalSearchActive = new SimpleBooleanProperty(false); + private final OptionalObjectProperty searchQueryProperty; private GlobalSearchResultDialog globalSearchResultDialog; public GlobalSearchBar(LibraryTabContainer tabContainer, StateManager stateManager, PreferencesService preferencesService, - CountingUndoManager undoManager, - DialogService dialogService) { + UndoManager undoManager, + DialogService dialogService, + SearchType searchType) { super(); this.stateManager = stateManager; this.preferencesService = preferencesService; this.searchPreferences = preferencesService.getSearchPreferences(); this.undoManager = undoManager; this.dialogService = dialogService; + this.tabContainer = tabContainer; + + if (searchType == SearchType.NORMAL_SEARCH) { + searchQueryProperty = stateManager.activeSearchQueryProperty(); + } else { + searchQueryProperty = stateManager.activeGlobalSearchQueryProperty(); + } searchField.disableProperty().bind(needsDatabase(stateManager).not()); @@ -138,7 +147,9 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, if (keyBinding.get() == KeyBinding.CLOSE) { // Clear search and select first entry, if available searchField.setText(""); - tabContainer.getCurrentLibraryTab().getMainTable().getSelectionModel().selectFirst(); + if (searchType == SearchType.NORMAL_SEARCH) { + tabContainer.getCurrentLibraryTab().getMainTable().getSelectionModel().selectFirst(); + } event.consume(); } } @@ -188,7 +199,13 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, keepSearchString.visibleProperty().unbind(); keepSearchString.visibleProperty().bind(focusedOrActive); - StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString)); + StackPane modifierButtons; + if (searchType == SearchType.NORMAL_SEARCH) { + modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString)); + } else { + modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton)); + } + modifierButtons.setAlignment(Pos.CENTER); searchField.setRight(new HBox(searchField.getRight(), modifierButtons)); searchField.getStyleClass().add("search-field"); @@ -203,13 +220,18 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, visualizer.setDecoration(new IconValidationDecorator(Pos.CENTER_LEFT)); Platform.runLater(() -> visualizer.initVisualization(regexValidator.getValidationStatus(), searchField)); - this.getChildren().addAll(searchField, openGlobalSearchButton, currentResults); + if (searchType == SearchType.NORMAL_SEARCH) { + this.getChildren().addAll(searchField, openGlobalSearchButton, currentResults); + } else { + this.getChildren().addAll(searchField, currentResults); + } + this.setSpacing(4.0); this.setAlignment(Pos.CENTER_LEFT); Timer searchTask = FxTimer.create(Duration.ofMillis(SEARCH_DELAY), this::updateSearchQuery); BindingsHelper.bindBidirectional( - stateManager.activeSearchQueryProperty(), + searchQueryProperty, searchField.textProperty(), searchTerm -> { // Async update @@ -217,8 +239,8 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, }, query -> setSearchTerm(query.map(SearchQuery::getQuery).orElse(""))); - this.stateManager.activeSearchQueryProperty().addListener((obs, oldvalue, newValue) -> newValue.ifPresent(this::updateSearchResultsForQuery)); - this.stateManager.activeDatabaseProperty().addListener((obs, oldValue, newValue) -> stateManager.activeSearchQueryProperty().get().ifPresent(this::updateSearchResultsForQuery)); + this.searchQueryProperty.addListener((obs, oldValue, newValue) -> newValue.ifPresent(this::updateSearchResultsForQuery)); + this.searchQueryProperty.addListener((obs, oldValue, newValue) -> searchQueryProperty.get().ifPresent(this::updateSearchResultsForQuery)); /* * The listener tracks a change on the focus property value. * This happens, from active (user types a query) to inactive / focus @@ -234,7 +256,7 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, } private void updateSearchResultsForQuery(SearchQuery query) { - updateResults(this.stateManager.getSearchResultSize().intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), + updateResults(this.stateManager.getSearchResultSize(searchQueryProperty).intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), query.isGrammarBasedSearch()); } @@ -276,7 +298,7 @@ private void initSearchModifierButtons() { initSearchModifierButton(openGlobalSearchButton); openGlobalSearchButton.setOnAction(evt -> { globalSearchActive.setValue(true); - globalSearchResultDialog = new GlobalSearchResultDialog(undoManager); + globalSearchResultDialog = new GlobalSearchResultDialog(undoManager, tabContainer); updateSearchQuery(); dialogService.showCustomDialogAndWait(globalSearchResultDialog); globalSearchActive.setValue(false); @@ -312,7 +334,7 @@ public void updateSearchQuery() { if (searchField.getText().isEmpty()) { currentResults.setText(""); setSearchFieldHintTooltip(null); - stateManager.clearSearchQuery(); + stateManager.clearSearchQuery(searchQueryProperty); return; } @@ -327,7 +349,7 @@ public void updateSearchQuery() { informUserAboutInvalidSearchQuery(); return; } - stateManager.setSearchQuery(searchQuery); + stateManager.setSearchQuery(searchQueryProperty, searchQuery); } private boolean validRegex() { @@ -343,7 +365,7 @@ private boolean validRegex() { private void informUserAboutInvalidSearchQuery() { searchField.pseudoClassStateChanged(CLASS_NO_RESULTS, true); - stateManager.clearSearchQuery(); + stateManager.clearSearchQuery(searchQueryProperty); String illegalSearch = Localization.lang("Search failed: illegal search expression"); currentResults.setText(illegalSearch); diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml index 88dbc0cbbe4..0823a696e1c 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml +++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml @@ -13,7 +13,7 @@ prefHeight="400.0" prefWidth="600.0"> - + diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java index 338ed960023..1803fa5af9a 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java @@ -5,11 +5,14 @@ import javafx.fxml.FXML; import javafx.scene.control.SplitPane; import javafx.scene.control.ToggleButton; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.stage.Modality; import javafx.stage.Stage; import javafx.stage.WindowEvent; import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTabContainer; import org.jabref.gui.StateManager; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.maintable.columns.SpecialFieldColumn; @@ -28,9 +31,9 @@ public class GlobalSearchResultDialog extends BaseDialog { @FXML private SplitPane container; @FXML private ToggleButton keepOnTop; - + @FXML private HBox searchBarContainer; private final UndoManager undoManager; - + private final LibraryTabContainer libraryTabContainer; @Inject private PreferencesService preferencesService; @Inject private StateManager stateManager; @Inject private DialogService dialogService; @@ -39,8 +42,9 @@ public class GlobalSearchResultDialog extends BaseDialog { private GlobalSearchResultDialogViewModel viewModel; - public GlobalSearchResultDialog(UndoManager undoManager) { + public GlobalSearchResultDialog(UndoManager undoManager, LibraryTabContainer libraryTabContainer) { this.undoManager = undoManager; + this.libraryTabContainer = libraryTabContainer; setTitle(Localization.lang("Search results from open libraries")); ViewLoader.view(this) @@ -53,6 +57,10 @@ public GlobalSearchResultDialog(UndoManager undoManager) { private void initialize() { viewModel = new GlobalSearchResultDialogViewModel(preferencesService); + GlobalSearchBar searchBar = new GlobalSearchBar(libraryTabContainer, stateManager, preferencesService, undoManager, dialogService, SearchType.GLOBAL_SEARCH); + searchBarContainer.getChildren().addFirst(searchBar); + HBox.setHgrow(searchBar, Priority.ALWAYS); + PreviewViewer previewViewer = new PreviewViewer(viewModel.getSearchDatabaseContext(), dialogService, preferencesService, stateManager, themeManager, taskExecutor); previewViewer.setLayout(preferencesService.getPreviewPreferences().getSelectedPreviewLayout()); diff --git a/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java b/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java index 967c397b799..21b0199ff5d 100644 --- a/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java +++ b/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java @@ -3,9 +3,11 @@ import java.util.List; import java.util.Optional; +import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; @@ -23,29 +25,37 @@ public class SearchResultsTableDataModel { - private final FilteredList entriesFiltered; private final SortedList entriesSorted; private final ObjectProperty fieldValueFormatter; private final NameDisplayPreferences nameDisplayPreferences; private final BibDatabaseContext bibDatabaseContext; + private final StateManager stateManager; public SearchResultsTableDataModel(BibDatabaseContext bibDatabaseContext, PreferencesService preferencesService, StateManager stateManager) { this.nameDisplayPreferences = preferencesService.getNameDisplayPreferences(); this.bibDatabaseContext = bibDatabaseContext; + this.stateManager = stateManager; this.fieldValueFormatter = new SimpleObjectProperty<>(new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext)); ObservableList entriesViewModel = FXCollections.observableArrayList(); + populateEntriesViewModel(entriesViewModel); + stateManager.getOpenDatabases().addListener((ListChangeListener) change -> populateEntriesViewModel(entriesViewModel)); + + FilteredList entriesFiltered = new FilteredList<>(entriesViewModel); + entriesFiltered.predicateProperty().bind(EasyBind.map(stateManager.activeGlobalSearchQueryProperty(), query -> entry -> isMatchedBySearch(query, entry))); + stateManager.getGlobalSearchResultSize().bind(Bindings.size(entriesFiltered)); + + // We need to wrap the list since otherwise sorting in the table does not work + entriesSorted = new SortedList<>(entriesFiltered); + } + + private void populateEntriesViewModel(ObservableList entriesViewModel) { + entriesViewModel.clear(); for (BibDatabaseContext context : stateManager.getOpenDatabases()) { ObservableList entriesForDb = context.getDatabase().getEntries(); List viewModelForDb = EasyBind.mapBacked(entriesForDb, entry -> new BibEntryTableViewModel(entry, context, fieldValueFormatter)); entriesViewModel.addAll(viewModelForDb); } - - entriesFiltered = new FilteredList<>(entriesViewModel); - entriesFiltered.predicateProperty().bind(EasyBind.map(stateManager.activeSearchQueryProperty(), query -> entry -> isMatchedBySearch(query, entry))); - - // We need to wrap the list since otherwise sorting in the table does not work - entriesSorted = new SortedList<>(entriesFiltered); } private boolean isMatchedBySearch(Optional query, BibEntryTableViewModel entry) { diff --git a/src/main/java/org/jabref/gui/search/SearchType.java b/src/main/java/org/jabref/gui/search/SearchType.java new file mode 100644 index 00000000000..b4309d88ac1 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/SearchType.java @@ -0,0 +1,6 @@ +package org.jabref.gui.search; + +public enum SearchType { + NORMAL_SEARCH, + GLOBAL_SEARCH +} diff --git a/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java b/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java index f9f41a73ab4..1623ee43cf8 100644 --- a/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java @@ -50,6 +50,7 @@ public void onStart(Stage stage) { area.appendText("some example\n text to go here\n across a couple of \n lines...."); StateManager stateManager = mock(StateManager.class); when(stateManager.activeSearchQueryProperty()).thenReturn(OptionalObjectProperty.empty()); + when(stateManager.activeGlobalSearchQueryProperty()).thenReturn(OptionalObjectProperty.empty()); KeyBindingRepository keyBindingRepository = new KeyBindingRepository(Collections.emptyList(), Collections.emptyList()); ImportFormatPreferences importFormatPreferences = mock(ImportFormatPreferences.class, Answers.RETURNS_DEEP_STUBS); when(importFormatPreferences.bibEntryPreferences().getKeywordSeparator()).thenReturn(','); diff --git a/src/test/java/org/jabref/gui/search/GlobalSearchBarTest.java b/src/test/java/org/jabref/gui/search/GlobalSearchBarTest.java index 4a3b9819cb9..48568812f5b 100644 --- a/src/test/java/org/jabref/gui/search/GlobalSearchBarTest.java +++ b/src/test/java/org/jabref/gui/search/GlobalSearchBarTest.java @@ -55,7 +55,8 @@ public void onStart(Stage stage) { stateManager, prefs, mock(CountingUndoManager.class), - mock(DialogService.class) + mock(DialogService.class), + SearchType.NORMAL_SEARCH ); hBox = new HBox(searchBar);