From 274c813e7e0deaaaa68f53b0a333029483023e43 Mon Sep 17 00:00:00 2001 From: subhramit Date: Fri, 21 Nov 2025 01:03:52 +0530 Subject: [PATCH 01/42] Rework openreference PR Co-authored-by: Brandon Lau <1brandonlau@gmail.com> Signed-off-by: subhramit --- CHANGELOG.md | 2 + .../jabref/gui/actions/StandardActions.java | 2 + .../org/jabref/gui/maintable/MainTable.java | 3 +- .../jabref/gui/maintable/RightClickMenu.java | 20 +- .../maintable/SearchGoogleScholarAction.java | 51 +++ .../maintable/SearchShortScienceAction.java | 7 +- .../websearch/SearchEngineItem.java | 34 ++ .../preferences/websearch/WebSearchTab.java | 17 + .../websearch/WebSearchTabViewModel.java | 31 ++ .../preferences/websearch/WebSearchTab.fxml | 24 ++ .../logic/importer/ImporterPreferences.java | 15 +- .../preferences/JabRefCliPreferences.java | 3 +- .../logic/util/ExternalLinkCreator.java | 144 +++++++- .../main/resources/l10n/JabRef_en.properties | 7 + .../logic/util/ExternalLinkCreatorTest.java | 337 ++++++++++++++++-- 15 files changed, 641 insertions(+), 56 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java create mode 100644 jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 08cf5cc0eb8..3d06abb88b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We added the possibility to configure the email provided to unpaywall. [#14340](https://github.com/JabRef/jabref/pull/14340) - We added a "Regenerate" button for the AI chat allowing the user to make the language model reformulate its response to the previous prompt. [#12191](https://github.com/JabRef/jabref/issues/12191) - We added support for transliteration of fields to English and automatic transliteration of generated citation key. [#11377](https://github.com/JabRef/jabref/issues/11377) +- We added support for "Search Google Scholar" to quickly search for a selected entry's title in Google Scholar directly from the main table's context menu [#12268](https://github.com/JabRef/jabref/issues/12268) +- We introduced a new "Search Engine URL Template" setting in Preferences to allow users to customize their search engine URL templates [#12268](https://github.com/JabRef/jabref/issues/12268) ### Changed diff --git a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java index 0c37633c8c4..4817631df62 100644 --- a/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java +++ b/jabgui/src/main/java/org/jabref/gui/actions/StandardActions.java @@ -45,6 +45,8 @@ public enum StandardActions implements Action { EXTRACT_FILE_REFERENCES_OFFLINE(Localization.lang("Extract references from file (offline)"), IconTheme.JabRefIcons.FILE_STAR), OPEN_URL(Localization.lang("Open URL or DOI"), IconTheme.JabRefIcons.WWW, KeyBinding.OPEN_URL_OR_DOI), SEARCH_SHORTSCIENCE(Localization.lang("Search ShortScience")), + SEARCH_GOOGLE_SCHOLAR(Localization.lang("Search Google Scholar")), + SEARCH(Localization.lang("Search...")), MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0", "DOI/ISBN/..."), KeyBinding.MERGE_WITH_FETCHED_ENTRY), BATCH_MERGE_WITH_FETCHED_ENTRY(Localization.lang("Get bibliographic data from %0 (fully automated)", "DOI/ISBN/...")), ATTACH_FILE(Localization.lang("Attach file"), IconTheme.JabRefIcons.ATTACH_FILE), diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java b/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java index 7c6660d0b95..3b691c0a3ed 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/MainTable.java @@ -162,7 +162,8 @@ public MainTable(MainTableDataModel model, taskExecutor, Injector.instantiateModelOrService(JournalAbbreviationRepository.class), entryTypesManager, - importHandler)) + importHandler, + preferences.getImporterPreferences())) .withPseudoClass(MATCHING_SEARCH_AND_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_AND_GROUPS)) .withPseudoClass(MATCHING_SEARCH_NOT_GROUPS, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_SEARCH_NOT_GROUPS)) .withPseudoClass(MATCHING_GROUPS_NOT_SEARCH, entry -> entry.matchCategory().isEqualTo(MatchCategory.MATCHING_GROUPS_NOT_SEARCH)) diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java index 16d67dcaf49..e718c93ab60 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/RightClickMenu.java @@ -34,6 +34,7 @@ import org.jabref.gui.specialfields.SpecialFieldMenuItemFactory; import org.jabref.logic.citationstyle.CitationStyleOutputFormat; import org.jabref.logic.citationstyle.CitationStylePreviewLayout; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.importer.WebFetchers; import org.jabref.logic.journals.JournalAbbreviationRepository; import org.jabref.logic.l10n.Localization; @@ -60,7 +61,8 @@ public static ContextMenu create(BibEntryTableViewModel entry, TaskExecutor taskExecutor, JournalAbbreviationRepository abbreviationRepository, BibEntryTypesManager entryTypesManager, - ImportHandler importHandler) { + ImportHandler importHandler, + ImporterPreferences importerPreferences) { ActionFactory factory = new ActionFactory(); ContextMenu contextMenu = new ContextMenu(); @@ -99,7 +101,8 @@ public static ContextMenu create(BibEntryTableViewModel entry, extractFileReferencesOffline, factory.createMenuItem(StandardActions.OPEN_URL, new OpenUrlAction(dialogService, stateManager, preferences)), - factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences)), + + createSearchSubMenu(factory, dialogService, stateManager, preferences, importerPreferences), new SeparatorMenuItem(), @@ -237,4 +240,17 @@ private static Menu createSendSubMenu(ActionFactory factory, return sendMenu; } + + private static Menu createSearchSubMenu(ActionFactory factory, + DialogService dialogService, + StateManager stateManager, + GuiPreferences preferences, + ImporterPreferences importerPreferences) { + Menu searchMenu = factory.createMenu(StandardActions.SEARCH); + searchMenu.getItems().addAll( + factory.createMenuItem(StandardActions.SEARCH_SHORTSCIENCE, new SearchShortScienceAction(dialogService, stateManager, preferences, importerPreferences)), + factory.createMenuItem(StandardActions.SEARCH_GOOGLE_SCHOLAR, new SearchGoogleScholarAction(dialogService, stateManager, preferences, importerPreferences)) + ); + return searchMenu; + } } diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java new file mode 100644 index 00000000000..aa49fa9332f --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchGoogleScholarAction.java @@ -0,0 +1,51 @@ +package org.jabref.gui.maintable; + +import java.io.IOException; +import java.util.List; + +import javafx.beans.binding.BooleanExpression; + +import org.jabref.gui.DialogService; +import org.jabref.gui.StateManager; +import org.jabref.gui.actions.SimpleCommand; +import org.jabref.gui.desktop.os.NativeDesktop; +import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.importer.ImporterPreferences; +import org.jabref.logic.l10n.Localization; +import org.jabref.logic.util.ExternalLinkCreator; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.field.StandardField; + +import static org.jabref.gui.actions.ActionHelper.isFieldSetForSelectedEntry; +import static org.jabref.gui.actions.ActionHelper.needsEntriesSelected; + +public class SearchGoogleScholarAction extends SimpleCommand { + private final DialogService dialogService; + private final StateManager stateManager; + private final GuiPreferences preferences; + private final ExternalLinkCreator externalLinkCreator; + + public SearchGoogleScholarAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, ImporterPreferences importerPreferences) { + this.dialogService = dialogService; + this.stateManager = stateManager; + this.preferences = preferences; + this.externalLinkCreator = new ExternalLinkCreator(importerPreferences); + + BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager); + this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet)); + } + + @Override + public void execute() { + stateManager.getActiveDatabase().ifPresent(databaseContext -> { + final List bibEntries = stateManager.getSelectedEntries(); + externalLinkCreator.getGoogleScholarSearchURL(bibEntries.getFirst()).ifPresent(url -> { + try { + NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst()); + } catch (IOException ex) { + dialogService.showErrorDialogAndWait(Localization.lang("Unable to open Google Scholar."), ex); + } + }); + }); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java b/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java index 4b6a251c4ff..a8220e9765d 100644 --- a/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java +++ b/jabgui/src/main/java/org/jabref/gui/maintable/SearchShortScienceAction.java @@ -10,6 +10,7 @@ import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.desktop.os.NativeDesktop; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.l10n.Localization; import org.jabref.logic.util.ExternalLinkCreator; import org.jabref.model.entry.BibEntry; @@ -22,11 +23,13 @@ public class SearchShortScienceAction extends SimpleCommand { private final DialogService dialogService; private final StateManager stateManager; private final GuiPreferences preferences; + private final ExternalLinkCreator externalLinkCreator; - public SearchShortScienceAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences) { + public SearchShortScienceAction(DialogService dialogService, StateManager stateManager, GuiPreferences preferences, ImporterPreferences importerPreferences) { this.dialogService = dialogService; this.stateManager = stateManager; this.preferences = preferences; + this.externalLinkCreator = new ExternalLinkCreator(importerPreferences); BooleanExpression fieldIsSet = isFieldSetForSelectedEntry(StandardField.TITLE, stateManager); this.executable.bind(needsEntriesSelected(1, stateManager).and(fieldIsSet)); @@ -41,7 +44,7 @@ public void execute() { dialogService.notify(Localization.lang("This operation requires exactly one item to be selected.")); return; } - ExternalLinkCreator.getShortScienceSearchURL(bibEntries.getFirst()).ifPresent(url -> { + externalLinkCreator.getShortScienceSearchURL(bibEntries.getFirst()).ifPresent(url -> { try { NativeDesktop.openExternalViewer(databaseContext, preferences, url, StandardField.URL, dialogService, bibEntries.getFirst()); } catch (IOException ex) { diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java new file mode 100644 index 00000000000..c10d3f79f0b --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/SearchEngineItem.java @@ -0,0 +1,34 @@ +package org.jabref.gui.preferences.websearch; + +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; + +public class SearchEngineItem { + private final StringProperty name; + private final StringProperty urlTemplate; + + public SearchEngineItem(String name, String urlTemplate) { + this.name = new SimpleStringProperty(name); + this.urlTemplate = new SimpleStringProperty(urlTemplate); + } + + public StringProperty nameProperty() { + return name; + } + + public StringProperty urlTemplateProperty() { + return urlTemplate; + } + + public String getName() { + return name.get(); + } + + public String getUrlTemplate() { + return urlTemplate.get(); + } + + public void setUrlTemplate(String urlTemplate) { + this.urlTemplate.set(urlTemplate); + } +} diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java index 2ff5b80a836..f8a65396df0 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTab.java @@ -12,7 +12,10 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; import javafx.scene.control.TextField; +import javafx.scene.control.cell.TextFieldTableCell; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; @@ -45,6 +48,10 @@ public class WebSearchTab extends AbstractPreferenceTabView searchEngineTable; + @FXML private TableColumn searchEngineName; + @FXML private TableColumn searchEngineUrlTemplate; + @FXML private VBox fetchersContainer; private final ReadOnlyBooleanProperty refAiEnabled; @@ -77,6 +84,16 @@ public String getTabName() { public void initialize() { this.viewModel = new WebSearchTabViewModel(preferences, refAiEnabled, taskExecutor); + searchEngineName.setCellValueFactory(param -> param.getValue().nameProperty()); + searchEngineName.setCellFactory(TextFieldTableCell.forTableColumn()); + searchEngineName.setEditable(false); + + searchEngineUrlTemplate.setCellValueFactory(param -> param.getValue().urlTemplateProperty()); + searchEngineUrlTemplate.setCellFactory(TextFieldTableCell.forTableColumn()); + searchEngineUrlTemplate.setEditable(true); + + searchEngineTable.setItems(viewModel.getSearchEngines()); + enableWebSearch.selectedProperty().bindBidirectional(viewModel.enableWebSearchProperty()); warnAboutDuplicatesOnImport.selectedProperty().bindBidirectional(viewModel.warnAboutDuplicatesOnImportProperty()); downloadLinkedOnlineFiles.selectedProperty().bindBidirectional(viewModel.shouldDownloadLinkedOnlineFiles()); diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java index ac4e5ddd7af..32e0a315ca1 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/WebSearchTabViewModel.java @@ -5,6 +5,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.function.Consumer; @@ -72,6 +73,8 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel { private final BooleanProperty apikeyPersistProperty = new SimpleBooleanProperty(); private final BooleanProperty apikeyPersistAvailableProperty = new SimpleBooleanProperty(); + private final ObservableList searchEngines = FXCollections.observableArrayList(); + private final CliPreferences preferences; private final DOIPreferences doiPreferences; private final GrobidPreferences grobidPreferences; @@ -96,6 +99,7 @@ public WebSearchTabViewModel(CliPreferences preferences, ReadOnlyBooleanProperty this.refAiEnabled = refAiEnabled; setupPlainCitationParsers(preferences); + setupSearchEngines(); } private void setupPlainCitationParsers(CliPreferences preferences) { @@ -136,6 +140,14 @@ private void setupPlainCitationParsers(CliPreferences preferences) { }); } + private void setupSearchEngines() { + // add default search engines + searchEngines.addAll( + new SearchEngineItem("Google Scholar", "https://scholar.google.com/scholar?q={title}"), + new SearchEngineItem("Short Science", "https://www.shortscience.org/internalsearch?q={title}") + ); + } + @Override public void setValues() { enableWebSearchProperty.setValue(importerPreferences.areImporterEnabled()); @@ -186,6 +198,13 @@ public void setValues() { apikeyPersistAvailableProperty.setValue(OS.isKeyringAvailable()); apikeyPersistProperty.setValue(preferences.getImporterPreferences().shouldPersistCustomKeys()); + + // Load custom URL templates from preferences if they exist + Map savedTemplates = preferences.getImporterPreferences().getSearchEngineUrlTemplates(); + if (!savedTemplates.isEmpty()) { + searchEngines.clear(); + savedTemplates.forEach((name, url) -> searchEngines.add(new SearchEngineItem(name, url))); + } } @Override @@ -225,6 +244,14 @@ public void storeSettings() { if (apikeyPersistAvailableProperty.get()) { preferences.getImporterPreferences().getApiKeys().addAll(apiKeysToStore); } + + // Save custom URL templates to preferences + Map templates = searchEngines.stream() + .collect(Collectors.toMap( + SearchEngineItem::getName, + SearchEngineItem::getUrlTemplate + )); + preferences.getImporterPreferences().setSearchEngineUrlTemplates(templates); } public BooleanProperty enableWebSearchProperty() { @@ -291,6 +318,10 @@ public IntegerProperty citationsRelationsStoreTTLProperty() { return citationsRelationStoreTTL; } + public ObservableList getSearchEngines() { + return searchEngines; + } + public void checkApiKey(FetcherViewModel fetcherViewModel, String apiKey, Consumer onFinished) { Callable tester = () -> { WebFetcher webFetcher = fetcherViewModel.getFetcher(); diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml index cf9d45183de..ba3e945bba4 100644 --- a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/WebSearchTab.fxml @@ -3,6 +3,8 @@ + + @@ -47,6 +49,28 @@ +