Skip to content

Commit a9b50c6

Browse files
w0nderfu11calixtusSiedlerchr
authored
Add context menu for multi-file entries (#12567) (#13726)
* Add context menu for multi-file entries (#12567) Extend the Entry Editor context menu to handle entries with multiple linked files. This improves UX when managing more than one file per entry and aligns behavior across single- and multi-file scenarios. Changes: - Add plural actions to StandardActions enum - Rewrite ContextAction.execute() for multi-file cases - Extend ContextMenuFactory to build multi-file items - Rework MultiContextAction to operate on selections - Introduce CopyMultipleFilesAction (new class) - Update/add localization keys; tests pass - Add unit tests: ContextActionTest, ContextMenuFactoryTest, MultiContextActionTest, CopyMultipleFilesActionTest - Guard LinkedFileViewModel to avoid a JavaFX crash Fixes #12567 Keywords: context menu, linked files, multi-selection, UI * Apply OpenRewrite recipes * Fixed test and optimized imports * fixed library and space in catch block * Runned rewrite, fixed catch block and optimize logic of test ContextMenuFactory * Fix tests: initialize JavaFX toolkit; replace empty catch with meaningful check * Fix tests: added space after { * Trigger CI * Trigger CI * Update jabgui/src/test/java/org/jabref/gui/copyfiles/CopyMultipleFilesActionTest.java Co-authored-by: Carl Christian Snethlage <[email protected]> * refactor(gui/contextmenu): restore Strategy pattern for linked files menu - Introduce ContextMenuBuilder + SingleSelectionMenuBuilder + MultiSelectionMenuBuilder - Extract shared checks/openContainingFolders into SelectionChecks - ContextMenuFactory delegates to strategies; no more branching by selection size - LinkedFilesEditor initializes ContextMenuFactory once and just requests menus on right-click - Replace plural menu commands with single StandardActions; multi-selection handled inside builders - Fix NPE in ContextAction executable binding by removing null observables and binding menu disable state properly - Remove obsolete MultiContextAction and its tests Follow-ups: - Convert hardcoded labels to i18n keys (Download file(s), Open folder(s), etc.) - Consider removing *_FILES actions from enum if unused elsewhere - Re-add unit tests around builders/factory (TestFX/JUnit5) once API stabilized * feat(gui): multi-file context menu for linked files - Introduce selection-aware builders (SingleSelectionMenuBuilder / MultiSelectionMenuBuilder) with SelectionChecks - Wire into LinkedFilesEditor via ContextMenuFactory/ContextAction; proper enablement bindings - Remove CopyMultipleFilesAction; update StandardActions and view models - i18n updates for pluralized items and copy messages - Tests for ContextAction/ContextMenuFactory/Single&MultiSelection builders/SelectionChecks - Changelog + cleanup Closes #12567 * Address review: Optional/map, non-null params, logger, tests, menus * fixed tests * Update jabgui/src/main/java/org/jabref/gui/copyfiles/CopySingleFileAction.java Co-authored-by: Carl Christian Snethlage <[email protected]> * Update jabgui/src/main/java/org/jabref/gui/copyfiles/CopySingleFileAction.java Co-authored-by: Carl Christian Snethlage <[email protected]> * wip: local changes before style sync * Fix styles * Final style and CI checks * docs: sync http-server howto; jablib: sync jspecify @nullable from upstream * Naming convention * Fix LOGGER naming (convension) * fixed submodules * Fix submodules * chore: reset submodules to upstream/main (csl-styles, csl-locales) * Fix submodules * Fix test classes * Fix test classes V2 (forget to update MultiSelectionMenuBuilderTest and SelectionChecksTest) * Apply JSpecify annotations * Fixed bot's cases and fixed submodules * Fix submodules * Migrate to JSpecify annotations * Fix wording * Changed assert methods for readability * interface ---< class, deleted variable * Refactored ContextMenuFactoryTest for readability * Added fixes, deleted SelectionChecks * changelog fixed and localization --------- Co-authored-by: Carl Christian Snethlage <[email protected]> Co-authored-by: Christoph <[email protected]> Co-authored-by: Carl Christian Snethlage <[email protected]>
1 parent 8ec4808 commit a9b50c6

17 files changed

+1212
-380
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
4545
- We added support for using OpenAlex fetcher. [#13940](https://github.com/JabRef/jabref/issues/13940)
4646
- We added an option to choose the group during import of the entry(s). [#9191](https://github.com/JabRef/jabref/issues/9191)
4747
- We added an option to search and filter the fields and formatters in the Clean up entries dialog. [#13890](https://github.com/JabRef/jabref/issues/13890)
48+
- We added support for managing multiple linked files via the entry context menu. [#12567](https://github.com/JabRef/jabref/issues/12567)
4849

4950
### Changed
5051

jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,8 @@ public enum StandardActions implements Action {
115115
EDIT_EXISTING_STUDY(Localization.lang("Manage study definition")),
116116

117117
OPEN_DATABASE_FOLDER(Localization.lang("Reveal in file explorer")),
118-
OPEN_FOLDER(Localization.lang("Open folder"), Localization.lang("Open folder"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER),
119-
OPEN_FILE(Localization.lang("Open file"), Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE),
118+
OPEN_FOLDER(Localization.lang("Open folder(s)"), Localization.lang("Open folder"), IconTheme.JabRefIcons.FOLDER, KeyBinding.OPEN_FOLDER),
119+
OPEN_FILE(Localization.lang("Open file(s)"), Localization.lang("Open file"), IconTheme.JabRefIcons.FILE, KeyBinding.OPEN_FILE),
120120
OPEN_CONSOLE(Localization.lang("Open terminal here"), Localization.lang("Open terminal here"), IconTheme.JabRefIcons.CONSOLE, KeyBinding.OPEN_CONSOLE),
121121
COPY_LINKED_FILES(Localization.lang("Copy linked files to folder...")),
122122
COPY_DOI(Localization.lang("Copy DOI")),
@@ -169,16 +169,16 @@ public enum StandardActions implements Action {
169169
SET_FILE_LINKS(Localization.lang("Automatically set file links"), KeyBinding.AUTOMATICALLY_LINK_FILES),
170170

171171
EDIT_FILE_LINK(Localization.lang("Edit"), IconTheme.JabRefIcons.EDIT, KeyBinding.OPEN_CLOSE_ENTRY_EDITOR),
172-
DOWNLOAD_FILE(Localization.lang("Download file"), IconTheme.JabRefIcons.DOWNLOAD_FILE),
173-
REDOWNLOAD_FILE(Localization.lang("Redownload file"), IconTheme.JabRefIcons.DOWNLOAD_FILE),
172+
DOWNLOAD_FILE(Localization.lang("Download file(s)"), IconTheme.JabRefIcons.DOWNLOAD_FILE),
173+
REDOWNLOAD_FILE(Localization.lang("Redownload file(s)"), IconTheme.JabRefIcons.DOWNLOAD_FILE),
174174
RENAME_FILE_TO_PATTERN(Localization.lang("Rename file to defined pattern"), IconTheme.JabRefIcons.AUTO_RENAME),
175-
RENAME_FILE_TO_NAME(Localization.lang("Rename files to configured filename format pattern"), IconTheme.JabRefIcons.RENAME, KeyBinding.REPLACE_STRING),
176-
MOVE_FILE_TO_FOLDER(Localization.lang("Move file to file directory"), IconTheme.JabRefIcons.MOVE_TO_FOLDER),
175+
RENAME_FILE_TO_NAME(Localization.lang("Rename file(s) to configured filename format pattern"), IconTheme.JabRefIcons.RENAME, KeyBinding.REPLACE_STRING),
176+
MOVE_FILE_TO_FOLDER(Localization.lang("Move file(s) to file directory"), IconTheme.JabRefIcons.MOVE_TO_FOLDER),
177177
MOVE_FILE_TO_FOLDER_AND_RENAME(Localization.lang("Move file to file directory and rename file")),
178-
COPY_FILE_TO_FOLDER(Localization.lang("Copy linked file to folder..."), IconTheme.JabRefIcons.COPY_TO_FOLDER, KeyBinding.COPY),
178+
COPY_FILE_TO_FOLDER(Localization.lang("Copy linked file(s) to folder..."), IconTheme.JabRefIcons.COPY_TO_FOLDER, KeyBinding.COPY),
179179
REMOVE_LINK(Localization.lang("Remove link"), IconTheme.JabRefIcons.REMOVE_LINK),
180180
REMOVE_LINKS(Localization.lang("Remove links"), IconTheme.JabRefIcons.REMOVE_LINK),
181-
DELETE_FILE(Localization.lang("Permanently delete local file"), IconTheme.JabRefIcons.DELETE_FILE, KeyBinding.DELETE_ENTRY),
181+
DELETE_FILE(Localization.lang("Permanently delete local file(s)"), IconTheme.JabRefIcons.DELETE_FILE, KeyBinding.DELETE_ENTRY),
182182

183183
HELP(Localization.lang("Online help"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
184184
HELP_GROUPS(Localization.lang("Open Help page"), IconTheme.JabRefIcons.HELP, KeyBinding.HELP),
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package org.jabref.gui.copyfiles;
2+
3+
import java.nio.file.Path;
4+
import java.util.ArrayList;
5+
import java.util.Collection;
6+
import java.util.List;
7+
import java.util.Optional;
8+
import java.util.function.BiFunction;
9+
10+
import javafx.beans.Observable;
11+
import javafx.beans.binding.Bindings;
12+
13+
import org.jabref.gui.DialogService;
14+
import org.jabref.gui.actions.SimpleCommand;
15+
import org.jabref.gui.util.DirectoryDialogConfiguration;
16+
import org.jabref.logic.FilePreferences;
17+
import org.jabref.logic.l10n.Localization;
18+
import org.jabref.logic.util.io.FileUtil;
19+
import org.jabref.model.database.BibDatabaseContext;
20+
import org.jabref.model.entry.LinkedFile;
21+
22+
public class CopyLinkedFilesAction extends SimpleCommand {
23+
24+
private final List<LinkedFile> linkedFiles;
25+
private final DialogService dialogService;
26+
private final BibDatabaseContext databaseContext;
27+
private final FilePreferences filePreferences;
28+
29+
private final BiFunction<Path, Path, Path> resolvePathFilename = (dir, file) -> dir.resolve(file.getFileName());
30+
31+
public CopyLinkedFilesAction(LinkedFile linkedFile,
32+
DialogService dialogService,
33+
BibDatabaseContext databaseContext,
34+
FilePreferences filePreferences) {
35+
this(List.of(linkedFile), dialogService, databaseContext, filePreferences);
36+
}
37+
38+
public CopyLinkedFilesAction(Collection<LinkedFile> linkedFiles,
39+
DialogService dialogService,
40+
BibDatabaseContext databaseContext,
41+
FilePreferences filePreferences) {
42+
this.linkedFiles = new ArrayList<>(linkedFiles);
43+
this.dialogService = dialogService;
44+
this.databaseContext = databaseContext;
45+
this.filePreferences = filePreferences;
46+
47+
this.executable.bind(Bindings.createBooleanBinding(
48+
() -> this.linkedFiles.stream().allMatch(this::isLocalExisting),
49+
dependencies(this.linkedFiles)));
50+
}
51+
52+
@Override
53+
public void execute() {
54+
DirectoryDialogConfiguration dirDialogConfiguration = new DirectoryDialogConfiguration.Builder()
55+
.withInitialDirectory(filePreferences.getWorkingDirectory())
56+
.build();
57+
58+
Optional<Path> exportDir = dialogService.showDirectorySelectionDialog(dirDialogConfiguration);
59+
if (exportDir.isEmpty()) {
60+
return;
61+
}
62+
63+
int copiedFiles = 0;
64+
int failedCount = 0;
65+
66+
for (LinkedFile file : linkedFiles) {
67+
Optional<Path> srcOpt = file.findIn(databaseContext, filePreferences);
68+
if (srcOpt.isEmpty()) {
69+
failedCount++;
70+
continue;
71+
}
72+
73+
Path src = srcOpt.get();
74+
Path dst = resolvePathFilename.apply(exportDir.get(), src);
75+
76+
if (FileUtil.copyFile(src, dst, false)) {
77+
copiedFiles++;
78+
} else {
79+
failedCount++;
80+
}
81+
}
82+
83+
String target = exportDir.map(Path::toString).orElse("");
84+
85+
if (linkedFiles.size() == 1) {
86+
if (copiedFiles == 1) {
87+
dialogService.notify(Localization.lang("Successfully copied %0 file(s) to %1.", copiedFiles, target));
88+
} else {
89+
dialogService.notify(Localization.lang("Could not copy file to %0, maybe the file is already existing?", target));
90+
}
91+
} else {
92+
if (failedCount == 0) {
93+
dialogService.notify(Localization.lang("Successfully copied %0 file(s) to %1.", copiedFiles, target));
94+
} else {
95+
dialogService.notify(Localization.lang("Copied %0 file(s). Failed: %1", copiedFiles, failedCount));
96+
}
97+
}
98+
}
99+
100+
private boolean isLocalExisting(LinkedFile lf) {
101+
return !lf.isOnlineLink() && lf.findIn(databaseContext, filePreferences).isPresent();
102+
}
103+
104+
private static Observable[] dependencies(Collection<LinkedFile> files) {
105+
return files.stream().map(LinkedFile::linkProperty).toArray(Observable[]::new);
106+
}
107+
}

jabgui/src/main/java/org/jabref/gui/copyfiles/CopySingleFileAction.java

Lines changed: 0 additions & 65 deletions
This file was deleted.

jabgui/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,6 @@ public void acceptAsLinked() {
203203

204204
public Observable[] getObservables() {
205205
List<Observable> observables = new ArrayList<>(Arrays.asList(linkedFile.getObservables()));
206-
observables.add(downloadOngoing);
207-
observables.add(downloadProgress);
208206
observables.add(isAutomaticallyFound);
209207
return observables.toArray(new Observable[0]);
210208
}
@@ -442,9 +440,15 @@ public void redownload() {
442440
throw new UnsupportedOperationException("In order to download the file, the source url has to be an online link");
443441
}
444442

443+
DownloadLinkedFileAction downloadLinkedFileAction = getDownloadLinkedFileAction();
444+
downloadProgress.bind(downloadLinkedFileAction.downloadProgress());
445+
downloadLinkedFileAction.execute();
446+
}
447+
448+
private DownloadLinkedFileAction getDownloadLinkedFileAction() {
445449
String fileName = Path.of(linkedFile.getLink()).getFileName().toString();
446450

447-
DownloadLinkedFileAction downloadLinkedFileAction = new DownloadLinkedFileAction(
451+
return new DownloadLinkedFileAction(
448452
databaseContext,
449453
entry,
450454
linkedFile,
@@ -455,8 +459,6 @@ public void redownload() {
455459
taskExecutor,
456460
fileName,
457461
true);
458-
downloadProgress.bind(downloadLinkedFileAction.downloadProgress());
459-
downloadLinkedFileAction.execute();
460462
}
461463

462464
public void download(boolean keepHtmlLink) {

0 commit comments

Comments
 (0)