diff --git a/CHANGELOG.md b/CHANGELOG.md index 67855ca84f8..1cadf26400e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv ### Added +- We added automatic lookup of DOI at citation relations [#13234](https://github.com/JabRef/jabref/issues/13234) - We added focus on the field Link in the "Add file link" dialog. [#13486](https://github.com/JabRef/jabref/issues/13486) - We introduced a settings parameter to manage citations' relations local storage time-to-live with a default value set to 30 days. [#11189](https://github.com/JabRef/jabref/issues/11189) - We distribute arm64 images for Linux. [#10842](https://github.com/JabRef/jabref/issues/10842) diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationComponents.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationComponents.java new file mode 100644 index 00000000000..00e61751bc7 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationComponents.java @@ -0,0 +1,19 @@ +package org.jabref.gui.entryeditor.citationrelationtab; + +import javafx.scene.control.Button; +import javafx.scene.control.ProgressIndicator; + +import org.jabref.logic.importer.fetcher.citation.CitationFetcher; +import org.jabref.model.entry.BibEntry; + +import org.controlsfx.control.CheckListView; + +public record CitationComponents( + BibEntry entry, + CheckListView listView, + Button abortButton, + Button refreshButton, + CitationFetcher.SearchType searchType, + Button importButton, + ProgressIndicator progress) { +} diff --git a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java index b883672d884..afdd909a4a8 100644 --- a/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java +++ b/jabgui/src/main/java/org/jabref/gui/entryeditor/citationrelationtab/CitationRelationsTab.java @@ -20,6 +20,7 @@ import javafx.scene.control.Button; import javafx.scene.control.ButtonType; import javafx.scene.control.DialogPane; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.ScrollPane; @@ -52,6 +53,7 @@ import org.jabref.logic.citation.SearchCitationsRelationsService; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.exporter.BibWriter; +import org.jabref.logic.importer.fetcher.CrossRef; import org.jabref.logic.importer.fetcher.citation.CitationFetcher; import org.jabref.logic.l10n.Localization; import org.jabref.logic.os.OS; @@ -199,52 +201,207 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { citingVBox.getChildren().addAll(citingHBox, citingListView); citedByVBox.getChildren().addAll(citedByHBox, citedByListView); - refreshCitingButton.setOnMouseClicked(_ -> { - searchForRelations( - entry, - citingListView, - abortCitingButton, - refreshCitingButton, - CitationFetcher.SearchType.CITES, - importCitingButton, - citingProgress); - }); + CitationComponents citingComponents = new CitationComponents( + entry, + citingListView, + abortCitingButton, + refreshCitingButton, + CitationFetcher.SearchType.CITES, + importCitingButton, + citingProgress); - refreshCitedByButton.setOnMouseClicked(_ -> searchForRelations( - entry, - citedByListView, + CitationComponents citedByComponents = new CitationComponents( + entry, + citedByListView, abortCitedButton, - refreshCitedByButton, - CitationFetcher.SearchType.CITED_BY, - importCitedByButton, - citedByProgress)); + refreshCitedByButton, + CitationFetcher.SearchType.CITED_BY, + importCitedByButton, + citedByProgress); + + refreshCitingButton.setOnMouseClicked(_ -> searchForRelations(citingComponents, citedByComponents)); + refreshCitedByButton.setOnMouseClicked(_ -> searchForRelations(citedByComponents, citingComponents)); // Create SplitPane to hold all nodes above SplitPane container = new SplitPane(citingVBox, citedByVBox); styleFetchedListView(citedByListView); styleFetchedListView(citingListView); - searchForRelations( - entry, - citingListView, - abortCitingButton, - refreshCitingButton, - CitationFetcher.SearchType.CITES, - importCitingButton, - citingProgress); - - searchForRelations( - entry, - citedByListView, - abortCitedButton, - refreshCitedByButton, - CitationFetcher.SearchType.CITED_BY, - importCitedByButton, - citedByProgress); + searchForRelations(citingComponents, citedByComponents); + searchForRelations(citedByComponents, citingComponents); return container; } + private void searchForRelations(CitationComponents citationComponents, + CitationComponents otherCitationComponents) { + if (citationComponents.entry().getDOI().isEmpty()) { + setUpEmptyPanel(citationComponents, otherCitationComponents); + return; + } + executeSearch(citationComponents); + } + + private void setUpEmptyPanel(CitationComponents citationComponents, + CitationComponents otherCitationComponents) { + hideNodes(citationComponents.abortButton(), citationComponents.progress()); + showNodes(citationComponents.refreshButton()); + + HBox hBox = new HBox(); + Label label = new Label(Localization.lang("The selected entry doesn't have a DOI linked to it.")); + Hyperlink link = new Hyperlink(Localization.lang("Look up a DOI and try again.")); + + link.setOnAction(e -> { + CrossRef doiFetcher = new CrossRef(); + + BackgroundTask.wrap(() -> doiFetcher.findIdentifier(citationComponents.entry())) + .onRunning(() -> { + showNodes(citationComponents.progress(), otherCitationComponents.progress()); + setLabelOn(citationComponents.listView(), Localization.lang("Looking up DOI...")); + setLabelOn(otherCitationComponents.listView(), Localization.lang("Looking up DOI...")); + }) + .onSuccess(identifier -> { + if (identifier.isPresent()) { + citationComponents.entry().setField(StandardField.DOI, identifier.get().asString()); + executeSearch(citationComponents); + executeSearch(otherCitationComponents); + } else { + dialogService.notify(Localization.lang("No DOI found")); + setUpEmptyPanel(citationComponents, otherCitationComponents); + setUpEmptyPanel(otherCitationComponents, citationComponents); + } + }).onFailure(ex -> { + hideNodes(citationComponents.progress(), otherCitationComponents.progress()); + setLabelOn(citationComponents.listView(), "Error " + ex.getMessage()); + setLabelOn(otherCitationComponents.listView(), "Error " + ex.getMessage()); + }).executeWith(taskExecutor); + }); + + hBox.getChildren().add(label); + hBox.getChildren().add(link); + hBox.setSpacing(2d); + hBox.setStyle("-fx-alignment: center;"); + hBox.setFillHeight(true); + + citationComponents.listView().getItems().clear(); + citationComponents.listView().setPlaceholder(hBox); + } + + private void executeSearch(CitationComponents citationComponents) { + ObservableList observableList = FXCollections.observableArrayList(); + citationComponents.listView().setItems(observableList); + + // TODO: It should not be possible to cancel a search task that is already running for same tab + if (citingTask != null && !citingTask.isCancelled() && citationComponents.searchType() == CitationFetcher.SearchType.CITES) { + citingTask.cancel(); + } else if (citedByTask != null && !citedByTask.isCancelled() && citationComponents.searchType() == CitationFetcher.SearchType.CITED_BY) { + citedByTask.cancel(); + } + + this.createBackgroundTask(citationComponents.entry(), citationComponents.searchType()) + .consumeOnRunning(task -> prepareToSearchForRelations(citationComponents, task)) + .onSuccess(fetchedList -> onSearchForRelationsSucceed(citationComponents, + fetchedList, + observableList + )) + .onFailure(exception -> { + LOGGER.error("Error while fetching citing Articles", exception); + hideNodes(citationComponents.abortButton(), citationComponents.progress(), citationComponents.importButton()); + citationComponents.listView().setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", + exception.getMessage()))); + citationComponents.refreshButton().setVisible(true); + dialogService.notify(exception.getMessage()); + }) + .executeWith(taskExecutor); + } + + private void prepareToSearchForRelations(CitationComponents citationComponents, BackgroundTask> task) { + showNodes(citationComponents.abortButton(), citationComponents.progress()); + hideNodes(citationComponents.refreshButton(), citationComponents.importButton()); + + citationComponents.abortButton().setOnAction(event -> { + hideNodes(citationComponents.abortButton(), citationComponents.progress(), citationComponents.importButton()); + showNodes(citationComponents.refreshButton()); + task.cancel(); + dialogService.notify(Localization.lang("Search aborted.")); + }); + } + + private void onSearchForRelationsSucceed(CitationComponents citationComponents, + List fetchedList, + ObservableList observableList) { + + hideNodes(citationComponents.abortButton(), citationComponents.progress()); + + BibDatabase database = stateManager.getActiveDatabase().map(BibDatabaseContext::getDatabase).orElse(new BibDatabase()); + observableList.setAll( + fetchedList.stream().map(entr -> + duplicateCheck.containsDuplicate( + database, + entr, + BibDatabaseModeDetection.inferMode(database)) + .map(localEntry -> new CitationRelationItem(entr, localEntry, true)) + .orElseGet(() -> new CitationRelationItem(entr, false))) + .toList() + ); + + if (!observableList.isEmpty()) { + citationComponents.listView().refresh(); + } else { + Label placeholder = new Label(Localization.lang("No articles found")); + citationComponents.listView().setPlaceholder(placeholder); + } + BooleanBinding booleanBind = Bindings.isEmpty(citationComponents.listView().getCheckModel().getCheckedItems()); + citationComponents.importButton().disableProperty().bind(booleanBind); + citationComponents.importButton().setOnMouseClicked(event -> importEntries(citationComponents.listView().getCheckModel().getCheckedItems(), citationComponents.searchType(), citationComponents.entry())); + showNodes(citationComponents.refreshButton(), citationComponents.importButton()); + } + + private void jumpToEntry(CitationRelationItem entry) { + citingTask.cancel(); + citedByTask.cancel(); + stateManager.activeTabProperty().get().ifPresent(tab -> tab.showAndEdit(entry.localEntry())); + } + + /** + * @implNote This code is similar to {@link PreviewWithSourceTab#getSourceString(BibEntry, BibDatabaseMode, FieldPreferences, BibEntryTypesManager)}. + */ + private String getSourceString(BibEntry entry, BibDatabaseMode type, FieldPreferences fieldPreferences, BibEntryTypesManager entryTypesManager) throws IOException { + StringWriter writer = new StringWriter(); + BibWriter bibWriter = new BibWriter(writer, OS.NEWLINE); + FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); + new BibEntryWriter(fieldWriter, entryTypesManager).write(entry, bibWriter, type); + return writer.toString(); + } + + private void showEntrySourceDialog(BibEntry entry) { + CodeArea ca = new CodeArea(); + try { + BibDatabaseMode mode = stateManager.getActiveDatabase().map(BibDatabaseContext::getMode) + .orElse(BibDatabaseMode.BIBLATEX); + ca.appendText(getSourceString(entry, mode, preferences.getFieldPreferences(), this.entryTypesManager)); + } catch (IOException e) { + LOGGER.warn("Incorrect entry, could not load source:", e); + return; + } + + ca.setWrapText(true); + ca.setPadding(new Insets(0, 10, 0, 10)); + ca.showParagraphAtTop(0); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setFitToWidth(true); + scrollPane.setFitToHeight(true); + scrollPane.setContent(new VirtualizedScrollPane<>(ca)); + + DialogPane dialogPane = new DialogPane(); + dialogPane.setPrefSize(800, 400); + dialogPane.setContent(scrollPane); + String title = Localization.lang("Show BibTeX source"); + + dialogService.showCustomDialogAndWait(title, dialogPane, ButtonType.OK); + } + /** * Styles a given CheckListView to display BibEntries either with a hyperlink or an add button * @@ -271,9 +428,9 @@ private void styleFetchedListView(CheckListView listView) jumpTo.getStyleClass().add("addEntryButton"); jumpTo.setOnMouseClicked(_ -> jumpToEntry(entry)); hContainer.setOnMouseClicked(event -> { - if (event.getClickCount() == 2) { - jumpToEntry(entry); - } + if (event.getClickCount() == 2) { + jumpToEntry(entry); + } }); vContainer.getChildren().add(jumpTo); @@ -336,51 +493,6 @@ private void styleFetchedListView(CheckListView listView) listView.setSelectionModel(new NoSelectionModel<>()); } - private void jumpToEntry(CitationRelationItem entry) { - citingTask.cancel(); - citedByTask.cancel(); - stateManager.activeTabProperty().get().ifPresent(tab -> tab.showAndEdit(entry.localEntry())); - } - - /** - * @implNote This code is similar to {@link PreviewWithSourceTab#getSourceString(BibEntry, BibDatabaseMode, FieldPreferences, BibEntryTypesManager)}. - */ - private String getSourceString(BibEntry entry, BibDatabaseMode type, FieldPreferences fieldPreferences, BibEntryTypesManager entryTypesManager) throws IOException { - StringWriter writer = new StringWriter(); - BibWriter bibWriter = new BibWriter(writer, OS.NEWLINE); - FieldWriter fieldWriter = FieldWriter.buildIgnoreHashes(fieldPreferences); - new BibEntryWriter(fieldWriter, entryTypesManager).write(entry, bibWriter, type); - return writer.toString(); - } - - private void showEntrySourceDialog(BibEntry entry) { - CodeArea ca = new CodeArea(); - try { - BibDatabaseMode mode = stateManager.getActiveDatabase().map(BibDatabaseContext::getMode) - .orElse(BibDatabaseMode.BIBLATEX); - ca.appendText(getSourceString(entry, mode, preferences.getFieldPreferences(), this.entryTypesManager)); - } catch (IOException e) { - LOGGER.warn("Incorrect entry, could not load source:", e); - return; - } - - ca.setWrapText(true); - ca.setPadding(new Insets(0, 10, 0, 10)); - ca.showParagraphAtTop(0); - - ScrollPane scrollPane = new ScrollPane(); - scrollPane.setFitToWidth(true); - scrollPane.setFitToHeight(true); - scrollPane.setContent(new VirtualizedScrollPane<>(ca)); - - DialogPane dialogPane = new DialogPane(); - dialogPane.setPrefSize(800, 400); - dialogPane.setContent(scrollPane); - String title = Localization.lang("Show BibTeX source"); - - dialogService.showCustomDialogAndWait(title, dialogPane, ButtonType.OK); - } - /** * Method to style heading labels * @@ -423,62 +535,10 @@ protected void bindToEntry(BibEntry entry) { setContent(getPaneAndStartSearch(entry)); } - /** - * Method to start search for relations and display them in the associated ListView - * - * @param entry BibEntry currently selected in Jabref Database - * @param listView ListView to use - * @param abortButton Button to stop the search - * @param refreshButton refresh Button to use - * @param searchType type of search (CITING / CITEDBY) - */ - private void searchForRelations(BibEntry entry, CheckListView listView, Button abortButton, - Button refreshButton, CitationFetcher.SearchType searchType, Button importButton, - ProgressIndicator progress) { - if (entry.getDOI().isEmpty()) { - hideNodes(abortButton, progress); - showNodes(refreshButton); - listView.getItems().clear(); - listView.setPlaceholder( - new Label(Localization.lang("The selected entry doesn't have a DOI linked to it. Lookup a DOI and try again."))); - return; - } - - ObservableList observableList = FXCollections.observableArrayList(); - - listView.setItems(observableList); - - // TODO: It should not be possible to cancel a search task that is already running for same tab - if (citingTask != null && !citingTask.isCancelled() && searchType == CitationFetcher.SearchType.CITES) { - citingTask.cancel(); - } else if (citedByTask != null && !citedByTask.isCancelled() && searchType == CitationFetcher.SearchType.CITED_BY) { - citedByTask.cancel(); - } - - this.createBackgroundTask(entry, searchType) - .consumeOnRunning(task -> prepareToSearchForRelations( - abortButton, refreshButton, importButton, progress, task - )) - .onSuccess(fetchedList -> onSearchForRelationsSucceed( - entry, - listView, - abortButton, - refreshButton, - searchType, - importButton, - progress, - fetchedList, - observableList - )) - .onFailure(exception -> { - LOGGER.error("Error while fetching citing Articles", exception); - hideNodes(abortButton, progress, importButton); - listView.setPlaceholder(new Label(Localization.lang("Error while fetching citing entries: %0", - exception.getMessage()))); - refreshButton.setVisible(true); - dialogService.notify(exception.getMessage()); - }) - .executeWith(taskExecutor); + private static void setLabelOn(CheckListView listView, String message) { + Label lookingUpDoiLabel = new Label(message); + listView.getItems().clear(); + listView.setPlaceholder(lookingUpDoiLabel); } /** @@ -503,51 +563,6 @@ private BackgroundTask> createBackgroundTask( }; } - private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView, - Button abortButton, Button refreshButton, - CitationFetcher.SearchType searchType, Button importButton, - ProgressIndicator progress, List fetchedList, - ObservableList observableList) { - hideNodes(abortButton, progress); - - BibDatabase database = stateManager.getActiveDatabase().map(BibDatabaseContext::getDatabase) - .orElse(new BibDatabase()); - observableList.setAll( - fetchedList.stream().map(entr -> - duplicateCheck.containsDuplicate( - database, - entr, - BibDatabaseModeDetection.inferMode(database)) - .map(localEntry -> new CitationRelationItem(entr, localEntry, true)) - .orElseGet(() -> new CitationRelationItem(entr, false))) - .toList() - ); - - if (!observableList.isEmpty()) { - listView.refresh(); - } else { - Label placeholder = new Label(Localization.lang("No articles found")); - listView.setPlaceholder(placeholder); - } - BooleanBinding booleanBind = Bindings.isEmpty(listView.getCheckModel().getCheckedItems()); - importButton.disableProperty().bind(booleanBind); - importButton.setOnMouseClicked(event -> importEntries(listView.getCheckModel().getCheckedItems(), searchType, entry)); - showNodes(refreshButton, importButton); - } - - private void prepareToSearchForRelations(Button abortButton, Button refreshButton, Button importButton, - ProgressIndicator progress, BackgroundTask> task) { - showNodes(abortButton, progress); - hideNodes(refreshButton, importButton); - - abortButton.setOnAction(event -> { - hideNodes(abortButton, progress, importButton); - showNodes(refreshButton); - task.cancel(); - dialogService.notify(Localization.lang("Search aborted!")); - }); - } - private void hideNodes(Node... nodes) { Arrays.stream(nodes).forEach(node -> node.setVisible(false)); } diff --git a/jablib/src/main/resources/l10n/JabRef_en.properties b/jablib/src/main/resources/l10n/JabRef_en.properties index 5b983088790..1225a8e43f1 100644 --- a/jablib/src/main/resources/l10n/JabRef_en.properties +++ b/jablib/src/main/resources/l10n/JabRef_en.properties @@ -2801,14 +2801,17 @@ Miscellaneous=Miscellaneous File-related=File-related Add\ selected\ entry(s)\ to\ library=Add selected entry(s) to library -The\ selected\ entry\ doesn't\ have\ a\ DOI\ linked\ to\ it.\ Lookup\ a\ DOI\ and\ try\ again.=The selected entry doesn't have a DOI linked to it. Lookup a DOI and try again. +The\ selected\ entry\ doesn't\ have\ a\ DOI\ linked\ to\ it.=The selected entry doesn't have a DOI linked to it. +Look\ up\ a\ DOI\ and\ try\ again.=Look up a DOI and try again. +Looking\ up\ DOI...=Looking up DOI... +No\ DOI\ found=No DOI found Cited\ By=Cited By Cites=Cites No\ articles\ found=No articles found Restart\ search=Restart search Cancel\ search=Cancel search Select\ entry=Select entry -Search\ aborted!=Search aborted! +Search\ aborted.=Search aborted. Citation\ relations=Citation relations Show\ articles\ related\ by\ citation=Show articles related by citation Error\ while\ fetching\ citing\ entries\:\ %0=Error while fetching citing entries: %0 diff --git a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties index ab35614b31b..378e683942a 100644 --- a/jablib/src/main/resources/l10n/JabRef_zh_CN.properties +++ b/jablib/src/main/resources/l10n/JabRef_zh_CN.properties @@ -1297,7 +1297,7 @@ Convert\ to\ BibTeX\ format\ (e.g.,\ store\ publication\ date\ in\ year\ and\ mo Deprecated\ fields=废弃的字段 Shows\ fields\ having\ a\ successor\ in\ biblatex.=Shows fields having a successor in biblatex. -Shows\ fields\ having\ a\ successor\ in\ biblatex.\nFor\ instance,\ the\ publication\ month\ should\ be\ part\ of\ the\ date\ field.\nUse\ the\ Clean\ up\ Entries\ functionality\ to\ convert\ the\ entry\ to\ biblatex.=显示在biblatex中具有后继关系的field。\n例如,发布月份(publication month)应当是日期(date)的一部分。\n使用"清理记录"功能来转换条目为biblatex。 +Shows\ fields\ having\ a\ successor\ in\ biblatex.\nFor\ instance,\ the\ publication\ month\ should\ be\ part\ of\ the\ date\ field.\nUse\ the\ Clean\ up\ Entries\ functionality\ to\ convert\ the\ entry\ to\ biblatex.=显示在biblatex中具有后继关系的field。\n例如,发布月份(publication month)应当是日期(date)的一部分。\n使用"清理记录"功能来转换条目为biblatex。 No\ read\ status\ information=无阅读情况信息