diff --git a/docs/performance-review/entry-editor-first-open-performance.md b/docs/performance-review/entry-editor-first-open-performance.md new file mode 100644 index 000000000000..4b4bdc2d23e7 --- /dev/null +++ b/docs/performance-review/entry-editor-first-open-performance.md @@ -0,0 +1,342 @@ +# Entry editor first-open performance + +This report records the baseline Java Flight Recorder (JFR) evidence for the reported UI freeze when the entry editor is opened for the first time. It accompanies the raw recording so the evidence can be inspected again and compared with later implementations. The investigation is tracked in [JabRef pull request #16464](https://github.com/JabRef/jabref/pull/16464). + +## Result + +The recording supports eager field-editor construction followed by JavaFX CSS and layout work as the main source of the first-open stall. + +- The sampled JavaFX application thread enters `FieldsEditorTab.createLabelAndEditor(...)` while opening the editor. +- Samples then remain in JavaFX CSS selector matching, style-map sorting, CSS reapplication, and recursive bounds calculation. +- The confirmed sample window from field-editor creation to the last CSS-pass sample spans 2.033 seconds. +- A broader window of JavaFX scene, CSS, and layout samples around the interaction spans 3.436 seconds. +- Garbage collection does not explain a multi-second freeze. The longest stop-the-world pause during the relevant window was 8.41 milliseconds. +- No `jdk.JavaMonitorEnter` events were recorded, so the capture does not indicate contended Java-monitor blocking. +- A supplemental run with a library containing linked PDF files reproduced the same JavaFX CSS, control-construction, and layout profile. It recorded no file or socket reads on the JavaFX application thread during the relevant window, so linked-file I/O is not supported as the cause of this occurrence. +- The supplemental run also recorded a burst of JavaFX CSS conversion exceptions. The looked-up-color failures are consistent with [OpenJFX pull request #2225](https://github.com/openjdk/jfx/pull/2225), and the entry-editor construction path initializes ControlsFX validation decoration for many field controls. +- A restored-editor startup run after preinstalling ControlsFX's `DecorationPane` constructs the editor during startup without the prior looked-up-color `ClassCastException` burst. It still performs substantial JavaFX CSS and layout work, so it is evidence that the root replacement moved off the later open-editor path, not a comparable click-to-render improvement measurement. + +These spans are evidence windows between statistical samples, not exact method durations. The recording did not include a custom event marking the menu action or JavaFX pulse-duration events, so it cannot provide an exact click-to-first-render latency. + +## Recording + +The complete recording is stored at: + +- [entry-editor-first-open-before-lazy-loading.jfr](snapshots/entry-editor-first-open-before-lazy-loading.jfr) + +Artifact details: + +| Property | Value | +| --- | --- | +| Format | JFR 2.1 | +| Size | 2,592,250 bytes | +| SHA-256 | `519c927a9ac515003cfefcb531b7f98ff8e189c1cfed7f3eafe929e5736d4c2e` | +| Start | 2026-08-02 15:12:32 UTC / 17:12:32 Europe/Berlin | +| Duration | 34 seconds | +| Chunks | 1 | +| Settings | JFR `profile` | +| Maximum recording size | 256 MB | +| Source revision | `e868904e29aee6871242c9135e2e8c7264675a66` | +| JabRef version | `100.0.0` | + +### Loading the recording + +Open the `.jfr` file in one of the following tools: + +- JDK Mission Control: **File → Open File** +- IntelliJ IDEA: open the `.jfr` file as a profiler snapshot +- JDK command line: `jfr summary ` or `jfr print ` + +Verify the artifact before comparing results: + +```bash +shasum -a 256 docs/code-howtos/snapshots/entry-editor-first-open-before-lazy-loading.jfr +``` + +## Reproduction scenario + +The final capture used a known repository test library and selected a known citation key through the CLI before opening the editor. + +| Input | Value | +| --- | --- | +| Library | `../../jablib/src/test/resources/org/jabref/bibtexFiles/test.bib` | +| Selected citation key | `1102917` | +| Editor command | **View → Open entry editor** (`Ansicht → Eintragseditor öffnen`) | +| Preference state | Reset to defaults before the final capture | + +The application was launched with: + +```bash +./gradlew --no-daemon :jabgui:run \ + --args="/absolute/path/to/jabref/jablib/src/test/resources/org/jabref/bibtexFiles/test.bib --jumpToKey=1102917" \ + --console=plain +``` + +The `:jabgui:run` task received this additional JVM option through a temporary Gradle init script: + +```text +-XX:StartFlightRecording=filename=/tmp/entry-editor.jfr,settings=profile,dumponexit=true,maxsize=256m +``` + +The recording was dumped after the entry editor had opened: + +```bash +jcmd JFR.dump name=1 filename=entry-editor-first-open-before-lazy-loading.jfr +``` + +### Supplemental linked-file run + +A second cold-start run used a repository fixture whose six entries each link a local PDF. The first entry, `minimal-sentence-case`, was opened by double-clicking its row in the main table. + +| Property | Value | +| --- | --- | +| Library | `../../jablib/src/test/resources/org/jabref/logic/search/test-library-with-attached-files.bib` | +| Source revision | `72a34bfaa1` | +| JavaFX | 26.0.2, macOS AArch64 | +| JFR settings | `profile`, stack depth 256, maximum size 250 MB | +| Recording duration | 59 seconds | +| Recording size | approximately 2.8 MB | + +The application was launched with: + +```bash +JDK_JAVA_OPTIONS='-XX:StartFlightRecording=name=entry,settings=profile,disk=true,maxsize=250m -XX:FlightRecorderOptions=stackdepth=256' \ + ./gradlew :jabgui:run \ + --args="/absolute/path/to/jabref/jablib/src/test/resources/org/jabref/logic/search/test-library-with-attached-files.bib" +``` + +The recording was dumped while JabRef was still running: + +```bash +jcmd JFR.dump name=entry filename=/tmp/jabref-entry-editor-linked-cold.jfr +``` + +The supplemental recording was analyzed locally and is not part of the committed snapshots. The original recording above remains the reproducible artifact attached to this report. + +### AppleScript interaction probe + +The following accessibility script was used while attempting to automate the double-click and detect the editor toolbar: + +```applescript +tell application "System Events" + tell process "java" + set frontmost to true + click at {600, 300} + delay 0.05 + click at {600, 300} + repeat 400 times + if (count of (UI elements of front window whose role is "AXToolbar")) > 1 then + return "editor-visible" + end if + delay 0.025 + end repeat + return "timeout" + end tell +end tell +``` + +It was invoked as follows: + +```bash +osascript <<'APPLESCRIPT' +tell application "System Events" + tell process "java" + set frontmost to true + click at {600, 300} + delay 0.05 + click at {600, 300} + repeat 400 times + if (count of (UI elements of front window whose role is "AXToolbar")) > 1 then + return "editor-visible" + end if + delay 0.025 + end repeat + return "timeout" + end tell +end tell +APPLESCRIPT +``` + +This script did **not** produce a reliable JavaFX double-click. It selected the entry cell in the main table, and the entry editor was subsequently opened by a manual double-click while JFR continued recording. The script's approximately 10.8-second wall-clock result therefore includes the wait for that manual action and is invalid as click-to-render latency. A future automation must emit a verified double-click on the main-table row and record a custom input marker before its timing can be used. + +## Environment + +| Property | Value | +| --- | --- | +| Operating system | Darwin 24.6.0, ARM64 | +| CPU | AArch64, 10 cores, 10 hardware threads | +| JVM | OpenJDK 64-Bit Server VM 25.0.2+10-LTS | +| JavaFX | 26.0.1, macOS AArch64 | +| Garbage collector | G1 | +| Initial heap | 256 MB | +| Maximum heap | 4 GB | +| Compact object headers | Enabled | +| String deduplication | Enabled | + +## Recording overview + +The most relevant recorded event counts are: + +| Event | Count | +| --- | ---: | +| `jdk.ExecutionSample` | 235 | +| `jdk.NativeMethodSample` | 1,542 | +| `jdk.ObjectAllocationSample` | 1,149 | +| `jdk.GarbageCollection` | 17 | +| `jdk.ThreadPark` | 212 | +| `jdk.JavaMonitorEnter` | 0 | +| `javafx.Input` | 0 | +| `javafx.PulsePhase` | 0 | + +The absence of JavaFX input and pulse-phase events is an important limitation: the snapshot is suitable for locating CPU and allocation work, but not for computing an exact input-to-render latency. + +## Interaction timeline + +The timestamps below are shown in Europe/Berlin local time. Thread ID 48 is named `Thread-5` in the execution samples and appears as `JavaFX Application Thread` with the same Java thread ID in the final thread dump. + +| Time | Thread | Sampled work | +| --- | --- | --- | +| 17:13:02.724 | JavaFX application thread | `Scene.preferredSize()` and scene-size invalidation | +| 17:13:02.754 | JavaFX application thread | CSS style-cache lookup | +| 17:13:03.329 | JavaFX application thread | `StyleManager.stylesheetsChanged(...)` | +| 17:13:04.115 | JavaFX application thread | Text-field behavior and scene-listener changes | +| 17:13:04.127 | JavaFX application thread | `FieldsEditorTab.createLabelAndEditor(...)` | +| 17:13:04.148 | JavaFX application thread | CSS transition and cached-font lookup | +| 17:13:04.195 | JavaFX application thread | `Node.doProcessCSS()` | +| 17:13:04.207 | Quantum renderer | Rendering the newly created scene graph | +| 17:13:04.714 | JavaFX application thread | CSS selector matching | +| 17:13:04.736 | JavaFX application thread | Sorting JavaFX style data | +| 17:13:05.065 | JavaFX application thread | Recursive `Parent.updateBounds()` | +| 17:13:05.144 | JavaFX application thread | `StyleMap` sorting and merging | +| 17:13:05.168 | JavaFX application thread | Recursive node and parent bounds updates | +| 17:13:05.558 | JavaFX application thread | Popup and node bounds calculation | +| 17:13:05.583 | JavaFX application thread | Recursive `Parent.updateBounds()` | +| 17:13:05.593 | JavaFX application thread | Recursive `Parent.updateBounds()` | +| 17:13:05.629 | JavaFX application thread | `Node.reapplyCss()` | +| 17:13:06.125 | JavaFX application thread | CSS selector matching and style-map lookup | +| 17:13:06.160 | JavaFX application thread | `Scene.doCSSPass()` during a pulse | + +The 2.033-second confirmed evidence window is the difference between the first direct `FieldsEditorTab` sample at 17:13:04.127 and the final CSS-pass sample at 17:13:06.160. The 3.436-second broader window begins with scene-size work at 17:13:02.724. Sampling gaps mean neither number should be interpreted as continuous CPU time. + +### Supplemental interaction evidence + +The manual double-click in the linked-file run was followed by JavaFX application-thread work from approximately 18:02:08.515 until layout settled around 18:02:10.280. This approximately 1.765-second interval is another statistical evidence window, not exact input-to-render latency, because the recording has no custom input marker. + +During that interval, the JavaFX application thread recorded: + +| Evidence | Result | +| --- | ---: | +| Java exceptions | 160 | +| `ClassCastException`, predominantly CSS `String` to `Paint`, `Color`, or `ParsedValue` conversions | 90 | +| `MalformedURLException`, predominantly JavaFX CSS parsing of `HAND`, `TEXT`, and `DEFAULT` | 58 | +| `NoSuchMethodError` from method-handle linkage | 6 | +| `IllegalArgumentException` for invalid color specifications | 5 | +| `FileNotFoundException` for a JAR entry | 1 | +| Weighted sampled allocation | approximately 130 MB | +| Garbage-collection pause | 10.593 ms | +| `jdk.FileRead` on the JavaFX application thread | 0 | +| `jdk.SocketRead` on the JavaFX application thread | 0 | + +The allocation samples were dominated by JavaFX CSS state and matching structures, including `PseudoClassState` arrays and objects, byte and long arrays, and immutable-set iterators. Execution samples were dominated by `SelectorPartitioning.match(...)`, `StyleManager.findMatchingStyles(...)`, `CssStyleHelper.createStyleHelper(...)`, CSS state transitions, `Node.reapplyCss()`, and subsequent `VirtualFlow` and `TableView` layout. + +The absence of recorded UI-thread file reads does not prove that no filesystem metadata check occurred, but the profile contains no evidence that resolving or reading an attached PDF caused this stall. The dominant sampled work remains field-control creation, CSS, and layout. + +### ControlsFX-decoration startup experiment + +ControlsFX silently installs its internal `DecorationPane` as the scene root when the first validation decoration is added. `JabRefGUI` now performs this installation with a temporary `GraphicDecoration` immediately after creating the scene, while the `PowerPane` is the root. The temporary decoration is then removed; the `DecorationPane` remains around the `PowerPane` and later validations do not need to replace the scene root. + +A JFR run then launched the linked-file fixture after the entry editor had been open in the preceding session. The editor was constructed during startup without an input command: `EntryEditor.` was sampled at 20:51:04.050 and `EntryEditorViewModel.rebuildTabs()` at 20:51:04.670 (Europe/Berlin). The JavaFX application thread subsequently sampled CSS selector matching, style-map creation, CSS transitions, and layout until approximately 20:51:11.140. + +No `ClassCastException` events were recorded on the JavaFX application thread between 20:51:03 and 20:51:12, unlike the 90 looked-up-color conversion exceptions in the comparable-sized linked-file interaction window above. This supports the hypothesis that eager `DecorationPane` installation avoids that exception path. + +This is deliberately not reported as a speedup: the recording overlaps application and library startup, uses editor restoration rather than a verified table interaction, and has no click-to-render marker. The remaining startup CSS and layout activity also shows that eager field-editor construction remains material work. A controlled post-change capture with the original library, entry selection, and an input marker is still required for a direct before/after latency comparison. + +## Relevant code path + +The direct application frame in the execution sample is: + +```text +FieldsEditorTab.createLabelAndEditor(...) +FieldsEditorTab.setupPanel(...) +FieldsEditorTab.bindToEntry(...) +``` + +`FieldsEditorTab.setupPanel(...)` determines every field, then maps every field through `createLabelAndEditor(...)` before layout. Each call creates a `FieldEditorFX`, binds it to the entry, stores it, and creates a `FieldNameLabel`. Adding the resulting control tree triggers the CSS, bounds, and rendering work visible in subsequent samples. + +The entry editor also constructs all configured tabs in `EntryEditorViewModel.rebuildTabs()`. That can contribute to initial work, but the strongest direct evidence in this recording is the selected fields tab creating and laying out its editors. + +The current main tab also eagerly creates controls that are not immediately used: + +- `AllFieldsTab.createSectionPane(...)` builds add-field chip buttons even for sections that start collapsed. +- `AllFieldsTab.createFreeFormAddRow(...)` fills the editable field-name combo box with every known non-internal field during panel construction. + +The release tag `v6.0-alpha.5` predates both the June 2026 MVVM entry-editor refactor and the July 2026 new entry editor. Commit `6c67e6f99b` (`New entry editor (#16166)`) is therefore a useful regression boundary to test, particularly because it introduced the current all-fields main tab. This source-history observation narrows the A/B candidates; it does not by itself identify the cause. + +## CSS looked-up-color failures + +The supplemental profile and terminal output show repeated JavaFX CSS conversion failures during entry-editor construction. [OpenJFX pull request #2225](https://github.com/openjdk/jfx/pull/2225) fixes looked-up colors that fail when used for `-fx-background-color` in a deeply nested, optionally unstyled scene graph. That description and its affected CSS code match the observed `CssStyleHelper`, selector-matching, and invalid-color activity. + +There is also a direct entry-editor path to ControlsFX decoration: + +```text +field editor construction +EditorValidator.configureValidation(...) +MVVMFX ControlsFxVisualizer +JabRef IconValidationDecorator +ControlsFX GraphicValidationDecoration +JavaFX CSS processing +``` + +This makes ControlsFX validation decoration a plausible trigger for the looked-up-color issue when many editors are created together. OpenJFX #2225 is the likely JavaFX-side fix, but an A/B run with that patch, or with validation decoration disabled, is still needed to prove how much of the first-open latency and exception burst it removes. + +## Garbage collection + +The GC events overlapping the end of the interaction were: + +| Time | Event | Total duration | Sum of pauses | Longest pause | +| --- | --- | ---: | ---: | ---: | +| 17:13:05.610 | G1 young collection | 7.05 ms | 7.05 ms | 7.05 ms | +| 17:13:06.149 | G1 young collection | 5.32 ms | 5.32 ms | 5.32 ms | +| 17:13:06.154 | G1 old collection | 61.9 ms | 8.43 ms | 8.41 ms | + +The 61.9-millisecond old-collection duration includes concurrent work. Its longest application pause was 8.41 milliseconds. GC therefore cannot account for the observed multi-second JavaFX work window. + +## Other concurrent work + +The recording contains network, TLS, cryptography, and JSON samples on `pool-3-thread-1` during the editor interaction. These samples are on a background thread and are not the direct UI-thread stall. They may compete for CPU, but the machine had ten hardware threads and the JavaFX application thread independently shows sustained scene-graph work. + +## Best-supported improvement + +Create field editors incrementally instead of constructing the complete field-control tree during the first bind. + +A safe implementation should: + +1. Create enough rows to render an immediately usable first editor. +2. Create additional rows in bounded batches between JavaFX pulses. +3. Cancel or invalidate queued batches when the selected entry changes. +4. Immediately materialize a requested field for jump-to-field and focus restoration. +5. Avoid repeatedly recreating controls that were already bound. +6. Keep all JavaFX node construction on the JavaFX application thread. +7. Build the contents and add-field chips of initially collapsed sections only when they are first expanded. +8. Populate the free-form field-name combo box when it is first opened instead of during panel construction. + +Lazy tab creation is a secondary opportunity, especially for tabs that are configured but not selected. It does not replace incremental creation in the initially selected fields tab. + +Separately, test a JavaFX build containing OpenJFX #2225 and compare the CSS exception count and JavaFX application-thread work window. Prewarming the editor may hide the cold-path delay, but reducing the initial scene graph and removing the CSS failure path address the recorded work directly. + +## Validation plan + +Retain this recording as the before snapshot and capture an equivalent after snapshot with the same library, citation key, JVM, and JFR settings. + +The target outcomes are: + +- no single multi-second JavaFX application-thread work window after opening the editor; +- a shorter field-creation-to-final-CSS sample window; +- no regression in focus restoration, jump-to-field behavior, or field visibility; +- GC pauses remaining insignificant relative to UI work. +- no burst of looked-up-color conversion exceptions after the JavaFX fix; +- unchanged results when the linked-file fixture is compared with an equivalent entry without attachments. + +A JMH benchmark can measure isolated field-editor creation and binding, including cold and warm cases. It cannot fully measure JavaFX pulse, CSS, and rendering latency by itself. An automated JavaFX integration measurement should therefore complement JMH for the end-to-end open-editor latency. + + diff --git a/docs/performance-review/snapshots/entry-editor-first-open-before-lazy-loading.jfr b/docs/performance-review/snapshots/entry-editor-first-open-before-lazy-loading.jfr new file mode 100644 index 000000000000..9c21271c5a67 Binary files /dev/null and b/docs/performance-review/snapshots/entry-editor-first-open-before-lazy-loading.jfr differ diff --git a/docs/requirements/entry-editor.md b/docs/requirements/entry-editor.md index a56b3abdd587..014991f2dc10 100644 --- a/docs/requirements/entry-editor.md +++ b/docs/requirements/entry-editor.md @@ -12,6 +12,13 @@ When users search or select a group not containing the entry shown in the Entry Needs: impl +## Validation decoration is initialized before opening the entry editor +`req~entry-editor.validation-decoration.startup~1` + +ControlsFX validation decoration is initialized during GUI startup, so opening the first entry editor does not replace the JavaFX scene root and trigger a full CSS reapplication on the entry-editor interaction path. + +Needs: impl + ## Citations tab should show citation preview on hover `req~entry-editor.citations.hover-preview~1` diff --git a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java index 86074e67b972..84f42368bb3e 100644 --- a/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/jabgui/src/main/java/org/jabref/gui/JabRefGUI.java @@ -17,6 +17,7 @@ import javafx.geometry.Rectangle2D; import javafx.scene.Scene; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.Region; import javafx.scene.text.Font; import javafx.stage.Screen; import javafx.stage.Stage; @@ -69,6 +70,8 @@ import com.tobiasdiez.easybind.EasyBind; import io.github.kusoroadeolu.veneer.BibTeXSyntaxHighlighter; import kong.unirest.core.Unirest; +import org.controlsfx.control.decoration.Decorator; +import org.controlsfx.control.decoration.GraphicDecoration; import org.controlsfx.dialog.ExceptionDialog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -377,6 +380,7 @@ private void openWindow() { powerpane.getInfoCenterPane().autoHideProperty().bind(Bindings.isEmpty(dialogService.getPersistentNotifications())); Scene scene = new Scene(powerpane); + installControlsFxDecorationPane(powerpane); LOGGER.debug("installing CSS"); themeManager.installCssOnScene(scene); @@ -405,6 +409,20 @@ private void openWindow() { // Lifecycle note: after this method, #onShowing will be called } + // [impl->req~entry-editor.validation-decoration.startup~1] + + /// Installs ControlsFX's decoration root before validation is first used. + /// + /// ControlsFX injects its internal `DecorationPane` as the scene root on the first + /// decoration. Doing that during startup avoids replacing the root while opening the entry + /// editor, which would otherwise trigger a large CSS reapplication on the critical path. + private static void installControlsFxDecorationPane(PowerPane powerpane) { + Region probeNode = new Region(); + GraphicDecoration probeDecoration = new GraphicDecoration(probeNode); + Decorator.addDecoration(powerpane, probeDecoration); + Decorator.removeDecoration(powerpane, probeDecoration); + } + public void onShowing(WindowEvent event) { Platform.runLater(() -> { mainFrame.updateHorizontalDividerPosition(); diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/AllFieldsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/AllFieldsTab.java index a5bca7ec139e..e21f95506508 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/AllFieldsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/AllFieldsTab.java @@ -413,6 +413,37 @@ private TitledPane createSectionPane(FieldListSections.SectionType type, VBox content = new VBox(); content.getStyleClass().add("all-fields-section-content"); + Runnable populateContent = () -> populateSectionContent( + content, + type, + shownFields, + labelForField, + bibDatabaseContext, + entry); + + TitledPane pane = new TitledPane(type.header().orElseThrow(), content); + pane.getStyleClass().add("all-fields-section-pane"); + pane.setCollapsible(true); + pane.setAnimated(false); + pane.setExpanded(sectionExpandOverrides.getOrDefault(type, !shownFields.isEmpty())); + pane.expandedProperty().addListener((_, _, expanded) -> { + sectionExpandOverrides.put(type, expanded); + if (expanded && content.getChildren().isEmpty()) { + populateContent.run(); + } + }); + if (pane.isExpanded()) { + populateContent.run(); + } + return pane; + } + + private void populateSectionContent(VBox content, + FieldListSections.SectionType type, + SequencedSet shownFields, + Map labelForField, + BibDatabaseContext bibDatabaseContext, + BibEntry entry) { if (!shownFields.isEmpty()) { GridPane sectionGrid = new GridPane(); sectionGrid.setHgap(10); @@ -428,14 +459,6 @@ private TitledPane createSectionPane(FieldListSections.SectionType type, chipFields.forEach(field -> chips.getChildren().add(createAddChip(bibDatabaseContext, entry, field))); content.getChildren().add(chips); } - - TitledPane pane = new TitledPane(type.header().orElseThrow(), content); - pane.getStyleClass().add("all-fields-section-pane"); - pane.setCollapsible(true); - pane.setAnimated(false); - pane.setExpanded(sectionExpandOverrides.getOrDefault(type, !shownFields.isEmpty())); - pane.expandedProperty().addListener((_, _, expanded) -> sectionExpandOverrides.put(type, expanded)); - return pane; } /// All member fields of a section offered as add-chips; the comments section offers the @@ -498,10 +521,15 @@ private Node createMainChipBar(BibDatabaseContext bibDatabaseContext, BibEntry e private Node createFreeFormAddRow(BibDatabaseContext bibDatabaseContext, BibEntry entry) { ComboBox fieldNameBox = new ComboBox<>(); fieldNameBox.setEditable(true); - fieldNameBox.getItems().addAll(FieldFactory.getAllFieldsWithOutInternal().stream() - .map(Field::getName) - .sorted() - .toList()); + fieldNameBox.setOnShowing(_ -> { + if (fieldNameBox.getItems().isEmpty()) { + fieldNameBox.getItems().setAll( + FieldFactory.getAllFieldsWithOutInternal().stream() + .map(Field::getName) + .sorted() + .toList()); + } + }); fieldNameBox.setPromptText(Localization.lang("Field name")); Button addButton = new Button(Localization.lang("Add")); Runnable addAction = () -> addFreeFormField(bibDatabaseContext, entry, fieldNameBox.getEditor().getText());