Repository navigation
Reintroduce custom entry editor tabs with two-column preferences UI - #16598
Conversation
The entry editor rework (#16166) dropped user-defined tabs. Bring them back with a preferences UI modeled on the Custom entry types tab: a "Tabs" column (visibility checkboxes for built-in tabs, add/remove for custom tabs, drag reordering for all) and a "Fields" column editing the selected custom tab's ordered field list. Field names may be regular expressions (e.g. comment-.*), which capture every matching set field of the entry, refreshed live on field changes. A field listed on more than one tab is marked with a warning sign. Custom tabs are persisted under the pre-rework preference keys, so tabs configured in older versions are picked up again without migration; the combined tab order is stored in a new entryEditorTabOrder key. Fixes #16594 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
PR Summary by QodoRestore custom entry editor tabs with two-column preferences
AI Description
Diagram
High-Level Assessment
Files changed (13)
|
Code Review by Qodo
1.
|
Persisted data can contain custom tabs sharing a name (older versions or hand-edited stores; the preferences UI prevents new duplicates). The order map keyed by tab ID silently dropped all but the last such tab, so queue tabs per ID instead and consume one per matching order entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
BibEntry's event bus holds listeners strongly, so AllFieldsTab and UserDefinedFieldsTab instances stayed registered (and retained) after EntryEditorViewModel.disposeTabs() dropped them. Release the subscription in dispose(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
A burst of field changes (imports, fetchers) queued one Platform.runLater per event. Track a pending-refresh flag so only one callback is queued, and read the entry's state at callback time instead of the event payload, so a queued refresh can never apply stale state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
| private static final String CUSTOM_TAB_NAME = "customTabName_"; | ||
| private static final String CUSTOM_TAB_FIELDS = "customTabFields_"; |
There was a problem hiding this comment.
Old style, brittle model of backing store data storage.
Might be better to do simple StringLists or a custom (json?) format in a string.
Replace the low-level parallel numbered series (customTabName_i /
customTabFields_i) with one entryEditorCustomTabs key holding a JSON
object {"tab name": ["field pattern", ...], ...} in display order,
serialized with Jackson (already required by jabgui). The old series is
still read as a migration fallback and purged on the next store.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
Convert the pre-v6.0-alpha.7 numbered series (customTabName_i / customTabFields_i) to the entryEditorCustomTabs JSON key once at startup in the migrations package, instead of a read-time fallback in JabRefGuiPreferences. The old keys are kept for use by older JabRef versions and ignored once the new key exists. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
The migration hardcodes the key name locally (V6_0_* convention), like the other migrations, instead of exposing the constant publicly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011R3RHnvsmkk2CHm4uEjVDa
* main: (22 commits) Chore(deps): Bump org.itsallcode.openfasttrace from 3.1.2 to 3.2.0 (#16638) Chore(deps): Bump gradle-wrapper from 9.7.0 to 9.7.1 (#16637) Chore(deps): Bump net.bytebuddy:byte-buddy in /versions (#16621) Chore(deps): Bump org.glassfish.hk2:hk2-utils in /versions (#16632) Chore(deps): Bump org.glassfish.hk2:hk2-api in /versions (#16633) Chore(deps): Bump org.glassfish.hk2:hk2-locator in /versions (#16631) Chore(deps): Bump com.google.guava:guava in /versions (#16630) Chore(deps): Bump JetBrains/junie-github-action from 1.7.0 to 1.7.4 (#16629) New translations {fileName} ({languageName}) (#16628) Chore(deps): Bump com.google.guava:guava in /versions (#16622) Chore(deps): Bump com.squareup.okhttp3:okhttp in /versions (#16624) Reintroduce custom entry editor tabs with two-column preferences UI (#16598) Implemented PagedSearchBasedFetcher Of ScholarApi for List API (#16293) Chore(deps): Bump io.github.classgraph:classgraph in /versions (#16600) Chore(deps): Bump dev.langchain4j:langchain4j-bom in /versions (#16601) Chore(deps): Bump tools.jackson:jackson-bom in /versions (#16602) Chore(deps): Bump com.dlsc.gemsfx:gemsfx in /versions (#16599) fix equals in group tree viewmodel and add run config for jabref gui (#16605) refactor(null): fix nullaways issues in ChainNode (#16606) New Crowdin updates (#16612) ...
Summary
🤖 The entry editor rework (#16166) dropped support for user-defined ("custom") entry editor tabs, which broke existing user configurations (#16594). This PR brings custom tabs back and reworks the "Editor tabs" preferences into a two-column editor modeled on the "Custom entry types" page: a Tabs column (visibility checkboxes for built-in tabs, add/remove for custom tabs, drag-and-drop reordering for all tabs) and a Fields column editing the selected custom tab's ordered field list, where a field name may also be a regular expression (e.g.
comment-.*) and a field contained in multiple tabs is flagged with a warning sign. Custom tabs are stored as a JSON map ({"tab name": ["field pattern", ...], ...}) under a single preference key (serialized with Jackson, which jabgui already uses); the pre-rework keys are read as a migration fallback and purged on the next store, so tabs configured in older versions (e.g. v6.0-alpha.6) are picked up again automatically.jabref-contrib-policy:4.2:reviewed:okAnalogies: Like honey, this PR preserves what users already gathered — custom tab configurations from older versions flow back in unspoiled. Like chocolate, it is composed of familiar ingredients (the proven "Custom entry types" two-column design) recombined into something immediately recognizable and easy to consume. And like the moon, the custom tabs disappeared for a phase but were never truly gone — this PR completes the cycle and brings them back into full view, with regex support as the new bright side.
Steps to test
General) into "Tab name..." and press Enter — the custom tab appears and is selected.keywords,doi,comment-.*) via the "Field name" box (autocompletion offers known field names).comment-testin the Main or source tab) makes it appear on the custom tab immediately.Preferences UI:
Custom "General" tab in the entry editor with a
comment-testfield captured by thecomment-.*pattern:Library with a single test entry:
Related issues and pull requests
Closes #16594
User documentation PR: JabRef/user-documentation#659
AI usage
Claude Code (model claude-fable-5), AIL4 — the implementation was AI-generated under human direction; the change was manually tested in a running JabRef.
AI CHECKLIST.md walkthrough
1. Code self-review
Nullability and control flow
== null/!= nullchecks — JSpecify annotations (@NullMarked,@Nullable,@NonNull) used instead. (Remaining(tab == null)checks are JavaFX selection-model states in the view, matching surrounding code;(model != null)guards a possibly-absent drag payload.)Objects.requireNonNull(...)— nullability expressed via JSpecify annotations.@NullMarked(org.jspecify.annotations.NullMarked).Optionalconsumed withifPresent/ifPresentOrElse/map/orElseThrow— neverorElse(unusedValue)nor anisPresent()+get()block.StringUtil.isBlank(...)used instead ofs == null || s.isBlank().Exceptions
catch (Exception e)— onlyPatternSyntaxExceptionis caught (invalid user regex resolves to no fields).throw new RuntimeException(...)/IllegalStateException(...).Style and idioms
BibEntryobjects built with withers (withField, notsetField) — in tests.List.of()/List.copyOf(),SequencedSet, records, pattern-matching switch.Pattern.compile(...)constant, notString.matches(...)(the user-supplied patterns are compiled per resolution, which is inherent to the feature).BackgroundTask— no background work added.///) uses Markdown syntax, not JavaDoc inline tags.User-facing text
Localization.lang).!; labels do not end with:."Remove tab %0"), not string concatenation.Security
text/htmlresponses touched.Tests
EntryEditorTabModelTest(pattern resolution: literals, regex, order, invalid regex) and extendedEntryEditorTabViewModelTest(add/remove custom tabs, duplicate detection, order round-trip, reset).@DisplayName, no caught exceptions, no manual temp directories.2. Verification commands
xvfb-run -a ./gradlew :jablib:test --tests "*LocalizationConsistency*"passed (jablib change is localization keys only; GUI tests for the touched classes pass via:jabgui:test)../gradlew :jabgui:checkstyleMain :jabgui:checkstyleTestpassed../gradlew :jabgui:modernizerpassed../gradlew --no-configuration-cache :rewriteRunapplied, no remaining diff..idea/codeStyles/Project.xml(idea format).npx markdownlint-cli2— onlyCHANGELOG.md/docs/requirementslist entries changed, following existing formatting.idea formatwas run directly.3. Documentation
CHANGELOG.md: updated the existing "Changed" entry for the tab preferences rework and softened the "Removed" entry (the removal itself was unreleased); linked Entry editor introduced breaking changes to custom tabs #16594.req~entry-editor.custom-tabs~1indocs/requirements/entry-editor.md, with[impl->...]tags in code.docs/— no architecture change beyond the requirement entry.4. Pull request
.github/PULL_REQUEST_TEMPLATE.md, every section filled.gh pr create --body-file.TODOplaceholder.Checklist
CHANGELOG.mdin a way that can be understood by the average user (if change is visible to the user)🤖 Generated with Claude Code