diff --git a/CHANGELOG.md b/CHANGELOG.md index 87e03674bf0..5ae64cf1d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a field for the latest ICORE conference ranking lookup on the General Tab. [#13476](https://github.com/JabRef/jabref/issues/13476) - We added BibLaTeX datamodel validation support in order to improve error message quality in entries' fields validation. [#13318](https://github.com/JabRef/jabref/issues/13318) - We added more supported formats of CAYW endpoint of HTTP server. [#13578](https://github.com/JabRef/jabref/issues/13578) +- We added chronological navigation for entries in each library. [#6352](https://github.com/JabRef/jabref/issues/6352) ### Changed diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java b/jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java index c66beb82f7e..f4c0a0bfdcd 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java @@ -90,6 +90,8 @@ public class JabRefGuiStateManager implements StateManager { private final List aiChatWindows = new ArrayList<>(); private final BooleanProperty editorShowing = new SimpleBooleanProperty(false); private final OptionalObjectProperty activeWalkthrough = OptionalObjectProperty.empty(); + private final BooleanProperty canGoBack = new SimpleBooleanProperty(false); + private final BooleanProperty canGoForward = new SimpleBooleanProperty(false); @Override public ObservableList getVisibleSidePaneComponents() { @@ -307,4 +309,14 @@ public void setActiveWalkthrough(Walkthrough walkthrough) { public Optional getActiveWalkthrough() { return activeWalkthrough.get(); } + + @Override + public BooleanProperty canGoBackProperty() { + return canGoBack; + } + + @Override + public BooleanProperty canGoForwardProperty() { + return canGoForward; + } } diff --git a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java index a05eacf3020..4891b5c6b20 100644 --- a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java +++ b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java @@ -118,6 +118,11 @@ public class LibraryTab extends Tab implements CommandSelectionTab { private final BibEntryTypesManager entryTypesManager; private final BooleanProperty changedProperty = new SimpleBooleanProperty(false); private final BooleanProperty nonUndoableChangeProperty = new SimpleBooleanProperty(false); + private final NavigationHistory navigationHistory = new NavigationHistory(); + private final BooleanProperty canGoBackProperty = new SimpleBooleanProperty(false); + private final BooleanProperty canGoForwardProperty = new SimpleBooleanProperty(false); + private boolean backOrForwardNavigationActionTriggered = false; + private BibDatabaseContext bibDatabaseContext; @@ -490,6 +495,16 @@ private void createMainTable() { mainTable.addSelectionListener(event -> { List entries = event.getList().stream().map(BibEntryTableViewModel::getEntry).toList(); stateManager.setSelectedEntries(entries); + + // track navigation history for single selections + if (entries.size() == 1) { + newEntryShowing(entries.getFirst()); + } else if (entries.isEmpty()) { + // an empty selection isn't a navigational step, so we don't alter the history list + // this avoids adding a "null" entry to the back/forward stack + // we just refresh the UI button states to ensure they are consistent with the latest history. + updateNavigationState(); + } }); } @@ -964,6 +979,48 @@ public void resetChangedProperties() { this.changedProperty.setValue(false); } + public void back() { + navigationHistory.back().ifPresent(this::navigateToEntry); + } + + public void forward() { + navigationHistory.forward().ifPresent(this::navigateToEntry); + } + + private void navigateToEntry(BibEntry entry) { + backOrForwardNavigationActionTriggered = true; + clearAndSelect(entry); + updateNavigationState(); + } + + public boolean canGoBack() { + return navigationHistory.canGoBack(); + } + + public boolean canGoForward() { + return navigationHistory.canGoForward(); + } + + private void newEntryShowing(BibEntry entry) { + // skip history updates if this is from a back/forward operation + if (backOrForwardNavigationActionTriggered) { + backOrForwardNavigationActionTriggered = false; + return; + } + + navigationHistory.add(entry); + updateNavigationState(); + } + + /** + * Updates the StateManager with current navigation state + * Only update if this is the active tab + */ + public void updateNavigationState() { + canGoBackProperty.set(canGoBack()); + canGoForwardProperty.set(canGoForward()); + } + /** * Creates a new library tab. Contents are loaded by the {@code dataLoadingTask}. Most of the other parameters are required by {@code resetChangeMonitor()}. * @@ -1034,6 +1091,14 @@ public static LibraryTab createLibraryTab(@NonNull BibDatabaseContext databaseCo false); } + public BooleanProperty canGoBackProperty() { + return canGoBackProperty; + } + + public BooleanProperty canGoForwardProperty() { + return canGoForwardProperty; + } + private class GroupTreeListener { @Subscribe diff --git a/jabgui/src/main/java/org/jabref/gui/NavigationHistory.java b/jabgui/src/main/java/org/jabref/gui/NavigationHistory.java new file mode 100644 index 00000000000..bf0a802f2b2 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/NavigationHistory.java @@ -0,0 +1,77 @@ +package org.jabref.gui; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import org.jabref.model.entry.BibEntry; + +/** + * Manages the navigation history of viewed entries using two stacks. + * This class encapsulates the logic of moving back and forward by maintaining a "back" stack for past entries + * and a "forward" stack for future entries. + */ +public class NavigationHistory { + private final List previousEntries = new ArrayList<>(); + private final List nextEntries = new ArrayList<>(); + private BibEntry currentEntry; + + /** + * Sets a new entry as the current one, clearing the forward history. + * The previously current entry is moved to the back stack. + * + * @param entry The BibEntry to add to the history. + */ + public void add(BibEntry entry) { + if (Objects.equals(currentEntry, entry)) { + return; + } + + // a new selection invalidates the forward history + nextEntries.clear(); + + if (currentEntry != null) { + previousEntries.add(currentEntry); + } + currentEntry = entry; + } + + /** + * Moves to the previous entry in the history. + * The current entry is pushed to the forward stack, and the last entry from the back stack becomes current. + * + * @return An Optional containing the previous BibEntry, or an empty Optional if there's no history to go back to. + */ + public Optional back() { + if (canGoBack()) { + nextEntries.add(currentEntry); + currentEntry = previousEntries.removeLast(); + return Optional.of(currentEntry); + } + return Optional.empty(); + } + + /** + * Moves to the next entry in the history. + * The current entry is pushed to the back stack, and the last entry from the forward stack becomes current. + * + * @return An Optional containing the next BibEntry, or an empty Optional if there is no "forward" history. + */ + public Optional forward() { + if (canGoForward()) { + previousEntries.add(currentEntry); + currentEntry = nextEntries.removeLast(); + return Optional.of(currentEntry); + } + return Optional.empty(); + } + + public boolean canGoBack() { + return !previousEntries.isEmpty(); + } + + public boolean canGoForward() { + return !nextEntries.isEmpty(); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/StateManager.java b/jabgui/src/main/java/org/jabref/gui/StateManager.java index 23caafc512b..d8800a955e6 100644 --- a/jabgui/src/main/java/org/jabref/gui/StateManager.java +++ b/jabgui/src/main/java/org/jabref/gui/StateManager.java @@ -108,4 +108,8 @@ public interface StateManager extends SrvStateManager { void setActiveWalkthrough(Walkthrough walkthrough); Optional getActiveWalkthrough(); + + BooleanProperty canGoBackProperty(); + + BooleanProperty canGoForwardProperty(); } diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 3134567baba..cd5dccd7f6b 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -95,6 +95,9 @@ public enum StandardActions implements Action { MANAGE_KEYWORDS(Localization.lang("Manage keywords")), MASS_SET_FIELDS(Localization.lang("Manage field names & content")), + BACK(Localization.lang("Back"), IconTheme.JabRefIcons.LEFT, KeyBinding.BACK), + FORWARD(Localization.lang("Forward"), Localization.lang("Forward"), IconTheme.JabRefIcons.RIGHT, KeyBinding.FORWARD), + AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")), TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE), TOGGLE_OO(Localization.lang("OpenOffice/LibreOffice"), IconTheme.JabRefIcons.FILE_OPENOFFICE, KeyBinding.OPEN_OPEN_OFFICE_LIBRE_OFFICE_CONNECTION), diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java index 5b3bc9f17f1..94c372811cf 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java @@ -11,6 +11,7 @@ import javafx.beans.InvalidationListener; import javafx.beans.binding.Bindings; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.value.ObservableValue; import javafx.collections.ListChangeListener; @@ -371,6 +372,14 @@ private void initKeyBindings() { case NEW_INPROCEEDINGS: new NewEntryAction(StandardEntryType.InProceedings, this::getCurrentLibraryTab, dialogService, preferences, stateManager).execute(); break; + case BACK: + Optional.ofNullable(getCurrentLibraryTab()).ifPresent(LibraryTab::back); + event.consume(); + break; + case FORWARD: + Optional.ofNullable(getCurrentLibraryTab()).ifPresent(LibraryTab::forward); + event.consume(); + break; default: } } @@ -452,6 +461,22 @@ private void initBindings() { // Hide tab bar stateManager.getOpenDatabases().addListener((ListChangeListener) _ -> updateTabBarVisible()); EasyBind.subscribe(preferences.getWorkspacePreferences().hideTabBarProperty(), _ -> updateTabBarVisible()); + + stateManager.canGoBackProperty().bind( + stateManager.activeTabProperty().flatMap( + optionalTab -> optionalTab + .map(LibraryTab::canGoBackProperty) + .orElse(new SimpleBooleanProperty(false)) + ) + ); + + stateManager.canGoForwardProperty().bind( + stateManager.activeTabProperty().flatMap( + optionalTab -> optionalTab + .map(LibraryTab::canGoForwardProperty) + .orElse(new SimpleBooleanProperty(false)) + ) + ); } private void updateTabBarVisible() { diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java b/jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java index bf27beab906..aaa86141362 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java @@ -18,6 +18,7 @@ import org.jabref.gui.LibraryTabContainer; import org.jabref.gui.StateManager; import org.jabref.gui.actions.ActionFactory; +import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.citationkeypattern.GenerateCitationKeyAction; import org.jabref.gui.cleanup.CleanupAction; @@ -57,6 +58,8 @@ public class MainToolBar extends ToolBar { private final TaskExecutor taskExecutor; private final BibEntryTypesManager entryTypesManager; private final ClipBoardManager clipBoardManager; + private SimpleCommand backCommand; + private SimpleCommand forwardCommand; private final CountingUndoManager undoManager; private PopOver entryFromIdPopOver; @@ -99,6 +102,7 @@ private void createToolBar() { final Button pushToApplicationButton = factory.createIconButton(pushToApplicationCommand.getAction(), pushToApplicationCommand); pushToApplicationCommand.registerReconfigurable(pushToApplicationButton); + initNavigationCommands(); // Setup Toolbar @@ -121,6 +125,12 @@ private void createToolBar() { new Separator(Orientation.VERTICAL), + new HBox( + factory.createIconButton(StandardActions.BACK, backCommand), + factory.createIconButton(StandardActions.FORWARD, forwardCommand)), + + new Separator(Orientation.VERTICAL), + new HBox( factory.createIconButton(StandardActions.UNDO, new UndoAction(frame::getCurrentLibraryTab, undoManager, dialogService, stateManager)), factory.createIconButton(StandardActions.REDO, new RedoAction(frame::getCurrentLibraryTab, undoManager, dialogService, stateManager)), @@ -208,4 +218,32 @@ Group createTaskIndicator() { return new Group(indicator); } + + private void initNavigationCommands() { + backCommand = new SimpleCommand() { + { + executable.bind(stateManager.canGoBackProperty()); + } + + @Override + public void execute() { + if (frame.getCurrentLibraryTab() != null) { + frame.getCurrentLibraryTab().back(); + } + } + }; + + forwardCommand = new SimpleCommand() { + { + executable.bind(stateManager.canGoForwardProperty()); + } + + @Override + public void execute() { + if (frame.getCurrentLibraryTab() != null) { + frame.getCurrentLibraryTab().forward(); + } + } + }; + } } diff --git a/jabgui/src/main/java/org/jabref/gui/keyboard/KeyBinding.java b/jabgui/src/main/java/org/jabref/gui/keyboard/KeyBinding.java index 0002b8dd0dc..08d68d5f7e2 100644 --- a/jabgui/src/main/java/org/jabref/gui/keyboard/KeyBinding.java +++ b/jabgui/src/main/java/org/jabref/gui/keyboard/KeyBinding.java @@ -69,6 +69,9 @@ public enum KeyBinding { IMPORT_INTO_NEW_DATABASE("Import into new library", Localization.lang("Import into new library"), "ctrl+alt+I", KeyBindingCategory.FILE), MERGE_ENTRIES("Merge entries", Localization.lang("Merge entries"), "ctrl+M", KeyBindingCategory.TOOLS), + BACK("Back", Localization.lang("Back"), "alt+LEFT", KeyBindingCategory.VIEW), + FORWARD("Forward", Localization.lang("Forward"), "alt+RIGHT", KeyBindingCategory.VIEW), + ADD_ENTRY("Add entry", Localization.lang("Add entry"), "ctrl+N", KeyBindingCategory.BIBTEX), ADD_ENTRY_IDENTIFIER("Enter identifier", Localization.lang("Enter identifier"), "ctrl+alt+shift+N", KeyBindingCategory.BIBTEX), ADD_ENTRY_PLAINTEXT("Interpret citations", Localization.lang("Interpret citations"), "ctrl+shift+N", KeyBindingCategory.BIBTEX), diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 0c50f677e0b..90259aad6ce 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -3401,3 +3401,6 @@ Commit\ aborted\:\ Local\ repository\ has\ unresolved\ merge\ conflicts.=Commit Commit\ aborted\:\ Path\ is\ not\ inside\ a\ Git\ repository.=Commit aborted: Path is not inside a Git repository. Commit\ aborted\:\ The\ file\ is\ not\ under\ Git\ version\ control.=Commit aborted: The file is not under Git version control. Update\ references=Update references + +Back=Back +Forward=Forward