diff --git a/CHANGELOG.md b/CHANGELOG.md index 24722167fb7..a1b7599d741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,8 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - The Welcome tab now has a responsive layout. [#12664](https://github.com/JabRef/jabref/issues/12664) - We introduced a donation prompt in the Welcome tab. [#12664](https://github.com/JabRef/jabref/issues/12664) - We changed to syntax for the websearch to the one of the main search bar. [#13607](https://github.com/JabRef/jabref/issues/13607) +- We improved the for the web search tab in the preferences dialog [#13791](https://github.com/JabRef/jabref/pull/13791) +- We improved the event viewer for debugging [#13783](https://github.com/JabRef/jabref/pull/13783). ### Fixed @@ -118,6 +120,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv - We fixed an issue where "Copy to" was enabled even if no other library was opened. [#13280](https://github.com/JabRef/jabref/pull/13280) - We fixed an issue where the groups were still displayed after closing all libraries. [#13382](https://github.com/JabRef/jabref/issues/13382) - Enhanced field selection logic in the Merge Entries dialog when fetching from DOI to prefer valid years and entry types. [#12549](https://github.com/JabRef/jabref/issues/12549) +- We fixed an issue where theme or font size are not respected for all dialogs [#13558](https://github.com/JabRef/jabref/issues/13558) - We removed unnecessary spacing and margin in the AutomaticFieldEditor. [#13792](https://github.com/JabRef/jabref/pull/13792) ### Removed diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/websearch/ApiKeyDialog.java b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/ApiKeyDialog.java new file mode 100644 index 00000000000..c3a1e83f2f8 --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/preferences/websearch/ApiKeyDialog.java @@ -0,0 +1,110 @@ +package org.jabref.gui.preferences.websearch; + +import javafx.animation.Animation; +import javafx.animation.Interpolator; +import javafx.animation.RotateTransition; +import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.ButtonType; +import javafx.scene.control.CheckBox; +import javafx.scene.control.TextField; +import javafx.util.Duration; + +import org.jabref.gui.FXDialog; +import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.preferences.websearch.WebSearchTabViewModel.FetcherViewModel; +import org.jabref.gui.util.DelayedExecution; +import org.jabref.logic.l10n.Localization; + +import com.airhacks.afterburner.views.ViewLoader; + +public class ApiKeyDialog extends FXDialog { + public static final Duration STATUS_DURATION = Duration.seconds(5); + + @FXML private TextField apiKeyField; + @FXML private CheckBox persistApiKeysCheckBox; + @FXML private Button testButton; + + private final WebSearchTabViewModel viewModel; + private final FetcherViewModel fetcherViewModel; + private final BooleanProperty apiKeyValid = new SimpleBooleanProperty(); + private RotateTransition rotation; + + public ApiKeyDialog(WebSearchTabViewModel viewModel, FetcherViewModel fetcherViewModel) { + super(AlertType.NONE, Localization.lang("API Key for %0", fetcherViewModel.getName())); + this.viewModel = viewModel; + this.fetcherViewModel = fetcherViewModel; + + ViewLoader.view(this) + .load() + .setAsDialogPane(this); + + setResultConverter(this::convertResult); + } + + @FXML + private void initialize() { + apiKeyField.setText(fetcherViewModel.getApiKey()); + + apiKeyValid.set(apiKeyField.getText().isEmpty() || fetcherViewModel.shouldUseCustomApiKey()); + + Button okButton = (Button) getDialogPane().lookupButton(ButtonType.OK); + if (okButton != null) { + okButton.disableProperty().bind(apiKeyValid.not()); + } + + apiKeyField.textProperty().addListener((_, _, newValue) -> { + apiKeyValid.set(newValue.isEmpty()); + testButton.setGraphic(null); + testButton.setText(Localization.lang("Test connection")); + testButton.getStyleClass().removeAll("success", "error"); + }); + + persistApiKeysCheckBox.selectedProperty().bindBidirectional(viewModel.getApikeyPersistProperty()); + persistApiKeysCheckBox.disableProperty().bind(viewModel.apiKeyPersistAvailable().not()); + + rotation = new RotateTransition(Duration.seconds(1), IconTheme.JabRefIcons.REFRESH.getGraphicNode()); + rotation.setByAngle(360); + rotation.setCycleCount(Animation.INDEFINITE); + rotation.setInterpolator(Interpolator.LINEAR); + } + + @FXML + private void testApiKey() { + testButton.setDisable(true); + testButton.setText(Localization.lang("Testing...")); + testButton.setGraphic(rotation.getNode()); + rotation.play(); + viewModel.checkApiKey(fetcherViewModel, apiKeyField.getText(), this::handleTestResult); + } + + private void handleTestResult(boolean success) { + Platform.runLater(() -> { + rotation.stop(); + testButton.setDisable(false); + + testButton.setGraphic(success ? IconTheme.JabRefIcons.SUCCESS.getGraphicNode() : IconTheme.JabRefIcons.ERROR.getGraphicNode()); + testButton.getStyleClass().removeAll("success", "error"); + testButton.getStyleClass().add(success ? "success" : "error"); + testButton.setText(success ? Localization.lang("API Key Valid") : Localization.lang("API Key Invalid")); + apiKeyValid.set(success); + + new DelayedExecution(STATUS_DURATION, () -> { + testButton.setGraphic(null); + testButton.setText(Localization.lang("Test connection")); + }).start(); + }); + } + + private ButtonType convertResult(ButtonType button) { + if (button == ButtonType.OK) { + String apiKey = apiKeyField.getText().trim(); + fetcherViewModel.apiKeyProperty().set(apiKey); + fetcherViewModel.useCustomApiKeyProperty().set(!apiKey.isEmpty()); + } + return button; + } +} 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 93ff6b414dc..2fe53959136 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 @@ -1,35 +1,34 @@ package org.jabref.gui.preferences.websearch; +import java.util.Optional; + import javafx.beans.InvalidationListener; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; -import javafx.scene.control.SplitPane; -import javafx.scene.control.TableColumn; -import javafx.scene.control.TableView; +import javafx.scene.control.Label; import javafx.scene.control.TextField; -import javafx.scene.control.Tooltip; -import javafx.scene.control.cell.CheckBoxTableCell; -import javafx.scene.control.cell.TextFieldTableCell; -import javafx.scene.input.MouseButton; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; import org.jabref.gui.preferences.AbstractPreferenceTabView; import org.jabref.gui.preferences.PreferencesTab; -import org.jabref.gui.slr.StudyCatalogItem; import org.jabref.gui.util.ViewModelListCellFactory; -import org.jabref.gui.util.ViewModelTableRowFactory; +import org.jabref.gui.util.component.HelpButton; +import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.plaincitation.PlainCitationParserChoice; import org.jabref.logic.l10n.Localization; -import org.jabref.logic.preferences.FetcherApiKey; import org.jabref.model.strings.StringUtil; import com.airhacks.afterburner.views.ViewLoader; -import com.tobiasdiez.easybind.EasyBind; public class WebSearchTab extends AbstractPreferenceTabView implements PreferencesTab { - @FXML private CheckBox enableWebSearch; @FXML private CheckBox warnAboutDuplicatesOnImport; @FXML private CheckBox downloadLinkedOnlineFiles; @@ -45,17 +44,7 @@ public class WebSearchTab extends AbstractPreferenceTabView apiKeySelectorTable; - @FXML private TableColumn apiKeyName; - @FXML private TableColumn customApiKey; - @FXML private TableColumn useCustomApiKey; - @FXML private Button testCustomApiKey; - - @FXML private CheckBox persistApiKeys; - @FXML private SplitPane persistentTooltipWrapper; // The disabled persistApiKeys control does not show tooltips - @FXML private TableView catalogTable; - @FXML private TableColumn catalogEnabledColumn; - @FXML private TableColumn catalogColumn; + @FXML private VBox fetchersContainer; private final ReadOnlyBooleanProperty refAiEnabled; @@ -73,7 +62,7 @@ public String getTabName() { } public void initialize() { - this.viewModel = new WebSearchTabViewModel(preferences, dialogService, refAiEnabled); + this.viewModel = new WebSearchTabViewModel(preferences, refAiEnabled, taskExecutor); enableWebSearch.selectedProperty().bindBidirectional(viewModel.enableWebSearchProperty()); warnAboutDuplicatesOnImport.selectedProperty().bindBidirectional(viewModel.warnAboutDuplicatesOnImportProperty()); @@ -117,86 +106,47 @@ public void initialize() { useCustomDOIName.textProperty().bindBidirectional(viewModel.useCustomDOINameProperty()); useCustomDOIName.disableProperty().bind(useCustomDOI.selectedProperty().not()); - new ViewModelTableRowFactory() - .withOnMouseClickedEvent((entry, event) -> { - if (event.getButton() == MouseButton.PRIMARY) { - entry.setEnabled(!entry.isEnabled()); - } - }) - .install(catalogTable); - - catalogColumn.setReorderable(false); - catalogColumn.setCellFactory(TextFieldTableCell.forTableColumn()); - - catalogEnabledColumn.setResizable(false); - catalogEnabledColumn.setReorderable(false); - catalogEnabledColumn.setCellFactory(CheckBoxTableCell.forTableColumn(catalogEnabledColumn)); - catalogEnabledColumn.setCellValueFactory(param -> param.getValue().enabledProperty()); - - catalogColumn.setEditable(false); - catalogColumn.setCellValueFactory(param -> param.getValue().nameProperty()); - catalogTable.setItems(viewModel.getCatalogs()); - - testCustomApiKey.setDisable(true); - - new ViewModelTableRowFactory() - .install(apiKeySelectorTable); - - apiKeySelectorTable.getSelectionModel().selectedItemProperty().addListener((_, oldValue, newValue) -> { - if (oldValue != null) { - updateFetcherApiKey(oldValue); - } - if (newValue != null) { - viewModel.selectedApiKeyProperty().setValue(newValue); - testCustomApiKey.disableProperty().bind(newValue.useProperty().not()); - } - }); - - apiKeyName.setCellValueFactory(param -> param.getValue().nameProperty()); - apiKeyName.setCellFactory(TextFieldTableCell.forTableColumn()); - apiKeyName.setReorderable(false); - apiKeyName.setEditable(false); - - customApiKey.setCellValueFactory(param -> param.getValue().keyProperty()); - customApiKey.setCellFactory(TextFieldTableCell.forTableColumn()); - customApiKey.setReorderable(false); - customApiKey.setResizable(true); - customApiKey.setEditable(true); - - useCustomApiKey.setCellValueFactory(param -> param.getValue().useProperty()); - useCustomApiKey.setCellFactory(CheckBoxTableCell.forTableColumn(useCustomApiKey)); - useCustomApiKey.setEditable(true); - useCustomApiKey.setResizable(true); - useCustomApiKey.setReorderable(false); - - persistApiKeys.selectedProperty().bindBidirectional(viewModel.getApikeyPersistProperty()); - persistApiKeys.disableProperty().bind(viewModel.apiKeyPersistAvailable().not()); - EasyBind.subscribe(viewModel.apiKeyPersistAvailable(), available -> { - if (!available) { - persistentTooltipWrapper.setTooltip(new Tooltip(Localization.lang("Credential store not available."))); - } else { - persistentTooltipWrapper.setTooltip(null); - } - }); - - apiKeySelectorTable.setItems(viewModel.fetcherApiKeys()); - - // Content is set later - viewModel.fetcherApiKeys().addListener((InvalidationListener) _ -> { - if (!apiKeySelectorTable.getItems().isEmpty()) { - apiKeySelectorTable.getSelectionModel().selectFirst(); - } - }); + InvalidationListener listener = _ -> fetchersContainer + .getChildren() + .setAll(viewModel.getFetchers() + .stream() + .map(this::createFetcherNode).toList()); + viewModel.getFetchers().addListener(listener); } - private void updateFetcherApiKey(FetcherApiKey apiKey) { - if (apiKey != null) { - apiKey.setKey(customApiKey.getCellData(apiKey).trim()); + private Node createFetcherNode(WebSearchTabViewModel.FetcherViewModel item) { + HBox container = new HBox(); + container.getStyleClass().add("fetcher-list-cell"); + container.setAlignment(Pos.CENTER_LEFT); + + CheckBox enabledCheckBox = new CheckBox(); + enabledCheckBox.selectedProperty().bindBidirectional(item.enabledProperty()); + + Label nameLabel = new Label(item.getName()); + nameLabel.getStyleClass().add("fetcher-name"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + HelpButton helpButton = new HelpButton(); + Optional helpFile = item.getFetcher().getHelpPage(); + if (helpFile.isPresent() && !helpFile.get().getPageName().isEmpty()) { + helpButton.setHelpFile(helpFile.get(), dialogService, preferences.getExternalApplicationsPreferences()); + helpButton.setVisible(true); + } else { + helpButton.setVisible(false); } + + Button configureButton = new Button(Localization.lang("Configure API key")); + configureButton.getStyleClass().add("configure-button"); + configureButton.setOnAction(_ -> showApiKeyDialog(item)); + configureButton.setVisible(item.isCustomizable()); + + container.getChildren().addAll(enabledCheckBox, nameLabel, spacer, helpButton, configureButton); + return container; } - @FXML - void checkCustomApiKey() { - viewModel.checkCustomApiKey(); + private void showApiKeyDialog(WebSearchTabViewModel.FetcherViewModel fetcherViewModel) { + dialogService.showCustomDialogAndWait(new ApiKeyDialog(viewModel, fetcherViewModel)); } } 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 f5785fd2933..dca966ff906 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 @@ -2,7 +2,12 @@ import java.io.IOException; import java.net.HttpURLConnection; -import java.util.Optional; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.function.Consumer; import java.util.stream.Collectors; import javafx.beans.property.BooleanProperty; @@ -19,14 +24,13 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; -import org.jabref.gui.DialogService; import org.jabref.gui.preferences.PreferenceTabViewModel; -import org.jabref.gui.slr.StudyCatalogItem; import org.jabref.logic.FilePreferences; import org.jabref.logic.LibraryPreferences; import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.ImporterPreferences; import org.jabref.logic.importer.SearchBasedFetcher; +import org.jabref.logic.importer.WebFetcher; import org.jabref.logic.importer.WebFetchers; import org.jabref.logic.importer.fetcher.CompositeSearchBasedFetcher; import org.jabref.logic.importer.fetcher.CustomizableKeyFetcher; @@ -38,6 +42,8 @@ import org.jabref.logic.preferences.CliPreferences; import org.jabref.logic.preferences.DOIPreferences; import org.jabref.logic.preferences.FetcherApiKey; +import org.jabref.logic.util.BackgroundTask; +import org.jabref.logic.util.TaskExecutor; import kong.unirest.core.UnirestException; @@ -59,16 +65,13 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel { private final BooleanProperty useCustomDOIProperty = new SimpleBooleanProperty(); private final StringProperty useCustomDOINameProperty = new SimpleStringProperty(""); - private final ObservableList catalogs = FXCollections.observableArrayList(); + private final ObservableList fetchers = FXCollections.observableArrayList(); private final BooleanProperty grobidEnabledProperty = new SimpleBooleanProperty(); private final StringProperty grobidURLProperty = new SimpleStringProperty(""); - private final ObservableList apiKeys = FXCollections.observableArrayList(); - private final ObjectProperty selectedApiKeyProperty = new SimpleObjectProperty<>(); private final BooleanProperty apikeyPersistProperty = new SimpleBooleanProperty(); private final BooleanProperty apikeyPersistAvailableProperty = new SimpleBooleanProperty(); - private final DialogService dialogService; private final CliPreferences preferences; private final DOIPreferences doiPreferences; private final GrobidPreferences grobidPreferences; @@ -76,11 +79,11 @@ public class WebSearchTabViewModel implements PreferenceTabViewModel { private final FilePreferences filePreferences; private final ImportFormatPreferences importFormatPreferences; private final LibraryPreferences libraryPreferences; + private final TaskExecutor taskExecutor; private final ReadOnlyBooleanProperty refAiEnabled; - public WebSearchTabViewModel(CliPreferences preferences, DialogService dialogService, ReadOnlyBooleanProperty refAiEnabled) { - this.dialogService = dialogService; + public WebSearchTabViewModel(CliPreferences preferences, ReadOnlyBooleanProperty refAiEnabled, TaskExecutor taskExecutor) { this.preferences = preferences; this.importerPreferences = preferences.getImporterPreferences(); this.grobidPreferences = preferences.getGrobidPreferences(); @@ -88,6 +91,7 @@ public WebSearchTabViewModel(CliPreferences preferences, DialogService dialogSer this.filePreferences = preferences.getFilePreferences(); this.importFormatPreferences = preferences.getImportFormatPreferences(); this.libraryPreferences = preferences.getLibraryPreferences(); + this.taskExecutor = taskExecutor; this.refAiEnabled = refAiEnabled; @@ -149,21 +153,37 @@ public void setValues() { grobidEnabledProperty.setValue(grobidPreferences.isGrobidEnabled()); grobidURLProperty.setValue(grobidPreferences.getGrobidURL()); - apiKeys.setAll(preferences.getImporterPreferences().getApiKeys().stream() - .map(apiKey -> new FetcherApiKey(apiKey.getName(), apiKey.shouldUse(), apiKey.getKey())) - .toList()); + Set savedApiKeys = preferences.getImporterPreferences().getApiKeys(); + Set enabledCatalogs = new HashSet<>(importerPreferences.getCatalogs()); + + List allFetchers = WebFetchers.getSearchBasedFetchers(importFormatPreferences, importerPreferences) + .stream().sorted(Comparator.comparing(WebFetcher::getName)).toList(); + + Set customizableKeyFetchers = WebFetchers.getCustomizableKeyFetchers(importFormatPreferences, importerPreferences); + Set customizableFetcherNames = customizableKeyFetchers.stream().map(WebFetcher::getName).collect(Collectors.toSet()); + + fetchers.clear(); + for (SearchBasedFetcher fetcher : allFetchers) { + if (CompositeSearchBasedFetcher.FETCHER_NAME.equals(fetcher.getName())) { + continue; + } + boolean isEnabled = enabledCatalogs.contains(fetcher.getName()); + boolean isCustomizable = customizableFetcherNames.contains(fetcher.getName()); + FetcherViewModel fetcherViewModel = new FetcherViewModel(fetcher, isEnabled, isCustomizable); + if (isCustomizable) { + savedApiKeys.stream() + .filter(apiKey -> apiKey.getName().equals(fetcher.getName())) + .findFirst() + .ifPresent(apiKey -> { + fetcherViewModel.apiKeyProperty().set(apiKey.getKey()); + fetcherViewModel.useCustomApiKeyProperty().set(apiKey.shouldUse()); + }); + } + fetchers.add(fetcherViewModel); + } apikeyPersistAvailableProperty.setValue(OS.isKeyringAvailable()); apikeyPersistProperty.setValue(preferences.getImporterPreferences().shouldPersistCustomKeys()); - catalogs.addAll(WebFetchers.getSearchBasedFetchers(importFormatPreferences, importerPreferences) - .stream() - .map(SearchBasedFetcher::getName) - .filter(name -> !CompositeSearchBasedFetcher.FETCHER_NAME.equals(name)) - .map(name -> { - boolean enabled = importerPreferences.getCatalogs().contains(name); - return new StudyCatalogItem(name, enabled); - }) - .toList()); } @Override @@ -186,15 +206,22 @@ public void storeSettings() { grobidPreferences.setGrobidURL(grobidURLProperty.getValue()); doiPreferences.setUseCustom(useCustomDOIProperty.get()); doiPreferences.setDefaultBaseURI(useCustomDOINameProperty.getValue().trim()); + importerPreferences.setCatalogs( - FXCollections.observableList(catalogs.stream() - .filter(StudyCatalogItem::isEnabled) - .map(StudyCatalogItem::getName) - .collect(Collectors.toList()))); + fetchers.stream() + .filter(FetcherViewModel::isEnabled) + .map(FetcherViewModel::getName) + .toList()); + + List apiKeysToStore = fetchers.stream() + .filter(FetcherViewModel::isCustomizable) + .map(fetcherViewModel -> new FetcherApiKey(fetcherViewModel.getName(), fetcherViewModel.shouldUseCustomApiKey(), fetcherViewModel.getApiKey())) + .toList(); + importerPreferences.setPersistCustomKeys(apikeyPersistProperty.get()); preferences.getImporterPreferences().getApiKeys().clear(); if (apikeyPersistAvailableProperty.get()) { - preferences.getImporterPreferences().getApiKeys().addAll(apiKeys); + preferences.getImporterPreferences().getApiKeys().addAll(apiKeysToStore); } } @@ -226,8 +253,8 @@ public StringProperty useCustomDOINameProperty() { return this.useCustomDOINameProperty; } - public ObservableList getCatalogs() { - return catalogs; + public ObservableList getFetchers() { + return fetchers; } public BooleanProperty grobidEnabledProperty() { @@ -238,14 +265,6 @@ public StringProperty grobidURLProperty() { return grobidURLProperty; } - public ObservableList fetcherApiKeys() { - return apiKeys; - } - - public ObjectProperty selectedApiKeyProperty() { - return selectedApiKeyProperty; - } - public BooleanProperty warnAboutDuplicatesOnImportProperty() { return warnAboutDuplicatesOnImportProperty; } @@ -270,58 +289,100 @@ public IntegerProperty citationsRelationsStoreTTLProperty() { return citationsRelationStoreTTL; } - public void checkCustomApiKey() { - final String apiKeyName = selectedApiKeyProperty.get().getName(); - - final Optional fetcherOpt = - WebFetchers.getCustomizableKeyFetchers( - preferences.getImportFormatPreferences(), - preferences.getImporterPreferences()) - .stream() - .filter(fetcher -> fetcher.getName().equals(apiKeyName)) - .findFirst(); - - if (fetcherOpt.isEmpty()) { - dialogService.showErrorDialogAndWait( - Localization.lang("Check %0 API Key Setting", apiKeyName), - Localization.lang("Fetcher unknown!")); - return; - } + public void checkApiKey(FetcherViewModel fetcherViewModel, String apiKey, Consumer onFinished) { + Callable tester = () -> { + WebFetcher webFetcher = fetcherViewModel.getFetcher(); - final String testUrlWithoutApiKey = fetcherOpt.get().getTestUrl(); - if (testUrlWithoutApiKey == null) { - dialogService.showWarningDialogAndWait( - Localization.lang("Check %0 API Key Setting", apiKeyName), - Localization.lang("Fetcher cannot be tested!")); - return; - } + if (!(webFetcher instanceof CustomizableKeyFetcher fetcher)) { + return false; + } - final String apiKey = selectedApiKeyProperty.get().getKey(); + String testUrlWithoutApiKey = fetcher.getTestUrl(); + if (testUrlWithoutApiKey == null) { + return false; + } + + if (apiKey.isEmpty()) { + return false; + } - boolean keyValid; - if (!apiKey.isEmpty()) { - URLDownload urlDownload; try { - urlDownload = new URLDownload(testUrlWithoutApiKey + apiKey); + URLDownload urlDownload = new URLDownload(testUrlWithoutApiKey + apiKey); // The HEAD request cannot be used because its response is not 200 (maybe 404 or 596...). int statusCode = ((HttpURLConnection) urlDownload.getSource().openConnection()).getResponseCode(); - keyValid = (statusCode >= 200) && (statusCode < 300); + return (statusCode >= 200) && (statusCode < 300); } catch (IOException | UnirestException e) { - keyValid = false; + return false; } - } else { - keyValid = false; - } - - if (keyValid) { - dialogService.showInformationDialogAndWait(Localization.lang("Check %0 API Key Setting", apiKeyName), Localization.lang("Connection successful!")); - } else { - dialogService.showErrorDialogAndWait(Localization.lang("Check %0 API Key Setting", apiKeyName), Localization.lang("Connection failed!")); - } + }; + BackgroundTask.wrap(tester) + .onSuccess(onFinished) + .onFailure(_ -> onFinished.accept(false)) + .executeWith(taskExecutor); } @Override public boolean validateSettings() { - return getCatalogs().stream().anyMatch(StudyCatalogItem::isEnabled); + return getFetchers().stream().anyMatch(FetcherViewModel::isEnabled); + } + + public static class FetcherViewModel { + private final StringProperty name = new SimpleStringProperty(); + private final BooleanProperty enabled = new SimpleBooleanProperty(); + private final BooleanProperty customizable = new SimpleBooleanProperty(); + private final StringProperty apiKey = new SimpleStringProperty(""); + private final BooleanProperty useCustomApiKey = new SimpleBooleanProperty(false); + private final WebFetcher fetcher; + + public FetcherViewModel(WebFetcher fetcher, boolean enabled, boolean customizable) { + this.name.set(fetcher.getName()); + this.fetcher = fetcher; + this.enabled.set(enabled); + this.customizable.set(customizable); + } + + public String getName() { + return name.get(); + } + + public StringProperty nameProperty() { + return name; + } + + public boolean isEnabled() { + return enabled.get(); + } + + public BooleanProperty enabledProperty() { + return enabled; + } + + public boolean isCustomizable() { + return customizable.get(); + } + + public BooleanProperty customizableProperty() { + return customizable; + } + + public String getApiKey() { + return apiKey.get(); + } + + public StringProperty apiKeyProperty() { + return apiKey; + } + + public boolean shouldUseCustomApiKey() { + return useCustomApiKey.get(); + } + + public BooleanProperty useCustomApiKeyProperty() { + return useCustomApiKey; + } + + public WebFetcher getFetcher() { + return fetcher; + } } } diff --git a/jabgui/src/main/java/org/jabref/gui/util/component/HelpButton.java b/jabgui/src/main/java/org/jabref/gui/util/component/HelpButton.java index 5537cc485f9..eeb9b9c2e9c 100644 --- a/jabgui/src/main/java/org/jabref/gui/util/component/HelpButton.java +++ b/jabgui/src/main/java/org/jabref/gui/util/component/HelpButton.java @@ -4,8 +4,11 @@ import org.jabref.gui.DialogService; import org.jabref.gui.edit.OpenBrowserAction; +import org.jabref.gui.frame.ExternalApplicationsPreferences; +import org.jabref.gui.help.HelpAction; import org.jabref.gui.icon.IconTheme; import org.jabref.gui.preferences.GuiPreferences; +import org.jabref.logic.help.HelpFile; import com.airhacks.afterburner.injection.Injector; import org.jspecify.annotations.NonNull; @@ -31,4 +34,8 @@ public void setHelpPage(@NonNull String helpDocumentationUrl) { ).execute() ); } + + public void setHelpFile(@NonNull HelpFile helpFile, @NonNull DialogService dialogService, @NonNull ExternalApplicationsPreferences externalApplicationsPreferences) { + setOnAction(_ -> new HelpAction(helpFile, dialogService, externalApplicationsPreferences).execute()); + } } diff --git a/jabgui/src/main/resources/org/jabref/gui/Base.css b/jabgui/src/main/resources/org/jabref/gui/Base.css index 46019ed885b..62f0ddf5534 100644 --- a/jabgui/src/main/resources/org/jabref/gui/Base.css +++ b/jabgui/src/main/resources/org/jabref/gui/Base.css @@ -83,6 +83,7 @@ -jr-info: -jr-light-green; -jr-warn: -jr-orange; -jr-error: -jr-light-red; + -jr-success: -jr-green; /* Color for the small group view indicator for the number of hits */ -jr-group-hits-bg: derive(-jr-sidepane-background, -50%); @@ -1357,11 +1358,6 @@ We want to have a look that matches our icons in the tool-bar */ -fx-fill: -jr-warn; } -.info-message { - -fx-fill: -jr-info; - -fx-text-fill: -jr-info; -} - .warning-message { -fx-fill: -jr-warn; -fx-text-fill: -jr-warn; @@ -2502,6 +2498,51 @@ We want to have a look that matches our icons in the tool-bar */ -fx-padding: 2; } +/* WebSearchTab */ +.fetcher-list-cell { + -fx-padding: 0.3em 0.6em; + -fx-alignment: center-left; + -fx-spacing: 0.5em; +} + +.fetcher-list-cell:nth-child(odd) { + -fx-background-color: -jr-row-odd-background; +} + +.fetcher-list-cell .label { + -fx-font-size: 100%; +} + +.fetcher-list-cell .configure-button { + -fx-padding: 0.25em 0.5em; + -fx-background-color: -fx-control-inner-background; + -fx-text-fill: -fx-mid-text-color; +} + +#testButton { + -fx-padding: 0.25em 0.5em; +} + +#testButton.success { + -fx-text-fill: -jr-success; +} + +#testButton.success .glyph-icon { + -fx-icon-color: -jr-success; +} + +#testButton.error { + -fx-text-fill: -jr-error; +} + +#testButton.error .glyph-icon { + -fx-icon-color: -jr-error; +} + +.api-key-dialog-content { + -fx-padding: 1em; +} + /* CitationRelationsTab */ #citationRelationsTab .addEntryButton { @@ -3158,6 +3199,11 @@ journalInfo .grid-cell-b { -fx-border-width: 1px; } +.checkbox-flowpane { + -fx-hgap: 1.5em; + -fx-vgap: 0.8em; +} + /* Automatic Field Editor */ .edit-field-content-pane { -fx-padding: 0em 1em; diff --git a/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/ApiKeyDialog.fxml b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/ApiKeyDialog.fxml new file mode 100644 index 00000000000..c1243bccf28 --- /dev/null +++ b/jabgui/src/main/resources/org/jabref/gui/preferences/websearch/ApiKeyDialog.fxml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + +