diff --git a/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartViewModel.java b/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartViewModel.java index ae706c6c7e..9f9b90d66e 100644 --- a/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartViewModel.java +++ b/gui/src/main/java/io/bisq/gui/main/market/offerbook/OfferBookChartViewModel.java @@ -34,6 +34,7 @@ import io.bisq.gui.main.offer.offerbook.OfferBook; import io.bisq.gui.main.offer.offerbook.OfferBookListItem; import io.bisq.gui.main.settings.SettingsView; import io.bisq.gui.main.settings.preferences.PreferencesView; +import io.bisq.gui.util.CurrencyList; import io.bisq.gui.util.CurrencyListItem; import io.bisq.gui.util.GUIUtil; import javafx.beans.property.ObjectProperty; @@ -53,8 +54,6 @@ import java.util.Optional; import java.util.stream.Collectors; class OfferBookChartViewModel extends ActivatableViewModel { - private static final Logger log = LoggerFactory.getLogger(OfferBookChartViewModel.class); - private static final int TAB_INDEX = 0; private final OfferBook offerBook; @@ -67,7 +66,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { private final List sellData = new ArrayList<>(); private final ObservableList offerBookListItems; private final ListChangeListener offerBookListItemsListener; - final ObservableList currencyListItems = FXCollections.observableArrayList(); + final CurrencyList currencyListItems; private final ObservableList topBuyOfferList = FXCollections.observableArrayList(); private final ObservableList topSellOfferList = FXCollections.observableArrayList(); private final ChangeListener currenciesUpdatedListener; @@ -120,6 +119,8 @@ class OfferBookChartViewModel extends ActivatableViewModel { } } }; + + this.currencyListItems = new CurrencyList(preferences); } private void fillTradeCurrencies() { @@ -137,7 +138,7 @@ class OfferBookChartViewModel extends ActivatableViewModel { .filter(e -> e != null) .collect(Collectors.toList()); - GUIUtil.fillCurrencyListItems(tradeCurrencyList, currencyListItems, null, preferences); + currencyListItems.updateWithCurrencies(tradeCurrencyList, null); } @Override diff --git a/gui/src/main/java/io/bisq/gui/main/market/trades/TradesChartsViewModel.java b/gui/src/main/java/io/bisq/gui/main/market/trades/TradesChartsViewModel.java index 7ae616df09..a313ca058b 100644 --- a/gui/src/main/java/io/bisq/gui/main/market/trades/TradesChartsViewModel.java +++ b/gui/src/main/java/io/bisq/gui/main/market/trades/TradesChartsViewModel.java @@ -36,6 +36,7 @@ import io.bisq.gui.main.market.trades.charts.CandleData; import io.bisq.gui.main.settings.SettingsView; import io.bisq.gui.main.settings.preferences.PreferencesView; import io.bisq.gui.util.BSFormatter; +import io.bisq.gui.util.CurrencyList; import io.bisq.gui.util.CurrencyListItem; import io.bisq.gui.util.GUIUtil; import javafx.beans.property.BooleanProperty; @@ -82,7 +83,7 @@ class TradesChartsViewModel extends ActivatableViewModel { private final SetChangeListener setChangeListener; final ObjectProperty selectedTradeCurrencyProperty = new SimpleObjectProperty<>(); final BooleanProperty showAllTradeCurrenciesProperty = new SimpleBooleanProperty(false); - private final ObservableList currencyListItems = FXCollections.observableArrayList(); + private final CurrencyList currencyListItems; private final CurrencyListItem showAllCurrencyListItem = new CurrencyListItem(new CryptoCurrency(GUIUtil.SHOW_ALL_FLAG, GUIUtil.SHOW_ALL_FLAG), -1); final ObservableList tradeStatisticsByCurrency = FXCollections.observableArrayList(); final ObservableList> priceItems = FXCollections.observableArrayList(); @@ -119,6 +120,8 @@ class TradesChartsViewModel extends ActivatableViewModel { selectedTradeCurrencyProperty.set(GlobalSettings.getDefaultTradeCurrency()); tickUnit = TickUnit.values()[preferences.getTradeStatisticsTickUnitIndex()]; + + currencyListItems = new CurrencyList(this.preferences); } private void fillTradeCurrencies() { @@ -135,7 +138,7 @@ class TradesChartsViewModel extends ActivatableViewModel { .filter(e -> e != null) .collect(Collectors.toList()); - GUIUtil.fillCurrencyListItems(tradeCurrencyList, currencyListItems, showAllCurrencyListItem, preferences); + currencyListItems.updateWithCurrencies(tradeCurrencyList, showAllCurrencyListItem); } @Override diff --git a/gui/src/main/java/io/bisq/gui/util/CurrencyList.java b/gui/src/main/java/io/bisq/gui/util/CurrencyList.java new file mode 100644 index 0000000000..940a163146 --- /dev/null +++ b/gui/src/main/java/io/bisq/gui/util/CurrencyList.java @@ -0,0 +1,103 @@ +/* + * 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 io.bisq.gui.util; + +import com.google.common.collect.Lists; +import com.sun.javafx.collections.ObservableListWrapper; +import io.bisq.common.locale.TradeCurrency; +import io.bisq.core.user.Preferences; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.function.BiFunction; + +public class CurrencyList extends ObservableListWrapper { + private final CurrencyPredicates predicates; + private final Preferences preferences; + + public CurrencyList(Preferences preferences) { + this(new ArrayList<>(), preferences, new CurrencyPredicates()); + } + + CurrencyList(List delegate, Preferences preferences, CurrencyPredicates predicates) { + super(delegate); + this.predicates = predicates; + this.preferences = preferences; + } + + public void updateWithCurrencies(List currencies, @Nullable CurrencyListItem first) { + List result = Lists.newLinkedList(); + Optional.ofNullable(first).ifPresent(result::add); + result.addAll(getPartitionedSortedItems(currencies)); + setAll(result); + } + + private List getPartitionedSortedItems(List currencies) { + Map tradesPerCurrency = countTrades(currencies); + + Comparator comparator = getComparator(); + Queue fiatCurrencies = new PriorityQueue<>(comparator); + Queue cryptoCurrencies = new PriorityQueue<>(comparator); + + for (Map.Entry entry : tradesPerCurrency.entrySet()) { + TradeCurrency currency = entry.getKey(); + Integer count = entry.getValue(); + CurrencyListItem item = new CurrencyListItem(currency, count); + + if (predicates.isFiatCurrency(currency)) { + fiatCurrencies.add(item); + } + + if (predicates.isCryptoCurrency(currency)) { + cryptoCurrencies.add(item); + } + } + + List result = Lists.newLinkedList(); + result.addAll(fiatCurrencies); + result.addAll(cryptoCurrencies); + + return result; + } + + private Comparator getComparator() { + Comparator result; + if (preferences.isSortMarketCurrenciesNumerically()) { + Comparator byCount = Comparator.comparingInt(left -> left.numTrades); + result = byCount.reversed(); + } else { + result = Comparator.comparing(item -> item.tradeCurrency); + } + return result; + } + + private Map countTrades(List currencies) { + Map result = new HashMap<>(); + + BiFunction incrementCurrentOrOne = + (key, value) -> value == null ? 1 : value + 1; + currencies.forEach(currency -> result.compute(currency, incrementCurrentOrOne)); + + Set preferred = new HashSet<>(); + preferred.addAll(preferences.getFiatCurrencies()); + preferred.addAll(preferences.getCryptoCurrencies()); + preferred.forEach(currency -> result.putIfAbsent(currency, 0)); + + return result; + } +} diff --git a/gui/src/main/java/io/bisq/gui/util/CurrencyPredicates.java b/gui/src/main/java/io/bisq/gui/util/CurrencyPredicates.java new file mode 100644 index 0000000000..bf1918d4e6 --- /dev/null +++ b/gui/src/main/java/io/bisq/gui/util/CurrencyPredicates.java @@ -0,0 +1,31 @@ +/* + * 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 io.bisq.gui.util; + +import io.bisq.common.locale.CurrencyUtil; +import io.bisq.common.locale.TradeCurrency; + +class CurrencyPredicates { + boolean isCryptoCurrency(TradeCurrency currency) { + return CurrencyUtil.isCryptoCurrency(currency.getCode()); + } + + boolean isFiatCurrency(TradeCurrency currency) { + return CurrencyUtil.isFiatCurrency(currency.getCode()); + } +} diff --git a/gui/src/main/java/io/bisq/gui/util/GUIUtil.java b/gui/src/main/java/io/bisq/gui/util/GUIUtil.java index 3e499382dc..244458f401 100644 --- a/gui/src/main/java/io/bisq/gui/util/GUIUtil.java +++ b/gui/src/main/java/io/bisq/gui/util/GUIUtil.java @@ -255,60 +255,6 @@ public class GUIUtil { }; } - // TODO could be done more elegantly... - public static void fillCurrencyListItems(List tradeCurrencyList, - ObservableList currencyListItems, - @Nullable CurrencyListItem showAllCurrencyListItem, - Preferences preferences) { - Map tradesPerCurrencyMap = new HashMap<>(); - Set tradeCurrencySet = new HashSet<>(); - - // We get the list of all offers or trades. We want to find out how many items at each currency we have. - tradeCurrencyList.stream().forEach(tradeCurrency -> { - tradeCurrencySet.add(tradeCurrency); - String code = tradeCurrency.getCode(); - if (tradesPerCurrencyMap.containsKey(code)) - tradesPerCurrencyMap.put(code, tradesPerCurrencyMap.get(code) + 1); - else - tradesPerCurrencyMap.put(code, 1); - }); - - Set userSet = new HashSet<>(preferences.getFiatCurrencies()); - userSet.addAll(preferences.getCryptoCurrencies()); - // Now all those items which are not in the offers or trades list but comes from the user preferred currency list - // will get set to 0 - userSet.stream().forEach(tradeCurrency -> { - tradeCurrencySet.add(tradeCurrency); - String code = tradeCurrency.getCode(); - if (!tradesPerCurrencyMap.containsKey(code)) - tradesPerCurrencyMap.put(code, 0); - }); - - List list = tradeCurrencySet.stream() - .filter(e -> CurrencyUtil.isFiatCurrency(e.getCode())) - .map(e -> new CurrencyListItem(e, tradesPerCurrencyMap.get(e.getCode()))) - .collect(Collectors.toList()); - List cryptoList = tradeCurrencySet.stream() - .filter(e -> CurrencyUtil.isCryptoCurrency(e.getCode())) - .map(e -> new CurrencyListItem(e, tradesPerCurrencyMap.get(e.getCode()))) - .collect(Collectors.toList()); - - if (preferences.isSortMarketCurrenciesNumerically()) { - list.sort((o1, o2) -> new Integer(o2.numTrades).compareTo(o1.numTrades)); - cryptoList.sort((o1, o2) -> new Integer(o2.numTrades).compareTo(o1.numTrades)); - } else { - list.sort((o1, o2) -> o1.tradeCurrency.compareTo(o2.tradeCurrency)); - cryptoList.sort((o1, o2) -> o1.tradeCurrency.compareTo(o2.tradeCurrency)); - } - - list.addAll(cryptoList); - - if (showAllCurrencyListItem != null) - list.add(0, showAllCurrencyListItem); - - currencyListItems.setAll(list); - } - public static void updateConfidence(TransactionConfidence confidence, Tooltip tooltip, TxConfidenceIndicator txConfidenceIndicator) { if (confidence != null) { switch (confidence.getConfidenceType()) { diff --git a/gui/src/test/java/io/bisq/common/locale/TradeCurrencyMakers.java b/gui/src/test/java/io/bisq/common/locale/TradeCurrencyMakers.java index e73bff22c7..42c3ac38fc 100644 --- a/gui/src/test/java/io/bisq/common/locale/TradeCurrencyMakers.java +++ b/gui/src/test/java/io/bisq/common/locale/TradeCurrencyMakers.java @@ -1,7 +1,6 @@ package io.bisq.common.locale; import com.natpryce.makeiteasy.Instantiator; -import com.natpryce.makeiteasy.Maker; import com.natpryce.makeiteasy.Property; import static com.natpryce.makeiteasy.MakeItEasy.a; diff --git a/gui/src/test/java/io/bisq/gui/util/CurrencyListTest.java b/gui/src/test/java/io/bisq/gui/util/CurrencyListTest.java new file mode 100644 index 0000000000..2bda01396c --- /dev/null +++ b/gui/src/test/java/io/bisq/gui/util/CurrencyListTest.java @@ -0,0 +1,104 @@ +package io.bisq.gui.util; + +import com.google.common.collect.Lists; +import io.bisq.common.locale.CryptoCurrency; +import io.bisq.common.locale.FiatCurrency; +import io.bisq.common.locale.TradeCurrency; +import io.bisq.core.user.Preferences; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.modules.junit4.PowerMockRunner; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(PowerMockRunner.class) +@PrepareForTest(Preferences.class) +public class CurrencyListTest { + private static final TradeCurrency USD = new FiatCurrency("USD"); + private static final TradeCurrency RUR = new FiatCurrency("RUR"); + private static final TradeCurrency BTC = new CryptoCurrency("BTC", "Bitcoin"); + private static final TradeCurrency ETH = new CryptoCurrency("ETH", "Ether"); + private static final TradeCurrency BSQ = new CryptoCurrency("BSQ", "Bisq Token"); + + private Preferences preferences; + private List delegate; + private CurrencyList testedEntity; + + @Before + public void setUp() { + Locale.setDefault(new Locale("en", "US")); + + CurrencyPredicates predicates = mock(CurrencyPredicates.class); + when(predicates.isCryptoCurrency(USD)).thenReturn(false); + when(predicates.isCryptoCurrency(RUR)).thenReturn(false); + when(predicates.isCryptoCurrency(BTC)).thenReturn(true); + when(predicates.isCryptoCurrency(ETH)).thenReturn(true); + + when(predicates.isFiatCurrency(USD)).thenReturn(true); + when(predicates.isFiatCurrency(RUR)).thenReturn(true); + when(predicates.isFiatCurrency(BTC)).thenReturn(false); + when(predicates.isFiatCurrency(ETH)).thenReturn(false); + + this.preferences = mock(Preferences.class); + this.delegate = new ArrayList<>(); + this.testedEntity = new CurrencyList(delegate, preferences, predicates); + } + + @Test + public void testUpdateWhenSortNumerically() { + when(preferences.isSortMarketCurrenciesNumerically()).thenReturn(true); + + List currencies = Lists.newArrayList(USD, RUR, USD, ETH, ETH, BTC); + testedEntity.updateWithCurrencies(currencies, null); + + List expected = Lists.newArrayList( + new CurrencyListItem(USD, 2), + new CurrencyListItem(RUR, 1), + new CurrencyListItem(ETH, 2), + new CurrencyListItem(BTC, 1)); + + assertEquals(expected, delegate); + } + + @Test + public void testUpdateWhenNotSortNumerically() { + when(preferences.isSortMarketCurrenciesNumerically()).thenReturn(false); + + List currencies = Lists.newArrayList(USD, RUR, USD, ETH, ETH, BTC); + testedEntity.updateWithCurrencies(currencies, null); + + List expected = Lists.newArrayList( + new CurrencyListItem(RUR, 1), + new CurrencyListItem(USD, 2), + new CurrencyListItem(BTC, 1), + new CurrencyListItem(ETH, 2)); + + assertEquals(expected, delegate); + } + + @Test + public void testUpdateWhenSortNumericallyAndFirstSpecified() { + when(preferences.isSortMarketCurrenciesNumerically()).thenReturn(true); + + List currencies = Lists.newArrayList(USD, RUR, USD, ETH, ETH, BTC); + CurrencyListItem first = new CurrencyListItem(BSQ, 5); + testedEntity.updateWithCurrencies(currencies, first); + + List expected = Lists.newArrayList( + first, + new CurrencyListItem(USD, 2), + new CurrencyListItem(RUR, 1), + new CurrencyListItem(ETH, 2), + new CurrencyListItem(BTC, 1)); + + assertEquals(expected, delegate); + } +} diff --git a/gui/src/test/java/io/bisq/gui/util/GUIUtilTest.java b/gui/src/test/java/io/bisq/gui/util/GUIUtilTest.java index c110dde640..0f278e1922 100644 --- a/gui/src/test/java/io/bisq/gui/util/GUIUtilTest.java +++ b/gui/src/test/java/io/bisq/gui/util/GUIUtilTest.java @@ -2,14 +2,10 @@ package io.bisq.gui.util; import io.bisq.common.locale.Res; import io.bisq.common.locale.TradeCurrency; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; import javafx.util.StringConverter; import org.junit.Before; import org.junit.Test; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import static com.natpryce.makeiteasy.MakeItEasy.make; @@ -17,10 +13,8 @@ import static com.natpryce.makeiteasy.MakeItEasy.with; import static io.bisq.common.locale.TradeCurrencyMakers.bitcoin; import static io.bisq.common.locale.TradeCurrencyMakers.euro; import static io.bisq.core.user.PreferenceMakers.empty; -import static io.bisq.gui.util.CurrencyListItemMakers.bitcoinItem; -import static io.bisq.gui.util.CurrencyListItemMakers.euroItem; -import static io.bisq.gui.util.CurrencyListItemMakers.numberOfTrades; -import static org.junit.Assert.*; +import static io.bisq.gui.util.CurrencyListItemMakers.*; +import static org.junit.Assert.assertEquals; public class GUIUtilTest { @@ -50,15 +44,4 @@ public class GUIUtilTest { assertEquals("★ Euro (EUR) - 1 offer", currencyListItemConverter.toString(make(euroItem.but(with(numberOfTrades, 1))))); } - - @Test - public void testFillCurrencyListItems() { - ObservableList currencyListItems = FXCollections.observableArrayList(); - List tradeCurrencyList = new ArrayList<>(); - tradeCurrencyList.add(euro); - CurrencyListItem euroListItem = make(euroItem.but(with(numberOfTrades,1))); - - GUIUtil.fillCurrencyListItems(tradeCurrencyList, currencyListItems, null, empty ); - assertTrue(euroListItem.equals(currencyListItems.get(0))); - } }