diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f4adc29bf1..66951a4733d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,9 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We added unprotect_terms to the list of bracketed pattern modifiers [#7826](https://github.com/JabRef/jabref/pull/7960) - We added a dialog that allows to parse metadata from linked pdfs. [#7929](https://github.com/JabRef/jabref/pull/7929) - We added an icon picker in group edit dialog. [#6142](https://github.com/JabRef/jabref/issues/6142) -- We added a preference to Opt-In to JabRef's online metadata extraction service (Grobid) usage. [8002](https://github.com/JabRef/jabref/pull/8002) +- We added a preference to Opt-In to JabRef's online metadata extraction service (Grobid) usage. [#8002](https://github.com/JabRef/jabref/pull/8002) +- We readded the possibility to display the search results of all databases ("Global Search"). It is shown in a separate window. [#4096](https://github.com/JabRef/jabref/issues/4096) +- We readded the possibility to keep the search string when switching tabs. It is implemented by a toggle button. [#4096](https://github.com/JabRef/jabref/issues/4096#issuecomment-575986882) ### Changed diff --git a/src/main/java/org/jabref/gui/Base.css b/src/main/java/org/jabref/gui/Base.css index 4cb10cef40f..bf25ba043b3 100644 --- a/src/main/java/org/jabref/gui/Base.css +++ b/src/main/java/org/jabref/gui/Base.css @@ -960,6 +960,12 @@ TextFlow > .tooltip-text-monospaced { -fx-fill: -jr-search-text; } +.mainToolbar .search-field .button .glyph-icon { + -fx-fill: -jr-search-text; + -fx-text-fill: -jr-search-text; + -fx-icon-color: -jr-search-text; +} + /* magnifier glass */ .mainToolbar .search-field .glyph-icon { -fx-fill: -jr-search-text; diff --git a/src/main/java/org/jabref/gui/Dark.css b/src/main/java/org/jabref/gui/Dark.css index a6bd49cc401..ecc993453dc 100644 --- a/src/main/java/org/jabref/gui/Dark.css +++ b/src/main/java/org/jabref/gui/Dark.css @@ -108,6 +108,11 @@ -fx-background-color: -jr-background; } +.mainToolbar .search-field .button .glyph-icon { + -fx-fill: derive(-fx-light-text-color, 80%); + -fx-text-fill: derive(-fx-light-text-color, 80%); + -fx-icon-color: derive(-fx-light-text-color, 80%); +} .mainToolbar .search-field .toggle-button .glyph-icon { -fx-fill: -jr-search-text; diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 7c5f9e26e58..22ab411189c 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -180,8 +180,8 @@ public JabRefFrame(Stage mainStage) { this.dialogService = new JabRefDialogService(mainStage, this, prefs); this.stateManager = Globals.stateManager; this.pushToApplicationsManager = new PushToApplicationsManager(dialogService, stateManager, prefs); - this.globalSearchBar = new GlobalSearchBar(this, stateManager, prefs); this.undoManager = Globals.undoManager; + this.globalSearchBar = new GlobalSearchBar(this, stateManager, prefs, undoManager); this.fileHistory = new FileHistoryMenu(prefs, dialogService, getOpenDatabaseAction()); this.taskExecutor = Globals.TASK_EXECUTOR; this.setOnKeyTyped(key -> { @@ -594,8 +594,14 @@ public void init() { // Subscribe to the search EasyBind.subscribe(stateManager.activeSearchQueryProperty(), query -> { - if (getCurrentLibraryTab() != null) { - getCurrentLibraryTab().setCurrentSearchQuery(query); + if (prefs.getSearchPreferences().isKeepSearchString()) { + for (LibraryTab tab : getLibraryTabs()) { + tab.setCurrentSearchQuery(query); + } + } else { + if (getCurrentLibraryTab() != null) { + getCurrentLibraryTab().setCurrentSearchQuery(query); + } } }); diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index c098d12e30b..ff5b3ba05fd 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -1,6 +1,5 @@ package org.jabref.gui; -import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; @@ -109,7 +108,7 @@ public class LibraryTab extends Tab { // initializing it so we prevent NullPointerException private BackgroundTask dataLoadingTask = BackgroundTask.wrap(() -> null); - private IndexingTaskManager indexingTaskManager = new IndexingTaskManager(Globals.TASK_EXECUTOR); + private final IndexingTaskManager indexingTaskManager = new IndexingTaskManager(Globals.TASK_EXECUTOR); public LibraryTab(JabRefFrame frame, PreferencesService preferencesService, @@ -311,17 +310,9 @@ public void updateTabTitle(boolean isChanged) { } // Unique path fragment - List uniquePathParts = FileUtil.uniquePathSubstrings(collectAllDatabasePaths()); - Optional uniquePathPart = uniquePathParts.stream() - .filter(part -> databasePath.toString().contains(part) - && !part.equals(fileName) && part.contains(File.separator)) - .findFirst(); - if (uniquePathPart.isPresent()) { - String uniquePath = uniquePathPart.get(); - // remove filename - uniquePath = uniquePath.substring(0, uniquePath.lastIndexOf(File.separator)); - tabTitle.append(" \u2013 ").append(uniquePath); - } + Optional uniquePathPart = FileUtil.getUniquePathFragment(collectAllDatabasePaths(), databasePath); + uniquePathPart.ifPresent(part -> tabTitle.append(" \u2013 ").append(part)); + } else { if (databaseLocation == DatabaseLocation.LOCAL) { tabTitle.append(Localization.lang("untitled")); diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index f1ffee17247..fb6afbd5a30 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -52,7 +52,7 @@ public class StateManager { private final OptionalObjectProperty activeSearchQuery = OptionalObjectProperty.empty(); private final ObservableMap searchResultMap = FXCollections.observableHashMap(); private final OptionalObjectProperty focusOwner = OptionalObjectProperty.empty(); - private final ObservableList> backgroundTasks = FXCollections.observableArrayList(task -> new Observable[]{task.progressProperty(), task.runningProperty()}); + private final ObservableList> backgroundTasks = FXCollections.observableArrayList(task -> new Observable[] {task.progressProperty(), task.runningProperty()}); private final EasyBinding anyTaskRunning = EasyBind.reduce(backgroundTasks, tasks -> tasks.anyMatch(Task::isRunning)); private final EasyBinding tasksProgress = EasyBind.reduce(backgroundTasks, tasks -> tasks.filter(Task::isRunning).mapToDouble(Task::getProgress).average().orElse(1)); private final ObservableMap dialogWindowStates = FXCollections.observableHashMap(); diff --git a/src/main/java/org/jabref/gui/icon/IconTheme.java b/src/main/java/org/jabref/gui/icon/IconTheme.java index b00e81d1b27..cbaa6fac7d8 100644 --- a/src/main/java/org/jabref/gui/icon/IconTheme.java +++ b/src/main/java/org/jabref/gui/icon/IconTheme.java @@ -335,7 +335,11 @@ public enum JabRefIcons implements JabRefIcon { LINK(MaterialDesignL.LINK), LINK_VARIANT(MaterialDesignL.LINK_VARIANT), PROTECT_STRING(MaterialDesignC.CODE_BRACES), - SELECT_ICONS(MaterialDesignA.APPS); + SELECT_ICONS(MaterialDesignA.APPS), + KEEP_SEARCH_STRING(MaterialDesignE.EARTH), + KEEP_ON_TOP(MaterialDesignP.PIN), + KEEP_ON_TOP_OFF(MaterialDesignP.PIN_OFF_OUTLINE), + OPEN_GLOBAL_SEARCH(MaterialDesignO.OPEN_IN_NEW); private final JabRefIcon icon; diff --git a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java index bf335b05194..2a7e6f176c8 100644 --- a/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java +++ b/src/main/java/org/jabref/gui/maintable/MainTableDataModel.java @@ -54,7 +54,6 @@ public MainTableDataModel(BibDatabaseContext context, PreferencesService prefere // We need to wrap the list since otherwise sorting in the table does not work entriesSorted = new SortedList<>(entriesFiltered); groupViewMode = preferencesService.getGroupViewMode(); - } private boolean isMatched(ObservableList groups, Optional query, BibEntryTableViewModel entry) { diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java index 79e0b2f1184..b1a7ab4990c 100644 --- a/src/main/java/org/jabref/gui/search/GlobalSearchBar.java +++ b/src/main/java/org/jabref/gui/search/GlobalSearchBar.java @@ -6,15 +6,21 @@ import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; +import javax.swing.undo.UndoManager; + import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.css.PseudoClass; import javafx.event.Event; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; import javafx.scene.Node; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonBase; import javafx.scene.control.ContentDisplay; import javafx.scene.control.Label; import javafx.scene.control.ListView; @@ -40,10 +46,12 @@ import org.jabref.gui.autocompleter.AutoCompletionTextInputBinding; import org.jabref.gui.autocompleter.PersonNameStringConverter; import org.jabref.gui.autocompleter.SuggestionProvider; +import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.search.rules.describer.SearchDescribers; +import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.util.BindingsHelper; import org.jabref.gui.util.DefaultTaskExecutor; import org.jabref.gui.util.IconValidationDecorator; @@ -54,7 +62,6 @@ import org.jabref.preferences.PreferencesService; import org.jabref.preferences.SearchPreferences; -import com.tobiasdiez.easybind.EasyBind; import de.saxsys.mvvmfx.utils.validation.FunctionBasedValidator; import de.saxsys.mvvmfx.utils.validation.ValidationMessage; import de.saxsys.mvvmfx.utils.validation.Validator; @@ -81,6 +88,8 @@ public class GlobalSearchBar extends HBox { private final ToggleButton caseSensitiveButton; private final ToggleButton regularExpressionButton; private final ToggleButton fulltextButton; + private final Button openGlobalSearchButton; + private final ToggleButton keepSearchString; // private final Button searchModeButton; private final Tooltip searchFieldTooltip = new Tooltip(); private final Label currentResults = new Label(""); @@ -88,16 +97,21 @@ public class GlobalSearchBar extends HBox { private final StateManager stateManager; private final PreferencesService preferencesService; private final Validator regexValidator; + private final UndoManager undoManager; private SearchPreferences searchPreferences; - public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, PreferencesService preferencesService) { + private final BooleanProperty globalSearchActive = new SimpleBooleanProperty(false); + private GlobalSearchResultDialog globalSearchResultDialog; + + public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, PreferencesService preferencesService, CountingUndoManager undoManager) { super(); this.stateManager = stateManager; this.preferencesService = preferencesService; this.searchPreferences = preferencesService.getSearchPreferences(); + this.undoManager = undoManager; - this.searchField.disableProperty().bind(needsDatabase(stateManager).not()); + searchField.disableProperty().bind(needsDatabase(stateManager).not()); // fits the standard "found x entries"-message thus hinders the searchbar to jump around while searching if the frame width is too small currentResults.setPrefWidth(150); @@ -125,13 +139,16 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences regularExpressionButton = IconTheme.JabRefIcons.REG_EX.asToggleButton(); caseSensitiveButton = IconTheme.JabRefIcons.CASE_SENSITIVE.asToggleButton(); fulltextButton = IconTheme.JabRefIcons.FULLTEXT.asToggleButton(); - // searchModeButton = new Button(); + openGlobalSearchButton = IconTheme.JabRefIcons.OPEN_GLOBAL_SEARCH.asButton(); + keepSearchString = IconTheme.JabRefIcons.KEEP_SEARCH_STRING.asToggleButton(); + initSearchModifierButtons(); BooleanBinding focusedOrActive = searchField.focusedProperty() .or(regularExpressionButton.focusedProperty()) .or(caseSensitiveButton.focusedProperty()) .or(fulltextButton.focusedProperty()) + .or(keepSearchString.focusedProperty()) .or(searchField.textProperty() .isNotEmpty()); @@ -141,8 +158,10 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences caseSensitiveButton.visibleProperty().bind(focusedOrActive); fulltextButton.visibleProperty().unbind(); fulltextButton.visibleProperty().bind(focusedOrActive); + keepSearchString.visibleProperty().unbind(); + keepSearchString.visibleProperty().bind(focusedOrActive); - StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton)); + StackPane modifierButtons = new StackPane(new HBox(regularExpressionButton, caseSensitiveButton, fulltextButton, keepSearchString)); modifierButtons.setAlignment(Pos.CENTER); searchField.setRight(new HBox(searchField.getRight(), modifierButtons)); searchField.getStyleClass().add("search-field"); @@ -152,13 +171,12 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences regexValidator = new FunctionBasedValidator<>( searchField.textProperty(), query -> !(regularExpressionButton.isSelected() && !validRegex()), - ValidationMessage.error(Localization.lang("Invalid regular expression")) - ); + ValidationMessage.error(Localization.lang("Invalid regular expression"))); ControlsFxVisualizer visualizer = new ControlsFxVisualizer(); visualizer.setDecoration(new IconValidationDecorator(Pos.CENTER_LEFT)); Platform.runLater(() -> visualizer.initVisualization(regexValidator.getValidationStatus(), searchField)); - this.getChildren().addAll(searchField, currentResults); + this.getChildren().addAll(searchField, openGlobalSearchButton, currentResults); this.setSpacing(4.0); this.setAlignment(Pos.CENTER_LEFT); @@ -170,15 +188,18 @@ public GlobalSearchBar(JabRefFrame frame, StateManager stateManager, Preferences // Async update searchTask.restart(); }, - query -> setSearchTerm(query.map(SearchQuery::getQuery).orElse("")) - ); + query -> setSearchTerm(query.map(SearchQuery::getQuery).orElse(""))); - EasyBind.subscribe(this.stateManager.activeSearchQueryProperty(), searchQuery -> { - searchQuery.ifPresent(query -> { - updateResults(this.stateManager.getSearchResultSize().intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), - query.isGrammarBasedSearch()); - }); - }); + this.stateManager.activeSearchQueryProperty().addListener((obs, oldvalue, newValue) -> newValue.ifPresent(this::updateSearchResultsForQuery)); + this.stateManager.activeDatabaseProperty().addListener((obs, oldValue, newValue) -> stateManager.activeSearchQueryProperty().get().ifPresent(this::updateSearchResultsForQuery)); + } + + private void updateSearchResultsForQuery(SearchQuery query) { + updateResults(this.stateManager.getSearchResultSize().intValue(), SearchDescribers.getSearchDescriberFor(query).getDescription(), + query.isGrammarBasedSearch()); + if ((globalSearchResultDialog != null) && globalSearchActive.getValue()) { + globalSearchResultDialog.updateSearch(); + } } private void initSearchModifierButtons() { @@ -209,25 +230,29 @@ private void initSearchModifierButtons() { performSearch(); }); - // ToDo: Reimplement searchMode (searchModeButton) - /* searchModeButton.setText(searchPreferences.getSearchDisplayMode().getDisplayName()); - searchModeButton.setTooltip(new Tooltip(searchPreferences.getSearchDisplayMode().getToolTipText())); - searchModeButton.setOnAction(event -> { - SearchDisplayMode searchDisplayMode = searchPreferences.getSearchDisplayMode(); - int nextSearchMode = (searchDisplayMode.ordinal() + 1) % SearchDisplayMode.values().length; - searchDisplayMode = SearchDisplayMode.values()[nextSearchMode]; - - searchPreferences = searchPreferences..withSearchDisplayMode(searchDisplayMode); + keepSearchString.setSelected(searchPreferences.isKeepSearchString()); + keepSearchString.setTooltip(new Tooltip(Localization.lang("Keep search string across libraries"))); + initSearchModifierButton(keepSearchString); + keepSearchString.setOnAction(evt -> { + searchPreferences = searchPreferences.withKeepSearchString(keepSearchString.isSelected()); preferencesService.storeSearchPreferences(searchPreferences); + performSearch(); + }); - searchModeButton.setText(searchDisplayMode.getDisplayName()); - searchModeButton.setTooltip(new Tooltip(searchDisplayMode.getToolTipText())); - + openGlobalSearchButton.disableProperty().bindBidirectional(globalSearchActive); + openGlobalSearchButton.setTooltip(new Tooltip(Localization.lang("Search across libraries in a new window"))); + initSearchModifierButton(openGlobalSearchButton); + openGlobalSearchButton.setOnAction(evt -> { + globalSearchActive.setValue(true); + globalSearchResultDialog = new GlobalSearchResultDialog(ExternalFileTypes.getInstance(), undoManager); performSearch(); - }); */ + globalSearchResultDialog.updateSearch(); + globalSearchResultDialog.showAndWait(); + globalSearchActive.setValue(false); + }); } - private void initSearchModifierButton(ToggleButton searchButton) { + private void initSearchModifierButton(ButtonBase searchButton) { searchButton.setCursor(Cursor.DEFAULT); searchButton.setMinHeight(28); searchButton.setMaxHeight(28); diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml new file mode 100644 index 00000000000..ca098c167bc --- /dev/null +++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.fxml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java new file mode 100644 index 00000000000..e1a93283048 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialog.java @@ -0,0 +1,94 @@ +package org.jabref.gui.search; + +import javax.inject.Inject; +import javax.swing.undo.UndoManager; + +import javafx.fxml.FXML; +import javafx.scene.control.SplitPane; +import javafx.scene.control.ToggleButton; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.maintable.BibEntryTableViewModel; +import org.jabref.gui.maintable.MainTableColumnModel; +import org.jabref.gui.maintable.MainTableDataModel; +import org.jabref.gui.maintable.columns.FieldColumn; +import org.jabref.gui.maintable.columns.SpecialFieldColumn; +import org.jabref.gui.preview.PreviewViewer; +import org.jabref.gui.util.BaseDialog; +import org.jabref.gui.util.ValueTableCellFactory; +import org.jabref.logic.util.io.FileUtil; +import org.jabref.preferences.PreferencesService; + +import com.airhacks.afterburner.views.ViewLoader; +import com.tobiasdiez.easybind.EasyBind; + +public class GlobalSearchResultDialog extends BaseDialog { + + @FXML private SplitPane container; + @FXML private ToggleButton keepOnTop; + + private final ExternalFileTypes externalFileTypes; + private final UndoManager undoManager; + + @Inject private PreferencesService preferencesService; + @Inject private StateManager stateManager; + @Inject private DialogService dialogService; + + private GlobalSearchResultDialogViewModel viewModel; + + public GlobalSearchResultDialog(ExternalFileTypes externalFileTypes, UndoManager undoManager) { + this.undoManager = undoManager; + this.externalFileTypes = externalFileTypes; + + ViewLoader.view(this) + .load() + .setAsDialogPane(this); + initModality(Modality.NONE); + } + + @FXML + private void initialize() { + viewModel = new GlobalSearchResultDialogViewModel(stateManager, preferencesService); + + PreviewViewer previewViewer = new PreviewViewer(viewModel.getSearchDatabaseContext(), dialogService, stateManager); + previewViewer.setTheme(preferencesService.getTheme()); + previewViewer.setLayout(preferencesService.getPreviewPreferences().getCurrentPreviewStyle()); + + FieldColumn fieldColumn = new FieldColumn(MainTableColumnModel.parse("field:library")); + new ValueTableCellFactory().withText(FileUtil::getBaseName) + .install(fieldColumn); + + MainTableDataModel model = new MainTableDataModel(viewModel.getSearchDatabaseContext(), preferencesService, stateManager); + SearchResultsTable resultsTable = new SearchResultsTable(model, viewModel.getSearchDatabaseContext(), preferencesService, undoManager, dialogService, stateManager, externalFileTypes); + resultsTable.getColumns().add(0, fieldColumn); + resultsTable.getColumns().removeIf(col -> col instanceof SpecialFieldColumn); + resultsTable.getSelectionModel().selectFirst(); + resultsTable.getSelectionModel().selectedItemProperty().addListener((obs, old, newValue) -> { + if (newValue != null) { + previewViewer.setEntry(newValue.getEntry()); + } else { + previewViewer.setEntry(old.getEntry()); + } + }); + + container.getItems().addAll(resultsTable, previewViewer); + + keepOnTop.selectedProperty().bindBidirectional(viewModel.keepOnTop()); + EasyBind.subscribe(viewModel.keepOnTop(), value -> { + Stage stage = (Stage) getDialogPane().getScene().getWindow(); + stage.setAlwaysOnTop(value); + keepOnTop.setGraphic(value + ? IconTheme.JabRefIcons.KEEP_ON_TOP.getGraphicNode() + : IconTheme.JabRefIcons.KEEP_ON_TOP_OFF.getGraphicNode()); + }); + } + + public void updateSearch() { + viewModel.updateSearch(); + } +} diff --git a/src/main/java/org/jabref/gui/search/GlobalSearchResultDialogViewModel.java b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialogViewModel.java new file mode 100644 index 00000000000..fab8dd5ba16 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/GlobalSearchResultDialogViewModel.java @@ -0,0 +1,67 @@ +package org.jabref.gui.search; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; + +import org.jabref.gui.StateManager; +import org.jabref.logic.search.SearchQuery; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.preferences.PreferencesService; +import org.jabref.preferences.SearchPreferences; + +import com.tobiasdiez.easybind.EasyBind; + +public class GlobalSearchResultDialogViewModel { + + private final StateManager stateManager; + private final BibDatabaseContext searchDatabaseContext = new BibDatabaseContext(); + private final BooleanProperty keepOnTop = new SimpleBooleanProperty(); + private SearchPreferences searchPreferences; + + public GlobalSearchResultDialogViewModel(StateManager stateManager, PreferencesService preferencesService) { + this.stateManager = stateManager; + searchPreferences = preferencesService.getSearchPreferences(); + + keepOnTop.set(searchPreferences.isKeepWindowOnTop()); + + EasyBind.subscribe(this.keepOnTop, selected -> { + searchPreferences = searchPreferences.withKeepGlobalSearchDialogOnTop(selected); + preferencesService.storeSearchPreferences(searchPreferences); + }); + } + + public void updateSearch() { + BibDatabaseContext resultContext = new BibDatabaseContext(); + for (BibDatabaseContext dbContext : this.stateManager.getOpenDatabases()) { + List result = dbContext.getEntries().stream() + .filter(entry -> isMatchedBySearch(stateManager.activeSearchQueryProperty().get(), entry)) + .collect(Collectors.toList()); + resultContext.getDatabase().insertEntries(result); + } + this.addEntriesToBibContext(resultContext); + } + + private void addEntriesToBibContext(BibDatabaseContext context) { + List toBeRemoved = this.searchDatabaseContext.getDatabase().getEntries(); + this.searchDatabaseContext.getDatabase().removeEntries(toBeRemoved); + this.searchDatabaseContext.getDatabase().insertEntries(context.getEntries()); + } + + private boolean isMatchedBySearch(Optional query, BibEntry entry) { + return query.map(matcher -> matcher.isMatch(entry)) + .orElse(true); + } + + public BibDatabaseContext getSearchDatabaseContext() { + return searchDatabaseContext; + } + + public BooleanProperty keepOnTop() { + return this.keepOnTop; + } +} diff --git a/src/main/java/org/jabref/gui/search/SearchResultsTable.java b/src/main/java/org/jabref/gui/search/SearchResultsTable.java new file mode 100644 index 00000000000..e601a85d548 --- /dev/null +++ b/src/main/java/org/jabref/gui/search/SearchResultsTable.java @@ -0,0 +1,63 @@ +package org.jabref.gui.search; + +import javax.swing.undo.UndoManager; + +import javafx.scene.control.SelectionMode; +import javafx.scene.control.TableView; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.maintable.BibEntryTableViewModel; +import org.jabref.gui.maintable.MainTable; +import org.jabref.gui.maintable.MainTableColumnFactory; +import org.jabref.gui.maintable.MainTableDataModel; +import org.jabref.gui.maintable.MainTablePreferences; +import org.jabref.gui.maintable.SmartConstrainedResizePolicy; +import org.jabref.gui.maintable.columns.MainTableColumn; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.preferences.PreferencesService; + +public class SearchResultsTable extends TableView { + + public SearchResultsTable(MainTableDataModel model, + BibDatabaseContext database, + PreferencesService preferencesService, + UndoManager undoManager, + DialogService dialogService, + StateManager stateManager, + ExternalFileTypes externalFileTypes) { + super(); + + MainTablePreferences mainTablePreferences = preferencesService.getMainTablePreferences(); + + this.getColumns().addAll(new MainTableColumnFactory( + database, + preferencesService, + externalFileTypes, + undoManager, + dialogService, + stateManager).createColumns()); + + this.getSortOrder().clear(); + mainTablePreferences.getColumnPreferences().getColumnSortOrder().forEach(columnModel -> + this.getColumns().stream() + .map(column -> (MainTableColumn) column) + .filter(column -> column.getModel().equals(columnModel)) + .findFirst() + .ifPresent(column -> this.getSortOrder().add(column))); + + if (mainTablePreferences.getResizeColumnsToFit()) { + this.setColumnResizePolicy(new SmartConstrainedResizePolicy()); + } + + this.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + this.setItems(model.getEntriesFilteredAndSorted()); + // Enable sorting + model.getEntriesFilteredAndSorted().comparatorProperty().bind(this.comparatorProperty()); + + this.getStylesheets().add(MainTable.class.getResource("MainTable.css").toExternalForm()); + database.getDatabase().registerListener(this); + } +} + diff --git a/src/main/java/org/jabref/logic/util/io/FileUtil.java b/src/main/java/org/jabref/logic/util/io/FileUtil.java index bb0bd6e7bf1..29279cc2731 100644 --- a/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -106,6 +106,18 @@ public static Path addExtension(Path path, String extension) { return path.resolveSibling(path.getFileName() + extension); } + public static Optional getUniquePathFragment(List paths, Path databasePath) { + + String fileName = databasePath.getFileName().toString(); + + List uniquePathParts = uniquePathSubstrings(paths); + return uniquePathParts.stream() + .filter(part -> databasePath.toString().contains(part) + && !part.equals(fileName) && part.contains(File.separator)) + .findFirst() + .map(part -> part.substring(0, part.lastIndexOf(File.separator))); + } + /** * Creates the minimal unique path substring for each file among multiple file paths. * 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 9b6ffb51b77..614c353f834 100644 --- a/src/main/java/org/jabref/model/search/rules/SearchRules.java +++ b/src/main/java/org/jabref/model/search/rules/SearchRules.java @@ -41,6 +41,6 @@ static SearchRule getSearchRule(EnumSet searchFlags) { } public enum SearchFlags { - CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT; + CASE_SENSITIVE, REGULAR_EXPRESSION, FULLTEXT, KEEP_SEARCH_STRING; } } diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 64cffc8c800..325898a86c8 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -234,6 +234,8 @@ public class JabRefPreferences implements PreferencesService { public static final String SEARCH_CASE_SENSITIVE = "caseSensitiveSearch"; public static final String SEARCH_REG_EXP = "regExpSearch"; public static final String SEARCH_FULLTEXT = "fulltextSearch"; + public static final String SEARCH_KEEP_SEARCH_STRING = "keepSearchString"; + public static final String SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP = "keepOnTop"; public static final String GENERATE_KEY_ON_IMPORT = "generateKeyOnImport"; public static final String GROBID_ENABLED = "grobidEnabled"; @@ -452,6 +454,8 @@ private JabRefPreferences() { defaults.put(SEARCH_CASE_SENSITIVE, Boolean.FALSE); defaults.put(SEARCH_REG_EXP, Boolean.FALSE); defaults.put(SEARCH_FULLTEXT, Boolean.TRUE); + defaults.put(SEARCH_KEEP_SEARCH_STRING, Boolean.FALSE); + defaults.put(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP, Boolean.TRUE); defaults.put(GENERATE_KEY_ON_IMPORT, Boolean.TRUE); defaults.put(GROBID_ENABLED, Boolean.FALSE); @@ -2568,7 +2572,9 @@ public SearchPreferences getSearchPreferences() { searchDisplayMode, getBoolean(SEARCH_CASE_SENSITIVE), getBoolean(SEARCH_REG_EXP), - getBoolean(SEARCH_FULLTEXT)); + getBoolean(SEARCH_FULLTEXT), + getBoolean(SEARCH_KEEP_SEARCH_STRING), + getBoolean(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP)); } @Override @@ -2577,6 +2583,8 @@ public void storeSearchPreferences(SearchPreferences preferences) { putBoolean(SEARCH_CASE_SENSITIVE, preferences.isCaseSensitive()); putBoolean(SEARCH_REG_EXP, preferences.isRegularExpression()); putBoolean(SEARCH_FULLTEXT, preferences.isFulltext()); + putBoolean(SEARCH_KEEP_SEARCH_STRING, preferences.isKeepSearchString()); + putBoolean(SEARCH_KEEP_GLOBAL_WINDOW_ON_TOP, preferences.isKeepWindowOnTop()); } //************************************************************************************************************* diff --git a/src/main/java/org/jabref/preferences/SearchPreferences.java b/src/main/java/org/jabref/preferences/SearchPreferences.java index 2674c0e897f..036677de5b8 100644 --- a/src/main/java/org/jabref/preferences/SearchPreferences.java +++ b/src/main/java/org/jabref/preferences/SearchPreferences.java @@ -10,9 +10,11 @@ public class SearchPreferences { private final SearchDisplayMode searchDisplayMode; private final EnumSet searchFlags; + private final boolean keepWindowOnTop; - public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext) { + public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSensitive, boolean isRegularExpression, boolean isFulltext, boolean isKeepSearchString, boolean keepWindowOnTop) { this.searchDisplayMode = searchDisplayMode; + this.keepWindowOnTop = keepWindowOnTop; searchFlags = EnumSet.noneOf(SearchFlags.class); if (isCaseSensitive) { searchFlags.add(SearchFlags.CASE_SENSITIVE); @@ -23,11 +25,15 @@ public SearchPreferences(SearchDisplayMode searchDisplayMode, boolean isCaseSens if (isFulltext) { searchFlags.add(SearchFlags.FULLTEXT); } + if (isKeepSearchString) { + searchFlags.add(SearchFlags.KEEP_SEARCH_STRING); + } } - public SearchPreferences(SearchDisplayMode searchDisplayMode, EnumSet searchFlags) { + public SearchPreferences(SearchDisplayMode searchDisplayMode, EnumSet searchFlags, boolean keepWindowOnTop) { this.searchDisplayMode = searchDisplayMode; this.searchFlags = searchFlags; + this.keepWindowOnTop = keepWindowOnTop; } public SearchDisplayMode getSearchDisplayMode() { @@ -46,6 +52,14 @@ public boolean isFulltext() { return searchFlags.contains(SearchFlags.FULLTEXT); } + public boolean isKeepSearchString() { + return searchFlags.contains(SearchFlags.KEEP_SEARCH_STRING); + } + + public boolean isKeepWindowOnTop() { + return keepWindowOnTop; + } + public EnumSet getSearchFlags() { EnumSet searchFlags = EnumSet.noneOf(SearchFlags.class); if (isCaseSensitive()) { @@ -57,22 +71,33 @@ public EnumSet getSearchFlags() { if (isFulltext()) { searchFlags.add(SearchRules.SearchFlags.FULLTEXT); } + if (isKeepSearchString()) { + searchFlags.add(SearchRules.SearchFlags.KEEP_SEARCH_STRING); + } return searchFlags; } public SearchPreferences withSearchDisplayMode(SearchDisplayMode newSearchDisplayMode) { - return new SearchPreferences(newSearchDisplayMode, isCaseSensitive(), isRegularExpression(), isFulltext()); + return new SearchPreferences(newSearchDisplayMode, isCaseSensitive(), isRegularExpression(), isFulltext(), isKeepSearchString(), isKeepWindowOnTop()); } public SearchPreferences withCaseSensitive(boolean newCaseSensitive) { - return new SearchPreferences(searchDisplayMode, newCaseSensitive, isRegularExpression(), isFulltext()); + return new SearchPreferences(searchDisplayMode, newCaseSensitive, isRegularExpression(), isFulltext(), isKeepSearchString(), isKeepWindowOnTop()); } public SearchPreferences withRegularExpression(boolean newRegularExpression) { - return new SearchPreferences(searchDisplayMode, isCaseSensitive(), newRegularExpression, isFulltext()); + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), newRegularExpression, isFulltext(), isKeepSearchString(), isKeepWindowOnTop()); } public SearchPreferences withFulltext(boolean newFulltext) { - return new SearchPreferences(searchDisplayMode, isCaseSensitive(), isRegularExpression(), newFulltext); + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), isRegularExpression(), newFulltext, isKeepSearchString(), isKeepWindowOnTop()); + } + + public SearchPreferences withKeepSearchString(boolean newKeepSearchString) { + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), isRegularExpression(), isFulltext(), newKeepSearchString, isKeepWindowOnTop()); + } + + public SearchPreferences withKeepGlobalSearchDialogOnTop(boolean keepWindowOnTop) { + return new SearchPreferences(searchDisplayMode, isCaseSensitive(), isRegularExpression(), isFulltext(), isKeepSearchString(), keepWindowOnTop); } } diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index f05e23b1e4b..c6c3ea4083e 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2382,3 +2382,7 @@ Symmetric\ word\ by\ word=Symmetric word by word Verbatim=Verbatim Word\ by\ word=Word by word Could\ not\ extract\ Metadata\ from\:\ %0=Could not extract Metadata from: %0 + +Search\ across\ libraries\ in\ a\ new\ window=Search across libraries in a new window +Keep\ search\ string\ across\ libraries=Keep search string across libraries +Keep\ dialog\ always\ on\ top=Keep dialog always on top