Skip to content
Open
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We fixed an issue where the button shape changed when hovering over it. [#16188](https://github.com/JabRef/jabref/issues/16188)
- We fixed handling of `exit` in the LSP server. [#16268](https://github.com/JabRef/jabref/pull/16268)
- We fixed an issue where `LinkedFile.isOnlineLink()` did not recognize `ftp://` links as online links. [#16400](https://github.com/JabRef/jabref/issues/16400)
- We fixed an issue where the "Jump to field" dialog only searched fields already shown in the entry editor. It now searches all known fields and adds the selected field if it is not yet visible. [#16593](https://github.com/JabRef/jabref/issues/16593)

### Removed

Expand Down
45 changes: 33 additions & 12 deletions jabgui/src/main/java/org/jabref/gui/entryeditor/AllFieldsTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ public class AllFieldsTab extends FieldsEditorTab {
private final Map<FieldListSections.SectionType, Boolean> sectionExpandOverrides =
new EnumMap<>(FieldListSections.SectionType.class);

/// Tracks the [TitledPane] created for each section type, so we can expand a collapsed
/// section on demand (e.g. when jump-to-field targets a field inside it).
private final Map<FieldListSections.SectionType, TitledPane> sectionPanes =
new EnumMap<>(FieldListSections.SectionType.class);

/// Sticky per tab instance: whether the secondary-optional chips are expanded.
private boolean showSecondaryOptionalChips;

Expand Down Expand Up @@ -253,13 +258,22 @@ private void refreshShownFieldsIfNeeded(FieldChangedEvent event) {
}
}

@Override
public void requestFocus(Field fieldName) {
Optional.ofNullable(sectionPanes.get(FieldListSections.sectionOf(fieldName)))
.filter(pane -> !pane.isExpanded())
.ifPresent(pane -> pane.setExpanded(true));
super.requestFocus(fieldName);
}

/// Main fields as a grid with natural row heights, then the optional-field chip bar,
/// then the always-present collapsible sections (identifiers / files & links /
/// bibliometrics / comments / meta, collapsed when empty) each with its own add-chips,
/// then the free-form add row. The whole column scrolls instead of stretching to the
/// tab height.
@Override
protected void layoutEditors(BibDatabaseContext bibDatabaseContext, BibEntry entry, boolean compressed, List<Label> labels) {
sectionPanes.clear();
// labels were created in editors-map iteration order (see FieldsEditorTab#setupPanel)
Map<Field, Label> labelForField = new LinkedHashMap<>();
int labelIndex = 0;
Expand Down Expand Up @@ -444,6 +458,7 @@ private TitledPane createSectionPane(FieldListSections.SectionType type,
if (pane.isExpanded()) {
populateContent.run();
}
sectionPanes.put(type, pane);
return pane;
}

Expand Down Expand Up @@ -571,21 +586,27 @@ private void showFieldEditor(BibDatabaseContext bibDatabaseContext, BibEntry ent
userAddedFields.add(field);
rebuildPanel(bibDatabaseContext, entry);
Platform.runLater(() -> {
// The tab may have been rebound to a different entry before this deferred block runs;
// the editors map would then belong to that other entry, so focusing/adding here would
// act on the wrong entry. Bail out unless we are still showing the entry we started with.
if (getCurrentEntry() != entry) {
return;
}
requestFocus(field);
// Adding the File field via its "+" chip should immediately open the add-file dialog,
// since an empty File editor has no other purpose than to receive a file.
if ((StandardField.FILE == field) && (editors.get(field) instanceof LinkedFilesEditor linkedFilesEditor)) {
linkedFilesEditor.addNewFile();
}
// Re-check the staleness guard inside this inner block because the entry may have changed between the two pulses.
Platform.runLater(() -> {
if (getCurrentEntry() != entry) {
return;
}
requestFocus(field);
// Adding the File field via its "+" chip should immediately open the add-file dialog,
// since an empty File editor has no other purpose than to receive a file.
if ((StandardField.FILE == field) && (editors.get(field) instanceof LinkedFilesEditor linkedFilesEditor)) {
linkedFilesEditor.addNewFile();
}
});
});
}

/// Adds `field` to the entry's field list (if not already shown) and focuses it.
public void addFieldAndFocus(Field field) {
Optional.ofNullable(getCurrentEntry())
.ifPresent(entry -> showFieldEditor(activeDatabaseContext(), entry, field));
}

private void rebuildPanel(BibDatabaseContext bibDatabaseContext, BibEntry entry) {
setupPanel(bibDatabaseContext, entry, false);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,10 +254,6 @@ private void setupKeyBindings() {
tabSupplier.get().selectPreviousEntry();
event.consume();
}
case JUMP_TO_FIELD -> {
openJumpToFieldDialog();
event.consume();
}
case HELP -> {
new HelpAction(HelpFile.ENTRY_EDITOR, dialogService, preferences.getExternalApplicationsPreferences()).execute();
event.consume();
Expand Down Expand Up @@ -298,7 +294,9 @@ private void jumpToFieldButton() {
openJumpToFieldDialog();
}

private void openJumpToFieldDialog() {
/// Shows the jump-to-field dialog for the currently edited entry.
/// Handled globally in [org.jabref.gui.frame.JabRefFrame] so that it works regardless of where the keyboard focus lies.
public void openJumpToFieldDialog() {
if (jumpToFieldDialog != null && jumpToFieldDialog.isShowing()) {
BaseDialog.bringToFront(jumpToFieldDialog);
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ void setFocusToField(Field field) {
tab -> selectTabAndField(tab, field),
() -> {
Field aliasField = EntryConverter.FIELD_ALIASES.get(field);
getTabContainingField(aliasField).ifPresent(tab -> selectTabAndField(tab, aliasField));
getTabContainingField(aliasField).ifPresentOrElse(
tab -> selectTabAndField(tab, aliasField),
() -> addFieldViaAllFieldsTab(Optional.ofNullable(aliasField).orElse(field)));
}
));
}
Expand All @@ -85,6 +87,20 @@ private Optional<FieldsEditorTab> getTabContainingField(Field field) {
.findFirst();
}

private void addFieldViaAllFieldsTab(Field field) {
if (!FieldFactory.getAllFieldsWithOutInternal().contains(field)) {
return;
}
tabPane.getTabs().stream()
.filter(AllFieldsTab.class::isInstance)
.map(AllFieldsTab.class::cast)
.findFirst()
.ifPresent(allFieldsTab -> {
tabPane.getSelectionModel().select(allFieldsTab);
allFieldsTab.addFieldAndFocus(field);
});
}

private void selectTabAndField(FieldsEditorTab tab, Field field) {
Platform.runLater(() -> {
tabPane.getSelectionModel().select(tab);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package org.jabref.gui.entryeditor;

import java.util.List;
import java.util.stream.Collectors;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import org.jabref.gui.AbstractViewModel;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;

public class JumpToFieldViewModel extends AbstractViewModel {

Expand All @@ -27,13 +27,10 @@ public List<String> getFieldNames() {
return List.of();
}

return entryEditor.getAllPossibleTabs().stream()
.filter(FieldsEditorTab.class::isInstance)
.map(FieldsEditorTab.class::cast)
.flatMap(tab -> tab.getShownFields().stream())
.map(Field::getName)
.distinct()
.sorted()
.collect(Collectors.toList());
return FieldFactory.getAllFieldsWithOutInternal().stream()
.map(Field::getName)
.distinct()
.sorted()
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

import javafx.application.Platform;
import javafx.beans.property.StringProperty;
import javafx.geometry.Bounds;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextInputControl;
import javafx.scene.input.KeyEvent;

Expand Down Expand Up @@ -137,11 +141,46 @@ private void setTextAndUpdateCaretPosition(TextInputControl textInputControl, St
Parent getNode();

default void focus() {
getNode().getChildrenUnmodifiable()
.stream()
.findFirst()
.orElse(getNode())
.requestFocus();
Node target = findTextInput(getNode()).map(input -> (Node) input).orElseGet(this::getNode);
target.requestFocus();
Platform.runLater(() -> Optional.ofNullable(target.getScene()).ifPresent(_ -> scrollToVisible(target)));
}

private static Optional<TextInputControl> findTextInput(Node node) {
if (node instanceof TextInputControl textInput) {
return Optional.of(textInput);
}
if (node instanceof Parent parent) {
return parent.getChildrenUnmodifiable().stream()
.map(FieldEditorFX::findTextInput)
.flatMap(Optional::stream)
.findFirst();
}
return Optional.empty();
}

private static void scrollToVisible(Node node) {
Node current = node.getParent();
while (current != null) {
if (current instanceof ScrollPane scrollPane) {
Node content = scrollPane.getContent();
if (content == null) {
return;
}
double viewportHeight = scrollPane.getViewportBounds().getHeight();
double contentHeight = content.getBoundsInLocal().getHeight();
if (contentHeight <= viewportHeight) {
return;
}
Bounds targetInContent = content.sceneToLocal(node.localToScene(node.getBoundsInLocal()));
double targetCenterY = targetInContent.getCenterY();
double maxScrollY = contentHeight - viewportHeight;
double desiredScrollY = targetCenterY - (viewportHeight / 2);
scrollPane.setVvalue(Math.clamp(desiredScrollY / maxScrollY, 0, 1));
return;
}
current = current.getParent();
}
}

/// Returns relative size of the field editor in terms of display space.
Expand Down
7 changes: 7 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 @@ -397,6 +397,13 @@ private void initKeyBindings() {
event.consume();
}
break;
case JUMP_TO_FIELD:
// Handled here so that it also fires when the keyboard focus is outside the entry editor
if (stateManager.getEditorShowing().get()) {
entryEditor.openJumpToFieldDialog();
event.consume();
}
break;
case CLOSE_DATABASE:
new CloseDatabaseAction(this, stateManager).execute();
event.consume();
Expand Down