diff --git a/CHANGELOG.md b/CHANGELOG.md index 5027640ec86..ffc9eedfcf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We improved JabRef's internal document viewer. It now allows text section, searching and highlighting of search terms and page rotation [#13193](https://github.com/JabRef/jabref/pull/13193). - When importing a PDF, there is no empty entry column shown in the multi merge dialog. [#13132](https://github.com/JabRef/jabref/issues/13132) - We added a progress dialog to the "Check consistency" action and progress output to the corresponding cli command. [#12487](https://github.com/JabRef/jabref/issues/12487) +- We added Enhanced "Citation Relations" feature: "Look up a DOI and try again." is now a clickable hyperlink that triggers a DOI lookup. The link shows result states ("Looking up DOI...", "No DOI found", "List of citations") as appropriate. ### Fixed @@ -1568,6 +1569,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added the ability to use negation in export filter layouts. [#5138](https://github.com/JabRef/jabref/pull/5138) - Focus on Name Area instead of 'OK' button whenever user presses 'Add subgroup'. [#6307](https://github.com/JabRef/jabref/issues/6307) - We changed the behavior of merging that the entry which has "smaller" bibkey will be selected. [#7395](https://github.com/JabRef/jabref/issues/7395) +- we added a new ### Fixed 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 3cffd4e906f..26697e46ab1 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; @@ -30,6 +31,8 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; import org.jabref.gui.DialogService; import org.jabref.gui.LibraryTab; @@ -51,6 +54,9 @@ import org.jabref.logic.bibtex.FieldWriter; import org.jabref.logic.database.DuplicateCheck; import org.jabref.logic.exporter.BibWriter; +import org.jabref.logic.importer.FetcherClientException; +import org.jabref.logic.importer.FetcherServerException; +import org.jabref.logic.importer.fetcher.CrossRef; import org.jabref.logic.importer.fetcher.citation.CitationFetcher; import org.jabref.logic.importer.fetcher.citation.semanticscholar.SemanticScholarFetcher; import org.jabref.logic.l10n.Localization; @@ -97,13 +103,7 @@ public class CitationRelationsTab extends EntryEditorTab { private final StateManager stateManager; private final UndoManager undoManager; - public CitationRelationsTab(DialogService dialogService, - UndoManager undoManager, - StateManager stateManager, - FileUpdateMonitor fileUpdateMonitor, - GuiPreferences preferences, - TaskExecutor taskExecutor, - BibEntryTypesManager bibEntryTypesManager) { + public CitationRelationsTab(DialogService dialogService, UndoManager undoManager, StateManager stateManager, FileUpdateMonitor fileUpdateMonitor, GuiPreferences preferences, TaskExecutor taskExecutor, BibEntryTypesManager bibEntryTypesManager) { this.dialogService = dialogService; this.preferences = preferences; this.taskExecutor = taskExecutor; @@ -115,8 +115,7 @@ public CitationRelationsTab(DialogService dialogService, this.entryTypesManager = bibEntryTypesManager; this.duplicateCheck = new DuplicateCheck(entryTypesManager); - this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferences.getImporterPreferences()), - new BibEntryRelationsCache()); + this.bibEntryRelationsRepository = new BibEntryRelationsRepository(new SemanticScholarFetcher(preferences.getImporterPreferences()), new BibEntryRelationsCache()); citationsRelationsTabViewModel = new CitationsRelationsTabViewModel(preferences, undoManager, stateManager, dialogService, fileUpdateMonitor, taskExecutor); } @@ -191,29 +190,18 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { citingVBox.getChildren().addAll(citingHBox, citingListView); citedByVBox.getChildren().addAll(citedByHBox, citedByListView); - refreshCitingButton.setOnMouseClicked(event -> searchForRelations( - entry, - citingListView, - abortCitingButton, - refreshCitingButton, - CitationFetcher.SearchType.CITES, - importCitingButton, - citingProgress, - true)); + refreshCitingButton.setOnMouseClicked(event -> searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, true)); - refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, - refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); + refreshCitedByButton.setOnMouseClicked(event -> searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, true)); // 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, false); + searchForRelations(entry, citingListView, abortCitingButton, refreshCitingButton, CitationFetcher.SearchType.CITES, importCitingButton, citingProgress, false); - searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, - CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); + searchForRelations(entry, citedByListView, abortCitedButton, refreshCitedByButton, CitationFetcher.SearchType.CITED_BY, importCitedByButton, citedByProgress, false); return container; } @@ -225,86 +213,82 @@ private SplitPane getPaneAndStartSearch(BibEntry entry) { */ private void styleFetchedListView(CheckListView listView) { PseudoClass entrySelected = PseudoClass.getPseudoClass("selected"); - new ViewModelListCellFactory() - .withGraphic(entry -> { - - HBox separator = new HBox(); - HBox.setHgrow(separator, Priority.SOMETIMES); - Node entryNode = BibEntryView.getEntryNode(entry.entry()); - HBox.setHgrow(entryNode, Priority.ALWAYS); - HBox hContainer = new HBox(); - hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); - - VBox vContainer = new VBox(); - - if (entry.isLocal()) { - hContainer.getStyleClass().add("duplicate-entry"); - Button jumpTo = IconTheme.JabRefIcons.LINK.asButton(); - jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in library"))); - jumpTo.getStyleClass().add("addEntryButton"); - jumpTo.setOnMouseClicked(event -> jumpToEntry(entry)); - hContainer.setOnMouseClicked(event -> { - if (event.getClickCount() == 2) { - jumpToEntry(entry); - } - }); - vContainer.getChildren().add(jumpTo); - - Button compareButton = IconTheme.JabRefIcons.MERGE_ENTRIES.asButton(); - compareButton.setTooltip(new Tooltip(Localization.lang("Compare with existing entry"))); - compareButton.setOnMouseClicked(event -> openPossibleDuplicateEntriesWindow(entry, listView)); - vContainer.getChildren().add(compareButton); + new ViewModelListCellFactory().withGraphic(entry -> { + + HBox separator = new HBox(); + HBox.setHgrow(separator, Priority.SOMETIMES); + Node entryNode = BibEntryView.getEntryNode(entry.entry()); + HBox.setHgrow(entryNode, Priority.ALWAYS); + HBox hContainer = new HBox(); + hContainer.prefWidthProperty().bind(listView.widthProperty().subtract(25)); + + VBox vContainer = new VBox(); + + if (entry.isLocal()) { + hContainer.getStyleClass().add("duplicate-entry"); + Button jumpTo = IconTheme.JabRefIcons.LINK.asButton(); + jumpTo.setTooltip(new Tooltip(Localization.lang("Jump to entry in library"))); + jumpTo.getStyleClass().add("addEntryButton"); + jumpTo.setOnMouseClicked(event -> jumpToEntry(entry)); + hContainer.setOnMouseClicked(event -> { + if (event.getClickCount() == 2) { + jumpToEntry(entry); + } + }); + vContainer.getChildren().add(jumpTo); + + Button compareButton = IconTheme.JabRefIcons.MERGE_ENTRIES.asButton(); + compareButton.setTooltip(new Tooltip(Localization.lang("Compare with existing entry"))); + compareButton.setOnMouseClicked(event -> openPossibleDuplicateEntriesWindow(entry, listView)); + vContainer.getChildren().add(compareButton); + } else { + ToggleButton addToggle = IconTheme.JabRefIcons.ADD.asToggleButton(); + addToggle.setTooltip(new Tooltip(Localization.lang("Select entry"))); + EasyBind.subscribe(addToggle.selectedProperty(), selected -> { + if (selected) { + addToggle.setGraphic(IconTheme.JabRefIcons.ADD_FILLED.withColor(IconTheme.SELECTED_COLOR).getGraphicNode()); } else { - ToggleButton addToggle = IconTheme.JabRefIcons.ADD.asToggleButton(); - addToggle.setTooltip(new Tooltip(Localization.lang("Select entry"))); - EasyBind.subscribe(addToggle.selectedProperty(), selected -> { - if (selected) { - addToggle.setGraphic(IconTheme.JabRefIcons.ADD_FILLED.withColor(IconTheme.SELECTED_COLOR).getGraphicNode()); - } else { - addToggle.setGraphic(IconTheme.JabRefIcons.ADD.getGraphicNode()); - } - }); - addToggle.getStyleClass().add("addEntryButton"); - addToggle.selectedProperty().bindBidirectional(listView.getItemBooleanProperty(entry)); - vContainer.getChildren().add(addToggle); + addToggle.setGraphic(IconTheme.JabRefIcons.ADD.getGraphicNode()); } + }); + addToggle.getStyleClass().add("addEntryButton"); + addToggle.selectedProperty().bindBidirectional(listView.getItemBooleanProperty(entry)); + vContainer.getChildren().add(addToggle); + } - if (entry.entry().getDOI().isPresent() || entry.entry().getField(StandardField.URL).isPresent()) { - Button openWeb = IconTheme.JabRefIcons.OPEN_LINK.asButton(); - openWeb.setTooltip(new Tooltip(Localization.lang("Open URL or DOI"))); - openWeb.setOnMouseClicked(event -> { - String url = entry.entry().getDOI().flatMap(DOI::getExternalURI).map(URI::toString) - .or(() -> entry.entry().getField(StandardField.URL)).orElse(""); - if (StringUtil.isNullOrEmpty(url)) { - return; - } - try { - NativeDesktop.openBrowser(url, preferences.getExternalApplicationsPreferences()); - } catch (IOException ex) { - dialogService.notify(Localization.lang("Unable to open link.")); - } - }); - vContainer.getChildren().addLast(openWeb); + if (entry.entry().getDOI().isPresent() || entry.entry().getField(StandardField.URL).isPresent()) { + Button openWeb = IconTheme.JabRefIcons.OPEN_LINK.asButton(); + openWeb.setTooltip(new Tooltip(Localization.lang("Open URL or DOI"))); + openWeb.setOnMouseClicked(event -> { + String url = entry.entry().getDOI().flatMap(DOI::getExternalURI).map(URI::toString).or(() -> entry.entry().getField(StandardField.URL)).orElse(""); + if (StringUtil.isNullOrEmpty(url)) { + return; } + try { + NativeDesktop.openBrowser(url, preferences.getExternalApplicationsPreferences()); + } catch ( + IOException ex) { + dialogService.notify(Localization.lang("Unable to open link.")); + } + }); + vContainer.getChildren().addLast(openWeb); + } - Button showEntrySource = IconTheme.JabRefIcons.SOURCE.asButton(); - showEntrySource.setTooltip(new Tooltip(Localization.lang("%0 source", "BibTeX"))); - showEntrySource.setOnMouseClicked(event -> showEntrySourceDialog(entry.entry())); + Button showEntrySource = IconTheme.JabRefIcons.SOURCE.asButton(); + showEntrySource.setTooltip(new Tooltip(Localization.lang("%0 source", "BibTeX"))); + showEntrySource.setOnMouseClicked(event -> showEntrySourceDialog(entry.entry())); - vContainer.getChildren().addLast(showEntrySource); + vContainer.getChildren().addLast(showEntrySource); - hContainer.getChildren().addAll(entryNode, separator, vContainer); - hContainer.getStyleClass().add("entry-container"); + hContainer.getChildren().addAll(entryNode, separator, vContainer); + hContainer.getStyleClass().add("entry-container"); - return hContainer; - }) - .withOnMouseClickedEvent((ee, event) -> { - if (!ee.isLocal()) { - listView.getCheckModel().toggleCheckState(ee); - } - }) - .withPseudoClass(entrySelected, listView::getItemBooleanProperty) - .install(listView); + return hContainer; + }).withOnMouseClickedEvent((ee, event) -> { + if (!ee.isLocal()) { + listView.getCheckModel().toggleCheckState(ee); + } + }).withPseudoClass(entrySelected, listView::getItemBooleanProperty).install(listView); listView.setSelectionModel(new NoSelectionModel<>()); } @@ -329,10 +313,10 @@ private String getSourceString(BibEntry entry, BibDatabaseMode type, FieldPrefer private void showEntrySourceDialog(BibEntry entry) { CodeArea ca = new CodeArea(); try { - BibDatabaseMode mode = stateManager.getActiveDatabase().map(BibDatabaseContext::getMode) - .orElse(BibDatabaseMode.BIBLATEX); + BibDatabaseMode mode = stateManager.getActiveDatabase().map(BibDatabaseContext::getMode).orElse(BibDatabaseMode.BIBLATEX); ca.appendText(getSourceString(entry, mode, preferences.getFieldPreferences(), this.entryTypesManager)); - } catch (IOException e) { + } catch ( + IOException e) { LOGGER.warn("Incorrect entry, could not load source:", e); return; } @@ -405,15 +389,30 @@ protected void bindToEntry(BibEntry entry) { * @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, boolean shouldRefresh) { + private void searchForRelations(BibEntry entry, CheckListView listView, Button abortButton, Button refreshButton, CitationFetcher.SearchType searchType, Button importButton, ProgressIndicator progress, boolean shouldRefresh) { 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."))); + + Text doiLookUpText = new Text(Localization.lang("The selected entry doesn't have a DOI linked to it. ")); + Hyperlink doiLookUpHyperLink = new Hyperlink(Localization.lang("Lookup a DOI and try again.")); + TextFlow doiLookUpTextFlow = new TextFlow(doiLookUpText, doiLookUpHyperLink); + Label placeHolder = new Label("", doiLookUpTextFlow); + doiLookUpHyperLink.setOnAction(e -> { + CrossRef doiFetcher = new CrossRef(); + BackgroundTask.wrap(() -> doiFetcher.findIdentifier(entry)) + .onRunning(() -> listView.setPlaceholder(new Label("Looking up DOI..."))) + .onSuccess(doiIdentifier -> { + if (doiIdentifier.isPresent()) { + entry.setField(StandardField.DOI, doiIdentifier.get().asString()); + searchForRelations(entry, listView, abortButton, refreshButton, searchType, importButton, progress, shouldRefresh); + } else { + dialogService.notify("No DOI found"); + } + }).onFailure((exception) -> handleIdentifierFetchingError(exception, doiFetcher)).executeWith(taskExecutor); + }); + listView.setPlaceholder(placeHolder); return; } @@ -447,39 +446,34 @@ private void searchForRelations(BibEntry entry, CheckListView 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); + task.onRunning(() -> 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 void handleIdentifierFetchingError(Exception exception, CrossRef fetcher){ + LOGGER.error("Error while fetching identifier", exception); + if (exception instanceof FetcherClientException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("No data was found for the identifier")); + } else if (exception instanceof FetcherServerException) { + dialogService.showInformationDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Server not available")); + } else if (exception.getCause() != null) { + dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Error occurred %0", exception.getCause().getMessage())); + } else { + dialogService.showWarningDialogAndWait(Localization.lang("Look up %0", fetcher.getName()), Localization.lang("Error occurred %0", exception.getCause().getMessage())); + } } - private void onSearchForRelationsSucceed(BibEntry entry, CheckListView listView, - Button abortButton, Button refreshButton, - CitationFetcher.SearchType searchType, Button importButton, - ProgressIndicator progress, List fetchedList, - ObservableList observableList) { + 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() - ); + 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(); @@ -493,8 +487,7 @@ private void onSearchForRelationsSucceed(BibEntry entry, CheckListView> task) { + private void prepareToSearchForRelations(Button abortButton, Button refreshButton, Button importButton, ProgressIndicator progress, BackgroundTask> task) { showNodes(abortButton, progress); hideNodes(refreshButton, importButton); @@ -532,7 +525,7 @@ private void importEntries(List entriesToImport, CitationF * Function to open possible duplicate entries window to compare duplicate entries * * @param citationRelationItem duplicate in the citation relations tab - * @param listView CheckListView to display citations + * @param listView CheckListView to display citations */ private void openPossibleDuplicateEntriesWindow(CitationRelationItem citationRelationItem, CheckListView listView) { BibEntry libraryEntry = citationRelationItem.localEntry();