diff --git a/CHANGELOG.md b/CHANGELOG.md index bf91c2e88ff..a416bcffe57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ We refer to [GitHub issues](https://github.com/JabRef/jabref/issues) by using `# - We now set the WM_CLASS of the UI to org-jabref-JabRefMain to allow certain Un*x window managers to properly identify its windows - We changed the default paths for the OpenOffice/LibreOffice binaries to the default path for LibreOffice - We no longer create a new entry editor when selecting a new entry to increase performance. [#3187](https://github.com/JabRef/jabref/pull/3187) +- We added the possibility to copy linked files from entries to a single output folder [#2539](https://github.com/JabRef/jabref/pull/2593) - We increased performance and decreased the memory footprint of the entry editor drastically. [#3331](https://github.com/JabRef/jabref/pull/3331) - Late initialization of the context menus in the entry editor. This improves performance and memory footprint further [#3340](https://github.com/JabRef/jabref/pull/3340) diff --git a/src/main/java/org/jabref/gui/AbstractDialogView.java b/src/main/java/org/jabref/gui/AbstractDialogView.java index 3ab04359a7a..d7880e3cd11 100644 --- a/src/main/java/org/jabref/gui/AbstractDialogView.java +++ b/src/main/java/org/jabref/gui/AbstractDialogView.java @@ -1,10 +1,15 @@ package org.jabref.gui; +import java.util.function.Function; + public abstract class AbstractDialogView extends AbstractView { public AbstractDialogView() { super(); + } + public AbstractDialogView(Function injectionContext) { + super(injectionContext); } public abstract void show(); diff --git a/src/main/java/org/jabref/gui/DialogService.java b/src/main/java/org/jabref/gui/DialogService.java index 2475365ae13..482ca23243e 100644 --- a/src/main/java/org/jabref/gui/DialogService.java +++ b/src/main/java/org/jabref/gui/DialogService.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Optional; +import javafx.concurrent.Task; import javafx.scene.control.Alert; import javafx.scene.control.ButtonType; import javafx.scene.control.DialogPane; @@ -13,6 +14,8 @@ import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.logic.l10n.Localization; +import org.controlsfx.dialog.ProgressDialog; + /** * This interface provides methods to create dialogs and show them to the user. */ @@ -121,6 +124,12 @@ Optional showCustomButtonDialogAndWait(Alert.AlertType type, String */ Optional showCustomDialogAndWait(Dialog dialog); + /** + * Constructs and shows a canceable {@link ProgressDialog}. Clicking cancel will cancel the underlying service and close the dialog + * @param task The {@link Task} which executes the work and for which to show the dialog + */ + void showCanceableProgressDialogAndWait(Task task); + /** * Notify the user in an non-blocking way (i.e., update status message instead of showing a dialog). * @param message the message to show. @@ -173,4 +182,5 @@ Optional showCustomButtonDialogAndWait(Alert.AlertType type, String * @return A configured instance of the {@link FileChooser} */ FileChooser getConfiguredFileChooser(FileDialogConfiguration fileDialogConfiguration); + } diff --git a/src/main/java/org/jabref/gui/FXDialogService.java b/src/main/java/org/jabref/gui/FXDialogService.java index 6b474d13a2c..3beb1c4b416 100644 --- a/src/main/java/org/jabref/gui/FXDialogService.java +++ b/src/main/java/org/jabref/gui/FXDialogService.java @@ -7,7 +7,9 @@ import java.util.Optional; import java.util.stream.Collectors; +import javafx.concurrent.Task; import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.Button; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; import javafx.scene.control.DialogPane; @@ -22,6 +24,7 @@ import org.jabref.logic.l10n.Localization; import org.controlsfx.dialog.ExceptionDialog; +import org.controlsfx.dialog.ProgressDialog; /** * This class provides methods to create default @@ -127,6 +130,21 @@ public Optional showCustomDialogAndWait(Dialog dialog) { return dialog.showAndWait(); } + @Override + public void showCanceableProgressDialogAndWait(Task task) { + ProgressDialog progressDialog = new ProgressDialog(task); + progressDialog.setOnCloseRequest(evt -> task.cancel()); + DialogPane dialogPane = progressDialog.getDialogPane(); + dialogPane.getButtonTypes().add(ButtonType.CANCEL); + Button cancelButton = (Button) dialogPane.lookupButton(ButtonType.CANCEL); + cancelButton.setOnAction(evt -> { + task.cancel(); + progressDialog.close(); + }); + progressDialog.showAndWait(); + + } + @Override public void notify(String message) { JabRefGUI.getMainFrame().output(message); diff --git a/src/main/java/org/jabref/gui/JabRefFrame.java b/src/main/java/org/jabref/gui/JabRefFrame.java index 13d1a83366b..3e30b97409c 100644 --- a/src/main/java/org/jabref/gui/JabRefFrame.java +++ b/src/main/java/org/jabref/gui/JabRefFrame.java @@ -80,6 +80,7 @@ import org.jabref.gui.actions.SortTabsAction; import org.jabref.gui.autosaveandbackup.AutosaveUIManager; import org.jabref.gui.bibtexkeypattern.BibtexKeyPatternDialog; +import org.jabref.gui.copyfiles.CopyFilesAction; import org.jabref.gui.customentrytypes.EntryCustomizationDialog; import org.jabref.gui.dbproperties.DatabasePropertiesDialog; import org.jabref.gui.documentviewer.ShowDocumentViewerAction; @@ -389,6 +390,7 @@ public void actionPerformed(ActionEvent e) { Localization.menuTitle("Unabbreviate journal names"), Localization.lang("Unabbreviate journal names of the selected entries"), Globals.getKeyPrefs().getKey(KeyBinding.UNABBREVIATE)); + private final AbstractAction exportLinkedFiles = new CopyFilesAction(); private final AbstractAction manageJournals = new ManageJournalsAction(); private final AbstractAction databaseProperties = new DatabasePropertiesAction(); private final AbstractAction bibtexKeyPattern = new BibtexKeyPatternAction(); @@ -1059,6 +1061,7 @@ private void fillMenu() { file.add(importCurrent); file.add(exportAll); file.add(exportSelected); + file.add(exportLinkedFiles); file.addSeparator(); file.add(connectToSharedDatabaseAction); file.add(pullChangesFromSharedDatabase); @@ -1440,7 +1443,7 @@ dupliCheck, autoSetFile, newEntryAction, newSpec, customizeAction, plainTextImpo twoEntriesOnlyActions.addAll(Arrays.asList(mergeEntries)); atLeastOneEntryActions.clear(); - atLeastOneEntryActions.addAll(Arrays.asList(downloadFullText, lookupIdentifiers)); + atLeastOneEntryActions.addAll(Arrays.asList(downloadFullText, lookupIdentifiers, exportLinkedFiles)); tabbedPane.addChangeListener(event -> updateEnabledState()); diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesAction.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesAction.java new file mode 100644 index 00000000000..877efee2e37 --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesAction.java @@ -0,0 +1,70 @@ +package org.jabref.gui.copyfiles; + +import java.awt.event.ActionEvent; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.Optional; + +import javax.swing.AbstractAction; + +import javafx.concurrent.Task; + +import org.jabref.Globals; +import org.jabref.JabRefExecutorService; +import org.jabref.JabRefGUI; +import org.jabref.gui.DialogService; +import org.jabref.gui.FXDialogService; +import org.jabref.gui.util.DefaultTaskExecutor; +import org.jabref.gui.util.DirectoryDialogConfiguration; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.preferences.JabRefPreferences; + +public class CopyFilesAction extends AbstractAction { + + private final DialogService dialogService = new FXDialogService(); + private BibDatabaseContext databaseContext; + private List entries; + + public CopyFilesAction() { + super(Localization.lang("Copy linked files to folder...")); + } + + @Override + public void actionPerformed(ActionEvent e) { + + DirectoryDialogConfiguration dirDialogConfiguration = new DirectoryDialogConfiguration.Builder() + .withInitialDirectory(Paths.get(Globals.prefs.get(JabRefPreferences.EXPORT_WORKING_DIRECTORY))) + .build(); + entries = JabRefGUI.getMainFrame().getCurrentBasePanel().getSelectedEntries(); + + Optional exportPath = DefaultTaskExecutor + .runInJavaFXThread(() -> dialogService.showDirectorySelectionDialog(dirDialogConfiguration)); + + exportPath.ifPresent(path -> { + databaseContext = JabRefGUI.getMainFrame().getCurrentBasePanel().getDatabaseContext(); + + Task> exportTask = new CopyFilesTask(databaseContext, entries, path); + startServiceAndshowProgessDialog(exportTask); + }); + } + + private void startServiceAndshowProgessDialog(Task> exportService) { + DefaultTaskExecutor.runInJavaFXThread(() -> { + exportService.setOnSucceeded(value -> { + DefaultTaskExecutor.runInJavaFXThread(() -> showDialog(exportService.getValue())); + + }); + JabRefExecutorService.INSTANCE.executeInterruptableTask(exportService); + dialogService.showCanceableProgressDialogAndWait(exportService); + + }); + } + + private void showDialog(List data) { + CopyFilesDialogView dlg = new CopyFilesDialogView(databaseContext, new CopyFilesResultListDependency(data)); + dlg.show(); + } +} \ No newline at end of file diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogController.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogController.java new file mode 100644 index 00000000000..6c3b79bfa1d --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogController.java @@ -0,0 +1,59 @@ +package org.jabref.gui.copyfiles; + +import javax.inject.Inject; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; + +import org.jabref.gui.AbstractController; +import org.jabref.gui.util.ValueTableCellFactory; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; +import de.jensd.fx.glyphs.materialdesignicons.utils.MaterialDesignIconFactory; + +public class CopyFilesDialogController extends AbstractController { + + @FXML private TableView tvResult; + @FXML private TableColumn colStatus; + @FXML private TableColumn colMessage; + @FXML private TableColumn colFile; + + @Inject private CopyFilesResultListDependency copyfilesresultlistDependency; //This var must have the same name as the key in the View + + @FXML + private void close(@SuppressWarnings("unused") ActionEvent event) { + getStage().close(); + } + + @FXML + private void initialize() { + viewModel = new CopyFilesDialogViewModel(copyfilesresultlistDependency); + setupTable(); + } + + private void setupTable() { + colFile.setCellValueFactory(cellData -> cellData.getValue().getFile()); + colMessage.setCellValueFactory(cellData -> cellData.getValue().getMessage()); + colStatus.setCellValueFactory(cellData -> cellData.getValue().getIcon()); + + colFile.setCellFactory(new ValueTableCellFactory().withText(item -> item).withTooltip(item -> item)); + colStatus.setCellFactory(new ValueTableCellFactory().withGraphic(item -> { + + Text icon = MaterialDesignIconFactory.get().createIcon(item); + if (item == MaterialDesignIcon.CHECK) { + icon.setFill(Color.GREEN); + } + if (item == MaterialDesignIcon.ALERT) { + icon.setFill(Color.RED); + } + return icon; + })); + + tvResult.setItems(viewModel.copyFilesResultListProperty()); + tvResult.setColumnResizePolicy((param) -> true); + } +} diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogView.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogView.java new file mode 100644 index 00000000000..7c7c24b5a90 --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogView.java @@ -0,0 +1,36 @@ +package org.jabref.gui.copyfiles; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import javafx.scene.control.Alert.AlertType; +import javafx.scene.control.DialogPane; + +import org.jabref.gui.AbstractDialogView; +import org.jabref.gui.FXDialog; +import org.jabref.logic.l10n.Localization; +import org.jabref.model.database.BibDatabaseContext; + +public class CopyFilesDialogView extends AbstractDialogView { + + public CopyFilesDialogView(BibDatabaseContext bibDatabaseContext, CopyFilesResultListDependency results) { + super(createContext(bibDatabaseContext, results)); + } + + @Override + public void show() { + FXDialog copyFilesResultDlg = new FXDialog(AlertType.INFORMATION, Localization.lang("Result")); + copyFilesResultDlg.setResizable(true); + copyFilesResultDlg.setDialogPane((DialogPane) this.getView()); + copyFilesResultDlg.show(); + } + + private static Function createContext(BibDatabaseContext bibDatabaseContext, CopyFilesResultListDependency copyfilesresultlistDependency) { + Map context = new HashMap<>(); + //The "keys" of the HashMap must have the same name as the with @inject annotated field in the controller + context.put("bibdatabasecontext", bibDatabaseContext); + context.put("copyfilesresultlistDependency", copyfilesresultlistDependency); + return context::get; + } +} diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogViewModel.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogViewModel.java new file mode 100644 index 00000000000..912391ea6b5 --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesDialogViewModel.java @@ -0,0 +1,21 @@ +package org.jabref.gui.copyfiles; + +import javafx.beans.property.SimpleListProperty; +import javafx.collections.FXCollections; + +import org.jabref.gui.AbstractViewModel; + +public class CopyFilesDialogViewModel extends AbstractViewModel { + + private final SimpleListProperty copyFilesResultItems = new SimpleListProperty<>( + FXCollections.observableArrayList()); + + public CopyFilesDialogViewModel(CopyFilesResultListDependency results) { + copyFilesResultItems.addAll(results.getResults()); + } + + public SimpleListProperty copyFilesResultListProperty() { + return this.copyFilesResultItems; + } + +} diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesResultItemViewModel.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesResultItemViewModel.java new file mode 100644 index 00000000000..12fce765c84 --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesResultItemViewModel.java @@ -0,0 +1,42 @@ +package org.jabref.gui.copyfiles; + +import java.nio.file.Path; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + +public class CopyFilesResultItemViewModel { + + private final StringProperty file = new SimpleStringProperty(""); + private final ObjectProperty icon = new SimpleObjectProperty<>(MaterialDesignIcon.ALERT); + private final StringProperty message = new SimpleStringProperty(""); + + public CopyFilesResultItemViewModel(Path file, boolean success, String message) { + this.file.setValue(file.toString()); + this.message.setValue(message); + if (success) { + this.icon.setValue(MaterialDesignIcon.CHECK); + } + } + + public StringProperty getFile() { + return file; + } + + public StringProperty getMessage() { + return message; + } + + public ObjectProperty getIcon() { + return icon; + } + + @Override + public String toString() { + return "CopyFilesResultItemViewModel [file=" + file.get() + ", message=" + message.get() + "]"; + } +} diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesResultListDependency.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesResultListDependency.java new file mode 100644 index 00000000000..10f09fb27f1 --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesResultListDependency.java @@ -0,0 +1,31 @@ +package org.jabref.gui.copyfiles; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is a wrapper class for the containing list as it is currently not possible to inject complex object types into FXML controller + * + */ +public class CopyFilesResultListDependency { + + private List results = new ArrayList<>(); + + public CopyFilesResultListDependency() { + //empty, workaround for injection into FXML controller + } + + public CopyFilesResultListDependency(List results) { + this.results = results; + } + + public List getResults() { + return results; + } + + @Override + public String toString() { + return "CopyFilesResultListDependency [results=" + results + "]"; + } + +} diff --git a/src/main/java/org/jabref/gui/copyfiles/CopyFilesTask.java b/src/main/java/org/jabref/gui/copyfiles/CopyFilesTask.java new file mode 100644 index 00000000000..8142f6b328e --- /dev/null +++ b/src/main/java/org/jabref/gui/copyfiles/CopyFilesTask.java @@ -0,0 +1,122 @@ +package org.jabref.gui.copyfiles; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; + +import javafx.concurrent.Task; + +import org.jabref.Globals; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.OS; +import org.jabref.logic.util.io.FileUtil; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.util.OptionalUtil; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class CopyFilesTask extends Task> { + + private static final Log LOGGER = LogFactory.getLog(CopyFilesAction.class); + private static final String LOGFILE_PREFIX = "copyFileslog_"; + private static final String LOGFILE_EXT = ".log"; + private final BibDatabaseContext databaseContext; + private final Path exportPath; + private final String localizedSucessMessage = Localization.lang("Copied file successfully"); + private final String localizedErrorMessage = Localization.lang("Could not copy file") + ": " + Localization.lang("File exists"); + private final long totalFilesCount; + private final List entries; + private final List results = new ArrayList<>(); + private Optional newPath; + private int numberSucessful; + private int totalFilesCounter; + + private final BiFunction resolvePathFilename = (path, file) -> { + return path.resolve(file.getFileName()); + }; + + public CopyFilesTask(BibDatabaseContext databaseContext, List entries, Path path) { + this.databaseContext = databaseContext; + this.entries = entries; + this.exportPath = path; + totalFilesCount = entries.stream().flatMap(entry -> entry.getFiles().stream()).count(); + + } + + @Override + protected List call() + throws InterruptedException, IOException { + + updateMessage(Localization.lang("Copying files...")); + updateProgress(0, totalFilesCount); + + LocalDateTime currentTime = LocalDateTime.now(); + String currentDate = currentTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")); + + try (BufferedWriter bw = Files.newBufferedWriter(exportPath.resolve(LOGFILE_PREFIX + currentDate + LOGFILE_EXT), StandardCharsets.UTF_8)) { + + for (int i = 0; i < entries.size(); i++) { + + List files = entries.get(i).getFiles(); + + for (int j = 0; j < files.size(); j++) { + updateMessage(Localization.lang("Copying file %0 of entry %1", Integer.toString(j + 1), Integer.toString(i + 1))); + + LinkedFile fileName = files.get(j); + + Optional fileToExport = fileName.findIn(databaseContext, Globals.prefs.getFileDirectoryPreferences()); + + newPath = OptionalUtil.combine(Optional.of(exportPath), fileToExport, resolvePathFilename); + + newPath.ifPresent(newFile -> { + boolean success = FileUtil.copyFile(fileToExport.get(), newFile, false); + updateProgress(totalFilesCounter++, totalFilesCount); + if (success) { + updateMessage(localizedSucessMessage); + numberSucessful++; + writeLogMessage(newFile, bw, localizedSucessMessage); + addResultToList(newFile, success, localizedSucessMessage); + + } else { + + updateMessage(localizedErrorMessage); + writeLogMessage(newFile, bw, localizedErrorMessage); + addResultToList(newFile, success, localizedErrorMessage); + } + }); + } + } + updateMessage(Localization.lang("Finished copying")); + String sucessMessage = Localization.lang("Copied %0 files of %1 sucessfully to %2", Integer.toString(numberSucessful), Integer.toString(totalFilesCounter), newPath.map(Path::getParent).map(Path::toString).orElse("")); + updateMessage(sucessMessage); + bw.write(sucessMessage); + return results; + } + } + + private void writeLogMessage(Path newFile, BufferedWriter bw, String logMessage) { + try { + bw.write(logMessage + ": " + newFile); + bw.write(OS.NEWLINE); + } catch (IOException e) { + LOGGER.error("error writing log file", e); + } + } + + private void addResultToList(Path newFile, boolean success, String logMessage) { + CopyFilesResultItemViewModel result = new CopyFilesResultItemViewModel(newFile, success, logMessage); + results.add(result); + } + +} diff --git a/src/main/java/org/jabref/gui/menus/RightClickMenu.java b/src/main/java/org/jabref/gui/menus/RightClickMenu.java index 845bbef351e..9de2ba35609 100644 --- a/src/main/java/org/jabref/gui/menus/RightClickMenu.java +++ b/src/main/java/org/jabref/gui/menus/RightClickMenu.java @@ -22,6 +22,7 @@ import org.jabref.gui.IconTheme; import org.jabref.gui.JabRefFrame; import org.jabref.gui.actions.Actions; +import org.jabref.gui.copyfiles.CopyFilesAction; import org.jabref.gui.filelist.FileListTableModel; import org.jabref.gui.keyboard.KeyBinding; import org.jabref.gui.mergeentries.FetchAndMergeEntry; @@ -102,6 +103,7 @@ public RightClickMenu(JabRefFrame frame, BasePanel panel) { add(new GeneralAction(Actions.SEND_AS_EMAIL, Localization.lang("Send as email"), IconTheme.JabRefIcon.EMAIL.getSmallIcon())); addSeparator(); + add(new CopyFilesAction()); JMenu markSpecific = JabRefFrame.subMenu(Localization.menuTitle("Mark specific color")); markSpecific.setIcon(IconTheme.JabRefIcon.MARK_ENTRIES.getSmallIcon()); diff --git a/src/main/java/org/jabref/gui/util/ValueTableCellFactory.java b/src/main/java/org/jabref/gui/util/ValueTableCellFactory.java index 53d7bb7a21b..56dff50f25c 100644 --- a/src/main/java/org/jabref/gui/util/ValueTableCellFactory.java +++ b/src/main/java/org/jabref/gui/util/ValueTableCellFactory.java @@ -4,9 +4,12 @@ import javafx.scene.Node; import javafx.scene.control.TableCell; import javafx.scene.control.TableColumn; +import javafx.scene.control.Tooltip; import javafx.scene.input.MouseEvent; import javafx.util.Callback; +import org.jabref.model.strings.StringUtil; + /** * Constructs a {@link TableCell} based on the value of the cell and a bunch of specified converter methods. * @@ -18,6 +21,7 @@ public class ValueTableCellFactory implements Callback, private Callback toText; private Callback toGraphic; private Callback> toOnMouseClickedEvent; + private Callback toTooltip; public ValueTableCellFactory withText(Callback toText) { this.toText = toText; @@ -29,6 +33,11 @@ public ValueTableCellFactory withGraphic(Callback toGraphic) { return this; } + public ValueTableCellFactory withTooltip(Callback toTooltip) { + this.toTooltip = toTooltip; + return this; + } + public ValueTableCellFactory withOnMouseClickedEvent( Callback> toOnMouseClickedEvent) { this.toOnMouseClickedEvent = toOnMouseClickedEvent; @@ -44,10 +53,11 @@ public TableCell call(TableColumn param) { protected void updateItem(T item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) { + if (empty || (item == null)) { setText(null); setGraphic(null); setOnMouseClicked(null); + setTooltip(null); } else { if (toText != null) { setText(toText.call(item)); @@ -55,6 +65,12 @@ protected void updateItem(T item, boolean empty) { if (toGraphic != null) { setGraphic(toGraphic.call(item)); } + if (toTooltip != null) { + String tooltipText = toTooltip.call(item); + if (StringUtil.isNotBlank(tooltipText)) { + setTooltip(new Tooltip(tooltipText)); + } + } if (toOnMouseClickedEvent != null) { setOnMouseClicked(toOnMouseClickedEvent.call(item)); } diff --git a/src/main/java/org/jabref/logic/exporter/ExportFormat.java b/src/main/java/org/jabref/logic/exporter/ExportFormat.java index 871d3509136..031b315d5c6 100644 --- a/src/main/java/org/jabref/logic/exporter/ExportFormat.java +++ b/src/main/java/org/jabref/logic/exporter/ExportFormat.java @@ -313,12 +313,6 @@ public void performExport(final BibDatabaseContext databaseContext, final String } - @Override - public void performExport(final BibDatabaseContext databaseContext, Path file, final Charset encoding, - List entries) throws Exception { - performExport(databaseContext, file.getFileName().toString(), encoding, entries); - } - /** * See if there is a name formatter file bundled with this export format. If so, read * all the name formatters so they can be used by the filter layouts. diff --git a/src/main/java/org/jabref/logic/exporter/IExportFormat.java b/src/main/java/org/jabref/logic/exporter/IExportFormat.java index 0b176a6c9c8..b2f5a9b781a 100644 --- a/src/main/java/org/jabref/logic/exporter/IExportFormat.java +++ b/src/main/java/org/jabref/logic/exporter/IExportFormat.java @@ -1,7 +1,6 @@ package org.jabref.logic.exporter; import java.nio.charset.Charset; -import java.nio.file.Path; import java.util.List; import org.jabref.model.database.BibDatabaseContext; @@ -38,18 +37,4 @@ public interface IExportFormat { void performExport(BibDatabaseContext databaseContext, String file, Charset encoding, List entries) throws Exception; - /** - * Perform the Export. - * Gets the path as a java.nio.path instead of a string. - * - * @param databaseContext the database to export from. - * @param file the Path to the file to write to.The path should be an java.nio.Path - * @param encoding The encoding to use. - * @param entries A list containing all entries that - * should be exported. The list of entries must be non null - * @throws Exception - */ - void performExport(BibDatabaseContext databaseContext, Path file, Charset encoding, List entries) - throws Exception; - } diff --git a/src/main/java/org/jabref/logic/pdf/FileAnnotationCache.java b/src/main/java/org/jabref/logic/pdf/FileAnnotationCache.java index bbfdbb031ba..e4b2796aa8a 100644 --- a/src/main/java/org/jabref/logic/pdf/FileAnnotationCache.java +++ b/src/main/java/org/jabref/logic/pdf/FileAnnotationCache.java @@ -25,7 +25,7 @@ public class FileAnnotationCache { /** * Creates an empty fil annotation cache. Required to allow the annotation cache to be injected into views without - * hitting the bug https://github.com/AdamBien/afterburner.fx/issues/71. + * hitting the bug https://github.com/AdamBien/afterburner.fx/issues/71 . */ public FileAnnotationCache() { diff --git a/src/main/java/org/jabref/logic/util/io/FileUtil.java b/src/main/java/org/jabref/logic/util/io/FileUtil.java index 3386aebdcc5..5a734fd47ff 100644 --- a/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -31,6 +31,7 @@ import org.apache.commons.logging.LogFactory; public class FileUtil { + public static final boolean IS_POSIX_COMPILANT = FileSystems.getDefault().supportedFileAttributeViews().contains("posix"); public static final int MAXIMUM_FILE_NAME_LENGTH = 255; private static final Log LOGGER = LogFactory.getLog(FileUtil.class); @@ -260,7 +261,7 @@ public static List getListOfLinkedFiles(List bes, List fil */ @Deprecated public static String createFileNameFromPattern(BibDatabase database, BibEntry entry, String fileNamePattern, - LayoutFormatterPreferences prefs) { + LayoutFormatterPreferences prefs) { String targetName = null; StringReader sr = new StringReader(fileNamePattern); diff --git a/src/main/resources/l10n/JabRef_da.properties b/src/main/resources/l10n/JabRef_da.properties index 65cd98df5d2..eebe3cf8fa9 100644 --- a/src/main/resources/l10n/JabRef_da.properties +++ b/src/main/resources/l10n/JabRef_da.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_de.properties b/src/main/resources/l10n/JabRef_de.properties index 83dea0b6cfb..24d79f54c42 100644 --- a/src/main/resources/l10n/JabRef_de.properties +++ b/src/main/resources/l10n/JabRef_de.properties @@ -2335,6 +2335,14 @@ Delete_from_disk=Auf_der_Festplatte_löschen Remove_from_entry=Vom_Eintrag_entfernen The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.=Der_Gruppename_enthält_das_Trennzeichen_"%0"_und_wird_deswegen_möglicherweise_nicht_wie_erwartet_funktionieren. There_exists_already_a_group_with_the_same_name.=Es_existiert_bereits_eine_Gruppe_mit_demselben_Namen. + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...=Copying_files... +Copying_file_%0_of_entry_%1= +Finished_copying=Finished_copying +Could_not_copy_file=Could_not_copy_file +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed=Umbenennen_fehlgeschlagen JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.=JabRef_kann_nicht_auf_die_Datei_zugreifen,_da_sie_von_einem_anderen_Prozess_verwendet_wird. Show_console_output_(only_necessary_when_the_launcher_is_used)=Anzeigen_der_Konsolen-Ausgabe_(Nur_notwendig_wenn_der_Launcher_benutzt_wird) diff --git a/src/main/resources/l10n/JabRef_el.properties b/src/main/resources/l10n/JabRef_el.properties index 05f875c196a..d0fa4a8d88f 100644 --- a/src/main/resources/l10n/JabRef_el.properties +++ b/src/main/resources/l10n/JabRef_el.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index 87abfe1ce71..4997a9adcbe 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2335,6 +2335,14 @@ Delete_from_disk=Delete_from_disk Remove_from_entry=Remove_from_entry The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.=The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected. There_exists_already_a_group_with_the_same_name.=There_exists_already_a_group_with_the_same_name. + +Copy_linked_files_to_folder...=Copy_linked_files_to_folder... +Copied_file_successfully=Copied_file_successfully +Copying_files...=Copying_files... +Copying_file_%0_of_entry_%1=Copying_file_%0_of_entry_%1 +Finished_copying=Finished_copying +Could_not_copy_file=Could_not_copy_file +Copied_%0_files_of_%1_sucessfully_to_%2=Copied_%0_files_of_%1_sucessfully_to_%2 Rename_failed=Rename_failed JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.=JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process. Show_console_output_(only_necessary_when_the_launcher_is_used)=Show_console_output_(only_necessary_when_the_launcher_is_used) diff --git a/src/main/resources/l10n/JabRef_es.properties b/src/main/resources/l10n/JabRef_es.properties index 542112d1651..eda93ba0fd0 100644 --- a/src/main/resources/l10n/JabRef_es.properties +++ b/src/main/resources/l10n/JabRef_es.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_fa.properties b/src/main/resources/l10n/JabRef_fa.properties index 24dd3b29d54..a91a881493d 100644 --- a/src/main/resources/l10n/JabRef_fa.properties +++ b/src/main/resources/l10n/JabRef_fa.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_fr.properties b/src/main/resources/l10n/JabRef_fr.properties index d190a6b8fd5..b936f9d475b 100644 --- a/src/main/resources/l10n/JabRef_fr.properties +++ b/src/main/resources/l10n/JabRef_fr.properties @@ -2335,6 +2335,15 @@ Delete_from_disk=Supprimer_du_disque Remove_from_entry=Effacer_de_l'entrée The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.=Le_nom_du_groupe_contient_le_séparateur_de_mot-clef_"%0"_et_ne_fonctionnera_probablement_donc_pas_comme_attendu. There_exists_already_a_group_with_the_same_name.=Un_groupe_portant_ce_nom_existe_déjà. + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2== + Rename_failed=Échec_du_renommage JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.=JabRef_ne_peut_pas_accéder_au_fichier_parce_qu'il_est_utilisé_par_un_autre_processus. Show_console_output_(only_necessary_when_the_launcher_is_used)=Afficher_la_sortie_de_la_console_(uniquement_nécessaire_quand_le_lanceur_est_utilisé) diff --git a/src/main/resources/l10n/JabRef_in.properties b/src/main/resources/l10n/JabRef_in.properties index 9e3c200840a..88bf3094d56 100644 --- a/src/main/resources/l10n/JabRef_in.properties +++ b/src/main/resources/l10n/JabRef_in.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_it.properties b/src/main/resources/l10n/JabRef_it.properties index 737394f837b..b8305b9c41a 100644 --- a/src/main/resources/l10n/JabRef_it.properties +++ b/src/main/resources/l10n/JabRef_it.properties @@ -2335,6 +2335,14 @@ Delete_from_disk=Cancella_dal_disco Remove_from_entry=Rimuovi_dalla_voce The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.=Il_nome_di_gruppo_contiene_il_separatore_di_keyword_"%0"_e_quindi_probabilmente_non_funziona_come_ci_si_aspetta. There_exists_already_a_group_with_the_same_name.=Esiste_già_almeno_un_gruppo_con_lo_stesso_nome. + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_ja.properties b/src/main/resources/l10n/JabRef_ja.properties index 1064e1fcf0a..ccc96237b6a 100644 --- a/src/main/resources/l10n/JabRef_ja.properties +++ b/src/main/resources/l10n/JabRef_ja.properties @@ -2335,6 +2335,14 @@ Delete_from_disk=ディスクから削除 Remove_from_entry=項目から除去 The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.=グループ名にキーワード区切りの「%0」が含まれているので,想定通りにはおそらく動作しません. There_exists_already_a_group_with_the_same_name.=同じ名称のグループがすでに存在します. + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...=Copying_files... +Copying_file_%0_of_entry_%1= +Finished_copying=Finished_copying +Could_not_copy_file=Could_not_copy_file +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_nl.properties b/src/main/resources/l10n/JabRef_nl.properties index b16c2f31a2e..f13cc48f120 100644 --- a/src/main/resources/l10n/JabRef_nl.properties +++ b/src/main/resources/l10n/JabRef_nl.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_no.properties b/src/main/resources/l10n/JabRef_no.properties index 1b8348287ae..388aec1b2ea 100644 --- a/src/main/resources/l10n/JabRef_no.properties +++ b/src/main/resources/l10n/JabRef_no.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_pt_BR.properties b/src/main/resources/l10n/JabRef_pt_BR.properties index afc2cf1cc07..ac90cd1dbfb 100644 --- a/src/main/resources/l10n/JabRef_pt_BR.properties +++ b/src/main/resources/l10n/JabRef_pt_BR.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_ru.properties b/src/main/resources/l10n/JabRef_ru.properties index a26fdb69084..b61ce9b96ce 100644 --- a/src/main/resources/l10n/JabRef_ru.properties +++ b/src/main/resources/l10n/JabRef_ru.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_sv.properties b/src/main/resources/l10n/JabRef_sv.properties index 5c457860186..f27295fce07 100644 --- a/src/main/resources/l10n/JabRef_sv.properties +++ b/src/main/resources/l10n/JabRef_sv.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_tr.properties b/src/main/resources/l10n/JabRef_tr.properties index 985f5187150..097b47950e7 100644 --- a/src/main/resources/l10n/JabRef_tr.properties +++ b/src/main/resources/l10n/JabRef_tr.properties @@ -2335,6 +2335,14 @@ Delete_from_disk=Diskten_sil Remove_from_entry=Girdiden_sil The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.=Grup_adı_anahtar_sözcük_ayracı_olan_"%0"_içeriyor_ve_bu_sebeple_muhtemelen_beklendiğini_gibi_çalışmayacak. There_exists_already_a_group_with_the_same_name.=Aynı_isimli_bir_grup_zaten_var. + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed=Yeniden_adlandırma_başarısız_oldu JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.=JabRef_dosyaya_erişemiyor_çünkü_dosya_başka_bir_süreç_tarafından_kullanılıyor. Show_console_output_(only_necessary_when_the_launcher_is_used)=Consol_çıktısını_göster_(sadece_başlatıcı_kullanıldığında_gereklidir) diff --git a/src/main/resources/l10n/JabRef_vi.properties b/src/main/resources/l10n/JabRef_vi.properties index 79b95120b63..08a71811caa 100644 --- a/src/main/resources/l10n/JabRef_vi.properties +++ b/src/main/resources/l10n/JabRef_vi.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/l10n/JabRef_zh.properties b/src/main/resources/l10n/JabRef_zh.properties index 8d56d18cd9e..c9e83995c9c 100644 --- a/src/main/resources/l10n/JabRef_zh.properties +++ b/src/main/resources/l10n/JabRef_zh.properties @@ -2335,6 +2335,14 @@ Delete_from_disk= Remove_from_entry= The_group_name_contains_the_keyword_separator_"%0"_and_thus_probably_does_not_work_as_expected.= There_exists_already_a_group_with_the_same_name.= + +Copy_linked_files_to_folder...= +Copied_file_successfully= +Copying_files...= +Copying_file_%0_of_entry_%1= +Finished_copying= +Could_not_copy_file= +Copied_%0_files_of_%1_sucessfully_to_%2= Rename_failed= JabRef_cannot_access_the_file_because_it_is_being_used_by_another_process.= Show_console_output_(only_necessary_when_the_launcher_is_used)= diff --git a/src/main/resources/org/jabref/gui/copyfiles/CopyFilesDialog.fxml b/src/main/resources/org/jabref/gui/copyfiles/CopyFilesDialog.fxml new file mode 100644 index 00000000000..88aa1b0a03b --- /dev/null +++ b/src/main/resources/org/jabref/gui/copyfiles/CopyFilesDialog.fxml @@ -0,0 +1,34 @@ + + + + + + + + + + + + +
+ + + + + + + + + + +
+ + + +