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