diff --git a/CHANGELOG.md b/CHANGELOG.md index 669fcc59ee6..fd71d92e478 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 the option in Preferences → Linked files → Attached files to adjust the path of attached files and copy them if needed when entries are copied to another library [#12267](https://github.com/JabRef/jabref/issues/12267) - We fixed an issue where "Print preview" would throw a `NullPointerException` if no printers were available. [#13708](https://github.com/JabRef/jabref/issues/13708) - We added the option to enable the language server in the preferences. [#13697](https://github.com/JabRef/jabref/pull/13697) - We introduced an option in Preferences under (under Linked files -> Linked file name conventions) to automatically rename linked files when an entry data changes. [#11316](https://github.com/JabRef/jabref/issues/11316) diff --git a/docs/requirements/files.md b/docs/requirements/files.md new file mode 100644 index 00000000000..d127e152e23 --- /dev/null +++ b/docs/requirements/files.md @@ -0,0 +1,25 @@ +# File Transfer Between Bib Entries + +*Note:* +"Reachable" here denotes that the linked file can be accessed via a relative path that does **not** climb up the directory structure (i.e., no "`..`" segments beyond the root directory). +Additionally, this check respects all configured **directories for files** as defined in JabRef's file linking settings (see [directories for files](https://docs.jabref.org/finding-sorting-and-cleaning-entries/filelinks#directories-for-files)). + +## File is reachable and should not be copied +`req~logic.externalfiles.file-transfer.reachable-no-copy~1` +When a linked file is reachable from the target context, the system must adjust the relative path in the target entry but must not copy the file again. + +Needs: impl + +## File is not reachable, but the path is the same +`req~logic.externalfiles.file-transfer.not-reachable-same-path~1` +When a linked file is not reachable from the target context, and the relative path in both source and target entry is the same, the file must be copied to the target context. + +Needs: impl + +## File is not reachable, and a different path is used +`req~logic.externalfiles.file-transfer.not-reachable-different-path~1` +When a linked file is not reachable from the target context, and the relative path differs between source and target entries, the file must be copied and the directory structure must be created to preserve the relative link. + +Needs: impl + + diff --git a/jabgui/src/main/java/org/jabref/gui/ClipBoardManager.java b/jabgui/src/main/java/org/jabref/gui/ClipBoardManager.java index f96510c1e02..373ca5c75f7 100644 --- a/jabgui/src/main/java/org/jabref/gui/ClipBoardManager.java +++ b/jabgui/src/main/java/org/jabref/gui/ClipBoardManager.java @@ -7,6 +7,7 @@ import java.awt.datatransfer.UnsupportedFlavorException; import java.io.IOException; import java.util.List; +import java.util.Optional; import javafx.application.Platform; import javafx.scene.control.TextInputControl; @@ -18,12 +19,14 @@ import org.jabref.logic.bibtex.BibEntryWriter; import org.jabref.logic.bibtex.FieldWriter; import org.jabref.logic.preferences.CliPreferences; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.database.BibDatabaseMode; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.BibEntryTypesManager; import org.jabref.model.entry.BibtexString; import com.airhacks.afterburner.injection.Injector; +import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,8 +35,11 @@ public class ClipBoardManager { private static final Logger LOGGER = LoggerFactory.getLogger(ClipBoardManager.class); private static Clipboard clipboard; + private static java.awt.datatransfer.Clipboard primary; + private BibDatabaseContext sourceDatabaseContext; + public ClipBoardManager() { this(Clipboard.getSystemClipboard(), Toolkit.getDefaultToolkit().getSystemSelection()); } @@ -162,6 +168,14 @@ public void setContent(List entries, BibEntryTypesManager entryTypesMa setContent(builder.toString()); } + public Optional getSourceBibDatabaseContext() { + return Optional.ofNullable(sourceDatabaseContext); + } + + public void setSourceBibDatabaseContext(@NonNull BibDatabaseContext context) { + sourceDatabaseContext = context; + } + private String serializeEntries(List entries, BibEntryTypesManager entryTypesManager) throws IOException { CliPreferences preferences = Injector.instantiateModelOrService(CliPreferences.class); // BibEntry is not Java serializable. Thus, we need to do the serialization manually diff --git a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java index e68abe6fa04..5da70e00b4a 100644 --- a/jabgui/src/main/java/org/jabref/gui/LibraryTab.java +++ b/jabgui/src/main/java/org/jabref/gui/LibraryTab.java @@ -104,6 +104,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.jabref.gui.util.CopyUtil.copyEntriesWithFeedback; + /** * Represents the ui area where the notifier pane, the library table and the entry editor are shown. */ @@ -810,6 +812,7 @@ public void insertEntries(final List entries) { } public void copyEntry() { + clipBoardManager.setSourceBibDatabaseContext(this.getBibDatabaseContext()); int entriesCopied = doCopyEntry(getSelectedEntries()); if (entriesCopied >= 0) { dialogService.notify(Localization.lang("Copied %0 entry(s)", entriesCopied)); @@ -838,17 +841,27 @@ private int doCopyEntry(List selectedEntries) { } public void pasteEntry() { - List entriesToAdd; String content = ClipBoardManager.getContents(); - entriesToAdd = importHandler.handleBibTeXData(content); + List entriesToAdd = importHandler.handleBibTeXData(content); if (entriesToAdd.isEmpty()) { entriesToAdd = handleNonBibTeXStringData(content); } if (entriesToAdd.isEmpty()) { return; } - - importHandler.importEntriesWithDuplicateCheck(bibDatabaseContext, entriesToAdd); + // Now, the BibEntries to add are known + // The definitive insertion needs to happen now. + BibDatabaseContext sourceBibDatabaseContext = clipBoardManager.getSourceBibDatabaseContext().orElse(null); + copyEntriesWithFeedback( + sourceBibDatabaseContext, + entriesToAdd, + bibDatabaseContext, + Localization.lang("Pasted %0 entry(s) to %1"), + Localization.lang("Pasted %0 entry(s) to %1. %2 were skipped"), + dialogService, + preferences.getFilePreferences(), + importHandler + ); } private List handleNonBibTeXStringData(String data) { @@ -866,11 +879,21 @@ private List handleNonBibTeXStringData(String data) { } } - public void dropEntry(List entriesToAdd) { - importHandler.importEntriesWithDuplicateCheck(bibDatabaseContext, entriesToAdd); + public void dropEntry(BibDatabaseContext sourceBibDatabaseContext, List entriesToAdd) { + copyEntriesWithFeedback( + sourceBibDatabaseContext, + entriesToAdd, + bibDatabaseContext, + Localization.lang("Moved %0 entry(s) to %1"), + Localization.lang("Moved %0 entry(s) to %1. %2 were skipped"), + dialogService, + preferences.getFilePreferences(), + importHandler + ); } public void cutEntry() { + clipBoardManager.setSourceBibDatabaseContext(this.getBibDatabaseContext()); int entriesCopied = doCopyEntry(getSelectedEntries()); int entriesDeleted = doDeleteEntry(StandardActions.CUT, mainTable.getSelectedEntries()); diff --git a/jabgui/src/main/java/org/jabref/gui/edit/CopyTo.java b/jabgui/src/main/java/org/jabref/gui/edit/CopyTo.java index 2164994660c..e12510450a0 100644 --- a/jabgui/src/main/java/org/jabref/gui/edit/CopyTo.java +++ b/jabgui/src/main/java/org/jabref/gui/edit/CopyTo.java @@ -8,8 +8,8 @@ import org.jabref.gui.StateManager; import org.jabref.gui.actions.ActionHelper; import org.jabref.gui.actions.SimpleCommand; -import org.jabref.gui.externalfiles.EntryImportHandlerTracker; import org.jabref.gui.externalfiles.ImportHandler; +import org.jabref.logic.FilePreferences; import org.jabref.logic.l10n.Localization; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; @@ -18,6 +18,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static org.jabref.gui.util.CopyUtil.copyEntriesWithFeedback; + public class CopyTo extends SimpleCommand { private static final Logger LOGGER = LoggerFactory.getLogger(CopyTo.class); @@ -25,6 +27,7 @@ public class CopyTo extends SimpleCommand { private final DialogService dialogService; private final StateManager stateManager; private final CopyToPreferences copyToPreferences; + private final FilePreferences filePreferences; private final ImportHandler importHandler; private final BibDatabaseContext sourceDatabaseContext; private final BibDatabaseContext targetDatabaseContext; @@ -32,12 +35,14 @@ public class CopyTo extends SimpleCommand { public CopyTo(DialogService dialogService, StateManager stateManager, CopyToPreferences copyToPreferences, + FilePreferences filePreferences, ImportHandler importHandler, BibDatabaseContext sourceDatabaseContext, BibDatabaseContext targetDatabaseContext) { this.dialogService = dialogService; this.stateManager = stateManager; this.copyToPreferences = copyToPreferences; + this.filePreferences = filePreferences; this.importHandler = importHandler; this.sourceDatabaseContext = sourceDatabaseContext; this.targetDatabaseContext = targetDatabaseContext; @@ -75,37 +80,29 @@ public void copyEntriesWithCrossRef(List selectedEntries, BibDatabaseC .flatMap(entry -> getCrossRefEntry(entry, sourceDatabaseContext).stream()).toList(); entriesToAdd.addAll(entriesWithCrossRef); - copyEntriesWithFeedback(entriesToAdd, targetDatabaseContext, - Localization.lang("Copied %0 entry(s) to %1, including cross-references"), - Localization.lang("Copied %0 entry(s) to %1. %2 were skipped including cross-references")); + copyEntriesWithFeedback( + sourceDatabaseContext, + entriesToAdd, + targetDatabaseContext, + Localization.lang("Copied %0 entry(s) to %1, including cross-references"), + Localization.lang("Copied %0 entry(s) to %1. %2 were skipped including cross-references"), + dialogService, + filePreferences, + importHandler + ); } public void copyEntriesWithoutCrossRef(List selectedEntries, BibDatabaseContext targetDatabaseContext) { - copyEntriesWithFeedback(selectedEntries, targetDatabaseContext, - Localization.lang("Copied %0 entry(s) to %1 without cross-references"), - Localization.lang("Copied %0 entry(s) to %1. %2 were skipped without cross-references")); - } - - private void copyEntriesWithFeedback(List entriesToAdd, BibDatabaseContext targetDatabaseContext, String successMessage, String partialMessage) { - EntryImportHandlerTracker tracker = new EntryImportHandlerTracker(entriesToAdd.size()); - tracker.setOnFinish(() -> { - int importedCount = tracker.getImportedCount(); - int skippedCount = tracker.getSkippedCount(); - - String targetName = targetDatabaseContext.getDatabasePath() - .map(path -> path.getFileName().toString()) - .orElse(Localization.lang("target library")); - - if (importedCount == entriesToAdd.size()) { - dialogService.notify(Localization.lang(successMessage, String.valueOf(importedCount), targetName)); - } else if (importedCount == 0) { - dialogService.notify(Localization.lang("No entry was copied to %0", targetName)); - } else { - dialogService.notify(Localization.lang(partialMessage, String.valueOf(importedCount), targetName, String.valueOf(skippedCount))); - } - }); - - importHandler.importEntriesWithDuplicateCheck(targetDatabaseContext, entriesToAdd, tracker); + copyEntriesWithFeedback( + sourceDatabaseContext, + selectedEntries, + targetDatabaseContext, + Localization.lang("Copied %0 entry(s) to %1, without cross-references"), + Localization.lang("Copied %0 entry(s) to %1. %2 were skipped without cross-references"), + dialogService, + filePreferences, + importHandler + ); } public Optional getCrossRefEntry(BibEntry bibEntryToCheck, BibDatabaseContext sourceDatabaseContext) { diff --git a/jabgui/src/main/java/org/jabref/gui/edit/EditAction.java b/jabgui/src/main/java/org/jabref/gui/edit/EditAction.java index 48133bb10da..49c45007caf 100644 --- a/jabgui/src/main/java/org/jabref/gui/edit/EditAction.java +++ b/jabgui/src/main/java/org/jabref/gui/edit/EditAction.java @@ -30,7 +30,8 @@ public class EditAction extends SimpleCommand { private final StateManager stateManager; private final UndoManager undoManager; - public EditAction(StandardActions action, Supplier tabSupplier, StateManager stateManager, UndoManager undoManager) { + public EditAction(StandardActions action, Supplier tabSupplier, StateManager stateManager, + UndoManager undoManager) { this.action = action; this.tabSupplier = tabSupplier; this.stateManager = stateManager; diff --git a/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index bb201b85f18..7658cb93760 100644 --- a/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -250,8 +250,20 @@ public void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext, importEntryWithDuplicateCheck(bibDatabaseContext, entry, BREAK, new EntryImportHandlerTracker()); } + /** + * Imports an entry into the database with duplicate checking and handling. + * Creates a copy of the entry for processing - the original entry parameter is not modified. + * The copied entry may be modified during cleanup and duplicate handling. + * + * @param bibDatabaseContext the database context to import into + * @param entry the entry to import (original will not be modified) + * @param decision the duplicate resolution strategy to apply + * @param tracker tracks the import status of the entry + */ private void importEntryWithDuplicateCheck(BibDatabaseContext bibDatabaseContext, BibEntry entry, DuplicateResolverDialog.DuplicateResolverResult decision, EntryImportHandlerTracker tracker) { - BibEntry entryToInsert = cleanUpEntry(bibDatabaseContext, entry); + // The original entry should not be modified + BibEntry entryCopy = new BibEntry(entry); + BibEntry entryToInsert = cleanUpEntry(bibDatabaseContext, entryCopy); BackgroundTask.wrap(() -> findDuplicate(bibDatabaseContext, entryToInsert)) .onFailure(e -> { diff --git a/jabgui/src/main/java/org/jabref/gui/frame/FrameDndHandler.java b/jabgui/src/main/java/org/jabref/gui/frame/FrameDndHandler.java index 1365fcfb959..a0a96398858 100644 --- a/jabgui/src/main/java/org/jabref/gui/frame/FrameDndHandler.java +++ b/jabgui/src/main/java/org/jabref/gui/frame/FrameDndHandler.java @@ -21,6 +21,7 @@ import org.jabref.gui.importer.actions.OpenDatabaseAction; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.io.FileUtil; +import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; import org.jabref.model.groups.GroupTreeNode; @@ -106,7 +107,8 @@ private void onTabDragDropped(Node destinationTabNode, DragEvent tabDragEvent, T if (hasEntries(dragboard)) { List entryCopies = stateManager.getLocalDragboard().getBibEntries().stream() .map(BibEntry::new).toList(); - destinationLibraryTab.dropEntry(entryCopies); + BibDatabaseContext sourceBibDatabaseContext = stateManager.getActiveDatabase().orElse(null); + destinationLibraryTab.dropEntry(sourceBibDatabaseContext, entryCopies); } else if (hasGroups(dragboard)) { dropGroups(dragboard, destinationLibraryTab); } @@ -213,7 +215,7 @@ private void copyGroupTreeNode(LibraryTab destinationLibraryTab, GroupTreeNode p // add groupTreeNodeToCopy to the parent-- in the first run that will the source/main GroupTreeNode GroupTreeNode copiedNode = parent.addSubgroup(groupTreeNodeToCopy.copyNode().getGroup()); // add all entries of a groupTreeNode to the new library. - destinationLibraryTab.dropEntry(groupTreeNodeToCopy.getEntriesInGroup(allEntries)); + destinationLibraryTab.dropEntry(stateManager.getActiveDatabase().get(), groupTreeNodeToCopy.getEntriesInGroup(allEntries)); // List of all children of groupTreeNodeToCopy List children = groupTreeNodeToCopy.getChildren(); diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index a7a913bf6e3..5f6e92c8a7a 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -152,7 +152,7 @@ private static Menu createCopyToMenu(ActionFactory factory, factory.createCustomMenuItem( StandardActions.COPY_TO, new CopyTo(dialogService, stateManager, preferences.getCopyToPreferences(), - importHandler, sourceDatabaseContext, targetDatabaseContext), + preferences.getFilePreferences(), importHandler, sourceDatabaseContext, targetDatabaseContext), targetDatabaseName ) ); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java index f4683d4a202..e3a55392b7a 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java @@ -42,6 +42,7 @@ public class LinkedFilesTab extends AbstractPreferenceTabView entriesToAdd, + BibDatabaseContext targetDatabaseContext, + String successMessage, + String partialMessage, + DialogService dialogService, + FilePreferences filePreferences, + ImportHandler importHandler) { + EntryImportHandlerTracker tracker = new EntryImportHandlerTracker(entriesToAdd.size()); + tracker.setOnFinish(() -> { + int importedCount = tracker.getImportedCount(); + int skippedCount = tracker.getSkippedCount(); + + String targetName = targetDatabaseContext.getDatabasePath() + .map(path -> path.getFileName().toString()) + .orElse(Localization.lang("target library")); + + if (importedCount == entriesToAdd.size()) { + dialogService.notify(Localization.lang(successMessage, String.valueOf(importedCount), targetName)); + } else if (importedCount == 0) { + dialogService.notify(Localization.lang("No entry was copied to %0", targetName)); + } else { + dialogService.notify(Localization.lang(partialMessage, String.valueOf(importedCount), targetName, String.valueOf(skippedCount))); + } + if (sourceDatabaseContext != null) { + LinkedFileTransferHelper + .adjustLinkedFilesForTarget(sourceDatabaseContext, + targetDatabaseContext, filePreferences); + } + }); + + importHandler.importEntriesWithDuplicateCheck(targetDatabaseContext, entriesToAdd, tracker); + } +} diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml index 947d1bdb7ae..40d422a89c5 100644 --- a/jabgui/src/main/resources/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml @@ -87,4 +87,5 @@