From 8227613e31433ab375cde91ce3b17a5acc716d06 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Sun, 25 Feb 2024 19:52:42 +0100 Subject: [PATCH 01/13] issue #10661 - feat: added option to export in cff --- .../logic/exporter/ExporterFactory.java | 1 + .../org/jabref/logic/layout/LayoutEntry.java | 2 + .../jabref/logic/layout/format/CffType.java | 24 +++ src/main/resources/resource/layout/cff.layout | 14 ++ .../logic/exporter/CffExporterTest.java | 159 ++++++++++++++++++ 5 files changed, 200 insertions(+) create mode 100644 src/main/java/org/jabref/logic/layout/format/CffType.java create mode 100644 src/main/resources/resource/layout/cff.layout create mode 100644 src/test/java/org/jabref/logic/exporter/CffExporterTest.java diff --git a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java index b38bb57dc95..31ec6edb281 100644 --- a/src/main/java/org/jabref/logic/exporter/ExporterFactory.java +++ b/src/main/java/org/jabref/logic/exporter/ExporterFactory.java @@ -55,6 +55,7 @@ public static ExporterFactory create(PreferencesService preferencesService, exporters.add(new TemplateExporter("MIS Quarterly", "misq", "misq", "misq", StandardFileType.RTF, layoutPreferences, saveOrder)); exporters.add(new TemplateExporter("CSL YAML", "yaml", "yaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); exporters.add(new TemplateExporter("Hayagriva YAML", "hayagrivayaml", "hayagrivayaml", null, StandardFileType.YAML, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); + exporters.add(new TemplateExporter("CFF", "cff", "cff", null, StandardFileType.CFF, layoutPreferences, saveOrder, BlankLineBehaviour.DELETE_BLANKS)); exporters.add(new OpenOfficeDocumentCreator()); exporters.add(new OpenDocumentSpreadsheetCreator()); exporters.add(new MSBibExporter()); diff --git a/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/src/main/java/org/jabref/logic/layout/LayoutEntry.java index 7d0cf3d3a4d..1101e437557 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -35,6 +35,7 @@ import org.jabref.logic.layout.format.AuthorOrgSci; import org.jabref.logic.layout.format.Authors; import org.jabref.logic.layout.format.CSLType; +import org.jabref.logic.layout.format.CffType; import org.jabref.logic.layout.format.CompositeFormat; import org.jabref.logic.layout.format.CreateBibORDFAuthors; import org.jabref.logic.layout.format.CreateDocBook4Authors; @@ -486,6 +487,7 @@ private LayoutFormatter getLayoutFormatterByName(String name) { case "ShortMonth" -> new ShortMonthFormatter(); case "ReplaceWithEscapedDoubleQuotes" -> new ReplaceWithEscapedDoubleQuotes(); case "HayagrivaType" -> new HayagrivaType(); + case "CffType" -> new CffType(); default -> null; }; } diff --git a/src/main/java/org/jabref/logic/layout/format/CffType.java b/src/main/java/org/jabref/logic/layout/format/CffType.java new file mode 100644 index 00000000000..5de168b77ba --- /dev/null +++ b/src/main/java/org/jabref/logic/layout/format/CffType.java @@ -0,0 +1,24 @@ +package org.jabref.logic.layout.format; + +import org.jabref.logic.layout.LayoutFormatter; +import org.jabref.model.entry.types.StandardEntryType; + +public class CffType implements LayoutFormatter { + @Override + public String format(String value) { + return switch (StandardEntryType.valueOf(value)) { + case Article, Conference -> "article"; + case Book -> "book"; + case Booklet -> "pamphlet"; + case InProceedings -> "conference-paper"; + case Proceedings -> "proceedings"; + case Misc -> "misc"; + case Manual -> "manual"; + case Software -> "software"; + case Report, TechReport -> "report"; + case Unpublished -> "unpublished"; + default -> "generic"; + }; + } +} + diff --git a/src/main/resources/resource/layout/cff.layout b/src/main/resources/resource/layout/cff.layout new file mode 100644 index 00000000000..5739a112194 --- /dev/null +++ b/src/main/resources/resource/layout/cff.layout @@ -0,0 +1,14 @@ +cff-version: 1.2.0 +message: "If you use this, please cite the work from preferred-citation." +authors: + - name: \format[Default(No author specified.)]{\author} +title: \format[Default(No title specified.)]{\title} +preferred-citation: + type: \format[CffType, Default(generic)]{\entrytype} + authors: + - name: \format[Default(No author specified.)]{\author} + title: \format[Default(No title specified.)]{\title} +\begin{abstract} abstract: \abstract\end{abstract} +\begin{doi} doi: \doi\end{doi} +\begin{volume} volume: \volume\end{volume} +\begin{url} url: "\url"\end{url} \ No newline at end of file diff --git a/src/test/java/org/jabref/logic/exporter/CffExporterTest.java b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java new file mode 100644 index 00000000000..f9314e11b56 --- /dev/null +++ b/src/test/java/org/jabref/logic/exporter/CffExporterTest.java @@ -0,0 +1,159 @@ +package org.jabref.logic.exporter; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +import org.jabref.logic.layout.LayoutFormatterPreferences; +import org.jabref.logic.util.StandardFileType; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; +import org.jabref.model.entry.types.StandardEntryType; +import org.jabref.model.metadata.SaveOrder; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Answers; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +public class CffExporterTest { + + private static Exporter cffExporter; + private static BibDatabaseContext databaseContext; + + @BeforeAll + static void setUp() { + cffExporter = new TemplateExporter( + "CFF", + "cff", + "cff", + null, + StandardFileType.CFF, + mock(LayoutFormatterPreferences.class, Answers.RETURNS_DEEP_STUBS), + SaveOrder.getDefaultSaveOrder(), + BlankLineBehaviour.DELETE_BLANKS); + + databaseContext = new BibDatabaseContext(); + } + + @Test + public final void exportForNoEntriesWritesNothing(@TempDir Path tempFile) throws Exception { + Path file = tempFile.resolve("ThisIsARandomlyNamedFile"); + Files.createFile(file); + cffExporter.export(databaseContext, tempFile, Collections.emptyList()); + assertEquals(Collections.emptyList(), Files.readAllLines(file)); + } + + @Test + public final void exportsCorrectContent(@TempDir Path tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.URL, "http://example.com"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: \"If you use this, please cite the work from preferred-citation.\"", + "authors:", + " - name: Test Author", + "title: Test Title", + "preferred-citation:", + " type: article", + " authors:", + " - name: Test Author", + " title: Test Title", + " url: \"http://example.com\""); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void usesCorrectType(@TempDir Path tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.InProceedings) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "Test Author") + .withField(StandardField.TITLE, "Test Title") + .withField(StandardField.DOI, "random_doi_value"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: \"If you use this, please cite the work from preferred-citation.\"", + "authors:", + " - name: Test Author", + "title: Test Title", + "preferred-citation:", + " type: conference-paper", + " authors:", + " - name: Test Author", + " title: Test Title", + " doi: random_doi_value"); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + public final void usesCorrectDefaultValues(@TempDir Path tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Thesis) + .withCitationKey("test"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: \"If you use this, please cite the work from preferred-citation.\"", + "authors:", + " - name: No author specified.", + "title: No title specified.", + "preferred-citation:", + " type: generic", + " authors:", + " - name: No author specified.", + " title: No title specified."); + + assertEquals(expected, Files.readAllLines(file)); + } + + @Test + void passesModifiedCharset(@TempDir Path tempFile) throws Exception { + BibEntry entry = new BibEntry(StandardEntryType.Article) + .withCitationKey("test") + .withField(StandardField.AUTHOR, "谷崎 潤一郎") + .withField(StandardField.TITLE, "細雪") + .withField(StandardField.URL, "http://example.com"); + + Path file = tempFile.resolve("RandomFileName"); + Files.createFile(file); + cffExporter.export(databaseContext, file, Collections.singletonList(entry)); + + List expected = List.of( + "cff-version: 1.2.0", + "message: \"If you use this, please cite the work from preferred-citation.\"", + "authors:", + " - name: 谷崎 潤一郎", + "title: 細雪", + "preferred-citation:", + " type: article", + " authors:", + " - name: 谷崎 潤一郎", + " title: 細雪", + " url: \"http://example.com\""); + + assertEquals(expected, Files.readAllLines(file)); + } +} From 212171861c1ba7cdef8d1754f48dc9f7dec340e4 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Sun, 25 Feb 2024 20:11:54 +0100 Subject: [PATCH 02/13] issue #10661 - feat: updated CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9db672a5f8a..c9f55df3044 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - When pasting HTML into the abstract or a comment field, the hypertext is automatically converted to Markdown. [#10558](https://github.com/JabRef/jabref/issues/10558) - We added the possibility to redownload files that had been present but are no longer in the specified location. [#10848](https://github.com/JabRef/jabref/issues/10848) - We added the citation key pattern `[camelN]`. Equivalent to the first N words of the `[camel]` pattern. +- We added ability to export in CFF (Citation File Format) [#10661](https://github.com/JabRef/jabref/issues/10661). ### Changed From 3ead720fb4b55b821af8aeb942a3eed231d378a5 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 26 Feb 2024 00:21:28 +0100 Subject: [PATCH 03/13] issue #10661 - feat: added date field to cff export --- .../org/jabref/logic/layout/LayoutEntry.java | 2 + .../jabref/logic/layout/format/CffDate.java | 62 +++++++++++++++++++ src/main/resources/resource/layout/cff.layout | 3 + .../logic/layout/format/CffDateTest.java | 44 +++++++++++++ 4 files changed, 111 insertions(+) create mode 100644 src/main/java/org/jabref/logic/layout/format/CffDate.java create mode 100644 src/test/java/org/jabref/logic/layout/format/CffDateTest.java diff --git a/src/main/java/org/jabref/logic/layout/LayoutEntry.java b/src/main/java/org/jabref/logic/layout/LayoutEntry.java index 1101e437557..211d9b02172 100644 --- a/src/main/java/org/jabref/logic/layout/LayoutEntry.java +++ b/src/main/java/org/jabref/logic/layout/LayoutEntry.java @@ -35,6 +35,7 @@ import org.jabref.logic.layout.format.AuthorOrgSci; import org.jabref.logic.layout.format.Authors; import org.jabref.logic.layout.format.CSLType; +import org.jabref.logic.layout.format.CffDate; import org.jabref.logic.layout.format.CffType; import org.jabref.logic.layout.format.CompositeFormat; import org.jabref.logic.layout.format.CreateBibORDFAuthors; @@ -488,6 +489,7 @@ private LayoutFormatter getLayoutFormatterByName(String name) { case "ReplaceWithEscapedDoubleQuotes" -> new ReplaceWithEscapedDoubleQuotes(); case "HayagrivaType" -> new HayagrivaType(); case "CffType" -> new CffType(); + case "CffDate" -> new CffDate(); default -> null; }; } diff --git a/src/main/java/org/jabref/logic/layout/format/CffDate.java b/src/main/java/org/jabref/logic/layout/format/CffDate.java new file mode 100644 index 00000000000..865b7376f2e --- /dev/null +++ b/src/main/java/org/jabref/logic/layout/format/CffDate.java @@ -0,0 +1,62 @@ +package org.jabref.logic.layout.format; + +import java.time.LocalDate; +import java.time.Year; +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import org.jabref.logic.layout.LayoutFormatter; + +public class CffDate implements LayoutFormatter { + + /* + This class is used to parse dates for CFF exports. Since we do not know if the input String contains + year, month and day, we must go through all these cases to return the best CFF format possible. + Different cases are stated below. + + Year, Month and Day contained => date-released: yyyy-mm-dd + Year and Month contained => month: mm + year: yyyy + Year contained => year: yyyy + Poorly formatted => issue-date: + */ + + @Override + public String format(String fieldText) { + StringBuilder builder = new StringBuilder(); + String formatString = "yyyy-MM-dd"; + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + LocalDate date = LocalDate.parse(fieldText, DateTimeFormatter.ISO_LOCAL_DATE); + builder.append("date-released: "); + builder.append(date.format(formatter)); + } catch (DateTimeParseException e) { + try { + formatString = "yyyy-MM"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + YearMonth yearMonth = YearMonth.parse(fieldText, formatter); + int month = yearMonth.getMonth().getValue(); + int year = yearMonth.getYear(); + builder.append("month: "); + builder.append(month); + builder.append(System.lineSeparator()); + builder.append(" "); + builder.append("year: "); + builder.append(year); + } catch (DateTimeParseException f) { + try { + formatString = "yyyy"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(formatString); + int year = Year.parse(fieldText, formatter).getValue(); + builder.append("year: "); + builder.append(year); + } catch (DateTimeParseException g){ + builder.append("issue-date: "); + builder.append(fieldText); + } + } + } + return builder.toString(); + } +} diff --git a/src/main/resources/resource/layout/cff.layout b/src/main/resources/resource/layout/cff.layout index 5739a112194..e78b70e29f1 100644 --- a/src/main/resources/resource/layout/cff.layout +++ b/src/main/resources/resource/layout/cff.layout @@ -8,6 +8,9 @@ preferred-citation: authors: - name: \format[Default(No author specified.)]{\author} title: \format[Default(No title specified.)]{\title} +\begin{date} + \format[CffDate]{\date} +\end{date} \begin{abstract} abstract: \abstract\end{abstract} \begin{doi} doi: \doi\end{doi} \begin{volume} volume: \volume\end{volume} diff --git a/src/test/java/org/jabref/logic/layout/format/CffDateTest.java b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java new file mode 100644 index 00000000000..a4758c34c49 --- /dev/null +++ b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java @@ -0,0 +1,44 @@ +package org.jabref.logic.layout.format; + +import org.jabref.logic.layout.LayoutFormatter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CffDateTest { + + private LayoutFormatter formatter; + private String newLine; + + @BeforeEach + public void setUp() { + formatter = new CffDate(); + newLine = System.lineSeparator(); + } + + @Test + public void dayMonthYear(){ + String expected = "date-released: 2003-11-06"; + assertEquals(expected, formatter.format("2003-11-06")); + } + + @Test + public void monthYear(){ + String expected = "month: 7" + newLine + " " + "year: 2016"; + assertEquals(expected, formatter.format("2016-07")); + } + + @Test + public void year(){ + String expected = "year: 2021"; + assertEquals(expected, formatter.format("2021")); + } + + @Test + public void poorlyFormatted(){ + String expected = "issue-date: -2023"; + assertEquals(expected, formatter.format("-2023")); + } +} From 8216a62d4616ebdbfa916c12953faf565664c290 Mon Sep 17 00:00:00 2001 From: Jean Perbet Date: Mon, 26 Feb 2024 00:30:54 +0100 Subject: [PATCH 04/13] issue #10661 - fix: fixed checkstyle errors --- src/main/java/org/jabref/logic/layout/format/CffDate.java | 2 +- .../java/org/jabref/logic/layout/format/CffDateTest.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/jabref/logic/layout/format/CffDate.java b/src/main/java/org/jabref/logic/layout/format/CffDate.java index 865b7376f2e..f1d010f1eac 100644 --- a/src/main/java/org/jabref/logic/layout/format/CffDate.java +++ b/src/main/java/org/jabref/logic/layout/format/CffDate.java @@ -51,7 +51,7 @@ public String format(String fieldText) { int year = Year.parse(fieldText, formatter).getValue(); builder.append("year: "); builder.append(year); - } catch (DateTimeParseException g){ + } catch (DateTimeParseException g) { builder.append("issue-date: "); builder.append(fieldText); } diff --git a/src/test/java/org/jabref/logic/layout/format/CffDateTest.java b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java index a4758c34c49..c474deb571f 100644 --- a/src/test/java/org/jabref/logic/layout/format/CffDateTest.java +++ b/src/test/java/org/jabref/logic/layout/format/CffDateTest.java @@ -19,25 +19,25 @@ public void setUp() { } @Test - public void dayMonthYear(){ + public void dayMonthYear() { String expected = "date-released: 2003-11-06"; assertEquals(expected, formatter.format("2003-11-06")); } @Test - public void monthYear(){ + public void monthYear() { String expected = "month: 7" + newLine + " " + "year: 2016"; assertEquals(expected, formatter.format("2016-07")); } @Test - public void year(){ + public void year() { String expected = "year: 2021"; assertEquals(expected, formatter.format("2021")); } @Test - public void poorlyFormatted(){ + public void poorlyFormatted() { String expected = "issue-date: -2023"; assertEquals(expected, formatter.format("-2023")); } From df182cea25af41043649cd14c9ddde671ad1828b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 18:32:59 +0000 Subject: [PATCH 05/13] Bump org.apache.lucene:lucene-queries from 9.9.1 to 9.10.0 (#10920) * Bump org.apache.lucene:lucene-queries from 9.9.1 to 9.10.0 Bumps org.apache.lucene:lucene-queries from 9.9.1 to 9.10.0. --- updated-dependencies: - dependency-name: org.apache.lucene:lucene-queries dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * introduce var for lucene --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Siedlerchr --- build.gradle | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index b7c10d8cfa7..d17ee536c11 100644 --- a/build.gradle +++ b/build.gradle @@ -119,11 +119,12 @@ dependencies { exclude group: 'org.junit.jupiter' } - implementation 'org.apache.lucene:lucene-core:9.9.2' - implementation 'org.apache.lucene:lucene-queryparser:9.9.2' - implementation 'org.apache.lucene:lucene-queries:9.9.1' - implementation 'org.apache.lucene:lucene-analysis-common:9.9.2' - implementation 'org.apache.lucene:lucene-highlighter:9.9.2' + def luceneVersion = "9.10.0" + implementation "org.apache.lucene:lucene-core:$luceneVersion" + implementation "org.apache.lucene:lucene-queryparser:$luceneVersion" + implementation "org.apache.lucene:lucene-queries:$luceneVersion" + implementation "org.apache.lucene:lucene-analysis-common:$luceneVersion" + implementation "org.apache.lucene:lucene-highlighter:$luceneVersion" implementation group: 'org.apache.commons', name: 'commons-csv', version: '1.10.0' implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.14.0' From e147f8aaba64a3a007030915f613298a86eadc8d Mon Sep 17 00:00:00 2001 From: shawn-jj <134609685+shawn-jj@users.noreply.github.com> Date: Tue, 27 Feb 2024 03:10:43 +0800 Subject: [PATCH 06/13] Fix for delete entries should ask user (#10591) * Implemented the feature that deleting files which linked to selected entries when user select deletion, and keeping files unchanged when user select cut * The following features are implemented: 1.Initializes a pop-up dialog box to confirm whether the user wants to delete attached files from selected entry. 2.Keep track of user preference: if the user prefers always delete attached files, delete the files without displaying the dialog box. 3. Add preference options in File>Preference>Linked Files>Attached files so that users can manage preferences * update CHANGELOG.md * Add language keys to english language file * restore files in src/main/resources/csl-locales and src/main/resources/csl-styles * Removed unnecessary comments and finxed some requested changes. Added new features: 1. When deleting attached files, the name of files to be deleted will be displayed. 2. Solved the access error caused by repeated deletion of files when one file is attached to multiple entries. * Add language keys to english language file * Modify language keys to english language file * made deleteFileFromDisk method static * update comment of method deleteFileFromDisk * fixed coding styles * restored unexpected code changes * fix logic * try null * todo * Unify dialogs that confirmation deleting files * Get around LinkedFile in LIbraryTab, Encapsulate LinkedFile into LinkedFileViewModel * fix style * restore files * Unified the different dialogs when deleting entries, removerd unnecessary dialogs * fix csl-styles * try to fix csl-styles * try to fix csl-styles again * try to fix csl-styles again 2 * try to fix csl-styles again 3 * Update prompts in en.properties * New features - Add to Trash - Group file-related language strings together * Fix architecture tests * Introduce list of files to delete * Streamline 1 vs. many files * Fix openRewrite * Discard changes to src/test/resources/org/jabref/logic/search/test-library-with-attached-files.bib * Adapt true/false logic according to expectations * Add "Trash" to CHANGELOG.md * Fix localization * Fix JabRef_en.properties * Add some debug statements * Fix preferences * Separate log entries by empty line * More refined dialog --------- Co-authored-by: Siedlerchr Co-authored-by: Oliver Kopp Co-authored-by: Carl Christian Snethlage <50491877+calixtus@users.noreply.github.com> --- CHANGELOG.md | 2 + src/main/java/org/jabref/gui/LibraryTab.java | 41 +++- .../org/jabref/gui/desktop/JabRefDesktop.java | 27 ++- .../jabref/gui/desktop/os/NativeDesktop.java | 23 ++- .../gui/fieldeditors/LinkedFileViewModel.java | 45 +---- .../gui/fieldeditors/LinkedFilesEditor.java | 8 +- .../gui/linkedfile/DeleteFileAction.java | 188 ++++++++++++------ .../DetectOpenOfficeInstallation.java | 6 +- .../EditExternalFileTypeEntryDialog.java | 6 +- .../linkedfiles/LinkedFilesTab.fxml | 4 + .../linkedfiles/LinkedFilesTab.java | 7 + .../linkedfiles/LinkedFilesTabViewModel.java | 14 ++ .../model/database/BibDatabaseContext.java | 4 +- .../org/jabref/model/entry/LinkedFile.java | 18 ++ .../jabref/preferences/FilePreferences.java | 32 ++- .../jabref/preferences/JabRefPreferences.java | 13 +- src/main/resources/l10n/JabRef_en.properties | 41 ++-- src/main/resources/tinylog.properties | 2 + .../architecture/MainArchitectureTest.java | 15 ++ .../fieldeditors/LinkedFileViewModelTest.java | 41 ++-- 20 files changed, 377 insertions(+), 160 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9076c7071..d36448418f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added a fetcher for [ISIDORE](https://isidore.science/), simply paste in the link into the text field or the last 6 digits in the link that identify that paper. [#10423](https://github.com/JabRef/jabref/issues/10423) - 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 new group icon column to the main table showing the icons of the entry's groups. [#10801](https://github.com/JabRef/jabref/pull/10801) +- When deleting an entry, the files linked to the entry are now optionally deleted as well. [#10509](https://github.com/JabRef/jabref/issues/10509) +- We added support to move the file to the system trash (instead of deleting it). [#10591](https://github.com/JabRef/jabref/pull/10591) - We added ability to jump to an entry in the command line using `-j CITATIONKEY`. [koppor#540](https://github.com/koppor/jabref/issues/540) - We added a new boolean to the style files for Openoffice/Libreoffice integration to switch between ZERO_WIDTH_SPACE (default) and no space. [#10843](https://github.com/JabRef/jabref/pull/10843) - When pasting HTML into the abstract or a comment field, the hypertext is automatically converted to Markdown. [#10558](https://github.com/JabRef/jabref/issues/10558) diff --git a/src/main/java/org/jabref/gui/LibraryTab.java b/src/main/java/org/jabref/gui/LibraryTab.java index 0567113d38b..8b9488b1559 100644 --- a/src/main/java/org/jabref/gui/LibraryTab.java +++ b/src/main/java/org/jabref/gui/LibraryTab.java @@ -8,6 +8,7 @@ import java.util.Objects; import java.util.Optional; import java.util.Random; +import java.util.stream.Collectors; import javax.swing.undo.UndoManager; @@ -42,7 +43,9 @@ import org.jabref.gui.dialogs.AutosaveUiManager; import org.jabref.gui.entryeditor.EntryEditor; import org.jabref.gui.exporter.SaveDatabaseAction; +import org.jabref.gui.fieldeditors.LinkedFileViewModel; import org.jabref.gui.importer.actions.OpenDatabaseAction; +import org.jabref.gui.linkedfile.DeleteFileAction; import org.jabref.gui.maintable.BibEntryTableViewModel; import org.jabref.gui.maintable.MainTable; import org.jabref.gui.maintable.MainTableDataModel; @@ -430,7 +433,7 @@ public SuggestionProviders getSuggestionProviders() { } /** - * Removes the selected entries from the database + * Removes the selected entries and files linked to selected entries from the database * * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" */ @@ -439,7 +442,7 @@ public void delete(StandardActions mode) { } /** - * Removes the selected entries from the database + * Removes the selected entries and files linked to selected entries from the database * * @param mode If DELETE_ENTRY the user will get asked if he really wants to delete the entries, and it will be localized as "deleted". If true the action will be localized as "cut" */ @@ -451,16 +454,31 @@ private void delete(StandardActions mode, List entries) { return; } + // Delete selected entries getUndoManager().addEdit(new UndoableRemoveEntries(bibDatabaseContext.getDatabase(), entries, mode == StandardActions.CUT)); bibDatabaseContext.getDatabase().removeEntries(entries); + + if (mode != StandardActions.CUT) { + List linkedFileList = entries.stream() + .flatMap(entry -> entry.getFiles().stream()) + .distinct() + .toList(); + + if (!linkedFileList.isEmpty()) { + List viewModels = linkedFileList.stream() + .map(linkedFile -> linkedFile.toModel(null, bibDatabaseContext, null, null, preferencesService)) + .collect(Collectors.toList()); + + new DeleteFileAction(dialogService, preferencesService.getFilePreferences(), bibDatabaseContext, viewModels).execute(); + } + } + ensureNotShowingBottomPanel(entries); this.changedProperty.setValue(true); switch (mode) { - case StandardActions.CUT -> - dialogService.notify(Localization.lang("Cut %0 entry(ies)", entries.size())); - case StandardActions.DELETE_ENTRY -> - dialogService.notify(Localization.lang("Deleted %0 entry(ies)", entries.size())); + case StandardActions.CUT -> dialogService.notify(Localization.lang("Cut %0 entry(ies)", entries.size())); + case StandardActions.DELETE_ENTRY -> dialogService.notify(Localization.lang("Deleted %0 entry(ies)", entries.size())); } // prevent the main table from loosing focus @@ -678,6 +696,14 @@ public BibDatabase getDatabase() { return bibDatabaseContext.getDatabase(); } + /** + * Initializes a pop-up dialog box to confirm whether the user wants to delete the selected entry + * Keep track of user preference: + * if the user prefers not to ask before deleting, delete the selected entry without displaying the dialog box + * + * @param numberOfEntries number of entries user is selecting + * @return true if user confirm to delete entry + */ private boolean showDeleteConfirmationDialog(int numberOfEntries) { if (preferencesService.getWorkspacePreferences().shouldConfirmDelete()) { String title = Localization.lang("Delete entry"); @@ -691,7 +717,8 @@ private boolean showDeleteConfirmationDialog(int numberOfEntries) { cancelButton = Localization.lang("Keep entries"); } - return dialogService.showConfirmationDialogWithOptOutAndWait(title, + return dialogService.showConfirmationDialogWithOptOutAndWait( + title, message, okButton, cancelButton, diff --git a/src/main/java/org/jabref/gui/desktop/JabRefDesktop.java b/src/main/java/org/jabref/gui/desktop/JabRefDesktop.java index 6eae1291e31..c3bc152641e 100644 --- a/src/main/java/org/jabref/gui/desktop/JabRefDesktop.java +++ b/src/main/java/org/jabref/gui/desktop/JabRefDesktop.java @@ -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; @@ -52,7 +54,7 @@ private JabRefDesktop() { /** * Open a http/pdf/ps viewer for the given link string. - * + *

* Opening a PDF file at the file field is done at {@link org.jabref.gui.fieldeditors.LinkedFileViewModel#open} */ public static void openExternalViewer(BibDatabaseContext databaseContext, @@ -239,7 +241,6 @@ public static void openFolderAndSelectFile(Path fileLink, * If no command is specified in {@link Globals}, the default system console will be executed. * * @param file Location the console should be opened at. - * */ public static void openConsole(Path file, PreferencesService preferencesService, DialogService dialogService) throws IOException { if (file == null) { @@ -314,4 +315,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(); + } } diff --git a/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java b/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java index 18bf4b82c66..561a4a28c08 100644 --- a/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java +++ b/src/main/java/org/jabref/gui/desktop/os/NativeDesktop.java @@ -1,5 +1,6 @@ package org.jabref.gui.desktop.os; +import java.awt.Desktop; import java.io.File; import java.io.IOException; import java.net.InetAddress; @@ -8,6 +9,7 @@ import java.nio.file.Path; import org.jabref.Launcher; +import org.jabref.architecture.AllowedToUseAwt; import org.jabref.gui.DialogService; import org.jabref.logic.util.BuildInfo; import org.jabref.logic.util.OS; @@ -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. + *

* 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. - * - *

+ * *

* 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/ - *

*/ +@AllowedToUseAwt("Because of moveToTrash() is not available elsewhere.") public abstract class NativeDesktop { public abstract void openFile(String filePath, String fileType, FilePreferences filePreferences) throws IOException; @@ -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); + } } diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java index 5f4d8ef8478..a1b223e40f7 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFileViewModel.java @@ -19,9 +19,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; @@ -30,6 +27,7 @@ import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.icon.JabRefIcon; +import org.jabref.gui.linkedfile.DeleteFileAction; import org.jabref.gui.linkedfile.DownloadLinkedFileAction; import org.jabref.gui.linkedfile.LinkedFileEditDialogView; import org.jabref.gui.mergeentries.MultiMergeEntriesView; @@ -86,7 +84,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()); @@ -365,42 +362,16 @@ public void moveToDefaultDirectoryAndRename() { } /** - * Asks the user for confirmation that he really wants to the delete the file from disk (or just remove the link). + * Asks the user for confirmation that he really wants to the delete the file from disk (or just remove the link) + * and then proceeds accordingly. * - * @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) + * @return true if the linked file has been removed afterward 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 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 = 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() { diff --git a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java index afec9677e52..b871e61ef91 100644 --- a/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java +++ b/src/main/java/org/jabref/gui/fieldeditors/LinkedFilesEditor.java @@ -254,8 +254,7 @@ private void setUpKeyBindings() { if (keyBinding.isPresent()) { switch (keyBinding.get()) { case DELETE_ENTRY: - new DeleteFileAction(dialogService, preferencesService, databaseContext, - viewModel, listView).execute(); + deleteAttachedFilesWithConfirmation(); event.consume(); break; default: @@ -265,6 +264,11 @@ private void setUpKeyBindings() { }); } + private void deleteAttachedFilesWithConfirmation() { + new DeleteFileAction(dialogService, preferencesService.getFilePreferences(), databaseContext, + viewModel, listView.getSelectionModel().getSelectedItems()).execute(); + } + public LinkedFilesEditorViewModel getViewModel() { return viewModel; } diff --git a/src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java b/src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java index a3db9a78910..580ec7b74d8 100644 --- a/src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java +++ b/src/main/java/org/jabref/gui/linkedfile/DeleteFileAction.java @@ -6,133 +6,205 @@ import java.util.List; import java.util.Optional; -import javafx.scene.control.Alert; +import javafx.collections.FXCollections; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; +import javafx.scene.control.DialogPane; +import javafx.scene.control.Label; import javafx.scene.control.ListView; +import javafx.scene.layout.VBox; 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.gui.icon.IconTheme; +import org.jabref.gui.icon.JabRefIconView; +import org.jabref.gui.util.ViewModelListCellFactory; 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 ListView listView; + private final @Nullable LinkedFilesEditorViewModel viewModel; + private final List filesToDelete; + private boolean success = false; public DeleteFileAction(DialogService dialogService, - PreferencesService preferences, + FilePreferences filePreferences, BibDatabaseContext databaseContext, - LinkedFilesEditorViewModel viewModel, - ListView listView) { + @Nullable LinkedFilesEditorViewModel viewModel, + List filesToDelete) { this.dialogService = dialogService; - this.preferences = preferences; + this.filePreferences = filePreferences; this.databaseContext = databaseContext; this.viewModel = viewModel; - this.listView = listView; + this.filesToDelete = List.copyOf(filesToDelete); + } + + /** + * Called when the user wants to delete a complete entry. + */ + public DeleteFileAction(DialogService dialogService, + FilePreferences filePreferences, + BibDatabaseContext databaseContext, + List filesToDelete) { + this(dialogService, filePreferences, databaseContext, null, filesToDelete); + } + + private boolean deletionOfCompleteEntry() { + return viewModel == null; } @Override public void execute() { - List toBeDeleted = List.copyOf(listView.getSelectionModel().getSelectedItems()); - - if (toBeDeleted.isEmpty()) { + if (filesToDelete.isEmpty()) { dialogService.notify(Localization.lang("This operation requires selected linked files.")); return; } + if (!filePreferences.confirmDeleteLinkedFile()) { + LOGGER.info("Deleting {} files without confirmation.", filesToDelete.size()); + deleteFiles(true); + return; + } + String dialogTitle; - String dialogContent; + String dialogDescription; - if (toBeDeleted.size() != 1) { - dialogTitle = Localization.lang("Delete %0 files", toBeDeleted.size()); - 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.", toBeDeleted.size()); - } else { - Optional file = toBeDeleted.getFirst().getFile().findIn(databaseContext, preferences.getFilePreferences()); + int numberOfLinkedFiles = filesToDelete.size(); + dialogDescription = Localization.lang("How should these files be handled?"); + if (numberOfLinkedFiles != 1) { + dialogTitle = Localization.lang("Delete %0 files", numberOfLinkedFiles); + } else { + LinkedFile linkedFile = filesToDelete.getFirst().getFile(); + Optional file = linkedFile.findIn(databaseContext, filePreferences); if (file.isPresent()) { - dialogTitle = Localization.lang("Delete '%0'", file.get().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()); + Path path = file.get(); + dialogTitle = Localization.lang("Delete '%0'", path.getFileName().toString()); } else { - dialogService.notify(Localization.lang("Error accessing file '%0'.", toBeDeleted.getFirst().getFile().getLink())); + dialogService.notify(Localization.lang("Error accessing file '%0'.", linkedFile.getLink())); + // Deleting a non-existing file is a success + success = true; return; } } - ButtonType removeFromEntry = new ButtonType(Localization.lang("Remove from entry"), ButtonBar.ButtonData.YES); - ButtonType deleteFromEntry = new ButtonType(Localization.lang("Delete from disk")); - Optional buttonType = dialogService.showCustomButtonDialogAndWait(Alert.AlertType.INFORMATION, - dialogTitle, dialogContent, removeFromEntry, deleteFromEntry, ButtonType.CANCEL); + DialogPane dialogPane = createDeleteFilesDialog(dialogDescription); - if (buttonType.isPresent()) { - if (buttonType.get().equals(removeFromEntry)) { - deleteFiles(toBeDeleted, false); - } + String label; + if (filePreferences.moveToTrash()) { + label = Localization.lang("Move file(s) to trash"); + } else { + label = Localization.lang("Delete from disk"); + } + ButtonType deleteFromDisk = new ButtonType(label); + + ButtonType removeFromEntry = new ButtonType(Localization.lang("Keep file(s)"), ButtonBar.ButtonData.YES); - if (buttonType.get().equals(deleteFromEntry)) { - deleteFiles(toBeDeleted, true); + Optional buttonType = dialogService.showCustomDialogAndWait( + dialogTitle, dialogPane, removeFromEntry, deleteFromDisk, ButtonType.CANCEL); + + if (buttonType.isPresent()) { + ButtonType theButtonType = buttonType.get(); + if (theButtonType.equals(removeFromEntry)) { + deleteFiles(false); + } else if (theButtonType.equals(deleteFromDisk)) { + deleteFiles(true); } } } + private DialogPane createDeleteFilesDialog(String description) { + JabRefIconView warning = new JabRefIconView(IconTheme.JabRefIcons.WARNING); + warning.setGlyphSize(24.0); + Label header = new Label(description, warning); + header.setWrapText(true); + header.setStyle(""" + -fx-padding: 10px; + -fx-background-color: -fx-background;"""); + + ListView filesToDeleteList = new ListView<>(FXCollections.observableArrayList(filesToDelete)); + new ViewModelListCellFactory() + .withText(item -> item.getFile().getLink()) + .install(filesToDeleteList); + + VBox content = new VBox(header, filesToDeleteList); + DialogPane dialogPane = new DialogPane(); + dialogPane.setHeader(header); + dialogPane.setContent(content); + return dialogPane; + } + /** * 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 toBeDeleted, boolean deleteFromDisk) { - for (LinkedFileViewModel fileViewModel : toBeDeleted) { - if (fileViewModel.getFile().isOnlineLink()) { + private void deleteFiles(boolean deleteFromDisk) { + // default: We have a success + success = true; + for (LinkedFileViewModel fileViewModel : filesToDelete) { + if (!fileViewModel.getFile().isOnlineLink() && deleteFromDisk) { + deleteFileHelper(databaseContext, fileViewModel.getFile()); + } + if (viewModel != null) { viewModel.removeFileLink(fileViewModel); - } else { - if (deleteFromDisk) { - deleteFileFromDisk(fileViewModel); - } - viewModel.getFiles().remove(fileViewModel); } } } /** - * Deletes the file from disk without asking the user for confirmation. + * Helper method to delete the specified file from disk * - * @param fileViewModel the file to be deleted + * @param linkedFile The LinkedFile (file which linked to an entry) to be deleted from disk */ - public void deleteFileFromDisk(LinkedFileViewModel fileViewModel) { - LinkedFile linkedFile = fileViewModel.getFile(); - - Optional file = linkedFile.findIn(databaseContext, preferences.getFilePreferences()); + private void deleteFileHelper(BibDatabaseContext databaseContext, LinkedFile linkedFile) { + Optional 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())); + // Deleting a non-existing file is a success + success = true; + 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 { + boolean preferencesMoveToTrash = filePreferences.moveToTrash(); + LOGGER.debug("filePreferences.moveToTrash() = {}", preferencesMoveToTrash); + if (preferencesMoveToTrash) { + LOGGER.debug("Moving to trash: {}", theFile); + JabRefDesktop.moveToTrash(theFile); + } else { + LOGGER.debug("Deleting: {}", theFile); + 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; + } } diff --git a/src/main/java/org/jabref/gui/openoffice/DetectOpenOfficeInstallation.java b/src/main/java/org/jabref/gui/openoffice/DetectOpenOfficeInstallation.java index d0d71c01ed2..9ab8e25b623 100644 --- a/src/main/java/org/jabref/gui/openoffice/DetectOpenOfficeInstallation.java +++ b/src/main/java/org/jabref/gui/openoffice/DetectOpenOfficeInstallation.java @@ -6,7 +6,7 @@ import java.util.Optional; import org.jabref.gui.DialogService; -import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.util.DirectoryDialogConfiguration; import org.jabref.logic.l10n.Localization; import org.jabref.logic.openoffice.OpenOfficePreferences; @@ -32,12 +32,10 @@ public boolean isExecutablePathDefined() { } public Optional selectInstallationPath() { - final NativeDesktop nativeDesktop = OS.getNativeDesktop(); - dialogService.showInformationDialogAndWait(Localization.lang("Could not find OpenOffice/LibreOffice installation"), Localization.lang("Unable to autodetect OpenOffice/LibreOffice installation. Please choose the installation directory manually.")); DirectoryDialogConfiguration dirDialogConfiguration = new DirectoryDialogConfiguration.Builder() - .withInitialDirectory(nativeDesktop.getApplicationDirectory()) + .withInitialDirectory(JabRefDesktop.getApplicationDirectory()) .build(); return dialogService.showDirectorySelectionDialog(dirDialogConfiguration); } diff --git a/src/main/java/org/jabref/gui/preferences/externalfiletypes/EditExternalFileTypeEntryDialog.java b/src/main/java/org/jabref/gui/preferences/externalfiletypes/EditExternalFileTypeEntryDialog.java index d0dc110b581..7e930b0bd93 100644 --- a/src/main/java/org/jabref/gui/preferences/externalfiletypes/EditExternalFileTypeEntryDialog.java +++ b/src/main/java/org/jabref/gui/preferences/externalfiletypes/EditExternalFileTypeEntryDialog.java @@ -12,11 +12,10 @@ import javafx.scene.control.ToggleGroup; import org.jabref.gui.DialogService; -import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.gui.desktop.JabRefDesktop; import org.jabref.gui.util.BaseDialog; import org.jabref.gui.util.FileDialogConfiguration; import org.jabref.gui.util.IconValidationDecorator; -import org.jabref.logic.util.OS; import com.airhacks.afterburner.views.ViewLoader; import de.saxsys.mvvmfx.utils.validation.visualization.ControlsFxVisualizer; @@ -35,8 +34,7 @@ public class EditExternalFileTypeEntryDialog extends BaseDialog { @FXML private Label icon; @Inject private DialogService dialogService; - private final NativeDesktop nativeDesktop = OS.getNativeDesktop(); - private final FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder().withInitialDirectory(nativeDesktop.getApplicationDirectory()).build(); + private final FileDialogConfiguration fileDialogConfiguration = new FileDialogConfiguration.Builder().withInitialDirectory(JabRefDesktop.getApplicationDirectory()).build(); private final ExternalFileTypeItemViewModel item; private final ObservableList fileTypes; diff --git a/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml b/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml index 0f26e9890e7..56600d64e34 100644 --- a/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml +++ b/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.fxml @@ -76,4 +76,8 @@ + +