Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ 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 and the event viewer for debugging.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two unrelated things which do not seem very minor adjustments (like the add button) - so each deserve their own entry.


### Fixed

Expand Down Expand Up @@ -117,6 +118,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((obs, oldValue, 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