diff --git a/core/src/main/java/bisq/core/user/Preferences.java b/core/src/main/java/bisq/core/user/Preferences.java index 656f056522a..3e65be22617 100644 --- a/core/src/main/java/bisq/core/user/Preferences.java +++ b/core/src/main/java/bisq/core/user/Preferences.java @@ -280,12 +280,12 @@ public void readPersisted() { // if no valid Bitcoin block explorer is set, select the 1st valid Bitcoin block explorer ArrayList btcExplorers = getBlockChainExplorers(); - if (!blockExplorerExists(btcExplorers, getBlockChainExplorer())) + if (getBlockChainExplorer() == null || getBlockChainExplorer().name.length() == 0) setBlockChainExplorer(btcExplorers.get(0)); // if no valid BSQ block explorer is set, randomly select a valid BSQ block explorer ArrayList bsqExplorers = getBsqBlockChainExplorers(); - if (!blockExplorerExists(bsqExplorers, getBsqBlockChainExplorer())) + if (getBsqBlockChainExplorer() == null || getBsqBlockChainExplorer().name.length() == 0) setBsqBlockChainExplorer(bsqExplorers.get((new Random()).nextInt(bsqExplorers.size()))); tradeCurrenciesAsObservable.addAll(prefPayload.getFiatCurrencies()); @@ -902,15 +902,6 @@ else if (change.wasRemoved() && change.getRemovedSize() == 1 && initialReadDone) requestPersistence(); } - private boolean blockExplorerExists(ArrayList explorers, - BlockChainExplorer explorer) { - if (explorer != null && explorers != null && explorers.size() > 0) - for (int i = 0; i < explorers.size(); i++) - if (explorers.get(i).name.equals(explorer.name)) - return true; - return false; - } - private interface ExcludesDelegateMethods { void setTacAccepted(boolean tacAccepted); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 9c3e4fc792c..083eb9efd1f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1237,6 +1237,14 @@ setting.preferences.dao.fullNodeInfo=For running Bisq as DAO full node you need After changing the mode you need to restart. setting.preferences.dao.fullNodeInfo.ok=Open docs page setting.preferences.dao.fullNodeInfo.cancel=No, I stick with lite node mode +settings.preferences.editCustomExplorer.headline=Explorer Settings +settings.preferences.editCustomExplorer.description=Choose a system defined explorer from the list on the left, and/or \ + customize to suit your own preferences. +settings.preferences.editCustomExplorer.available=Available explorers +settings.preferences.editCustomExplorer.chosen = Chosen explorer settings +settings.preferences.editCustomExplorer.name=Name +settings.preferences.editCustomExplorer.txUrl=Transaction URL +settings.preferences.editCustomExplorer.addressUrl=Address URL settings.net.btcHeader=Bitcoin network settings.net.p2pHeader=Bisq network diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/EditCustomExplorerWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/EditCustomExplorerWindow.java new file mode 100644 index 00000000000..44a53361543 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/EditCustomExplorerWindow.java @@ -0,0 +1,199 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.desktop.main.overlays.windows; + +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipLabel; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.LengthValidator; + +import bisq.core.locale.Res; +import bisq.core.util.validation.UrlInputValidator; + +import bisq.common.util.Tuple2; + +import bisq.core.user.BlockChainExplorer; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.ColumnConstraints; +import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.geometry.VPos; + +import javafx.event.EventHandler; + +import javafx.collections.FXCollections; + +import javafx.util.Callback; + +import java.util.ArrayList; + +import static bisq.desktop.util.FormBuilder.*; +import static javafx.beans.binding.Bindings.createBooleanBinding; + +public class EditCustomExplorerWindow extends Overlay { + + private InputTextField nameInputTextField, txUrlInputTextField, addressUrlInputTextField; + private UrlInputValidator urlInputValidator; + private BlockChainExplorer currentExplorer; + private ListView listView; + + public EditCustomExplorerWindow(String coin, + BlockChainExplorer currentExplorer, + ArrayList availableExplorers) { + this.currentExplorer = currentExplorer; + listView = new ListView<>(); + listView.setItems(FXCollections.observableArrayList(availableExplorers)); + headLine = coin + " " + Res.get("settings.preferences.editCustomExplorer.headline"); + } + + public BlockChainExplorer getEditedBlockChainExplorer() { + return new BlockChainExplorer(nameInputTextField.getText(), + txUrlInputTextField.getText(), addressUrlInputTextField.getText()); + } + + public void show() { + + width = 1000; + createGridPane(); + addHeadLine(); + addContent(); + addButtons(); + + urlInputValidator = new UrlInputValidator(); + txUrlInputTextField.setValidator(urlInputValidator); + addressUrlInputTextField.setValidator(urlInputValidator); + nameInputTextField.setValidator(new LengthValidator(1, 50)); + + actionButton.disableProperty().bind(createBooleanBinding(() -> { + String name = nameInputTextField.getText(); + String txUrl = txUrlInputTextField.getText(); + String addressUrl = addressUrlInputTextField.getText(); + + // Otherwise we require that input is valid + return !nameInputTextField.getValidator().validate(name).isValid || + !txUrlInputTextField.getValidator().validate(txUrl).isValid || + !addressUrlInputTextField.getValidator().validate(addressUrl).isValid; + }, + nameInputTextField.textProperty(), txUrlInputTextField.textProperty(), addressUrlInputTextField.textProperty())); + + applyStyles(); + display(); + } + + @Override + protected void createGridPane() { + gridPane = new GridPane(); + gridPane.setHgap(15); + gridPane.setVgap(15); + gridPane.setPadding(new Insets(64, 64, 64, 64)); + gridPane.setPrefWidth(width); + ColumnConstraints columnConstraints1 = new ColumnConstraints(); + ColumnConstraints columnConstraints2 = new ColumnConstraints(); + columnConstraints1.setPercentWidth(45); + columnConstraints2.setPercentWidth(55); + gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + } + + private void addContent() { + Label mlm = addMultilineLabel(gridPane, rowIndex++, Res.get("settings.preferences.editCustomExplorer.description"), 0); + GridPane.setColumnSpan(mlm, 2); + GridPane.setMargin(mlm, new Insets(40, 0, 0, 0)); + + Button button = new AutoTooltipButton(">>"); + button.setOnAction(e -> { + BlockChainExplorer blockChainExplorer = listView.getSelectionModel().getSelectedItem(); + if (blockChainExplorer != null) { + nameInputTextField.setText(blockChainExplorer.name); + txUrlInputTextField.setText(blockChainExplorer.txUrl); + addressUrlInputTextField.setText(blockChainExplorer.addressUrl); + } + }); + button.setStyle("-fx-pref-width: 50px; -fx-pref-height: 30; -fx-padding: 3 3 3 3;"); + VBox vBox = new VBox(button); + vBox.setAlignment(Pos.CENTER); + final Tuple2 topLabelWithVBox = getTopLabelWithVBox(Res.get("settings.preferences.editCustomExplorer.available"), listView); + listView.setPrefWidth(300); + HBox hBox = new HBox(topLabelWithVBox.second, vBox); + hBox.setAlignment(Pos.CENTER_LEFT); + hBox.setSpacing(20); + hBox.setMaxHeight(200); + gridPane.add(hBox, 0, rowIndex); + GridPane.setColumnIndex(hBox, 0); + GridPane.setValignment(hBox, VPos.TOP); + GridPane.setMargin(hBox, new Insets(10, 0, 0, 0)); + + listView.setCellFactory(new Callback<>() { + @Override + public ListCell call(ListView list) { + ListCell cell = new ListCell<>() { + final Label label = new AutoTooltipLabel(); + final AnchorPane pane = new AnchorPane(label); + @Override + public void updateItem(final BlockChainExplorer item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + label.setText(item.name); + setGraphic(pane); + } else { + setGraphic(null); + } + } + }; + + cell.addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler() { + @Override + public void handle(MouseEvent event) { + if (event.getClickCount() == 2) { + BlockChainExplorer blockChainExplorer = listView.getSelectionModel().getSelectedItem(); + nameInputTextField.setText(blockChainExplorer.name); + txUrlInputTextField.setText(blockChainExplorer.txUrl); + addressUrlInputTextField.setText(blockChainExplorer.addressUrl); + } + } + }); + return cell; + } + }); + + GridPane autoConfirmGridPane = new GridPane(); + autoConfirmGridPane.setPrefHeight(150); + GridPane.setMargin(autoConfirmGridPane, new Insets(10, 0, 0, 0)); + gridPane.add(autoConfirmGridPane, 1, rowIndex); + addTitledGroupBg(autoConfirmGridPane, 0, 6, Res.get("settings.preferences.editCustomExplorer.chosen"), 0); + int localRowIndex = 0; + nameInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.name"), Layout.FIRST_ROW_DISTANCE); + nameInputTextField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); + txUrlInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.txUrl")); + addressUrlInputTextField = addInputTextField(autoConfirmGridPane, ++localRowIndex, Res.get("settings.preferences.editCustomExplorer.addressUrl")); + nameInputTextField.setText(currentExplorer.name); + txUrlInputTextField.setText(currentExplorer.txUrl); + addressUrlInputTextField.setText(currentExplorer.addressUrl); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index ef8c6951096..9d9674af302 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -26,6 +26,7 @@ import bisq.desktop.components.PasswordTextField; import bisq.desktop.components.TitledGroupBg; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.EditCustomExplorerWindow; import bisq.desktop.util.GUIUtil; import bisq.desktop.util.ImageUtil; import bisq.desktop.util.Layout; @@ -45,7 +46,6 @@ import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.provider.fee.FeeService; -import bisq.core.user.BlockChainExplorer; import bisq.core.user.Preferences; import bisq.core.util.FormattingUtils; import bisq.core.util.ParsingUtils; @@ -57,6 +57,7 @@ import bisq.common.UserThread; import bisq.common.app.DevEnv; import bisq.common.config.Config; +import bisq.common.util.Tuple2; import bisq.common.util.Tuple3; import bisq.common.util.Utilities; @@ -73,6 +74,7 @@ import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.Separator; +import javafx.scene.control.TextField; import javafx.scene.control.ToggleButton; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; @@ -107,8 +109,7 @@ @FxmlView public class PreferencesView extends ActivatableViewAndModel { private final CoinFormatter formatter; - private ComboBox blockChainExplorerComboBox; - private ComboBox bsqBlockChainExplorerComboBox; + private TextField btcExplorerTextField, bsqExplorerTextField; private ComboBox userLanguageComboBox; private ComboBox userCountryComboBox; private ComboBox preferredTradeCurrencyComboBox; @@ -138,9 +139,8 @@ public class PreferencesView extends ActivatableViewAndModel fiatCurrenciesComboBox; private ListView cryptoCurrenciesListView; private ComboBox cryptoCurrenciesComboBox; - private Button resetDontShowAgainButton, resyncDaoFromGenesisButton, resyncDaoFromResourcesButton; - private ObservableList blockExplorers; - private ObservableList bsqBlockChainExplorers; + private Button resetDontShowAgainButton, resyncDaoFromGenesisButton, resyncDaoFromResourcesButton, + editCustomBtcExplorer, editCustomBsqExplorer; private ObservableList languageCodes; private ObservableList countries; private ObservableList fiatCurrencies; @@ -194,8 +194,6 @@ public PreferencesView(PreferencesViewModel model, @Override public void initialize() { - blockExplorers = FXCollections.observableArrayList(preferences.getBlockChainExplorers()); - bsqBlockChainExplorers = FXCollections.observableArrayList(preferences.getBsqBlockChainExplorers()); languageCodes = FXCollections.observableArrayList(LanguageUtil.getUserLanguageCodes()); countries = FXCollections.observableArrayList(CountryUtil.getAllCountries()); fiatCurrencies = preferences.getFiatCurrenciesAsObservable(); @@ -255,15 +253,13 @@ private void initializeGeneralOptions() { userCountryComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("shared.country"), userCountryComboBox, false)); - blockChainExplorerComboBox = addComboBox(root, ++gridRow, - Res.get("setting.preferences.explorer")); - blockChainExplorerComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("setting.preferences.explorer"), - blockChainExplorerComboBox, false)); + Tuple2 btcExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer")); + btcExplorerTextField = btcExp.first; + editCustomBtcExplorer = btcExp.second; - bsqBlockChainExplorerComboBox = addComboBox(root, ++gridRow, - Res.get("setting.preferences.explorer.bsq")); - bsqBlockChainExplorerComboBox.setButtonCell(GUIUtil.getComboBoxButtonCell(Res.get("setting.preferences.explorer.bsq"), - bsqBlockChainExplorerComboBox, false)); + Tuple2 bsqExp = addTextFieldWithEditButton(root, ++gridRow, Res.get("setting.preferences.explorer.bsq")); + bsqExplorerTextField = bsqExp.first; + editCustomBsqExplorer = bsqExp.second; Tuple3 tuple = addTopLabelInputTextFieldSlideToggleButton(root, ++gridRow, Res.get("setting.preferences.txFee"), Res.get("setting.preferences.useCustomValue")); @@ -831,35 +827,8 @@ public Country fromString(String string) { } }); - blockChainExplorerComboBox.setItems(blockExplorers); - blockChainExplorerComboBox.getSelectionModel().select(preferences.getBlockChainExplorer()); - blockChainExplorerComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(BlockChainExplorer blockChainExplorer) { - return blockChainExplorer.name; - } - - @Override - public BlockChainExplorer fromString(String string) { - return null; - } - }); - blockChainExplorerComboBox.setOnAction(e -> preferences.setBlockChainExplorer(blockChainExplorerComboBox.getSelectionModel().getSelectedItem())); - - bsqBlockChainExplorerComboBox.setItems(bsqBlockChainExplorers); - bsqBlockChainExplorerComboBox.getSelectionModel().select(preferences.getBsqBlockChainExplorer()); - bsqBlockChainExplorerComboBox.setConverter(new StringConverter<>() { - @Override - public String toString(BlockChainExplorer bsqBlockChainExplorer) { - return bsqBlockChainExplorer.name; - } - - @Override - public BlockChainExplorer fromString(String string) { - return null; - } - }); - bsqBlockChainExplorerComboBox.setOnAction(e -> preferences.setBsqBlockChainExplorer(bsqBlockChainExplorerComboBox.getSelectionModel().getSelectedItem())); + btcExplorerTextField.setText(preferences.getBlockChainExplorer().name); + bsqExplorerTextField.setText(preferences.getBsqBlockChainExplorer().name); deviationInputTextField.setText(FormattingUtils.formatPercentagePrice(preferences.getMaxPriceDistanceInPercent())); deviationInputTextField.textProperty().addListener(deviationListener); @@ -937,6 +906,34 @@ private void activateDisplayPreferences() { resetDontShowAgainButton.setOnAction(e -> preferences.resetDontShowAgain()); + editCustomBtcExplorer.setOnAction(e -> { + EditCustomExplorerWindow urlWindow = new EditCustomExplorerWindow("BTC", + preferences.getBlockChainExplorer(), preferences.getBlockChainExplorers()); + urlWindow + .actionButtonText(Res.get("shared.save")) + .onAction(() -> { + preferences.setBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); + btcExplorerTextField.setText(preferences.getBlockChainExplorer().name); + }) + .closeButtonText(Res.get("shared.cancel")) + .onClose(urlWindow::hide) + .show(); + }); + + editCustomBsqExplorer.setOnAction(e -> { + EditCustomExplorerWindow urlWindow = new EditCustomExplorerWindow("BSQ", + preferences.getBsqBlockChainExplorer(), preferences.getBsqBlockChainExplorers()); + urlWindow + .actionButtonText(Res.get("shared.save")) + .onAction(() -> { + preferences.setBsqBlockChainExplorer(urlWindow.getEditedBlockChainExplorer()); + bsqExplorerTextField.setText(preferences.getBsqBlockChainExplorer().name); + }) + .closeButtonText(Res.get("shared.cancel")) + .onClose(urlWindow::hide) + .show(); + }); + // We use opposite property (useStandbyMode) in preferences to have the default value (false) set as we want it, // so users who update gets set avoidStandbyMode=true (useStandbyMode=false) if (displayStandbyModeFeature) { @@ -1059,8 +1056,8 @@ private void deactivateGeneralOptions() { //selectBaseCurrencyNetworkComboBox.setOnAction(null); userLanguageComboBox.setOnAction(null); userCountryComboBox.setOnAction(null); - blockChainExplorerComboBox.setOnAction(null); - bsqBlockChainExplorerComboBox.setOnAction(null); + editCustomBtcExplorer.setOnAction(null); + editCustomBsqExplorer.setOnAction(null); deviationInputTextField.textProperty().removeListener(deviationListener); deviationInputTextField.focusedProperty().removeListener(deviationFocusedListener); transactionFeeInputTextField.focusedProperty().removeListener(transactionFeeFocusedListener); diff --git a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java index f8061e04f99..f91a4c83ff0 100644 --- a/desktop/src/main/java/bisq/desktop/util/FormBuilder.java +++ b/desktop/src/main/java/bisq/desktop/util/FormBuilder.java @@ -338,6 +338,31 @@ public static Tuple3 addTopLabelTextField(GridPane gridP return new Tuple3<>(topLabelWithVBox.first, textField, topLabelWithVBox.second); } + public static Tuple2 addTextFieldWithEditButton(GridPane gridPane, int rowIndex, String title) { + TextField textField = new BisqTextField(); + textField.setPromptText(title); + textField.setEditable(false); + textField.setFocusTraversable(false); + textField.setPrefWidth(Layout.INITIAL_WINDOW_WIDTH); + + Button button = new AutoTooltipButton("..."); + button.setStyle("-fx-min-width: 35px; -fx-pref-height: 20; -fx-padding: 3 3 3 3; -fx-border-insets: 5px;"); + button.managedProperty().bind(button.visibleProperty()); + VBox vBoxButton = new VBox(button); + vBoxButton.setAlignment(Pos.CENTER); + HBox hBox2 = new HBox(textField, vBoxButton); + + Label label = getTopLabel(title); + VBox textFieldVbox = getTopLabelVBox(0); + textFieldVbox.getChildren().addAll(label, hBox2); + + gridPane.getChildren().add(textFieldVbox); + GridPane.setRowIndex(textFieldVbox, rowIndex); + GridPane.setMargin(textFieldVbox, new Insets(Layout.FLOATING_LABEL_DISTANCE, 0, 0, 0)); + + return new Tuple2<>(textField, button); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Confirmation Fields ///////////////////////////////////////////////////////////////////////////////////////////