diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenu.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenu.java index 4e5ee43126..a4c54e037c 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenu.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenu.java @@ -164,8 +164,7 @@ private void attachListeners() { // Once the contextMenu has calculated the width on the first render time we update the items // so that they all have the same size. for (MenuItem item : contextMenu.getItems()) { - if (item instanceof DropdownMenuItem) { - DropdownMenuItem dropdownMenuItem = (DropdownMenuItem) item; + if (item instanceof DropdownMenuItem dropdownMenuItem) { dropdownMenuItem.updateWidth(contextMenu.getWidth() - 18); // Remove margins } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenuItem.java b/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenuItem.java index f393743676..6bf0e3eabf 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenuItem.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/components/controls/DropdownMenuItem.java @@ -22,8 +22,8 @@ import javafx.scene.control.CustomMenuItem; import lombok.Getter; +@Getter public class DropdownMenuItem extends CustomMenuItem { - @Getter private final BisqMenuItem bisqMenuItem; public DropdownMenuItem(String defaultIconId, String activeIconId, String text) { diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListController.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListController.java index d89260cfd3..9da7d2961f 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListController.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListController.java @@ -17,6 +17,8 @@ package bisq.desktop.main.content.bisq_easy.offerbook.offerbook_list; +import bisq.account.payment_method.FiatPaymentMethod; +import bisq.account.payment_method.FiatPaymentMethodUtil; import bisq.bonded_roles.market_price.MarketPriceService; import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookChannel; import bisq.chat.bisqeasy.offerbook.BisqEasyOfferbookMessage; @@ -48,7 +50,7 @@ public class OfferbookListController implements bisq.desktop.common.view.Control private final MarketPriceService marketPriceService; private final ReputationService reputationService; private Pin showBuyOffersPin, showOfferListExpandedSettingsPin, offerMessagesPin; - private Subscription showBuyOffersFromModelPin; + private Subscription showBuyOffersFromModelPin, activeMarketPaymentsCountPin; public OfferbookListController(ServiceProvider serviceProvider, ChatMessageContainerController chatMessageContainerController) { @@ -69,10 +71,12 @@ public ReadOnlyBooleanProperty getShowOfferListExpanded() { public void onActivate() { showBuyOffersPin = FxBindings.bindBiDir(model.getShowBuyOffers()).to(settingsService.getShowBuyOffers()); showOfferListExpandedSettingsPin = FxBindings.bindBiDir(model.getShowOfferListExpanded()).to(settingsService.getShowOfferListExpanded()); - showBuyOffersFromModelPin = EasyBind.subscribe(model.getShowBuyOffers(), showBuyOffers -> - model.getFilteredOfferbookListItems().setPredicate(item -> - showBuyOffers == item.isBuyOffer() - )); + showBuyOffersFromModelPin = EasyBind.subscribe(model.getShowBuyOffers(), showBuyOffers -> applyPredicate()); + activeMarketPaymentsCountPin = EasyBind.subscribe(model.getActiveMarketPaymentsCount(), count -> { + String hint = count.intValue() == 0 ? Res.get("bisqEasy.offerbook.offerList.table.filters.paymentMethods.title.all") : count.toString(); + model.getPaymentFilterTitle().set(Res.get("bisqEasy.offerbook.offerList.table.filters.paymentMethods.title", hint)); + applyPredicate(); + }); } @Override @@ -82,6 +86,7 @@ public void onDeactivate() { showBuyOffersPin.unbind(); showOfferListExpandedSettingsPin.unbind(); showBuyOffersFromModelPin.unsubscribe(); + activeMarketPaymentsCountPin.unsubscribe(); if (offerMessagesPin != null) { offerMessagesPin.unbind(); } @@ -96,6 +101,9 @@ public void setSelectedChannel(BisqEasyOfferbookChannel channel) { model.getFiatAmountTitle().set(Res.get("bisqEasy.offerbook.offerList.table.columns.fiatAmount", channel.getMarket().getQuoteCurrencyCode()).toUpperCase()); + model.getAvailableMarketPayments().setAll(FiatPaymentMethodUtil.getPaymentMethods(channel.getMarket().getQuoteCurrencyCode())); + resetPaymentFilters(); + offerMessagesPin = channel.getChatMessages().addObserver(new CollectionObserver<>() { @Override public void add(BisqEasyOfferbookMessage bisqEasyOfferbookMessage) { @@ -158,4 +166,45 @@ void onSelectBuyFromFilter() { void onSelectSellToFilter() { model.getShowBuyOffers().set(true); } + + void toggleMethodFilter(FiatPaymentMethod paymentMethod, boolean isSelected) { + if (isSelected) { + model.getSelectedMarketPayments().add(paymentMethod); + } else { + model.getSelectedMarketPayments().remove(paymentMethod); + } + updateActiveMarketPaymentsCount(); + } + + void toggleCustomMethodFilter(boolean isSelected) { + model.getIsCustomPaymentsSelected().set(isSelected); + updateActiveMarketPaymentsCount(); + } + + private void resetPaymentFilters() { + model.getSelectedMarketPayments().clear(); + model.getIsCustomPaymentsSelected().set(false); + updateActiveMarketPaymentsCount(); + } + + private void updateActiveMarketPaymentsCount() { + int count = model.getSelectedMarketPayments().size(); + if (model.getIsCustomPaymentsSelected().get()) { + ++count; + } + model.getActiveMarketPaymentsCount().set(count); + } + + private void applyPredicate() { + model.getFilteredOfferbookListItems().setPredicate(this::shouldShowListItem); + } + + private boolean shouldShowListItem(OfferbookListItem item) { + boolean matchesDirection = model.getShowBuyOffers().get() == item.isBuyOffer(); + boolean paymentFiltersApplied = model.getActiveMarketPaymentsCount().get() != 0; + boolean matchesPaymentFilters = paymentFiltersApplied && item.getFiatPaymentMethods().stream() + .anyMatch(payment -> (payment.isCustomPaymentMethod() && model.getIsCustomPaymentsSelected().get()) + || model.getSelectedMarketPayments().contains(payment)); + return matchesDirection && (!paymentFiltersApplied || matchesPaymentFilters); + } } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListItem.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListItem.java index 6abd0f1c37..9f3af8040d 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListItem.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListItem.java @@ -123,7 +123,6 @@ private void updatePriceSpecAsPercent() { } } - private List retrieveAndSortFiatPaymentMethods() { List paymentMethods = PaymentMethodSpecUtil.getPaymentMethods(bisqEasyOffer.getQuoteSidePaymentMethodSpecs()); diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListModel.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListModel.java index 50af67bd0d..504b695eb7 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListModel.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListModel.java @@ -17,12 +17,16 @@ package bisq.desktop.main.content.bisq_easy.offerbook.offerbook_list; +import bisq.account.payment_method.FiatPaymentMethod; import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; import javafx.collections.transformation.FilteredList; import javafx.collections.transformation.SortedList; import lombok.Getter; @@ -35,6 +39,11 @@ class OfferbookListModel implements bisq.desktop.common.view.Model { private final StringProperty fiatAmountTitle = new SimpleStringProperty(); private final BooleanProperty showBuyOffers = new SimpleBooleanProperty(); private final BooleanProperty showOfferListExpanded = new SimpleBooleanProperty(); + private final StringProperty paymentFilterTitle = new SimpleStringProperty(); + private final ObservableList availableMarketPayments = FXCollections.observableArrayList(); + private final ObservableSet selectedMarketPayments = FXCollections.observableSet(); + private final BooleanProperty isCustomPaymentsSelected = new SimpleBooleanProperty(); + private final IntegerProperty activeMarketPaymentsCount = new SimpleIntegerProperty(); OfferbookListModel() { } diff --git a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListView.java b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListView.java index 4b6c2402ea..b7f21e5f8d 100644 --- a/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListView.java +++ b/apps/desktop/desktop/src/main/java/bisq/desktop/main/content/bisq_easy/offerbook/offerbook_list/OfferbookListView.java @@ -35,6 +35,8 @@ import bisq.desktop.main.content.components.UserProfileIcon; import bisq.i18n.Res; import com.google.common.base.Joiner; +import javafx.collections.ListChangeListener; +import javafx.css.PseudoClass; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Cursor; @@ -62,13 +64,15 @@ public class OfferbookListView extends bisq.desktop.common.view.View tableView; private final BisqTooltip titleTooltip; private final HBox header; private final ImageView offerListWhiteIcon, offerListGreyIcon, offerListGreenIcon; - private final DropdownMenu filterDropdownMenu; - private final DropdownMenuItem buyFromOffers, sellToOffers; + private final DropdownMenu offerDirectionFilterMenu, paymentsFilterMenu; + private final ListChangeListener listChangeListener; + private DropdownMenuItem buyFromOffers, sellToOffers; + private Label offerDirectionFilterLabel, paymentsFilterLabel; private Subscription showOfferListExpandedPin, showBuyFromOffersPin, offerListTableViewSelectionPin; OfferbookListView(OfferbookListModel model, OfferbookListController controller) { @@ -90,18 +94,14 @@ public class OfferbookListView extends bisq.desktop.common.view.View updateMarketPaymentFilters(); + offerDirectionFilterMenu = createAndGetOffersDirectionFilterMenu(); + paymentsFilterMenu = createAndGetPaymentsFilterDropdownMenu(); - HBox subheader = new HBox(); + HBox subheader = new HBox(10); subheader.setAlignment(Pos.CENTER_LEFT); subheader.getStyleClass().add("offer-list-subheader"); - subheader.getChildren().add(filterDropdownMenu); + subheader.getChildren().addAll(offerDirectionFilterMenu, paymentsFilterMenu); tableView = new BisqTableView<>(model.getSortedOfferbookListItems()); tableView.getStyleClass().add("offers-list"); @@ -117,12 +117,14 @@ public class OfferbookListView extends bisq.desktop.common.view.View { if (showOfferListExpanded != null) { tableView.setVisible(showOfferListExpanded); tableView.setManaged(showOfferListExpanded); - filterDropdownMenu.setVisible(showOfferListExpanded); - filterDropdownMenu.setManaged(showOfferListExpanded); + offerDirectionFilterMenu.setVisible(showOfferListExpanded); + offerDirectionFilterMenu.setManaged(showOfferListExpanded); title.setGraphic(offerListGreyIcon); if (showOfferListExpanded) { header.setAlignment(Pos.CENTER_LEFT); @@ -161,17 +163,20 @@ protected void onViewAttached() { showBuyFromOffersPin = EasyBind.subscribe(model.getShowBuyOffers(), showBuyFromOffers -> { if (showBuyFromOffers != null) { - offerListByDirectionFilter.getStyleClass().clear(); + offerDirectionFilterLabel.getStyleClass().clear(); if (showBuyFromOffers) { - offerListByDirectionFilter.setText(sellToOffers.getLabelText()); - offerListByDirectionFilter.getStyleClass().add("sell-to-offers"); + offerDirectionFilterLabel.setText(sellToOffers.getLabelText()); + offerDirectionFilterLabel.getStyleClass().add("sell-to-offers"); } else { - offerListByDirectionFilter.setText(buyFromOffers.getLabelText()); - offerListByDirectionFilter.getStyleClass().add("buy-from-offers"); + offerDirectionFilterLabel.setText(buyFromOffers.getLabelText()); + offerDirectionFilterLabel.getStyleClass().add("buy-from-offers"); } } }); + model.getAvailableMarketPayments().addListener(listChangeListener); + updateMarketPaymentFilters(); + title.setOnMouseEntered(e -> title.setGraphic(offerListWhiteIcon)); title.setOnMouseClicked(e -> controller.toggleOfferList()); buyFromOffers.setOnAction(e -> controller.onSelectBuyFromFilter()); @@ -182,10 +187,14 @@ protected void onViewAttached() { @Override protected void onViewDetached() { + paymentsFilterLabel.textProperty().unbind(); + showOfferListExpandedPin.unsubscribe(); offerListTableViewSelectionPin.unsubscribe(); showBuyFromOffersPin.unsubscribe(); + model.getAvailableMarketPayments().removeListener(listChangeListener); + title.setOnMouseEntered(null); title.setOnMouseExited(null); title.setOnMouseClicked(null); @@ -193,6 +202,57 @@ protected void onViewDetached() { sellToOffers.setOnAction(null); title.setTooltip(null); + + cleanUpPaymentsFilterMenu(); + } + + private DropdownMenu createAndGetOffersDirectionFilterMenu() { + DropdownMenu menu = new DropdownMenu("chevron-drop-menu-grey", "chevron-drop-menu-white", false); + menu.getStyleClass().add("dropdown-offer-list-direction-filter-menu"); + menu.setOpenToTheRight(true); + offerDirectionFilterLabel = new Label(); + menu.setLabel(offerDirectionFilterLabel); + buyFromOffers = new DropdownMenuItem(Res.get("bisqEasy.offerbook.offerList.table.filters.offerDirection.buyFrom")); + sellToOffers = new DropdownMenuItem(Res.get("bisqEasy.offerbook.offerList.table.filters.offerDirection.sellTo")); + menu.addMenuItems(buyFromOffers, sellToOffers); + return menu; + } + + private DropdownMenu createAndGetPaymentsFilterDropdownMenu() { + DropdownMenu menu = new DropdownMenu("chevron-drop-menu-grey", "chevron-drop-menu-white", false); + menu.getStyleClass().add("dropdown-offer-list-payment-filter-menu"); + menu.setOpenToTheRight(true); + paymentsFilterLabel = new Label(); + menu.setLabel(paymentsFilterLabel); + return menu; + } + + private void updateMarketPaymentFilters() { + cleanUpPaymentsFilterMenu(); + + model.getAvailableMarketPayments().forEach(payment -> { + PaymentMenuItem item = new PaymentMenuItem(payment.getDisplayString()); + item.setOnAction(e -> { + item.updateSelection(!item.isSelected()); + controller.toggleMethodFilter(payment, item.isSelected()); + }); + paymentsFilterMenu.addMenuItems(item); + }); + + PaymentMenuItem customItem = new PaymentMenuItem(Res.get("bisqEasy.offerbook.offerList.table.filters.paymentMethods.customMethod")); + customItem.setOnAction(e -> { + customItem.updateSelection(!customItem.isSelected()); + controller.toggleCustomMethodFilter(customItem.isSelected()); + }); + paymentsFilterMenu.addMenuItems(customItem); + } + + private void cleanUpPaymentsFilterMenu() { + paymentsFilterMenu.getMenuItems().stream() + .filter(item -> item instanceof PaymentMenuItem) + .map(item -> (PaymentMenuItem) item) + .forEach(PaymentMenuItem::dispose); + paymentsFilterMenu.clearMenuItems(); } private void configOffersTableView() { @@ -249,7 +309,6 @@ private void configOffersTableView() { .build()); } - private Callback, TableCell> getUserProfileCellFactory() { return column -> new TableCell<>() { @@ -406,4 +465,32 @@ protected void updateItem(OfferbookListItem item, boolean empty) { } }; } + + private static final class PaymentMenuItem extends DropdownMenuItem { + private static final PseudoClass SELECTED_PSEUDO_CLASS = PseudoClass.getPseudoClass("selected"); + + PaymentMenuItem(String displayName) { + // TODO: Update code so that we can pass label instead of text + super("check-white", "check-white", displayName); + + getStyleClass().add("dropdown-menu-item"); + updateSelection(false); + initialize(); + } + + public void initialize() { + } + + public void dispose() { + setOnAction(null); + } + + void updateSelection(boolean isSelected) { + getContent().pseudoClassStateChanged(SELECTED_PSEUDO_CLASS, isSelected); + } + + boolean isSelected() { + return getContent().getPseudoClassStates().contains(SELECTED_PSEUDO_CLASS); + } + } } diff --git a/apps/desktop/desktop/src/main/resources/css/bisq_easy.css b/apps/desktop/desktop/src/main/resources/css/bisq_easy.css index 6bf57eea85..bcbc2d3a1f 100644 --- a/apps/desktop/desktop/src/main/resources/css/bisq_easy.css +++ b/apps/desktop/desktop/src/main/resources/css/bisq_easy.css @@ -328,6 +328,11 @@ -fx-text-fill: -bisq2-red-lit-40; } +/* PAYMENT METHODS FILTER */ +.dropdown-offer-list-payment-filter-menu { + -fx-padding: 0 5 0 5; +} + /* COLLAPSE AND EXPAND COLUMNS */ .collapsed-offer-list-container { -fx-background-color: -bisq-dark-grey-20; diff --git a/i18n/src/main/resources/bisq_easy.properties b/i18n/src/main/resources/bisq_easy.properties index 6550e2d8dd..1026853046 100644 --- a/i18n/src/main/resources/bisq_easy.properties +++ b/i18n/src/main/resources/bisq_easy.properties @@ -453,6 +453,9 @@ bisqEasy.offerbook.offerList.table.columns.paymentMethod=Payment bisqEasy.offerbook.offerList.table.columns.settlementMethod=Settlement bisqEasy.offerbook.offerList.table.filters.offerDirection.buyFrom=Buy from bisqEasy.offerbook.offerList.table.filters.offerDirection.sellTo=Sell to +bisqEasy.offerbook.offerList.table.filters.paymentMethods.title=Payments ({0}) +bisqEasy.offerbook.offerList.table.filters.paymentMethods.title.all=All +bisqEasy.offerbook.offerList.table.filters.paymentMethods.customMethod=Custom methods bisqEasy.offerbook.offerList.table.columns.price.tooltip.fixPrice=Fixed price: {0}\nPercentage from current market price: {1} bisqEasy.offerbook.offerList.table.columns.price.tooltip.marketPrice=Market price: {0}