/*
* This file is part of Bitsquare.
*
* Bitsquare 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.
*
* Bitsquare 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 Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.main.offer.takeoffer;
import de.jensd.fx.fontawesome.AwesomeDude;
import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bitsquare.app.DevFlags;
import io.bitsquare.common.UserThread;
import io.bitsquare.common.util.Tuple2;
import io.bitsquare.common.util.Tuple3;
import io.bitsquare.common.util.Utilities;
import io.bitsquare.gui.Navigation;
import io.bitsquare.gui.common.view.ActivatableViewAndModel;
import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.components.*;
import io.bitsquare.gui.main.MainView;
import io.bitsquare.gui.main.account.AccountView;
import io.bitsquare.gui.main.account.content.arbitratorselection.ArbitratorSelectionView;
import io.bitsquare.gui.main.account.settings.AccountSettingsView;
import io.bitsquare.gui.main.funds.FundsView;
import io.bitsquare.gui.main.funds.withdrawal.WithdrawalView;
import io.bitsquare.gui.main.offer.OfferView;
import io.bitsquare.gui.main.overlays.notifications.Notification;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.overlays.windows.OfferDetailsWindow;
import io.bitsquare.gui.main.overlays.windows.QRCodeWindow;
import io.bitsquare.gui.main.portfolio.PortfolioView;
import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesView;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.gui.util.Layout;
import io.bitsquare.locale.BSResources;
import io.bitsquare.payment.PaymentAccount;
import io.bitsquare.payment.PaymentMethod;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.user.Preferences;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.geometry.*;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.scene.text.Font;
import javafx.stage.Window;
import javafx.util.StringConverter;
import net.glxn.qrgen.QRCode;
import net.glxn.qrgen.image.ImageType;
import org.bitcoinj.core.Coin;
import org.bitcoinj.uri.BitcoinURI;
import org.controlsfx.control.PopOver;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.jetbrains.annotations.NotNull;
import javax.inject.Inject;
import java.io.ByteArrayInputStream;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import static io.bitsquare.gui.util.FormBuilder.*;
import static javafx.beans.binding.Bindings.createStringBinding;
// TODO Implement other positioning method in InoutTextField to display it over the field instead of right side
// priceAmountHBox is too large after redesign as to be used as layoutReference.
@FxmlView
public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOfferViewModel> {
private final Navigation navigation;
private final BSFormatter formatter;
private final OfferDetailsWindow offerDetailsWindow;
private final Preferences preferences;
private ScrollPane scrollPane;
private GridPane gridPane;
private ImageView imageView;
private AddressTextField addressTextField;
private BalanceTextField balanceTextField;
private BusyAnimation waitingForFundsBusyAnimation, offerAvailabilityBusyAnimation;
private TitledGroupBg payFundsPane;
private Button nextButton, cancelButton1, cancelButton2, fundFromSavingsWalletButton, fundFromExternalWalletButton, takeOfferButton;
private InputTextField amountTextField;
private TextField paymentMethodTextField, currencyTextField, priceTextField, priceAsPercentageTextField, volumeTextField, amountRangeTextField;
private Label directionLabel, amountDescriptionLabel, addressLabel, balanceLabel, totalToPayLabel, totalToPayInfoIconLabel,
amountBtcLabel, priceCurrencyLabel, priceAsPercentageLabel,
volumeCurrencyLabel, amountRangeBtcLabel, priceDescriptionLabel, volumeDescriptionLabel, waitingForFundsLabel, offerAvailabilityLabel;
private TextFieldWithCopyIcon totalToPayTextField;
private PopOver totalToPayInfoPopover;
private OfferView.CloseHandler closeHandler;
private ChangeListener<Boolean> amountFocusedListener;
private int gridRow = 0;
private ComboBox<PaymentAccount> paymentAccountsComboBox;
private Label paymentAccountsLabel;
private Label paymentMethodLabel;
private Subscription offerWarningSubscription;
private Subscription errorMessageSubscription, isOfferAvailableSubscription;
private Subscription isWaitingForFundsSubscription;
private Subscription showWarningInvalidBtcDecimalPlacesSubscription;
private Subscription showTransactionPublishedScreenSubscription;
private SimpleBooleanProperty errorPopupDisplayed;
private boolean offerDetailsWindowDisplayed;
private Notification walletFundedNotification;
private ImageView qrCodeImageView;
private HBox fundingHBox;
private Subscription balanceSubscription;
// private Subscription noSufficientFeeSubscription;
// private MonadicBinding<Boolean> noSufficientFeeBinding;
private Subscription cancelButton2StyleSubscription;
private VBox priceAsPercentageInputBox;
private boolean clearXchangeWarningDisplayed;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private TakeOfferView(TakeOfferViewModel model, Navigation navigation, BSFormatter formatter,
OfferDetailsWindow offerDetailsWindow, Preferences preferences) {
super(model);
this.navigation = navigation;
this.formatter = formatter;
this.offerDetailsWindow = offerDetailsWindow;
this.preferences = preferences;
}
@Override
protected void initialize() {
addScrollPane();
addGridPane();
addPaymentGroup();
addAmountPriceGroup();
addFundingGroup();
balanceTextField.setFormatter(model.getFormatter());
amountFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText());
amountTextField.setText(model.amount.get());
};
}
@Override
protected void activate() {
addBindings();
addSubscriptions();
amountTextField.focusedProperty().addListener(amountFocusedListener);
if (offerAvailabilityBusyAnimation != null && !model.showPayFundsScreenDisplayed.get()) {
offerAvailabilityBusyAnimation.play();
offerAvailabilityLabel.setVisible(true);
offerAvailabilityLabel.setManaged(true);
} else {
offerAvailabilityLabel.setVisible(false);
offerAvailabilityLabel.setManaged(false);
}
if (waitingForFundsBusyAnimation != null && model.isWaitingForFunds.get()) {
waitingForFundsBusyAnimation.play();
waitingForFundsLabel.setVisible(true);
waitingForFundsLabel.setManaged(true);
} else {
waitingForFundsLabel.setVisible(false);
waitingForFundsLabel.setManaged(false);
}
volumeCurrencyLabel.setText(model.dataModel.getCurrencyCode());
String currencyCode = model.dataModel.getCurrencyCode();
priceDescriptionLabel.setText(BSResources.get("createOffer.amountPriceBox.priceDescriptionFiat", currencyCode));
/* priceDescriptionLabel.setText(CurrencyUtil.isCryptoCurrency(currencyCode) ?
BSResources.get("createOffer.amountPriceBox.priceDescriptionAltcoin", currencyCode) :
BSResources.get("createOffer.amountPriceBox.priceDescriptionFiat", currencyCode));*/
volumeDescriptionLabel.setText(model.volumeDescriptionLabel.get());
if (model.getPossiblePaymentAccounts().size() > 1) {
paymentAccountsComboBox.setItems(model.getPossiblePaymentAccounts());
paymentAccountsComboBox.getSelectionModel().select(0);
}
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
/* if (DevFlags.DEV_MODE)
UserThread.runAfter(() -> onShowPayFundsScreen(), 200, TimeUnit.MILLISECONDS);*/
maybeShowClearXchangeWarning();
}
private void maybeShowClearXchangeWarning() {
if (model.getPaymentMethod().getId().equals(PaymentMethod.CLEAR_X_CHANGE_ID) &&
!clearXchangeWarningDisplayed) {
clearXchangeWarningDisplayed = true;
UserThread.runAfter(() -> GUIUtil.showClearXchangeWarning(model.getPaymentMethod(), preferences),
500, TimeUnit.MILLISECONDS);
}
}
@Override
protected void deactivate() {
removeBindings();
removeSubscriptions();
amountTextField.focusedProperty().removeListener(amountFocusedListener);
if (offerAvailabilityBusyAnimation != null)
offerAvailabilityBusyAnimation.stop();
if (waitingForFundsBusyAnimation != null)
waitingForFundsBusyAnimation.stop();
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initWithData(Offer offer) {
model.initWithData(offer);
priceAsPercentageInputBox.setVisible(offer.getUseMarketBasedPrice());
if (model.getOffer().getDirection() == Offer.Direction.SELL) {
imageView.setId("image-buy-large");
directionLabel.setId("direction-icon-label-buy");
takeOfferButton.setId("buy-button-big");
takeOfferButton.setText("Review offer to buy bitcoin");
nextButton.setId("buy-button");
} else {
imageView.setId("image-sell-large");
directionLabel.setId("direction-icon-label-sell");
takeOfferButton.setId("sell-button-big");
nextButton.setId("sell-button");
takeOfferButton.setText("Review offer to sell bitcoin");
}
boolean showComboBox = model.getPossiblePaymentAccounts().size() > 1;
paymentAccountsLabel.setVisible(showComboBox);
paymentAccountsLabel.setManaged(showComboBox);
paymentAccountsComboBox.setVisible(showComboBox);
paymentAccountsComboBox.setManaged(showComboBox);
paymentMethodTextField.setVisible(!showComboBox);
paymentMethodTextField.setManaged(!showComboBox);
paymentMethodLabel.setVisible(!showComboBox);
paymentMethodLabel.setManaged(!showComboBox);
if (!showComboBox)
paymentMethodTextField.setText(BSResources.get(model.getPaymentMethod().getId()));
currencyTextField.setText(model.dataModel.getCurrencyNameAndCode());
directionLabel.setText(model.getDirectionLabel());
amountDescriptionLabel.setText(model.getAmountDescription());
amountRangeTextField.setText(model.getAmountRange());
priceTextField.setText(model.getPrice());
priceAsPercentageTextField.setText(model.marketPriceMargin);
addressTextField.setPaymentLabel(model.getPaymentLabel());
addressTextField.setAddress(model.dataModel.getAddressEntry().getAddressString());
if (offer.getPrice() == null)
new Popup().warning("You cannot take that offer as it uses a percentage price based on the " +
"market price but there is no price feed available.")
.onClose(this::close)
.show();
}
public void setCloseHandler(OfferView.CloseHandler closeHandler) {
this.closeHandler = closeHandler;
}
// called form parent as the view does not get notified when the tab is closed
public void onClose() {
Coin balance = model.dataModel.balance.get();
if (balance != null && balance.isPositive() && !model.takeOfferCompleted.get() && !DevFlags.DEV_MODE) {
model.dataModel.swapTradeToSavings();
new Popup().information("You had already funded that offer.\n" +
"Your funds have been moved to your local Bitsquare wallet and are available for " +
"withdrawal in the \"Funds/Available for withdrawal\" screen.")
.actionButtonText("Go to \"Funds/Available for withdrawal\"")
.onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class))
.show();
}
// TODO need other implementation as it is displayed also if there are old funds in the wallet
/*
if (model.dataModel.isWalletFunded.get())
new Popup().warning("You have already funds paid in.\nIn the <Funds/Open for withdrawal> section you can withdraw those funds.").show();*/
}
public void onTabSelected(boolean isSelected) {
model.dataModel.onTabSelected(isSelected);
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
private void onTakeOffer() {
if (model.hasAcceptedArbitrators()) {
if (!DevFlags.DEV_MODE) {
offerDetailsWindow.onTakeOffer(() ->
model.onTakeOffer(() -> {
offerDetailsWindow.hide();
offerDetailsWindowDisplayed = false;
})
).show(model.getOffer(), model.dataModel.amountAsCoin.get(), model.dataModel.tradePrice);
offerDetailsWindowDisplayed = true;
} else {
model.onTakeOffer(() -> {
});
}
} else {
new Popup().warning("You have no arbitrator selected.\n" +
"You need to select at least one arbitrator.")
.actionButtonText("Go to \"Arbitrator selection\"")
.onAction(() -> navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, ArbitratorSelectionView.class))
.show();
}
}
private void onShowPayFundsScreen() {
model.onShowPayFundsScreen();
amountTextField.setMouseTransparent(true);
amountTextField.setFocusTraversable(false);
priceTextField.setMouseTransparent(true);
priceAsPercentageTextField.setMouseTransparent(true);
volumeTextField.setMouseTransparent(true);
balanceTextField.setTargetAmount(model.dataModel.totalToPayAsCoin.get());
if (!DevFlags.DEV_MODE) {
String key = "securityDepositInfo";
new Popup().backgroundInfo("To ensure that both traders follow the trade protocol they need to pay a security deposit.\n\n" +
"The deposit will stay in your local trading wallet until the offer gets accepted by another trader.\n" +
"It will be refunded to you after the trade has successfully completed.")
.actionButtonText("Visit FAQ web page")
.onAction(() -> GUIUtil.openWebPage("https://bitsquare.io/faq#6"))
.closeButtonText("I understand")
.dontShowAgainId(key, preferences)
.show();
key = "takeOfferFundWalletInfo";
String tradeAmountText = model.isSeller() ? "- Trade amount: " + model.getAmount() + "\n" : "";
new Popup().headLine("Fund your trade").instruction("You need to deposit " +
model.totalToPay.get() + " for taking this offer.\n\n" +
"The amount is the sum of:\n" +
tradeAmountText +
"- Security deposit: " + model.getSecurityDeposit() + "\n" +
"- Trading fee: " + model.getTakerFee() + "\n" +
"- Bitcoin mining fee: " + model.getNetworkFee() + "\n\n" +
"You can choose between two options when funding your trade:\n" +
"- Use your Bitsquare wallet (convenient, but transactions may be linkable) OR\n" +
"- Transfer from an external wallet (potentially more private)\n\n" +
"You will see all funding options and details after closing this popup.")
.dontShowAgainId(key, preferences)
.show();
}
nextButton.setVisible(false);
offerAvailabilityBusyAnimation.stop();
cancelButton1.setVisible(false);
cancelButton1.setOnAction(null);
cancelButton2.setVisible(true);
waitingForFundsBusyAnimation.play();
payFundsPane.setVisible(true);
totalToPayLabel.setVisible(true);
totalToPayInfoIconLabel.setVisible(true);
totalToPayTextField.setVisible(true);
addressLabel.setVisible(true);
addressTextField.setVisible(true);
qrCodeImageView.setVisible(true);
balanceLabel.setVisible(true);
balanceTextField.setVisible(true);
setupTotalToPayInfoIconLabel();
if (model.dataModel.isWalletFunded.get()) {
if (walletFundedNotification == null) {
walletFundedNotification = new Notification()
.headLine("Trading wallet update")
.notification("Your trading wallet was already sufficiently funded from an earlier take offer attempt.\n" +
"Amount: " + formatter.formatCoinWithCode(model.dataModel.totalToPayAsCoin.get()))
.autoClose();
walletFundedNotification.show();
}
}
final byte[] imageBytes = QRCode
.from(getBitcoinURI())
.withSize(98, 98) // code has 41 elements 8 px is border with 98 we get double scale and min. border
.to(ImageType.PNG)
.stream()
.toByteArray();
Image qrImage = new Image(new ByteArrayInputStream(imageBytes));
qrCodeImageView.setImage(qrImage);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Navigation
///////////////////////////////////////////////////////////////////////////////////////////
private void close() {
if (closeHandler != null)
closeHandler.close();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Bindings, Listeners
///////////////////////////////////////////////////////////////////////////////////////////
private void addBindings() {
amountBtcLabel.textProperty().bind(model.btcCode);
amountTextField.textProperty().bindBidirectional(model.amount);
volumeTextField.textProperty().bindBidirectional(model.volume);
totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.dataModel.missingCoin);
amountTextField.validationResultProperty().bind(model.amountValidationResult);
priceCurrencyLabel.textProperty().bind(createStringBinding(() -> model.dataModel.getCurrencyCode() + "/" + model.btcCode.get(), model.btcCode));
priceAsPercentageLabel.prefWidthProperty().bind(priceCurrencyLabel.widthProperty());
amountRangeBtcLabel.textProperty().bind(model.btcCode);
nextButton.disableProperty().bind(model.isNextButtonDisabled);
// funding
fundingHBox.visibleProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
fundingHBox.managedProperty().bind(model.dataModel.isWalletFunded.not().and(model.showPayFundsScreenDisplayed));
waitingForFundsLabel.textProperty().bind(model.spinnerInfoText);
takeOfferButton.disableProperty().bind(model.isTakeOfferButtonDisabled);
takeOfferButton.visibleProperty().bind(model.dataModel.isWalletFunded.and(model.showPayFundsScreenDisplayed));
takeOfferButton.managedProperty().bind(model.dataModel.isWalletFunded.and(model.showPayFundsScreenDisplayed));
}
private void removeBindings() {
amountBtcLabel.textProperty().unbind();
amountTextField.textProperty().unbindBidirectional(model.amount);
volumeTextField.textProperty().unbindBidirectional(model.volume);
totalToPayTextField.textProperty().unbind();
addressTextField.amountAsCoinProperty().unbind();
amountTextField.validationResultProperty().unbind();
priceCurrencyLabel.textProperty().unbind();
priceAsPercentageLabel.prefWidthProperty().unbind();
amountRangeBtcLabel.textProperty().unbind();
nextButton.disableProperty().unbind();
// funding
fundingHBox.visibleProperty().unbind();
fundingHBox.managedProperty().unbind();
waitingForFundsLabel.textProperty().unbind();
takeOfferButton.visibleProperty().unbind();
takeOfferButton.managedProperty().unbind();
takeOfferButton.disableProperty().unbind();
}
private void addSubscriptions() {
errorPopupDisplayed = new SimpleBooleanProperty();
offerWarningSubscription = EasyBind.subscribe(model.offerWarning, newValue -> {
if (newValue != null) {
if (offerDetailsWindowDisplayed)
offerDetailsWindow.hide();
UserThread.runAfter(() -> new Popup().warning(newValue + "\n\n" +
"If you have already paid in funds you can withdraw it in the " +
"\"Funds/Available for withdrawal\" screen.")
.actionButtonText("Go to \"Available for withdrawal\"")
.onAction(() -> {
errorPopupDisplayed.set(true);
model.resetOfferWarning();
close();
navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class);
})
.onClose(() -> {
errorPopupDisplayed.set(true);
model.resetOfferWarning();
close();
})
.show(), 100, TimeUnit.MILLISECONDS);
}
});
errorMessageSubscription = EasyBind.subscribe(model.errorMessage, newValue -> {
if (newValue != null) {
new Popup().error(BSResources.get("takeOffer.error.message", model.errorMessage.get()) +
"Please try to restart you application and check your network connection to see if you can resolve the issue.")
.onClose(() -> {
errorPopupDisplayed.set(true);
model.resetErrorMessage();
close();
})
.show();
}
});
isOfferAvailableSubscription = EasyBind.subscribe(model.isOfferAvailable, isOfferAvailable -> {
if (isOfferAvailable)
offerAvailabilityBusyAnimation.stop();
offerAvailabilityLabel.setVisible(!isOfferAvailable);
offerAvailabilityLabel.setManaged(!isOfferAvailable);
});
isWaitingForFundsSubscription = EasyBind.subscribe(model.isWaitingForFunds, isWaitingForFunds -> {
waitingForFundsBusyAnimation.setIsRunning(isWaitingForFunds);
waitingForFundsLabel.setVisible(isWaitingForFunds);
waitingForFundsLabel.setManaged(isWaitingForFunds);
});
showWarningInvalidBtcDecimalPlacesSubscription = EasyBind.subscribe(model.showWarningInvalidBtcDecimalPlaces, newValue -> {
if (newValue) {
new Popup().warning(BSResources.get("takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces")).show();
model.showWarningInvalidBtcDecimalPlaces.set(false);
}
});
showTransactionPublishedScreenSubscription = EasyBind.subscribe(model.showTransactionPublishedScreen, newValue -> {
if (newValue && DevFlags.DEV_MODE) {
close();
} else if (newValue && model.getTrade() != null && model.getTrade().errorMessageProperty().get() == null) {
String key = "takeOfferSuccessInfo";
if (preferences.showAgain(key)) {
UserThread.runAfter(() -> new Popup().headLine(BSResources.get("takeOffer.success.headline"))
.feedback(BSResources.get("takeOffer.success.info"))
.actionButtonText("Go to \"Open trades\"")
.dontShowAgainId(key, preferences)
.onAction(() -> {
UserThread.runAfter(
() -> navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class)
, 100, TimeUnit.MILLISECONDS);
close();
})
.onClose(this::close)
.show(), 1);
} else {
close();
}
}
});
/* noSufficientFeeBinding = EasyBind.combine(model.dataModel.isWalletFunded, model.dataModel.isMainNet, model.dataModel.isFeeFromFundingTxSufficient,
(isWalletFunded, isMainNet, isFeeSufficient) -> isWalletFunded && isMainNet && !isFeeSufficient);
noSufficientFeeSubscription = noSufficientFeeBinding.subscribe((observable, oldValue, newValue) -> {
if (newValue)
new Popup().warning("The mining fee from your funding transaction is not sufficiently high.\n\n" +
"You need to use at least a mining fee of " +
model.formatter.formatCoinWithCode(FeePolicy.getMinRequiredFeeForFundingTx()) + ".\n\n" +
"The fee used in your funding transaction was only " +
model.formatter.formatCoinWithCode(model.dataModel.feeFromFundingTx) + ".\n\n" +
"The trade transactions might take too much time to be included in " +
"a block if the fee is too low.\n" +
"Please check at your external wallet that you set the required fee and " +
"do a funding again with the correct fee.\n\n" +
"In the \"Funds/Open for withdrawal\" section you can withdraw those funds.")
.closeButtonText("Close")
.onClose(() -> {
close();
navigation.navigateTo(MainView.class, FundsView.class, WithdrawalView.class);
})
.show();
});*/
balanceSubscription = EasyBind.subscribe(model.dataModel.balance, newValue -> balanceTextField.setBalance(newValue));
cancelButton2StyleSubscription = EasyBind.subscribe(takeOfferButton.visibleProperty(),
isVisible -> cancelButton2.setId(isVisible ? "cancel-button" : null));
}
private void removeSubscriptions() {
offerWarningSubscription.unsubscribe();
errorMessageSubscription.unsubscribe();
isOfferAvailableSubscription.unsubscribe();
isWaitingForFundsSubscription.unsubscribe();
showWarningInvalidBtcDecimalPlacesSubscription.unsubscribe();
showTransactionPublishedScreenSubscription.unsubscribe();
// noSufficientFeeSubscription.unsubscribe();
balanceSubscription.unsubscribe();
cancelButton2StyleSubscription.unsubscribe();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Build UI elements
///////////////////////////////////////////////////////////////////////////////////////////
private void addScrollPane() {
scrollPane = new ScrollPane();
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
scrollPane.setOnScroll(e -> InputTextField.hideErrorMessageDisplay());
AnchorPane.setLeftAnchor(scrollPane, 0d);
AnchorPane.setTopAnchor(scrollPane, 0d);
AnchorPane.setRightAnchor(scrollPane, 0d);
AnchorPane.setBottomAnchor(scrollPane, 0d);
root.getChildren().add(scrollPane);
}
private void addGridPane() {
gridPane = new GridPane();
gridPane.setPadding(new Insets(30, 25, -1, 25));
gridPane.setHgap(5);
gridPane.setVgap(5);
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
columnConstraints1.setHgrow(Priority.NEVER);
columnConstraints1.setMinWidth(200);
ColumnConstraints columnConstraints2 = new ColumnConstraints();
columnConstraints2.setHgrow(Priority.ALWAYS);
ColumnConstraints columnConstraints3 = new ColumnConstraints();
columnConstraints3.setHgrow(Priority.NEVER);
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2, columnConstraints3);
scrollPane.setContent(gridPane);
}
private void addPaymentGroup() {
TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, gridRow, 2, "Payment info");
GridPane.setColumnSpan(titledGroupBg, 3);
Tuple2<Label, ComboBox> tuple = addLabelComboBox(gridPane, gridRow, "Trading account:", Layout.FIRST_ROW_DISTANCE);
paymentAccountsLabel = tuple.first;
paymentAccountsLabel.setVisible(false);
paymentAccountsLabel.setManaged(false);
paymentAccountsComboBox = tuple.second;
paymentAccountsComboBox.setPromptText("Select trading account");
paymentAccountsComboBox.setConverter(new StringConverter<PaymentAccount>() {
@Override
public String toString(PaymentAccount paymentAccount) {
return paymentAccount.getAccountName() + " (" + paymentAccount.getSingleTradeCurrency().getCode() + ", " +
BSResources.get(paymentAccount.getPaymentMethod().getId()) + ")";
}
@Override
public PaymentAccount fromString(String s) {
return null;
}
});
paymentAccountsComboBox.setVisible(false);
paymentAccountsComboBox.setManaged(false);
paymentAccountsComboBox.setOnAction(e -> {
maybeShowClearXchangeWarning();
model.onPaymentAccountSelected(paymentAccountsComboBox.getSelectionModel().getSelectedItem());
});
Tuple2<Label, TextField> tuple2 = addLabelTextField(gridPane, gridRow, "Payment method:", "", Layout.FIRST_ROW_DISTANCE);
paymentMethodLabel = tuple2.first;
paymentMethodTextField = tuple2.second;
currencyTextField = addLabelTextField(gridPane, ++gridRow, "Trade currency:", "").second;
}
private void addAmountPriceGroup() {
TitledGroupBg titledGroupBg = addTitledGroupBg(gridPane, ++gridRow, 2, "Set amount and price", Layout.GROUP_DISTANCE);
GridPane.setColumnSpan(titledGroupBg, 3);
imageView = new ImageView();
imageView.setPickOnBounds(true);
directionLabel = new Label();
directionLabel.setAlignment(Pos.CENTER);
directionLabel.setPadding(new Insets(-5, 0, 0, 0));
VBox imageVBox = new VBox();
imageVBox.setAlignment(Pos.CENTER);
imageVBox.setSpacing(6);
imageVBox.getChildren().addAll(imageView, directionLabel);
GridPane.setRowIndex(imageVBox, gridRow);
GridPane.setRowSpan(imageVBox, 2);
GridPane.setMargin(imageVBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 10, 10));
gridPane.getChildren().add(imageVBox);
addAmountPriceFields();
addSecondRow();
HBox hBox = new HBox();
hBox.setSpacing(10);
nextButton = new Button(BSResources.get("takeOffer.amountPriceBox.next"));
nextButton.setDefaultButton(true);
nextButton.setOnAction(e -> onShowPayFundsScreen());
//UserThread.runAfter(() -> nextButton.requestFocus(), 100, TimeUnit.MILLISECONDS);
cancelButton1 = new Button(BSResources.get("shared.cancel"));
cancelButton1.setDefaultButton(false);
cancelButton1.setId("cancel-button");
cancelButton1.setOnAction(e -> {
model.dataModel.swapTradeToSavings();
close();
});
offerAvailabilityBusyAnimation = new BusyAnimation();
offerAvailabilityLabel = new Label(BSResources.get("takeOffer.fundsBox.isOfferAvailable"));
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(nextButton, cancelButton1, offerAvailabilityBusyAnimation, offerAvailabilityLabel);
GridPane.setRowIndex(hBox, ++gridRow);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(-30, 0, 0, 0));
gridPane.getChildren().add(hBox);
}
private void addFundingGroup() {
// don't increase gridRow as we removed button when this gets visible
payFundsPane = addTitledGroupBg(gridPane, gridRow, 3, BSResources.get("takeOffer.fundsBox.title"), Layout.GROUP_DISTANCE);
GridPane.setColumnSpan(payFundsPane, 3);
payFundsPane.setVisible(false);
totalToPayLabel = new Label(BSResources.get("takeOffer.fundsBox.totalsNeeded"));
totalToPayLabel.setVisible(false);
totalToPayInfoIconLabel = new Label();
totalToPayInfoIconLabel.setVisible(false);
HBox totalToPayBox = new HBox();
totalToPayBox.setSpacing(4);
totalToPayBox.setAlignment(Pos.CENTER_RIGHT);
totalToPayBox.getChildren().addAll(totalToPayLabel, totalToPayInfoIconLabel);
GridPane.setMargin(totalToPayBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
GridPane.setRowIndex(totalToPayBox, gridRow);
gridPane.getChildren().add(totalToPayBox);
totalToPayTextField = new TextFieldWithCopyIcon();
totalToPayTextField.setFocusTraversable(false);
totalToPayTextField.setVisible(false);
totalToPayTextField.setPromptText(BSResources.get("createOffer.fundsBox.totalsNeeded.prompt"));
totalToPayTextField.setCopyWithoutCurrencyPostFix(true);
GridPane.setRowIndex(totalToPayTextField, gridRow);
GridPane.setColumnIndex(totalToPayTextField, 1);
GridPane.setMargin(totalToPayTextField, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
gridPane.getChildren().add(totalToPayTextField);
qrCodeImageView = new ImageView();
qrCodeImageView.setVisible(false);
qrCodeImageView.setStyle("-fx-cursor: hand;");
Tooltip.install(qrCodeImageView, new Tooltip("Open large QR-Code window"));
qrCodeImageView.setOnMouseClicked(e -> GUIUtil.showFeeInfoBeforeExecute(
() -> UserThread.runAfter(
() -> new QRCodeWindow(getBitcoinURI()).show(),
200, TimeUnit.MILLISECONDS)
));
GridPane.setRowIndex(qrCodeImageView, gridRow);
GridPane.setColumnIndex(qrCodeImageView, 2);
GridPane.setRowSpan(qrCodeImageView, 3);
GridPane.setMargin(qrCodeImageView, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE - 9, 0, 0, 5));
gridPane.getChildren().add(qrCodeImageView);
Tuple2<Label, AddressTextField> addressTuple = addLabelAddressTextField(gridPane, ++gridRow, BSResources.get("takeOffer.fundsBox.address"));
addressLabel = addressTuple.first;
addressLabel.setVisible(false);
addressTextField = addressTuple.second;
addressTextField.setVisible(false);
Tuple2<Label, BalanceTextField> balanceTuple = addLabelBalanceTextField(gridPane, ++gridRow, BSResources.get("takeOffer.fundsBox.balance"));
balanceLabel = balanceTuple.first;
balanceLabel.setVisible(false);
balanceTextField = balanceTuple.second;
balanceTextField.setVisible(false);
fundingHBox = new HBox();
fundingHBox.setVisible(false);
fundingHBox.setManaged(false);
fundingHBox.setSpacing(10);
fundFromSavingsWalletButton = new Button("Transfer funds from Bitsquare wallet");
fundFromSavingsWalletButton.setDefaultButton(true);
fundFromSavingsWalletButton.setDefaultButton(false);
fundFromSavingsWalletButton.setOnAction(e -> model.fundFromSavingsWallet());
Label label = new Label("OR");
label.setPadding(new Insets(5, 0, 0, 0));
fundFromExternalWalletButton = new Button("Open your external wallet for funding");
fundFromExternalWalletButton.setDefaultButton(false);
fundFromExternalWalletButton.setOnAction(e -> GUIUtil.showFeeInfoBeforeExecute(this::openWallet));
waitingForFundsBusyAnimation = new BusyAnimation(false);
waitingForFundsLabel = new Label();
waitingForFundsLabel.setPadding(new Insets(5, 0, 0, 0));
fundingHBox.getChildren().addAll(fundFromSavingsWalletButton, label, fundFromExternalWalletButton, waitingForFundsBusyAnimation, waitingForFundsLabel);
GridPane.setRowIndex(fundingHBox, ++gridRow);
GridPane.setColumnIndex(fundingHBox, 1);
GridPane.setMargin(fundingHBox, new Insets(15, 10, 0, 0));
gridPane.getChildren().add(fundingHBox);
takeOfferButton = addButtonAfterGroup(gridPane, gridRow, "");
takeOfferButton.setVisible(false);
takeOfferButton.setManaged(false);
takeOfferButton.setMinHeight(40);
takeOfferButton.setPadding(new Insets(0, 20, 0, 20));
takeOfferButton.setOnAction(e -> onTakeOffer());
cancelButton2 = addButton(gridPane, ++gridRow, BSResources.get("shared.cancel"));
cancelButton2.setOnAction(e -> {
if (model.dataModel.isWalletFunded.get()) {
new Popup().warning("You have already funded that offer.\n" +
"If you cancel now, your funds will be moved to your local Bitsquare wallet and are available " +
"for withdrawal in the \"Funds/Available for withdrawal\" screen.\n" +
"Are you sure you want to cancel?")
.closeButtonText("No")
.actionButtonText("Yes, cancel")
.onAction(() -> {
model.dataModel.swapTradeToSavings();
close();
})
.show();
} else {
close();
model.dataModel.swapTradeToSavings();
}
});
cancelButton2.setDefaultButton(false);
cancelButton2.setVisible(false);
}
private void openWallet() {
try {
Utilities.openURI(URI.create(getBitcoinURI()));
} catch (Exception ex) {
log.warn(ex.getMessage());
new Popup().warning("Opening a default bitcoin wallet application has failed. " +
"Perhaps you don't have one installed?").show();
}
}
@NotNull
private String getBitcoinURI() {
String addressString = model.dataModel.getAddressEntry().getAddressString();
return addressString != null ? BitcoinURI.convertToBitcoinURI(addressString, model.dataModel.missingCoin.get(),
model.getPaymentLabel(), null) : "";
}
private void addAmountPriceFields() {
// amountBox
Tuple3<HBox, InputTextField, Label> amountValueCurrencyBoxTuple = getAmountCurrencyBox(BSResources.get("takeOffer.amount.prompt"));
HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first;
amountTextField = amountValueCurrencyBoxTuple.second;
amountBtcLabel = amountValueCurrencyBoxTuple.third;
Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, model.getAmountDescription());
amountDescriptionLabel = amountInputBoxTuple.first;
VBox amountBox = amountInputBoxTuple.second;
// x
Label xLabel = new Label("x");
xLabel.setFont(Font.font("Helvetica-Bold", 20));
xLabel.setPadding(new Insets(14, 3, 0, 3));
// price
Tuple3<HBox, TextField, Label> priceValueCurrencyBoxTuple = getValueCurrencyBox();
HBox priceValueCurrencyBox = priceValueCurrencyBoxTuple.first;
priceTextField = priceValueCurrencyBoxTuple.second;
priceCurrencyLabel = priceValueCurrencyBoxTuple.third;
Tuple2<Label, VBox> priceInputBoxTuple = getTradeInputBox(priceValueCurrencyBox, BSResources.get("takeOffer.amountPriceBox.priceDescription"));
priceDescriptionLabel = priceInputBoxTuple.first;
VBox priceBox = priceInputBoxTuple.second;
// =
Label resultLabel = new Label("=");
resultLabel.setFont(Font.font("Helvetica-Bold", 20));
resultLabel.setPadding(new Insets(14, 2, 0, 2));
// volume
Tuple3<HBox, TextField, Label> volumeValueCurrencyBoxTuple = getValueCurrencyBox();
HBox volumeValueCurrencyBox = volumeValueCurrencyBoxTuple.first;
volumeTextField = volumeValueCurrencyBoxTuple.second;
volumeCurrencyLabel = volumeValueCurrencyBoxTuple.third;
Tuple2<Label, VBox> volumeInputBoxTuple = getTradeInputBox(volumeValueCurrencyBox, model.volumeDescriptionLabel.get());
volumeDescriptionLabel = volumeInputBoxTuple.first;
VBox volumeBox = volumeInputBoxTuple.second;
HBox hBox = new HBox();
hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(amountBox, xLabel, priceBox, resultLabel, volumeBox);
GridPane.setRowIndex(hBox, gridRow);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(Layout.FIRST_ROW_AND_GROUP_DISTANCE, 10, 0, 0));
GridPane.setColumnSpan(hBox, 2);
gridPane.getChildren().add(hBox);
}
private void addSecondRow() {
Tuple3<HBox, TextField, Label> priceAsPercentageTuple = getValueCurrencyBox();
HBox priceAsPercentageValueCurrencyBox = priceAsPercentageTuple.first;
priceAsPercentageTextField = priceAsPercentageTuple.second;
priceAsPercentageLabel = priceAsPercentageTuple.third;
Tuple2<Label, VBox> priceAsPercentageInputBoxTuple = getTradeInputBox(priceAsPercentageValueCurrencyBox, "Distance in % from market price");
priceAsPercentageInputBoxTuple.first.setPrefWidth(220);
priceAsPercentageInputBox = priceAsPercentageInputBoxTuple.second;
priceAsPercentageTextField.setPromptText("Enter % value");
priceAsPercentageLabel.setText("%");
priceAsPercentageLabel.setStyle("-fx-alignment: center;");
Tuple3<HBox, TextField, Label> amountValueCurrencyBoxTuple = getValueCurrencyBox();
HBox amountValueCurrencyBox = amountValueCurrencyBoxTuple.first;
amountRangeTextField = amountValueCurrencyBoxTuple.second;
amountRangeBtcLabel = amountValueCurrencyBoxTuple.third;
Tuple2<Label, VBox> amountInputBoxTuple = getTradeInputBox(amountValueCurrencyBox, BSResources.get("takeOffer.amountPriceBox.amountRangeDescription"));
Label xLabel = new Label("x");
xLabel.setFont(Font.font("Helvetica-Bold", 20));
xLabel.setPadding(new Insets(14, 3, 0, 3));
xLabel.setVisible(false); // we just use it to get the same layout as the upper row
HBox hBox = new HBox();
hBox.setSpacing(5);
hBox.setAlignment(Pos.CENTER_LEFT);
hBox.getChildren().addAll(amountInputBoxTuple.second, xLabel, priceAsPercentageInputBox);
GridPane.setRowIndex(hBox, ++gridRow);
GridPane.setColumnIndex(hBox, 1);
GridPane.setMargin(hBox, new Insets(5, 10, 5, 0));
GridPane.setColumnSpan(hBox, 2);
gridPane.getChildren().add(hBox);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private Tuple2<Label, VBox> getTradeInputBox(HBox amountValueBox, String promptText) {
Label descriptionLabel = new Label(promptText);
descriptionLabel.setId("input-description-label");
descriptionLabel.setPrefWidth(190);
VBox box = new VBox();
box.setSpacing(4);
box.getChildren().addAll(descriptionLabel, amountValueBox);
return new Tuple2<>(descriptionLabel, box);
}
private Tuple3<HBox, InputTextField, Label> getAmountCurrencyBox(String promptText) {
InputTextField input = new InputTextField();
input.setPrefWidth(190);
input.setAlignment(Pos.CENTER_RIGHT);
input.setId("text-input-with-currency-text-field");
input.setPromptText(promptText);
Label currency = new Label();
currency.setId("currency-info-label");
HBox box = new HBox();
box.getChildren().addAll(input, currency);
return new Tuple3<>(box, input, currency);
}
private Tuple3<HBox, TextField, Label> getValueCurrencyBox() {
TextField textField = new InputTextField();
textField.setPrefWidth(190);
textField.setAlignment(Pos.CENTER_RIGHT);
textField.setId("text-input-with-currency-text-field");
textField.setMouseTransparent(true);
textField.setEditable(false);
textField.setFocusTraversable(false);
Label currency = new Label();
currency.setId("currency-info-label-disabled");
HBox box = new HBox();
box.getChildren().addAll(textField, currency);
return new Tuple3<>(box, textField, currency);
}
private void setupTotalToPayInfoIconLabel() {
totalToPayInfoIconLabel.setId("clickable-icon");
AwesomeDude.setIcon(totalToPayInfoIconLabel, AwesomeIcon.QUESTION_SIGN);
totalToPayInfoIconLabel.setOnMouseEntered(e -> createInfoPopover());
totalToPayInfoIconLabel.setOnMouseExited(e -> {
if (totalToPayInfoPopover != null)
totalToPayInfoPopover.hide();
});
}
// As we don't use binding here we need to recreate it on mouse over to reflect the current state
private void createInfoPopover() {
GridPane infoGridPane = new GridPane();
infoGridPane.setHgap(5);
infoGridPane.setVgap(5);
infoGridPane.setPadding(new Insets(10, 10, 10, 10));
int i = 0;
if (model.isSeller())
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.tradeAmount"), model.getAmount());
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.securityDeposit"), model.getSecurityDeposit());
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.offerFee"), model.getTakerFee());
addPayInfoEntry(infoGridPane, i++, BSResources.get("takeOffer.fundsBox.networkFee"), model.getNetworkFee());
Separator separator = new Separator();
separator.setOrientation(Orientation.HORIZONTAL);
separator.setStyle("-fx-background: #666;");
GridPane.setConstraints(separator, 1, i++);
infoGridPane.getChildren().add(separator);
addPayInfoEntry(infoGridPane, i, BSResources.get("takeOffer.fundsBox.total"),
model.totalToPay.get());
totalToPayInfoPopover = new PopOver(infoGridPane);
if (totalToPayInfoIconLabel.getScene() != null) {
totalToPayInfoPopover.setDetachable(false);
totalToPayInfoPopover.setArrowIndent(5);
totalToPayInfoPopover.show(totalToPayInfoIconLabel.getScene().getWindow(),
getPopupPosition().getX(),
getPopupPosition().getY());
}
}
private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) {
Label label = new Label(labelText);
TextField textField = new TextField(value);
textField.setEditable(false);
textField.setFocusTraversable(false);
textField.setId("payment-info");
GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER);
GridPane.setConstraints(textField, 1, row);
infoGridPane.getChildren().addAll(label, textField);
}
private Point2D getPopupPosition() {
Window window = totalToPayInfoIconLabel.getScene().getWindow();
Point2D point = totalToPayInfoIconLabel.localToScene(0, 0);
double x = point.getX() + window.getX() + totalToPayInfoIconLabel.getWidth() + 2;
double y = point.getY() + window.getY() + Math.floor(totalToPayInfoIconLabel.getHeight() / 2) - 9;
return new Point2D(x, y);
}
}