diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css
index 6d18f20939..7d3415a25f 100644
--- a/desktop/src/main/java/bisq/desktop/bisq.css
+++ b/desktop/src/main/java/bisq/desktop/bisq.css
@@ -1247,6 +1247,13 @@ textfield */
-fx-alignment: center-left;
}
+.combo-box-editor-bold {
+ -fx-font-weight: bold;
+ -fx-padding: 5 8 5 8 !important;
+ -fx-text-fill: -bs-rd-black;
+ -fx-font-family: "IBM Plex Sans Medium";
+}
+
.currency-label-small {
-fx-font-size: 0.692em;
-fx-text-fill: -bs-rd-font-lighter;
diff --git a/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java b/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java
new file mode 100644
index 0000000000..2d8be1e561
--- /dev/null
+++ b/desktop/src/main/java/bisq/desktop/components/AutocompleteComboBox.java
@@ -0,0 +1,194 @@
+/*
+ * 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.components;
+
+import bisq.common.UserThread;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.jfoenix.controls.JFXComboBox;
+import com.jfoenix.skins.JFXComboBoxListViewSkin;
+
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+
+import javafx.event.Event;
+import javafx.event.EventHandler;
+
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Implements searchable dropdown (an autocomplete like experience).
+ *
+ * Clients must use setAutocompleteItems() instead of setItems().
+ *
+ * @param type of the ComboBox item; in the simplest case this can be a String
+ */
+public class AutocompleteComboBox extends JFXComboBox {
+ private ArrayList completeList;
+ private ArrayList matchingList;
+ private JFXComboBoxListViewSkin comboBoxListViewSkin;
+
+ public AutocompleteComboBox() {
+ this(FXCollections.observableArrayList());
+ }
+
+ private AutocompleteComboBox(ObservableList items) {
+ super(items);
+ setEditable(true);
+ clearOnFocus();
+ setEmptySkinToGetMoreControlOverListView();
+ fixSpaceKey();
+ setAutocompleteItems(items);
+ reactToQueryChanges();
+ }
+
+ /**
+ * Set the complete list of ComboBox items. Use this instead of setItems().
+ */
+ public void setAutocompleteItems(List items) {
+ completeList = new ArrayList<>(items);
+ matchingList = new ArrayList<>(completeList);
+ setValue(null);
+ getSelectionModel().clearSelection();
+ setItems(FXCollections.observableList(matchingList));
+ getEditor().setText("");
+ }
+
+ /**
+ * Triggered when value change is *confirmed*. In practical terms
+ * this is when user clicks item on the dropdown or hits [ENTER]
+ * while typing in the text.
+ *
+ * This is in contrast to onAction event that is triggered
+ * on every (unconfirmed) value change. The onAction is not really
+ * suitable for the search enabled ComboBox.
+ */
+ public final void setOnChangeConfirmed(EventHandler eh) {
+ setOnHidden(e -> {
+ var inputText = getEditor().getText();
+
+ // Case 1: fire if input text selects (matches) an item
+ var selectedItem = getSelectionModel().getSelectedItem();
+ var inputTextItem = getConverter().fromString(inputText);
+ if (selectedItem != null && selectedItem.equals(inputTextItem)) {
+ eh.handle(e);
+ return;
+ }
+
+ // Case 2: fire if the text is empty to support special "show all" case
+ if (inputText.isEmpty())
+ eh.handle(e);
+ });
+ }
+
+ // Clear selection and query when ComboBox gets new focus. This is usually what user
+ // wants - to have a blank slate for a new search. The primary motivation though
+ // was to work around UX glitches related to (starting) editing text when combobox
+ // had specific item selected.
+ private void clearOnFocus() {
+ getEditor().focusedProperty().addListener((observableValue, hadFocus, hasFocus) -> {
+ if (!hadFocus && hasFocus) {
+ removeFilter();
+ forceRedraw();
+ }
+ });
+ }
+
+ // The ComboBox API does not provide enough control over the underlying
+ // ListView that is used as a dropdown. The only way to get this control
+ // is to set custom ListViewSkin. The default skin is null and so useless.
+ private void setEmptySkinToGetMoreControlOverListView() {
+ comboBoxListViewSkin = new JFXComboBoxListViewSkin<>(this);
+ setSkin(comboBoxListViewSkin);
+ }
+
+ // By default pressing [SPACE] caused editor text to reset. The solution
+ // is to suppress relevant event on the underlying ListViewSkin.
+ private void fixSpaceKey() {
+ comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) -> {
+ if (event.getCode() == KeyCode.SPACE)
+ event.consume();
+ });
+ }
+
+ private void filterBy(String query) {
+ ArrayList newMatchingList = new ArrayList<>();
+ for (T item : completeList)
+ if (StringUtils.containsIgnoreCase(asString(item), query))
+ newMatchingList.add(item);
+ matchingList = newMatchingList;
+ setValue(null);
+ getSelectionModel().clearSelection();
+ setItems(FXCollections.observableList(matchingList));
+ int pos = getEditor().getCaretPosition();
+ if (pos > query.length()) pos = query.length();
+ getEditor().setText(query);
+ getEditor().positionCaret(pos);
+ }
+
+ private void reactToQueryChanges() {
+ getEditor().addEventHandler(KeyEvent.KEY_RELEASED, (KeyEvent event) -> {
+ UserThread.execute(() -> {
+ String query = getEditor().getText();
+ var exactMatch = completeList.stream().anyMatch(item -> asString(item).equalsIgnoreCase(query));
+ if (!exactMatch) {
+ if (query.isEmpty())
+ removeFilter();
+ else
+ filterBy(query);
+ forceRedraw();
+ }
+ });
+ });
+ }
+
+ private void removeFilter() {
+ matchingList = new ArrayList<>(completeList);
+ setValue(null);
+ getSelectionModel().clearSelection();
+ setItems(FXCollections.observableList(matchingList));
+ getEditor().setText("");
+ }
+
+ private void forceRedraw() {
+ adjustVisibleRowCount();
+ if (matchingListSize() > 0) {
+ comboBoxListViewSkin.getPopupContent().autosize();
+ show();
+ } else {
+ hide();
+ }
+ }
+
+ private void adjustVisibleRowCount() {
+ setVisibleRowCount(Math.min(10, matchingListSize()));
+ }
+
+ private String asString(T item) {
+ return getConverter().toString(item);
+ }
+
+ private int matchingListSize() {
+ return matchingList.size();
+ }
+}
diff --git a/desktop/src/main/java/bisq/desktop/components/SearchComboBox.java b/desktop/src/main/java/bisq/desktop/components/SearchComboBox.java
deleted file mode 100644
index 4d30b23e5c..0000000000
--- a/desktop/src/main/java/bisq/desktop/components/SearchComboBox.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * 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.components;
-
-import bisq.common.UserThread;
-
-import org.apache.commons.lang3.StringUtils;
-
-import com.jfoenix.controls.JFXComboBox;
-import com.jfoenix.skins.JFXComboBoxListViewSkin;
-
-import javafx.scene.control.skin.ComboBoxListViewSkin;
-import javafx.scene.input.KeyCode;
-import javafx.scene.input.KeyEvent;
-
-import javafx.event.Event;
-import javafx.event.EventHandler;
-
-import javafx.collections.FXCollections;
-import javafx.collections.ObservableList;
-import javafx.collections.transformation.FilteredList;
-
-public class SearchComboBox extends JFXComboBox {
- @SuppressWarnings("CanBeFinal")
- private FilteredList filteredList;
- private ComboBoxListViewSkin comboBoxListViewSkin;
-
- public SearchComboBox() {
- this(FXCollections.observableArrayList());
- }
-
- private SearchComboBox(ObservableList items) {
- super(items);
- setEditable(true);
- setEmptySkinToGetMoreControlOverListView();
- fixSpaceKey();
- wrapItemsInFilteredList();
- reactToQueryChanges();
- }
-
- // The ComboBox API does not provide enough control over the underlying
- // ListView that is used as a dropdown. The only way to get this control
- // is to set custom ListViewSkin. Default skin is null and so useless.
- private void setEmptySkinToGetMoreControlOverListView() {
- comboBoxListViewSkin = new JFXComboBoxListViewSkin<>(this);
- setSkin(comboBoxListViewSkin);
- }
-
- // By default pressing [SPACE] caused editor text to reset. The solution
- // is to suppress relevant event on the underlying ListViewSkin.
- private void fixSpaceKey() {
- comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) -> {
- if (event.getCode() == KeyCode.SPACE)
- event.consume();
- });
- }
-
- // Whenever ComboBox.setItems() is called we need to intercept it
- // and wrap the physical list in a FilteredList view.
- // The default predicate is null meaning no filtering occurs.
- private void wrapItemsInFilteredList() {
- itemsProperty().addListener((obsValue, oldList, newList) -> {
- filteredList = new FilteredList<>(newList);
- setItems(filteredList);
- });
- }
-
- // Whenever query changes we need to reset the list-filter and refresh the ListView
- private void reactToQueryChanges() {
- getEditor().textProperty().addListener((observable, oldQuery, query) -> {
- var exactMatch = unfilteredItems().stream().anyMatch(item -> asString(item).equalsIgnoreCase(query));
- if (!exactMatch) {
- UserThread.execute(() -> {
- if (query.isEmpty())
- removeFilter();
- else
- filterBy(query);
- forceRedraw();
- });
- }
- });
- }
-
- private ObservableList unfilteredItems() {
- return (ObservableList) filteredList.getSource();
- }
-
- private String asString(T item) {
- return getConverter().toString(item);
- }
-
- private int filteredItemsSize() {
- return filteredList.size();
- }
-
- private void removeFilter() {
- filteredList.setPredicate(null);
- }
-
- private void filterBy(String query) {
- filteredList.setPredicate(item ->
- StringUtils.containsIgnoreCase(asString(item), query)
- );
- }
-
- /**
- * Triggered when value change is *confirmed*. In practical terms
- * this is when user clicks item on the dropdown or hits [ENTER]
- * while typing in the text.
- *
- * This is in contrast to onAction event that is triggered
- * on every (unconfirmed) value change. The onAction is not really
- * suitable for the search enabled ComboBox.
- */
- public final void setOnChangeConfirmed(EventHandler eh) {
- setOnHidden(e -> {
- var selectedItem = getSelectionModel().getSelectedItem();
- var selectedItemText = asString(selectedItem);
- var inputText = getEditor().getText();
- if (inputText.equals(selectedItemText)) {
- eh.handle(e);
- }
- });
- }
-
- private void forceRedraw() {
- setVisibleRowCount(Math.min(10, filteredItemsSize()));
- if (filteredItemsSize() > 0) {
- comboBoxListViewSkin.getPopupContent().autosize();
- show();
- } else {
- hide();
- }
- }
-
- public void deactivate() {
- setOnHidden(null);
- }
-}
diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java
index a7634741ee..58078de9e4 100644
--- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java
+++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/AssetsForm.java
@@ -19,7 +19,7 @@ package bisq.desktop.components.paymentmethods;
import bisq.desktop.components.InputTextField;
import bisq.desktop.components.NewBadge;
-import bisq.desktop.components.SearchComboBox;
+import bisq.desktop.components.AutocompleteComboBox;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.FormBuilder;
import bisq.desktop.util.Layout;
@@ -55,12 +55,8 @@ import javafx.scene.layout.VBox;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
-import javafx.collections.FXCollections;
-
import javafx.util.StringConverter;
-import java.util.Optional;
-
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static bisq.desktop.util.FormBuilder.addLabelCheckBox;
@@ -219,7 +215,7 @@ public class AssetsForm extends PaymentMethodForm {
@Override
protected void addTradeCurrencyComboBox() {
- currencyComboBox = FormBuilder.addLabelSearchComboBox(gridPane, ++gridRow, Res.get("payment.altcoin"),
+ currencyComboBox = FormBuilder.addLabelAutocompleteComboBox(gridPane, ++gridRow, Res.get("payment.altcoin"),
Layout.FIRST_ROW_AND_GROUP_DISTANCE).second;
currencyComboBox.setPromptText(Res.get("payment.select.altcoin"));
currencyComboBox.setButtonCell(getComboBoxButtonCell(Res.get("payment.select.altcoin"), currencyComboBox));
@@ -228,8 +224,9 @@ public class AssetsForm extends PaymentMethodForm {
currencyComboBox.setPromptText("");
});
- currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(assetService, filterManager)));
+ ((AutocompleteComboBox) currencyComboBox).setAutocompleteItems(CurrencyUtil.getActiveSortedCryptoCurrencies(assetService, filterManager));
currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10));
+
currencyComboBox.setConverter(new StringConverter() {
@Override
public String toString(TradeCurrency tradeCurrency) {
@@ -238,14 +235,13 @@ public class AssetsForm extends PaymentMethodForm {
@Override
public TradeCurrency fromString(String s) {
- Optional tradeCurrencyOptional = currencyComboBox.getItems().stream().
- filter(tradeCurrency -> tradeCurrency.getNameAndCode().equals(s)).
- findAny();
- return tradeCurrencyOptional.orElse(null);
+ return currencyComboBox.getItems().stream().
+ filter(item -> item.getNameAndCode().equals(s)).
+ findAny().orElse(null);
}
});
- ((SearchComboBox) currencyComboBox).setOnChangeConfirmed(e -> {
+ ((AutocompleteComboBox) currencyComboBox).setOnChangeConfirmed(e -> {
addressInputTextField.resetValidation();
addressInputTextField.validate();
paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem());
diff --git a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java
index 66392ab496..e3b13b2c6d 100644
--- a/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java
+++ b/desktop/src/main/java/bisq/desktop/main/market/offerbook/OfferBookChartView.java
@@ -25,6 +25,7 @@ import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.ColoredDecimalPlacesWithZerosText;
import bisq.desktop.components.PeerInfoIconSmall;
+import bisq.desktop.components.AutocompleteComboBox;
import bisq.desktop.main.MainView;
import bisq.desktop.main.offer.BuyOfferView;
import bisq.desktop.main.offer.SellOfferView;
@@ -93,7 +94,7 @@ import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
-import static bisq.desktop.util.FormBuilder.addTopLabelComboBox;
+import static bisq.desktop.util.FormBuilder.addTopLabelAutocompleteComboBox;
import static bisq.desktop.util.Layout.INITIAL_WINDOW_HEIGHT;
@FxmlView
@@ -108,7 +109,7 @@ public class OfferBookChartView extends ActivatableViewAndModel sellOfferTableView;
private AreaChart areaChart;
private AnchorPane chartPane;
- private ComboBox currencyComboBox;
+ private AutocompleteComboBox currencyComboBox;
private Subscription tradeCurrencySubscriber;
private final StringProperty volumeColumnLabel = new SimpleStringProperty();
private final StringProperty priceColumnLabel = new SimpleStringProperty();
@@ -146,11 +147,8 @@ public class OfferBookChartView extends ActivatableViewAndModel> currencyComboBoxTuple = addTopLabelComboBox(Res.get("shared.currency"),
- Res.get("list.currency.select"), 0);
+ final Tuple3> currencyComboBoxTuple = addTopLabelAutocompleteComboBox(Res.get("shared.currency"), 0);
this.currencyComboBox = currencyComboBoxTuple.third;
- this.currencyComboBox.setButtonCell(GUIUtil.getCurrencyListItemButtonCell(Res.get("shared.oneOffer"),
- Res.get("shared.multipleOffers"), model.preferences));
this.currencyComboBox.setCellFactory(GUIUtil.getCurrencyListItemCellFactory(Res.get("shared.oneOffer"),
Res.get("shared.multipleOffers"), model.preferences));
@@ -191,13 +189,19 @@ public class OfferBookChartView extends ActivatableViewAndModel {
+ currencyComboBox.setOnChangeConfirmed(e -> {
CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem();
if (selectedItem != null) {
model.onSetTradeCurrency(selectedItem.tradeCurrency);
@@ -281,6 +285,26 @@ public class OfferBookChartView extends ActivatableViewAndModel {
+ private ComboBox comboBox;
+
+ CurrencyListItemStringConverter(ComboBox comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public String toString(CurrencyListItem currencyItem) {
+ return currencyItem != null ? currencyItem.codeDashNameString() : "";
+ }
+
+ @Override
+ public CurrencyListItem fromString(String s) {
+ return comboBox.getItems().stream().
+ filter(currencyItem -> currencyItem.codeDashNameString().equals(s)).
+ findAny().orElse(null);
+ }
+ }
+
private void createListener() {
changeListener = c -> updateChartData();
@@ -313,7 +337,6 @@ public class OfferBookChartView extends ActivatableViewAndModel tableView;
- private ComboBox currencyComboBox;
+ private AutocompleteComboBox currencyComboBox;
private VolumeChart volumeChart;
private CandleStickChart priceChart;
private NumberAxis priceAxisX, priceAxisY, volumeAxisY, volumeAxisX;
@@ -128,6 +129,7 @@ public class TradesChartsView extends ActivatableViewAndModel priceColumnLabelListener;
private AnchorPane priceChartPane, volumeChartPane;
+ private static final int SHOW_ALL = 0;
///////////////////////////////////////////////////////////////////////////////////////////
@@ -186,21 +188,26 @@ public class TradesChartsView extends ActivatableViewAndModel {
+ currencyComboBox.setOnChangeConfirmed(e -> {
+ if (currencyComboBox.getEditor().getText().isEmpty())
+ currencyComboBox.getSelectionModel().select(SHOW_ALL);
CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem();
if (selectedItem != null)
model.onSetTradeCurrency(selectedItem.tradeCurrency);
});
-
toggleGroup.getToggles().get(model.tickUnit.ordinal()).setSelected(true);
model.priceItems.addListener(itemsChangeListener);
@@ -240,8 +247,7 @@ public class TradesChartsView extends ActivatableViewAndModel {
- });
+ currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> {});
sortedList = new SortedList<>(model.tradeStatisticsByCurrency);
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
@@ -266,8 +272,6 @@ public class TradesChartsView extends ActivatableViewAndModel {
+ private ComboBox comboBox;
+
+ CurrencyStringConverter(ComboBox comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public String toString(CurrencyListItem currencyItem) {
+ return currencyItem != null ? currencyItem.codeDashNameString() : "";
+ }
+
+ @Override
+ public CurrencyListItem fromString(String query) {
+ if (comboBox.getItems().isEmpty())
+ return null;
+ if (query.isEmpty())
+ return specialShowAllItem();
+ return comboBox.getItems().stream().
+ filter(currencyItem -> currencyItem.codeDashNameString().equals(query)).
+ findAny().orElse(null);
+ }
+
+ private CurrencyListItem specialShowAllItem() {
+ return comboBox.getItems().get(0);
+ }
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Chart
@@ -467,16 +499,12 @@ public class TradesChartsView extends ActivatableViewAndModel> currencyComboBoxTuple = addTopLabelComboBox(Res.get("shared.currency"),
- Res.get("list.currency.select"));
+ final Tuple3> currencyComboBoxTuple = addTopLabelAutocompleteComboBox(
+ Res.get("shared.currency"));
currencyComboBox = currencyComboBoxTuple.third;
- currencyComboBox.setButtonCell(GUIUtil.getCurrencyListItemButtonCell(Res.get("shared.trade"),
- Res.get("shared.trades"), model.preferences));
currencyComboBox.setCellFactory(GUIUtil.getCurrencyListItemCellFactory(Res.get("shared.trade"),
Res.get("shared.trades"), model.preferences));
- currencyComboBox.setPromptText(Res.get("list.currency.select"));
-
Pane spacer = new Pane();
HBox.setHgrow(spacer, Priority.ALWAYS);
diff --git a/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java
index d2fa0955da..b4c6d3603e 100644
--- a/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java
+++ b/desktop/src/main/java/bisq/desktop/main/offer/OfferView.java
@@ -298,4 +298,3 @@ public abstract class OfferView extends ActivatableView {
void close();
}
}
-
diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java
index 1f6efadec7..fc93632c93 100644
--- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java
+++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java
@@ -23,6 +23,7 @@ import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.AutoTooltipTableColumn;
+import bisq.desktop.components.AutocompleteComboBox;
import bisq.desktop.components.ColoredDecimalPlacesWithZerosText;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.components.InfoAutoTooltipLabel;
@@ -100,6 +101,7 @@ import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.util.Callback;
+import javafx.util.StringConverter;
import java.util.Comparator;
import java.util.Optional;
@@ -117,8 +119,8 @@ public class OfferBookView extends ActivatableViewAndModel currencyComboBox;
- private ComboBox paymentMethodComboBox;
+ private AutocompleteComboBox currencyComboBox;
+ private AutocompleteComboBox paymentMethodComboBox;
private AutoTooltipButton createOfferButton;
private AutoTooltipTableColumn amountColumn, volumeColumn, marketColumn,
priceColumn, avatarColumn;
@@ -130,6 +132,7 @@ public class OfferBookView extends ActivatableViewAndModel offerListListener;
private ChangeListener priceFeedUpdateCounterListener;
private Subscription currencySelectionSubscriber;
+ private static final int SHOW_ALL = 0;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
@@ -163,10 +166,10 @@ public class OfferBookView extends ActivatableViewAndModel> currencyBoxTuple = FormBuilder.addTopLabelComboBox(
- Res.get("offerbook.filterByCurrency"), Res.get("list.currency.select"));
- final Tuple3> paymentBoxTuple = FormBuilder.addTopLabelComboBox(
- Res.get("offerbook.filterByPaymentMethod"), Res.get("shared.selectPaymentMethod"));
+ final Tuple3> currencyBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox(
+ Res.get("offerbook.filterByCurrency"));
+ final Tuple3> paymentBoxTuple = FormBuilder.addTopLabelAutocompleteComboBox(
+ Res.get("offerbook.filterByPaymentMethod"));
createOfferButton = new AutoTooltipButton();
createOfferButton.setMinHeight(40);
@@ -191,8 +194,6 @@ public class OfferBookView extends ActivatableViewAndModel();
@@ -262,22 +263,27 @@ public class OfferBookView extends ActivatableViewAndModel model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem()));
+ currencyComboBox.setAutocompleteItems(model.getTradeCurrencies());
+ currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 10));
+
+ currencyComboBox.setOnChangeConfirmed(e -> {
+ if (currencyComboBox.getEditor().getText().isEmpty())
+ currencyComboBox.getSelectionModel().select(SHOW_ALL);
+ model.onSetTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem());
+ });
if (model.showAllTradeCurrenciesProperty.get())
- currencyComboBox.getSelectionModel().select(0);
+ currencyComboBox.getSelectionModel().select(SHOW_ALL);
else
currencyComboBox.getSelectionModel().select(model.getSelectedTradeCurrency());
+ currencyComboBox.getEditor().setText(new CurrencyStringConverter(currencyComboBox).toString(currencyComboBox.getSelectionModel().getSelectedItem()));
volumeColumn.sortableProperty().bind(model.showAllTradeCurrenciesProperty.not());
priceColumn.sortableProperty().bind(model.showAllTradeCurrenciesProperty.not());
@@ -285,12 +291,23 @@ public class OfferBookView extends ActivatableViewAndModel priceColumn.setSortType(newValue));
priceColumn.setSortType(model.priceSortTypeProperty.get());
- paymentMethodComboBox.setItems(model.getPaymentMethods());
- paymentMethodComboBox.setOnAction(e -> model.onSetPaymentMethod(paymentMethodComboBox.getSelectionModel().getSelectedItem()));
+ paymentMethodComboBox.setConverter(new PaymentMethodStringConverter(paymentMethodComboBox));
+ paymentMethodComboBox.getEditor().getStyleClass().add("combo-box-editor-bold");
+
+ paymentMethodComboBox.setAutocompleteItems(model.getPaymentMethods());
+ paymentMethodComboBox.setVisibleRowCount(Math.min(paymentMethodComboBox.getItems().size(), 10));
+
+ paymentMethodComboBox.setOnChangeConfirmed(e -> {
+ if (paymentMethodComboBox.getEditor().getText().isEmpty())
+ paymentMethodComboBox.getSelectionModel().select(SHOW_ALL);
+ model.onSetPaymentMethod(paymentMethodComboBox.getSelectionModel().getSelectedItem());
+ });
+
if (model.showAllPaymentMethods)
- paymentMethodComboBox.getSelectionModel().select(0);
+ paymentMethodComboBox.getSelectionModel().select(SHOW_ALL);
else
paymentMethodComboBox.getSelectionModel().select(model.selectedPaymentMethod);
+ paymentMethodComboBox.getEditor().setText(new PaymentMethodStringConverter(paymentMethodComboBox).toString(paymentMethodComboBox.getSelectionModel().getSelectedItem()));
createOfferButton.setOnAction(e -> onCreateOffer());
@@ -315,8 +332,8 @@ public class OfferBookView extends ActivatableViewAndModel {
- });
+
+ currencySelectionSubscriber = currencySelectionBinding.subscribe((observable, oldValue, newValue) -> {});
tableView.setItems(model.getOfferList());
@@ -328,8 +345,6 @@ public class OfferBookView extends ActivatableViewAndModel {
+ private ComboBox comboBox;
+
+ CurrencyStringConverter(ComboBox comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public String toString(TradeCurrency item) {
+ return item != null ? asString(item) : "";
+ }
+
+ @Override
+ public TradeCurrency fromString(String query) {
+ if (comboBox.getItems().isEmpty())
+ return null;
+ if (query.isEmpty())
+ return specialShowAllItem();
+ return comboBox.getItems().stream().
+ filter(item -> asString(item).equals(query)).
+ findAny().orElse(null);
+ }
+
+ private String asString(TradeCurrency item) {
+ if (isSpecialShowAllItem(item))
+ return Res.get(GUIUtil.SHOW_ALL_FLAG);
+ if (isSpecialEditItem(item))
+ return Res.get(GUIUtil.EDIT_FLAG);
+ return item.getCode() + " - " + item.getName();
+ }
+
+ private boolean isSpecialShowAllItem(TradeCurrency item) {
+ return item.getCode().equals(GUIUtil.SHOW_ALL_FLAG);
+ }
+
+ private boolean isSpecialEditItem(TradeCurrency item) {
+ return item.getCode().equals(GUIUtil.EDIT_FLAG);
+ }
+
+ private TradeCurrency specialShowAllItem() {
+ return comboBox.getItems().get(SHOW_ALL);
+ }
+ }
+
+ static class PaymentMethodStringConverter extends StringConverter {
+ private ComboBox comboBox;
+
+ PaymentMethodStringConverter(ComboBox comboBox) {
+ this.comboBox = comboBox;
+ }
+
+ @Override
+ public String toString(PaymentMethod item) {
+ return item != null ? asString(item) : "";
+ }
+
+ @Override
+ public PaymentMethod fromString(String query) {
+ if (comboBox.getItems().isEmpty())
+ return null;
+ if (query.isEmpty())
+ return specialShowAllItem();
+ return comboBox.getItems().stream().
+ filter(item -> asString(item).equals(query)).
+ findAny().orElse(null);
+ }
+
+ private String asString(PaymentMethod item) {
+ if (isSpecialShowAllItem(item))
+ return Res.get(GUIUtil.SHOW_ALL_FLAG);
+ return Res.get(item.getId());
+ }
+
+ private boolean isSpecialShowAllItem(PaymentMethod item) {
+ return item.getId().equals(GUIUtil.SHOW_ALL_FLAG);
+ }
+
+ private PaymentMethod specialShowAllItem() {
+ return comboBox.getItems().get(SHOW_ALL);
+ }
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
@@ -993,4 +1090,3 @@ public class OfferBookView extends ActivatableViewAndModel(vBox, label, comboBox);
}
+ public static Tuple3> addTopLabelAutocompleteComboBox(String title) {
+ return addTopLabelAutocompleteComboBox(title, 0);
+ }
+
+ public static Tuple3> addTopLabelAutocompleteComboBox(String title, int top) {
+ Label label = getTopLabel(title);
+ VBox vBox = getTopLabelVBox(top);
+
+ final AutocompleteComboBox comboBox = new AutocompleteComboBox<>();
+
+ vBox.getChildren().addAll(label, comboBox);
+
+ return new Tuple3<>(vBox, label, comboBox);
+ }
+
@NotNull
private static VBox getTopLabelVBox(int top) {
VBox vBox = new VBox();
@@ -984,15 +999,12 @@ public class FormBuilder {
}
///////////////////////////////////////////////////////////////////////////////////////////
- // Label + SearchComboBox
+ // Label + AutocompleteComboBox
///////////////////////////////////////////////////////////////////////////////////////////
- public static Tuple2