From db9f83cf77b1a5215e729d3c336371b4edef04a4 Mon Sep 17 00:00:00 2001 From: Loay Ghreeb <52158423+LoayGhreeb@users.noreply.github.com> Date: Mon, 5 Aug 2024 00:16:51 +0300 Subject: [PATCH] Search floating mode (#11510) * Search/Groups floating mode * Hide search rank column * Update query listener from global to library-specific * Update selected groups listener from global to library-specific * Move table-row CSS classes to Base.css * Adapt tests * Update JabRef_en.properties * CHANGELOG * Fix jumpToSearchKey * Add shortcut to scroll to the next/prev rank Left, right arrows * Localization * Fix scroll shortcut to handle rank gaps * OpenRewrite * Add temporary MappedBackedList implementation * Use constants for rank values * Update rank colors * Add CustomFilteredList * Improve group switching and search performance - Update matches in the background - Prevent unnecessary search query rechecks when switching groups * OpenRewrite * Fix NPE * Fix NPE * Create a list of observables * refilter the list after updateVisibility * Add onUpdateCallback to the CustomFilteredList * iterate over updated range * Rename onUpdateCallback to onUpdate * Register events to the row * Update matches in the background * Delete MappedBackedList.java * Update matches in the background for global search * Pass properties to MainTableDataModel instead of LibraryTab * Remove search rank column from the preferences * Add SearchRank enum * EnumSet constructor * Remove int value from SearchRank enum * Move SearchRank enum to search package * Remove FILTERING_SEARCH from search flags * Remove KEEP_SEARCH_STRING from search flags * Fix SearchPreferences constructor * Move comment up * Rename SearchRank to MatchCategory * Update src/main/java/org/jabref/gui/util/CustomFilteredList.java Co-authored-by: Oliver Kopp * Update src/main/java/org/jabref/gui/util/CustomFilteredList.java Co-authored-by: Oliver Kopp * Update CustomFilteredList.java * Update PreferencesMigrations.java * Replace CustomFilteredList.java with reflection Co-authored-by: Oliver Kopp * Minor stylistic fixes * Fix reflection Co-authored-by: Oliver Kopp * Correct typo * Fix typo * Fix unit tests * CHANGELOG --------- Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> Co-authored-by: Oliver Kopp --- .github/workflows/deployment-arm64.yml | 4 + .github/workflows/deployment.yml | 4 + CHANGELOG.md | 3 + build.gradle | 10 +- src/main/java/org/jabref/gui/Base.css | 16 +++ src/main/java/org/jabref/gui/LibraryTab.java | 55 +++++--- .../java/org/jabref/gui/StateManager.java | 72 ++-------- .../jabref/gui/entryeditor/EntryEditor.java | 6 +- .../org/jabref/gui/entryeditor/SourceTab.java | 13 +- .../FulltextSearchResultsTab.java | 18 ++- .../jabref/gui/entrytype/EntryTypeView.java | 3 +- .../org/jabref/gui/frame/JabRefFrame.java | 25 ++-- .../jabref/gui/groups/GroupModeViewModel.java | 20 ++- .../org/jabref/gui/groups/GroupTreeView.java | 3 +- .../jabref/gui/groups/GroupTreeViewModel.java | 8 +- .../org/jabref/gui/groups/GroupViewMode.java | 3 +- .../jabref/gui/groups/GroupsPreferences.java | 53 +++++-- .../java/org/jabref/gui/icon/IconTheme.java | 2 + .../org/jabref/gui/keyboard/KeyBinding.java | 2 + .../gui/maintable/BibEntryTableViewModel.java | 57 +++++++- .../org/jabref/gui/maintable/MainTable.java | 104 +++++++++++--- .../gui/maintable/MainTableColumnFactory.java | 30 ++-- .../gui/maintable/MainTableColumnModel.java | 1 + .../gui/maintable/MainTableDataModel.java | 132 ++++++++++++++---- .../maintable/MainTableHeaderContextMenu.java | 6 +- .../PersistenceVisualStateTable.java | 7 +- .../groups/GroupsTabViewModel.java | 17 +-- .../org/jabref/gui/preview/PreviewViewer.java | 5 +- .../jabref/gui/search/GlobalSearchBar.java | 83 +++++------ .../gui/search/GlobalSearchResultDialog.java | 4 +- .../org/jabref/gui/search/MatchCategory.java | 8 ++ .../jabref/gui/search/SearchDisplayMode.java | 31 +--- .../search/SearchResultsTableDataModel.java | 45 +++--- .../gui/sidepane/GroupsSidePaneComponent.java | 42 ++++-- .../gui/sidepane/SidePaneComponent.java | 2 +- .../jabref/gui/util/FilteredListProxy.java | 113 +++++++++++++++ .../gui/util/ViewModelTableRowFactory.java | 24 +++- .../util/ViewModelTreeTableRowFactory.java | 2 +- .../org/jabref/logic/search/SearchQuery.java | 2 +- .../migrations/PreferencesMigrations.java | 3 +- .../model/search/rules/SearchRules.java | 2 +- .../jabref/preferences/JabRefPreferences.java | 36 ++--- .../jabref/preferences/SearchPreferences.java | 63 +++++---- src/main/resources/l10n/JabRef_en.properties | 11 +- .../org/jabref/cli/ArgumentProcessorTest.java | 14 +- .../jabref/gui/entryeditor/SourceTabTest.java | 8 +- .../gui/groups/GroupNodeViewModelTest.java | 3 +- .../gui/groups/GroupTreeViewModelTest.java | 3 +- .../migrations/PreferencesMigrationsTest.java | 2 +- 49 files changed, 773 insertions(+), 407 deletions(-) create mode 100644 src/main/java/org/jabref/gui/search/MatchCategory.java create mode 100644 src/main/java/org/jabref/gui/util/FilteredListProxy.java diff --git a/.github/workflows/deployment-arm64.yml b/.github/workflows/deployment-arm64.yml index 05bed52fe2c..eca11cf1b66 100644 --- a/.github/workflows/deployment-arm64.yml +++ b/.github/workflows/deployment-arm64.yml @@ -141,6 +141,8 @@ jobs: --java-options --add-opens=javafx.graphics/javafx.scene=org.jabref \ --java-options --add-opens=javafx.controls/javafx.scene.control=org.jabref \ --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections.transformation=org.jabref \ --java-options --add-modules=jdk.incubator.vector - name: Build pkg (macOS) if: (steps.checksecrets.outputs.secretspresent == 'YES') @@ -179,6 +181,8 @@ jobs: --java-options --add-opens=javafx.graphics/javafx.scene=org.jabref \ --java-options --add-opens=javafx.controls/javafx.scene.control=org.jabref \ --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections.transformation=org.jabref \ --java-options --add-modules=jdk.incubator.vector - name: Rename files with arm64 suffix as well if: (steps.checksecrets.outputs.secretspresent == 'YES') diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 0c7f4a9968d..6aa9ca9bfdf 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -153,6 +153,8 @@ jobs: --java-options --add-opens=javafx.graphics/javafx.scene=org.jabref \ --java-options --add-opens=javafx.controls/javafx.scene.control=org.jabref \ --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections.transformation=org.jabref \ --java-options --add-modules=jdk.incubator.vector - name: Build pkg (macOS) if: (matrix.os == 'macos-13') && (steps.checksecrets.outputs.secretspresent == 'YES') @@ -192,6 +194,8 @@ jobs: --java-options --add-opens=javafx.graphics/javafx.scene=org.jabref \ --java-options --add-opens=javafx.controls/javafx.scene.control=org.jabref \ --java-options --add-opens=javafx.controls/com.sun.javafx.scene.control=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections=org.jabref \ + --java-options --add-opens=javafx.base/javafx.collections.transformation=org.jabref \ --java-options --add-modules=jdk.incubator.vector - name: Build runtime image and installer (linux, Windows) if: (matrix.os != 'macos-13') && (steps.checksecrets.outputs.secretspresent == 'YES') diff --git a/CHANGELOG.md b/CHANGELOG.md index aaa8590f40f..f24776931cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - The dialog for [adding an entry using reference text](https://docs.jabref.org/collect/newentryfromplaintext) is now filled with the clipboard contents as default. [#11565](https://github.com/JabRef/jabref/pull/11565) - Added minimal support for [biblatex data annotation](https://mirrors.ctan.org/macros/latex/contrib/biblatex/doc/biblatex.pdf#subsection.3.7) fields in `.layout` files. [#11505](https://github.com/JabRef/jabref/issues/11505) - Added saving of selected options in the [Lookup -> Search for unlinked local files dialog](https://docs.jabref.org/collect/findunlinkedfiles#link-the-pdfs-to-your-bib-library). [#11439](https://github.com/JabRef/jabref/issues/11439) +- We added a toggle button to invert the selected groups. [#9073](https://github.com/JabRef/jabref/issues/9073) +- We reintroduced the floating search in the main table. [#4237](https://github.com/JabRef/jabref/issues/4237) +- We fixed an issue where the selection of an entry in the table lost after searching for a group. [#3176](https://github.com/JabRef/jabref/issues/3176) ### Changed diff --git a/build.gradle b/build.gradle index 1b3cb79e7bf..dcdc107f4fa 100644 --- a/build.gradle +++ b/build.gradle @@ -89,7 +89,10 @@ application { '--add-exports=javafx.controls/com.sun.javafx.scene.control=org.jabref', '--add-opens=javafx.graphics/javafx.scene=org.jabref', '--add-opens=javafx.controls/javafx.scene.control=org.jabref', - '--add-opens=javafx.controls/com.sun.javafx.scene.control=org.jabref' + '--add-opens=javafx.controls/com.sun.javafx.scene.control=org.jabref', + + '--add-opens=javafx.base/javafx.collections=org.jabref', + '--add-opens=javafx.base/javafx.collections.transformation=org.jabref' ] } @@ -495,7 +498,10 @@ run { 'org.controlsfx.controls/org.controlsfx.control.textfield' : 'org.jabref', 'javafx.controls/javafx.scene.control.skin' : 'org.controlsfx.controls', - 'javafx.graphics/javafx.scene' : 'org.controlsfx.controls' + 'javafx.graphics/javafx.scene' : 'org.controlsfx.controls', + + 'javafx.base/javafx.collections' : 'org.jabref', + 'javafx.base/javafx.collections.transformation' : 'org.jabref' ] addModules = [ diff --git a/src/main/java/org/jabref/gui/Base.css b/src/main/java/org/jabref/gui/Base.css index fc477ba1b31..409b582b5fb 100644 --- a/src/main/java/org/jabref/gui/Base.css +++ b/src/main/java/org/jabref/gui/Base.css @@ -648,6 +648,22 @@ TextFlow > .tooltip-text-monospaced { -fx-padding: 0 .5 0 .5; } +.table-row-cell:matching-search-and-groups { + -fx-background-color: white; +} + +.table-row-cell:matching-search-not-groups { + -fx-background-color: rgba(180, 180, 180, 0.86); +} + +.table-row-cell:matching-groups-not-search { + -fx-background-color: rgba(140, 140, 140, 0.86); +} + +.table-row-cell:not-matching-search-and-groups { + -fx-opacity: 60%; +} + .table-row-cell:hover, .tree-table-row-cell:hover { -fx-background-color: -jr-hover; diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index bd16389dd9c..76fe7b0395b 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -16,7 +16,11 @@ import javafx.animation.PauseTransition; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ListProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleListProperty; import javafx.beans.value.ObservableBooleanValue; import javafx.collections.ListChangeListener; import javafx.event.Event; @@ -58,6 +62,7 @@ import org.jabref.gui.undo.UndoableInsertEntries; import org.jabref.gui.undo.UndoableRemoveEntries; import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.logic.citationstyle.CitationStyleCache; @@ -88,6 +93,7 @@ import org.jabref.model.entry.field.Field; import org.jabref.model.entry.field.FieldFactory; import org.jabref.model.entry.field.StandardField; +import org.jabref.model.groups.GroupTreeNode; import org.jabref.model.util.DirectoryMonitor; import org.jabref.model.util.DirectoryMonitorManager; import org.jabref.model.util.FileUpdateMonitor; @@ -149,8 +155,9 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR } @SuppressWarnings({"FieldCanBeLocal"}) private Subscription dividerPositionSubscription; - // the query the user searches when this BasePanel is active - private Optional currentSearchQuery = Optional.empty(); + private ListProperty selectedGroupsProperty; + private final OptionalObjectProperty searchQueryProperty = OptionalObjectProperty.empty(); + private final IntegerProperty resultSize = new SimpleIntegerProperty(0); private Optional changeMonitor = Optional.empty(); @@ -162,15 +169,15 @@ private enum PanelMode { MAIN_TABLE, MAIN_TABLE_AND_ENTRY_EDITOR } private final DirectoryMonitorManager directoryMonitorManager; private LibraryTab(BibDatabaseContext bibDatabaseContext, - LibraryTabContainer tabContainer, - DialogService dialogService, - PreferencesService preferencesService, - StateManager stateManager, - FileUpdateMonitor fileUpdateMonitor, - BibEntryTypesManager entryTypesManager, - CountingUndoManager undoManager, - ClipBoardManager clipBoardManager, - TaskExecutor taskExecutor) { + LibraryTabContainer tabContainer, + DialogService dialogService, + PreferencesService preferencesService, + StateManager stateManager, + FileUpdateMonitor fileUpdateMonitor, + BibEntryTypesManager entryTypesManager, + CountingUndoManager undoManager, + ClipBoardManager clipBoardManager, + TaskExecutor taskExecutor) { this.tabContainer = Objects.requireNonNull(tabContainer); this.bibDatabaseContext = Objects.requireNonNull(bibDatabaseContext); this.undoManager = undoManager; @@ -187,7 +194,8 @@ private LibraryTab(BibDatabaseContext bibDatabaseContext, bibDatabaseContext.getDatabase().registerListener(this); bibDatabaseContext.getMetaData().registerListener(this); - this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferencesService, stateManager); + this.selectedGroupsProperty = new SimpleListProperty<>(stateManager.getSelectedGroups(bibDatabaseContext)); + this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferencesService, taskExecutor, selectedGroupsProperty(), searchQueryProperty(), resultSizeProperty()); citationStyleCache = new CitationStyleCache(bibDatabaseContext); annotationCache = new FileAnnotationCache(bibDatabaseContext, preferencesService.getFilePreferences()); @@ -318,7 +326,10 @@ private void setDatabaseContext(BibDatabaseContext bibDatabaseContext) { bibDatabaseContext.getDatabase().registerListener(this); bibDatabaseContext.getMetaData().registerListener(this); - this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferencesService, stateManager); + this.tableModel.unbind(); + this.selectedGroupsProperty = new SimpleListProperty<>(stateManager.getSelectedGroups(bibDatabaseContext)); + this.tableModel = new MainTableDataModel(getBibDatabaseContext(), preferencesService, taskExecutor, selectedGroupsProperty(), searchQueryProperty(), resultSizeProperty()); + citationStyleCache = new CitationStyleCache(bibDatabaseContext); annotationCache = new FileAnnotationCache(bibDatabaseContext, preferencesService.getFilePreferences()); @@ -875,6 +886,9 @@ private void onClosed(Event event) { LOGGER.error("Problem when shutting down backup manager", e); } + if (tableModel != null) { + tableModel.unbind(); + } // clean up the groups map stateManager.clearSelectedGroups(bibDatabaseContext); } @@ -916,19 +930,16 @@ public MainTable getMainTable() { return mainTable; } - public Optional getCurrentSearchQuery() { - return currentSearchQuery; + public ListProperty selectedGroupsProperty() { + return selectedGroupsProperty; } - /** - * Set the query the user currently searches while this basepanel is active - */ - public void setCurrentSearchQuery(Optional currentSearchQuery) { - this.currentSearchQuery = currentSearchQuery; + public OptionalObjectProperty searchQueryProperty() { + return searchQueryProperty; } - public CitationStyleCache getCitationStyleCache() { - return citationStyleCache; + public IntegerProperty resultSizeProperty() { + return resultSize; } public FileAnnotationCache getAnnotationCache() { diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index 792a6ba01b3..b2df5fe0251 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -6,11 +6,8 @@ import java.util.Optional; import javafx.beans.Observable; -import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.ReadOnlyListProperty; -import javafx.beans.property.ReadOnlyListWrapper; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.collections.FXCollections; @@ -22,6 +19,7 @@ import javafx.util.Pair; import org.jabref.gui.edit.automaticfiededitor.LastAutomaticFieldEditorEdit; +import org.jabref.gui.search.SearchType; import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.CustomLocalDragboard; @@ -56,13 +54,12 @@ public class StateManager { private final ObservableList openDatabases = FXCollections.observableArrayList(); private final OptionalObjectProperty activeDatabase = OptionalObjectProperty.empty(); private final OptionalObjectProperty activeTab = OptionalObjectProperty.empty(); - private final ReadOnlyListWrapper activeGroups = new ReadOnlyListWrapper<>(FXCollections.observableArrayList()); 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 searchResultSize = new SimpleIntegerProperty(0); 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()}); private final EasyBinding anyTaskRunning = EasyBind.reduce(backgroundTasks, tasks -> tasks.map(Pair::getValue).anyMatch(Task::isRunning)); @@ -70,15 +67,9 @@ public class StateManager { private final EasyBinding tasksProgress = EasyBind.reduce(backgroundTasks, tasks -> tasks.map(Pair::getValue).filter(Task::isRunning).mapToDouble(Task::getProgress).average().orElse(1)); private final ObservableMap dialogWindowStates = FXCollections.observableHashMap(); private final ObservableList visibleSidePanes = FXCollections.observableArrayList(); - private final ObjectProperty lastAutomaticFieldEditorEdit = new SimpleObjectProperty<>(); - private final ObservableList searchHistory = FXCollections.observableArrayList(); - public StateManager() { - activeGroups.bind(Bindings.valueAt(selectedGroups, activeDatabase.orElseOpt(null).map(BibDatabaseContext::getUid))); - } - public ObservableList getVisibleSidePaneComponents() { return visibleSidePanes; } @@ -99,36 +90,12 @@ public OptionalObjectProperty activeTabProperty() { return activeTab; } - public OptionalObjectProperty activeSearchQueryProperty() { - return activeSearchQuery; - } - - public void setActiveSearchResultSize(BibDatabaseContext database, IntegerProperty resultSize) { - searchResultMap.put(database.getUid(), resultSize); - } - - public IntegerProperty getSearchResultSize() { - return searchResultMap.getOrDefault(activeDatabase.getValue().orElse(new BibDatabaseContext()).getUid(), new SimpleIntegerProperty(0)); - } - - public OptionalObjectProperty activeGlobalSearchQueryProperty() { - return activeGlobalSearchQuery; - } - - public IntegerProperty getGlobalSearchResultSize() { - return globalSearchResultSize; + public OptionalObjectProperty activeSearchQuery(SearchType type) { + return type == SearchType.NORMAL_SEARCH ? activeSearchQuery : activeGlobalSearchQuery; } - public IntegerProperty getSearchResultSize(OptionalObjectProperty searchQueryProperty) { - if (searchQueryProperty.equals(activeSearchQuery)) { - return getSearchResultSize(); - } else { - return getGlobalSearchResultSize(); - } - } - - public ReadOnlyListProperty activeGroupProperty() { - return activeGroups.getReadOnlyProperty(); + public IntegerProperty searchResultSize(SearchType type) { + return type == SearchType.NORMAL_SEARCH ? searchResultSize : globalSearchResultSize; } public ObservableList getSelectedEntries() { @@ -139,18 +106,17 @@ public void setSelectedEntries(List newSelectedEntries) { selectedEntries.setAll(newSelectedEntries); } - public void setSelectedGroups(BibDatabaseContext database, List newSelectedGroups) { + public void setSelectedGroups(BibDatabaseContext context, List newSelectedGroups) { Objects.requireNonNull(newSelectedGroups); - selectedGroups.put(database.getUid(), FXCollections.observableArrayList(newSelectedGroups)); + selectedGroups.computeIfAbsent(context.getUid(), k -> FXCollections.observableArrayList()).setAll(newSelectedGroups); } public ObservableList getSelectedGroups(BibDatabaseContext context) { - ObservableList selectedGroupsForDatabase = selectedGroups.get(context.getUid()); - return selectedGroupsForDatabase != null ? selectedGroupsForDatabase : FXCollections.observableArrayList(); + return selectedGroups.computeIfAbsent(context.getUid(), k -> FXCollections.observableArrayList()); } - public void clearSelectedGroups(BibDatabaseContext database) { - selectedGroups.remove(database.getUid()); + public void clearSelectedGroups(BibDatabaseContext context) { + selectedGroups.computeIfAbsent(context.getUid(), k -> FXCollections.observableArrayList()).clear(); } public Optional getActiveDatabase() { @@ -166,18 +132,6 @@ public void setActiveDatabase(BibDatabaseContext database) { } } - public void clearSearchQuery() { - activeSearchQuery.setValue(Optional.empty()); - } - - public void setSearchQuery(OptionalObjectProperty searchQueryProperty, SearchQuery query) { - searchQueryProperty.setValue(Optional.of(query)); - } - - public void clearSearchQuery(OptionalObjectProperty searchQueryProperty) { - searchQueryProperty.setValue(Optional.empty()); - } - public OptionalObjectProperty focusOwnerProperty() { return focusOwner; } @@ -219,10 +173,6 @@ public ObjectProperty lastAutomaticFieldEditorEdit return lastAutomaticFieldEditorEdit; } - public LastAutomaticFieldEditorEdit getLastAutomaticFieldEditorEdit() { - return lastAutomaticFieldEditorEditProperty().get(); - } - public void setLastAutomaticFieldEditorEdit(LastAutomaticFieldEditorEdit automaticFieldEditorEdit) { lastAutomaticFieldEditorEditProperty().set(automaticFieldEditorEdit); } diff --git a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java index 5d758895069..5219721ee4b 100644 --- a/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java +++ b/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java @@ -301,12 +301,12 @@ private List createTabs() { preferencesService.getImportFormatPreferences(), fileMonitor, dialogService, - stateManager, bibEntryTypesManager, - keyBindingRepository); + keyBindingRepository, + libraryTab.searchQueryProperty()); tabs.add(sourceTab); tabs.add(new LatexCitationsTab(databaseContext, preferencesService, dialogService, directoryMonitorManager)); - tabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, taskExecutor)); + tabs.add(new FulltextSearchResultsTab(stateManager, preferencesService, dialogService, taskExecutor, libraryTab.searchQueryProperty())); return tabs; } diff --git a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index d8b3a7856f1..a290f3d9422 100644 --- a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -21,7 +21,6 @@ import javafx.scene.input.KeyEvent; import org.jabref.gui.DialogService; -import org.jabref.gui.StateManager; import org.jabref.gui.actions.ActionFactory; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; @@ -32,6 +31,7 @@ import org.jabref.gui.undo.NamedCompound; import org.jabref.gui.undo.UndoableChangeType; import org.jabref.gui.undo.UndoableFieldChange; +import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldPreferences; @@ -104,9 +104,9 @@ public SourceTab(BibDatabaseContext bibDatabaseContext, ImportFormatPreferences importFormatPreferences, FileUpdateMonitor fileMonitor, DialogService dialogService, - StateManager stateManager, BibEntryTypesManager entryTypesManager, - KeyBindingRepository keyBindingRepository) { + KeyBindingRepository keyBindingRepository, + OptionalObjectProperty searchQueryProperty) { this.mode = bibDatabaseContext.getMode(); this.setText(Localization.lang("%0 source", mode.getFormattedName())); this.setTooltip(new Tooltip(Localization.lang("Show/edit %0 source", mode.getFormattedName()))); @@ -119,12 +119,7 @@ public SourceTab(BibDatabaseContext bibDatabaseContext, this.entryTypesManager = entryTypesManager; this.keyBindingRepository = keyBindingRepository; - stateManager.activeSearchQueryProperty().addListener((observable, oldValue, newValue) -> { - searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); - highlightSearchPattern(); - }); - - stateManager.activeGlobalSearchQueryProperty().addListener((observable, oldValue, newValue) -> { + searchQueryProperty.addListener((observable, oldValue, newValue) -> { searchHighlightPattern = newValue.flatMap(SearchQuery::getPatternForWords); highlightSearchPattern(); }); diff --git a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java index 5a15f17f288..b5b306a3a12 100644 --- a/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/fileannotationtab/FulltextSearchResultsTab.java @@ -24,9 +24,11 @@ import org.jabref.gui.entryeditor.EntryEditorTab; import org.jabref.gui.maintable.OpenExternalFileAction; import org.jabref.gui.maintable.OpenFolderAction; +import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.gui.util.TaskExecutor; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.logic.l10n.Localization; +import org.jabref.logic.search.SearchQuery; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.model.pdf.search.PdfSearchResults; @@ -48,18 +50,21 @@ public class FulltextSearchResultsTab extends EntryEditorTab { private final ActionFactory actionFactory; private final TaskExecutor taskExecutor; private final TextFlow content; + private final OptionalObjectProperty searchQueryProperty; private BibEntry entry; private DocumentViewerView documentViewerView; public FulltextSearchResultsTab(StateManager stateManager, PreferencesService preferencesService, DialogService dialogService, - TaskExecutor taskExecutor) { + TaskExecutor taskExecutor, + OptionalObjectProperty searchQueryProperty) { this.stateManager = stateManager; this.preferencesService = preferencesService; this.dialogService = dialogService; this.actionFactory = new ActionFactory(); this.taskExecutor = taskExecutor; + this.searchQueryProperty = searchQueryProperty; content = new TextFlow(); ScrollPane scrollPane = new ScrollPane(content); @@ -67,15 +72,14 @@ public FulltextSearchResultsTab(StateManager stateManager, content.setPadding(new Insets(10)); setContent(scrollPane); setText(Localization.lang("Search results")); - this.stateManager.activeSearchQueryProperty().addListener((observable, oldValue, newValue) -> bindToEntry(entry)); + searchQueryProperty.addListener((observable, oldValue, newValue) -> bindToEntry(entry)); } @Override public boolean shouldShow(BibEntry entry) { - return this.stateManager.activeSearchQueryProperty().isPresent().get() && - this.stateManager.activeSearchQueryProperty().get().isPresent() && - this.stateManager.activeSearchQueryProperty().get().get().getSearchFlags().contains(SearchRules.SearchFlags.FULLTEXT) && - !this.stateManager.activeSearchQueryProperty().get().get().getQuery().isEmpty(); + return searchQueryProperty.get().map(query -> + !query.getQuery().isEmpty() && query.getSearchFlags().contains(SearchRules.SearchFlags.FULLTEXT) + ).orElse(false); } @Override @@ -87,7 +91,7 @@ protected void bindToEntry(BibEntry entry) { documentViewerView = new DocumentViewerView(); } this.entry = entry; - PdfSearchResults searchResults = stateManager.activeSearchQueryProperty().get().get().getRule().getFulltextResults(stateManager.activeSearchQueryProperty().get().get().getQuery(), entry); + PdfSearchResults searchResults = searchQueryProperty.get().get().getRule().getFulltextResults(searchQueryProperty.get().get().getQuery(), entry); content.getChildren().clear(); diff --git a/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java b/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java index 8d8e48dc8fc..88b00c0bee6 100644 --- a/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java +++ b/src/main/java/org/jabref/gui/entrytype/EntryTypeView.java @@ -20,6 +20,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.LibraryTab; import org.jabref.gui.StateManager; +import org.jabref.gui.search.SearchType; import org.jabref.gui.util.BaseDialog; import org.jabref.gui.util.ControlHelper; import org.jabref.gui.util.IconValidationDecorator; @@ -219,7 +220,7 @@ private void focusTextField(Event event) { private void setEntryTypeForReturnAndClose(Optional entryType) { type = entryType.map(BibEntryType::getType).orElse(null); viewModel.stopFetching(); - this.stateManager.clearSearchQuery(); + stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH).set(Optional.empty()); this.close(); } diff --git a/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 8088c0501e2..b42cde59333 100644 --- a/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -326,18 +326,11 @@ private void initBindings() { // the binding for stateManager.activeDatabaseProperty() is at org.jabref.gui.LibraryTab.onDatabaseLoadingSucceed // Subscribe to the search - EasyBind.subscribe(stateManager.activeSearchQueryProperty(), - query -> { - if (prefs.getSearchPreferences().shouldKeepSearchString()) { - for (LibraryTab tab : getLibraryTabs()) { - tab.setCurrentSearchQuery(query); - } - } else { - if (getCurrentLibraryTab() != null) { - getCurrentLibraryTab().setCurrentSearchQuery(query); - } - } - }); + EasyBind.subscribe(stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH), query -> { + if (getCurrentLibraryTab() != null) { + getCurrentLibraryTab().searchQueryProperty().set(query); + } + }); // Wait for the scene to be created, otherwise focusOwnerProperty is not provided Platform.runLater(() -> stateManager.focusOwnerProperty().bind( @@ -371,12 +364,12 @@ private void initBindings() { stateManager.setSelectedEntries(libraryTab.getSelectedEntries()); // Update active search query when switching between databases - if (prefs.getSearchPreferences().shouldKeepSearchString() && libraryTab.getCurrentSearchQuery().isEmpty() && stateManager.activeSearchQueryProperty().get().isPresent()) { - // apply search query also when opening a new library and keep search string is activated - libraryTab.setCurrentSearchQuery(stateManager.activeSearchQueryProperty().get()); + if (prefs.getSearchPreferences().shouldKeepSearchString()) { + libraryTab.searchQueryProperty().set(stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH).get()); } else { - stateManager.activeSearchQueryProperty().set(libraryTab.getCurrentSearchQuery()); + stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH).set(libraryTab.searchQueryProperty().get()); } + stateManager.searchResultSize(SearchType.NORMAL_SEARCH).bind(libraryTab.resultSizeProperty()); // Update search autocompleter with information for the correct database: globalSearchBar.setAutoCompleter(libraryTab.getAutoCompleter()); diff --git a/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java index 022941a97d5..6813de6227e 100644 --- a/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupModeViewModel.java @@ -1,5 +1,7 @@ package org.jabref.gui.groups; +import java.util.Set; + import javafx.scene.Node; import javafx.scene.control.Tooltip; @@ -8,29 +10,25 @@ public class GroupModeViewModel { - private final GroupViewMode mode; + private final Set mode; - public GroupModeViewModel(GroupViewMode mode) { + public GroupModeViewModel(Set mode) { this.mode = mode; } public Node getUnionIntersectionGraphic() { - if (mode == GroupViewMode.UNION) { - return JabRefIcons.GROUP_UNION.getGraphicNode(); - } else if (mode == GroupViewMode.INTERSECTION) { + if (mode.contains(GroupViewMode.INTERSECTION)) { return JabRefIcons.GROUP_INTERSECTION.getGraphicNode(); + } else { + return JabRefIcons.GROUP_UNION.getGraphicNode(); } - - // As there is no concept like an empty node/icon, we return simply the other icon - return JabRefIcons.GROUP_INTERSECTION.getGraphicNode(); } public Tooltip getUnionIntersectionTooltip() { - if (mode == GroupViewMode.UNION) { + if (!mode.contains(GroupViewMode.INTERSECTION)) { return new Tooltip(Localization.lang("Toggle intersection")); - } else if (mode == GroupViewMode.INTERSECTION) { + } else { return new Tooltip(Localization.lang("Toggle union")); } - return new Tooltip(); } } diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeView.java b/src/main/java/org/jabref/gui/groups/GroupTreeView.java index ef837d2586e..ef0084fe4f7 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeView.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeView.java @@ -60,7 +60,6 @@ import org.jabref.gui.util.ViewModelTreeTableRowFactory; import org.jabref.logic.l10n.Localization; import org.jabref.model.entry.BibEntry; -import org.jabref.model.groups.AllEntriesGroup; import org.jabref.preferences.PreferencesService; import com.tobiasdiez.easybind.EasyBind; @@ -398,7 +397,7 @@ private void updateSelection(List> newSelectedGroup if ((newSelectedGroups == null) || newSelectedGroups.isEmpty()) { viewModel.selectedGroupsProperty().clear(); } else { - List list = newSelectedGroups.stream().filter(model -> (model != null) && !(model.getValue().getGroupNode().getGroup() instanceof AllEntriesGroup)).map(TreeItem::getValue).collect(Collectors.toList()); + List list = newSelectedGroups.stream().filter(model -> (model != null) && (model.getValue() != null)).map(TreeItem::getValue).collect(Collectors.toList()); viewModel.selectedGroupsProperty().setAll(list); } } diff --git a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java index ae7dcc241fd..5593b7052e5 100644 --- a/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java +++ b/src/main/java/org/jabref/gui/groups/GroupTreeViewModel.java @@ -85,9 +85,6 @@ public GroupTreeViewModel(StateManager stateManager, DialogService dialogService // Set-up bindings filterPredicate.bind(EasyBind.map(filterText, text -> group -> group.isMatchedBy(text))); - - // Init - refresh(); } private void refresh() { @@ -278,11 +275,8 @@ public void editGroup(GroupNodeViewModel oldGroup) { // we need to check the old name for duplicates. If the new group name occurs more than once, it won't matter groupsWithSameName = databaseRootGroup.get().findChildrenSatisfying(g -> g.getName().equals(oldGroupName)).size(); } - boolean removePreviousAssignments = true; // We found more than 2 groups, so we cannot simply remove old assignment - if (groupsWithSameName >= 2) { - removePreviousAssignments = false; - } + boolean removePreviousAssignments = groupsWithSameName < 2; oldGroup.getGroupNode().setGroup( group, diff --git a/src/main/java/org/jabref/gui/groups/GroupViewMode.java b/src/main/java/org/jabref/gui/groups/GroupViewMode.java index 564c85496e6..23784615fa3 100644 --- a/src/main/java/org/jabref/gui/groups/GroupViewMode.java +++ b/src/main/java/org/jabref/gui/groups/GroupViewMode.java @@ -1,4 +1,3 @@ package org.jabref.gui.groups; -public enum GroupViewMode { INTERSECTION, UNION } - +public enum GroupViewMode { INTERSECTION, FILTER, INVERT } diff --git a/src/main/java/org/jabref/gui/groups/GroupsPreferences.java b/src/main/java/org/jabref/gui/groups/GroupsPreferences.java index e83d5546daa..9c71262b4de 100644 --- a/src/main/java/org/jabref/gui/groups/GroupsPreferences.java +++ b/src/main/java/org/jabref/gui/groups/GroupsPreferences.java @@ -1,40 +1,77 @@ package org.jabref.gui.groups; +import java.util.EnumSet; + import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SetProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleSetProperty; +import javafx.collections.FXCollections; import org.jabref.model.groups.GroupHierarchyType; +import com.google.common.annotations.VisibleForTesting; + public class GroupsPreferences { - private final ObjectProperty groupViewMode; + private final SetProperty groupViewMode; private final BooleanProperty shouldAutoAssignGroup; private final BooleanProperty shouldDisplayGroupCount; private final ObjectProperty defaultHierarchicalContext; - public GroupsPreferences(GroupViewMode groupViewMode, + public GroupsPreferences(boolean viewModeIntersection, + boolean viewModeFilter, + boolean viewModeInvert, boolean shouldAutoAssignGroup, boolean shouldDisplayGroupCount, GroupHierarchyType defaultHierarchicalContext) { - this.groupViewMode = new SimpleObjectProperty<>(groupViewMode); + this.groupViewMode = new SimpleSetProperty<>(FXCollections.observableSet()); + this.shouldAutoAssignGroup = new SimpleBooleanProperty(shouldAutoAssignGroup); + this.shouldDisplayGroupCount = new SimpleBooleanProperty(shouldDisplayGroupCount); + this.defaultHierarchicalContext = new SimpleObjectProperty<>(defaultHierarchicalContext); + + if (viewModeIntersection) { + this.groupViewMode.add(GroupViewMode.INTERSECTION); + } + if (viewModeFilter) { + this.groupViewMode.add(GroupViewMode.FILTER); + } + if (viewModeInvert) { + this.groupViewMode.add(GroupViewMode.INVERT); + } + } + + @VisibleForTesting + public GroupsPreferences(EnumSet groupViewMode, + boolean shouldAutoAssignGroup, + boolean shouldDisplayGroupCount, + GroupHierarchyType defaultHierarchicalContext) { + this.groupViewMode = new SimpleSetProperty<>(FXCollections.observableSet(groupViewMode)); this.shouldAutoAssignGroup = new SimpleBooleanProperty(shouldAutoAssignGroup); this.shouldDisplayGroupCount = new SimpleBooleanProperty(shouldDisplayGroupCount); this.defaultHierarchicalContext = new SimpleObjectProperty<>(defaultHierarchicalContext); } - public GroupViewMode getGroupViewMode() { - return groupViewMode.getValue(); + public EnumSet getGroupViewMode() { + if (groupViewMode.isEmpty()) { + return EnumSet.noneOf(GroupViewMode.class); + } + return EnumSet.copyOf(groupViewMode); } - public ObjectProperty groupViewModeProperty() { + public SetProperty groupViewModeProperty() { return groupViewMode; } - public void setGroupViewMode(GroupViewMode groupViewMode) { - this.groupViewMode.set(groupViewMode); + public void setGroupViewMode(GroupViewMode mode, boolean value) { + if (value) { + groupViewMode.add(mode); + } else { + groupViewMode.remove(mode); + } } public boolean shouldAutoAssignGroup() { diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java index 79a812f0b18..566c62c1f32 100644 --- a/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -293,6 +293,8 @@ public enum JabRefIcons implements JabRefIcon { CASE_SENSITIVE(MaterialDesignA.ALPHABETICAL), REG_EX(MaterialDesignR.REGEX), FULLTEXT(MaterialDesignF.FILE_EYE), + FILTER(MaterialDesignF.FILTER), + INVERT(MaterialDesignI.INVERT_COLORS), CONSOLE(MaterialDesignC.CONSOLE), FORUM(MaterialDesignF.FORUM), FACEBOOK(MaterialDesignF.FACEBOOK), diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBinding.java b/src/main/java/org/jabref/gui/keyboard/KeyBinding.java index 40bbc0ca7e0..d0cb6100338 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBinding.java +++ b/src/main/java/org/jabref/gui/keyboard/KeyBinding.java @@ -93,6 +93,8 @@ public enum KeyBinding { PULL_CHANGES_FROM_SHARED_DATABASE("Pull changes from shared database", Localization.lang("Pull changes from shared database"), "ctrl+shift+R", KeyBindingCategory.FILE), PREVIOUS_PREVIEW_LAYOUT("Previous preview layout", Localization.lang("Previous preview layout"), "shift+F9", KeyBindingCategory.VIEW), PREVIOUS_LIBRARY("Previous library", Localization.lang("Previous library"), "ctrl+PAGE_UP", KeyBindingCategory.VIEW), + SCROLL_TO_NEXT_MATCH_CATEGORY("Scroll to next match category", Localization.lang("Scroll to next match category"), "right", KeyBindingCategory.VIEW), + SCROLL_TO_PREVIOUS_MATCH_CATEGORY("Scroll to previous match category", Localization.lang("Scroll to previous match category"), "left", KeyBindingCategory.VIEW), PUSH_TO_APPLICATION("Push to application", Localization.lang("Push to application"), "ctrl+L", KeyBindingCategory.TOOLS), QUIT_JABREF("Quit JabRef", Localization.lang("Quit JabRef"), "ctrl+Q", KeyBindingCategory.FILE), REDO("Redo", Localization.lang("Redo"), "ctrl+Y", KeyBindingCategory.EDIT), diff --git a/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java b/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java index 0954107786b..1273ae58294 100644 --- a/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java +++ b/src/main/java/org/jabref/gui/maintable/BibEntryTableViewModel.java @@ -13,10 +13,15 @@ import javafx.beans.Observable; import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyStringWrapper; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; +import org.jabref.gui.search.MatchCategory; import org.jabref.gui.specialfields.SpecialFieldValueViewModel; import org.jabref.gui.util.uithreadaware.UiThreadBinding; import org.jabref.logic.importer.util.FileFieldParser; @@ -35,7 +40,6 @@ import com.tobiasdiez.easybind.optional.OptionalBinding; public class BibEntryTableViewModel { - private final BibEntry entry; private final ObservableValue fieldValueFormatter; private final Map> fieldValues = new HashMap<>(); @@ -44,6 +48,11 @@ public class BibEntryTableViewModel { private final EasyBinding> linkedIdentifiers; private final Binding> matchedGroups; private final BibDatabaseContext bibDatabaseContext; + private final BooleanProperty isMatchedBySearch = new SimpleBooleanProperty(true); + private final BooleanProperty isVisibleBySearch = new SimpleBooleanProperty(true); + private final BooleanProperty isMatchedByGroup = new SimpleBooleanProperty(true); + private final BooleanProperty isVisibleByGroup = new SimpleBooleanProperty(true); + private final ObjectProperty matchCategory = new SimpleObjectProperty<>(MatchCategory.MATCHING_SEARCH_AND_GROUPS); public BibEntryTableViewModel(BibEntry entry, BibDatabaseContext bibDatabaseContext, ObservableValue fieldValueFormatter) { this.entry = entry; @@ -81,10 +90,10 @@ private static Binding> createMatchedGroupsBinding(BibDataba return new UiThreadBinding<>(EasyBind.combine(entry.getFieldBinding(StandardField.GROUPS), database.getMetaData().groupsBinding(), (a, b) -> database.getMetaData().getGroups().map(groupTreeNode -> - groupTreeNode.getMatchingGroups(entry).stream() - .map(GroupTreeNode::getGroup) - .filter(Predicate.not(Predicate.isEqual(groupTreeNode.getGroup()))) - .collect(Collectors.toList())) + groupTreeNode.getMatchingGroups(entry).stream() + .map(GroupTreeNode::getGroup) + .filter(Predicate.not(Predicate.isEqual(groupTreeNode.getGroup()))) + .collect(Collectors.toList())) .orElse(Collections.emptyList()))); } @@ -149,4 +158,42 @@ public StringProperty bibDatabasePathProperty() { public BibDatabaseContext getBibDatabaseContext() { return bibDatabaseContext; } + + public BooleanProperty isMatchedBySearch() { + return isMatchedBySearch; + } + + public BooleanProperty isVisibleBySearch() { + return isVisibleBySearch; + } + + public BooleanProperty isMatchedByGroup() { + return isMatchedByGroup; + } + + public BooleanProperty isVisibleByGroup() { + return isVisibleByGroup; + } + + public ObjectProperty matchCategory() { + return matchCategory; + } + + public boolean isVisible() { + return isVisibleBySearch.get() && isVisibleByGroup.get(); + } + + public void updateMatchCategory() { + MatchCategory category = MatchCategory.NOT_MATCHING_SEARCH_AND_GROUPS; + + if (isMatchedBySearch.get() && isMatchedByGroup.get()) { + category = MatchCategory.MATCHING_SEARCH_AND_GROUPS; + } else if (isMatchedBySearch.get()) { + category = MatchCategory.MATCHING_SEARCH_NOT_GROUPS; + } else if (isMatchedByGroup.get()) { + category = MatchCategory.MATCHING_GROUPS_NOT_SEARCH; + } + + matchCategory.set(category); + } } diff --git a/src/main/java/org/jabref/gui/maintable/MainTable.java b/src/main/java/org/jabref/gui/maintable/MainTable.java index 16b51d631cc..d334aec7913 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -11,6 +11,7 @@ import javax.swing.undo.UndoManager; import javafx.collections.ListChangeListener; +import javafx.css.PseudoClass; import javafx.scene.control.SelectionMode; import javafx.scene.control.TableColumn; import javafx.scene.control.TableRow; @@ -38,6 +39,7 @@ import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.maintable.columns.LibraryColumn; import org.jabref.gui.maintable.columns.MainTableColumn; +import org.jabref.gui.search.MatchCategory; import org.jabref.gui.util.ControlHelper; import org.jabref.gui.util.CustomLocalDragboard; import org.jabref.gui.util.TaskExecutor; @@ -65,6 +67,10 @@ public class MainTable extends TableView { private static final Logger LOGGER = LoggerFactory.getLogger(MainTable.class); + private static final PseudoClass MATCHING_SEARCH_AND_GROUPS = PseudoClass.getPseudoClass("matching-search-and-groups"); + private static final PseudoClass MATCHING_SEARCH_NOT_GROUPS = PseudoClass.getPseudoClass("matching-search-not-groups"); + private static final PseudoClass MATCHING_GROUPS_NOT_SEARCH = PseudoClass.getPseudoClass("matching-groups-not-search"); + private static final PseudoClass NOT_MATCHING_SEARCH_AND_GROUPS = PseudoClass.getPseudoClass("not-matching-search-and-groups"); private final LibraryTab libraryTab; private final DialogService dialogService; @@ -120,16 +126,16 @@ public MainTable(MainTableDataModel model, this.setOnDragOver(this::handleOnDragOverTableView); this.setOnDragDropped(this::handleOnDragDroppedTableView); - this.getColumns().addAll( - new MainTableColumnFactory( - database, - preferencesService, - preferencesService.getMainTableColumnPreferences(), - undoManager, - dialogService, - stateManager, - taskExecutor).createColumns()); + MainTableColumnFactory mainTableColumnFactory = new MainTableColumnFactory( + database, + preferencesService, + preferencesService.getMainTableColumnPreferences(), + undoManager, + dialogService, + stateManager, + taskExecutor); + this.getColumns().addAll(mainTableColumnFactory.createColumns()); this.getColumns().removeIf(LibraryColumn.class::isInstance); new ViewModelTableRowFactory() @@ -149,6 +155,10 @@ public MainTable(MainTableDataModel model, taskExecutor, Injector.instantiateModelOrService(JournalAbbreviationRepository.class), entryTypesManager)) + .withPseudoClass(MATCHING_SEARCH_AND_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_AND_GROUPS)) + .withPseudoClass(MATCHING_SEARCH_NOT_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_NOT_GROUPS)) + .withPseudoClass(MATCHING_GROUPS_NOT_SEARCH, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_GROUPS_NOT_SEARCH)) + .withPseudoClass(NOT_MATCHING_SEARCH_AND_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.NOT_MATCHING_SEARCH_AND_GROUPS)) .setOnDragDetected(this::handleOnDragDetected) .setOnDragDropped(this::handleOnDragDropped) .setOnDragOver(this::handleOnDragOver) @@ -158,6 +168,14 @@ public MainTable(MainTableDataModel model, this.getSortOrder().clear(); + // force match category column to be the first sort order, (match_category column is always the first column) + this.getSortOrder().addFirst(getColumns().getFirst()); + this.getSortOrder().addListener((ListChangeListener>) change -> { + if (!this.getSortOrder().getFirst().equals(getColumns().getFirst())) { + this.getSortOrder().addFirst(getColumns().getFirst()); + } + }); + mainTablePreferences.getColumnPreferences().getColumnSortOrder().forEach(columnModel -> this.getColumns().stream() .map(column -> (MainTableColumn) column) @@ -187,25 +205,17 @@ public MainTable(MainTableDataModel model, setupKeyBindings(keyBindingRepository); this.setOnKeyTyped(key -> { - if (this.getSortOrder().isEmpty()) { + if (this.getSortOrder().size() <= 1) { return; } - this.jumpToSearchKey(getSortOrder().getFirst(), key); + // skip match category column + this.jumpToSearchKey(getSortOrder().get(1), key); }); database.getDatabase().registerListener(this); - MainTableColumnFactory rightClickMenuFactory = new MainTableColumnFactory( - database, - preferencesService, - preferencesService.getMainTableColumnPreferences(), - undoManager, - dialogService, - stateManager, - taskExecutor); - // Enable the header right-click menu. - new MainTableHeaderContextMenu(this, rightClickMenuFactory, tabContainer, dialogService).show(true); + new MainTableHeaderContextMenu(this, mainTableColumnFactory, tabContainer, dialogService).show(true); } /** @@ -239,8 +249,9 @@ private void jumpToSearchKey(TableColumn sortedColumn .startsWith(columnSearchTerm)) .findFirst() .ifPresent(item -> { - this.scrollTo(item); - this.clearAndSelect(item.getEntry()); + getSelectionModel().clearSelection(); + getSelectionModel().select(item); + scrollTo(item); }); } @@ -280,6 +291,45 @@ public void cut() { libraryTab.delete(StandardActions.CUT); } + private void scrollToNextMatchCategory() { + BibEntryTableViewModel selectedEntry = getSelectionModel().getSelectedItem(); + if (selectedEntry == null) { + return; + } + + MatchCategory currentMatchCategory = selectedEntry.matchCategory().get(); + for (int i = getSelectionModel().getSelectedIndex(); i < getItems().size(); i++) { + if (!getItems().get(i).matchCategory().get().equals(currentMatchCategory)) { + getSelectionModel().clearSelection(); + getSelectionModel().select(i); + scrollTo(i); + return; + } + } + } + + private void scrollToPreviousMatchCategory() { + BibEntryTableViewModel selectedEntry = getSelectionModel().getSelectedItem(); + if (selectedEntry == null) { + return; + } + + MatchCategory currentMatchCategory = selectedEntry.matchCategory().get(); + for (int i = getSelectionModel().getSelectedIndex(); i >= 0; i--) { + if (!getItems().get(i).matchCategory().get().equals(currentMatchCategory)) { + MatchCategory targetMatchCategory = getItems().get(i).matchCategory().get(); + // found the previous category, scroll to the first entry of that category + while ((i >= 0) && getItems().get(i).matchCategory().get().equals(targetMatchCategory)) { + i--; + } + getSelectionModel().clearSelection(); + getSelectionModel().select(i + 1); + scrollTo(i + 1); + return; + } + } + } + private void setupKeyBindings(KeyBindingRepository keyBindings) { EditAction pasteAction = new EditAction(StandardActions.PASTE, () -> libraryTab, stateManager, undoManager); EditAction copyAction = new EditAction(StandardActions.COPY, () -> libraryTab, stateManager, undoManager); @@ -322,6 +372,14 @@ private void setupKeyBindings(KeyBindingRepository keyBindings) { deleteAction.execute(); event.consume(); break; + case SCROLL_TO_NEXT_MATCH_CATEGORY: + scrollToNextMatchCategory(); + event.consume(); + break; + case SCROLL_TO_PREVIOUS_MATCH_CATEGORY: + scrollToPreviousMatchCategory(); + event.consume(); + break; default: // Pass other keys to parent } diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java index e0781bace97..39a38d9bee6 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnFactory.java @@ -31,6 +31,7 @@ import org.jabref.gui.maintable.columns.LinkedIdentifierColumn; import org.jabref.gui.maintable.columns.MainTableColumn; import org.jabref.gui.maintable.columns.SpecialFieldColumn; +import org.jabref.gui.search.MatchCategory; import org.jabref.gui.specialfields.SpecialFieldValueViewModel; import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.TaskExecutor; @@ -61,7 +62,6 @@ public class MainTableColumnFactory { private final UndoManager undoManager; private final DialogService dialogService; private final TaskExecutor taskExecutor; - private final ThemeManager themeManager = Injector.instantiateModelOrService(ThemeManager.class); private final StateManager stateManager; private final MainTableTooltip tooltip; @@ -80,8 +80,8 @@ public MainTableColumnFactory(BibDatabaseContext database, this.cellFactory = new CellFactory(preferencesService, undoManager); this.undoManager = undoManager; this.stateManager = stateManager; - this.tooltip = new MainTableTooltip(database, dialogService, preferencesService, stateManager, - themeManager, taskExecutor); + ThemeManager themeManager = Injector.instantiateModelOrService(ThemeManager.class); + this.tooltip = new MainTableTooltip(database, dialogService, preferencesService, stateManager, themeManager, taskExecutor); } public TableColumn createColumn(MainTableColumnModel column) { @@ -134,10 +134,8 @@ public MainTableColumnFactory(BibDatabaseContext database, public List> createColumns() { List> columns = new ArrayList<>(); - columnPreferences.getColumns().forEach(column -> { - columns.add(createColumn(column)); - }); - + columns.add(createMatchCategoryColumn(new MainTableColumnModel(MainTableColumnModel.Type.MATCH_CATEGORY))); + columnPreferences.getColumns().forEach(column -> columns.add(createColumn(column))); return columns; } @@ -147,8 +145,22 @@ public static void setExactWidth(TableColumn column, double width) { column.setMaxWidth(width); } + private TableColumn createMatchCategoryColumn(MainTableColumnModel columnModel) { + TableColumn column = new MainTableColumn<>(columnModel); + Node header = new Text(Localization.lang("Match category")); + header.getStyleClass().add("mainTable-header"); + Tooltip.install(header, new Tooltip(MainTableColumnModel.Type.MATCH_CATEGORY.getDisplayName())); + column.setGraphic(header); + column.setCellValueFactory(cellData -> cellData.getValue().matchCategory()); + new ValueTableCellFactory().withText(String::valueOf).install(column); + column.setSortable(true); + column.setSortType(TableColumn.SortType.ASCENDING); + column.setVisible(false); + return column; + } + /** - * Creates a column with a continous number + * Creates a column with a continuous number */ private TableColumn createIndexColumn(MainTableColumnModel columnModel) { TableColumn column = new MainTableColumn<>(columnModel); @@ -192,7 +204,7 @@ private TableColumn createIndexColumn(MainTableC private TableColumn createGroupIconColumn(MainTableColumnModel columnModel) { TableColumn> column = new MainTableColumn<>(columnModel); Node headerGraphic = IconTheme.JabRefIcons.DEFAULT_GROUP_ICON_COLUMN.getGraphicNode(); - Tooltip.install(headerGraphic, new Tooltip(Localization.lang("Group icons"))); + Tooltip.install(headerGraphic, new Tooltip(MainTableColumnModel.Type.GROUP_ICONS.getDisplayName())); column.setGraphic(headerGraphic); column.getStyleClass().add(STYLE_ICON_COLUMN); column.setResizable(true); diff --git a/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java b/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java index bd007f8f722..90e239c79aa 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableColumnModel.java @@ -35,6 +35,7 @@ public class MainTableColumnModel { private static final Logger LOGGER = LoggerFactory.getLogger(MainTableColumnModel.class); public enum Type { + MATCH_CATEGORY("match_category", Localization.lang("Match category")), INDEX("index", Localization.lang("Index")), EXTRAFILE("extrafile", Localization.lang("File type")), FILES("files", Localization.lang("Linked files")), diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java index f80a1d84baa..a0f8ecb92a0 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java @@ -5,17 +5,23 @@ import javafx.beans.binding.Bindings; import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ListProperty; import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; -import org.jabref.gui.StateManager; import org.jabref.gui.groups.GroupViewMode; import org.jabref.gui.groups.GroupsPreferences; +import org.jabref.gui.search.MatchCategory; +import org.jabref.gui.search.SearchDisplayMode; +import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.BindingsHelper; +import org.jabref.gui.util.FilteredListProxy; +import org.jabref.gui.util.OptionalObjectProperty; +import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.search.SearchQuery; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -23,66 +29,127 @@ import org.jabref.model.search.matchers.MatcherSet; import org.jabref.model.search.matchers.MatcherSets; import org.jabref.preferences.PreferencesService; +import org.jabref.preferences.SearchPreferences; import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.Subscription; public class MainTableDataModel { + private final ObservableList entriesViewModel; private final FilteredList entriesFiltered; private final SortedList entriesFilteredAndSorted; private final ObjectProperty fieldValueFormatter = new SimpleObjectProperty<>(); private final GroupsPreferences groupsPreferences; + private final SearchPreferences searchPreferences; private final NameDisplayPreferences nameDisplayPreferences; private final BibDatabaseContext bibDatabaseContext; - - public MainTableDataModel(BibDatabaseContext context, PreferencesService preferencesService, StateManager stateManager) { + private final TaskExecutor taskExecutor; + private final Subscription searchQuerySubscription; + private final Subscription searchDisplayModeSubscription; + private final Subscription selectedGroupsSubscription; + private final Subscription groupViewModeSubscription; + private Optional groupsMatcher; + + public MainTableDataModel(BibDatabaseContext context, + PreferencesService preferencesService, + TaskExecutor taskExecutor, + ListProperty selectedGroupsProperty, + OptionalObjectProperty searchQueryProperty, + IntegerProperty resultSizeProperty) { this.groupsPreferences = preferencesService.getGroupsPreferences(); + this.searchPreferences = preferencesService.getSearchPreferences(); this.nameDisplayPreferences = preferencesService.getNameDisplayPreferences(); + this.taskExecutor = taskExecutor; this.bibDatabaseContext = context; + this.groupsMatcher = createGroupMatcher(selectedGroupsProperty.get(), groupsPreferences); resetFieldFormatter(); ObservableList allEntries = BindingsHelper.forUI(context.getDatabase().getEntries()); - ObservableList entriesViewModel = EasyBind.mapBacked(allEntries, entry -> - new BibEntryTableViewModel(entry, bibDatabaseContext, fieldValueFormatter)); - - entriesFiltered = new FilteredList<>(entriesViewModel); - entriesFiltered.predicateProperty().bind( - EasyBind.combine(stateManager.activeGroupProperty(), - stateManager.activeSearchQueryProperty(), - groupsPreferences.groupViewModeProperty(), - (groups, query, groupViewMode) -> entry -> isMatched(groups, query, entry)) - ); - - IntegerProperty resultSize = new SimpleIntegerProperty(); - resultSize.bind(Bindings.size(entriesFiltered)); - stateManager.setActiveSearchResultSize(context, resultSize); + entriesViewModel = EasyBind.mapBacked(allEntries, entry -> new BibEntryTableViewModel(entry, bibDatabaseContext, fieldValueFormatter), false); + entriesFiltered = new FilteredList<>(entriesViewModel, BibEntryTableViewModel::isVisible); + + entriesViewModel.addListener((ListChangeListener.Change change) -> { + while (change.next()) { + if (change.wasAdded() || change.wasUpdated()) { + BackgroundTask.wrap(() -> { + for (BibEntryTableViewModel entry : change.getList().subList(change.getFrom(), change.getTo())) { + updateEntrySearchMatch(searchQueryProperty.get(), entry, searchPreferences.getSearchDisplayMode() == SearchDisplayMode.FLOAT); + updateEntryGroupMatch(entry, groupsMatcher, groupsPreferences.getGroupViewMode().contains(GroupViewMode.INVERT), !groupsPreferences.getGroupViewMode().contains(GroupViewMode.FILTER)); + } + }).onSuccess(result -> FilteredListProxy.refilterListReflection(entriesFiltered, change.getFrom(), change.getTo())).executeWith(taskExecutor); + } + } + }); + + searchQuerySubscription = EasyBind.listen(searchQueryProperty, (observable, oldValue, newValue) -> updateSearchMatches(newValue)); + searchDisplayModeSubscription = EasyBind.listen(searchPreferences.searchDisplayModeProperty(), (observable, oldValue, newValue) -> updateSearchDisplayMode(newValue)); + selectedGroupsSubscription = EasyBind.listen(selectedGroupsProperty, (observable, oldValue, newValue) -> updateGroupMatches(newValue)); + groupViewModeSubscription = EasyBind.listen(preferencesService.getGroupsPreferences().groupViewModeProperty(), observable -> updateGroupMatches(selectedGroupsProperty.get())); + + resultSizeProperty.bind(Bindings.size(entriesFiltered.filtered(entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_AND_GROUPS).get()))); // We need to wrap the list since otherwise sorting in the table does not work entriesFilteredAndSorted = new SortedList<>(entriesFiltered); } - private boolean isMatched(ObservableList groups, Optional query, BibEntryTableViewModel entry) { - return isMatchedByGroup(groups, entry) && isMatchedBySearch(query, entry); + private void updateSearchMatches(Optional query) { + BackgroundTask.wrap(() -> { + boolean isFloatingMode = searchPreferences.getSearchDisplayMode() == SearchDisplayMode.FLOAT; + entriesViewModel.forEach(entry -> updateEntrySearchMatch(query, entry, isFloatingMode)); + }).onSuccess(result -> FilteredListProxy.refilterListReflection(entriesFiltered)).executeWith(taskExecutor); + } + + private static void updateEntrySearchMatch(Optional query, BibEntryTableViewModel entry, boolean isFloatingMode) { + boolean isMatched = query.map(matcher -> matcher.isMatch(entry.getEntry())).orElse(true); + entry.isMatchedBySearch().set(isMatched); + entry.updateMatchCategory(); + setEntrySearchVisibility(entry, isMatched, isFloatingMode); + } + + private static void setEntrySearchVisibility(BibEntryTableViewModel entry, boolean isMatched, boolean isFloatingMode) { + if (isMatched) { + entry.isVisibleBySearch().set(true); + } else { + entry.isVisibleBySearch().set(isFloatingMode); + } } - private boolean isMatchedBySearch(Optional query, BibEntryTableViewModel entry) { - return query.map(matcher -> matcher.isMatch(entry.getEntry())) - .orElse(true); + private void updateSearchDisplayMode(SearchDisplayMode mode) { + BackgroundTask.wrap(() -> { + boolean isFloatingMode = mode == SearchDisplayMode.FLOAT; + entriesViewModel.forEach(entry -> setEntrySearchVisibility(entry, entry.isMatchedBySearch().get(), isFloatingMode)); + }).onSuccess(result -> FilteredListProxy.refilterListReflection(entriesFiltered)).executeWith(taskExecutor); } - private boolean isMatchedByGroup(ObservableList groups, BibEntryTableViewModel entry) { - return createGroupMatcher(groups) - .map(matcher -> matcher.isMatch(entry.getEntry())) - .orElse(true); + private void updateGroupMatches(ObservableList groups) { + BackgroundTask.wrap(() -> { + groupsMatcher = createGroupMatcher(groups, groupsPreferences); + boolean isInvertMode = groupsPreferences.getGroupViewMode().contains(GroupViewMode.INVERT); + boolean isFloatingMode = !groupsPreferences.getGroupViewMode().contains(GroupViewMode.FILTER); + entriesViewModel.forEach(entry -> updateEntryGroupMatch(entry, groupsMatcher, isInvertMode, isFloatingMode)); + }).onSuccess(result -> FilteredListProxy.refilterListReflection(entriesFiltered)).executeWith(taskExecutor); } - private Optional createGroupMatcher(List selectedGroups) { + private void updateEntryGroupMatch(BibEntryTableViewModel entry, Optional groupsMatcher, boolean isInvertMode, boolean isFloatingMode) { + boolean isMatched = groupsMatcher.map(matcher -> matcher.isMatch(entry.getEntry()) ^ isInvertMode) + .orElse(true); + entry.isMatchedByGroup().set(isMatched); + entry.updateMatchCategory(); + if (isMatched) { + entry.isVisibleByGroup().set(true); + } else { + entry.isVisibleByGroup().set(isFloatingMode); + } + } + + private static Optional createGroupMatcher(List selectedGroups, GroupsPreferences groupsPreferences) { if ((selectedGroups == null) || selectedGroups.isEmpty()) { // No selected group, show all entries return Optional.empty(); } final MatcherSet searchRules = MatcherSets.build( - groupsPreferences.getGroupViewMode() == GroupViewMode.INTERSECTION + groupsPreferences.getGroupViewMode().contains(GroupViewMode.INTERSECTION) ? MatcherSets.MatcherType.AND : MatcherSets.MatcherType.OR); @@ -92,6 +159,13 @@ private Optional createGroupMatcher(List selectedGrou return Optional.of(searchRules); } + public void unbind() { + searchQuerySubscription.unsubscribe(); + searchDisplayModeSubscription.unsubscribe(); + selectedGroupsSubscription.unsubscribe(); + groupViewModeSubscription.unsubscribe(); + } + public SortedList getEntriesFilteredAndSorted() { return entriesFilteredAndSorted; } diff --git a/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java b/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java index 5cb8c5d9a6f..7cfe366ff12 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableHeaderContextMenu.java @@ -67,6 +67,9 @@ private void constructItems() { // Populate the menu with currently used fields for (TableColumn column : mainTable.getColumns()) { + if (((MainTableColumn) column).getModel().getType() == MainTableColumnModel.Type.MATCH_CATEGORY) { + continue; + } // Append only if the column has not already been added (a common column) RightClickMenuItem itemToAdd = createMenuItem(column, true); this.getItems().add(itemToAdd); @@ -150,8 +153,7 @@ private boolean isACommonColumn(MainTableColumn tableColumn) { * Determines if a list of TableColumns contains the searched column. */ private boolean isColumnInList(MainTableColumn searchColumn, List> tableColumns) { - for (TableColumn column: - tableColumns) { + for (TableColumn column: tableColumns) { MainTableColumnModel model = ((MainTableColumn) column).getModel(); if (model.equals(searchColumn.getModel())) { return true; diff --git a/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java b/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java index f47ead43426..e0a4fd36400 100644 --- a/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java +++ b/src/main/java/org/jabref/gui/maintable/PersistenceVisualStateTable.java @@ -67,8 +67,9 @@ private void updateSortOrder() { private List toList(List> columns) { return columns.stream() - .filter(col -> col instanceof MainTableColumn) - .map(column -> ((MainTableColumn) column).getModel()) - .collect(Collectors.toList()); + .filter(col -> col instanceof MainTableColumn) + .map(column -> ((MainTableColumn) column).getModel()) + .filter(model -> !model.getType().equals(MainTableColumnModel.Type.MATCH_CATEGORY)) + .collect(Collectors.toList()); } } diff --git a/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java b/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java index 84286ab62f3..0d1d095108f 100644 --- a/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java +++ b/src/main/java/org/jabref/gui/preferences/groups/GroupsTabViewModel.java @@ -22,15 +22,12 @@ public GroupsTabViewModel(GroupsPreferences groupsPreferences) { @Override public void setValues() { - switch (groupsPreferences.getGroupViewMode()) { - case INTERSECTION -> { - groupViewModeIntersectionProperty.setValue(true); - groupViewModeUnionProperty.setValue(false); - } - case UNION -> { - groupViewModeIntersectionProperty.setValue(false); - groupViewModeUnionProperty.setValue(true); - } + if (groupsPreferences.getGroupViewMode().contains(GroupViewMode.INTERSECTION)) { + groupViewModeIntersectionProperty.setValue(true); + groupViewModeUnionProperty.setValue(false); + } else { + groupViewModeIntersectionProperty.setValue(false); + groupViewModeUnionProperty.setValue(true); } autoAssignGroupProperty.setValue(groupsPreferences.shouldAutoAssignGroup()); displayGroupCountProperty.setValue(groupsPreferences.shouldDisplayGroupCount()); @@ -38,7 +35,7 @@ public void setValues() { @Override public void storeSettings() { - groupsPreferences.setGroupViewMode(groupViewModeIntersectionProperty.getValue() ? GroupViewMode.INTERSECTION : GroupViewMode.UNION); + groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION, groupViewModeIntersectionProperty.getValue()); groupsPreferences.setAutoAssignGroup(autoAssignGroupProperty.getValue()); groupsPreferences.setDisplayGroupCount(displayGroupCountProperty.getValue()); } diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java index e12ba4d82ef..d17400cd6e1 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -21,6 +21,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; import org.jabref.gui.desktop.JabRefDesktop; +import org.jabref.gui.search.SearchType; import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.BackgroundTask; import org.jabref.gui.util.TaskExecutor; @@ -169,8 +170,8 @@ public PreviewViewer(BibDatabaseContext database, return; } if (!registered) { - stateManager.activeSearchQueryProperty().addListener(listener); - stateManager.activeGlobalSearchQueryProperty().addListener(listener); + stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH).addListener(listener); + stateManager.activeSearchQuery(SearchType.GLOBAL_SEARCH).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 8838b0b7f7a..16f8e19e430 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -4,6 +4,7 @@ import java.time.Duration; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -54,7 +55,6 @@ import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.search.rules.describer.SearchDescribers; import org.jabref.gui.util.BindingsHelper; -import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.gui.util.TooltipTextUtil; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.logic.l10n.Localization; @@ -88,20 +88,18 @@ public class GlobalSearchBar extends HBox { private final ToggleButton fulltextButton; private final Button openGlobalSearchButton; private final ToggleButton keepSearchString; + private final ToggleButton filterModeButton; private final Tooltip searchFieldTooltip = new Tooltip(); private final Label currentResults = new Label(""); - private final StateManager stateManager; private final PreferencesService preferencesService; 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; + private final SearchType searchType; public GlobalSearchBar(LibraryTabContainer tabContainer, StateManager stateManager, @@ -116,12 +114,7 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, this.undoManager = undoManager; this.dialogService = dialogService; this.tabContainer = tabContainer; - - if (searchType == SearchType.NORMAL_SEARCH) { - searchQueryProperty = stateManager.activeSearchQueryProperty(); - } else { - searchQueryProperty = stateManager.activeGlobalSearchQueryProperty(); - } + this.searchType = searchType; KeyBindingRepository keyBindingRepository = preferencesService.getKeyBindingRepository(); @@ -130,6 +123,17 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, // fits the standard "found x entries"-message thus hinders the searchbar to jump around while searching if the tabContainer width is too small currentResults.setPrefWidth(150); + currentResults.visibleProperty().bind(stateManager.activeSearchQuery(searchType).isPresent()); + currentResults.textProperty().bind(Bindings.createStringBinding(() -> { + int matched = stateManager.searchResultSize(searchType).get(); + if (matched == 0) { + searchField.pseudoClassStateChanged(CLASS_NO_RESULTS, true); + return Localization.lang("No results found."); + } else { + searchField.pseudoClassStateChanged(CLASS_RESULTS_FOUND, true); + return Localization.lang("Found %0 results.", String.valueOf(matched)); + } + }, stateManager.searchResultSize(searchType))); searchField.setTooltip(searchFieldTooltip); searchFieldTooltip.setContentDisplay(ContentDisplay.GRAPHIC_ONLY); @@ -159,6 +163,7 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, fulltextButton = IconTheme.JabRefIcons.FULLTEXT.asToggleButton(); openGlobalSearchButton = IconTheme.JabRefIcons.OPEN_GLOBAL_SEARCH.asButton(); keepSearchString = IconTheme.JabRefIcons.KEEP_SEARCH_STRING.asToggleButton(); + filterModeButton = IconTheme.JabRefIcons.FILTER.asToggleButton(); initSearchModifierButtons(); @@ -167,6 +172,7 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, .or(caseSensitiveButton.focusedProperty()) .or(fulltextButton.focusedProperty()) .or(keepSearchString.focusedProperty()) + .or(filterModeButton.focusedProperty()) .or(searchField.textProperty().isNotEmpty()); regularExpressionButton.visibleProperty().unbind(); @@ -177,10 +183,12 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, fulltextButton.visibleProperty().bind(focusedOrActive); keepSearchString.visibleProperty().unbind(); keepSearchString.visibleProperty().bind(focusedOrActive); + filterModeButton.visibleProperty().unbind(); + filterModeButton.visibleProperty().bind(focusedOrActive); StackPane modifierButtons; if (searchType == SearchType.NORMAL_SEARCH) { - modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString)); + modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString, filterModeButton)); } else { modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton)); } @@ -202,7 +210,7 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, Timer searchTask = FxTimer.create(Duration.ofMillis(SEARCH_DELAY), this::updateSearchQuery); BindingsHelper.bindBidirectional( - searchQueryProperty, + stateManager.activeSearchQuery(searchType), searchField.textProperty(), searchTerm -> { // Async update @@ -210,8 +218,8 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, }, query -> setSearchTerm(query.map(SearchQuery::getQuery).orElse(""))); - this.searchQueryProperty.addListener((obs, oldValue, newValue) -> newValue.ifPresent(this::updateSearchResultsForQuery)); - this.stateManager.activeDatabaseProperty().addListener(obs -> searchQueryProperty.get().ifPresent(this::updateSearchResultsForQuery)); + stateManager.activeSearchQuery(searchType).addListener((obs, oldValue, newValue) -> + newValue.ifPresent(query -> setSearchFieldHintTooltip(SearchDescribers.getSearchDescriberFor(query).getDescription()))); /* * The listener tracks a change on the focus property value. * This happens, from active (user types a query) to inactive / focus @@ -226,11 +234,6 @@ public GlobalSearchBar(LibraryTabContainer tabContainer, }); } - private void updateSearchResultsForQuery(SearchQuery query) { - updateResults(this.stateManager.getSearchResultSize(searchQueryProperty).intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), - query.isGrammarBasedSearch()); - } - private void initSearchModifierButtons() { regularExpressionButton.setSelected(searchPreferences.isRegularExpression()); regularExpressionButton.setTooltip(new Tooltip(Localization.lang("regular expression"))); @@ -259,10 +262,12 @@ private void initSearchModifierButtons() { keepSearchString.setSelected(searchPreferences.shouldKeepSearchString()); keepSearchString.setTooltip(new Tooltip(Localization.lang("Keep search string across libraries"))); initSearchModifierButton(keepSearchString); - keepSearchString.selectedProperty().addListener((obs, oldVal, newVal) -> { - searchPreferences.setSearchFlag(SearchRules.SearchFlags.KEEP_SEARCH_STRING, newVal); - updateSearchQuery(); - }); + keepSearchString.selectedProperty().addListener((obs, oldVal, newVal) -> searchPreferences.setKeepSearchString(newVal)); + + filterModeButton.setSelected(searchPreferences.getSearchDisplayMode() == SearchDisplayMode.FILTER); + filterModeButton.setTooltip(new Tooltip(Localization.lang("Filter search results"))); + initSearchModifierButton(filterModeButton); + filterModeButton.setOnAction(event -> searchPreferences.setSearchDisplayMode(filterModeButton.isSelected() ? SearchDisplayMode.FILTER : SearchDisplayMode.FLOAT)); openGlobalSearchButton.disableProperty().bindBidirectional(globalSearchActive); openGlobalSearchButton.setTooltip(new Tooltip(Localization.lang("Search across libraries in a new window"))); @@ -273,7 +278,6 @@ private void initSearchModifierButtons() { regularExpressionButton.setSelected(searchPreferences.isRegularExpression()); caseSensitiveButton.setSelected(searchPreferences.isCaseSensitive()); fulltextButton.setSelected(searchPreferences.isFulltext()); - keepSearchString.setSelected(searchPreferences.shouldKeepSearchString()); }); } @@ -285,7 +289,8 @@ public void openGlobalSearchDialog() { if (globalSearchResultDialog == null) { globalSearchResultDialog = new GlobalSearchResultDialog(undoManager, tabContainer); } - stateManager.activeGlobalSearchQueryProperty().setValue(searchQueryProperty.get()); + stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH).get().ifPresent(query -> + stateManager.activeSearchQuery(SearchType.GLOBAL_SEARCH).set(Optional.of(query))); updateSearchQuery(); dialogService.showCustomDialogAndWait(globalSearchResultDialog); globalSearchActive.setValue(false); @@ -319,9 +324,8 @@ public void updateSearchQuery() { // An empty search field should cause the search to be cleared. if (searchField.getText().isEmpty()) { - currentResults.setText(""); setSearchFieldHintTooltip(null); - stateManager.clearSearchQuery(searchQueryProperty); + stateManager.activeSearchQuery(searchType).set(Optional.empty()); return; } @@ -336,7 +340,7 @@ public void updateSearchQuery() { informUserAboutInvalidSearchQuery(); return; } - stateManager.setSearchQuery(searchQueryProperty, searchQuery); + stateManager.activeSearchQuery(searchType).set(Optional.of(searchQuery)); } private boolean validRegex() { @@ -352,7 +356,7 @@ private boolean validRegex() { private void informUserAboutInvalidSearchQuery() { searchField.pseudoClassStateChanged(CLASS_NO_RESULTS, true); - stateManager.clearSearchQuery(searchQueryProperty); + stateManager.activeSearchQuery(searchType).set(Optional.empty()); String illegalSearch = Localization.lang("Search failed: illegal search expression"); currentResults.setText(illegalSearch); @@ -385,25 +389,6 @@ private AutoCompletePopup getPopup(AutoCompletionBinding autoCompletio } } - private void updateResults(int matched, TextFlow description, boolean grammarBasedSearch) { - if (matched == 0) { - currentResults.setText(Localization.lang("No results found.")); - searchField.pseudoClassStateChanged(CLASS_NO_RESULTS, true); - } else { - currentResults.setText(Localization.lang("Found %0 results.", String.valueOf(matched))); - searchField.pseudoClassStateChanged(CLASS_RESULTS_FOUND, true); - } - if (grammarBasedSearch) { - // TODO: switch Icon color - // searchIcon.setIcon(IconTheme.JabRefIcon.ADVANCED_SEARCH.getIcon()); - } else { - // TODO: switch Icon color - // searchIcon.setIcon(IconTheme.JabRefIcon.SEARCH.getIcon()); - } - - setSearchFieldHintTooltip(description); - } - private void setSearchFieldHintTooltip(TextFlow description) { if (preferencesService.getWorkspacePreferences().shouldShowAdvancedHints()) { String genericDescription = Localization.lang("Hint:\n\nTo search all fields for Smith, enter:\nsmith\n\nTo search the field author for Smith and the field title for electrical, enter:\nauthor=Smith and title=electrical"); diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java index 46d5eaf536e..ed529ed5849 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java @@ -68,7 +68,7 @@ private void initialize() { PreviewViewer previewViewer = new PreviewViewer(viewModel.getSearchDatabaseContext(), dialogService, preferencesService, stateManager, themeManager, taskExecutor); previewViewer.setLayout(preferencesService.getPreviewPreferences().getSelectedPreviewLayout()); - SearchResultsTableDataModel model = new SearchResultsTableDataModel(viewModel.getSearchDatabaseContext(), preferencesService, stateManager); + SearchResultsTableDataModel model = new SearchResultsTableDataModel(viewModel.getSearchDatabaseContext(), preferencesService, stateManager, taskExecutor); SearchResultsTable resultsTable = new SearchResultsTable(model, viewModel.getSearchDatabaseContext(), preferencesService, undoManager, dialogService, stateManager, taskExecutor); resultsTable.getColumns().removeIf(SpecialFieldColumn.class::isInstance); @@ -94,7 +94,7 @@ private void initialize() { .findFirst() .ifPresent(libraryTabContainer::showLibraryTab); - stateManager.clearSearchQuery(); + stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH).set(stateManager.activeSearchQuery(SearchType.GLOBAL_SEARCH).get()); stateManager.activeTabProperty().get().ifPresent(tab -> tab.clearAndSelect(selectedEntry.getEntry())); stage.close(); } diff --git a/src/main/java/org/jabref/gui/search/MatchCategory.java b/src/main/java/org/jabref/gui/search/MatchCategory.java new file mode 100644 index 00000000000..e67aa15fbca --- /dev/null +++ b/src/main/java/org/jabref/gui/search/MatchCategory.java @@ -0,0 +1,8 @@ +package org.jabref.gui.search; + +public enum MatchCategory { + MATCHING_SEARCH_AND_GROUPS, + MATCHING_SEARCH_NOT_GROUPS, + MATCHING_GROUPS_NOT_SEARCH, + NOT_MATCHING_SEARCH_AND_GROUPS +} diff --git a/src/main/java/org/jabref/gui/search/SearchDisplayMode.java b/src/main/java/org/jabref/gui/search/SearchDisplayMode.java index 18074f57a71..f0029758c99 100644 --- a/src/main/java/org/jabref/gui/search/SearchDisplayMode.java +++ b/src/main/java/org/jabref/gui/search/SearchDisplayMode.java @@ -1,33 +1,6 @@ package org.jabref.gui.search; -import java.util.function.Supplier; - -import org.jabref.logic.l10n.Localization; - -/** - * Collects the possible search modes - */ public enum SearchDisplayMode { - - FLOAT(() -> Localization.lang("Float"), () -> Localization.lang("Gray out non-hits")), - FILTER(() -> Localization.lang("Filter"), () -> Localization.lang("Hide non-hits")); - - private final Supplier displayName; - private final Supplier toolTipText; - - /** - * We have to use supplier for the localized text so that language changes are correctly reflected. - */ - SearchDisplayMode(Supplier displayName, Supplier toolTipText) { - this.displayName = displayName; - this.toolTipText = toolTipText; - } - - public String getDisplayName() { - return displayName.get(); - } - - public String getToolTipText() { - return toolTipText.get(); - } + FLOAT, + FILTER } diff --git a/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java b/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java index 21b0199ff5d..c3f38e05acc 100644 --- a/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java +++ b/src/main/java/org/jabref/gui/search/SearchResultsTableDataModel.java @@ -1,6 +1,5 @@ package org.jabref.gui.search; -import java.util.List; import java.util.Optional; import javafx.beans.binding.Bindings; @@ -16,6 +15,9 @@ import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.MainTableFieldValueFormatter; import org.jabref.gui.maintable.NameDisplayPreferences; +import org.jabref.gui.util.BackgroundTask; +import org.jabref.gui.util.FilteredListProxy; +import org.jabref.gui.util.TaskExecutor; import org.jabref.logic.search.SearchQuery; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -25,49 +27,50 @@ public class SearchResultsTableDataModel { + private final ObservableList entriesViewModel = FXCollections.observableArrayList(); private final SortedList entriesSorted; private final ObjectProperty fieldValueFormatter; - private final NameDisplayPreferences nameDisplayPreferences; - private final BibDatabaseContext bibDatabaseContext; private final StateManager stateManager; + private final FilteredList entriesFiltered; + private final TaskExecutor taskExecutor; - public SearchResultsTableDataModel(BibDatabaseContext bibDatabaseContext, PreferencesService preferencesService, StateManager stateManager) { - this.nameDisplayPreferences = preferencesService.getNameDisplayPreferences(); - this.bibDatabaseContext = bibDatabaseContext; + public SearchResultsTableDataModel(BibDatabaseContext bibDatabaseContext, PreferencesService preferencesService, StateManager stateManager, TaskExecutor taskExecutor) { + NameDisplayPreferences nameDisplayPreferences = preferencesService.getNameDisplayPreferences(); this.stateManager = stateManager; + this.taskExecutor = taskExecutor; 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)); + populateEntriesViewModel(); + stateManager.getOpenDatabases().addListener((ListChangeListener) change -> populateEntriesViewModel()); + entriesFiltered = new FilteredList<>(entriesViewModel, BibEntryTableViewModel::isVisible); // We need to wrap the list since otherwise sorting in the table does not work entriesSorted = new SortedList<>(entriesFiltered); + + EasyBind.listen(stateManager.activeSearchQuery(SearchType.GLOBAL_SEARCH), (observable, oldValue, newValue) -> updateSearchMatches(newValue)); + stateManager.searchResultSize(SearchType.GLOBAL_SEARCH).bind(Bindings.size(entriesFiltered)); } - private void populateEntriesViewModel(ObservableList entriesViewModel) { + private void populateEntriesViewModel() { entriesViewModel.clear(); for (BibDatabaseContext context : stateManager.getOpenDatabases()) { ObservableList entriesForDb = context.getDatabase().getEntries(); - List viewModelForDb = EasyBind.mapBacked(entriesForDb, entry -> new BibEntryTableViewModel(entry, context, fieldValueFormatter)); + ObservableList viewModelForDb = EasyBind.mapBacked(entriesForDb, entry -> new BibEntryTableViewModel(entry, context, fieldValueFormatter), false); entriesViewModel.addAll(viewModelForDb); } } + private void updateSearchMatches(Optional query) { + BackgroundTask.wrap(() -> entriesViewModel.forEach(entry -> entry.isVisibleBySearch().set(isMatchedBySearch(query, entry)))) + .onSuccess(result -> FilteredListProxy.refilterListReflection(entriesFiltered)) + .executeWith(taskExecutor); + } + private boolean isMatchedBySearch(Optional query, BibEntryTableViewModel entry) { - return query.map(matcher -> matcher.isMatch(entry.getEntry())) - .orElse(true); + return query.map(matcher -> matcher.isMatch(entry.getEntry())).orElse(true); } public SortedList getEntriesFilteredAndSorted() { return entriesSorted; } - - public void refresh() { - this.fieldValueFormatter.setValue(new MainTableFieldValueFormatter(nameDisplayPreferences, bibDatabaseContext)); - } } diff --git a/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java b/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java index 1ead8207f46..e5f48ae21f4 100644 --- a/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java +++ b/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java @@ -1,6 +1,11 @@ package org.jabref.gui.sidepane; +import java.util.EnumSet; + +import javafx.collections.SetChangeListener; import javafx.scene.control.Button; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.Tooltip; import org.jabref.gui.DialogService; import org.jabref.gui.actions.SimpleCommand; @@ -10,12 +15,12 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.logic.l10n.Localization; -import com.tobiasdiez.easybind.EasyBind; - public class GroupsSidePaneComponent extends SidePaneComponent { private final GroupsPreferences groupsPreferences; private final DialogService dialogService; private final Button intersectionUnionToggle = IconTheme.JabRefIcons.GROUP_INTERSECTION.asButton(); + private final ToggleButton filterToggle = IconTheme.JabRefIcons.FILTER.asToggleButton(); + private final ToggleButton invertToggle = IconTheme.JabRefIcons.INVERT.asToggleButton(); public GroupsSidePaneComponent(SimpleCommand closeCommand, SimpleCommand moveUpCommand, @@ -26,31 +31,48 @@ public GroupsSidePaneComponent(SimpleCommand closeCommand, super(SidePaneType.GROUPS, closeCommand, moveUpCommand, moveDownCommand, contentFactory); this.groupsPreferences = groupsPreferences; this.dialogService = dialogService; + + setupInvertToggle(); + setupFilterToggle(); setupIntersectionUnionToggle(); - EasyBind.subscribe(groupsPreferences.groupViewModeProperty(), mode -> { - GroupModeViewModel modeViewModel = new GroupModeViewModel(mode); + groupsPreferences.groupViewModeProperty().addListener((SetChangeListener) change -> { + GroupModeViewModel modeViewModel = new GroupModeViewModel(groupsPreferences.groupViewModeProperty()); intersectionUnionToggle.setGraphic(modeViewModel.getUnionIntersectionGraphic()); intersectionUnionToggle.setTooltip(modeViewModel.getUnionIntersectionTooltip()); }); } private void setupIntersectionUnionToggle() { - addExtraButtonToHeader(intersectionUnionToggle, 0); + addExtraNodeToHeader(intersectionUnionToggle, 0); intersectionUnionToggle.setOnAction(event -> new ToggleUnionIntersectionAction().execute()); } + private void setupFilterToggle() { + addExtraNodeToHeader(filterToggle, 0); + filterToggle.setTooltip(new Tooltip(Localization.lang("Filter by groups"))); + filterToggle.setSelected(groupsPreferences.groupViewModeProperty().contains(GroupViewMode.FILTER)); + filterToggle.selectedProperty().addListener((observable, oldValue, newValue) -> groupsPreferences.setGroupViewMode(GroupViewMode.FILTER, newValue)); + } + + private void setupInvertToggle() { + addExtraNodeToHeader(invertToggle, 0); + invertToggle.setTooltip(new Tooltip(Localization.lang("Invert groups"))); + invertToggle.setSelected(groupsPreferences.groupViewModeProperty().contains(GroupViewMode.INVERT)); + invertToggle.selectedProperty().addListener((observable, oldValue, newValue) -> groupsPreferences.setGroupViewMode(GroupViewMode.INVERT, newValue)); + } + private class ToggleUnionIntersectionAction extends SimpleCommand { @Override public void execute() { - GroupViewMode mode = groupsPreferences.getGroupViewMode(); + EnumSet mode = groupsPreferences.getGroupViewMode(); - if (mode == GroupViewMode.UNION) { - groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION); + if (!mode.contains(GroupViewMode.INTERSECTION)) { + groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION, true); dialogService.notify(Localization.lang("Group view mode set to intersection")); - } else if (mode == GroupViewMode.INTERSECTION) { - groupsPreferences.setGroupViewMode(GroupViewMode.UNION); + } else { + groupsPreferences.setGroupViewMode(GroupViewMode.INTERSECTION, false); dialogService.notify(Localization.lang("Group view mode set to union")); } } diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java b/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java index 8abb96582dc..6260a908123 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java @@ -69,7 +69,7 @@ private Node createHeaderView() { return headerView; } - protected void addExtraButtonToHeader(Button button, int position) { + protected void addExtraNodeToHeader(Node button, int position) { this.buttonContainer.getChildren().add(position, button); } diff --git a/src/main/java/org/jabref/gui/util/FilteredListProxy.java b/src/main/java/org/jabref/gui/util/FilteredListProxy.java new file mode 100644 index 00000000000..5faa613ebc1 --- /dev/null +++ b/src/main/java/org/jabref/gui/util/FilteredListProxy.java @@ -0,0 +1,113 @@ +package org.jabref.gui.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.ListIterator; +import java.util.function.Predicate; + +import javafx.collections.ObservableListBase; +import javafx.collections.transformation.FilteredList; + +import org.jabref.gui.maintable.BibEntryTableViewModel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class FilteredListProxy { + private static final Logger LOGGER = LoggerFactory.getLogger(FilteredListProxy.class); + + private FilteredListProxy() { + } + + public static void refilterListReflection(FilteredList filteredList) { + try { + Method refilter = FilteredList.class.getDeclaredMethod("refilter"); + refilter.setAccessible(true); + refilter.invoke(filteredList); + } catch (Exception e) { + LOGGER.warn("Could not refilter list", e); + } + } + + public static void refilterListReflection(FilteredList filteredList, int sourceFrom, int sourceTo) { + try { + if (sourceFrom < 0 || sourceTo > filteredList.getSource().size() || sourceFrom > sourceTo) { + throw new IndexOutOfBoundsException(); + } + + invoke(filteredList, ObservableListBase.class, "beginChange"); + + invoke(filteredList, FilteredList.class, "ensureSize", filteredList.getSource().size()); + + @SuppressWarnings("unchecked") + Predicate predicateImpl = (Predicate) invoke(filteredList, FilteredList.class, "getPredicateImpl"); + ListIterator it = filteredList.getSource().listIterator(sourceFrom); + + Field filteredField = getField("filtered"); + int[] filtered = (int[]) filteredField.get(filteredList); + + Field sizeField = getField("size"); + int size = (int) sizeField.get(filteredList); + + for (int i = sourceFrom; i < sourceTo; ++i) { + BibEntryTableViewModel el = it.next(); + int pos = Arrays.binarySearch(filtered, 0, size, i); + boolean passedBefore = pos >= 0; + boolean passedNow = predicateImpl.test(el); + /* 1. passed before and now -> nextUpdate + * 2. passed before and not now -> nextRemove + * 3. not passed before and now -> nextAdd + * 4. not passed before and not now -> do nothing */ + if (passedBefore && passedNow) { + invoke(filteredList, ObservableListBase.class, "nextUpdate", pos); + } else if (passedBefore) { + invoke(filteredList, ObservableListBase.class, "nextRemove", pos, el); + System.arraycopy(filtered, pos + 1, filtered, pos, size - pos - 1); + size--; + } else if (passedNow) { + int insertionPoint = ~pos; + System.arraycopy(filtered, insertionPoint, filtered, insertionPoint + 1, size - insertionPoint); + filtered[insertionPoint] = i; + invoke(filteredList, ObservableListBase.class, "nextAdd", insertionPoint, insertionPoint + 1); + size++; + } + } + + // Write back + filteredField.set(filteredList, filtered); + sizeField.set(filteredList, size); + + invoke(filteredList, ObservableListBase.class, "endChange"); + } catch (ReflectiveOperationException e) { + LOGGER.warn("Could not refilter list", e); + } + } + + /** + * We directly invoke the specified method on the given filteredList + */ + private static Object invoke(FilteredList filteredList, Class clazz, String methodName, Object... params) throws ReflectiveOperationException { + // Determine the parameter types for the method lookup + Class[] paramTypes = new Class[params.length]; + for (int i = 0; i < params.length; i++) { + paramTypes[i] = params[i].getClass(); + if (paramTypes[i] == Integer.class) { + // quick hack, because int is converted to Object when calling this method. + paramTypes[i] = int.class; + } + } + Method method = clazz.getDeclaredMethod(methodName, paramTypes); + method.setAccessible(true); + return method.invoke(filteredList, params); + } + + /** + * Get the class field (we need it for read and write later) + */ + private static Field getField(String fieldName) throws ReflectiveOperationException { + Field field = FilteredList.class.getDeclaredField(fieldName); + field.setAccessible(true); + return field; + } +} diff --git a/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java b/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java index 87114e3b6a0..da698c6111d 100644 --- a/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java +++ b/src/main/java/org/jabref/gui/util/ViewModelTableRowFactory.java @@ -1,8 +1,12 @@ package org.jabref.gui.util; +import java.util.HashMap; +import java.util.Map; import java.util.function.BiConsumer; import java.util.function.Function; +import javafx.beans.value.ObservableValue; +import javafx.css.PseudoClass; import javafx.geometry.Bounds; import javafx.geometry.Point2D; import javafx.scene.control.ContextMenu; @@ -37,6 +41,7 @@ public class ViewModelTableRowFactory implements Callback, Table private TriConsumer, S, ? super DragEvent> toOnDragOver; private TriConsumer, S, ? super MouseDragEvent> toOnMouseDragEntered; private Callback toTooltip; + private final Map>> pseudoClasses = new HashMap<>(); public ViewModelTableRowFactory withOnMouseClickedEvent(BiConsumer onMouseClickedEvent) { this.onMouseClickedEvent = onMouseClickedEvent; @@ -104,9 +109,26 @@ public ViewModelTableRowFactory withTooltip(Callback toTooltip) { return this; } + public ViewModelTableRowFactory withPseudoClass(PseudoClass pseudoClass, Callback> toCondition) { + this.pseudoClasses.putIfAbsent(pseudoClass, toCondition); + return this; + } + @Override public TableRow call(TableView tableView) { - TableRow row = new TableRow<>(); + TableRow row = new TableRow<>() { + @Override + protected void updateItem(S item, boolean empty) { + super.updateItem(item, empty); + + if (empty || getItem() == null) { + pseudoClasses.forEach((pseudoClass, toCondition) -> pseudoClassStateChanged(pseudoClass, false)); + } else { + pseudoClasses.forEach((pseudoClass, toCondition) -> + pseudoClassStateChanged(pseudoClass, toCondition.call(getItem()).getValue())); + } + } + }; if (toTooltip != null) { String tooltipText = toTooltip.call(row.getItem()); diff --git a/src/main/java/org/jabref/gui/util/ViewModelTreeTableRowFactory.java b/src/main/java/org/jabref/gui/util/ViewModelTreeTableRowFactory.java index c89436d8064..cb97c9c2433 100644 --- a/src/main/java/org/jabref/gui/util/ViewModelTreeTableRowFactory.java +++ b/src/main/java/org/jabref/gui/util/ViewModelTreeTableRowFactory.java @@ -151,7 +151,7 @@ protected void updateItem(S row, boolean empty) { // Activate context menu if user presses the "context menu" key treeTableView.addEventHandler(KeyEvent.KEY_RELEASED, event -> { - boolean rowFocused = isEmpty() && treeTableView.getFocusModel().getFocusedIndex() == getIndex(); + boolean rowFocused = !isEmpty() && treeTableView.getFocusModel().getFocusedIndex() == getIndex(); if (event.getCode() == KeyCode.CONTEXT_MENU && rowFocused) { // Get center of focused cell Bounds anchorBounds = getBoundsInParent(); diff --git a/src/main/java/org/jabref/logic/search/SearchQuery.java b/src/main/java/org/jabref/logic/search/SearchQuery.java index c6ac26e7cca..4a960c51100 100644 --- a/src/main/java/org/jabref/logic/search/SearchQuery.java +++ b/src/main/java/org/jabref/logic/search/SearchQuery.java @@ -58,7 +58,7 @@ String format(String regex) { } private final String query; - private EnumSet searchFlags; + private final EnumSet searchFlags; private final SearchRule rule; public SearchQuery(String query, EnumSet searchFlags) { diff --git a/src/main/java/org/jabref/migrations/PreferencesMigrations.java b/src/main/java/org/jabref/migrations/PreferencesMigrations.java index 996612f21a5..b5b19344f51 100644 --- a/src/main/java/org/jabref/migrations/PreferencesMigrations.java +++ b/src/main/java/org/jabref/migrations/PreferencesMigrations.java @@ -436,7 +436,8 @@ static void changeColumnVariableNamesFor51(JabRefPreferences preferences) { * they can deal with. */ static void restoreVariablesForBackwardCompatibility(JabRefPreferences preferences) { - List oldColumnNames = preferences.getStringList(JabRefPreferences.COLUMN_NAMES); + // 5.0 preference name "columnNames". The new one is {@link JabRefPreferences#COLUMN_NAMES} + List oldColumnNames = preferences.getStringList("columnNames"); List fieldColumnNames = oldColumnNames.stream() .filter(columnName -> columnName.startsWith("field:") || columnName.startsWith("special:")) .map(columnName -> { diff --git a/src/main/java/org/jabref/model/search/rules/SearchRules.java b/src/main/java/org/jabref/model/search/rules/SearchRules.java index ba28e66b133..f2923a25d95 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRules.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRules.java @@ -44,6 +44,6 @@ static SearchRule getSearchRule(EnumSet searchFlags) { } public enum SearchFlags { - CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT, KEEP_SEARCH_STRING + CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT } } diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index e47e007e812..138e9ece042 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -440,7 +440,9 @@ public class JabRefPreferences implements PreferencesService { private static final String PROTECTED_TERMS_DISABLED_INTERNAL = "protectedTermsDisabledInternal"; // GroupViewMode - private static final String GROUP_INTERSECT_UNION_VIEW_MODE = "groupIntersectUnionViewModes"; + private static final String GROUP_VIEW_INTERSECTION = "groupIntersection"; + private static final String GROUP_VIEW_FILTER = "groupFilter"; + private static final String GROUP_VIEW_INVERT = "groupInvert"; private static final String DEFAULT_HIERARCHICAL_CONTEXT = "defaultHierarchicalContext"; // Dialog states @@ -536,11 +538,11 @@ private JabRefPreferences() { // Since some of the preference settings themselves use localized strings, we cannot set the language after // the initialization of the preferences in main // Otherwise that language framework will be instantiated and more importantly, statically initialized preferences - // like the SearchDisplayMode will never be translated. + // will never be translated. Localization.setLanguage(getLanguage()); - defaults.put(SEARCH_DISPLAY_MODE, SearchDisplayMode.FILTER.toString()); defaults.put(SEARCH_CASE_SENSITIVE, Boolean.FALSE); + defaults.put(SEARCH_DISPLAY_MODE, Boolean.TRUE); defaults.put(SEARCH_REG_EXP, Boolean.FALSE); defaults.put(SEARCH_FULLTEXT, Boolean.FALSE); defaults.put(SEARCH_KEEP_SEARCH_STRING, Boolean.FALSE); @@ -697,7 +699,9 @@ private JabRefPreferences() { defaults.put(AUTOCOMPLETER_COMPLETE_FIELDS, "author;editor;title;journal;publisher;keywords;crossref;related;entryset"); defaults.put(AUTO_ASSIGN_GROUP, Boolean.TRUE); defaults.put(DISPLAY_GROUP_COUNT, Boolean.TRUE); - defaults.put(GROUP_INTERSECT_UNION_VIEW_MODE, GroupViewMode.INTERSECTION.name()); + defaults.put(GROUP_VIEW_INTERSECTION, Boolean.TRUE); + defaults.put(GROUP_VIEW_FILTER, Boolean.TRUE); + defaults.put(GROUP_VIEW_INVERT, Boolean.FALSE); defaults.put(DEFAULT_HIERARCHICAL_CONTEXT, GroupHierarchyType.INDEPENDENT.name()); defaults.put(KEYWORD_SEPARATOR, ", "); defaults.put(DEFAULT_ENCODING, StandardCharsets.UTF_8.name()); @@ -1425,13 +1429,19 @@ public GroupsPreferences getGroupsPreferences() { } groupsPreferences = new GroupsPreferences( - GroupViewMode.valueOf(get(GROUP_INTERSECT_UNION_VIEW_MODE)), + getBoolean(GROUP_VIEW_INTERSECTION), + getBoolean(GROUP_VIEW_FILTER), + getBoolean(GROUP_VIEW_INVERT), getBoolean(AUTO_ASSIGN_GROUP), getBoolean(DISPLAY_GROUP_COUNT), GroupHierarchyType.valueOf(get(DEFAULT_HIERARCHICAL_CONTEXT)) ); - EasyBind.listen(groupsPreferences.groupViewModeProperty(), (obs, oldValue, newValue) -> put(GROUP_INTERSECT_UNION_VIEW_MODE, newValue.name())); + groupsPreferences.groupViewModeProperty().addListener((SetChangeListener) change -> { + putBoolean(GROUP_VIEW_INTERSECTION, groupsPreferences.groupViewModeProperty().contains(GroupViewMode.INTERSECTION)); + putBoolean(GROUP_VIEW_FILTER, groupsPreferences.groupViewModeProperty().contains(GroupViewMode.FILTER)); + putBoolean(GROUP_VIEW_INVERT, groupsPreferences.groupViewModeProperty().contains(GroupViewMode.INVERT)); + }); EasyBind.listen(groupsPreferences.autoAssignGroupProperty(), (obs, oldValue, newValue) -> putBoolean(AUTO_ASSIGN_GROUP, newValue)); EasyBind.listen(groupsPreferences.displayGroupCountProperty(), (obs, oldValue, newValue) -> putBoolean(DISPLAY_GROUP_COUNT, newValue)); EasyBind.listen(groupsPreferences.defaultHierarchicalContextProperty(), (obs, oldValue, newValue) -> put(DEFAULT_HIERARCHICAL_CONTEXT, newValue.name())); @@ -2726,16 +2736,8 @@ public SearchPreferences getSearchPreferences() { return searchPreferences; } - SearchDisplayMode searchDisplayMode; - try { - searchDisplayMode = SearchDisplayMode.valueOf(get(SEARCH_DISPLAY_MODE)); - } catch (IllegalArgumentException ex) { - // Should only occur when the searchmode is set directly via preferences.put and the enum was not used - searchDisplayMode = SearchDisplayMode.valueOf((String) defaults.get(SEARCH_DISPLAY_MODE)); - } - searchPreferences = new SearchPreferences( - searchDisplayMode, + getBoolean(SEARCH_DISPLAY_MODE) ? SearchDisplayMode.FILTER : SearchDisplayMode.FLOAT, getBoolean(SEARCH_CASE_SENSITIVE), getBoolean(SEARCH_REG_EXP), getBoolean(SEARCH_FULLTEXT), @@ -2745,13 +2747,13 @@ public SearchPreferences getSearchPreferences() { getDouble(SEARCH_WINDOW_WIDTH), getDouble(SEARCH_WINDOW_DIVIDER_POS)); - EasyBind.listen(searchPreferences.searchDisplayModeProperty(), (obs, oldValue, newValue) -> put(SEARCH_DISPLAY_MODE, Objects.requireNonNull(searchPreferences.getSearchDisplayMode()).toString())); searchPreferences.getObservableSearchFlags().addListener((SetChangeListener) c -> { putBoolean(SEARCH_CASE_SENSITIVE, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.CASE_SENSITIVE)); putBoolean(SEARCH_REG_EXP, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.REGULAR_EXPRESSION)); putBoolean(SEARCH_FULLTEXT, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.FULLTEXT)); - putBoolean(SEARCH_KEEP_SEARCH_STRING, searchPreferences.getObservableSearchFlags().contains(SearchRules.SearchFlags.KEEP_SEARCH_STRING)); }); + EasyBind.listen(searchPreferences.searchDisplayModeProperty(), (obs, oldValue, newValue) -> putBoolean(SEARCH_DISPLAY_MODE, newValue == SearchDisplayMode.FILTER)); + EasyBind.listen(searchPreferences.keepSearchStingProperty(), (obs, oldValue, newValue) -> putBoolean(SEARCH_KEEP_SEARCH_STRING, newValue)); EasyBind.listen(searchPreferences.keepWindowOnTopProperty(), (obs, oldValue, newValue) -> putBoolean(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP, searchPreferences.shouldKeepWindowOnTop())); EasyBind.listen(searchPreferences.getSearchWindowHeightProperty(), (obs, oldValue, newValue) -> putDouble(SEARCH_WINDOW_HEIGHT, searchPreferences.getSearchWindowHeight())); EasyBind.listen(searchPreferences.getSearchWindowWidthProperty(), (obs, oldValue, newValue) -> putDouble(SEARCH_WINDOW_WIDTH, searchPreferences.getSearchWindowWidth())); diff --git a/src/main/java/org/jabref/preferences/SearchPreferences.java b/src/main/java/org/jabref/preferences/SearchPreferences.java index 737c267158b..387b1eb0e20 100644 --- a/src/main/java/org/jabref/preferences/SearchPreferences.java +++ b/src/main/java/org/jabref/preferences/SearchPreferences.java @@ -14,23 +14,21 @@ import org.jabref.gui.search.SearchDisplayMode; import org.jabref.model.search.rules.SearchRules.SearchFlags; +import com.google.common.annotations.VisibleForTesting; + public class SearchPreferences { - private final ObjectProperty searchDisplayMode; private final ObservableSet searchFlags; private final BooleanProperty keepWindowOnTop; private final DoubleProperty searchWindowHeight; private final DoubleProperty searchWindowWidth; private final DoubleProperty searchWindowDividerPosition; + private final BooleanProperty keepSearchSting; + private final ObjectProperty searchDisplayMode; - public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext, boolean isKeepSearchString, boolean keepWindowOnTop, double searchWindowHeight, double searchWindowWidth, double searchWindowDividerPosition) { - this.searchDisplayMode = new SimpleObjectProperty<>(searchDisplayMode); - this.keepWindowOnTop = new SimpleBooleanProperty(keepWindowOnTop); - this.searchWindowHeight = new SimpleDoubleProperty(searchWindowHeight); - this.searchWindowWidth = new SimpleDoubleProperty(searchWindowWidth); - this.searchWindowDividerPosition = new SimpleDoubleProperty(searchWindowDividerPosition); + public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext, boolean keepSearchString, boolean keepWindowOnTop, double searchWindowHeight, double searchWindowWidth, double searchWindowDividerPosition) { + this(searchDisplayMode, EnumSet.noneOf(SearchFlags.class), keepSearchString, keepWindowOnTop, searchWindowHeight, searchWindowWidth, searchWindowDividerPosition); - searchFlags = FXCollections.observableSet(EnumSet.noneOf(SearchFlags.class)); if (isCaseSensitive) { searchFlags.add(SearchFlags.CASE_SENSITIVE); } @@ -40,9 +38,18 @@ public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSens if (isFulltext) { searchFlags.add(SearchFlags.FULLTEXT); } - if (isKeepSearchString) { - searchFlags.add(SearchFlags.KEEP_SEARCH_STRING); - } + } + + @VisibleForTesting + public SearchPreferences(SearchDisplayMode searchDisplayMode, EnumSet searchFlags, boolean keepSearchString, boolean keepWindowOnTop, double searchWindowHeight, double searchWindowWidth, double searchWindowDividerPosition) { + this.searchDisplayMode = new SimpleObjectProperty<>(searchDisplayMode); + this.searchFlags = FXCollections.observableSet(searchFlags); + + this.keepWindowOnTop = new SimpleBooleanProperty(keepWindowOnTop); + this.searchWindowHeight = new SimpleDoubleProperty(searchWindowHeight); + this.searchWindowWidth = new SimpleDoubleProperty(searchWindowWidth); + this.searchWindowDividerPosition = new SimpleDoubleProperty(searchWindowDividerPosition); + this.keepSearchSting = new SimpleBooleanProperty(keepSearchString); } public EnumSet getSearchFlags() { @@ -57,18 +64,6 @@ public ObservableSet getObservableSearchFlags() { return searchFlags; } - public SearchDisplayMode getSearchDisplayMode() { - return searchDisplayMode.get(); - } - - public ObjectProperty searchDisplayModeProperty() { - return searchDisplayMode; - } - - public void setSearchDisplayMode(SearchDisplayMode searchDisplayMode) { - this.searchDisplayMode.set(searchDisplayMode); - } - public boolean isCaseSensitive() { return searchFlags.contains(SearchFlags.CASE_SENSITIVE); } @@ -90,7 +85,7 @@ public boolean isFulltext() { } public boolean shouldKeepSearchString() { - return searchFlags.contains(SearchFlags.KEEP_SEARCH_STRING); + return keepSearchSting.get(); } public boolean shouldKeepWindowOnTop() { @@ -125,10 +120,22 @@ public DoubleProperty getSearchWindowWidthProperty() { return this.searchWindowWidth; } + public SearchDisplayMode getSearchDisplayMode() { + return searchDisplayMode.get(); + } + public DoubleProperty getSearchWindowDividerPositionProperty() { return this.searchWindowDividerPosition; } + public ObjectProperty searchDisplayModeProperty() { + return this.searchDisplayMode; + } + + public BooleanProperty keepSearchStingProperty() { + return keepSearchSting; + } + public void setSearchWindowHeight(double height) { this.searchWindowHeight.set(height); } @@ -140,4 +147,12 @@ public void setSearchWindowWidth(double width) { public void setSearchWindowDividerPosition(double position) { this.searchWindowDividerPosition.set(position); } + + public void setSearchDisplayMode(SearchDisplayMode searchDisplayMode) { + this.searchDisplayMode.set(searchDisplayMode); + } + + public void setKeepSearchString(boolean keepSearchString) { + this.keepSearchSting.set(keepSearchString); + } } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 9908c8aafae..70999556ae2 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -396,7 +396,6 @@ Autolink\ only\ files\ that\ match\ the\ citation\ key=Autolink only files that Fit\ table\ horizontally\ on\ screen=Fit table horizontally on screen -Float=Float Format\:\ Tab\:field;field;...\ (e.g.\ General\:url;pdf;note...)=Format\: Tab\:field;field;... (e.g. General\:url;pdf;note...) Format\ of\ author\ and\ editor\ names=Format of author and editor names @@ -413,8 +412,6 @@ General=General Get\ fulltext=Get fulltext -Gray\ out\ non-hits=Gray out non-hits - Groups=Groups has/have\ both\ a\ 'Comment'\ and\ a\ 'Review'\ field.=has/have both a 'Comment' and a 'Review' field. @@ -424,8 +421,6 @@ Help=Help Help\ on\ key\ patterns=Help on key patterns -Hide\ non-hits=Hide non-hits - Hierarchical\ context=Hierarchical context Highlight=Highlight @@ -781,6 +776,12 @@ Unable\ to\ save\ library=Unable to save library Always\ reformat\ library\ on\ save\ and\ export=Always reformat library on save and export Character\ encoding\ '%0'\ is\ not\ supported.=Character encoding '%0' is not supported. +Filter\ search\ results=Filter search results +Filter\ by\ groups=Filter by groups +Invert\ groups=Invert groups +Match\ category=Match category +Scroll\ to\ previous\ match\ category=Scroll to previous match category +Scroll\ to\ next\ match\ category=Scroll to next match category Search=Search Search...=Search... Searching...=Searching... diff --git a/src/test/java/org/jabref/cli/ArgumentProcessorTest.java b/src/test/java/org/jabref/cli/ArgumentProcessorTest.java index 66502ed18b6..799538402be 100644 --- a/src/test/java/org/jabref/cli/ArgumentProcessorTest.java +++ b/src/test/java/org/jabref/cli/ArgumentProcessorTest.java @@ -2,12 +2,14 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.EnumSet; import java.util.List; import java.util.Objects; import javafx.collections.FXCollections; import org.jabref.cli.ArgumentProcessor.Mode; +import org.jabref.gui.search.SearchDisplayMode; import org.jabref.logic.bibtex.BibEntryAssert; import org.jabref.logic.exporter.BibDatabaseWriter; import org.jabref.logic.exporter.SelfContainedSaveConfiguration; @@ -18,6 +20,7 @@ import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.metadata.SaveOrder; import org.jabref.model.metadata.SelfContainedSaveOrder; +import org.jabref.model.search.rules.SearchRules; import org.jabref.model.util.DummyFileUpdateMonitor; import org.jabref.model.util.FileUpdateMonitor; import org.jabref.preferences.ExportPreferences; @@ -46,9 +49,14 @@ void setup() { when(preferencesService.getImporterPreferences()).thenReturn(importerPreferences); when(preferencesService.getImportFormatPreferences()).thenReturn(importFormatPreferences); - when(preferencesService.getSearchPreferences()).thenReturn( - new SearchPreferences(null, false, false, false, false, false, 0, 0, 0) - ); + when(preferencesService.getSearchPreferences()).thenReturn(new SearchPreferences( + SearchDisplayMode.FILTER, + EnumSet.noneOf(SearchRules.SearchFlags.class), + false, + false, + 0, + 0, + 0)); } @Test diff --git a/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java b/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java index 1623ee43cf8..f11b938805a 100644 --- a/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java +++ b/src/test/java/org/jabref/gui/entryeditor/SourceTabTest.java @@ -12,6 +12,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; import org.jabref.gui.keyboard.KeyBindingRepository; +import org.jabref.gui.search.SearchType; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.logic.bibtex.FieldPreferences; @@ -49,8 +50,7 @@ public void onStart(Stage stage) { area = new CodeArea(); 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()); + when(stateManager.activeSearchQuery(SearchType.NORMAL_SEARCH)).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(','); @@ -64,9 +64,9 @@ public void onStart(Stage stage) { importFormatPreferences, new DummyFileUpdateMonitor(), mock(DialogService.class), - stateManager, mock(BibEntryTypesManager.class), - keyBindingRepository); + keyBindingRepository, + OptionalObjectProperty.empty()); pane = new TabPane( new Tab("main area", area), new Tab("other tab", new Label("some text")), diff --git a/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java b/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java index f05b398d285..3f85fe53cd6 100644 --- a/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java +++ b/src/test/java/org/jabref/gui/groups/GroupNodeViewModelTest.java @@ -1,6 +1,7 @@ package org.jabref.gui.groups; import java.util.Arrays; +import java.util.EnumSet; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -45,7 +46,7 @@ void setUp() { taskExecutor = new CurrentThreadTaskExecutor(); preferencesService = mock(PreferencesService.class); when(preferencesService.getGroupsPreferences()).thenReturn(new GroupsPreferences( - GroupViewMode.UNION, + EnumSet.noneOf(GroupViewMode.class), true, true, GroupHierarchyType.INDEPENDENT diff --git a/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java b/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java index bdb27ee2fc1..77c19f2a1f2 100644 --- a/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java +++ b/src/test/java/org/jabref/gui/groups/GroupTreeViewModelTest.java @@ -1,5 +1,6 @@ package org.jabref.gui.groups; +import java.util.EnumSet; import java.util.Optional; import org.jabref.gui.DialogService; @@ -46,7 +47,7 @@ void setUp() { dialogService = mock(DialogService.class, Answers.RETURNS_DEEP_STUBS); when(preferencesService.getGroupsPreferences()).thenReturn(new GroupsPreferences( - GroupViewMode.UNION, + EnumSet.noneOf(GroupViewMode.class), true, true, GroupHierarchyType.INDEPENDENT)); diff --git a/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java b/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java index e9df40ccb9b..e22899ae189 100644 --- a/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java +++ b/src/test/java/org/jabref/migrations/PreferencesMigrationsTest.java @@ -184,7 +184,7 @@ void restoreColumnVariablesForBackwardCompatibility() { List columnNames = Arrays.asList("entrytype", "author/editor", "title", "year", "journal/booktitle", "citationkey", "printed"); List columnWidths = Arrays.asList("100", "100", "100", "100", "100", "100", "100"); - when(prefs.getStringList(JabRefPreferences.COLUMN_NAMES)).thenReturn(updatedNames); + when(prefs.getStringList("columnNames")).thenReturn(updatedNames); when(prefs.get(JabRefPreferences.MAIN_FONT_SIZE)).thenReturn("11.2");