diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d88cebc9e..300f712383b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We reworked the export order in the preferences and the save order in the library preferences. You can now set more than three sort criteria in your library preferences. [#7935](https://github.com/JabRef/jabref/pull/7935) - The metadata-to-pdf actions now also embeds the bibfile to the PDF. [#8037](https://github.com/JabRef/jabref/pull/8037) - The snap was updated to use the core20 base and to use lzo compression for better startup performance [#8109](https://github.com/JabRef/jabref/pull/8109) +- We moved the union/intersection view button in the group sidepane to the left of the other controls. [#8202](https://github.com/JabRef/jabref/pull/8202) - We improved the Drag and Drop behavior in the "Customize Entry Types" Dialog [#6338](https://github.com/JabRef/jabref/issues/6338) - When determining the URL of an ArXiV eprint, the URL now points to the version [#8149](https://github.com/JabRef/jabref/pull/8149) - We Included all standard fields with citation key when exporting to Old OpenOffice/LibreOffice Calc Format [#8176](https://github.com/JabRef/jabref/pull/8176) @@ -61,6 +62,8 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We fixed some icons that were drawn in the wrong color when JabRef used a custom theme. [#7853](https://github.com/JabRef/jabref/issues/7853) - We fixed an issue where the `Aux file` on `Edit group` doesn't support relative sub-directories path to import. [#7719](https://github.com/JabRef/jabref/issues/7719). - We fixed an issue where it was impossible to add or modify groups. [#7912](https://github.com/JabRef/jabref/pull/793://github.com/JabRef/jabref/pull/7921) +- We fixed an issue about the visible side pane components being out of sync with the view menu. [#8115](https://github.com/JabRef/jabref/issues/8115) +- We fixed an issue where the side pane would not close when all its components were closed. [#8082]((https://github.com/JabRef/jabref/issues/8082)) - We fixed an issue where exported entries from a Citavi bib containing URLs could not be imported [#7892](https://github.com/JabRef/jabref/issues/7882) - We fixed an issue where the icons in the search bar had the same color, toggled as well as untoggled. [#8014](https://github.com/JabRef/jabref/pull/8014) - We fixed an issue where typing an invalid UNC path into the "Main file directory" text field caused an error. [#8107](https://github.com/JabRef/jabref/issues/8107) @@ -73,6 +76,7 @@ Note that this project **does not** adhere to [Semantic Versioning](http://semve - We fixed an issue where selecting a citation style in the preferences would sometimes produce an exception [#7860](https://github.com/JabRef/jabref/issues/7860) - We fixed an issue where an exception would occur when clicking on a DOI link in the preview pane [#7706](https://github.com/JabRef/jabref/issues/7706) + ### Removed - We removed two orphaned preferences options [#8164](https://github.com/JabRef/jabref/pull/8164) diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index edb26429e3b..2a89967fbdd 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -12,10 +12,10 @@ import java.util.stream.Collectors; import javafx.application.Platform; +import javafx.beans.InvalidationListener; +import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.StringBinding; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.collections.transformation.FilteredList; import javafx.concurrent.Task; import javafx.geometry.Orientation; @@ -109,7 +109,6 @@ import org.jabref.gui.shared.ConnectToSharedDatabaseCommand; import org.jabref.gui.shared.PullChangesFromSharedAction; import org.jabref.gui.sidepane.SidePane; -import org.jabref.gui.sidepane.SidePaneComponent; import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.slr.ExistingStudySearchAction; import org.jabref.gui.slr.StartNewStudyAction; @@ -143,6 +142,7 @@ import com.google.common.eventbus.Subscribe; import com.tobiasdiez.easybind.EasyBind; import com.tobiasdiez.easybind.EasyObservableList; +import com.tobiasdiez.easybind.Subscription; import org.controlsfx.control.PopOver; import org.controlsfx.control.TaskProgressView; import org.fxmisc.richtext.CodeArea; @@ -176,6 +176,8 @@ public class JabRefFrame extends BorderPane { private PopOver progressViewPopOver; private PopOver entryFromIdPopOver; + private Subscription dividerSubscription; + private final TaskExecutor taskExecutor; public JabRefFrame(Stage mainStage) { @@ -341,7 +343,7 @@ public void about() { * FIXME: Currently some threads remain and therefore hinder JabRef to be closed properly * * @param filenames the filenames of all currently opened files - used for storing them if prefs openLastEdited is - * set to true + * set to true */ private void tearDownJabRef(List filenames) { if (prefs.getGuiPreferences().shouldOpenLastEdited()) { @@ -433,29 +435,18 @@ private void initLayout() { head.setSpacing(0d); setTop(head); - splitPane.getItems().addAll(sidePane, tabbedPane); + splitPane.getItems().addAll(tabbedPane); SplitPane.setResizableWithParent(sidePane, false); - // We need to wait with setting the divider since it gets reset a few times during the initial set-up - mainStage.showingProperty().addListener(new ChangeListener<>() { + sidePane.getChildren().addListener((InvalidationListener) c -> updateSidePane()); + updateSidePane(); + // We need to wait with setting the divider since it gets reset a few times during the initial set-up + mainStage.showingProperty().addListener(new InvalidationListener() { @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean showing) { - if (showing) { + public void invalidated(Observable observable) { + if (mainStage.isShowing()) { setDividerPosition(); - - EasyBind.subscribe(sidePane.visibleProperty(), visible -> { - if (visible) { - if (!splitPane.getItems().contains(sidePane)) { - splitPane.getItems().add(0, sidePane); - setDividerPosition(); - } - } else { - splitPane.getItems().remove(sidePane); - } - }); - - mainStage.showingProperty().removeListener(this); observable.removeListener(this); } } @@ -464,10 +455,24 @@ public void changed(ObservableValue observable, Boolean oldVa setCenter(splitPane); } + private void updateSidePane() { + if (sidePane.getChildren().isEmpty()) { + if (dividerSubscription != null) { + dividerSubscription.unsubscribe(); + } + splitPane.getItems().remove(sidePane); + } else { + if (!splitPane.getItems().contains(sidePane)) { + splitPane.getItems().add(0, sidePane); + setDividerPosition(); + } + } + } + private void setDividerPosition() { splitPane.setDividerPositions(prefs.getGuiPreferences().getSidePaneWidth()); - if (!splitPane.getDividers().isEmpty()) { - EasyBind.subscribe(splitPane.getDividers().get(0).positionProperty(), + if (mainStage.isShowing() && !sidePane.getChildren().isEmpty()) { + dividerSubscription = EasyBind.subscribe(splitPane.getDividers().get(0).positionProperty(), position -> prefs.getGuiPreferences().setSidePaneWidth(position.doubleValue())); } } @@ -577,7 +582,6 @@ public void showLibraryTab(LibraryTab libraryTab) { public void init() { sidePane = new SidePane(prefs, taskExecutor, dialogService, stateManager, undoManager); - tabbedPane = new TabPane(); tabbedPane.setTabDragPolicy(TabPane.TabDragPolicy.REORDER); @@ -838,15 +842,13 @@ private MenuBar createMenu() { factory.createMenuItem(StandardActions.REBUILD_FULLTEXT_SEARCH_INDEX, new RebuildFulltextSearchIndexAction(stateManager, this::getCurrentLibraryTab, dialogService, prefs.getFilePreferences())) ); - - SidePaneComponent webSearch = sidePane.getComponent(SidePaneType.WEB_SEARCH); - SidePaneComponent groups = sidePane.getComponent(SidePaneType.GROUPS); - SidePaneComponent openOffice = sidePane.getComponent(SidePaneType.OPEN_OFFICE); - + SidePaneType webSearchPane = SidePaneType.WEB_SEARCH; + SidePaneType groupsPane = SidePaneType.GROUPS; + SidePaneType openOfficePane = SidePaneType.OPEN_OFFICE; view.getItems().addAll( - factory.createCheckMenuItem(webSearch.getToggleAction(), webSearch.getToggleCommand(), sidePane.isComponentVisible(SidePaneType.WEB_SEARCH)), - factory.createCheckMenuItem(groups.getToggleAction(), groups.getToggleCommand(), sidePane.isComponentVisible(SidePaneType.GROUPS)), - factory.createCheckMenuItem(openOffice.getToggleAction(), openOffice.getToggleCommand(), sidePane.isComponentVisible(SidePaneType.OPEN_OFFICE)), + factory.createCheckMenuItem(webSearchPane.getToggleAction(), sidePane.getToggleCommandFor(webSearchPane), sidePane.paneVisibleBinding(webSearchPane)), + factory.createCheckMenuItem(groupsPane.getToggleAction(), sidePane.getToggleCommandFor(groupsPane), sidePane.paneVisibleBinding(groupsPane)), + factory.createCheckMenuItem(openOfficePane.getToggleAction(), sidePane.getToggleCommandFor(openOfficePane), sidePane.paneVisibleBinding(openOfficePane)), new SeparatorMenuItem(), @@ -1111,7 +1113,7 @@ private boolean readyForAutosave(BibDatabaseContext context) { /** * Opens the import inspection dialog to let the user decide which of the given entries to import. * - * @param panel The BasePanel to add to. + * @param panel The BasePanel to add to. * @param parserResult The entries to add. */ private void addImportedEntries(final LibraryTab panel, final ParserResult parserResult) { @@ -1291,7 +1293,7 @@ public CloseDatabaseAction(LibraryTab libraryTab) { /** * Using this constructor will result in executing the command on the currently open library tab - * */ + */ public CloseDatabaseAction() { this(null); } diff --git a/src/main/java/org/jabref/gui/StateManager.java b/src/main/java/org/jabref/gui/StateManager.java index fb6afbd5a30..386e08779e9 100644 --- a/src/main/java/org/jabref/gui/StateManager.java +++ b/src/main/java/org/jabref/gui/StateManager.java @@ -17,6 +17,7 @@ import javafx.concurrent.Task; import javafx.scene.Node; +import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.util.CustomLocalDragboard; import org.jabref.gui.util.DialogWindowState; import org.jabref.gui.util.OptionalObjectProperty; @@ -56,11 +57,16 @@ public class StateManager { 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(); + private final ObservableList visibleSidePanes = FXCollections.observableArrayList(); public StateManager() { activeGroups.bind(Bindings.valueAt(selectedGroups, activeDatabase.orElse(null))); } + public ObservableList getVisibleSidePaneComponents() { + return visibleSidePanes; + } + public CustomLocalDragboard getLocalDragboard() { return localDragboard; } diff --git a/src/main/java/org/jabref/gui/actions/ActionFactory.java b/src/main/java/org/jabref/gui/actions/ActionFactory.java index 151a596b66c..7112ffc8f12 100644 --- a/src/main/java/org/jabref/gui/actions/ActionFactory.java +++ b/src/main/java/org/jabref/gui/actions/ActionFactory.java @@ -5,6 +5,7 @@ import java.lang.reflect.Method; import java.util.Objects; +import javafx.beans.binding.BooleanExpression; import javafx.scene.control.Button; import javafx.scene.control.ButtonBase; import javafx.scene.control.CheckMenuItem; @@ -113,6 +114,14 @@ public CheckMenuItem createCheckMenuItem(Action action, Command command, boolean return checkMenuItem; } + public CheckMenuItem createCheckMenuItem(Action action, Command command, BooleanExpression selectedBinding) { + CheckMenuItem checkMenuItem = ActionUtils.createCheckMenuItem(new JabRefAction(action, command, keyBindingRepository, Sources.FromMenu)); + EasyBind.subscribe(selectedBinding, checkMenuItem::setSelected); + setGraphic(checkMenuItem, action); + + return checkMenuItem; + } + public Menu createMenu(Action action) { Menu menu = ActionUtils.createMenu(new JabRefAction(action, keyBindingRepository)); diff --git a/src/main/java/org/jabref/gui/groups/GroupSidePane.java b/src/main/java/org/jabref/gui/groups/GroupSidePane.java deleted file mode 100644 index 86bc9cfb725..00000000000 --- a/src/main/java/org/jabref/gui/groups/GroupSidePane.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.jabref.gui.groups; - -import java.util.Collections; -import java.util.List; - -import javafx.scene.Node; -import javafx.scene.control.Button; -import javafx.scene.layout.Priority; - -import org.jabref.gui.DialogService; -import org.jabref.gui.StateManager; -import org.jabref.gui.actions.Action; -import org.jabref.gui.actions.StandardActions; -import org.jabref.gui.icon.IconTheme; -import org.jabref.gui.sidepane.SidePane; -import org.jabref.gui.sidepane.SidePaneComponent; -import org.jabref.gui.sidepane.SidePaneType; -import org.jabref.gui.util.TaskExecutor; -import org.jabref.logic.l10n.Localization; -import org.jabref.preferences.PreferencesService; - -/** - * The groups side pane. - */ -public class GroupSidePane extends SidePaneComponent { - - private final PreferencesService preferences; - private final DialogService dialogService; - private final TaskExecutor taskExecutor; - private final StateManager stateManager; - private final Button intersectionUnionToggle = IconTheme.JabRefIcons.GROUP_INTERSECTION.asButton(); - - public GroupSidePane(SidePane sidePane, TaskExecutor taskExecutor, StateManager stateManager, PreferencesService preferences, DialogService dialogService) { - super(sidePane, IconTheme.JabRefIcons.TOGGLE_GROUPS, Localization.lang("Groups")); - this.preferences = preferences; - this.taskExecutor = taskExecutor; - this.stateManager = stateManager; - this.dialogService = dialogService; - } - - @Override - protected List getAdditionalHeaderButtons() { - intersectionUnionToggle.setOnAction(event -> toggleUnionIntersection()); - return Collections.singletonList(intersectionUnionToggle); - } - - @Override - public Priority getResizePolicy() { - return Priority.ALWAYS; - } - - @Override - public void beforeClosing() { - preferences.getSidePanePreferences().visiblePanes().remove(SidePaneType.GROUPS); - } - - @Override - public void afterOpening() { - preferences.getSidePanePreferences().visiblePanes().add(SidePaneType.GROUPS); - setGraphicsAndTooltipForButton(preferences.getGroupViewMode()); - } - - @Override - public Action getToggleAction() { - return StandardActions.TOGGLE_GROUPS; - } - - private void toggleUnionIntersection() { - GroupViewMode mode = preferences.getGroupViewMode(); - - if (mode == GroupViewMode.UNION) { - preferences.setGroupViewMode(GroupViewMode.INTERSECTION); - dialogService.notify(Localization.lang("Group view mode set to intersection")); - } else if (mode == GroupViewMode.INTERSECTION) { - preferences.setGroupViewMode(GroupViewMode.UNION); - dialogService.notify(Localization.lang("Group view mode set to union")); - } - - setGraphicsAndTooltipForButton(mode); - } - - private void setGraphicsAndTooltipForButton(GroupViewMode mode) { - GroupModeViewModel modeViewModel = new GroupModeViewModel(mode); - intersectionUnionToggle.setGraphic(modeViewModel.getUnionIntersectionGraphic()); - intersectionUnionToggle.setTooltip(modeViewModel.getUnionIntersectionTooltip()); - } - - @Override - protected Node createContentPane() { - return new GroupTreeView(taskExecutor, stateManager, preferences, dialogService); - } - - @Override - public SidePaneType getType() { - return SidePaneType.GROUPS; - } -} diff --git a/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPane.java b/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java similarity index 71% rename from src/main/java/org/jabref/gui/importer/fetcher/WebSearchPane.java rename to src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java index ad7b69cd21c..fb4f9352850 100644 --- a/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPane.java +++ b/src/main/java/org/jabref/gui/importer/fetcher/WebSearchPaneView.java @@ -2,7 +2,6 @@ import javafx.css.PseudoClass; import javafx.geometry.Pos; -import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.TextField; @@ -15,15 +14,10 @@ import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; -import org.jabref.gui.actions.Action; import org.jabref.gui.actions.ActionFactory; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.help.HelpAction; -import org.jabref.gui.icon.IconTheme; import org.jabref.gui.search.SearchTextField; -import org.jabref.gui.sidepane.SidePane; -import org.jabref.gui.sidepane.SidePaneComponent; -import org.jabref.gui.sidepane.SidePaneType; import org.jabref.gui.util.ViewModelListCellFactory; import org.jabref.logic.importer.SearchBasedFetcher; import org.jabref.logic.l10n.Localization; @@ -31,27 +25,20 @@ import com.tobiasdiez.easybind.EasyBind; -public class WebSearchPane extends SidePaneComponent { +public class WebSearchPaneView extends VBox { private static final PseudoClass QUERY_INVALID = PseudoClass.getPseudoClass("invalid"); - private final PreferencesService preferences; private final WebSearchPaneViewModel viewModel; + private final PreferencesService preferences; - public WebSearchPane(SidePane sidePane, PreferencesService preferences, DialogService dialogService, StateManager stateManager) { - super(sidePane, IconTheme.JabRefIcons.WWW, Localization.lang("Web search")); + public WebSearchPaneView(PreferencesService preferences, DialogService dialogService, StateManager stateManager) { this.preferences = preferences; this.viewModel = new WebSearchPaneViewModel(preferences, dialogService, stateManager); + initialize(); } - @Override - public Action getToggleAction() { - return StandardActions.TOGGLE_WEB_SEARCH; - } - - @Override - protected Node createContentPane() { - // Setup combo box for fetchers + private void initialize() { ComboBox fetchers = new ComboBox<>(); new ViewModelListCellFactory() .withText(SearchBasedFetcher::getName) @@ -90,7 +77,7 @@ protected Node createContentPane() { } }); - // Allows to trigger search on pressing enter + // Allows triggering search on pressing enter query.setOnKeyPressed(event -> { if (event.getCode() == KeyCode.ENTER) { viewModel.search(); @@ -102,30 +89,7 @@ protected Node createContentPane() { search.setDefaultButton(false); search.setOnAction(event -> viewModel.search()); - // Put everything together - VBox container = new VBox(); - container.setAlignment(Pos.CENTER); - container.getChildren().addAll(fetcherContainer, query, search); - return container; - } - - @Override - public SidePaneType getType() { - return SidePaneType.WEB_SEARCH; - } - - @Override - public void beforeClosing() { - preferences.getSidePanePreferences().visiblePanes().remove(SidePaneType.WEB_SEARCH); - } - - @Override - public void afterOpening() { - preferences.getSidePanePreferences().visiblePanes().add(SidePaneType.WEB_SEARCH); - } - - @Override - public Priority getResizePolicy() { - return Priority.NEVER; + setAlignment(Pos.CENTER); + getChildren().addAll(fetcherContainer, query, search); } } diff --git a/src/main/java/org/jabref/gui/openoffice/OpenOfficeSidePanel.java b/src/main/java/org/jabref/gui/openoffice/OpenOfficeSidePanel.java deleted file mode 100644 index 569b5b78428..00000000000 --- a/src/main/java/org/jabref/gui/openoffice/OpenOfficeSidePanel.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.jabref.gui.openoffice; - -import javax.swing.undo.UndoManager; - -import javafx.scene.Node; -import javafx.scene.layout.Priority; - -import org.jabref.gui.DialogService; -import org.jabref.gui.StateManager; -import org.jabref.gui.actions.Action; -import org.jabref.gui.actions.StandardActions; -import org.jabref.gui.icon.IconTheme; -import org.jabref.gui.sidepane.SidePane; -import org.jabref.gui.sidepane.SidePaneComponent; -import org.jabref.gui.sidepane.SidePaneType; -import org.jabref.gui.util.TaskExecutor; -import org.jabref.logic.openoffice.OpenOfficePreferences; -import org.jabref.preferences.PreferencesService; - -public class OpenOfficeSidePanel extends SidePaneComponent { - - private final TaskExecutor taskExecutor; - private final PreferencesService preferencesService; - private final DialogService dialogService; - private final StateManager stateManager; - private final UndoManager undoManager; - private final OpenOfficePreferences ooPrefs; - - public OpenOfficeSidePanel(SidePane sidePane, - TaskExecutor taskExecutor, - PreferencesService preferencesService, - DialogService dialogService, - StateManager stateManager, - UndoManager undoManager) { - super(sidePane, IconTheme.JabRefIcons.FILE_OPENOFFICE, "OpenOffice/LibreOffice"); - this.taskExecutor = taskExecutor; - this.preferencesService = preferencesService; - this.dialogService = dialogService; - this.stateManager = stateManager; - this.undoManager = undoManager; - this.ooPrefs = preferencesService.getOpenOfficePreferences(); - } - - @Override - public void beforeClosing() { - preferencesService.getSidePanePreferences().visiblePanes().remove(SidePaneType.OPEN_OFFICE); - } - - @Override - public void afterOpening() { - preferencesService.getSidePanePreferences().visiblePanes().add(SidePaneType.OPEN_OFFICE); - } - - @Override - public Priority getResizePolicy() { - return Priority.NEVER; - } - - @Override - public Action getToggleAction() { - return StandardActions.TOOGLE_OO; - } - - @Override - protected Node createContentPane() { - return new OpenOfficePanel(preferencesService, ooPrefs, preferencesService.getKeyBindingRepository(), taskExecutor, dialogService, stateManager, undoManager).getContent(); - } - - @Override - public SidePaneType getType() { - return SidePaneType.OPEN_OFFICE; - } -} diff --git a/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java b/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java new file mode 100644 index 00000000000..b560c41524c --- /dev/null +++ b/src/main/java/org/jabref/gui/sidepane/GroupsSidePaneComponent.java @@ -0,0 +1,61 @@ +package org.jabref.gui.sidepane; + +import javafx.scene.control.Button; + +import org.jabref.gui.DialogService; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.groups.GroupModeViewModel; +import org.jabref.gui.groups.GroupViewMode; +import org.jabref.gui.icon.IconTheme; +import org.jabref.logic.l10n.Localization; +import org.jabref.preferences.PreferencesService; + +public class GroupsSidePaneComponent extends SidePaneComponent { + private final PreferencesService preferences; + private final DialogService dialogService; + private final Button intersectionUnionToggle = IconTheme.JabRefIcons.GROUP_INTERSECTION.asButton(); + + public GroupsSidePaneComponent(SimpleCommand closeCommand, + SimpleCommand moveUpCommand, + SimpleCommand moveDownCommand, + SidePaneContentFactory contentFactory, + PreferencesService preferences, + DialogService dialogService) { + super(SidePaneType.GROUPS, closeCommand, moveUpCommand, moveDownCommand, contentFactory); + this.preferences = preferences; + this.dialogService = dialogService; + setupIntersectionUnionToggle(); + } + + private void setupIntersectionUnionToggle() { + addExtraButtonToHeader(intersectionUnionToggle, 0); + intersectionUnionToggle.setOnAction(event -> new ToggleUnionIntersectionAction().execute()); + } + + public void afterOpening() { + setGraphicsAndTooltipForButton(preferences.getGroupViewMode()); + } + + private void setGraphicsAndTooltipForButton(GroupViewMode mode) { + GroupModeViewModel modeViewModel = new GroupModeViewModel(mode); + intersectionUnionToggle.setGraphic(modeViewModel.getUnionIntersectionGraphic()); + intersectionUnionToggle.setTooltip(modeViewModel.getUnionIntersectionTooltip()); + } + + private class ToggleUnionIntersectionAction extends SimpleCommand { + + @Override + public void execute() { + GroupViewMode mode = preferences.getGroupViewMode(); + + if (mode == GroupViewMode.UNION) { + preferences.setGroupViewMode(GroupViewMode.INTERSECTION); + dialogService.notify(Localization.lang("Group view mode set to intersection")); + } else if (mode == GroupViewMode.INTERSECTION) { + preferences.setGroupViewMode(GroupViewMode.UNION); + dialogService.notify(Localization.lang("Group view mode set to union")); + } + setGraphicsAndTooltipForButton(mode); + } + } +} diff --git a/src/main/java/org/jabref/gui/sidepane/SidePane.java b/src/main/java/org/jabref/gui/sidepane/SidePane.java index ef565fbbf2d..6e9476abcb7 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePane.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePane.java @@ -1,205 +1,60 @@ package org.jabref.gui.sidepane; -import java.util.Collection; -import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; import java.util.Map; import javax.swing.undo.UndoManager; -import javafx.scene.layout.BorderPane; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.collections.ListChangeListener; import javafx.scene.layout.VBox; import org.jabref.gui.DialogService; import org.jabref.gui.StateManager; -import org.jabref.gui.groups.GroupSidePane; -import org.jabref.gui.importer.fetcher.WebSearchPane; -import org.jabref.gui.openoffice.OpenOfficeSidePanel; +import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.util.TaskExecutor; import org.jabref.preferences.PreferencesService; -/** - * Manages which {@link SidePaneComponent}s are shown. - */ public class SidePane extends VBox { - - private final Map components = new LinkedHashMap<>(); - private final List visibleComponents = new LinkedList<>(); - + private final SidePaneViewModel viewModel; private final PreferencesService preferencesService; - private final TaskExecutor taskExecutor; - private final DialogService dialogService; private final StateManager stateManager; - private final UndoManager undoManager; + + // These bindings need to be stored, otherwise they are garbage collected + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final Map visibleBindings = new HashMap<>(); public SidePane(PreferencesService preferencesService, TaskExecutor taskExecutor, DialogService dialogService, StateManager stateManager, UndoManager undoManager) { - this.preferencesService = preferencesService; - this.taskExecutor = taskExecutor; - this.dialogService = dialogService; this.stateManager = stateManager; - this.undoManager = undoManager; - - setId("sidePane"); - - preferencesService.getSidePanePreferences().visiblePanes().forEach(this::show); + this.preferencesService = preferencesService; + this.viewModel = new SidePaneViewModel(preferencesService, stateManager, taskExecutor, dialogService, undoManager); + stateManager.getVisibleSidePaneComponents().addListener((ListChangeListener) c -> updateView()); updateView(); } - public boolean isComponentVisible(SidePaneType type) { - return visibleComponents.contains(getComponent(type)); - } - - public SidePaneComponent getComponent(SidePaneType type) { - SidePaneComponent component = components.get(type); - if (component == null) { - component = switch (type) { - case OPEN_OFFICE -> new OpenOfficeSidePanel(this, taskExecutor, preferencesService, dialogService, stateManager, undoManager); - case WEB_SEARCH -> new WebSearchPane(this, preferencesService, dialogService, stateManager); - case GROUPS -> new GroupSidePane(this, taskExecutor, stateManager, preferencesService, dialogService); - }; - components.put(component.getType(), component); - } - return component; - } - - /** - * If the given component is visible it will be hidden and the other way around. - */ - protected void toggle(SidePaneType type) { - if (isComponentVisible(type)) { - hide(type); - } else { - show(type); - } - } - - /** - * Makes sure that the given component is visible. - */ - protected void show(SidePaneType type) { - SidePaneComponent component = getComponent(type); - if (!visibleComponents.contains(component)) { - // Add the new component - visibleComponents.add(component); - - // Sort the visible components by their preferred position - visibleComponents.sort(new PreferredIndexSort(preferencesService)); - - updateView(); - - component.afterOpening(); - } - } - - /** - * Makes sure that the given component is not visible. - */ - protected void hide(SidePaneType type) { - SidePaneComponent component = getComponent(type); - if (visibleComponents.contains(component)) { - component.beforeClosing(); - - visibleComponents.remove(component); - - updateView(); - } - } - - /** - * Stores the current configuration of visible components in the preferences, - * so that we show components at the preferred position next time. - */ - private void updatePreferredPositions() { - Map preferredPositions = new HashMap<>(preferencesService.getSidePanePreferences().getPreferredPositions()); - - // Use the currently shown positions of all visible components - int index = 0; - for (SidePaneComponent comp : visibleComponents) { - preferredPositions.put(comp.getType(), index); - index++; - } - preferencesService.getSidePanePreferences().setPreferredPositions(preferredPositions); - } - - /** - * Moves the given component up. - */ - protected void moveUp(SidePaneComponent component) { - if (visibleComponents.contains(component)) { - int currentPosition = visibleComponents.indexOf(component); - if (currentPosition > 0) { - int newPosition = currentPosition - 1; - visibleComponents.remove(currentPosition); - visibleComponents.add(newPosition, component); - - updatePreferredPositions(); - updateView(); - } - } - } - - /** - * Moves the given component down. - */ - protected void moveDown(SidePaneComponent comp) { - if (visibleComponents.contains(comp)) { - int currentPosition = visibleComponents.indexOf(comp); - if (currentPosition < (visibleComponents.size() - 1)) { - int newPosition = currentPosition + 1; - visibleComponents.remove(currentPosition); - visibleComponents.add(newPosition, comp); - - updatePreferredPositions(); - updateView(); - } - } - } - - /** - * Updates the view to reflect changes to visible components. - */ - private void updateView() { - setComponents(visibleComponents); - setVisible(!visibleComponents.isEmpty()); - } - - private void setComponents(Collection components) { + private void updateView() { getChildren().clear(); - - for (SidePaneComponent component : components) { - BorderPane node = new BorderPane(); - node.getStyleClass().add("sidePaneComponent"); - node.setTop(component.getHeader()); - node.setCenter(component.getContentPane()); - getChildren().add(node); - VBox.setVgrow(node, component.getResizePolicy()); - } - } - - /** - * Helper class for sorting visible components based on their preferred position. - */ - private static class PreferredIndexSort implements Comparator { - - private final Map preferredPositions; - - public PreferredIndexSort(PreferencesService preferencesService) { - preferredPositions = preferencesService.getSidePanePreferences().getPreferredPositions(); - } - - @Override - public int compare(SidePaneComponent comp1, SidePaneComponent comp2) { - int pos1 = preferredPositions.getOrDefault(comp1.getType(), 0); - int pos2 = preferredPositions.getOrDefault(comp2.getType(), 0); - return Integer.compare(pos1, pos2); - } + for (SidePaneType type : stateManager.getVisibleSidePaneComponents()) { + SidePaneComponent view = viewModel.getSidePaneComponent(type); + getChildren().add(view); + } + } + + public BooleanBinding paneVisibleBinding(SidePaneType pane) { + BooleanBinding visibility = Bindings.createBooleanBinding( + () -> stateManager.getVisibleSidePaneComponents().contains(pane), + stateManager.getVisibleSidePaneComponents()); + visibleBindings.put(pane, visibility); + return visibility; + } + + public SimpleCommand getToggleCommandFor(SidePaneType sidePane) { + return new TogglePaneAction(stateManager, sidePane, preferencesService.getSidePanePreferences()); } } diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java b/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java index 15e80c6ab68..5993f1a4c95 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneComponent.java @@ -1,8 +1,5 @@ package org.jabref.gui.sidepane; -import java.util.Collections; -import java.util.List; - import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.Label; @@ -10,148 +7,68 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; -import org.jabref.gui.actions.Action; import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.icon.IconTheme; -import org.jabref.gui.icon.JabRefIcon; import org.jabref.logic.l10n.Localization; -public abstract class SidePaneComponent { - - private final SidePane sidePane; - private final ToggleCommand toggleCommand; - private final JabRefIcon icon; - private final String title; - private Node contentNode; - - public SidePaneComponent(SidePane sidePane, JabRefIcon icon, String title) { - this.sidePane = sidePane; - this.icon = icon; - this.title = title; - this.toggleCommand = new ToggleCommand(this); - } - - protected void hide() { - sidePane.hide(this.getType()); - } - - protected void show() { - sidePane.show(this.getType()); +public class SidePaneComponent extends BorderPane { + private final SidePaneType sidePaneType; + private final SimpleCommand closeCommand; + private final SimpleCommand moveUpCommand; + private final SimpleCommand moveDownCommand; + private final SidePaneContentFactory contentFactory; + + private HBox buttonContainer; + + public SidePaneComponent(SidePaneType sidePaneType, + SimpleCommand closeCommand, + SimpleCommand moveUpCommand, + SimpleCommand moveDownCommand, + SidePaneContentFactory contentFactory) { + this.sidePaneType = sidePaneType; + this.closeCommand = closeCommand; + this.moveUpCommand = moveUpCommand; + this.moveDownCommand = moveDownCommand; + this.contentFactory = contentFactory; + initialize(); } - protected void moveUp() { - sidePane.moveUp(this); + private void initialize() { + getStyleClass().add("sidePaneComponent"); + setTop(createHeaderView()); + setCenter(contentFactory.create(sidePaneType)); + VBox.setVgrow(this, sidePaneType == SidePaneType.GROUPS ? Priority.ALWAYS : Priority.NEVER); } - protected void moveDown() { - sidePane.moveDown(this); - } + private Node createHeaderView() { + Button closeButton = IconTheme.JabRefIcons.CLOSE.asButton(); + closeButton.setTooltip(new Tooltip(Localization.lang("Hide panel"))); + closeButton.setOnAction(e -> closeCommand.execute()); - /** - * Override this method if the component needs to make any changes before it can close. - */ - public void beforeClosing() { - // Nothing to do by default - } + Button upButton = IconTheme.JabRefIcons.UP.asButton(); + upButton.setTooltip(new Tooltip(Localization.lang("Move panel up"))); + upButton.setOnAction(e -> moveUpCommand.execute()); - /** - * Override this method if the component needs to do any actions after it is shown. - */ - public void afterOpening() { - // Nothing to do by default - } + Button downButton = IconTheme.JabRefIcons.DOWN.asButton(); + downButton.setTooltip(new Tooltip(Localization.lang("Move panel down"))); + downButton.setOnAction(e -> moveDownCommand.execute()); - /** - * Specifies how to this side pane component behaves if there is additional vertical space. - */ - public abstract Priority getResizePolicy(); + this.buttonContainer = new HBox(); + buttonContainer.getChildren().addAll(upButton, downButton, closeButton); - /** - * @return the command which toggles this {@link SidePaneComponent} - */ - public ToggleCommand getToggleCommand() { - return toggleCommand; - } + Label label = new Label(sidePaneType.getTitle()); - /** - * @return the action to toggle this {@link SidePaneComponent} - */ - public abstract Action getToggleAction(); + BorderPane headerView = new BorderPane(); + headerView.setCenter(label); + headerView.setRight(buttonContainer); + headerView.getStyleClass().add("sidePaneComponentHeader"); - /** - * @return the content of this component - */ - public final Node getContentPane() { - if (contentNode == null) { - contentNode = createContentPane(); - } - - return contentNode; + return headerView; } - /** - * @return the header pane for this component - */ - public final Node getHeader() { - Button close = IconTheme.JabRefIcons.CLOSE.asButton(); - close.setTooltip(new Tooltip(Localization.lang("Hide panel"))); - close.setOnAction(event -> hide()); - - Button up = IconTheme.JabRefIcons.UP.asButton(); - up.setTooltip(new Tooltip(Localization.lang("Move panel up"))); - up.setOnAction(event -> moveUp()); - - Button down = IconTheme.JabRefIcons.DOWN.asButton(); - down.setTooltip(new Tooltip(Localization.lang("Move panel down"))); - down.setOnAction(event -> moveDown()); - - final HBox buttonContainer = new HBox(); - buttonContainer.getChildren().addAll(up, down); - buttonContainer.getChildren().addAll(getAdditionalHeaderButtons()); - buttonContainer.getChildren().add(close); - - BorderPane graphic = new BorderPane(); - graphic.setCenter(icon.getGraphicNode()); - - final Label label = new Label(title); - BorderPane container = new BorderPane(); - container.setCenter(label); - container.setRight(buttonContainer); - container.getStyleClass().add("sidePaneComponentHeader"); - - return container; - } - - protected List getAdditionalHeaderButtons() { - return Collections.emptyList(); - } - - /** - * Create the content of this component - * - * @implNote The {@link SidePane} always creates an instance of every side component (e.g., to get the toggle action) - * but we only want to create the content view if the component is shown to save resources. - * This is the reason for the lazy loading. - */ - protected abstract Node createContentPane(); - - /** - * @return the type of this component - */ - public abstract SidePaneType getType(); - - public class ToggleCommand extends SimpleCommand { - - private final SidePaneComponent component; - - public ToggleCommand(SidePaneComponent component) { - this.component = component; - } - - @Override - public void execute() { - sidePane.toggle(component.getType()); - } + protected void addExtraButtonToHeader(Button button, int position) { + this.buttonContainer.getChildren().add(position, button); } } diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java b/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java new file mode 100644 index 00000000000..f3da681a510 --- /dev/null +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneContentFactory.java @@ -0,0 +1,55 @@ +package org.jabref.gui.sidepane; + +import javax.swing.undo.UndoManager; + +import javafx.scene.Node; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.groups.GroupTreeView; +import org.jabref.gui.importer.fetcher.WebSearchPaneView; +import org.jabref.gui.openoffice.OpenOfficePanel; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.preferences.PreferencesService; + +public class SidePaneContentFactory { + private final PreferencesService preferences; + private final TaskExecutor taskExecutor; + private final DialogService dialogService; + private final StateManager stateManager; + private final UndoManager undoManager; + + public SidePaneContentFactory(PreferencesService preferences, + TaskExecutor taskExecutor, + DialogService dialogService, + StateManager stateManager, + UndoManager undoManager) { + this.preferences = preferences; + this.taskExecutor = taskExecutor; + this.dialogService = dialogService; + this.stateManager = stateManager; + this.undoManager = undoManager; + } + + public Node create(SidePaneType sidePaneType) { + return switch (sidePaneType) { + case GROUPS -> new GroupTreeView( + taskExecutor, + stateManager, + preferences, + dialogService); + case OPEN_OFFICE -> new OpenOfficePanel( + preferences, + preferences.getOpenOfficePreferences(), + preferences.getKeyBindingRepository(), + taskExecutor, + dialogService, + stateManager, + undoManager).getContent(); + case WEB_SEARCH -> new WebSearchPaneView( + preferences, + dialogService, + stateManager); + }; + } +} diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneType.java b/src/main/java/org/jabref/gui/sidepane/SidePaneType.java index 038e48feb3d..494f936bbe9 100644 --- a/src/main/java/org/jabref/gui/sidepane/SidePaneType.java +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneType.java @@ -1,10 +1,38 @@ package org.jabref.gui.sidepane; +import org.jabref.gui.actions.Action; +import org.jabref.gui.actions.StandardActions; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.icon.JabRefIcon; +import org.jabref.logic.l10n.Localization; + /** * Definition of all possible components in the side pane. */ public enum SidePaneType { - OPEN_OFFICE, - WEB_SEARCH, - GROUPS + OPEN_OFFICE("OpenOffice/LibreOffice", IconTheme.JabRefIcons.FILE_OPENOFFICE, StandardActions.TOOGLE_OO), + WEB_SEARCH(Localization.lang("Web search"), IconTheme.JabRefIcons.WWW, StandardActions.TOGGLE_WEB_SEARCH), + GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, StandardActions.TOGGLE_GROUPS); + + private final String title; + private final JabRefIcon icon; + private final Action toggleAction; + + SidePaneType(String title, JabRefIcon icon, Action toggleAction) { + this.title = title; + this.icon = icon; + this.toggleAction = toggleAction; + } + + public String getTitle() { + return title; + } + + public JabRefIcon getIcon() { + return icon; + } + + public Action getToggleAction() { + return toggleAction; + } } diff --git a/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java b/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java new file mode 100644 index 00000000000..8e661995dfa --- /dev/null +++ b/src/main/java/org/jabref/gui/sidepane/SidePaneViewModel.java @@ -0,0 +1,212 @@ +package org.jabref.gui.sidepane; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.IntStream; + +import javax.swing.undo.UndoManager; + +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; + +import org.jabref.gui.AbstractViewModel; +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.preferences.PreferencesService; +import org.jabref.preferences.SidePanePreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jabref.gui.sidepane.SidePaneType.GROUPS; + +public class SidePaneViewModel extends AbstractViewModel { + private static final Logger LOGGER = LoggerFactory.getLogger(SidePaneViewModel.class); + + private final Map sidePaneComponentLookup = new HashMap<>(); + + private final PreferencesService preferencesService; + private final StateManager stateManager; + private final SidePaneContentFactory sidePaneContentFactory; + private final DialogService dialogService; + + public SidePaneViewModel(PreferencesService preferencesService, + StateManager stateManager, + TaskExecutor taskExecutor, + DialogService dialogService, + UndoManager undoManager) { + this.preferencesService = preferencesService; + this.stateManager = stateManager; + this.dialogService = dialogService; + this.sidePaneContentFactory = new SidePaneContentFactory( + preferencesService, + taskExecutor, + dialogService, + stateManager, + undoManager); + + preferencesService.getSidePanePreferences().visiblePanes().forEach(this::show); + getPanes().addListener((ListChangeListener) change -> { + while (change.next()) { + if (change.wasAdded()) { + preferencesService.getSidePanePreferences().visiblePanes().add(change.getAddedSubList().get(0)); + } else if (change.wasRemoved()) { + preferencesService.getSidePanePreferences().visiblePanes().remove(change.getRemoved().get(0)); + } + } + }); + } + + protected SidePaneComponent getSidePaneComponent(SidePaneType pane) { + + SidePaneComponent sidePaneComponent = sidePaneComponentLookup.get(pane); + if (sidePaneComponent == null) { + sidePaneComponent = switch (pane) { + case GROUPS -> new GroupsSidePaneComponent( + new ClosePaneAction(pane), + new MoveUpAction(pane), + new MoveDownAction(pane), + sidePaneContentFactory, + preferencesService, + dialogService); + case WEB_SEARCH, OPEN_OFFICE -> new SidePaneComponent(pane, + new ClosePaneAction(pane), + new MoveUpAction(pane), + new MoveDownAction(pane), + sidePaneContentFactory); + }; + sidePaneComponentLookup.put(pane, sidePaneComponent); + } + return sidePaneComponent; + } + + /** + * Stores the current configuration of visible panes in the preferences, so that we show panes at the preferred + * position next time. + */ + private void updatePreferredPositions() { + Map preferredPositions = new HashMap<>(preferencesService.getSidePanePreferences() + .getPreferredPositions()); + IntStream.range(0, getPanes().size()).forEach(i -> preferredPositions.put(getPanes().get(i), i)); + preferencesService.getSidePanePreferences().setPreferredPositions(preferredPositions); + } + + public void moveUp(SidePaneType pane) { + if (getPanes().contains(pane)) { + int currentPosition = getPanes().indexOf(pane); + if (currentPosition > 0) { + int newPosition = currentPosition - 1; + swap(getPanes(), currentPosition, newPosition); + updatePreferredPositions(); + } else { + LOGGER.debug("SidePaneComponent is already at the bottom"); + } + } else { + LOGGER.warn("SidePaneComponent {} not visible", pane.getTitle()); + } + } + + public void moveDown(SidePaneType pane) { + if (getPanes().contains(pane)) { + int currentPosition = getPanes().indexOf(pane); + if (currentPosition < (getPanes().size() - 1)) { + int newPosition = currentPosition + 1; + swap(getPanes(), currentPosition, newPosition); + updatePreferredPositions(); + } else { + LOGGER.debug("SidePaneComponent {} is already at the top", pane.getTitle()); + } + } else { + LOGGER.warn("SidePaneComponent {} not visible", pane.getTitle()); + } + } + + private void show(SidePaneType pane) { + if (!getPanes().contains(pane)) { + getPanes().add(pane); + getPanes().sort(new PreferredIndexSort(preferencesService.getSidePanePreferences())); + } else { + LOGGER.warn("SidePaneComponent {} not visible", pane.getTitle()); + } + + if (pane == GROUPS + && stateManager.getVisibleSidePaneComponents().contains(GROUPS) + && getSidePaneComponent(pane) instanceof GroupsSidePaneComponent component) { + component.afterOpening(); + } + } + + private ObservableList getPanes() { + return stateManager.getVisibleSidePaneComponents(); + } + + private void swap(ObservableList observableList, int i, int j) { + List placeholder = new ArrayList<>(observableList); + Collections.swap(placeholder, i, j); + observableList.sort(Comparator.comparingInt(placeholder::indexOf)); + } + + /** + * Helper class for sorting visible side panes based on their preferred position. + */ + protected static class PreferredIndexSort implements Comparator { + + private final Map preferredPositions; + + public PreferredIndexSort(SidePanePreferences sidePanePreferences) { + this.preferredPositions = sidePanePreferences.getPreferredPositions(); + } + + @Override + public int compare(SidePaneType type1, SidePaneType type2) { + int pos1 = preferredPositions.getOrDefault(type1, 0); + int pos2 = preferredPositions.getOrDefault(type2, 0); + return Integer.compare(pos1, pos2); + } + } + + private class MoveUpAction extends SimpleCommand { + private final SidePaneType toMoveUpPane; + + public MoveUpAction(SidePaneType toMoveUpPane) { + this.toMoveUpPane = toMoveUpPane; + } + + @Override + public void execute() { + moveUp(toMoveUpPane); + } + } + + private class MoveDownAction extends SimpleCommand { + private final SidePaneType toMoveDownPane; + + public MoveDownAction(SidePaneType toMoveDownPane) { + this.toMoveDownPane = toMoveDownPane; + } + + @Override + public void execute() { + moveDown(toMoveDownPane); + } + } + + public class ClosePaneAction extends SimpleCommand { + private final SidePaneType toClosePane; + + public ClosePaneAction(SidePaneType toClosePane) { + this.toClosePane = toClosePane; + } + + @Override + public void execute() { + stateManager.getVisibleSidePaneComponents().remove(toClosePane); + } + } +} diff --git a/src/main/java/org/jabref/gui/sidepane/TogglePaneAction.java b/src/main/java/org/jabref/gui/sidepane/TogglePaneAction.java new file mode 100644 index 00000000000..087a53db78f --- /dev/null +++ b/src/main/java/org/jabref/gui/sidepane/TogglePaneAction.java @@ -0,0 +1,27 @@ +package org.jabref.gui.sidepane; + +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.preferences.SidePanePreferences; + +public class TogglePaneAction extends SimpleCommand { + private final StateManager stateManager; + private final SidePaneType pane; + private final SidePanePreferences sidePanePreferences; + + public TogglePaneAction(StateManager stateManager, SidePaneType pane, SidePanePreferences sidePanePreferences) { + this.stateManager = stateManager; + this.pane = pane; + this.sidePanePreferences = sidePanePreferences; + } + + @Override + public void execute() { + if (!stateManager.getVisibleSidePaneComponents().contains(pane)) { + stateManager.getVisibleSidePaneComponents().add(pane); + stateManager.getVisibleSidePaneComponents().sort(new SidePaneViewModel.PreferredIndexSort(sidePanePreferences)); + } else { + stateManager.getVisibleSidePaneComponents().remove(pane); + } + } +} diff --git a/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java b/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java new file mode 100644 index 00000000000..03fb4dc6483 --- /dev/null +++ b/src/test/java/org/jabref/gui/sidepane/SidePaneViewModelTest.java @@ -0,0 +1,97 @@ +package org.jabref.gui.sidepane; + +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; + +import javax.swing.undo.UndoManager; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.util.CustomLocalDragboard; +import org.jabref.gui.util.OptionalObjectProperty; +import org.jabref.gui.util.TaskExecutor; +import org.jabref.preferences.PreferencesService; +import org.jabref.preferences.SidePanePreferences; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.framework.junit5.ApplicationExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(ApplicationExtension.class) +class SidePaneViewModelTest { + + PreferencesService preferencesService = mock(PreferencesService.class); + StateManager stateManager = mock(StateManager.class); + TaskExecutor taskExecutor = mock(TaskExecutor.class); + DialogService dialogService = mock(DialogService.class); + UndoManager undoManager = mock(UndoManager.class); + + SidePanePreferences sidePanePreferences = new SidePanePreferences(new HashSet<>(), new HashMap<>(), 0); + ObservableList sidePaneComponents = FXCollections.observableArrayList(); + SidePaneViewModel sidePaneViewModel; + + @BeforeEach + void setUp() { + when(stateManager.getVisibleSidePaneComponents()).thenReturn(sidePaneComponents); + when(stateManager.getLocalDragboard()).thenReturn(mock(CustomLocalDragboard.class)); + when(stateManager.activeDatabaseProperty()).thenReturn(OptionalObjectProperty.empty()); + when(preferencesService.getSidePanePreferences()).thenReturn(sidePanePreferences); + + sidePanePreferences.visiblePanes().addAll(EnumSet.allOf(SidePaneType.class)); + sidePanePreferences.getPreferredPositions().put(SidePaneType.GROUPS, 0); + sidePanePreferences.getPreferredPositions().put(SidePaneType.WEB_SEARCH, 1); + sidePanePreferences.getPreferredPositions().put(SidePaneType.OPEN_OFFICE, 2); + + sidePaneViewModel = new SidePaneViewModel(preferencesService, stateManager, taskExecutor, dialogService, undoManager); + } + + @Test + void moveUp() { + sidePaneViewModel.moveUp(SidePaneType.WEB_SEARCH); + + assertEquals(sidePaneComponents.get(0), SidePaneType.WEB_SEARCH); + assertEquals(sidePaneComponents.get(1), SidePaneType.GROUPS); + } + + @Test + void moveUpFromFirstPosition() { + sidePaneViewModel.moveUp(SidePaneType.GROUPS); + + assertEquals(sidePaneComponents.get(0), SidePaneType.GROUPS); + } + + @Test + void moveDown() { + sidePaneViewModel.moveDown(SidePaneType.WEB_SEARCH); + + assertEquals(sidePaneComponents.get(1), SidePaneType.OPEN_OFFICE); + assertEquals(sidePaneComponents.get(2), SidePaneType.WEB_SEARCH); + } + + @Test + void moveDownFromLastPosition() { + sidePaneViewModel.moveDown(SidePaneType.OPEN_OFFICE); + + assertEquals(sidePaneComponents.get(2), SidePaneType.OPEN_OFFICE); + } + + @Test + void sortByPreferredPositions() { + sidePanePreferences.getPreferredPositions().put(SidePaneType.GROUPS, 2); + sidePanePreferences.getPreferredPositions().put(SidePaneType.OPEN_OFFICE, 0); + + sidePaneComponents.sort(new SidePaneViewModel.PreferredIndexSort(sidePanePreferences)); + + assertTrue(sidePaneComponents.get(0) == SidePaneType.OPEN_OFFICE && sidePaneComponents.get(2) == SidePaneType.GROUPS); + } +}