Skip to content

Commit e7a5420

Browse files
authored
Chronological navigation in libraries (#13863)
* Add basic implementation * Extend implementation to per-tab * Add changelog entry * Refine comment * l10n * l10n format * Refine comments * Better name for variable * Even better name * Refactor lists into class
1 parent 014b8d5 commit e7a5420

File tree

10 files changed

+231
-0
lines changed

10 files changed

+231
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
3939
- We added a field for the latest ICORE conference ranking lookup on the General Tab. [#13476](https://github.com/JabRef/jabref/issues/13476)
4040
- 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)
4141
- We added more supported formats of CAYW endpoint of HTTP server. [#13578](https://github.com/JabRef/jabref/issues/13578)
42+
- We added chronological navigation for entries in each library. [#6352](https://github.com/JabRef/jabref/issues/6352)
4243

4344
### Changed
4445

jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ public class JabRefGuiStateManager implements StateManager {
9090
private final List<AiChatWindow> aiChatWindows = new ArrayList<>();
9191
private final BooleanProperty editorShowing = new SimpleBooleanProperty(false);
9292
private final OptionalObjectProperty<Walkthrough> activeWalkthrough = OptionalObjectProperty.empty();
93+
private final BooleanProperty canGoBack = new SimpleBooleanProperty(false);
94+
private final BooleanProperty canGoForward = new SimpleBooleanProperty(false);
9395

9496
@Override
9597
public ObservableList<SidePaneType> getVisibleSidePaneComponents() {
@@ -307,4 +309,14 @@ public void setActiveWalkthrough(Walkthrough walkthrough) {
307309
public Optional<Walkthrough> getActiveWalkthrough() {
308310
return activeWalkthrough.get();
309311
}
312+
313+
@Override
314+
public BooleanProperty canGoBackProperty() {
315+
return canGoBack;
316+
}
317+
318+
@Override
319+
public BooleanProperty canGoForwardProperty() {
320+
return canGoForward;
321+
}
310322
}

jabgui/src/main/java/org/jabref/gui/LibraryTab.java

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ public class LibraryTab extends Tab implements CommandSelectionTab {
118118
private final BibEntryTypesManager entryTypesManager;
119119
private final BooleanProperty changedProperty = new SimpleBooleanProperty(false);
120120
private final BooleanProperty nonUndoableChangeProperty = new SimpleBooleanProperty(false);
121+
private final NavigationHistory navigationHistory = new NavigationHistory();
122+
private final BooleanProperty canGoBackProperty = new SimpleBooleanProperty(false);
123+
private final BooleanProperty canGoForwardProperty = new SimpleBooleanProperty(false);
124+
private boolean backOrForwardNavigationActionTriggered = false;
125+
121126

122127
private BibDatabaseContext bibDatabaseContext;
123128

@@ -490,6 +495,16 @@ private void createMainTable() {
490495
mainTable.addSelectionListener(event -> {
491496
List<BibEntry> entries = event.getList().stream().map(BibEntryTableViewModel::getEntry).toList();
492497
stateManager.setSelectedEntries(entries);
498+
499+
// track navigation history for single selections
500+
if (entries.size() == 1) {
501+
newEntryShowing(entries.getFirst());
502+
} else if (entries.isEmpty()) {
503+
// an empty selection isn't a navigational step, so we don't alter the history list
504+
// this avoids adding a "null" entry to the back/forward stack
505+
// we just refresh the UI button states to ensure they are consistent with the latest history.
506+
updateNavigationState();
507+
}
493508
});
494509
}
495510

@@ -964,6 +979,48 @@ public void resetChangedProperties() {
964979
this.changedProperty.setValue(false);
965980
}
966981

982+
public void back() {
983+
navigationHistory.back().ifPresent(this::navigateToEntry);
984+
}
985+
986+
public void forward() {
987+
navigationHistory.forward().ifPresent(this::navigateToEntry);
988+
}
989+
990+
private void navigateToEntry(BibEntry entry) {
991+
backOrForwardNavigationActionTriggered = true;
992+
clearAndSelect(entry);
993+
updateNavigationState();
994+
}
995+
996+
public boolean canGoBack() {
997+
return navigationHistory.canGoBack();
998+
}
999+
1000+
public boolean canGoForward() {
1001+
return navigationHistory.canGoForward();
1002+
}
1003+
1004+
private void newEntryShowing(BibEntry entry) {
1005+
// skip history updates if this is from a back/forward operation
1006+
if (backOrForwardNavigationActionTriggered) {
1007+
backOrForwardNavigationActionTriggered = false;
1008+
return;
1009+
}
1010+
1011+
navigationHistory.add(entry);
1012+
updateNavigationState();
1013+
}
1014+
1015+
/**
1016+
* Updates the StateManager with current navigation state
1017+
* Only update if this is the active tab
1018+
*/
1019+
public void updateNavigationState() {
1020+
canGoBackProperty.set(canGoBack());
1021+
canGoForwardProperty.set(canGoForward());
1022+
}
1023+
9671024
/**
9681025
* Creates a new library tab. Contents are loaded by the {@code dataLoadingTask}. Most of the other parameters are required by {@code resetChangeMonitor()}.
9691026
*
@@ -1034,6 +1091,14 @@ public static LibraryTab createLibraryTab(@NonNull BibDatabaseContext databaseCo
10341091
false);
10351092
}
10361093

1094+
public BooleanProperty canGoBackProperty() {
1095+
return canGoBackProperty;
1096+
}
1097+
1098+
public BooleanProperty canGoForwardProperty() {
1099+
return canGoForwardProperty;
1100+
}
1101+
10371102
private class GroupTreeListener {
10381103

10391104
@Subscribe
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package org.jabref.gui;
2+
3+
import java.util.ArrayList;
4+
import java.util.List;
5+
import java.util.Objects;
6+
import java.util.Optional;
7+
8+
import org.jabref.model.entry.BibEntry;
9+
10+
/**
11+
* Manages the navigation history of viewed entries using two stacks.
12+
* This class encapsulates the logic of moving back and forward by maintaining a "back" stack for past entries
13+
* and a "forward" stack for future entries.
14+
*/
15+
public class NavigationHistory {
16+
private final List<BibEntry> previousEntries = new ArrayList<>();
17+
private final List<BibEntry> nextEntries = new ArrayList<>();
18+
private BibEntry currentEntry;
19+
20+
/**
21+
* Sets a new entry as the current one, clearing the forward history.
22+
* The previously current entry is moved to the back stack.
23+
*
24+
* @param entry The BibEntry to add to the history.
25+
*/
26+
public void add(BibEntry entry) {
27+
if (Objects.equals(currentEntry, entry)) {
28+
return;
29+
}
30+
31+
// a new selection invalidates the forward history
32+
nextEntries.clear();
33+
34+
if (currentEntry != null) {
35+
previousEntries.add(currentEntry);
36+
}
37+
currentEntry = entry;
38+
}
39+
40+
/**
41+
* Moves to the previous entry in the history.
42+
* The current entry is pushed to the forward stack, and the last entry from the back stack becomes current.
43+
*
44+
* @return An Optional containing the previous BibEntry, or an empty Optional if there's no history to go back to.
45+
*/
46+
public Optional<BibEntry> back() {
47+
if (canGoBack()) {
48+
nextEntries.add(currentEntry);
49+
currentEntry = previousEntries.removeLast();
50+
return Optional.of(currentEntry);
51+
}
52+
return Optional.empty();
53+
}
54+
55+
/**
56+
* Moves to the next entry in the history.
57+
* The current entry is pushed to the back stack, and the last entry from the forward stack becomes current.
58+
*
59+
* @return An Optional containing the next BibEntry, or an empty Optional if there is no "forward" history.
60+
*/
61+
public Optional<BibEntry> forward() {
62+
if (canGoForward()) {
63+
previousEntries.add(currentEntry);
64+
currentEntry = nextEntries.removeLast();
65+
return Optional.of(currentEntry);
66+
}
67+
return Optional.empty();
68+
}
69+
70+
public boolean canGoBack() {
71+
return !previousEntries.isEmpty();
72+
}
73+
74+
public boolean canGoForward() {
75+
return !nextEntries.isEmpty();
76+
}
77+
}

jabgui/src/main/java/org/jabref/gui/StateManager.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,4 +108,8 @@ public interface StateManager extends SrvStateManager {
108108
void setActiveWalkthrough(Walkthrough walkthrough);
109109

110110
Optional<Walkthrough> getActiveWalkthrough();
111+
112+
BooleanProperty canGoBackProperty();
113+
114+
BooleanProperty canGoForwardProperty();
111115
}

jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ public enum StandardActions implements Action {
9595
MANAGE_KEYWORDS(Localization.lang("Manage keywords")),
9696
MASS_SET_FIELDS(Localization.lang("Manage field names & content")),
9797

98+
BACK(Localization.lang("Back"), IconTheme.JabRefIcons.LEFT, KeyBinding.BACK),
99+
FORWARD(Localization.lang("Forward"), Localization.lang("Forward"), IconTheme.JabRefIcons.RIGHT, KeyBinding.FORWARD),
100+
98101
AUTOMATIC_FIELD_EDITOR(Localization.lang("Automatic field editor")),
99102
TOGGLE_GROUPS(Localization.lang("Groups"), IconTheme.JabRefIcons.TOGGLE_GROUPS, KeyBinding.TOGGLE_GROUPS_INTERFACE),
100103
TOGGLE_OO(Localization.lang("OpenOffice/LibreOffice"), IconTheme.JabRefIcons.FILE_OPENOFFICE, KeyBinding.OPEN_OPEN_OFFICE_LIBRE_OFFICE_CONNECTION),

jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import javafx.beans.InvalidationListener;
1212
import javafx.beans.binding.Bindings;
1313
import javafx.beans.property.ObjectProperty;
14+
import javafx.beans.property.SimpleBooleanProperty;
1415
import javafx.beans.property.SimpleObjectProperty;
1516
import javafx.beans.value.ObservableValue;
1617
import javafx.collections.ListChangeListener;
@@ -371,6 +372,14 @@ private void initKeyBindings() {
371372
case NEW_INPROCEEDINGS:
372373
new NewEntryAction(StandardEntryType.InProceedings, this::getCurrentLibraryTab, dialogService, preferences, stateManager).execute();
373374
break;
375+
case BACK:
376+
Optional.ofNullable(getCurrentLibraryTab()).ifPresent(LibraryTab::back);
377+
event.consume();
378+
break;
379+
case FORWARD:
380+
Optional.ofNullable(getCurrentLibraryTab()).ifPresent(LibraryTab::forward);
381+
event.consume();
382+
break;
374383
default:
375384
}
376385
}
@@ -452,6 +461,22 @@ private void initBindings() {
452461
// Hide tab bar
453462
stateManager.getOpenDatabases().addListener((ListChangeListener<BibDatabaseContext>) _ -> updateTabBarVisible());
454463
EasyBind.subscribe(preferences.getWorkspacePreferences().hideTabBarProperty(), _ -> updateTabBarVisible());
464+
465+
stateManager.canGoBackProperty().bind(
466+
stateManager.activeTabProperty().flatMap(
467+
optionalTab -> optionalTab
468+
.map(LibraryTab::canGoBackProperty)
469+
.orElse(new SimpleBooleanProperty(false))
470+
)
471+
);
472+
473+
stateManager.canGoForwardProperty().bind(
474+
stateManager.activeTabProperty().flatMap(
475+
optionalTab -> optionalTab
476+
.map(LibraryTab::canGoForwardProperty)
477+
.orElse(new SimpleBooleanProperty(false))
478+
)
479+
);
455480
}
456481

457482
private void updateTabBarVisible() {

jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.jabref.gui.LibraryTabContainer;
1919
import org.jabref.gui.StateManager;
2020
import org.jabref.gui.actions.ActionFactory;
21+
import org.jabref.gui.actions.SimpleCommand;
2122
import org.jabref.gui.actions.StandardActions;
2223
import org.jabref.gui.citationkeypattern.GenerateCitationKeyAction;
2324
import org.jabref.gui.cleanup.CleanupAction;
@@ -57,6 +58,8 @@ public class MainToolBar extends ToolBar {
5758
private final TaskExecutor taskExecutor;
5859
private final BibEntryTypesManager entryTypesManager;
5960
private final ClipBoardManager clipBoardManager;
61+
private SimpleCommand backCommand;
62+
private SimpleCommand forwardCommand;
6063
private final CountingUndoManager undoManager;
6164

6265
private PopOver entryFromIdPopOver;
@@ -99,6 +102,7 @@ private void createToolBar() {
99102

100103
final Button pushToApplicationButton = factory.createIconButton(pushToApplicationCommand.getAction(), pushToApplicationCommand);
101104
pushToApplicationCommand.registerReconfigurable(pushToApplicationButton);
105+
initNavigationCommands();
102106

103107
// Setup Toolbar
104108

@@ -121,6 +125,12 @@ private void createToolBar() {
121125

122126
new Separator(Orientation.VERTICAL),
123127

128+
new HBox(
129+
factory.createIconButton(StandardActions.BACK, backCommand),
130+
factory.createIconButton(StandardActions.FORWARD, forwardCommand)),
131+
132+
new Separator(Orientation.VERTICAL),
133+
124134
new HBox(
125135
factory.createIconButton(StandardActions.UNDO, new UndoAction(frame::getCurrentLibraryTab, undoManager, dialogService, stateManager)),
126136
factory.createIconButton(StandardActions.REDO, new RedoAction(frame::getCurrentLibraryTab, undoManager, dialogService, stateManager)),
@@ -208,4 +218,32 @@ Group createTaskIndicator() {
208218

209219
return new Group(indicator);
210220
}
221+
222+
private void initNavigationCommands() {
223+
backCommand = new SimpleCommand() {
224+
{
225+
executable.bind(stateManager.canGoBackProperty());
226+
}
227+
228+
@Override
229+
public void execute() {
230+
if (frame.getCurrentLibraryTab() != null) {
231+
frame.getCurrentLibraryTab().back();
232+
}
233+
}
234+
};
235+
236+
forwardCommand = new SimpleCommand() {
237+
{
238+
executable.bind(stateManager.canGoForwardProperty());
239+
}
240+
241+
@Override
242+
public void execute() {
243+
if (frame.getCurrentLibraryTab() != null) {
244+
frame.getCurrentLibraryTab().forward();
245+
}
246+
}
247+
};
248+
}
211249
}

jabgui/src/main/java/org/jabref/gui/keyboard/KeyBinding.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ public enum KeyBinding {
6969
IMPORT_INTO_NEW_DATABASE("Import into new library", Localization.lang("Import into new library"), "ctrl+alt+I", KeyBindingCategory.FILE),
7070
MERGE_ENTRIES("Merge entries", Localization.lang("Merge entries"), "ctrl+M", KeyBindingCategory.TOOLS),
7171

72+
BACK("Back", Localization.lang("Back"), "alt+LEFT", KeyBindingCategory.VIEW),
73+
FORWARD("Forward", Localization.lang("Forward"), "alt+RIGHT", KeyBindingCategory.VIEW),
74+
7275
ADD_ENTRY("Add entry", Localization.lang("Add entry"), "ctrl+N", KeyBindingCategory.BIBTEX),
7376
ADD_ENTRY_IDENTIFIER("Enter identifier", Localization.lang("Enter identifier"), "ctrl+alt+shift+N", KeyBindingCategory.BIBTEX),
7477
ADD_ENTRY_PLAINTEXT("Interpret citations", Localization.lang("Interpret citations"), "ctrl+shift+N", KeyBindingCategory.BIBTEX),

jablib/src/main/resources/l10n/JabRef_en.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3401,3 +3401,6 @@ Commit\ aborted\:\ Local\ repository\ has\ unresolved\ merge\ conflicts.=Commit
34013401
Commit\ aborted\:\ Path\ is\ not\ inside\ a\ Git\ repository.=Commit aborted: Path is not inside a Git repository.
34023402
Commit\ aborted\:\ The\ file\ is\ not\ under\ Git\ version\ control.=Commit aborted: The file is not under Git version control.
34033403
Update\ references=Update references
3404+
3405+
Back=Back
3406+
Forward=Forward

0 commit comments

Comments
 (0)