diff --git a/CHANGELOG.md b/CHANGELOG.md index f3f83475079f..aadd804727a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added "Open folder as library" (File menu): a folder of PDFs and Hayagriva sidecar files (`.yml`, or `.md` notes with a Hayagriva frontmatter) opens as a library, and it is reopened on the next start. PDFs without a sidecar appear right away and get their metadata extracted in the background. Edits are not yet written back to the files. [#737](https://github.com/JabRef/jabref-koppor/pull/737) - We added `jabkit git merge-driver`, a Git merge driver that merges `.bib` files semantically. [#16838](https://github.com/JabRef/jabref/pull/16838) ### Changed diff --git a/docs/decisions/0075-directory-as-library-with-hayagriva-sidecars.md b/docs/decisions/0075-directory-as-library-with-hayagriva-sidecars.md new file mode 100644 index 000000000000..4eab13d3ec34 --- /dev/null +++ b/docs/decisions/0075-directory-as-library-with-hayagriva-sidecars.md @@ -0,0 +1,85 @@ +--- +nav_order: 0075 +parent: Decision Records +--- + +# Directory as library with Hayagriva sidecars + +## Context and Problem Statement + +Users organize papers as PDFs in folder trees (often synced via cloud storage) and want JabRef +to treat such a folder directly as a library, without maintaining a separate `.bib` file. Each +work's bibliographic data (including notes) should live in a plain-text file next to its PDF, so +the folder stays usable from other tools (e.g. Typst). How should such a "directory library" be +represented in JabRef, and where does the per-entry data live? + +## Decision Drivers + +* The folder must remain self-describing and usable outside JabRef. +* JabRef's existing architecture assumes a `BibDatabaseContext` per library tab. +* File changes must eventually sync in both directions (external edits appear live, JabRef edits + persist back) without echo loops. +* The groups panel should later mirror the folder structure ([#10930](https://github.com/JabRef/jabref/issues/10930)). + +## Considered Options + +* Hayagriva-based sidecars (Markdown notes files with a Hayagriva YAML frontmatter, plain + Hayagriva `.yml` also read), directory represented as a third `DatabaseLocation` with an + empty database path +* Pure Hayagriva YAML sidecars (`X.yml` next to `X.pdf`) +* A hidden auto-maintained `.bib` file inside the directory +* XMP metadata embedded in the PDFs as the only store + +## Decision Outcome + +Chosen option: "Hayagriva-based sidecars with a third `DatabaseLocation`", because sidecars +keep the folder tool-agnostic (Hayagriva is Typst's bibliography format and JabRef has a +symmetric importer/exporter for it), embedded XMP cannot represent all fields and rewrites the +PDFs themselves, and a hidden `.bib` would duplicate state that immediately drifts from the +files. Pure YAML sidecars lost against the Markdown form because per-entry notes (JabRef's +comment fields) are long-form Markdown that reads terribly as YAML block scalars but naturally +as a Markdown body — the folder then doubles as a plain notes collection (Obsidian, any text +editor). + +Key points of the chosen design: + +* A JabRef-authored sidecar is a Markdown file (`X.md` next to `X.pdf`): the YAML frontmatter + (between two `---` lines) is a regular Hayagriva document, the body below is markdownlint-clean + Markdown — a `# Notes` heading, the entry's comment text beneath it, and one + `## comment-` section per per-user comment field. Body content under other headings is + kept but not imported. Plain Hayagriva `.yml`/`.yaml` files are still read and written back + (they stay directly loadable by Typst); JabRef-only fields are written there as + `comment`/`comment-` extension keys, which the Hayagriva parser ignores. + +* `DatabaseLocation.DIRECTORY`: the context keeps an **empty** database path plus a separate + directory root. Empty path gives correct default behavior at almost every existing decision + point (no autosave/backup managers, no `.bib` change monitor, "needs saved local database" + actions disabled). The directory root is registered as the library-specific file directory, so + relative PDF links resolve without a database path. +* Pairing is by convention — `X.md`/`X.yml`/`X.yaml` next to `X.pdf` — because Hayagriva has no + file-path field; nothing JabRef-specific is written into the YAML for the association. +* Non-Hayagriva `.yml` files (CI configs, ...) and `.md` files without a Hayagriva frontmatter + (READMEs, plain notes) are ignored via format recognition, not reported + as errors. PDFs without a sidecar appear immediately as stubs; their metadata is extracted + asynchronously after the library is shown (the standard PDF import pipeline, enriching the + stub in place); a sidecar is only written once the user edits the entry (scanning never + writes files). +* Later synchronization mirrors the shared-SQL seam (`convertToSharedDatabase` / + `DBMSSynchronizer`): a directory synchronizer subscribes to entry events through a + `CoarseChangeFilter` for write-back and applies inbound file changes with a non-local + `EntriesEventSource` to prevent echo loops; directory watching uses the Commons-IO + `DirectoryMonitor` ([ADR-0030](0030-use-apache-commons-io-for-directory-monitoring.md)). + "Save as" converts a directory library into a regular `.bib` library. + +### Consequences + +* Good, because the directory stays the single source of truth; `.yml` sidecars are usable from + Typst as-is, and `.md` sidecars double as plain Markdown notes. +* Good, because the empty-database-path representation needs only a handful of explicit UI + branches (tab title, close confirmation, save-as). +* Bad, because a `.md` sidecar is not directly loadable by Typst — its frontmatter must be + extracted (trivially, e.g. with `sed`/`yq`) or exported to obtain a plain Hayagriva file. +* Bad, because YAML comments in hand-edited sidecars will not survive JabRef rewrites (the YAML + parser drops them). +* Bad, because library-level metadata (groups, save actions) has no natural home yet; a + metadata file in the root may be added later. diff --git a/docs/requirements/directory-library.md b/docs/requirements/directory-library.md new file mode 100644 index 000000000000..811a7cf9e03e --- /dev/null +++ b/docs/requirements/directory-library.md @@ -0,0 +1,37 @@ +--- +parent: Requirements +--- +# Directory library + +## Directory scan builds the library from sidecars and PDFs +`req~directory-library.scan~6` + +Opening a directory as a library must fill the library from the directory tree: every Hayagriva +`.yml`/`.yaml` file and every Markdown sidecar (`.md` opening with a Hayagriva YAML frontmatter +block; the notes body below maps to the entry's comment fields — the text under `# Notes` to +the comment, each `## comment-` section to that per-user comment) contributes its +entries, a PDF with the same base name next to a sidecar is +linked to the sidecar's entry, and PDFs without a sidecar appear immediately as stub entries +titled after the file; their metadata (embedded BibTeX, XMP, content heuristics — the standard +PDF import pipeline) and a generated citation key arrive asynchronously after the library is +shown, without replacing the entry instances. When the PDF yields no DOI, the DOI is looked up +online and the metadata behind it fills only the fields the PDF did not provide. Opening must not block on PDF parsing. Hidden files/directories, gitignored non-content files and +gitignored subtrees, `.yml` files not recognized as Hayagriva, and `.md` files without a Hayagriva +frontmatter are skipped; the library's own sidecars and PDFs are never hidden by `.gitignore` (a +scratch PDF folder ignored by a catch-all `*` still opens). Unparseable Hayagriva files are +reported as warnings without aborting the scan. +Scanning must not write or modify any file in the directory. +See [ADR 75](../decisions/0075-directory-as-library-with-hayagriva-sidecars.md) for more details. + +Needs: impl + +## Directory libraries are part of the restored session +`req~directory-library.session-restore~1` + +When "Open last edited libraries" is enabled, a directory library that was open on shutdown is +reopened on the next start, exactly like `.bib` libraries: its root directory is remembered in +the last-opened list and routed back through the directory-library opener. + +Needs: impl + + diff --git a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java index 62cff73d0cdf..680e6b4604af 100644 --- a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java +++ b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java @@ -596,12 +596,21 @@ public void updateTabTitle(boolean isChanged) { } else { tabTitle.append(Localization.lang("untitled")); } + } else if (databaseLocation == DatabaseLocation.DIRECTORY) { + if (isChanged) { + tabTitle.append('*'); + } + bibDatabaseContext.getDirectoryLibraryRoot().ifPresent(root -> { + tabTitle.append(root.getFileName().toString()); + toolTipText.append(root.toAbsolutePath()); + }); } else { addSharedDbInformation(tabTitle, bibDatabaseContext); addSharedDbInformation(toolTipText, bibDatabaseContext); } addModeInfo(toolTipText, bibDatabaseContext); - if ((databaseLocation == DatabaseLocation.LOCAL) && bibDatabaseContext.getDatabase().hasEntries()) { + if ((databaseLocation == DatabaseLocation.LOCAL || databaseLocation == DatabaseLocation.DIRECTORY) + && bibDatabaseContext.getDatabase().hasEntries()) { addChangedInformation(toolTipText); } } @@ -639,6 +648,13 @@ static IconTheme.JabRefIcons tabIcon(DatabaseLocation location, BibDatabaseMode /// library that does not need saving, never a library closing without asking. @Subscribe public void listen(BibDatabaseContextChangedEvent event) { + // Background enrichment of a directory library is system-initiated (SHARED-sourced), + // not something the user would be asked to save + if (bibDatabaseContext.getLocation() == DatabaseLocation.DIRECTORY + && event instanceof EntriesEvent entriesEvent + && entriesEvent.getEntriesEventSource() == EntriesEventSource.SHARED) { + return; + } boolean unrecorded = ((event instanceof MetaDataChangedEvent metaDataChangedEvent) && (metaDataChangedEvent.getSource() == MetaDataChangeSource.LOCAL)) || ((event instanceof EntriesEvent entriesEvent) @@ -793,7 +809,9 @@ private boolean showDeleteConfirmationDialog(int numberOfEntries) { } public boolean requestClose() { - if (bibDatabaseContext.getLocation() == DatabaseLocation.LOCAL) { + // DIRECTORY prompts as well: until file write-back exists, edits are in-memory only + if (bibDatabaseContext.getLocation() == DatabaseLocation.LOCAL + || bibDatabaseContext.getLocation() == DatabaseLocation.DIRECTORY) { if (isModified()) { return confirmClose(); } @@ -814,7 +832,7 @@ private boolean confirmClose() { } String filename = getBibDatabaseContext() - .getDatabasePath() + .getPathOnDisk() .map(Path::toAbsolutePath) .map(Path::toString) .orElse(Localization.lang("untitled")); diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 555d4ec6ab27..3df2e42bd591 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -80,6 +80,7 @@ public enum StandardActions implements Action { RELEVANT(Localization.lang("Toggle relevance"), IconTheme.JabRefIcons.RELEVANCE), NEW_LIBRARY(Localization.lang("New empty library"), IconTheme.JabRefIcons.NEW), OPEN_LIBRARY(Localization.lang("Open library..."), IconTheme.JabRefIcons.OPEN, KeyBinding.OPEN_LIBRARY), + OPEN_FOLDER_AS_LIBRARY(Localization.lang("Open folder as library..."), IconTheme.JabRefIcons.OPEN), MERGE_LIBRARY(Localization.lang("Merge..."), IconTheme.JabRefIcons.MERGE_ENTRIES), IMPORT(Localization.lang("Import"), IconTheme.JabRefIcons.IMPORT), EXPORT(Localization.lang("Export"), IconTheme.JabRefIcons.EXPORT, KeyBinding.EXPORT), diff --git a/jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java b/jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java index 3c89a70edf31..35c2260fdf96 100644 --- a/jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java +++ b/jabgui/src/main/java/org/jabref/gui/exporter/SaveDatabaseAction.java @@ -209,6 +209,11 @@ boolean saveAs(Path file, SaveDatabaseMode mode, boolean mayAutoCommit) { if (saveResult == SaveResult.SUCCESS) { // we managed to successfully save the file // thus, we can store the path into the context + if (context.getLocation() == DatabaseLocation.DIRECTORY) { + // "Save as" snapshots a directory library into a regular .bib library; only now, + // so a failed save leaves the directory library intact + context.convertToLocalDatabase(); + } context.setDatabasePath(file); stateManager.setActiveDatabase(context); libraryTab.updateTabTitle(false); diff --git a/jabgui/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesCrawler.java b/jabgui/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesCrawler.java index 5131dab2e03d..4b97e04ea017 100644 --- a/jabgui/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesCrawler.java +++ b/jabgui/src/main/java/org/jabref/gui/externalfiles/UnlinkedFilesCrawler.java @@ -24,6 +24,7 @@ import org.jabref.logic.util.BackgroundTask; import org.jabref.logic.util.io.AutoLinkPreferences; import org.jabref.logic.util.io.FileUtil; +import org.jabref.logic.util.io.GitIgnoreFileFilter; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; diff --git a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java index 2fac15abc6f3..d3f3e53cc3dc 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/JabRefFrameViewModel.java @@ -146,16 +146,18 @@ public boolean close() { } } - // Read the opened and focused databases before closing them + // Read the opened and focused databases before closing them. Directory libraries have + // no database path; their root stands in so they are restored on the next start. + // [impl->req~directory-library.session-restore~1] List openedLibraries = tabContainer.getLibraryTabs().stream() .map(LibraryTab::getBibDatabaseContext) - .map(BibDatabaseContext::getDatabasePath) + .map(BibDatabaseContext::getPathOnDisk) .flatMap(Optional::stream) .map(Path::toAbsolutePath) .toList(); Path focusedLibraries = Optional.ofNullable(tabContainer.getCurrentLibraryTab()) .map(LibraryTab::getBibDatabaseContext) - .flatMap(BibDatabaseContext::getDatabasePath) + .flatMap(BibDatabaseContext::getPathOnDisk) .map(Path::toAbsolutePath) .orElse(null); SequencedMap sharedDatabases = collectSharedDatabases(tabContainer.getLibraryTabs()); diff --git a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java index 9da5a1be4def..bfba51f6b952 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/MainMenu.java @@ -50,6 +50,7 @@ import org.jabref.gui.importer.NewEntryAction; import org.jabref.gui.importer.actions.ImportCommand; import org.jabref.gui.importer.actions.OpenDatabaseAction; +import org.jabref.gui.importer.actions.OpenDirectoryLibraryAction; import org.jabref.gui.importer.fetcher.LookupIdentifierAction; import org.jabref.gui.integrity.IntegrityCheckAction; import org.jabref.gui.libraryproperties.LibraryPropertiesAction; @@ -168,6 +169,7 @@ private void createMenu() { file.getItems().addAll( factory.createMenuItem(StandardActions.NEW_LIBRARY, new NewDatabaseAction(frame, preferences)), factory.createMenuItem(StandardActions.OPEN_LIBRARY, openDatabaseActionSupplier.get()), + factory.createMenuItem(StandardActions.OPEN_FOLDER_AS_LIBRARY, new OpenDirectoryLibraryAction(frame, dialogService, preferences, aiService, stateManager, fileUpdateMonitor, entryTypesManager, gitHandlerRegistry, clipBoardManager, taskExecutor)), fileHistoryMenu, factory.createMenuItem(StandardActions.SAVE_LIBRARY, new SaveAction(SaveAction.SaveMethod.SAVE, frame::getCurrentLibraryTab, dialogService, preferences, stateManager, entryTypesManager, journalAbbreviationRepository)), factory.createMenuItem(StandardActions.SAVE_LIBRARY_AS, new SaveAction(SaveAction.SaveMethod.SAVE_AS, frame::getCurrentLibraryTab, dialogService, preferences, stateManager, entryTypesManager, journalAbbreviationRepository)), diff --git a/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java b/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java index 64fd135ccf85..6cad3c8c7586 100644 --- a/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java +++ b/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDatabaseAction.java @@ -162,6 +162,17 @@ public void openFile(Path file) { /// /// @param filesToOpen the filesToOpen, may be null or not existing public void openFiles(List filesToOpen) { + // Directories are directory libraries (e.g. restored from the last session) + // [impl->req~directory-library.session-restore~1] + OpenDirectoryLibraryAction openDirectoryLibraryAction = new OpenDirectoryLibraryAction(tabContainer, dialogService, preferences, + aiService, stateManager, fileUpdateMonitor, entryTypesManager, gitHandlerRegistry, clipboardManager, taskExecutor); + filesToOpen.stream() + .map(FileUtil::resolveIfShortcut) + .filter(Files::isDirectory) + .map(directory -> directory.toAbsolutePath().normalize()) + .distinct() + .forEach(openDirectoryLibraryAction::openDirectory); + // Resolve any shortcuts to their targets and filter to only .bib files. // The resulting list must remain modifiable for downstream processing (iterator.remove() calls below). Path baseDirectoryPath = JabRefBaseDirectoryLocator.getBaseDirectoryPath(); diff --git a/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDirectoryLibraryAction.java b/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDirectoryLibraryAction.java new file mode 100644 index 000000000000..d0fc7e2fc1f0 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/importer/actions/OpenDirectoryLibraryAction.java @@ -0,0 +1,147 @@ +package org.jabref.gui.importer.actions; + +import java.nio.file.Path; + +import javafx.event.Event; +import javafx.event.EventHandler; + +import org.jabref.gui.DialogService; +import org.jabref.gui.LibraryTab; +import org.jabref.gui.LibraryTabContainer; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.clipboard.ClipBoardManager; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.gui.util.UiTaskExecutor; +import org.jabref.logic.ai.AiService; +import org.jabref.logic.directorylibrary.DirectoryLibraryScanner; +import org.jabref.logic.directorylibrary.PdfEnrichmentTask; +import org.jabref.logic.directorylibrary.PdfEntryFactory; +import org.jabref.logic.git.util.GitHandlerRegistry; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.TaskExecutor; +import org.jabref.model.entry.BibEntryTypesManager; +import org.jabref.model.util.FileUpdateMonitor; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/// Opens a directory as a library: the main table fills from the Hayagriva `.yml` sidecars and +/// `.pdf` files found in the directory tree (see [DirectoryLibraryScanner]). +public class OpenDirectoryLibraryAction extends SimpleCommand { + + private static final Logger LOGGER = LoggerFactory.getLogger(OpenDirectoryLibraryAction.class); + + private final LibraryTabContainer tabContainer; + private final DialogService dialogService; + private final GuiPreferences preferences; + private final AiService aiService; + private final StateManager stateManager; + private final FileUpdateMonitor fileUpdateMonitor; + private final BibEntryTypesManager entryTypesManager; + private final GitHandlerRegistry gitHandlerRegistry; + private final ClipBoardManager clipBoardManager; + private final TaskExecutor taskExecutor; + + public OpenDirectoryLibraryAction(LibraryTabContainer tabContainer, + DialogService dialogService, + GuiPreferences preferences, + AiService aiService, + StateManager stateManager, + FileUpdateMonitor fileUpdateMonitor, + BibEntryTypesManager entryTypesManager, + GitHandlerRegistry gitHandlerRegistry, + ClipBoardManager clipBoardManager, + TaskExecutor taskExecutor) { + this.tabContainer = tabContainer; + this.dialogService = dialogService; + this.preferences = preferences; + this.aiService = aiService; + this.stateManager = stateManager; + this.fileUpdateMonitor = fileUpdateMonitor; + this.entryTypesManager = entryTypesManager; + this.gitHandlerRegistry = gitHandlerRegistry; + this.clipBoardManager = clipBoardManager; + this.taskExecutor = taskExecutor; + } + + @Override + public void execute() { + DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(preferences.getFilePreferences().getWorkingDirectory()) + .build(); + dialogService.showDirectorySelectionDialog(directoryDialogConfiguration) + .ifPresent(root -> { + preferences.getFilePreferences().setWorkingDirectory(root); + openDirectory(root); + }); + } + + /// Opens the directory as a library without showing the chooser (used by the session + /// restore and internal routing). An already open directory library is raised instead. + public void openDirectory(Path directory) { + Path root = directory.toAbsolutePath().normalize(); + tabContainer.getLibraryTabs().stream() + .filter(tab -> tab.getBibDatabaseContext().getDirectoryLibraryRoot() + .map(openRoot -> openRoot.toAbsolutePath().normalize()) + .filter(root::equals) + .isPresent()) + .findFirst() + .ifPresentOrElse(tabContainer::showLibraryTab, () -> scanAndShow(root)); + } + + private void scanAndShow(Path root) { + PdfEntryFactory pdfEntryFactory = new PdfEntryFactory( + preferences.getImportFormatPreferences(), preferences.getFilePreferences(), + preferences.getCitationKeyPatternPreferences()); + BackgroundTask.wrap(() -> new DirectoryLibraryScanner(pdfEntryFactory).scan(root)) + .onSuccess(scanResult -> showLibraryTab(scanResult, pdfEntryFactory)) + .onFailure(exception -> dialogService.showErrorDialogAndWait( + Localization.lang("Open folder as library"), + Localization.lang("Could not open folder '%0' as library.", root.toString()), + exception)) + .executeWith(taskExecutor); + } + + private void showLibraryTab(DirectoryLibraryScanner.ScanResult scanResult, PdfEntryFactory pdfEntryFactory) { + // The synchronous factory keeps the DIRECTORY location: the ParserResult-based one + // reconstructs a fresh (LOCAL) context from database + metadata on loading success + LibraryTab libraryTab = LibraryTab.createLibraryTab( + scanResult.databaseContext(), + tabContainer, + dialogService, + aiService, + preferences, + stateManager, + fileUpdateMonitor, + entryTypesManager, + clipBoardManager, + taskExecutor, + gitHandlerRegistry); + tabContainer.addTab(libraryTab, true); + if (!scanResult.pendingPdfImports().isEmpty()) { + PdfEnrichmentTask enrichment = new PdfEnrichmentTask(scanResult.pendingPdfImports(), pdfEntryFactory, + scanResult.databaseContext(), UiTaskExecutor::runInJavaFXThread); + enrichment.onFailure(exception -> LOGGER.error("Extracting PDF metadata failed", exception)); + cancelOnClose(libraryTab, enrichment); + enrichment.executeWith(taskExecutor); + } + + if (!scanResult.warnings().isEmpty()) { + dialogService.showWarningDialogAndWait( + Localization.lang("Open folder as library"), + String.join("\n", scanResult.warnings())); + } + } + + /// The enrichment mutates entries of the tab's library, so it must not outlive the tab. + private static void cancelOnClose(LibraryTab libraryTab, PdfEnrichmentTask enrichment) { + EventHandler onClosed = libraryTab.getOnClosed(); + libraryTab.setOnClosed(event -> { + enrichment.cancel(); + onClosed.handle(event); + }); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesView.java b/jabgui/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesView.java index d93aedbe247e..e745030452ff 100644 --- a/jabgui/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesView.java +++ b/jabgui/src/main/java/org/jabref/gui/libraryproperties/general/GeneralPropertiesView.java @@ -19,6 +19,7 @@ import org.jabref.gui.util.ViewModelListCellFactory; import org.jabref.logic.l10n.Localization; import org.jabref.logic.preferences.CliPreferences; +import org.jabref.logic.shared.DatabaseLocation; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; @@ -39,6 +40,8 @@ public class GeneralPropertiesView extends AbstractPropertiesTabView { boolean isAbsolute = Path.of(newValue).isAbsolute(); libSpecificFileDirSwitchIcon.setGlyph(isAbsolute ? RELATIVE_PATH : ABSOLUTE_PATH); diff --git a/jabgui/src/main/resources/org/jabref/gui/libraryproperties/general/GeneralProperties.fxml b/jabgui/src/main/resources/org/jabref/gui/libraryproperties/general/GeneralProperties.fxml index b1d2e0bef31b..ab0665cee773 100644 --- a/jabgui/src/main/resources/org/jabref/gui/libraryproperties/general/GeneralProperties.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/libraryproperties/general/GeneralProperties.fxml @@ -51,7 +51,7 @@ - -