Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
79b5fc5
Fix TAB navigation in EntryEditor to move focus to next tab's field
tejjgv Sep 12, 2025
77cf911
Merge branch 'JabRef:main' into tab-navigation-test
tejjgv Sep 12, 2025
e0ddc5b
- Fixed TAB navigation in the EntryEditor so that pressing TAB now mo…
tejjgv Sep 13, 2025
4ac7bb9
Merge branch 'main' of https://github.com/tejjgv/jabref
tejjgv Sep 13, 2025
ee1f72d
- Fixed TAB navigation in the EntryEditor so that pressing TAB now mo…
tejjgv Sep 13, 2025
13425c9
-Fixed TAB navigation in the EntryEditor so that pressing TAB now mov…
tejjgv Sep 13, 2025
af87b0f
Replace null returns with Optional
tejjgv Sep 13, 2025
dce3209
Update jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
tejjgv Sep 13, 2025
c22c466
Replace null returns with Optional
tejjgv Sep 13, 2025
236277c
Merge remote-tracking branch 'upstream/main' into fix-issue-11937
tejjgv Sep 13, 2025
c40c287
chore: trigger CI rerun
tejjgv Sep 13, 2025
70c99ea
chore: remove dummy.txt
tejjgv Sep 13, 2025
90b9bbe
Use KeyCode.TAB instead of text comparison for tab key detection
tejjgv Sep 14, 2025
3a48756
Merge branch 'main' into fix-issue-11937
tejjgv Sep 14, 2025
e9774b4
Merge branch 'main' into fix-issue-11937
calixtus Sep 14, 2025
54d51e2
Update CHANGELOG.md
koppor Sep 14, 2025
e75bbea
Update jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
koppor Sep 14, 2025
7a16a93
Reformat
calixtus Sep 14, 2025
c777854
Refine tab to move focus even when the last item is a button
tejjgv Sep 19, 2025
e705c3f
Merge branch 'fix-issue-11937' of https://github.com/tejjgv/jabref in…
tejjgv Sep 19, 2025
60a53aa
Merge branch 'JabRef:main' into fix-issue-11937
tejjgv Sep 19, 2025
ab99d8e
Refine tab to move focus even when the last item is a button
tejjgv Sep 19, 2025
beae442
Refine tab to move focus even when the last item is a button
tejjgv Sep 19, 2025
5074fb7
added supress warning
tejjgv Sep 20, 2025
c02a9a2
Merge branch 'main' into fix-issue-11937
tejjgv Sep 26, 2025
6f1a1b0
removed static injection
tejjgv Sep 27, 2025
38c4984
Merge branch 'fix-issue-11937' of https://github.com/tejjgv/jabref in…
tejjgv Sep 27, 2025
a45dd3c
removed static injection
tejjgv Sep 27, 2025
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 @@ -98,6 +98,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

### Fixed

- We fixed an issue where pressing <kbd>Tab</kbd> in the last text field of a tab did not move the focus to the next tab in the entry editor. [#11937](https://github.com/JabRef/jabref/issues/11937)
- When filename pattern is missing for linked files, pattern handling has been introduced to avoid suggesting meaningless filenames like "-". [#13735](https://github.com/JabRef/jabref/issues/13735)
- We fixed an issue where "Specify Bib(La)TeX" tab was not focused when Bib(La)TeX was in the clipboard [#13597](https://github.com/JabRef/jabref/issues/13597)
- We fixed an issue whereby the 'About' dialog was not honouring the user's configured font preferences. [#13558](https://github.com/JabRef/jabref/issues/13558)
Expand Down
26 changes: 26 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@

import javafx.collections.ObservableList;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.RowConstraints;

Expand Down Expand Up @@ -136,6 +139,8 @@ protected void setupPanel(BibDatabaseContext bibDatabaseContext, BibEntry entry,
if (!entry.hasField(userSpecificCommentField)) {
if (shouldShowHideButton) {
Button hideDefaultOwnerCommentButton = new Button(Localization.lang("Hide user-specific comments field"));
hideDefaultOwnerCommentButton.setId("HIDE_COMMENTS_BUTTON");
setupButtonTabNavigation(hideDefaultOwnerCommentButton);
hideDefaultOwnerCommentButton.setOnAction(e -> {
gridPane.getChildren().removeIf(node ->
(node instanceof FieldNameLabel fieldNameLabel && fieldNameLabel.getText().equals(userSpecificCommentField.getName()))
Expand All @@ -152,6 +157,8 @@ protected void setupPanel(BibDatabaseContext bibDatabaseContext, BibEntry entry,
} else {
// Show "Show" button when user comments field is hidden
Button showDefaultOwnerCommentButton = new Button(Localization.lang("Show user-specific comments field"));
showDefaultOwnerCommentButton.setId("SHOW_COMMENTS_BUTTON");
setupButtonTabNavigation(showDefaultOwnerCommentButton);
showDefaultOwnerCommentButton.setOnAction(e -> {
shouldShowHideButton = true;
setupPanel(bibDatabaseContext, entry, false);
Expand All @@ -162,4 +169,23 @@ protected void setupPanel(BibDatabaseContext bibDatabaseContext, BibEntry entry,
}
}
}

private void setupButtonTabNavigation(Button button) {
button.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.TAB && !event.isShiftDown()) {
// Find the EntryEditor in the parent hierarchy
Node parent = button.getParent();
while (parent != null && !(parent instanceof EntryEditor)) {
parent = parent.getParent();
}

if (parent instanceof EntryEditor entryEditor) {
if (entryEditor.isLastFieldInCurrentTab(button)) {
entryEditor.moveToNextTabAndFocus();
event.consume();
}
}
}
});
}
}
130 changes: 130 additions & 0 deletions jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -18,12 +19,15 @@
import javafx.beans.InvalidationListener;
import javafx.fxml.FXML;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.DataFormat;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.TransferMode;
Expand All @@ -38,6 +42,7 @@
import org.jabref.gui.entryeditor.fileannotationtab.FileAnnotationTab;
import org.jabref.gui.entryeditor.fileannotationtab.FulltextSearchResultsTab;
import org.jabref.gui.externalfiles.ExternalFilesEntryLinker;
import org.jabref.gui.fieldeditors.EditorTextField;
import org.jabref.gui.help.HelpAction;
import org.jabref.gui.importer.GrobidUseDialogHelper;
import org.jabref.gui.keyboard.KeyBinding;
Expand Down Expand Up @@ -167,6 +172,9 @@ public EntryEditor(Supplier<LibraryTab> tabSupplier, UndoAction undoAction, Redo
EntryEditorTab activeTab = (EntryEditorTab) tab;
if (activeTab != null) {
activeTab.notifyAboutFocus(currentlyEditedEntry);
if (activeTab instanceof FieldsEditorTab fieldsTab) {
Platform.runLater(() -> setupNavigationForTab(fieldsTab));
}
}
});

Expand Down Expand Up @@ -222,6 +230,23 @@ private void setupDragAndDrop() {
});
}

private void setupNavigationForTab(FieldsEditorTab tab) {
Node content = tab.getContent();
if (content instanceof Parent parent) {
findAndSetupEditorTextFields(parent);
}
}

private void findAndSetupEditorTextFields(Parent parent) {
for (Node child : parent.getChildrenUnmodifiable()) {
if (child instanceof EditorTextField editor) {
editor.setupTabNavigation(this::isLastFieldInCurrentTab, this::moveToNextTabAndFocus);
} else if (child instanceof Parent childParent) {
findAndSetupEditorTextFields(childParent);
}
}
}

/**
* Set up key bindings specific for the entry editor.
*/
Expand Down Expand Up @@ -445,6 +470,13 @@ public void setCurrentlyEditedEntry(@NonNull BibEntry currentlyEditedEntry) {
if (preferences.getEntryEditorPreferences().showSourceTabByDefault()) {
tabbed.getSelectionModel().select(sourceTab);
}
Platform.runLater(() -> {
for (Tab tab : tabbed.getTabs()) {
if (tab instanceof FieldsEditorTab fieldsTab) {
setupNavigationForTab(fieldsTab);
}
}
});
}

private EntryEditorTab getSelectedTab() {
Expand Down Expand Up @@ -528,4 +560,102 @@ public void nextPreviewStyle() {
public void previousPreviewStyle() {
this.previewPanel.previousPreviewStyle();
}

/**
* Checks if the given TextField is the last field in the currently selected tab.
*
* @param node the Node to check
* @return true if this is the last field in the current tab, false otherwise
*/
boolean isLastFieldInCurrentTab(Node node) {
if (node == null || tabbed.getSelectionModel().getSelectedItem() == null) {
return false;
}

Tab selectedTab = tabbed.getSelectionModel().getSelectedItem();
if (!(selectedTab instanceof FieldsEditorTab currentTab)) {
return false;
}

Collection<Field> shownFields = currentTab.getShownFields();
if (shownFields.isEmpty() || node.getId() == null) {
return false;
}

Optional<Field> lastField = shownFields.stream()
.reduce((first, second) -> second);

if (node instanceof Button) {
return true;
}

return lastField.map(Field::getName)
.map(displayName -> displayName.equalsIgnoreCase(node.getId()))
.orElse(false);
}

/**
* Moves to the next tab and focuses on its first field.
*/
void moveToNextTabAndFocus() {
tabbed.getSelectionModel().selectNext();

Platform.runLater(() -> {
Tab selectedTab = tabbed.getSelectionModel().getSelectedItem();
if (selectedTab instanceof FieldsEditorTab currentTab) {
focusFirstFieldInTab(currentTab);
}
});
}

private void focusFirstFieldInTab(FieldsEditorTab tab) {
Node tabContent = tab.getContent();
if (tabContent instanceof Parent parent) {
// First try to find field by ID (preferred method)
Collection<Field> shownFields = tab.getShownFields();
if (!shownFields.isEmpty()) {
Field firstField = shownFields.iterator().next();
String firstFieldId = firstField.getName();
Optional<TextInputControl> firstTextInput = findTextInputById(parent, firstFieldId);
if (firstTextInput.isPresent()) {
firstTextInput.get().requestFocus();
return;
}
}

Optional<TextInputControl> anyTextInput = findAnyTextInput(parent);
if (anyTextInput.isPresent()) {
anyTextInput.get().requestFocus();
}
}
}

/// Recursively searches for a TextInputControl (TextField or TextArea) with the given ID.
private Optional<TextInputControl> findTextInputById(Parent parent, String id) {
for (Node child : parent.getChildrenUnmodifiable()) {
if (child instanceof TextInputControl textInput && id.equalsIgnoreCase(textInput.getId())) {
return Optional.of(textInput);
} else if (child instanceof Parent childParent) {
Optional<TextInputControl> found = findTextInputById(childParent, id);
if (found.isPresent()) {
return found;
}
}
}
return Optional.empty();
}

private Optional<TextInputControl> findAnyTextInput(Parent parent) {
for (Node child : parent.getChildrenUnmodifiable()) {
if (child instanceof TextInputControl textInput) {
return Optional.of(textInput);
} else if (child instanceof Parent childParent) {
Optional<TextInputControl> found = findAnyTextInput(childParent);
if (found.isPresent()) {
return found;
}
}
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ public CitationKeyEditor(Field field,
undoManager,
dialogService);

textField.setId(field.getName());

establishBinding(textField, viewModel.textProperty(), keyBindingRepository, undoAction, redoAction);
textField.initContextMenu(Collections::emptyList, keyBindingRepository);
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textField);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;
import java.util.function.Predicate;
import java.util.function.Supplier;

import javafx.fxml.Initializable;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;

Expand All @@ -20,13 +23,26 @@

public class EditorTextField extends TextField implements Initializable, ContextMenuAddable {

private Runnable nextTabSelector;
private Predicate<TextField> isLastFieldChecker;
private final ContextMenu contextMenu = new ContextMenu();

private Runnable additionalPasteActionHandler = () -> {
// No additional paste behavior
};

public EditorTextField() {
this("");
this.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.TAB &&
isLastFieldChecker != null &&
isLastFieldChecker.test(this)) {
if (nextTabSelector != null) {
nextTabSelector.run();
}
event.consume();
}
});
}

public EditorTextField(final String text) {
Expand All @@ -39,6 +55,11 @@ public EditorTextField(final String text) {
ClipBoardManager.addX11Support(this);
}

public void setupTabNavigation(Predicate<TextField> isLastFieldChecker, Runnable nextTabSelector) {
Copy link

Choose a reason for hiding this comment

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

The method 'setupTabNavigation' allows null values for 'isLastFieldChecker' and 'nextTabSelector', which could lead to runtime errors. Consider using Optional to avoid null checks.

Copy link
Member

Choose a reason for hiding this comment

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

Use JSpecify annotations

this.isLastFieldChecker = isLastFieldChecker;
this.nextTabSelector = nextTabSelector;
}

@Override
public void initContextMenu(final Supplier<List<MenuItem>> items, KeyBindingRepository keyBindingRepository) {
setOnContextMenuRequested(event -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public MarkdownEditor(Field field, SuggestionProvider<?> suggestionProvider, Fie
}

@Override
protected TextInputControl createTextInputControl() {
protected TextInputControl createTextInputControl(@SuppressWarnings("unused") Field field) {
return new EditorTextArea() {
@Override
public void paste() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public PersonsEditor(final Field field,

this.viewModel = new PersonsEditorViewModel(field, suggestionProvider, preferences.getAutoCompletePreferences(), fieldCheckers, undoManager);
textInput = isMultiLine ? new EditorTextArea() : new EditorTextField();
textInput.setId(field.getName());
decoratedStringProperty = new UiThreadStringProperty(viewModel.textProperty());
establishBinding(textInput, decoratedStringProperty, keyBindingRepository, undoAction, redoAction);
((ContextMenuAddable) textInput).initContextMenu(EditorMenus.getNameMenu(textInput), keyBindingRepository);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public SimpleEditor(final Field field,
this.viewModel = new SimpleEditorViewModel(field, suggestionProvider, fieldCheckers, undoManager);
this.isMultiLine = isMultiLine;

textInput = createTextInputControl();
textInput = createTextInputControl(field);
HBox.setHgrow(textInput, Priority.ALWAYS);

establishBinding(textInput, viewModel.textProperty(), preferences.getKeyBindingRepository(), undoAction, redoAction);
Expand All @@ -54,8 +54,10 @@ public SimpleEditor(final Field field,
new EditorValidator(preferences).configureValidation(viewModel.getFieldValidator().getValidationStatus(), textInput);
}

protected TextInputControl createTextInputControl() {
return isMultiLine ? new EditorTextArea() : new EditorTextField();
protected TextInputControl createTextInputControl(Field field) {
TextInputControl inputControl = isMultiLine ? new EditorTextArea() : new EditorTextField();
inputControl.setId(field.getName());
return inputControl;
}

@Override
Expand Down
Loading