diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc4fbe4094..4d66125d527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - By double clicking on a local citation in the Citation Relations Tab you can now jump the linked entry. [#11955](https://github.com/JabRef/jabref/pull/11955) - We use the menu icon for background tasks as a progress indicator to visualise an import's progress when dragging and dropping several PDF files into the main table. [#12072](https://github.com/JabRef/jabref/pull/12072) - The PDF content importer now supports importing title from upto the second page of the PDF. [#12139](https://github.com/JabRef/jabref/issues/12139) +- We added the ability for users to display a cover image in the preview panel of the entry editor for book, in-book and booklet citations. [#10120](https://github.com/JabRef/jabref/issues/10120) ### Changed diff --git a/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 306c543af77..00e1616d6f1 100644 --- a/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -218,14 +218,35 @@ private void setPreviewText(String text) { layoutText = """ -
%s
+
+
+ %s +
+
+ +
+
- """.formatted(text); + """.formatted(text, getBookCoverURI()); highlightLayoutText(); this.setHvalue(0); } + private String getBookCoverURI() { + if (entry != null) { + if (entry.getCoverImageFile().isPresent()) { + return "file:///" + entry.getCoverImageFile().get().getLink(); + } + } + + return ""; + } + private void highlightLayoutText() { if (layoutText == null) { return; diff --git a/src/main/java/org/jabref/model/entry/BibEntry.java b/src/main/java/org/jabref/model/entry/BibEntry.java index 6f2a38aca7c..8b1adc9c9ae 100644 --- a/src/main/java/org/jabref/model/entry/BibEntry.java +++ b/src/main/java/org/jabref/model/entry/BibEntry.java @@ -99,6 +99,16 @@ public class BibEntry implements Cloneable { public static final EntryType DEFAULT_TYPE = StandardEntryType.Misc; + + private static final HashSet COVERABLE_TYPES = new HashSet<>(); + static { + COVERABLE_TYPES.add(StandardEntryType.Book); + COVERABLE_TYPES.add(StandardEntryType.InBook); + COVERABLE_TYPES.add(StandardEntryType.Booklet); + } + + private static final String COVER_TAG = "cover"; + private static final Logger LOGGER = LoggerFactory.getLogger(BibEntry.class); private final SharedBibEntryData sharedBibEntryData; @@ -1124,6 +1134,36 @@ public Optional addFiles(List filesToAdd) { currentFiles.addAll(filesToAdd); return setFiles(currentFiles); } + + /** + * @return LinkedFile that contains the cover image + * if the BibEntry is a Book or other COVERABLE_TYPES + */ + public Optional getCoverImageFile() { + if (!isCoverable()) { + return Optional.empty(); + } + + List files = getFiles(); + + if (files == null) { + return Optional.empty(); + } + + if (files.isEmpty()) { + return Optional.empty(); + } + + for (LinkedFile file : getFiles()) { + if (file.getDescription().equalsIgnoreCase(COVER_TAG)) { + if (file.isImage()) { + return Optional.of(file); + } + } + } + + return Optional.empty(); + } // endregion /** @@ -1253,4 +1293,11 @@ public boolean isEmpty() { } return StandardField.AUTOMATIC_FIELDS.containsAll(this.getFields()); } + + /** + * @return true if this entry's type is a Book or one of COVERABLE_TYPES + */ + private boolean isCoverable() { + return COVERABLE_TYPES.contains(getType()); + } } diff --git a/src/main/java/org/jabref/model/entry/LinkedFile.java b/src/main/java/org/jabref/model/entry/LinkedFile.java index ec189fab2bf..cd7167cc6d2 100644 --- a/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -220,6 +220,14 @@ public boolean isOnlineLink() { return isOnlineLink(link.get()); } + /** + * @return true if fileType contains the string "image" + */ + public boolean isImage() { + // Cannot compare fileType to StandardExternalFileType enum since it is a StringProperty + return getFileType().toLowerCase().contains("image"); + } + public Optional findIn(BibDatabaseContext databaseContext, FilePreferences filePreferences) { List dirs = databaseContext.getFileDirectories(filePreferences); return findIn(dirs); diff --git a/src/test/java/org/jabref/model/entry/BibEntryTest.java b/src/test/java/org/jabref/model/entry/BibEntryTest.java index a9910037379..3dbd7f28741 100644 --- a/src/test/java/org/jabref/model/entry/BibEntryTest.java +++ b/src/test/java/org/jabref/model/entry/BibEntryTest.java @@ -1,6 +1,7 @@ package org.jabref.model.entry; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -813,6 +814,82 @@ void mergeEntriesWithOverlapAndPriorityGivenToOverlappingField() { assertEquals(expected.getFields(), copyEntry.getFields()); } + @Test + void getCoverImageReturnsCorrectImage() { + LinkedFile cover1 = new LinkedFile("", Paths.get("JabRef-icon-128.png"), "PNG image"); + LinkedFile cover2 = new LinkedFile("", Paths.get("JabRef-icon-64.png"), "PNG image"); + LinkedFile cover3 = new LinkedFile("cover", Paths.get("wallpaper.jpg"), "JPG image"); + BibEntry entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value"); + entry.addFile(cover1); + entry.addFile(cover2); + entry.addFile(cover3); + assertEquals(Optional.of(cover3), entry.getCoverImageFile()); + } + + @Test + void getCoverImageReturnsEmptyIfNoFiles() { + entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value"); + assertEquals(Optional.empty(), entry.getCoverImageFile()); + } + + @Test + void getCoverImageReturnsEmptyIfNoImageFiles() { + LinkedFile pdf = new LinkedFile("", Paths.get("Baldoni2002.pdf").toAbsolutePath().toString(), "pdf"); + LinkedFile markdown = new LinkedFile("", "readme.md", "md"); + entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value"); + entry.addFile(markdown); + entry.addFile(pdf); + assertEquals(Optional.empty(), entry.getCoverImageFile()); + } + + @ParameterizedTest + @MethodSource("nonCoverableEntryTypes") + void getCoverImageReturnsEmptyIfEntryIsNotCoverable(StandardEntryType entryType) { + BibEntry entry = new BibEntry(entryType).withField(StandardField.AUTHOR, "value"); + assertEquals(Optional.empty(), entry.getCoverImageFile()); + } + + static Stream nonCoverableEntryTypes() { + return Stream.of( + StandardEntryType.Proceedings, + StandardEntryType.Dataset, + StandardEntryType.Software + ); + } + + @ParameterizedTest + @MethodSource("imagesWithoutCoverDescription") + void getCoverImageDoesNotReturnImagesWithoutCoverDescription(LinkedFile image) { + entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value"); + entry.addFile(image); + assertEquals(Optional.empty(), entry.getCoverImageFile()); + } + + static Stream imagesWithoutCoverDescription() { + return Stream.of( + new LinkedFile("", Paths.get("JabRef-icon-128.png"), "PNG image"), + new LinkedFile("", Paths.get("JabRef-icon-64.png"), "PNG image"), + new LinkedFile("", Paths.get("JabRef-icon-32.png"), "PNG image") + ); + } + + @ParameterizedTest + @MethodSource("docsWithCoverDescription") + void getCoverImageDoesNotReturnDocumentsWithCoverDescription(LinkedFile file) { + entry = new BibEntry(StandardEntryType.Book).withField(StandardField.AUTHOR, "value"); + entry.addFile(file); + assertEquals(Optional.empty(), entry.getCoverImageFile()); + } + + static Stream docsWithCoverDescription() { + return Stream.of( + new LinkedFile("cover", Paths.get("Baldoni2002.pdf"), "pdf"), + new LinkedFile("cover", Paths.get("readme.md"), "md"), + new LinkedFile("cover", Paths.get("BiblioscapeImporterTestArticleST.txt"), "txt"), + new LinkedFile("cover", Paths.get("emptyFile.xml"), "xml") + ); + } + public static Stream isEmpty() { return Stream.of( new BibEntry(),