Skip to content
Merged
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
- We extended the per-fetcher timeout for fulltext PDF lookups from 10 to 120 seconds so fetchers that bounce through an institutional sign-in or a slow publisher CDN have a chance to complete. [#15877](https://github.com/JabRef/jabref/pull/15877)
- When an imported entry has an empty citation key, it is generated. [#15624](https://github.com/JabRef/jabref/pull/15624)
- We made the `Move file to directory` operation for Linked Files show every configured JabRef directory as possible options. [#12287](https://github.com/JabRef/jabref/issues/12287)
- We reworked the entry editor tab preferences into a single "Editor tabs" list where you can show, hide, add, and remove both built-in and custom tabs. [#15998](https://github.com/JabRef/jabref/pull/15998)
- We reworked the entry editor tab preferences into a two-column "Tabs"/"Fields" editor supporting reordering, custom tabs, and regular expressions as field names. [#15998](https://github.com/JabRef/jabref/pull/15998). [#16594](https://github.com/JabRef/jabref/issues/16594)
- We extended library pseudonymization to also pseudonymize group names, not just the entries. [#14117](https://github.com/JabRef/jabref/issues/14117)
- We introduced a leightweight search engine without fulltext search in linked files as default variant. [#15599](https://github.com/JabRef/jabref/pull/15599)
- We moved arXiv handling out of the DOI cleanup into the dedicated "arXiv DOI" cleanup. [#16033](https://github.com/JabRef/jabref/pull/16033)
Expand Down Expand Up @@ -153,7 +153,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv

- We removed the redundant "Look up BibTeX entries in all open libraries" setting from the LibreOffice panel, which is now the toggle of "Look up BibTeX entries in the currently selected library only". [#16484](https://github.com/JabRef/jabref/pull/16484)
- We removed the entry editor tabs "Required fields", "Optional fields", "Optional fields 2", "Deprecated fields", "Other fields", and "Comments"; their content is part of the new "Main" tab. [#12711](https://github.com/JabRef/jabref/issues/12711)
- We removed the ability to define custom entry editor tabs (including the default "General" and "Abstract" tabs); the entry editor tab preferences now only toggle the visibility of the built-in tabs. [#12711](https://github.com/JabRef/jabref/issues/12711)
- We removed the default custom entry editor tabs "General" and "Abstract"; their content is part of the new "Main" tab. User-defined custom tabs are kept. [#12711](https://github.com/JabRef/jabref/issues/12711)
- The citation key integrity check now includes the generated citation key in its warning message. [#15776](https://github.com/JabRef/jabref/pull/15776)
- We removed the [CiteSeerX](https://en.wikipedia.org/wiki/CiteSeerX) fetcher, because the service is defunct and its links now redirect to the Wayback Machine. [#16299](https://github.com/JabRef/jabref/issues/16299)

Expand Down
7 changes: 7 additions & 0 deletions docs/requirements/entry-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ A field's row shows a small gray "remove field" icon button pinned to its top-ri

Needs: impl

## Custom tabs show a user-defined list of field patterns
`req~entry-editor.custom-tabs~1`

Users can define custom entry editor tabs in the preferences ("Entry editor" β†’ "Editor tabs"): a "Tabs" column lists all tabs (built-in tabs with a visibility checkbox, custom tabs with a delete action) and supports adding custom tabs and reordering all tabs via drag and drop; a "Fields" column edits the selected custom tab's ordered field list, also reorderable via drag and drop. A field entry is either a plain field name (always shown on the tab, even while unset) or a regular expression (e.g. `comment-.*`), which shows every set field of the entry whose name matches. A field listed on more than one tab is marked with a warning sign. Custom tabs configured in JabRef versions before the "Main" tab rework are picked up again without migration.

Needs: impl

## Special fields are edited with the same icon controls as the main table
`req~entry-editor.special-field-editors~1`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ public class DragAndDropDataFormats {
public static final DataFormat LINKED_FILE = new DataFormat("dnd/org.jabref.model.entry.LinkedFile");
public static final DataFormat ENTRIES = new DataFormat("dnd/org.jabref.model.entry.BibEntries");
public static final DataFormat PREVIEWLAYOUTS = new DataFormat("dnd/org.jabref.logic.citationstyle.PreviewLayouts");
public static final DataFormat ENTRY_EDITOR_TAB = new DataFormat("dnd/org.jabref.gui.preferences.entryeditor.EditorTabViewModel");
@SuppressWarnings("unchecked") public static final Class<List<PreviewLayout>> PREVIEWLAYOUT_LIST_CLASS = (Class<List<PreviewLayout>>) (Class<?>) List.class;
}
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,15 @@ protected void bindToEntry(BibEntry entry) {
super.bindToEntry(entry);
}

@Override
protected void dispose() {
// The entry's event bus holds listeners strongly; without unregistering, a discarded tab
// instance would be retained (and keep reacting) for as long as the entry lives.
subscribedEntry.ifPresent(entry -> entry.unregisterListener(this));
subscribedEntry = Optional.empty();
super.dispose();
}

/// Refreshes the list when a field is set or unset from outside this tab
/// (Source tab, fetchers, undo, …). Rebuilds only when the set of shown fields
/// actually changes, so typing inside a visible editor never rebuilds or steals focus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ public EntryEditorTab createTab(EntryEditorTabModel model) {
: entryEditorPreferences.tabVisibleProperty(type));
yield tab;
}
// Custom tabs have no preference-driven visibility gate: they exist exactly while configured,
// and hide themselves via content-driven visibility when their patterns resolve to no fields.
case EntryEditorTabModel.CustomizedFieldsTab customTab ->
new UserDefinedFieldsTab(
customTab,
undoManager,
undoAction,
redoAction,
preferences,
journalAbbreviationRepository,
stateManager,
previewPanel);
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package org.jabref.gui.entryeditor;

import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedSet;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.jabref.logic.l10n.Localization;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.field.Field;
import org.jabref.model.entry.field.FieldFactory;

/// Model of the tabs that appear in the entry editor.
///
/// Since the single scroll-list "Main" tab (issue #12711) replaced the classic category
/// tabs and the user-customizable field-set tabs, every tab is a fixed [BuiltInTab]
/// identified by a [BuiltIn] constant; users only toggle visibility.
/// [BuiltInTab] β€” a fixed tab (the single scroll-list "Main" tab of issue #12711 and the
/// feature tabs) identified by a [BuiltIn] constant; users only toggle visibility and order.
/// [CustomizedFieldsTab] β€” a user-defined tab backed by a named, ordered list of field patterns.
public sealed interface EntryEditorTabModel
permits EntryEditorTabModel.BuiltInTab {
permits EntryEditorTabModel.BuiltInTab, EntryEditorTabModel.CustomizedFieldsTab {

boolean isVisible();

Expand Down Expand Up @@ -86,4 +95,56 @@ public EntryEditorTabModel withVisible(boolean visible) {
return new BuiltInTab(type, visible);
}
}

/// A user-defined tab showing an explicit, ordered list of field patterns. Always shown while its
/// pattern list resolves to at least one field; toggled only by being added to or removed from the
/// tab list, so it carries no visibility flag (unlike [BuiltInTab]).
///
/// A pattern is either a plain field name (always shown, even while unset on the entry) or a
/// regular expression (contains regex metacharacters), which captures every *set* field of the
/// entry whose name matches it β€” e.g. `comment-.*` for all user-specific comment fields.
record CustomizedFieldsTab(String name, List<String> fieldPatterns)
implements EntryEditorTabModel {

/// A pattern without any regex metacharacter is a plain field name.
private static final Pattern PLAIN_FIELD_NAME = Pattern.compile("[^\\\\^$.|?*+()\\[\\]{}]+");

public CustomizedFieldsTab {
fieldPatterns = List.copyOf(fieldPatterns);
}

@Override
public boolean isVisible() {
return true;
}

@Override
public EntryEditorTabModel withVisible(boolean visible) {
return this;
}

/// Resolves [#fieldPatterns] against the given entry, keeping the configured pattern order;
/// the fields captured by one regex pattern are sorted by name. Invalid regexes resolve to nothing.
// [impl->req~entry-editor.custom-tabs~1]
public SequencedSet<Field> resolveFields(BibEntry entry) {
SequencedSet<Field> result = new LinkedHashSet<>();
for (String pattern : fieldPatterns) {
if (PLAIN_FIELD_NAME.matcher(pattern).matches()) {
result.add(FieldFactory.parseField(entry.getType(), pattern));
continue;
}
try {
Pattern compiled = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
entry.getFields().stream()
.filter(field -> compiled.matcher(field.getName()).matches())
.sorted(Comparator.comparing(Field::getName))
.forEach(result::add);
} catch (PatternSyntaxException _) {
// Invalid regex: resolves to no fields; the preferences UI accepts any string, so
// a broken pattern must not break rendering the tab.
}
}
return result;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package org.jabref.gui.entryeditor;

import java.util.Optional;
import java.util.SequencedSet;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.swing.undo.UndoManager;

import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

import org.jabref.gui.StateManager;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.preferences.GuiPreferences;
import org.jabref.gui.preview.PreviewPanel;
import org.jabref.gui.undo.RedoAction;
import org.jabref.gui.undo.UndoAction;
import org.jabref.logic.journals.JournalAbbreviationRepository;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.BibEntry;
import org.jabref.model.entry.event.FieldChangedEvent;
import org.jabref.model.entry.field.Field;

import com.google.common.eventbus.Subscribe;
import com.tobiasdiez.easybind.EasyBind;
import org.jspecify.annotations.NullMarked;

/// A user-defined tab showing the fields resolved from a [EntryEditorTabModel.CustomizedFieldsTab]
/// (configured in the entry editor preferences).
@NullMarked
public class UserDefinedFieldsTab extends FieldsEditorTab {

private final EntryEditorTabModel.CustomizedFieldsTab model;

/// Replaces the base class's content-driven visibility: a regex pattern's matches change when fields
/// are set or cleared on the *same* entry (e.g. from the Main or Source tab), which the base class's
/// entry-switch-only evaluation would miss.
private final BooleanProperty hasResolvedFields = new SimpleBooleanProperty();

/// The entry whose event bus this tab is subscribed to, for live refresh of regex-captured fields.
private Optional<BibEntry> subscribedEntry = Optional.empty();

/// Set while a refresh is queued on the FX thread (events may arrive from background threads,
/// e.g. fetchers), so bursts of field changes coalesce into one refresh instead of one each.
private final AtomicBoolean refreshQueued = new AtomicBoolean();

public UserDefinedFieldsTab(EntryEditorTabModel.CustomizedFieldsTab model,
UndoManager undoManager,
UndoAction undoAction,
RedoAction redoAction,
GuiPreferences preferences,
JournalAbbreviationRepository journalAbbreviationRepository,
StateManager stateManager,
PreviewPanel previewPanel) {
super(
false,
undoManager,
undoAction,
redoAction,
preferences,
journalAbbreviationRepository,
stateManager,
previewPanel);

this.model = model;

setContentDrivenVisibility(hasResolvedFields);
EasyBind.subscribe(currentEntryProperty(), entry ->
hasResolvedFields.set((entry != null) && !model.resolveFields(entry).isEmpty()));

setText(model.name());
setGraphic(IconTheme.JabRefIcons.OPTIONAL.getGraphicNode());
}

@Override
protected SequencedSet<Field> determineFieldsToShow(BibEntry entry) {
return model.resolveFields(entry);
}

@Override
protected void bindToEntry(BibEntry entry) {
if (subscribedEntry.filter(current -> current == entry).isEmpty()) {
subscribedEntry.ifPresent(previous -> previous.unregisterListener(this));
entry.registerListener(this);
subscribedEntry = Optional.of(entry);
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}
super.bindToEntry(entry);
}

@Override
protected void dispose() {
// The entry's event bus holds listeners strongly; without unregistering, a discarded tab
// instance would be retained (and keep reacting) for as long as the entry lives.
subscribedEntry.ifPresent(entry -> entry.unregisterListener(this));
subscribedEntry = Optional.empty();
super.dispose();
}

/// Refreshes the tab when a field is set or cleared from outside (Main tab, Source tab, fetchers,
/// undo, …), since that can change which fields a regex pattern captures. Event bursts coalesce
/// into a single deferred refresh, and the refresh reads the entry's state at callback time (the
/// event's payload is deliberately ignored), so a queued callback can never apply stale state.
/// Rebuilds only when the resolved field set actually changes, so typing inside this tab's
/// editors never steals focus.
@Subscribe
public void listen(FieldChangedEvent event) {
if (refreshQueued.getAndSet(true)) {
return;
}
Platform.runLater(() -> {
// Cleared before refreshing: an event arriving while we refresh must queue a new callback.
refreshQueued.set(false);
BibEntry entry = getCurrentEntry();
if (entry == null) {
return;
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
}
SequencedSet<Field> target = determineFieldsToShow(entry);
hasResolvedFields.set(!target.isEmpty());
if ((gridPane != null) && !target.equals(editors.keySet())) {
setupPanel(stateManager.getActiveDatabase().orElse(new BibDatabaseContext()), entry, false);
}
});
}
}
Loading
Loading