diff --git a/CHANGELOG.md b/CHANGELOG.md
index 582b7b9bb32..5e8c2b0696a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -102,6 +102,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
### Fixed
+- We fixed an issue where pressing Tab 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)
diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java
index c1bbee669bc..b3636522304 100644
--- a/jabgui/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java
+++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/CommentsTab.java
@@ -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;
@@ -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()))
@@ -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);
@@ -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();
+ }
+ }
+ }
+ });
+ }
}
diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
index fe4573adb69..895609f78cc 100644
--- a/jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
+++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
@@ -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;
@@ -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;
@@ -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;
@@ -167,6 +172,9 @@ public EntryEditor(Supplier tabSupplier, UndoAction undoAction, Redo
EntryEditorTab activeTab = (EntryEditorTab) tab;
if (activeTab != null) {
activeTab.notifyAboutFocus(currentlyEditedEntry);
+ if (activeTab instanceof FieldsEditorTab fieldsTab) {
+ Platform.runLater(() -> setupNavigationForTab(fieldsTab));
+ }
}
});
@@ -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.
*/
@@ -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() {
@@ -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 shownFields = currentTab.getShownFields();
+ if (shownFields.isEmpty() || node.getId() == null) {
+ return false;
+ }
+
+ Optional 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 shownFields = tab.getShownFields();
+ if (!shownFields.isEmpty()) {
+ Field firstField = shownFields.iterator().next();
+ String firstFieldId = firstField.getName();
+ Optional firstTextInput = findTextInputById(parent, firstFieldId);
+ if (firstTextInput.isPresent()) {
+ firstTextInput.get().requestFocus();
+ return;
+ }
+ }
+
+ Optional anyTextInput = findAnyTextInput(parent);
+ if (anyTextInput.isPresent()) {
+ anyTextInput.get().requestFocus();
+ }
+ }
+ }
+
+ /// Recursively searches for a TextInputControl (TextField or TextArea) with the given ID.
+ private Optional 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 found = findTextInputById(childParent, id);
+ if (found.isPresent()) {
+ return found;
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ private Optional findAnyTextInput(Parent parent) {
+ for (Node child : parent.getChildrenUnmodifiable()) {
+ if (child instanceof TextInputControl textInput) {
+ return Optional.of(textInput);
+ } else if (child instanceof Parent childParent) {
+ Optional found = findAnyTextInput(childParent);
+ if (found.isPresent()) {
+ return found;
+ }
+ }
+ }
+ return Optional.empty();
+ }
}
diff --git a/jabgui/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java b/jabgui/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java
index 53251ba46a2..c97e740a3cd 100644
--- a/jabgui/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java
+++ b/jabgui/src/main/java/org/jabref/gui/fieldeditors/CitationKeyEditor.java
@@ -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);
diff --git a/jabgui/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java b/jabgui/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
index c6fff798320..fafb7ef7c34 100644
--- a/jabgui/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
+++ b/jabgui/src/main/java/org/jabref/gui/fieldeditors/EditorTextField.java
@@ -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;
@@ -20,13 +23,26 @@
public class EditorTextField extends TextField implements Initializable, ContextMenuAddable {
+ private Runnable nextTabSelector;
+ private Predicate 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) {
@@ -39,6 +55,11 @@ public EditorTextField(final String text) {
ClipBoardManager.addX11Support(this);
}
+ public void setupTabNavigation(Predicate isLastFieldChecker, Runnable nextTabSelector) {
+ this.isLastFieldChecker = isLastFieldChecker;
+ this.nextTabSelector = nextTabSelector;
+ }
+
@Override
public void initContextMenu(final Supplier> items, KeyBindingRepository keyBindingRepository) {
setOnContextMenuRequested(event -> {
diff --git a/jabgui/src/main/java/org/jabref/gui/fieldeditors/MarkdownEditor.java b/jabgui/src/main/java/org/jabref/gui/fieldeditors/MarkdownEditor.java
index 7d230d3db2a..7aae8de4ffc 100644
--- a/jabgui/src/main/java/org/jabref/gui/fieldeditors/MarkdownEditor.java
+++ b/jabgui/src/main/java/org/jabref/gui/fieldeditors/MarkdownEditor.java
@@ -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() {
diff --git a/jabgui/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java b/jabgui/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java
index 8a15574dee4..3fe9a6ffeab 100644
--- a/jabgui/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java
+++ b/jabgui/src/main/java/org/jabref/gui/fieldeditors/PersonsEditor.java
@@ -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);
diff --git a/jabgui/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java b/jabgui/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java
index 9b9537c967e..0debe111224 100644
--- a/jabgui/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java
+++ b/jabgui/src/main/java/org/jabref/gui/fieldeditors/SimpleEditor.java
@@ -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);
@@ -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