Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/JabRefGuiStateManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ public class JabRefGuiStateManager implements StateManager {
private final List<AiChatWindow> aiChatWindows = new ArrayList<>();
private final BooleanProperty editorShowing = new SimpleBooleanProperty(false);
private final OptionalObjectProperty<Walkthrough> activeWalkthrough = OptionalObjectProperty.empty();
private final BooleanProperty canGoBack = new SimpleBooleanProperty(false);
private final BooleanProperty canGoForward = new SimpleBooleanProperty(false);

@Override
public ObservableList<SidePaneType> getVisibleSidePaneComponents() {
Expand Down Expand Up @@ -307,4 +309,14 @@ public void setActiveWalkthrough(Walkthrough walkthrough) {
public Optional<Walkthrough> getActiveWalkthrough() {
return activeWalkthrough.get();
}

@Override
public BooleanProperty canGoBackProperty() {
return canGoBack;
}

@Override
public BooleanProperty canGoForwardProperty() {
return canGoForward;
}
}
88 changes: 88 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/LibraryTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -118,6 +119,13 @@ 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 List<BibEntry> previousEntries = new ArrayList<>();
private final List<BibEntry> nextEntries = new ArrayList<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can replace this by one list and an index that points to the currently selected entry:

  • When an entry is selected we add it to the list.
  • When back called we decrement (if possible) the index and navigate to that entry
  • When forward called we increment (if possible) and navigate.

Copy link
Member Author

@subhramit subhramit Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this a bit, I am a bit reluctant about this scenario:
You seelct entry A, then B, then C, then D.

You click Back twice. You are now viewing B.

From B, you click on a new entry, E.

The old "forward" history (entries C, D) must be discarded. The new history should be A -> B -> E.

The logic to truncate the "forward" part of the list will be slightly less cleaner (clearing the forward "sublist" of the original list and adding the new entry) than the way we just do nextEntries.clear() now, but if two people are in favor of this I'll shift to a single list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought whether Eclipse Collections and sublists could help here. However, the current code is very clean and I would keep it.


image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, managing one state is much easier them managing 2+, because you need to keep them in sync: Whenever you modify one you need to think which one needs to also change.

But, you can still make the current design better by encapsulating the forward and backward list into a record.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Record is suitable in a context of immutability. If we wish to encapsulate, can do with a separate class

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactored in commit d890eea, lets see if you guys like this

private BibEntry currentlyShowing;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This misses a connection to org.jabref.gui.maintable.MainTable#clearAndSelect(org.jabref.model.entry.BibEntry).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't get this - there is BibEntry toShow in navigateToEntry, on which this class' clearAndSelect is called, which is indeed MainTable#clearAndSelect internally. I don't think we can use it for currentlyShowing?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My quick thoughts were: If there is an entry selected, I push the back button and then the forward button, the entry should be selected - and not an "arbitrary" one.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh got it. Yeah, no arbitrary entries are selected.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope, its just a binding thing 😅

private final BooleanProperty canGoBackProperty = new SimpleBooleanProperty(false);
private final BooleanProperty canGoForwardProperty = new SimpleBooleanProperty(false);
private boolean backOrForwardNavigation = false;


private BibDatabaseContext bibDatabaseContext;

Expand Down Expand Up @@ -490,6 +498,16 @@ private void createMainTable() {
mainTable.addSelectionListener(event -> {
List<BibEntry> 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();
}
});
}

Expand Down Expand Up @@ -964,6 +982,68 @@ public void resetChangedProperties() {
this.changedProperty.setValue(false);
}

public void back() {
navigateToEntry(previousEntries, nextEntries);
}

public void forward() {
navigateToEntry(nextEntries, previousEntries);
}

private void navigateToEntry(List<BibEntry> sourceHistory, List<BibEntry> destinationHistory) {
if (!sourceHistory.isEmpty()) {
BibEntry toShow = sourceHistory.getLast();
sourceHistory.removeLast();

// add current entry to destination history
if (currentlyShowing != null) {
destinationHistory.add(currentlyShowing);
}

backOrForwardNavigation = true;
clearAndSelect(toShow);
updateNavigationState();
}
}

public boolean canGoBack() {
return !previousEntries.isEmpty();
}

public boolean canGoForward() {
return !nextEntries.isEmpty();
}

private void newEntryShowing(BibEntry entry) {
// skip history updates if this is from a back/forward operation
if (backOrForwardNavigation) {
currentlyShowing = entry;
backOrForwardNavigation = false;
updateNavigationState();
return;
}

nextEntries.clear(); // clear forward history when new entry selected in existing chronological sequence

if (!Objects.equals(entry, currentlyShowing)) {
// add the entry we are leaving to the history
if (currentlyShowing != null) {
previousEntries.add(currentlyShowing);
}
currentlyShowing = 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()}.
*
Expand Down Expand Up @@ -1034,6 +1114,14 @@ public static LibraryTab createLibraryTab(@NonNull BibDatabaseContext databaseCo
false);
}

public BooleanProperty canGoBackProperty() {
return canGoBackProperty;
}

public BooleanProperty canGoForwardProperty() {
return canGoForwardProperty;
}

private class GroupTreeListener {

@Subscribe
Expand Down
4 changes: 4 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/StateManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,8 @@ public interface StateManager extends SrvStateManager {
void setActiveWalkthrough(Walkthrough walkthrough);

Optional<Walkthrough> getActiveWalkthrough();

BooleanProperty canGoBackProperty();

BooleanProperty canGoForwardProperty();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
25 changes: 25 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/JabRefFrame.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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:
}
}
Expand Down Expand Up @@ -452,6 +461,22 @@ private void initBindings() {
// Hide tab bar
stateManager.getOpenDatabases().addListener((ListChangeListener<BibDatabaseContext>) _ -> 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() {
Expand Down
38 changes: 38 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/frame/MainToolBar.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -99,6 +102,7 @@ private void createToolBar() {

final Button pushToApplicationButton = factory.createIconButton(pushToApplicationCommand.getAction(), pushToApplicationCommand);
pushToApplicationCommand.registerReconfigurable(pushToApplicationButton);
initNavigationCommands();

// Setup Toolbar

Expand All @@ -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)),
Expand Down Expand Up @@ -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();
}
}
};
}
}
3 changes: 3 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/keyboard/KeyBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
3 changes: 3 additions & 0 deletions jablib/src/main/resources/l10n/JabRef_en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading