Skip to content
Prev Previous commit
Next Next commit
Handle mixed dropped files in import dialog flow
  • Loading branch information
Jiahao He committed May 18, 2026
commit 7461c6b7cf9ae99811c1c4462f359af2ea0c5ea7
10 changes: 6 additions & 4 deletions jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -236,12 +236,14 @@ private void setupDragAndDrop() {
stateManager,
dialogService,
taskExecutor);
if (importHandler.shouldShowImportDialog(files)) {
importHandler.importBibliographyFilesWithDialog(files);
} else {
ImportHandler.DroppedFileImportPlan droppedFileImportPlan = importHandler.planDroppedFiles(files);
if (droppedFileImportPlan.hasBibliographyFiles()) {
importHandler.importBibliographyFilesWithDialog(droppedFileImportPlan.bibliographyFiles());
}
if (!droppedFileImportPlan.remainingFiles().isEmpty()) {
// Modifiers do not work on macOS: https://bugs.openjdk.org/browse/JDK-8264172
// Similar code as org.jabref.gui.externalfiles.ImportHandler.importFilesInBackground
DragDrop.handleDropOfFiles(files, transferMode, fileLinker, entry);
DragDrop.handleDropOfFiles(droppedFileImportPlan.remainingFiles(), transferMode, fileLinker, entry);
}
success = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,22 @@ public ExternalFilesEntryLinker getFileLinker() {
}

public boolean shouldShowImportDialog(List<Path> files) {
return !files.isEmpty() && files.stream().allMatch(this::isImportableBibliographyFile);
return planDroppedFiles(files).hasBibliographyFiles();
}

public DroppedFileImportPlan planDroppedFiles(List<Path> files) {
List<Path> bibliographyFiles = new ArrayList<>();
List<Path> remainingFiles = new ArrayList<>();

for (Path file : files) {
if (isImportableBibliographyFile(file)) {
bibliographyFiles.add(file);
} else {
remainingFiles.add(file);
}
}

return new DroppedFileImportPlan(List.copyOf(bibliographyFiles), List.copyOf(remainingFiles));
}

public void importBibliographyFilesWithDialog(List<Path> files) {
Expand Down Expand Up @@ -272,9 +287,7 @@ private ParserResult parseBibliographyFiles(List<Path> files) {
UiTaskExecutor.runAndWaitInJavaFXThread(() ->
dialogService.showWarningDialogAndWait(
Localization.lang("Import error"),
Localization.lang("Please check your library file for wrong syntax.")
+ "\n\n"
+ e.getLocalizedMessage()));
Localization.lang("Please check your library file for wrong syntax.%0", "\n\n" + e.getLocalizedMessage())));
}
}

Expand All @@ -301,6 +314,12 @@ private ImportFormatReader createImportFormatReader() {
fileUpdateMonitor);
}

public record DroppedFileImportPlan(List<Path> bibliographyFiles, List<Path> remainingFiles) {
public boolean hasBibliographyFiles() {
return !bibliographyFiles.isEmpty();
}
}

/// Cleans up the given entries and adds them to the library.
public void importEntries(List<BibEntry> entries) {
ImportCleanup cleanup = ImportCleanup.targeting(targetBibDatabaseContext.getMode(), preferences.getFieldPreferences());
Expand Down
29 changes: 17 additions & 12 deletions jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ private void handleOnDragDropped(TableRow<BibEntryTableViewModel> row, BibEntryT
.map(File::toPath)
.map(FileUtil::resolveIfShortcut)
.collect(Collectors.toList());
ImportHandler.DroppedFileImportPlan droppedFileImportPlan = importHandler.planDroppedFiles(files);

Comment on lines 542 to 546

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Ui-thread file parsing 🐞 Bug ➹ Performance

EntryEditor/MainTable drag-and-drop handlers call ImportHandler.planDroppedFiles on the JavaFX event
thread, but planDroppedFiles classifies files by running ImportFormatReader.importWithAutoDetection
(full file I/O + parsing) per file. This can freeze drag-and-drop responsiveness and also wastes
work by parsing twice (classification + dialog parsing).
Agent Prompt
## Issue description
Drag-and-drop currently blocks the JavaFX event thread by calling `ImportHandler.planDroppedFiles(...)`, which synchronously parses each dropped file via `ImportFormatReader.importWithAutoDetection(...)` to decide whether it is a bibliography file. This can cause UI freezes for large files or multi-file drops, and it also causes double work because the same files are parsed again when the import dialog runs.

## Issue Context
- `planDroppedFiles` is invoked directly inside JavaFX drag-drop handlers (EntryEditor + MainTable).
- `planDroppedFiles` calls `isImportableBibliographyFile` for each file.
- `isImportableBibliographyFile` creates a new `ImportFormatReader` and runs `importWithAutoDetection`, which iterates through many importers and reads file contents.
- The import dialog later triggers parsing again (`parseBibliographyFiles`).

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java[537-600]
- jabgui/src/main/java/org/jabref/gui/entryeditor/EntryEditor.java[214-248]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[119-143]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[123-136]
- jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java[263-315]

## Suggested implementation direction
- Make dropped-file classification non-blocking:
  - Option A (preferred): classify by extension using available importers’ `FileType`/extensions (no file I/O) and let the import dialog/background task handle actual parsing + errors.
  - Option B: run `planDroppedFiles` in a `BackgroundTask` executed via `taskExecutor`, then open the dialog on the FX thread once classification completes.
- Avoid repeated work:
  - Reuse a single `ImportFormatReader` instance per drop operation (instead of constructing one per file).
  - If you keep content-based detection, consider caching `ImportResult`/`ParserResult` from classification and passing it into the dialog to avoid re-parsing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

// Depending on the pressed modifier, move/copy/link files to drop target
// Modifiers do not work on macOS: https://bugs.openjdk.org/browse/JDK-8264172
Expand All @@ -552,20 +553,22 @@ private void handleOnDragDropped(TableRow<BibEntryTableViewModel> row, BibEntryT
// - Bottom + top -> import entries
case TOP,
BOTTOM -> {
if (importHandler.shouldShowImportDialog(files)) {
importHandler.importBibliographyFilesWithDialog(files);
} else {
importHandler.importFilesInBackground(files, transferMode).executeWith(taskExecutor);
if (droppedFileImportPlan.hasBibliographyFiles()) {
importHandler.importBibliographyFilesWithDialog(droppedFileImportPlan.bibliographyFiles());
}
if (!droppedFileImportPlan.remainingFiles().isEmpty()) {
importHandler.importFilesInBackground(droppedFileImportPlan.remainingFiles(), transferMode).executeWith(taskExecutor);
}
}
// - Center -> modify entry: link files to entry
case CENTER -> {
if (importHandler.shouldShowImportDialog(files)) {
importHandler.importBibliographyFilesWithDialog(files);
} else {
if (droppedFileImportPlan.hasBibliographyFiles()) {
importHandler.importBibliographyFilesWithDialog(droppedFileImportPlan.bibliographyFiles());
}
if (!droppedFileImportPlan.remainingFiles().isEmpty()) {
BibEntry entry = target.getEntry();
ExternalFilesEntryLinker fileLinker = importHandler.getFileLinker();
DragDrop.handleDropOfFiles(files, transferMode, fileLinker, entry);
DragDrop.handleDropOfFiles(droppedFileImportPlan.remainingFiles(), transferMode, fileLinker, entry);
}
}
}
Expand All @@ -585,11 +588,13 @@ private void handleOnDragDroppedTableView(DragEvent event) {
.map(File::toPath)
.map(FileUtil::resolveIfShortcut)
.toList();
if (importHandler.shouldShowImportDialog(files)) {
importHandler.importBibliographyFilesWithDialog(files);
} else {
ImportHandler.DroppedFileImportPlan droppedFileImportPlan = importHandler.planDroppedFiles(files);
if (droppedFileImportPlan.hasBibliographyFiles()) {
importHandler.importBibliographyFilesWithDialog(droppedFileImportPlan.bibliographyFiles());
}
if (!droppedFileImportPlan.remainingFiles().isEmpty()) {
importHandler
.importFilesInBackground(files, event.getTransferMode())
.importFilesInBackground(droppedFileImportPlan.remainingFiles(), event.getTransferMode())
.executeWith(taskExecutor);
}
success = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,25 @@ void shouldNotShowImportDialogForPlainTextFile() throws IOException {
assertFalse(importHandler.shouldShowImportDialog(List.of(textFile)));
}

@Test
void planDroppedFilesSeparatesBibliographyFilesFromOtherFiles() throws IOException {
Path risFile = tempDir.resolve("test.ris");
Files.writeString(risFile, """
TY - JOUR
TI - Drag and drop test
AU - Doe, Jane
PY - 2024
ER -
""");
Path textFile = tempDir.resolve("notes.txt");
Files.writeString(textFile, "plain text");

ImportHandler.DroppedFileImportPlan droppedFileImportPlan = importHandler.planDroppedFiles(List.of(risFile, textFile));

assertEquals(List.of(risFile), droppedFileImportPlan.bibliographyFiles());
assertEquals(List.of(textFile), droppedFileImportPlan.remainingFiles());
}
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
Outdated

@Test
void findDuplicateTest() {
// Assume there is no duplicate initially
Expand Down