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> addLabelSearchComboBox(GridPane gridPane, int rowIndex, String title, double top) { - - SearchComboBox comboBox = new SearchComboBox<>(); - + public static Tuple2> addLabelAutocompleteComboBox(GridPane gridPane, int rowIndex, String title, double top) { + AutocompleteComboBox comboBox = new AutocompleteComboBox<>(); final Tuple2 labelVBoxTuple2 = addTopLabelWithVBox(gridPane, rowIndex, title, comboBox, top); - return new Tuple2<>(labelVBoxTuple2.first, comboBox); }