Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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

Expand Down Expand Up @@ -117,6 +119,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)

### Removed

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<WebSearchTabViewModel> implements PreferencesTab {

@FXML private CheckBox enableWebSearch;
@FXML private CheckBox warnAboutDuplicatesOnImport;
@FXML private CheckBox downloadLinkedOnlineFiles;
Expand All @@ -45,17 +44,7 @@ public class WebSearchTab extends AbstractPreferenceTabView<WebSearchTabViewMode
@FXML private CheckBox grobidEnabled;
@FXML private TextField grobidURL;

@FXML private TableView<FetcherApiKey> apiKeySelectorTable;
@FXML private TableColumn<FetcherApiKey, String> apiKeyName;
@FXML private TableColumn<FetcherApiKey, String> customApiKey;
@FXML private TableColumn<FetcherApiKey, Boolean> useCustomApiKey;
@FXML private Button testCustomApiKey;

@FXML private CheckBox persistApiKeys;
@FXML private SplitPane persistentTooltipWrapper; // The disabled persistApiKeys control does not show tooltips
@FXML private TableView<StudyCatalogItem> catalogTable;
@FXML private TableColumn<StudyCatalogItem, Boolean> catalogEnabledColumn;
@FXML private TableColumn<StudyCatalogItem, String> catalogColumn;
@FXML private VBox fetchersContainer;

private final ReadOnlyBooleanProperty refAiEnabled;

Expand All @@ -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());
Expand Down Expand Up @@ -117,86 +106,47 @@ public void initialize() {
useCustomDOIName.textProperty().bindBidirectional(viewModel.useCustomDOINameProperty());
useCustomDOIName.disableProperty().bind(useCustomDOI.selectedProperty().not());

new ViewModelTableRowFactory<StudyCatalogItem>()
.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<FetcherApiKey>()
.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> 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));
}
}
Loading