mirror of
https://github.com/haveno-dex/haveno.git
synced 2025-08-05 05:04:15 -04:00
support cloning up to 10 offers with shared reserved funds (#1668)
This commit is contained in:
parent
7e3a47de4a
commit
40e18890d6
55 changed files with 2006 additions and 611 deletions
|
@ -366,7 +366,7 @@ public class HavenoApp extends Application implements UncaughtExceptionHandler {
|
|||
}
|
||||
|
||||
// check for open offers
|
||||
if (injector.getInstance(OpenOfferManager.class).hasOpenOffers()) {
|
||||
if (injector.getInstance(OpenOfferManager.class).hasAvailableOpenOffers()) {
|
||||
String key = "showOpenOfferWarnPopupAtShutDown";
|
||||
if (injector.getInstance(Preferences.class).showAgain(key) && !DevEnv.isDevMode()) {
|
||||
new Popup().warning(Res.get("popup.info.shutDownWithOpenOffers"))
|
||||
|
|
|
@ -822,6 +822,10 @@ tree-table-view:focused {
|
|||
-fx-text-fill: -bs-rd-error-red;
|
||||
}
|
||||
|
||||
.icon {
|
||||
-fx-fill: -bs-text-color;
|
||||
}
|
||||
|
||||
.opaque-icon {
|
||||
-fx-fill: -bs-color-gray-bbb;
|
||||
-fx-opacity: 1;
|
||||
|
|
|
@ -346,7 +346,7 @@ public class DepositView extends ActivatableView<VBox, Void> {
|
|||
List<XmrAddressEntry> addressEntries = xmrWalletService.getAddressEntries();
|
||||
List<DepositListItem> items = new ArrayList<>();
|
||||
for (XmrAddressEntry addressEntry : addressEntries) {
|
||||
if (addressEntry.isTrade()) continue; // skip reserved for trade
|
||||
if (addressEntry.isTradePayout()) continue; // do not show trade payout addresses
|
||||
items.add(new DepositListItem(addressEntry, xmrWalletService, formatter));
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ class WithdrawalListItem {
|
|||
public final String getLabel() {
|
||||
if (addressEntry.isOpenOffer())
|
||||
return Res.getWithCol("shared.offerId") + " " + addressEntry.getShortOfferId();
|
||||
else if (addressEntry.isTrade())
|
||||
else if (addressEntry.isTradePayout())
|
||||
return Res.getWithCol("shared.tradeId") + " " + addressEntry.getShortOfferId();
|
||||
else if (addressEntry.getContext() == XmrAddressEntry.Context.ARBITRATOR)
|
||||
return Res.get("funds.withdrawal.arbitrationFee");
|
||||
|
|
|
@ -82,7 +82,7 @@ import lombok.Getter;
|
|||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public abstract class MutableOfferDataModel extends OfferDataModel {
|
||||
private final CreateOfferService createOfferService;
|
||||
protected final CreateOfferService createOfferService;
|
||||
protected final OpenOfferManager openOfferManager;
|
||||
private final XmrWalletService xmrWalletService;
|
||||
private final Preferences preferences;
|
||||
|
@ -115,7 +115,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
|
||||
protected PaymentAccount paymentAccount;
|
||||
boolean isTabSelected;
|
||||
protected double marketPriceMargin = 0;
|
||||
protected double marketPriceMarginPct = 0;
|
||||
@Getter
|
||||
private boolean marketPriceAvailable;
|
||||
protected boolean allowAmountUpdate = true;
|
||||
|
@ -189,12 +189,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
}
|
||||
|
||||
private void addListeners() {
|
||||
xmrWalletService.addBalanceListener(xmrBalanceListener);
|
||||
if (xmrBalanceListener != null) xmrWalletService.addBalanceListener(xmrBalanceListener);
|
||||
user.getPaymentAccountsAsObservable().addListener(paymentAccountsChangeListener);
|
||||
}
|
||||
|
||||
private void removeListeners() {
|
||||
xmrWalletService.removeBalanceListener(xmrBalanceListener);
|
||||
if (xmrBalanceListener != null) xmrWalletService.removeBalanceListener(xmrBalanceListener);
|
||||
user.getPaymentAccountsAsObservable().removeListener(paymentAccountsChangeListener);
|
||||
}
|
||||
|
||||
|
@ -204,14 +204,16 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// called before activate()
|
||||
public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) {
|
||||
addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) {
|
||||
@Override
|
||||
public void onBalanceChanged(BigInteger balance) {
|
||||
updateBalances();
|
||||
}
|
||||
};
|
||||
public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) {
|
||||
if (initAddressEntry) {
|
||||
addressEntry = xmrWalletService.getOrCreateAddressEntry(offerId, XmrAddressEntry.Context.OFFER_FUNDING);
|
||||
xmrBalanceListener = new XmrBalanceListener(getAddressEntry().getSubaddressIndex()) {
|
||||
@Override
|
||||
public void onBalanceChanged(BigInteger balance) {
|
||||
updateBalances();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.direction = direction;
|
||||
this.tradeCurrency = tradeCurrency;
|
||||
|
@ -278,6 +280,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
}
|
||||
|
||||
protected void updateBalances() {
|
||||
if (addressEntry == null) return;
|
||||
super.updateBalances();
|
||||
|
||||
// update remaining balance
|
||||
|
@ -302,7 +305,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
minAmount.get(),
|
||||
useMarketBasedPrice.get() ? null : price.get(),
|
||||
useMarketBasedPrice.get(),
|
||||
useMarketBasedPrice.get() ? marketPriceMargin : 0,
|
||||
useMarketBasedPrice.get() ? marketPriceMarginPct : 0,
|
||||
securityDepositPct.get(),
|
||||
paymentAccount,
|
||||
buyerAsTakerWithoutDeposit.get(), // private offer if buyer as taker without deposit
|
||||
|
@ -316,6 +319,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
triggerPrice,
|
||||
reserveExactAmount,
|
||||
false, // desktop ui resets address entries on cancel
|
||||
null,
|
||||
resultHandler,
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
@ -387,7 +391,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
volume.set(null);
|
||||
minVolume.set(null);
|
||||
price.set(null);
|
||||
marketPriceMargin = 0;
|
||||
marketPriceMarginPct = 0;
|
||||
}
|
||||
|
||||
this.tradeCurrency = tradeCurrency;
|
||||
|
@ -416,10 +420,6 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
updateBalances();
|
||||
}
|
||||
|
||||
protected void setMarketPriceMarginPct(double marketPriceMargin) {
|
||||
this.marketPriceMargin = marketPriceMargin;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Getters
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -469,7 +469,7 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
}
|
||||
|
||||
public double getMarketPriceMarginPct() {
|
||||
return marketPriceMargin;
|
||||
return marketPriceMarginPct;
|
||||
}
|
||||
|
||||
long getMaxTradeLimit() {
|
||||
|
@ -609,6 +609,10 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
this.triggerPrice = triggerPrice;
|
||||
}
|
||||
|
||||
public void setMarketPriceMarginPct(double marketPriceMarginPct) {
|
||||
this.marketPriceMarginPct = marketPriceMarginPct;
|
||||
}
|
||||
|
||||
public void setReserveExactAmount(boolean reserveExactAmount) {
|
||||
this.reserveExactAmount = reserveExactAmount;
|
||||
}
|
||||
|
@ -684,6 +688,14 @@ public abstract class MutableOfferDataModel extends OfferDataModel {
|
|||
return Restrictions.getMinSecurityDeposit().max(value);
|
||||
}
|
||||
|
||||
protected double getSecurityAsPercent(Offer offer) {
|
||||
BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit());
|
||||
double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit,
|
||||
offer.getAmount());
|
||||
return Math.min(offerSellerSecurityDepositAsPercent,
|
||||
Restrictions.getMaxSecurityDepositAsPercent());
|
||||
}
|
||||
|
||||
ReadOnlyObjectProperty<BigInteger> totalToPayAsProperty() {
|
||||
return totalToPay;
|
||||
}
|
||||
|
|
|
@ -297,11 +297,13 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
|
|||
model.getDataModel().onTabSelected(isSelected);
|
||||
}
|
||||
|
||||
public void initWithData(OfferDirection direction, TradeCurrency tradeCurrency,
|
||||
public void initWithData(OfferDirection direction,
|
||||
TradeCurrency tradeCurrency,
|
||||
boolean initAddressEntry,
|
||||
OfferView.OfferActionHandler offerActionHandler) {
|
||||
this.offerActionHandler = offerActionHandler;
|
||||
|
||||
boolean result = model.initWithData(direction, tradeCurrency);
|
||||
boolean result = model.initWithData(direction, tradeCurrency, initAddressEntry);
|
||||
|
||||
if (!result) {
|
||||
new Popup().headLine(Res.get("popup.warning.noTradingAccountSetup.headline"))
|
||||
|
|
|
@ -601,8 +601,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
|
|||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) {
|
||||
boolean result = dataModel.initWithData(direction, tradeCurrency);
|
||||
boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency, boolean initAddressEntry) {
|
||||
boolean result = dataModel.initWithData(direction, tradeCurrency, initAddressEntry);
|
||||
if (dataModel.getAddressEntry() != null) {
|
||||
addressAsString = dataModel.getAddressEntry().getAddressString();
|
||||
}
|
||||
|
|
|
@ -47,11 +47,10 @@ public class CreateOfferView extends MutableOfferView<CreateOfferViewModel> {
|
|||
super(model, navigation, preferences, offerDetailsWindow, btcFormatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initWithData(OfferDirection direction,
|
||||
TradeCurrency tradeCurrency,
|
||||
OfferView.OfferActionHandler offerActionHandler) {
|
||||
super.initWithData(direction, tradeCurrency, offerActionHandler);
|
||||
super.initWithData(direction, tradeCurrency, true, offerActionHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -19,13 +19,12 @@ package haveno.desktop.main.offer.offerbook;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import haveno.core.filter.FilterManager;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferBookService;
|
||||
import static haveno.core.offer.OfferDirection.BUY;
|
||||
import haveno.core.offer.OfferRestrictions;
|
||||
import haveno.network.p2p.storage.P2PDataStorage;
|
||||
import haveno.network.utils.Utils;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -49,7 +48,6 @@ public class OfferBook {
|
|||
private final ObservableList<OfferBookListItem> offerBookListItems = FXCollections.observableArrayList();
|
||||
private final Map<String, Integer> buyOfferCountMap = new HashMap<>();
|
||||
private final Map<String, Integer> sellOfferCountMap = new HashMap<>();
|
||||
private final FilterManager filterManager;
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -57,64 +55,47 @@ public class OfferBook {
|
|||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
OfferBook(OfferBookService offerBookService, FilterManager filterManager) {
|
||||
OfferBook(OfferBookService offerBookService) {
|
||||
this.offerBookService = offerBookService;
|
||||
this.filterManager = filterManager;
|
||||
|
||||
offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() {
|
||||
@Override
|
||||
public void onAdded(Offer offer) {
|
||||
printOfferBookListItems("Before onAdded");
|
||||
// We get onAdded called every time a new ProtectedStorageEntry is received.
|
||||
// Mostly it is the same OfferPayload but the ProtectedStorageEntry is different.
|
||||
// We filter here to only add new offers if the same offer (using equals) was not already added and it
|
||||
// is not banned.
|
||||
UserThread.execute(() -> {
|
||||
printOfferBookListItems("Before onAdded");
|
||||
|
||||
if (filterManager.isOfferIdBanned(offer.getId())) {
|
||||
log.debug("Ignored banned offer. ID={}", offer.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
if (OfferRestrictions.requiresNodeAddressUpdate() && !Utils.isV3Address(offer.getMakerNodeAddress().getHostName())) {
|
||||
log.debug("Ignored offer with Tor v2 node address. ID={}", offer.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// Use offer.equals(offer) to see if the OfferBook list contains an exact
|
||||
// match -- offer.equals(offer) includes comparisons of payload, state
|
||||
// and errorMessage.
|
||||
synchronized (offerBookListItems) {
|
||||
boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer));
|
||||
if (!hasSameOffer) {
|
||||
OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer);
|
||||
removeDuplicateItem(newOfferBookListItem);
|
||||
offerBookListItems.add(newOfferBookListItem); // Add replacement.
|
||||
if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
|
||||
log.debug("onAdded: Added new offer {}\n"
|
||||
+ "\twith newItem.payloadHash: {}",
|
||||
offer.getId(),
|
||||
newOfferBookListItem.hashOfPayload.getHex());
|
||||
// Use offer.equals(offer) to see if the OfferBook list contains an exact
|
||||
// match -- offer.equals(offer) includes comparisons of payload, state
|
||||
// and errorMessage.
|
||||
synchronized (offerBookListItems) {
|
||||
boolean hasSameOffer = offerBookListItems.stream().anyMatch(item -> item.getOffer().equals(offer));
|
||||
if (!hasSameOffer) {
|
||||
OfferBookListItem newOfferBookListItem = new OfferBookListItem(offer);
|
||||
removeDuplicateItem(newOfferBookListItem);
|
||||
offerBookListItems.add(newOfferBookListItem); // Add replacement.
|
||||
if (log.isDebugEnabled()) { // TODO delete debug stmt in future PR.
|
||||
log.debug("onAdded: Added new offer {}\n"
|
||||
+ "\twith newItem.payloadHash: {}",
|
||||
offer.getId(),
|
||||
newOfferBookListItem.hashOfPayload.getHex());
|
||||
}
|
||||
} else {
|
||||
log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId());
|
||||
}
|
||||
} else {
|
||||
log.debug("We have the exact same offer already in our list and ignore the onAdded call. ID={}", offer.getId());
|
||||
printOfferBookListItems("After onAdded");
|
||||
}
|
||||
printOfferBookListItems("After onAdded");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoved(Offer offer) {
|
||||
synchronized (offerBookListItems) {
|
||||
printOfferBookListItems("Before onRemoved");
|
||||
removeOffer(offer);
|
||||
printOfferBookListItems("After onRemoved");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
filterManager.filterProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (newValue != null) {
|
||||
// any notifications
|
||||
UserThread.execute(() -> {
|
||||
synchronized (offerBookListItems) {
|
||||
printOfferBookListItems("Before onRemoved");
|
||||
removeOffer(offer);
|
||||
printOfferBookListItems("After onRemoved");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -212,7 +193,6 @@ public class OfferBook {
|
|||
// Investigate why....
|
||||
offerBookListItems.clear();
|
||||
offerBookListItems.addAll(offerBookService.getOffers().stream()
|
||||
.filter(this::isOfferAllowed)
|
||||
.map(OfferBookListItem::new)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
|
@ -248,13 +228,6 @@ public class OfferBook {
|
|||
return sellOfferCountMap;
|
||||
}
|
||||
|
||||
private boolean isOfferAllowed(Offer offer) {
|
||||
boolean isBanned = filterManager.isOfferIdBanned(offer.getId());
|
||||
boolean isV3NodeAddressCompliant = !OfferRestrictions.requiresNodeAddressUpdate()
|
||||
|| Utils.isV3Address(offer.getMakerNodeAddress().getHostName());
|
||||
return !isBanned && isV3NodeAddressCompliant;
|
||||
}
|
||||
|
||||
private void fillOfferCountMaps() {
|
||||
buyOfferCountMap.clear();
|
||||
sellOfferCountMap.clear();
|
||||
|
|
|
@ -695,8 +695,13 @@ abstract public class OfferBookView<R extends GridPane, M extends OfferBookViewM
|
|||
case SIGNATURE_NOT_VALIDATED:
|
||||
new Popup().warning(Res.get("offerbook.warning.signatureNotValidated")).show();
|
||||
break;
|
||||
case RESERVE_FUNDS_SPENT:
|
||||
new Popup().warning(Res.get("offerbook.warning.reserveFundsSpent")).show();
|
||||
break;
|
||||
case VALID:
|
||||
break;
|
||||
default:
|
||||
log.warn("Unhandled offer filter service result: " + result);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -173,6 +173,11 @@ abstract class OfferBookViewModel extends ActivatableViewModel {
|
|||
|
||||
tradeCurrencyListChangeListener = c -> fillCurrencies();
|
||||
|
||||
// refresh filter on changes
|
||||
offerBook.getOfferBookListItems().addListener((ListChangeListener<OfferBookListItem>) c -> {
|
||||
filterOffers();
|
||||
});
|
||||
|
||||
filterItemsListener = c -> {
|
||||
final Optional<OfferBookListItem> highestAmountOffer = filteredItems.stream()
|
||||
.max(Comparator.comparingLong(o -> o.getOffer().getAmount().longValueExact()));
|
||||
|
|
|
@ -30,6 +30,8 @@ import haveno.desktop.common.view.CachingViewLoader;
|
|||
import haveno.desktop.common.view.FxmlView;
|
||||
import haveno.desktop.common.view.View;
|
||||
import haveno.desktop.main.MainView;
|
||||
import haveno.desktop.main.overlays.popups.Popup;
|
||||
import haveno.desktop.main.portfolio.cloneoffer.CloneOfferView;
|
||||
import haveno.desktop.main.portfolio.closedtrades.ClosedTradesView;
|
||||
import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
|
||||
import haveno.desktop.main.portfolio.editoffer.EditOfferView;
|
||||
|
@ -49,7 +51,7 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
|
||||
@FXML
|
||||
Tab openOffersTab, pendingTradesTab, closedTradesTab;
|
||||
private Tab editOpenOfferTab, duplicateOfferTab;
|
||||
private Tab editOpenOfferTab, duplicateOfferTab, cloneOpenOfferTab;
|
||||
private final Tab failedTradesTab = new Tab(Res.get("portfolio.tab.failed").toUpperCase());
|
||||
private Tab currentTab;
|
||||
private Navigation.Listener navigationListener;
|
||||
|
@ -61,7 +63,8 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
private final FailedTradesManager failedTradesManager;
|
||||
private EditOfferView editOfferView;
|
||||
private DuplicateOfferView duplicateOfferView;
|
||||
private boolean editOpenOfferViewOpen;
|
||||
private CloneOfferView cloneOfferView;
|
||||
private boolean editOpenOfferViewOpen, cloneOpenOfferViewOpen;
|
||||
private OpenOffer openOffer;
|
||||
private OpenOffersView openOffersView;
|
||||
|
||||
|
@ -99,12 +102,16 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
navigation.navigateTo(MainView.class, PortfolioView.class, EditOfferView.class);
|
||||
else if (newValue == duplicateOfferTab) {
|
||||
navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class);
|
||||
} else if (newValue == cloneOpenOfferTab) {
|
||||
navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class);
|
||||
}
|
||||
|
||||
if (oldValue != null && oldValue == editOpenOfferTab)
|
||||
editOfferView.onTabSelected(false);
|
||||
if (oldValue != null && oldValue == duplicateOfferTab)
|
||||
duplicateOfferView.onTabSelected(false);
|
||||
if (oldValue != null && oldValue == cloneOpenOfferTab)
|
||||
cloneOfferView.onTabSelected(false);
|
||||
|
||||
};
|
||||
|
||||
|
@ -115,6 +122,8 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
onEditOpenOfferRemoved();
|
||||
if (removedTabs.size() == 1 && removedTabs.get(0).equals(duplicateOfferTab))
|
||||
onDuplicateOfferRemoved();
|
||||
if (removedTabs.size() == 1 && removedTabs.get(0).equals(cloneOpenOfferTab))
|
||||
onCloneOpenOfferRemoved();
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -137,6 +146,16 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class);
|
||||
}
|
||||
|
||||
private void onCloneOpenOfferRemoved() {
|
||||
cloneOpenOfferViewOpen = false;
|
||||
if (cloneOfferView != null) {
|
||||
cloneOfferView.onClose();
|
||||
cloneOfferView = null;
|
||||
}
|
||||
|
||||
navigation.navigateTo(MainView.class, this.getClass(), OpenOffersView.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate() {
|
||||
failedTradesManager.getObservableList().addListener((ListChangeListener<Trade>) c -> {
|
||||
|
@ -166,6 +185,9 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
} else if (root.getSelectionModel().getSelectedItem() == duplicateOfferTab) {
|
||||
navigation.navigateTo(MainView.class, PortfolioView.class, DuplicateOfferView.class);
|
||||
if (duplicateOfferView != null) duplicateOfferView.onTabSelected(true);
|
||||
} else if (root.getSelectionModel().getSelectedItem() == cloneOpenOfferTab) {
|
||||
navigation.navigateTo(MainView.class, PortfolioView.class, CloneOfferView.class);
|
||||
if (cloneOfferView != null) cloneOfferView.onTabSelected(true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -178,10 +200,9 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
}
|
||||
|
||||
private void loadView(Class<? extends View> viewClass, @Nullable Object data) {
|
||||
// we want to get activate/deactivate called, so we remove the old view on tab change
|
||||
// TODO Don't understand the check for currentTab != editOpenOfferTab
|
||||
if (currentTab != null && currentTab != editOpenOfferTab)
|
||||
currentTab.setContent(null);
|
||||
|
||||
// nullify current tab to trigger activate/deactivate
|
||||
if (currentTab != null) currentTab.setContent(null);
|
||||
|
||||
View view = viewLoader.load(viewClass);
|
||||
|
||||
|
@ -235,6 +256,28 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
view = viewLoader.load(OpenOffersView.class);
|
||||
selectOpenOffersView((OpenOffersView) view);
|
||||
}
|
||||
} else if (view instanceof CloneOfferView) {
|
||||
if (data instanceof OpenOffer) {
|
||||
openOffer = (OpenOffer) data;
|
||||
}
|
||||
if (openOffer != null) {
|
||||
if (cloneOfferView == null) {
|
||||
cloneOfferView = (CloneOfferView) view;
|
||||
cloneOfferView.applyOpenOffer(openOffer);
|
||||
cloneOpenOfferTab = new Tab(Res.get("portfolio.tab.cloneOpenOffer").toUpperCase());
|
||||
cloneOfferView.setCloseHandler(() -> {
|
||||
root.getTabs().remove(cloneOpenOfferTab);
|
||||
});
|
||||
root.getTabs().add(cloneOpenOfferTab);
|
||||
}
|
||||
if (currentTab != cloneOpenOfferTab)
|
||||
cloneOfferView.onTabSelected(true);
|
||||
|
||||
currentTab = cloneOpenOfferTab;
|
||||
} else {
|
||||
view = viewLoader.load(OpenOffersView.class);
|
||||
selectOpenOffersView((OpenOffersView) view);
|
||||
}
|
||||
}
|
||||
|
||||
currentTab.setContent(view.getRoot());
|
||||
|
@ -245,20 +288,35 @@ public class PortfolioView extends ActivatableView<TabPane, Void> {
|
|||
openOffersView = view;
|
||||
currentTab = openOffersTab;
|
||||
|
||||
OpenOfferActionHandler openOfferActionHandler = openOffer -> {
|
||||
EditOpenOfferHandler editOpenOfferHandler = openOffer -> {
|
||||
if (!editOpenOfferViewOpen) {
|
||||
editOpenOfferViewOpen = true;
|
||||
PortfolioView.this.openOffer = openOffer;
|
||||
navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), EditOfferView.class);
|
||||
} else {
|
||||
log.error("You have already a \"Edit Offer\" tab open.");
|
||||
new Popup().warning(Res.get("editOffer.openTabWarning")).show();
|
||||
}
|
||||
};
|
||||
openOffersView.setOpenOfferActionHandler(openOfferActionHandler);
|
||||
openOffersView.setEditOpenOfferHandler(editOpenOfferHandler);
|
||||
|
||||
CloneOpenOfferHandler cloneOpenOfferHandler = openOffer -> {
|
||||
if (!cloneOpenOfferViewOpen) {
|
||||
cloneOpenOfferViewOpen = true;
|
||||
PortfolioView.this.openOffer = openOffer;
|
||||
navigation.navigateTo(MainView.class, PortfolioView.this.getClass(), CloneOfferView.class);
|
||||
} else {
|
||||
new Popup().warning(Res.get("cloneOffer.openTabWarning")).show();
|
||||
}
|
||||
};
|
||||
openOffersView.setCloneOpenOfferHandler(cloneOpenOfferHandler);
|
||||
}
|
||||
|
||||
public interface OpenOfferActionHandler {
|
||||
public interface EditOpenOfferHandler {
|
||||
void onEditOpenOffer(OpenOffer openOffer);
|
||||
}
|
||||
|
||||
public interface CloneOpenOfferHandler {
|
||||
void onCloneOpenOffer(OpenOffer openOffer);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.desktop.main.portfolio.cloneoffer;
|
||||
|
||||
|
||||
import haveno.desktop.Navigation;
|
||||
import haveno.desktop.main.offer.MutableOfferDataModel;
|
||||
import haveno.core.account.witness.AccountAgeWitnessService;
|
||||
import haveno.core.locale.CurrencyUtil;
|
||||
import haveno.core.locale.TradeCurrency;
|
||||
import haveno.core.offer.CreateOfferService;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferDirection;
|
||||
import haveno.core.offer.OfferUtil;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
import haveno.core.payment.PaymentAccount;
|
||||
import haveno.core.proto.persistable.CorePersistenceProtoResolver;
|
||||
import haveno.core.provider.price.PriceFeedService;
|
||||
import haveno.core.trade.statistics.TradeStatisticsManager;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.user.User;
|
||||
import haveno.core.util.FormattingUtils;
|
||||
import haveno.core.util.coin.CoinFormatter;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.network.p2p.P2PService;
|
||||
|
||||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
import haveno.common.handlers.ResultHandler;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
class CloneOfferDataModel extends MutableOfferDataModel {
|
||||
|
||||
private final CorePersistenceProtoResolver corePersistenceProtoResolver;
|
||||
private OpenOffer sourceOpenOffer;
|
||||
|
||||
@Inject
|
||||
CloneOfferDataModel(CreateOfferService createOfferService,
|
||||
OpenOfferManager openOfferManager,
|
||||
OfferUtil offerUtil,
|
||||
XmrWalletService xmrWalletService,
|
||||
Preferences preferences,
|
||||
User user,
|
||||
P2PService p2PService,
|
||||
PriceFeedService priceFeedService,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter xmrFormatter,
|
||||
CorePersistenceProtoResolver corePersistenceProtoResolver,
|
||||
TradeStatisticsManager tradeStatisticsManager,
|
||||
Navigation navigation) {
|
||||
|
||||
super(createOfferService,
|
||||
openOfferManager,
|
||||
offerUtil,
|
||||
xmrWalletService,
|
||||
preferences,
|
||||
user,
|
||||
p2PService,
|
||||
priceFeedService,
|
||||
accountAgeWitnessService,
|
||||
xmrFormatter,
|
||||
tradeStatisticsManager,
|
||||
navigation);
|
||||
this.corePersistenceProtoResolver = corePersistenceProtoResolver;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
direction = null;
|
||||
tradeCurrency = null;
|
||||
tradeCurrencyCode.set(null);
|
||||
useMarketBasedPrice.set(false);
|
||||
amount.set(null);
|
||||
minAmount.set(null);
|
||||
price.set(null);
|
||||
volume.set(null);
|
||||
minVolume.set(null);
|
||||
securityDepositPct.set(0);
|
||||
paymentAccounts.clear();
|
||||
paymentAccount = null;
|
||||
marketPriceMarginPct = 0;
|
||||
sourceOpenOffer = null;
|
||||
}
|
||||
|
||||
public void applyOpenOffer(OpenOffer openOffer) {
|
||||
this.sourceOpenOffer = openOffer;
|
||||
|
||||
Offer offer = openOffer.getOffer();
|
||||
direction = offer.getDirection();
|
||||
CurrencyUtil.getTradeCurrency(offer.getCurrencyCode())
|
||||
.ifPresent(c -> this.tradeCurrency = c);
|
||||
tradeCurrencyCode.set(offer.getCurrencyCode());
|
||||
|
||||
PaymentAccount tmpPaymentAccount = user.getPaymentAccount(openOffer.getOffer().getMakerPaymentAccountId());
|
||||
Optional<TradeCurrency> optionalTradeCurrency = CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode());
|
||||
if (optionalTradeCurrency.isPresent() && tmpPaymentAccount != null) {
|
||||
TradeCurrency selectedTradeCurrency = optionalTradeCurrency.get();
|
||||
this.paymentAccount = PaymentAccount.fromProto(tmpPaymentAccount.toProtoMessage(), corePersistenceProtoResolver);
|
||||
if (paymentAccount.getSingleTradeCurrency() != null)
|
||||
paymentAccount.setSingleTradeCurrency(selectedTradeCurrency);
|
||||
else
|
||||
paymentAccount.setSelectedTradeCurrency(selectedTradeCurrency);
|
||||
}
|
||||
|
||||
allowAmountUpdate = false;
|
||||
}
|
||||
|
||||
public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) {
|
||||
try {
|
||||
return super.initWithData(direction, tradeCurrency, false);
|
||||
} catch (NullPointerException e) {
|
||||
if (e.getMessage().contains("tradeCurrency")) {
|
||||
throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<PaymentAccount> getUserPaymentAccounts() {
|
||||
return Objects.requireNonNull(user.getPaymentAccounts()).stream()
|
||||
.filter(account -> !account.getPaymentMethod().isBsqSwap())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PaymentAccount getPreselectedPaymentAccount() {
|
||||
return paymentAccount;
|
||||
}
|
||||
|
||||
public void populateData() {
|
||||
Offer offer = sourceOpenOffer.getOffer();
|
||||
// Min amount need to be set before amount as if minAmount is null it would be set by amount
|
||||
setMinAmount(offer.getMinAmount());
|
||||
setAmount(offer.getAmount());
|
||||
setPrice(offer.getPrice());
|
||||
setVolume(offer.getVolume());
|
||||
setUseMarketBasedPrice(offer.isUseMarketBasedPrice());
|
||||
setTriggerPrice(sourceOpenOffer.getTriggerPrice());
|
||||
if (offer.isUseMarketBasedPrice()) {
|
||||
setMarketPriceMarginPct(offer.getMarketPriceMarginPct());
|
||||
}
|
||||
setBuyerAsTakerWithoutDeposit(offer.hasBuyerAsTakerWithoutDeposit());
|
||||
setSecurityDepositPct(getSecurityAsPercent(offer));
|
||||
setExtraInfo(offer.getOfferExtraInfo());
|
||||
}
|
||||
|
||||
public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
Offer clonedOffer = createClonedOffer();
|
||||
openOfferManager.placeOffer(clonedOffer,
|
||||
false,
|
||||
triggerPrice,
|
||||
false,
|
||||
true,
|
||||
sourceOpenOffer.getId(),
|
||||
transaction -> resultHandler.handleResult(),
|
||||
errorMessageHandler);
|
||||
}
|
||||
|
||||
private Offer createClonedOffer() {
|
||||
return createOfferService.createClonedOffer(sourceOpenOffer.getOffer(),
|
||||
tradeCurrencyCode.get(),
|
||||
useMarketBasedPrice.get() ? null : price.get(),
|
||||
useMarketBasedPrice.get(),
|
||||
useMarketBasedPrice.get() ? marketPriceMarginPct : 0,
|
||||
paymentAccount,
|
||||
extraInfo.get());
|
||||
}
|
||||
|
||||
public boolean hasConflictingClone() {
|
||||
Offer clonedOffer = createClonedOffer();
|
||||
return openOfferManager.hasConflictingClone(clonedOffer, sourceOpenOffer);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!--
|
||||
~ 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 <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<?import javafx.scene.layout.AnchorPane?>
|
||||
<AnchorPane fx:id="root" fx:controller="haveno.desktop.main.portfolio.cloneoffer.CloneOfferView"
|
||||
xmlns:fx="http://javafx.com/fxml">
|
||||
|
||||
</AnchorPane>
|
|
@ -0,0 +1,261 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.desktop.main.portfolio.cloneoffer;
|
||||
|
||||
import haveno.desktop.Navigation;
|
||||
import haveno.desktop.common.view.FxmlView;
|
||||
import haveno.desktop.components.AutoTooltipButton;
|
||||
import haveno.desktop.components.BusyAnimation;
|
||||
import haveno.desktop.main.offer.MutableOfferView;
|
||||
import haveno.desktop.main.overlays.popups.Popup;
|
||||
import haveno.desktop.main.overlays.windows.OfferDetailsWindow;
|
||||
|
||||
import haveno.core.locale.CurrencyUtil;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.payment.PaymentAccount;
|
||||
import haveno.core.user.DontShowAgainLookup;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.util.FormattingUtils;
|
||||
import haveno.core.util.coin.CoinFormatter;
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.util.Tuple4;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import static haveno.desktop.util.FormBuilder.addButtonBusyAnimationLabelAfterGroup;
|
||||
|
||||
@FxmlView
|
||||
public class CloneOfferView extends MutableOfferView<CloneOfferViewModel> {
|
||||
|
||||
private BusyAnimation busyAnimation;
|
||||
private Button cloneButton;
|
||||
private Button cancelButton;
|
||||
private Label spinnerInfoLabel;
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Constructor, lifecycle
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Inject
|
||||
private CloneOfferView(CloneOfferViewModel model,
|
||||
Navigation navigation,
|
||||
Preferences preferences,
|
||||
OfferDetailsWindow offerDetailsWindow,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter) {
|
||||
super(model, navigation, preferences, offerDetailsWindow, btcFormatter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initialize() {
|
||||
super.initialize();
|
||||
|
||||
addCloneGroup();
|
||||
renameAmountGroup();
|
||||
}
|
||||
|
||||
private void renameAmountGroup() {
|
||||
amountTitledGroupBg.setText(Res.get("editOffer.setPrice"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doSetFocus() {
|
||||
// Don't focus in any field before data was set
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doActivate() {
|
||||
super.doActivate();
|
||||
|
||||
|
||||
addBindings();
|
||||
|
||||
hideOptionsGroup();
|
||||
hideNextButtons();
|
||||
|
||||
// Lock amount field as it would require bigger changes to support increased amount values.
|
||||
amountTextField.setDisable(true);
|
||||
amountBtcLabel.setDisable(true);
|
||||
minAmountTextField.setDisable(true);
|
||||
minAmountBtcLabel.setDisable(true);
|
||||
volumeTextField.setDisable(true);
|
||||
volumeCurrencyLabel.setDisable(true);
|
||||
|
||||
// Workaround to fix margin on top of amount group
|
||||
gridPane.setPadding(new Insets(-20, 25, -1, 25));
|
||||
|
||||
updatePriceToggle();
|
||||
updateElementsWithDirection();
|
||||
|
||||
model.isNextButtonDisabled.setValue(false);
|
||||
cancelButton.setDisable(false);
|
||||
|
||||
model.onInvalidateMarketPriceMargin();
|
||||
model.onInvalidatePrice();
|
||||
|
||||
// To force re-validation of payment account validation
|
||||
onPaymentAccountsComboBoxSelected();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
super.deactivate();
|
||||
|
||||
removeBindings();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClose() {
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// API
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void applyOpenOffer(OpenOffer openOffer) {
|
||||
model.applyOpenOffer(openOffer);
|
||||
|
||||
initWithData(openOffer.getOffer().getDirection(),
|
||||
CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(),
|
||||
false,
|
||||
null);
|
||||
|
||||
if (!model.isSecurityDepositValid()) {
|
||||
new Popup().warning(Res.get("editOffer.invalidDeposit"))
|
||||
.onClose(this::close)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Bindings, Listeners
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void addBindings() {
|
||||
cloneButton.disableProperty().bind(model.isNextButtonDisabled);
|
||||
}
|
||||
|
||||
private void removeBindings() {
|
||||
cloneButton.disableProperty().unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObservableList<PaymentAccount> filterPaymentAccounts(ObservableList<PaymentAccount> paymentAccounts) {
|
||||
return paymentAccounts;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Build UI elements
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void addCloneGroup() {
|
||||
Tuple4<Button, BusyAnimation, Label, HBox> tuple4 = addButtonBusyAnimationLabelAfterGroup(gridPane, 6, Res.get("cloneOffer.clone"));
|
||||
|
||||
HBox hBox = tuple4.fourth;
|
||||
hBox.setAlignment(Pos.CENTER_LEFT);
|
||||
GridPane.setHalignment(hBox, HPos.LEFT);
|
||||
|
||||
cloneButton = tuple4.first;
|
||||
cloneButton.setMinHeight(40);
|
||||
cloneButton.setPadding(new Insets(0, 20, 0, 20));
|
||||
cloneButton.setGraphicTextGap(10);
|
||||
|
||||
busyAnimation = tuple4.second;
|
||||
spinnerInfoLabel = tuple4.third;
|
||||
|
||||
cancelButton = new AutoTooltipButton(Res.get("shared.cancel"));
|
||||
cancelButton.setDefaultButton(false);
|
||||
cancelButton.setOnAction(event -> close());
|
||||
hBox.getChildren().add(cancelButton);
|
||||
|
||||
cloneButton.setOnAction(e -> {
|
||||
cloneButton.requestFocus(); // fix issue #5460 (when enter key used, focus is wrong)
|
||||
onClone();
|
||||
});
|
||||
}
|
||||
|
||||
private void onClone() {
|
||||
if (model.dataModel.hasConflictingClone()) {
|
||||
new Popup().warning(Res.get("cloneOffer.hasConflictingClone"))
|
||||
.actionButtonText(Res.get("shared.yes"))
|
||||
.onAction(this::doClone)
|
||||
.closeButtonText(Res.get("shared.no"))
|
||||
.show();
|
||||
} else {
|
||||
doClone();
|
||||
}
|
||||
}
|
||||
|
||||
private void doClone() {
|
||||
if (model.isPriceInRange()) {
|
||||
model.isNextButtonDisabled.setValue(true);
|
||||
cancelButton.setDisable(true);
|
||||
busyAnimation.play();
|
||||
spinnerInfoLabel.setText(Res.get("cloneOffer.publishOffer"));
|
||||
model.onCloneOffer(() -> {
|
||||
UserThread.execute(() -> {
|
||||
String key = "cloneOfferSuccess";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup()
|
||||
.feedback(Res.get("cloneOffer.success"))
|
||||
.dontShowAgainId(key)
|
||||
.show();
|
||||
}
|
||||
spinnerInfoLabel.setText("");
|
||||
busyAnimation.stop();
|
||||
close();
|
||||
});
|
||||
},
|
||||
errorMessage -> {
|
||||
UserThread.execute(() -> {
|
||||
log.error(errorMessage);
|
||||
spinnerInfoLabel.setText("");
|
||||
busyAnimation.stop();
|
||||
model.isNextButtonDisabled.setValue(false);
|
||||
cancelButton.setDisable(false);
|
||||
new Popup().warning(errorMessage).show();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Utils
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void updateElementsWithDirection() {
|
||||
ImageView iconView = new ImageView();
|
||||
iconView.setId(model.isShownAsSellOffer() ? "image-sell-white" : "image-buy-white");
|
||||
cloneButton.setGraphic(iconView);
|
||||
cloneButton.setId(model.isShownAsSellOffer() ? "sell-button-big" : "buy-button-big");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package haveno.desktop.main.portfolio.cloneoffer;
|
||||
|
||||
import haveno.desktop.Navigation;
|
||||
import haveno.desktop.main.offer.MutableOfferViewModel;
|
||||
import haveno.desktop.main.offer.OfferViewUtil;
|
||||
|
||||
import haveno.core.account.witness.AccountAgeWitnessService;
|
||||
import haveno.core.offer.OfferUtil;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.payment.validation.FiatVolumeValidator;
|
||||
import haveno.core.payment.validation.SecurityDepositValidator;
|
||||
import haveno.core.payment.validation.XmrValidator;
|
||||
import haveno.core.provider.price.PriceFeedService;
|
||||
import haveno.core.user.Preferences;
|
||||
import haveno.core.util.FormattingUtils;
|
||||
import haveno.core.util.PriceUtil;
|
||||
import haveno.core.util.coin.CoinFormatter;
|
||||
import haveno.core.util.validation.AmountValidator4Decimals;
|
||||
import haveno.core.util.validation.AmountValidator8Decimals;
|
||||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
import haveno.common.handlers.ResultHandler;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
class CloneOfferViewModel extends MutableOfferViewModel<CloneOfferDataModel> {
|
||||
|
||||
@Inject
|
||||
public CloneOfferViewModel(CloneOfferDataModel dataModel,
|
||||
FiatVolumeValidator fiatVolumeValidator,
|
||||
AmountValidator4Decimals priceValidator4Decimals,
|
||||
AmountValidator8Decimals priceValidator8Decimals,
|
||||
XmrValidator xmrValidator,
|
||||
SecurityDepositValidator securityDepositValidator,
|
||||
PriceFeedService priceFeedService,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
Navigation navigation,
|
||||
Preferences preferences,
|
||||
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
|
||||
OfferUtil offerUtil) {
|
||||
super(dataModel,
|
||||
fiatVolumeValidator,
|
||||
priceValidator4Decimals,
|
||||
priceValidator8Decimals,
|
||||
xmrValidator,
|
||||
securityDepositValidator,
|
||||
priceFeedService,
|
||||
accountAgeWitnessService,
|
||||
navigation,
|
||||
preferences,
|
||||
btcFormatter,
|
||||
offerUtil);
|
||||
syncMinAmountWithAmount = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
super.activate();
|
||||
|
||||
dataModel.populateData();
|
||||
|
||||
long triggerPriceAsLong = dataModel.getTriggerPrice();
|
||||
dataModel.setTriggerPrice(triggerPriceAsLong);
|
||||
if (triggerPriceAsLong > 0) {
|
||||
triggerPrice.set(PriceUtil.formatMarketPrice(triggerPriceAsLong, dataModel.getCurrencyCode()));
|
||||
} else {
|
||||
triggerPrice.set("");
|
||||
}
|
||||
onTriggerPriceTextFieldChanged();
|
||||
}
|
||||
|
||||
public void applyOpenOffer(OpenOffer openOffer) {
|
||||
dataModel.reset();
|
||||
dataModel.applyOpenOffer(openOffer);
|
||||
}
|
||||
|
||||
public void onCloneOffer(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
dataModel.onCloneOffer(resultHandler, errorMessageHandler);
|
||||
}
|
||||
|
||||
public void onInvalidateMarketPriceMargin() {
|
||||
marketPriceMargin.set(FormattingUtils.formatToPercent(dataModel.getMarketPriceMarginPct()));
|
||||
}
|
||||
|
||||
public void onInvalidatePrice() {
|
||||
price.set(FormattingUtils.formatPrice(null));
|
||||
price.set(FormattingUtils.formatPrice(dataModel.getPrice().get()));
|
||||
}
|
||||
|
||||
public boolean isSecurityDepositValid() {
|
||||
return securityDepositValidator.validate(securityDeposit.get()).isValid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void triggerFocusOutOnAmountFields() {
|
||||
// do not update BTC Amount or minAmount here
|
||||
// issue 2798: "after a few edits of offer the BTC amount has increased"
|
||||
}
|
||||
|
||||
public boolean isShownAsSellOffer() {
|
||||
return OfferViewUtil.isShownAsSellOffer(getTradeCurrency(), dataModel.getDirection());
|
||||
}
|
||||
}
|
|
@ -444,7 +444,7 @@ public class ClosedTradesView extends ActivatableViewAndModel<VBox, ClosedTrades
|
|||
if (item != null && !empty && isMyOfferAsMaker(item.getTradable().getOffer().getOfferPayload())) {
|
||||
if (button == null) {
|
||||
button = FormBuilder.getRegularIconButton(MaterialDesignIcon.CONTENT_COPY);
|
||||
button.setTooltip(new Tooltip(Res.get("shared.duplicateOffer")));
|
||||
button.setTooltip(new Tooltip(Res.get("portfolio.context.offerLikeThis")));
|
||||
setGraphic(button);
|
||||
}
|
||||
button.setOnAction(event -> onDuplicateOffer(item.getTradable().getOffer()));
|
||||
|
|
|
@ -35,13 +35,10 @@ import haveno.core.user.Preferences;
|
|||
import haveno.core.user.User;
|
||||
import haveno.core.util.FormattingUtils;
|
||||
import haveno.core.util.coin.CoinFormatter;
|
||||
import haveno.core.util.coin.CoinUtil;
|
||||
import haveno.core.xmr.wallet.Restrictions;
|
||||
import haveno.core.xmr.wallet.XmrWalletService;
|
||||
import haveno.desktop.Navigation;
|
||||
import haveno.desktop.main.offer.MutableOfferDataModel;
|
||||
import haveno.network.p2p.P2PService;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
@ -101,14 +98,6 @@ class DuplicateOfferDataModel extends MutableOfferDataModel {
|
|||
if (openOffer != null) setTriggerPrice(openOffer.getTriggerPrice());
|
||||
}
|
||||
|
||||
private double getSecurityAsPercent(Offer offer) {
|
||||
BigInteger offerSellerSecurityDeposit = getBoundedSecurityDeposit(offer.getMaxSellerSecurityDeposit());
|
||||
double offerSellerSecurityDepositAsPercent = CoinUtil.getAsPercentPerXmr(offerSellerSecurityDeposit,
|
||||
offer.getAmount());
|
||||
return Math.min(offerSellerSecurityDepositAsPercent,
|
||||
Restrictions.getMaxSecurityDepositAsPercent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<PaymentAccount> getUserPaymentAccounts() {
|
||||
return Objects.requireNonNull(user.getPaymentAccounts()).stream()
|
||||
|
|
|
@ -70,6 +70,7 @@ public class DuplicateOfferView extends MutableOfferView<DuplicateOfferViewModel
|
|||
public void initWithData(OfferPayload offerPayload) {
|
||||
initWithData(offerPayload.getDirection(),
|
||||
CurrencyUtil.getTradeCurrency(offerPayload.getCurrencyCode()).get(),
|
||||
true,
|
||||
null);
|
||||
model.initWithData(offerPayload);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ package haveno.desktop.main.portfolio.editoffer;
|
|||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.handlers.ErrorMessageHandler;
|
||||
import haveno.common.handlers.ResultHandler;
|
||||
import haveno.core.account.witness.AccountAgeWitnessService;
|
||||
|
@ -56,6 +55,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
private final CorePersistenceProtoResolver corePersistenceProtoResolver;
|
||||
private OpenOffer openOffer;
|
||||
private OpenOffer.State initialState;
|
||||
private Offer editedOffer;
|
||||
|
||||
@Inject
|
||||
EditOfferDataModel(CreateOfferService createOfferService,
|
||||
|
@ -100,7 +100,7 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
securityDepositPct.set(0);
|
||||
paymentAccounts.clear();
|
||||
paymentAccount = null;
|
||||
marketPriceMargin = 0;
|
||||
marketPriceMarginPct = 0;
|
||||
}
|
||||
|
||||
public void applyOpenOffer(OpenOffer openOffer) {
|
||||
|
@ -142,10 +142,9 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
extraInfo.set(offer.getOfferExtraInfo());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initWithData(OfferDirection direction, TradeCurrency tradeCurrency) {
|
||||
try {
|
||||
return super.initWithData(direction, tradeCurrency);
|
||||
return super.initWithData(direction, tradeCurrency, false);
|
||||
} catch (NullPointerException e) {
|
||||
if (e.getMessage().contains("tradeCurrency")) {
|
||||
throw new IllegalArgumentException("Offers of removed assets cannot be edited. You can only cancel it.", e);
|
||||
|
@ -225,15 +224,16 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
offerPayload.getReserveTxKeyImages(),
|
||||
newOfferPayload.getExtraInfo());
|
||||
|
||||
final Offer editedOffer = new Offer(editedPayload);
|
||||
editedOffer = new Offer(editedPayload);
|
||||
editedOffer.setPriceFeedService(priceFeedService);
|
||||
editedOffer.setState(Offer.State.AVAILABLE);
|
||||
|
||||
openOfferManager.editOpenOfferPublish(editedOffer, triggerPrice, initialState, () -> {
|
||||
resultHandler.handleResult(); // process result before nullifying state
|
||||
openOffer = null;
|
||||
UserThread.execute(() -> resultHandler.handleResult());
|
||||
editedOffer = null;
|
||||
}, (errorMsg) -> {
|
||||
UserThread.execute(() -> errorMessageHandler.handleErrorMessage(errorMsg));
|
||||
errorMessageHandler.handleErrorMessage(errorMsg);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -243,6 +243,15 @@ class EditOfferDataModel extends MutableOfferDataModel {
|
|||
}, errorMessageHandler);
|
||||
}
|
||||
|
||||
public boolean hasConflictingClone() {
|
||||
Optional<OpenOffer> editedOpenOffer = openOfferManager.getOpenOffer(openOffer.getId());
|
||||
if (!editedOpenOffer.isPresent()) {
|
||||
log.warn("Edited open offer is no longer present");
|
||||
return false;
|
||||
}
|
||||
return openOfferManager.hasConflictingClone(editedOpenOffer.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<PaymentAccount> getUserPaymentAccounts() {
|
||||
throw new RuntimeException("Edit offer not supported with XMR");
|
||||
|
|
|
@ -19,6 +19,8 @@ package haveno.desktop.main.portfolio.editoffer;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.name.Named;
|
||||
|
||||
import haveno.common.UserThread;
|
||||
import haveno.common.util.Tuple4;
|
||||
import haveno.core.locale.CurrencyUtil;
|
||||
import haveno.core.locale.Res;
|
||||
|
@ -140,6 +142,7 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
|||
|
||||
initWithData(openOffer.getOffer().getDirection(),
|
||||
CurrencyUtil.getTradeCurrency(openOffer.getOffer().getCurrencyCode()).get(),
|
||||
false,
|
||||
null);
|
||||
|
||||
model.onStartEditOffer(errorMessage -> {
|
||||
|
@ -208,23 +211,31 @@ public class EditOfferView extends MutableOfferView<EditOfferViewModel> {
|
|||
|
||||
// edit offer
|
||||
model.onPublishOffer(() -> {
|
||||
String key = "editOfferSuccess";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup()
|
||||
.feedback(Res.get("editOffer.success"))
|
||||
.dontShowAgainId(key)
|
||||
.show();
|
||||
if (model.dataModel.hasConflictingClone()) {
|
||||
new Popup().warning(Res.get("editOffer.hasConflictingClone")).show();
|
||||
} else {
|
||||
String key = "editOfferSuccess";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup()
|
||||
.feedback(Res.get("editOffer.success"))
|
||||
.dontShowAgainId(key)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
spinnerInfoLabel.setText("");
|
||||
busyAnimation.stop();
|
||||
close();
|
||||
UserThread.execute(() -> {
|
||||
spinnerInfoLabel.setText("");
|
||||
busyAnimation.stop();
|
||||
close();
|
||||
});
|
||||
}, (message) -> {
|
||||
log.error(message);
|
||||
spinnerInfoLabel.setText("");
|
||||
busyAnimation.stop();
|
||||
model.isNextButtonDisabled.setValue(false);
|
||||
cancelButton.setDisable(false);
|
||||
new Popup().warning(Res.get("editOffer.failed", message)).show();
|
||||
UserThread.execute(() -> {
|
||||
log.error(message);
|
||||
spinnerInfoLabel.setText("");
|
||||
busyAnimation.stop();
|
||||
model.isNextButtonDisabled.setValue(false);
|
||||
cancelButton.setDisable(false);
|
||||
new Popup().warning(Res.get("editOffer.failed", message)).show();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -44,7 +44,7 @@ class EditOfferViewModel extends MutableOfferViewModel<EditOfferDataModel> {
|
|||
FiatVolumeValidator fiatVolumeValidator,
|
||||
AmountValidator4Decimals priceValidator4Decimals,
|
||||
AmountValidator8Decimals priceValidator8Decimals,
|
||||
XmrValidator btcValidator,
|
||||
XmrValidator xmrValidator,
|
||||
SecurityDepositValidator securityDepositValidator,
|
||||
PriceFeedService priceFeedService,
|
||||
AccountAgeWitnessService accountAgeWitnessService,
|
||||
|
@ -56,7 +56,7 @@ class EditOfferViewModel extends MutableOfferViewModel<EditOfferDataModel> {
|
|||
fiatVolumeValidator,
|
||||
priceValidator4Decimals,
|
||||
priceValidator8Decimals,
|
||||
btcValidator,
|
||||
xmrValidator,
|
||||
securityDepositValidator,
|
||||
priceFeedService,
|
||||
accountAgeWitnessService,
|
||||
|
|
|
@ -39,4 +39,8 @@ class OpenOfferListItem {
|
|||
public Offer getOffer() {
|
||||
return openOffer.getOffer();
|
||||
}
|
||||
|
||||
public String getGroupId() {
|
||||
return openOffer.getGroupId();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,8 @@
|
|||
</HBox>
|
||||
<TableView fx:id="tableView" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="offerIdColumn" minWidth="110" maxWidth="120"/>
|
||||
<TableColumn fx:id="offerIdColumn" minWidth="100" maxWidth="120"/>
|
||||
<TableColumn fx:id="groupIdColumn" minWidth="70"/>
|
||||
<TableColumn fx:id="dateColumn" minWidth="170"/>
|
||||
<TableColumn fx:id="marketColumn" minWidth="75"/>
|
||||
<TableColumn fx:id="priceColumn" minWidth="100"/>
|
||||
|
@ -50,11 +51,13 @@
|
|||
<TableColumn fx:id="triggerPriceColumn" minWidth="90"/>
|
||||
<TableColumn fx:id="amountColumn" minWidth="110"/>
|
||||
<TableColumn fx:id="volumeColumn" minWidth="110"/>
|
||||
<TableColumn fx:id="paymentMethodColumn" minWidth="120" maxWidth="170"/>
|
||||
<TableColumn fx:id="paymentMethodColumn" minWidth="110" maxWidth="170"/>
|
||||
<TableColumn fx:id="directionColumn" minWidth="70"/>
|
||||
<TableColumn fx:id="deactivateItemColumn" minWidth="60" maxWidth="60" sortable="false"/>
|
||||
<TableColumn fx:id="editItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="triggerIconColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="duplicateItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="cloneItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
<TableColumn fx:id="removeItemColumn" minWidth="30" maxWidth="30" sortable="false"/>
|
||||
</columns>
|
||||
</TableView>
|
||||
|
|
|
@ -22,8 +22,8 @@ import com.googlecode.jcsv.writer.CSVEntryConverter;
|
|||
import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon;
|
||||
import haveno.core.locale.Res;
|
||||
import haveno.core.offer.Offer;
|
||||
import haveno.core.offer.OfferPayload;
|
||||
import haveno.core.offer.OpenOffer;
|
||||
import haveno.core.offer.OpenOfferManager;
|
||||
import haveno.core.user.DontShowAgainLookup;
|
||||
import haveno.desktop.Navigation;
|
||||
import haveno.desktop.common.view.ActivatableViewAndModel;
|
||||
|
@ -40,8 +40,9 @@ import haveno.desktop.main.funds.withdrawal.WithdrawalView;
|
|||
import haveno.desktop.main.overlays.popups.Popup;
|
||||
import haveno.desktop.main.overlays.windows.OfferDetailsWindow;
|
||||
import haveno.desktop.main.portfolio.PortfolioView;
|
||||
import haveno.desktop.main.portfolio.duplicateoffer.DuplicateOfferView;
|
||||
import haveno.desktop.main.portfolio.presentation.PortfolioUtil;
|
||||
import static haveno.desktop.util.FormBuilder.getRegularIconButton;
|
||||
import haveno.desktop.util.FormBuilder;
|
||||
import haveno.desktop.util.GUIUtil;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
|
@ -51,13 +52,11 @@ import java.util.stream.Collectors;
|
|||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.transformation.FilteredList;
|
||||
import javafx.collections.transformation.SortedList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Label;
|
||||
|
@ -73,6 +72,7 @@ import javafx.scene.layout.Pane;
|
|||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
@ -80,12 +80,39 @@ import org.jetbrains.annotations.NotNull;
|
|||
@FxmlView
|
||||
public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersViewModel> {
|
||||
|
||||
private enum ColumnNames {
|
||||
OFFER_ID(Res.get("shared.offerId")),
|
||||
GROUP_ID(Res.get("openOffer.header.groupId")),
|
||||
DATE(Res.get("shared.dateTime")),
|
||||
MARKET(Res.get("shared.market")),
|
||||
PRICE(Res.get("shared.price")),
|
||||
DEVIATION(Res.get("shared.deviation")),
|
||||
TRIGGER_PRICE(Res.get("openOffer.header.triggerPrice")),
|
||||
AMOUNT(Res.get("shared.XMRMinMax")),
|
||||
VOLUME(Res.get("shared.amountMinMax")),
|
||||
PAYMENT_METHOD(Res.get("shared.paymentMethod")),
|
||||
DIRECTION(Res.get("shared.offerType")),
|
||||
STATUS(Res.get("shared.state"));
|
||||
|
||||
private final String text;
|
||||
|
||||
ColumnNames(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
TableView<OpenOfferListItem> tableView;
|
||||
@FXML
|
||||
TableColumn<OpenOfferListItem, OpenOfferListItem> priceColumn, deviationColumn, amountColumn, volumeColumn,
|
||||
marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn,
|
||||
removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn;
|
||||
marketColumn, directionColumn, dateColumn, offerIdColumn, deactivateItemColumn, groupIdColumn,
|
||||
removeItemColumn, editItemColumn, triggerPriceColumn, triggerIconColumn, paymentMethodColumn, duplicateItemColumn,
|
||||
cloneItemColumn;
|
||||
@FXML
|
||||
HBox searchBox;
|
||||
@FXML
|
||||
|
@ -108,37 +135,48 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
private SortedList<OpenOfferListItem> sortedList;
|
||||
private FilteredList<OpenOfferListItem> filteredList;
|
||||
private ChangeListener<String> filterTextFieldListener;
|
||||
private PortfolioView.OpenOfferActionHandler openOfferActionHandler;
|
||||
private final OpenOfferManager openOfferManager;
|
||||
private PortfolioView.EditOpenOfferHandler editOpenOfferHandler;
|
||||
private PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler;
|
||||
private ChangeListener<Number> widthListener;
|
||||
private ListChangeListener<OpenOfferListItem> sortedListChangedListener;
|
||||
|
||||
private Map<String, ChangeListener<OpenOffer.State>> offerStateChangeListeners = new HashMap<String, ChangeListener<OpenOffer.State>>();
|
||||
|
||||
@Inject
|
||||
public OpenOffersView(OpenOffersViewModel model, Navigation navigation, OfferDetailsWindow offerDetailsWindow) {
|
||||
public OpenOffersView(OpenOffersViewModel model,
|
||||
OpenOfferManager openOfferManager,
|
||||
Navigation navigation,
|
||||
OfferDetailsWindow offerDetailsWindow) {
|
||||
super(model);
|
||||
this.navigation = navigation;
|
||||
this.offerDetailsWindow = offerDetailsWindow;
|
||||
this.openOfferManager = openOfferManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
widthListener = (observable, oldValue, newValue) -> onWidthChange((double) newValue);
|
||||
paymentMethodColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.paymentMethod")));
|
||||
priceColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.price")));
|
||||
deviationColumn.setGraphic(new AutoTooltipTableColumn<>(Res.get("shared.deviation"),
|
||||
groupIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.GROUP_ID.toString()));
|
||||
paymentMethodColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PAYMENT_METHOD.toString()));
|
||||
priceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.PRICE.toString()));
|
||||
deviationColumn.setGraphic(new AutoTooltipTableColumn<>(ColumnNames.DEVIATION.toString(),
|
||||
Res.get("portfolio.closedTrades.deviation.help")).getGraphic());
|
||||
amountColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.XMRMinMax")));
|
||||
volumeColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.amountMinMax")));
|
||||
marketColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.market")));
|
||||
directionColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerType")));
|
||||
dateColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.dateTime")));
|
||||
offerIdColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.offerId")));
|
||||
triggerPriceColumn.setGraphic(new AutoTooltipLabel(Res.get("openOffer.header.triggerPrice")));
|
||||
deactivateItemColumn.setGraphic(new AutoTooltipLabel(Res.get("shared.enabled")));
|
||||
triggerPriceColumn.setGraphic(new AutoTooltipLabel(ColumnNames.TRIGGER_PRICE.toString()));
|
||||
amountColumn.setGraphic(new AutoTooltipLabel(ColumnNames.AMOUNT.toString()));
|
||||
volumeColumn.setGraphic(new AutoTooltipLabel(ColumnNames.VOLUME.toString()));
|
||||
marketColumn.setGraphic(new AutoTooltipLabel(ColumnNames.MARKET.toString()));
|
||||
directionColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DIRECTION.toString()));
|
||||
dateColumn.setGraphic(new AutoTooltipLabel(ColumnNames.DATE.toString()));
|
||||
offerIdColumn.setGraphic(new AutoTooltipLabel(ColumnNames.OFFER_ID.toString()));
|
||||
deactivateItemColumn.setGraphic(new AutoTooltipLabel(ColumnNames.STATUS.toString()));
|
||||
editItemColumn.setGraphic(new AutoTooltipLabel(""));
|
||||
duplicateItemColumn.setText("");
|
||||
cloneItemColumn.setText("");
|
||||
removeItemColumn.setGraphic(new AutoTooltipLabel(""));
|
||||
|
||||
setOfferIdColumnCellFactory();
|
||||
setGroupIdCellFactory();
|
||||
setDirectionColumnCellFactory();
|
||||
setMarketColumnCellFactory();
|
||||
setPriceColumnCellFactory();
|
||||
|
@ -151,12 +189,15 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
setEditColumnCellFactory();
|
||||
setTriggerIconColumnCellFactory();
|
||||
setTriggerPriceColumnCellFactory();
|
||||
setDuplicateColumnCellFactory();
|
||||
setCloneColumnCellFactory();
|
||||
setRemoveColumnCellFactory();
|
||||
|
||||
tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
|
||||
tableView.setPlaceholder(new AutoTooltipLabel(Res.get("table.placeholder.noItems", Res.get("shared.openOffers"))));
|
||||
|
||||
offerIdColumn.setComparator(Comparator.comparing(o -> o.getOffer().getId()));
|
||||
groupIdColumn.setComparator(Comparator.comparing(o -> o.getOpenOffer().getReserveTxHash()));
|
||||
directionColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDirection()));
|
||||
marketColumn.setComparator(Comparator.comparing(model::getMarketLabel));
|
||||
amountColumn.setComparator(Comparator.comparing(o -> o.getOffer().getAmount()));
|
||||
|
@ -168,23 +209,21 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
dateColumn.setComparator(Comparator.comparing(o -> o.getOffer().getDate()));
|
||||
paymentMethodColumn.setComparator(Comparator.comparing(o -> Res.get(o.getOffer().getPaymentMethod().getId())));
|
||||
|
||||
dateColumn.setSortType(TableColumn.SortType.DESCENDING);
|
||||
dateColumn.setSortType(TableColumn.SortType.ASCENDING);
|
||||
tableView.getSortOrder().add(dateColumn);
|
||||
|
||||
tableView.setRowFactory(
|
||||
tableView -> {
|
||||
final TableRow<OpenOfferListItem> row = new TableRow<>();
|
||||
final ContextMenu rowMenu = new ContextMenu();
|
||||
MenuItem editItem = new MenuItem(Res.get("portfolio.context.offerLikeThis"));
|
||||
editItem.setOnAction((event) -> {
|
||||
try {
|
||||
OfferPayload offerPayload = row.getItem().getOffer().getOfferPayload();
|
||||
navigation.navigateToWithData(offerPayload, MainView.class, PortfolioView.class, DuplicateOfferView.class);
|
||||
} catch (NullPointerException e) {
|
||||
log.warn("Unable to get offerPayload - {}", e.toString());
|
||||
}
|
||||
});
|
||||
rowMenu.getItems().add(editItem);
|
||||
|
||||
MenuItem duplicateOfferMenuItem = new MenuItem(Res.get("portfolio.context.offerLikeThis"));
|
||||
duplicateOfferMenuItem.setOnAction((event) -> onDuplicateOffer(row.getItem()));
|
||||
rowMenu.getItems().add(duplicateOfferMenuItem);
|
||||
|
||||
MenuItem cloneOfferMenuItem = new MenuItem(Res.get("offerbook.cloneOffer"));
|
||||
cloneOfferMenuItem.setOnAction((event) -> onCloneOffer(row.getItem()));
|
||||
rowMenu.getItems().add(cloneOfferMenuItem);
|
||||
row.contextMenuProperty().bind(
|
||||
Bindings.when(Bindings.isNotNull(row.itemProperty()))
|
||||
.then(rowMenu)
|
||||
|
@ -207,6 +246,15 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
HBox.setHgrow(footerSpacer, Priority.ALWAYS);
|
||||
HBox.setMargin(exportButton, new Insets(0, 10, 0, 0));
|
||||
exportButton.updateText(Res.get("shared.exportCSV"));
|
||||
|
||||
sortedListChangedListener = c -> {
|
||||
c.next();
|
||||
if (c.wasAdded() || c.wasRemoved()) {
|
||||
updateNumberOfOffers();
|
||||
updateGroupIdColumnVisibility();
|
||||
updateTriggerColumnVisibility();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -214,8 +262,11 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
filteredList = new FilteredList<>(model.getList());
|
||||
sortedList = new SortedList<>(filteredList);
|
||||
sortedList.comparatorProperty().bind(tableView.comparatorProperty());
|
||||
sortedList.addListener(sortedListChangedListener);
|
||||
tableView.setItems(sortedList);
|
||||
|
||||
updateGroupIdColumnVisibility();
|
||||
updateTriggerColumnVisibility();
|
||||
updateSelectToggleButtonState();
|
||||
|
||||
selectToggleButton.setOnAction(event -> {
|
||||
|
@ -231,37 +282,27 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
|
||||
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
|
||||
exportButton.setOnAction(event -> {
|
||||
ObservableList<TableColumn<OpenOfferListItem, ?>> tableColumns = tableView.getColumns();
|
||||
int reportColumns = tableColumns.size() - 3; // CSV report excludes the last columns (icons)
|
||||
CSVEntryConverter<OpenOfferListItem> headerConverter = item -> {
|
||||
String[] columns = new String[reportColumns];
|
||||
for (int i = 0; i < columns.length; i++) {
|
||||
Node graphic = tableColumns.get(i).getGraphic();
|
||||
if (graphic instanceof AutoTooltipLabel) {
|
||||
columns[i] = ((AutoTooltipLabel) graphic).getText();
|
||||
} else if (graphic instanceof HBox) {
|
||||
// Deviation has a Hbox with AutoTooltipLabel as first child in header
|
||||
columns[i] = ((AutoTooltipLabel) ((Parent) graphic).getChildrenUnmodifiable().get(0)).getText();
|
||||
} else {
|
||||
// Not expected
|
||||
columns[i] = "N/A";
|
||||
}
|
||||
String[] columns = new String[ColumnNames.values().length];
|
||||
for (ColumnNames m : ColumnNames.values()) {
|
||||
columns[m.ordinal()] = m.toString();
|
||||
}
|
||||
return columns;
|
||||
};
|
||||
CSVEntryConverter<OpenOfferListItem> contentConverter = item -> {
|
||||
String[] columns = new String[reportColumns];
|
||||
columns[0] = model.getOfferId(item);
|
||||
columns[1] = model.getDate(item);
|
||||
columns[2] = model.getMarketLabel(item);
|
||||
columns[3] = model.getPrice(item);
|
||||
columns[4] = model.getPriceDeviation(item);
|
||||
columns[5] = model.getTriggerPrice(item);
|
||||
columns[6] = model.getAmount(item);
|
||||
columns[7] = model.getVolume(item);
|
||||
columns[8] = model.getPaymentMethod(item);
|
||||
columns[9] = model.getDirectionLabel(item);
|
||||
columns[10] = String.valueOf(!item.getOpenOffer().isDeactivated());
|
||||
String[] columns = new String[ColumnNames.values().length];
|
||||
columns[ColumnNames.OFFER_ID.ordinal()] = model.getOfferId(item);
|
||||
columns[ColumnNames.GROUP_ID.ordinal()] = openOfferManager.hasClonedOffer(item.getOffer().getId()) ? getShortenedGroupId(item.getGroupId()) : "";
|
||||
columns[ColumnNames.DATE.ordinal()] = model.getDate(item);
|
||||
columns[ColumnNames.MARKET.ordinal()] = model.getMarketLabel(item);
|
||||
columns[ColumnNames.PRICE.ordinal()] = model.getPrice(item);
|
||||
columns[ColumnNames.DEVIATION.ordinal()] = model.getPriceDeviation(item);
|
||||
columns[ColumnNames.TRIGGER_PRICE.ordinal()] = model.getTriggerPrice(item);
|
||||
columns[ColumnNames.AMOUNT.ordinal()] = model.getAmount(item);
|
||||
columns[ColumnNames.VOLUME.ordinal()] = model.getVolume(item);
|
||||
columns[ColumnNames.PAYMENT_METHOD.ordinal()] = model.getPaymentMethod(item);
|
||||
columns[ColumnNames.DIRECTION.ordinal()] = model.getDirectionLabel(item);
|
||||
columns[ColumnNames.STATUS.ordinal()] = String.valueOf(!item.getOpenOffer().isDeactivated());
|
||||
return columns;
|
||||
};
|
||||
|
||||
|
@ -280,9 +321,24 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
onWidthChange(root.getWidth());
|
||||
}
|
||||
|
||||
private void updateNumberOfOffers() {
|
||||
numItems.setText(Res.get("shared.numItemsLabel", sortedList.size()));
|
||||
}
|
||||
|
||||
private void updateGroupIdColumnVisibility() {
|
||||
groupIdColumn.setVisible(openOfferManager.hasClonedOffers());
|
||||
}
|
||||
|
||||
private void updateTriggerColumnVisibility() {
|
||||
triggerIconColumn.setVisible(model.dataModel.getList().stream()
|
||||
.mapToLong(item -> item.getOpenOffer().getTriggerPrice())
|
||||
.sum() > 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void deactivate() {
|
||||
sortedList.comparatorProperty().unbind();
|
||||
sortedList.removeListener(sortedListChangedListener);
|
||||
exportButton.setOnAction(null);
|
||||
|
||||
filterTextField.textProperty().removeListener(filterTextFieldListener);
|
||||
|
@ -352,7 +408,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
}
|
||||
|
||||
private void onWidthChange(double width) {
|
||||
triggerPriceColumn.setVisible(width > 1200);
|
||||
triggerPriceColumn.setVisible(width > 1300);
|
||||
}
|
||||
|
||||
private void onDeactivateOpenOffer(OpenOffer openOffer) {
|
||||
|
@ -361,7 +417,7 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
() -> log.debug("Deactivate offer was successful"),
|
||||
(message) -> {
|
||||
log.error(message);
|
||||
new Popup().warning(Res.get("offerbook.deactivateOffer.failed", message)).show();
|
||||
new Popup().warning(message).show();
|
||||
});
|
||||
updateSelectToggleButtonState();
|
||||
}
|
||||
|
@ -397,12 +453,18 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
}
|
||||
|
||||
private void doRemoveOpenOffer(OpenOffer openOffer) {
|
||||
boolean hasClonedOffer = openOfferManager.hasClonedOffer(openOffer.getId());
|
||||
model.onRemoveOpenOffer(openOffer,
|
||||
() -> {
|
||||
log.debug("Remove offer was successful");
|
||||
|
||||
tableView.refresh();
|
||||
|
||||
// We do not show the popup if it's a cloned offer with shared maker reserve tx
|
||||
if (hasClonedOffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
String key = "WithdrawFundsAfterRemoveOfferInfo";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup().instruction(Res.get("offerbook.withdrawFundsHint", Res.get("navigation.funds.availableForWithdrawal")))
|
||||
|
@ -420,10 +482,46 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
|
||||
private void onEditOpenOffer(OpenOffer openOffer) {
|
||||
if (model.isBootstrappedOrShowPopup()) {
|
||||
openOfferActionHandler.onEditOpenOffer(openOffer);
|
||||
editOpenOfferHandler.onEditOpenOffer(openOffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void onDuplicateOffer(OpenOfferListItem item) {
|
||||
if (item == null || item.getOffer().getOfferPayload() == null) {
|
||||
return;
|
||||
}
|
||||
if (model.isBootstrappedOrShowPopup()) {
|
||||
PortfolioUtil.duplicateOffer(navigation, item.getOffer().getOfferPayload());
|
||||
}
|
||||
}
|
||||
|
||||
private void onCloneOffer(OpenOfferListItem item) {
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
if (model.isBootstrappedOrShowPopup()) {
|
||||
String key = "clonedOfferInfo";
|
||||
if (DontShowAgainLookup.showAgain(key)) {
|
||||
new Popup().headLine(Res.get("offerbook.clonedOffer.headline"))
|
||||
.instruction(Res.get("offerbook.clonedOffer.info"))
|
||||
.useIUnderstandButton()
|
||||
.dontShowAgainId(key)
|
||||
.onClose(() -> doCloneOffer(item))
|
||||
.show();
|
||||
} else {
|
||||
doCloneOffer(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doCloneOffer(OpenOfferListItem item) {
|
||||
OpenOffer openOffer = item.getOpenOffer();
|
||||
if (openOffer == null || openOffer.getOffer() == null || openOffer.getOffer().getOfferPayload() == null) {
|
||||
return;
|
||||
}
|
||||
cloneOpenOfferHandler.onCloneOpenOffer(openOffer);
|
||||
}
|
||||
|
||||
private void setOfferIdColumnCellFactory() {
|
||||
offerIdColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue()));
|
||||
offerIdColumn.getStyleClass().addAll("number-column", "first-column");
|
||||
|
@ -434,21 +532,28 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
public TableCell<OpenOfferListItem, OpenOfferListItem> call(TableColumn<OpenOfferListItem,
|
||||
OpenOfferListItem> column) {
|
||||
return new TableCell<>() {
|
||||
private HyperlinkWithIcon field;
|
||||
private HyperlinkWithIcon hyperlinkWithIcon;
|
||||
|
||||
@Override
|
||||
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null && !empty) {
|
||||
field = new HyperlinkWithIcon(model.getOfferId(item));
|
||||
field.setOnAction(event -> offerDetailsWindow.show(item.getOffer()));
|
||||
field.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails")));
|
||||
setGraphic(field);
|
||||
hyperlinkWithIcon = new HyperlinkWithIcon(item.getOffer().getShortId());
|
||||
if (model.isDeactivated(item)) {
|
||||
// getStyleClass().add("offer-disabled"); does not work with hyperlinkWithIcon;-(
|
||||
hyperlinkWithIcon.setStyle("-fx-text-fill: -bs-color-gray-3;");
|
||||
hyperlinkWithIcon.getIcon().setOpacity(0.2);
|
||||
}
|
||||
hyperlinkWithIcon.setOnAction(event -> {
|
||||
offerDetailsWindow.show(item.getOffer());
|
||||
});
|
||||
|
||||
hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openPopupForDetails")));
|
||||
setGraphic(hyperlinkWithIcon);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (field != null)
|
||||
field.setOnAction(null);
|
||||
if (hyperlinkWithIcon != null)
|
||||
hyperlinkWithIcon.setOnAction(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -456,6 +561,55 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
});
|
||||
}
|
||||
|
||||
private void setGroupIdCellFactory() {
|
||||
groupIdColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
|
||||
groupIdColumn.setCellFactory(
|
||||
new Callback<>() {
|
||||
@Override
|
||||
public TableCell<OpenOfferListItem, OpenOfferListItem> call(
|
||||
TableColumn<OpenOfferListItem, OpenOfferListItem> column) {
|
||||
|
||||
return new TableCell<>() {
|
||||
@Override
|
||||
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
getStyleClass().removeAll("offer-disabled");
|
||||
if (item != null) {
|
||||
Label label;
|
||||
Text icon;
|
||||
if (openOfferManager.hasClonedOffer(item.getOpenOffer().getId())) {
|
||||
label = new Label(getShortenedGroupId(item.getOpenOffer().getGroupId()));
|
||||
icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK, label, "icon");
|
||||
icon.setVisible(true);
|
||||
setTooltip(new Tooltip(Res.get("offerbook.clonedOffer.tooltip", item.getOpenOffer().getReserveTxHash())));
|
||||
} else {
|
||||
label = new Label("");
|
||||
icon = FormBuilder.getRegularIconForLabel(MaterialDesignIcon.LINK_OFF, label, "icon");
|
||||
icon.setVisible(false);
|
||||
setTooltip(new Tooltip(Res.get("offerbook.nonClonedOffer.tooltip", item.getOpenOffer().getReserveTxHash())));
|
||||
}
|
||||
|
||||
if (model.isDeactivated(item)) {
|
||||
getStyleClass().add("offer-disabled");
|
||||
icon.setOpacity(0.2);
|
||||
}
|
||||
setGraphic(label);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getShortenedGroupId(String groupId) {
|
||||
if (groupId.length() > 5) {
|
||||
return groupId.substring(0, 5);
|
||||
}
|
||||
return groupId;
|
||||
}
|
||||
|
||||
private void setDateColumnCellFactory() {
|
||||
dateColumn.setCellValueFactory((openOfferListItem) -> new ReadOnlyObjectWrapper<>(openOfferListItem.getValue()));
|
||||
dateColumn.setCellFactory(
|
||||
|
@ -779,6 +933,74 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
});
|
||||
}
|
||||
|
||||
private void setDuplicateColumnCellFactory() {
|
||||
duplicateItemColumn.getStyleClass().add("avatar-column");
|
||||
duplicateItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
|
||||
duplicateItemColumn.setCellFactory(
|
||||
new Callback<>() {
|
||||
@Override
|
||||
public TableCell<OpenOfferListItem, OpenOfferListItem> call(TableColumn<OpenOfferListItem, OpenOfferListItem> column) {
|
||||
return new TableCell<>() {
|
||||
Button button;
|
||||
|
||||
@Override
|
||||
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null && !empty) {
|
||||
if (button == null) {
|
||||
button = getRegularIconButton(MaterialDesignIcon.CONTENT_COPY);
|
||||
button.setTooltip(new Tooltip(Res.get("portfolio.context.offerLikeThis")));
|
||||
setGraphic(button);
|
||||
}
|
||||
button.setOnAction(event -> onDuplicateOffer(item));
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (button != null) {
|
||||
button.setOnAction(null);
|
||||
button = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setCloneColumnCellFactory() {
|
||||
cloneItemColumn.getStyleClass().add("avatar-column");
|
||||
cloneItemColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
|
||||
cloneItemColumn.setCellFactory(
|
||||
new Callback<>() {
|
||||
@Override
|
||||
public TableCell<OpenOfferListItem, OpenOfferListItem> call(TableColumn<OpenOfferListItem, OpenOfferListItem> column) {
|
||||
return new TableCell<>() {
|
||||
Button button;
|
||||
|
||||
@Override
|
||||
public void updateItem(final OpenOfferListItem item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
|
||||
if (item != null && !empty) {
|
||||
if (button == null) {
|
||||
button = getRegularIconButton(MaterialDesignIcon.BOX_SHADOW);
|
||||
button.setTooltip(new Tooltip(Res.get("offerbook.cloneOffer")));
|
||||
setGraphic(button);
|
||||
}
|
||||
button.setOnAction(event -> onCloneOffer(item));
|
||||
} else {
|
||||
setGraphic(null);
|
||||
if (button != null) {
|
||||
button.setOnAction(null);
|
||||
button = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setTriggerIconColumnCellFactory() {
|
||||
triggerIconColumn.setCellValueFactory((offerListItem) -> new ReadOnlyObjectWrapper<>(offerListItem.getValue()));
|
||||
triggerIconColumn.setCellFactory(
|
||||
|
@ -854,8 +1076,12 @@ public class OpenOffersView extends ActivatableViewAndModel<VBox, OpenOffersView
|
|||
});
|
||||
}
|
||||
|
||||
public void setOpenOfferActionHandler(PortfolioView.OpenOfferActionHandler openOfferActionHandler) {
|
||||
this.openOfferActionHandler = openOfferActionHandler;
|
||||
public void setEditOpenOfferHandler(PortfolioView.EditOpenOfferHandler editOpenOfferHandler) {
|
||||
this.editOpenOfferHandler = editOpenOfferHandler;
|
||||
}
|
||||
|
||||
public void setCloneOpenOfferHandler(PortfolioView.CloneOpenOfferHandler cloneOpenOfferHandler) {
|
||||
this.cloneOpenOfferHandler = cloneOpenOfferHandler;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,6 +84,10 @@ class OpenOffersViewModel extends ActivatableWithDataModel<OpenOffersDataModel>
|
|||
return item.getOffer().getShortId();
|
||||
}
|
||||
|
||||
String getGroupId(OpenOfferListItem item) {
|
||||
return item.getGroupId();
|
||||
}
|
||||
|
||||
String getAmount(OpenOfferListItem item) {
|
||||
return (item != null) ? DisplayUtils.formatAmount(item.getOffer(), btcFormatter) : "";
|
||||
}
|
||||
|
|
|
@ -2340,8 +2340,8 @@ public class FormBuilder {
|
|||
return getRegularIconForLabel(icon, label, null);
|
||||
}
|
||||
|
||||
public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String style) {
|
||||
return getIconForLabel(icon, "1.231em", label, style);
|
||||
public static Text getRegularIconForLabel(GlyphIcons icon, Label label, String styleClass) {
|
||||
return getIconForLabel(icon, "1.231em", label, styleClass);
|
||||
}
|
||||
|
||||
public static Text getIcon(GlyphIcons icon) {
|
||||
|
|
|
@ -88,7 +88,7 @@ public class CreateOfferDataModelTest {
|
|||
when(user.getPaymentAccounts()).thenReturn(paymentAccounts);
|
||||
when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount);
|
||||
|
||||
model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"));
|
||||
model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true);
|
||||
assertEquals("USD", model.getTradeCurrencyCode().get());
|
||||
}
|
||||
|
||||
|
@ -109,7 +109,7 @@ public class CreateOfferDataModelTest {
|
|||
when(user.findFirstPaymentAccountWithCurrency(new TraditionalCurrency("USD"))).thenReturn(zelleAccount);
|
||||
when(preferences.getSelectedPaymentAccountForCreateOffer()).thenReturn(revolutAccount);
|
||||
|
||||
model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"));
|
||||
model.initWithData(OfferDirection.BUY, new TraditionalCurrency("USD"), true);
|
||||
assertEquals("USD", model.getTradeCurrencyCode().get());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -117,7 +117,7 @@ public class CreateOfferViewModelTest {
|
|||
coinFormatter,
|
||||
tradeStats,
|
||||
null);
|
||||
dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero"));
|
||||
dataModel.initWithData(OfferDirection.BUY, new CryptoCurrency("XMR", "monero"), true);
|
||||
dataModel.activate();
|
||||
|
||||
model = new CreateOfferViewModel(dataModel,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue