Skip to content

Commit

Permalink
New features
Browse files Browse the repository at this point in the history
- Add to Trash
- Group file-related language strings together
  • Loading branch information
koppor committed Jan 11, 2024
1 parent b5ae963 commit 888d191
Show file tree
Hide file tree
Showing 18 changed files with 181 additions and 132 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
### Added

- When importing entries form the "Citation relations" tab, the field [cites](https://docs.jabref.org/advanced/entryeditor/entrylinks) is now filled according to the relationship between the entries. [#10572](https://github.com/JabRef/jabref/pull/10752)
- We added a popup, as well as a preferences setting item, so users can choose whether to delete files linked to selected entries when they delete entries. [#10509](https://github.com/JabRef/jabref/issues/10509)
- When deleting an entry, the files linked to the entry are now optionally deleted as well. [#10509](https://github.com/JabRef/jabref/issues/10509)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/jabref/gui/LibraryTab.java
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ private void delete(StandardActions mode, List<BibEntry> entries) {
.map(linkedFile -> linkedFile.toModel(null, bibDatabaseContext, null, null, preferencesService))
.collect(Collectors.toList());

new DeleteFileAction(dialogService, preferencesService, bibDatabaseContext, viewModels).execute();
new DeleteFileAction(dialogService, preferencesService.getFilePreferences(), bibDatabaseContext, viewModels).execute();
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/main/java/org/jabref/gui/desktop/JabRefDesktop.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jabref.gui.desktop;

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
Expand Down Expand Up @@ -307,4 +309,26 @@ public static void openBrowserShowPopup(String url, DialogService dialogService,
dialogService.showErrorDialogAndWait(couldNotOpenBrowser, couldNotOpenBrowser + "\n" + openManually + "\n" + copiedToClipboard);
}
}

/**
* Moves the given file to the trash.
*
* @throws UnsupportedOperationException if the current platform does not support the {@link Desktop.Action#MOVE_TO_TRASH} action
* @see Desktop#moveToTrash(File)
*/
public static void moveToTrash(Path path) {
NATIVE_DESKTOP.moveToTrash(path);
}

public static boolean moveToTrashSupported() {
return NATIVE_DESKTOP.moveToTrashSupported();
}

public static Path getApplicationDirectory() {
return NATIVE_DESKTOP.getApplicationDirectory();
}

public static Path getFulltextIndexBaseDirectory() {
return NATIVE_DESKTOP.getFulltextIndexBaseDirectory();
}
}
23 changes: 20 additions & 3 deletions src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.jabref.gui.desktop.os;

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;

import org.jabref.architecture.AllowedToUseAwt;
import org.jabref.cli.Launcher;
import org.jabref.gui.DialogService;
import org.jabref.logic.util.BuildInfo;
Expand All @@ -19,15 +21,16 @@
import org.slf4j.LoggerFactory;

/**
* This class is not meant to be used directly. Use {@link org.jabref.gui.desktop.JabRefDesktop} instead.
* <p>
* This class contains bundles OS specific implementations for file directories and file/application open handling methods.
* In case the default does not work, subclasses provide the correct behavior.
*
* <p>
* * <p>
* We cannot use a static logger instance here in this class as the Logger first needs to be configured in the {@link Launcher#addLogToDisk}
* The configuration of tinylog will become immutable as soon as the first log entry is issued.
* https://tinylog.org/v2/configuration/
* </p>
*/
@AllowedToUseAwt("Because of moveToTrash() is not available elsewhere.")
public abstract class NativeDesktop {

public abstract void openFile(String filePath, String fileType, FilePreferences filePreferences) throws IOException;
Expand Down Expand Up @@ -125,4 +128,18 @@ public String getHostName() {
}
return hostName;
}

/**
* Moves the given file to the trash.
*
* @throws UnsupportedOperationException if the current platform does not support the {@link Desktop.Action#MOVE_TO_TRASH} action
* @see Desktop#moveToTrash(File)
*/
public void moveToTrash(Path path) {
Desktop.getDesktop().moveToTrash(path.toFile());
}

public boolean moveToTrashSupported() {
return Desktop.getDesktop().isSupported(Desktop.Action.MOVE_TO_TRASH);
}
}
40 changes: 5 additions & 35 deletions src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Node;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;

import org.jabref.gui.AbstractViewModel;
import org.jabref.gui.DialogService;
Expand All @@ -38,6 +35,7 @@
import org.jabref.gui.externalfiletype.StandardExternalFileType;
import org.jabref.gui.icon.IconTheme;
import org.jabref.gui.icon.JabRefIcon;
import org.jabref.gui.linkedfile.DeleteFileAction;
import org.jabref.gui.linkedfile.LinkedFileEditDialogView;
import org.jabref.gui.mergeentries.MultiMergeEntriesView;
import org.jabref.gui.util.BackgroundTask;
Expand Down Expand Up @@ -98,7 +96,6 @@ public LinkedFileViewModel(LinkedFile linkedFile,
TaskExecutor taskExecutor,
DialogService dialogService,
PreferencesService preferencesService) {

this.linkedFile = linkedFile;
this.preferencesService = preferencesService;
this.linkedFileHandler = new LinkedFileHandler(linkedFile, entry, databaseContext, preferencesService.getFilePreferences());
Expand Down Expand Up @@ -379,40 +376,13 @@ public void moveToDefaultDirectoryAndRename() {
/**
* Asks the user for confirmation that he really wants to the delete the file from disk (or just remove the link).
*
* @return true if the linked file should be removed afterwards from the entry (i.e because it was deleted
* @return true if the linked file should be removed afterwards from the entry (i.e., because it was deleted
* successfully, does not exist in the first place or the user choose to remove it)
*/
public boolean delete() {
Optional<Path> file = linkedFile.findIn(databaseContext, preferencesService.getFilePreferences());

if (file.isEmpty()) {
LOGGER.warn("Could not find file {}", linkedFile.getLink());
return true;
}

ButtonType removeFromEntry = new ButtonType(Localization.lang("Remove from entry"), ButtonData.YES);
ButtonType deleteFromEntry = new ButtonType(Localization.lang("Delete from disk"));
Optional<ButtonType> buttonType = dialogService.showCustomButtonDialogAndWait(AlertType.INFORMATION,
Localization.lang("Delete '%0'", file.get().getFileName().toString()),
Localization.lang("Delete '%0' permanently from disk, or just remove the file from the entry? Pressing Delete will delete the file permanently from disk.", file.get().toString()),
removeFromEntry, deleteFromEntry, ButtonType.CANCEL);

if (buttonType.isPresent()) {
if (buttonType.get().equals(removeFromEntry)) {
return true;
}

if (buttonType.get().equals(deleteFromEntry)) {
try {
Files.delete(file.get());
return true;
} catch (IOException ex) {
dialogService.showErrorDialogAndWait(Localization.lang("Cannot delete file"), Localization.lang("File permission error"));
LOGGER.warn("File permission error while deleting: {}", linkedFile, ex);
}
}
}
return false;
DeleteFileAction deleteFileAction = new DeleteFileAction(dialogService, preferencesService.getFilePreferences(), databaseContext, null, List.of(this));
deleteFileAction.execute();
return deleteFileAction.isSuccess();
}

public void edit() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,7 @@ private void setUpKeyBindings() {
if (keyBinding.isPresent()) {
switch (keyBinding.get()) {
case DELETE_ENTRY:
// Delete multiple selected files in currently opened entry
// als
deleteEntry();
deleteAttachedFilesWithConfirmation();
event.consume();
break;
default:
Expand All @@ -263,8 +261,8 @@ private void setUpKeyBindings() {
});
}

private void deleteEntry() {
new DeleteFileAction(dialogService, preferencesService, databaseContext,
private void deleteAttachedFilesWithConfirmation() {
new DeleteFileAction(dialogService, preferencesService.getFilePreferences(), databaseContext,
viewModel, listView.getSelectionModel().getSelectedItems()).execute();
}

Expand Down
114 changes: 61 additions & 53 deletions src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,73 +12,81 @@

import org.jabref.gui.DialogService;
import org.jabref.gui.actions.SimpleCommand;
import org.jabref.gui.desktop.JabRefDesktop;
import org.jabref.gui.fieldeditors.LinkedFileViewModel;
import org.jabref.gui.fieldeditors.LinkedFilesEditorViewModel;
import org.jabref.logic.l10n.Localization;
import org.jabref.model.database.BibDatabaseContext;
import org.jabref.model.entry.LinkedFile;
import org.jabref.preferences.PreferencesService;
import org.jabref.preferences.FilePreferences;

import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@NullMarked
public class DeleteFileAction extends SimpleCommand {

private static final Logger LOGGER = LoggerFactory.getLogger(DeleteFileAction.class);

private final DialogService dialogService;
private final PreferencesService preferences;
private final FilePreferences filePreferences;
private final BibDatabaseContext databaseContext;
private final LinkedFilesEditorViewModel viewModel;
private final List<LinkedFileViewModel> linkedFiles;
private final @Nullable LinkedFilesEditorViewModel viewModel;
private final List<LinkedFileViewModel> filesToDelete;
private boolean success = false;

public DeleteFileAction(DialogService dialogService,
PreferencesService preferences,
FilePreferences filePreferences,
BibDatabaseContext databaseContext,
LinkedFilesEditorViewModel viewModel,
List<LinkedFileViewModel> linkedFiles) {
@Nullable LinkedFilesEditorViewModel viewModel,
List<LinkedFileViewModel> filesToDelete) {
this.dialogService = dialogService;
this.preferences = preferences;
this.filePreferences = filePreferences;
this.databaseContext = databaseContext;
this.viewModel = viewModel;
this.linkedFiles = List.copyOf(linkedFiles);
this.filesToDelete = List.copyOf(filesToDelete);
}

public DeleteFileAction(DialogService dialogService,
PreferencesService preferences,
FilePreferences filePreferences,
BibDatabaseContext databaseContext,
List<LinkedFileViewModel> linkedFiles) {
this(dialogService, preferences, databaseContext, null, linkedFiles);
List<LinkedFileViewModel> filesToDelete) {
this(dialogService, filePreferences, databaseContext, null, filesToDelete);
}

@Override
public void execute() {
if (linkedFiles.isEmpty()) {
if (filesToDelete.isEmpty()) {
dialogService.notify(Localization.lang("This operation requires selected linked files."));
return;
}

if (!preferences.getFilePreferences().confirmDeleteLinkedFile()) {
deleteFiles(linkedFiles, true);
if (!filePreferences.confirmDeleteLinkedFile()) {
LOGGER.info("Deleting {} files without confirmation.", filesToDelete.size());
deleteFiles(true);
return;
}

String dialogTitle;
String dialogContent;

if (linkedFiles.size() != 1) {
dialogTitle = Localization.lang("Delete %0 files", linkedFiles.size());
int numberOfLinkedFiles = filesToDelete.size();
if (numberOfLinkedFiles != 1) {
dialogTitle = Localization.lang("Delete %0 files", numberOfLinkedFiles);
dialogContent = Localization.lang("Delete %0 files permanently from disk, or just remove the files from the entry? " +
"Pressing Delete will delete the files permanently from disk.", linkedFiles.size());
"Pressing Delete will delete the files permanently from disk.", numberOfLinkedFiles);
} else {
Optional<Path> file = linkedFiles.get(0).getFile().findIn(databaseContext, preferences.getFilePreferences());

LinkedFile linkedFile = filesToDelete.getFirst().getFile();
Optional<Path> file = linkedFile.findIn(databaseContext, filePreferences);
if (file.isPresent()) {
dialogTitle = Localization.lang("Delete '%0'", file.get().getFileName().toString());
Path path = file.get();
dialogTitle = Localization.lang("Delete '%0'", path.getFileName().toString());
dialogContent = Localization.lang("Delete '%0' permanently from disk, or just remove the file from the entry? " +
"Pressing Delete will delete the file permanently from disk.", file.get().toString());
"Pressing Delete will delete the file permanently from disk.", path.toString());
} else {
dialogService.notify(Localization.lang("Error accessing file '%0'.", linkedFiles.get(0).getFile().getLink()));
dialogService.notify(Localization.lang("Error accessing file '%0'.", linkedFile.getLink()));
return;
}
}
Expand All @@ -90,35 +98,27 @@ public void execute() {
dialogTitle, dialogContent, removeFromEntry, deleteFromEntry, ButtonType.CANCEL);

if (buttonType.isPresent()) {
if (buttonType.get().equals(removeFromEntry)) {
deleteFiles(linkedFiles, false);
}

if (buttonType.get().equals(deleteFromEntry)) {
deleteFiles(linkedFiles, true);
ButtonType theButtonType = buttonType.get();
if (theButtonType.equals(removeFromEntry)) {
deleteFiles(false);
} else if (theButtonType.equals(deleteFromEntry)) {
deleteFiles(true);
}
}
}

/**
* Deletes the files from the entry and optionally from disk.
*
* @param toBeDeleted the files to be deleted
* @param deleteFromDisk if true, the files are deleted from disk, otherwise they are only removed from the entry
*/
private void deleteFiles(List<LinkedFileViewModel> toBeDeleted, boolean deleteFromDisk) {
for (LinkedFileViewModel fileViewModel : toBeDeleted) {
if (fileViewModel.getFile().isOnlineLink()) {
if (viewModel != null) {
viewModel.removeFileLink(fileViewModel);
}
} else {
if (deleteFromDisk) {
deleteFileHelper(databaseContext, fileViewModel.getFile());
}
if (viewModel != null) {
viewModel.getFiles().remove(fileViewModel);
}
private void deleteFiles(boolean deleteFromDisk) {
for (LinkedFileViewModel fileViewModel : filesToDelete) {
if (!fileViewModel.getFile().isOnlineLink() && deleteFromDisk) {
deleteFileHelper(databaseContext, fileViewModel.getFile());
}
if (viewModel != null) {
viewModel.removeFileLink(fileViewModel);
}
}
}
Expand All @@ -129,22 +129,30 @@ private void deleteFiles(List<LinkedFileViewModel> toBeDeleted, boolean deleteFr
* @param linkedFile The LinkedFile (file which linked to an entry) to be deleted from disk
*/
private void deleteFileHelper(BibDatabaseContext databaseContext, LinkedFile linkedFile) {
Optional<Path> file = linkedFile.findIn(databaseContext, preferences.getFilePreferences());
Optional<Path> file = linkedFile.findIn(databaseContext, filePreferences);

if (file.isEmpty()) {
LOGGER.warn("Could not find file {}", linkedFile.getLink());
dialogService.notify(Localization.lang("Error accessing file '%0'.", linkedFile.getLink()));
return;
}

if (file.isPresent()) {
try {
Files.delete(file.get());
} catch (
IOException ex) {
dialogService.showErrorDialogAndWait(Localization.lang("Cannot delete file"), Localization.lang("File permission error"));
LOGGER.warn("File permission error while deleting: {}", linkedFile, ex);
Path theFile = file.get();
try {
if (filePreferences.moveToTrash()) {
JabRefDesktop.moveToTrash(theFile);
} else {
Files.delete(theFile);
}
} else {
dialogService.notify(Localization.lang("Error accessing file '%0'.", linkedFile.getLink()));
success = true;
} catch (IOException ex) {
success = false;
dialogService.showErrorDialogAndWait(Localization.lang("Cannot delete file '%0'", theFile), Localization.lang("File permission error"));
LOGGER.warn("Error while deleting: {}", linkedFile, ex);
}
}

public boolean isSuccess() {
return success;
}
}
Loading

0 comments on commit 888d191

Please sign in to comment.