Skip to content

Commit

Permalink
Add dropdown autocomplete on Market, Buy, Sell tabs; fix #2226
Browse files Browse the repository at this point in the history
Basic autocomplete feature for all dropdowns on the major tabs:

* Market / Offer Book
* Market / Trades
* Buy BTC
* Sell BTC

Known limitations:

* Autocomplete still missing from Settings, Account, DAO tabs
* Minor UX glitches remain despite lots of debugging and polishing

Related issues:

* #2226 (fixed)
* #2712 (partially addressed)
* #112 (superseded)
  • Loading branch information
battleofwizards committed Aug 25, 2019
1 parent 6273246 commit bf51533
Show file tree
Hide file tree
Showing 9 changed files with 336 additions and 95 deletions.
7 changes: 7 additions & 0 deletions desktop/src/main/java/bisq/desktop/bisq.css
Original file line number Diff line number Diff line change
Expand Up @@ -1400,6 +1400,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;
Expand Down
118 changes: 76 additions & 42 deletions desktop/src/main/java/bisq/desktop/components/SearchComboBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

import javafx.beans.value.ChangeListener;

import javafx.event.Event;
import javafx.event.EventHandler;

Expand All @@ -39,6 +41,7 @@ public class SearchComboBox<T> extends JFXComboBox<T> {
@SuppressWarnings("CanBeFinal")
private FilteredList<T> filteredList;
private ComboBoxListViewSkin comboBoxListViewSkin;
private ChangeListener<? super String> queryChangedListener;

public SearchComboBox() {
this(FXCollections.observableArrayList());
Expand All @@ -50,7 +53,58 @@ private SearchComboBox(ObservableList<T> items) {
setEmptySkinToGetMoreControlOverListView();
fixSpaceKey();
wrapItemsInFilteredList();
reactToQueryChanges();
prepareQueryChangedListener();
}

/**
* Activates dropdown reaction to query changes. This is separate from instantiation
* to give clients more control over when exactly the autocomplete mechanism is
* showing / resizing / hiding the dropdown in response to query text changes.
*
* Mind that query text changes are triggered *not* only by the user typing in,
* but also internally with JavaFX ComboBox. For that reason clients may prefer
* to deactivate and then activate when it makes sense. This allows to avoid
* some UX glitches.
*/
public void activateAutocomplete() {
removeFilter();
adjustVisibleRowCount();
getEditor().textProperty().addListener(queryChangedListener);
}

/**
* See activate() for "why" and "when".
*/
public void deactivateAutocomplete() {
getEditor().textProperty().removeListener(queryChangedListener);
setOnHidden(null);
}

/**
* 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<Event> 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);
});
}

// The ComboBox API does not provide enough control over the underlying
Expand Down Expand Up @@ -80,22 +134,6 @@ private void wrapItemsInFilteredList() {
});
}

// 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<T> unfilteredItems() {
return (ObservableList<T>) filteredList.getSource();
}
Expand All @@ -104,7 +142,7 @@ private String asString(T item) {
return getConverter().toString(item);
}

private int filteredItemsSize() {
private int filteredListSize() {
return filteredList.size();
}

Expand All @@ -118,37 +156,33 @@ private void filterBy(String 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<Event> 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) {
adjustVisibleRowCount();
if (filteredListSize() > 0) {
comboBoxListViewSkin.getPopupContent().autosize();
show();
} else {
hide();
}
}

public void deactivate() {
setOnHidden(null);
private void adjustVisibleRowCount() {
setVisibleRowCount(Math.min(10, filteredListSize()));
}

private void prepareQueryChangedListener() {
queryChangedListener = (observable, oldQuery, query) -> {
UserThread.execute(() -> {
var exactMatch = unfilteredItems().stream().anyMatch(item -> asString(item).equalsIgnoreCase(query));
if (!exactMatch) {
if (query.isEmpty())
removeFilter();
else
filterBy(query);
forceRedraw();
}
});
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@

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;
Expand Down Expand Up @@ -238,10 +236,9 @@ public String toString(TradeCurrency tradeCurrency) {

@Override
public TradeCurrency fromString(String s) {
Optional<TradeCurrency> 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);
}
});

Expand All @@ -251,5 +248,7 @@ public TradeCurrency fromString(String s) {
paymentAccount.setSingleTradeCurrency(currencyComboBox.getSelectionModel().getSelectedItem());
updateFromInputs();
});

((SearchComboBox) currencyComboBox).activateAutocomplete();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.components.ColoredDecimalPlacesWithZerosText;
import bisq.desktop.components.PeerInfoIconSmall;
import bisq.desktop.components.SearchComboBox;
import bisq.desktop.main.MainView;
import bisq.desktop.main.offer.BuyOfferView;
import bisq.desktop.main.offer.SellOfferView;
Expand Down Expand Up @@ -93,7 +94,7 @@
import java.util.function.Function;
import java.util.function.Supplier;

import static bisq.desktop.util.FormBuilder.addTopLabelComboBox;
import static bisq.desktop.util.FormBuilder.addTopLabelSearchComboBox;
import static bisq.desktop.util.Layout.INITIAL_WINDOW_HEIGHT;

@FxmlView
Expand All @@ -108,7 +109,7 @@ public class OfferBookChartView extends ActivatableViewAndModel<VBox, OfferBookC
private TableView<OfferListItem> sellOfferTableView;
private AreaChart<Number, Number> areaChart;
private AnchorPane chartPane;
private ComboBox<CurrencyListItem> currencyComboBox;
private SearchComboBox<CurrencyListItem> currencyComboBox;
private Subscription tradeCurrencySubscriber;
private final StringProperty volumeColumnLabel = new SimpleStringProperty();
private final StringProperty priceColumnLabel = new SimpleStringProperty();
Expand Down Expand Up @@ -146,8 +147,7 @@ public OfferBookChartView(OfferBookChartViewModel model, Navigation navigation,
public void initialize() {
createListener();

final Tuple3<VBox, Label, ComboBox<CurrencyListItem>> currencyComboBoxTuple = addTopLabelComboBox(Res.get("shared.currency"),
Res.get("list.currency.select"), 0);
final Tuple3<VBox, Label, SearchComboBox<CurrencyListItem>> currencyComboBoxTuple = addTopLabelSearchComboBox(Res.get("shared.currency"), 0);
this.currencyComboBox = currencyComboBoxTuple.third;
this.currencyComboBox.setButtonCell(GUIUtil.getCurrencyListItemButtonCell(Res.get("shared.oneOffer"),
Res.get("shared.multipleOffers"), model.preferences));
Expand Down Expand Up @@ -191,20 +191,26 @@ protected void activate() {
model.setSelectedTabIndex(tabPaneSelectionModel.getSelectedIndex());
tabPaneSelectionModel.selectedIndexProperty().addListener(selectedTabIndexListener);

currencyComboBox.setItems(model.getCurrencyListItems());
currencyComboBox.setVisibleRowCount(12);
currencyComboBox.setConverter(new CurrencyListItemStringConverter(currencyComboBox));
currencyComboBox.getEditor().getStyleClass().add("combo-box-editor-bold");

if (model.getSelectedCurrencyListItem().isPresent())
currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get());
if (currencyComboBox.getItems().isEmpty())
currencyComboBox.setItems(model.getCurrencyListItems());
currencyComboBox.setVisibleRowCount(10);

currencyComboBox.setOnAction(e -> {
currencyComboBox.setOnChangeConfirmed(e -> {
CurrencyListItem selectedItem = currencyComboBox.getSelectionModel().getSelectedItem();
if (selectedItem != null) {
model.onSetTradeCurrency(selectedItem.tradeCurrency);
updateChartData();
}
});

if (model.getSelectedCurrencyListItem().isPresent())
currencyComboBox.getSelectionModel().select(model.getSelectedCurrencyListItem().get());

currencyComboBox.activateAutocomplete();

model.currencyListItems.getObservableList().addListener(currencyListItemsListener);

model.getOfferBookListItems().addListener(changeListener);
Expand Down Expand Up @@ -281,6 +287,28 @@ public Number fromString(String string) {
updateChartData();
}

static class CurrencyListItemStringConverter extends StringConverter<CurrencyListItem> {

private ComboBox<CurrencyListItem> comboBox;

CurrencyListItemStringConverter(ComboBox<CurrencyListItem> comboBox) {
this.comboBox = comboBox;
}

@Override
public String toString(CurrencyListItem currencyItem) {
return currencyItem != null ? currencyItem.codeDashNameString() : "";
}

@Override
public CurrencyListItem fromString(String s) {
Optional<CurrencyListItem> currencyItemOptional = comboBox.getItems().stream().
filter(currencyItem -> currencyItem.codeDashNameString().equals(s)).
findAny();
return currencyItemOptional.orElse(null);
}
}

private void createListener() {
changeListener = c -> updateChartData();

Expand Down Expand Up @@ -309,11 +337,11 @@ private void createListener() {

@Override
protected void deactivate() {
currencyComboBox.deactivateAutocomplete();
model.getOfferBookListItems().removeListener(changeListener);
tabPaneSelectionModel.selectedIndexProperty().removeListener(selectedTabIndexListener);
model.currencyListItems.getObservableList().removeListener(currencyListItemsListener);
tradeCurrencySubscriber.unsubscribe();
currencyComboBox.setOnAction(null);
buyOfferTableView.getSelectionModel().selectedItemProperty().removeListener(buyTableRowSelectionListener);
sellOfferTableView.getSelectionModel().selectedItemProperty().removeListener(sellTableRowSelectionListener);
}
Expand Down
Loading

0 comments on commit bf51533

Please sign in to comment.