diff --git a/src/main/java/io/bitsquare/gui/bitsquare.css b/src/main/java/io/bitsquare/gui/bitsquare.css index 90c576214e..d8f977302d 100644 --- a/src/main/java/io/bitsquare/gui/bitsquare.css +++ b/src/main/java/io/bitsquare/gui/bitsquare.css @@ -1,13 +1,22 @@ /* Theme colors: +logo colors: blue: 0096c9 +dark grey: #333333 + main bg grey: dddddd content bg grey: f4f4f4 */ /* Create offer */ +#direction-icon-label { + -fx-font-weight:bold; + -fx-font-size:16; + -fx-text-fill: #333; +} + #input-description-label { -fx-font-size: 11; -fx-alignment: center; @@ -28,6 +37,19 @@ content bg grey: f4f4f4 -fx-border-insets: 0 0 0 -2; } +#info-link { + -fx-padding: 0 0 0 -2; +} + +#totals-separator { + -fx-background: #aaa; +} + +#payment-info { + -fx-background-color: #f4f4f4; +} + + /* Registration */ #wizard-title-deactivated { -fx-font-weight: bold; @@ -193,15 +215,11 @@ content bg grey: f4f4f4 -fx-padding: 4 4 4 4; } -#address-label { +#address-text-field { -fx-cursor: hand; -fx-text-fill: #0096c9; - -fx-underline: true; - -fx-background-color: #FAFAFA; - -fx-border-radius: 4; - -fx-padding: 4 4 4 4; } -#address-label:hover { +#address-text-field:hover { -fx-text-fill: black; } @@ -303,10 +321,17 @@ content bg grey: f4f4f4 -fx-outer-border, -fx-inner-border, -fx-body-color; - -fx-background-insets: 0 0 -1 0, 0, 1.5, 2; + -fx-background-insets: 0 0 -1 0, 0, 1, 2; -fx-background-radius: 3px, 3px, 2px, 1px; } +#clickable-icon { + -fx-text-fill: #0096c9; + -fx-cursor: hand; +} +#clickable-icon:hover { + -fx-text-fill: #666; +} #form-title { -fx-font-weight: bold; diff --git a/src/main/java/io/bitsquare/gui/components/InputTextField.java b/src/main/java/io/bitsquare/gui/components/InputTextField.java index 88b7325d61..54e7be00c6 100644 --- a/src/main/java/io/bitsquare/gui/components/InputTextField.java +++ b/src/main/java/io/bitsquare/gui/components/InputTextField.java @@ -43,13 +43,16 @@ import org.slf4j.LoggerFactory; * There can be only 1 errorMessageDisplays at a time we use static field for it. * The position is derived from the position of the textField itself or if set from the layoutReference node. */ +//TODO There are some rare situation where it behaves buggy. Needs further investigation and improvements. Also +// consider replacement with controlsFX components. public class InputTextField extends TextField { private static final Logger log = LoggerFactory.getLogger(InputTextField.class); - private Effect invalidEffect = new DropShadow(BlurType.GAUSSIAN, Color.RED, 4, 0.0, 0, 0); + private final Effect invalidEffect = new DropShadow(BlurType.GAUSSIAN, Color.RED, 4, 0.0, 0, 0); - final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(new - InputValidator.ValidationResult(true)); + private final ObjectProperty amountValidationResult = new SimpleObjectProperty<> + (new + InputValidator.ValidationResult(true)); private static PopOver errorMessageDisplay; private Region layoutReference = this; @@ -100,7 +103,7 @@ public class InputTextField extends TextField { /////////////////////////////////////////////////////////////////////////////////////////// /** - * @param layoutReference The node used as reference for positioning. If not set explicitely the + * @param layoutReference The node used as reference for positioning. If not set explicitly the * ValidatingTextField instance is used. */ public void setLayoutReference(Region layoutReference) { @@ -123,18 +126,21 @@ public class InputTextField extends TextField { private void applyErrorMessage(InputValidator.ValidationResult validationResult) { if (validationResult.isValid) { - if (errorMessageDisplay != null) { + if (errorMessageDisplay != null) errorMessageDisplay.hide(); - } } else { + if (errorMessageDisplay == null) createErrorPopOver(validationResult.errorMessage); else ((Label) errorMessageDisplay.getContentNode()).setText(validationResult.errorMessage); - errorMessageDisplay.show(getScene().getWindow(), getErrorPopupPosition().getX(), - getErrorPopupPosition().getY()); + if (getScene() != null) + errorMessageDisplay.show(getScene().getWindow(), getErrorPopupPosition().getX(), + getErrorPopupPosition().getY()); + + InputTextField.errorMessageDisplay.setDetached(false); } } @@ -142,8 +148,10 @@ public class InputTextField extends TextField { Window window = getScene().getWindow(); Point2D point; point = layoutReference.localToScene(0, 0); - double x = point.getX() + window.getX() + layoutReference.getWidth() + 20; - double y = point.getY() + window.getY() + Math.floor(getHeight() / 2); + double x = Math.floor(point.getX() + window.getX() + layoutReference.getWidth() + 20 - getPadding().getLeft() - + getPadding().getRight()); + double y = Math.floor(point.getY() + window.getY() + getHeight() / 2 - getPadding().getTop() - getPadding() + .getBottom()); return new Point2D(x, y); } @@ -152,9 +160,11 @@ public class InputTextField extends TextField { Label errorLabel = new Label(errorMessage); errorLabel.setId("validation-error"); errorLabel.setPadding(new Insets(0, 10, 0, 10)); + errorLabel.setOnMouseClicked(e -> hideErrorMessageDisplay()); InputTextField.errorMessageDisplay = new PopOver(errorLabel); - InputTextField.errorMessageDisplay.setDetachable(false); + InputTextField.errorMessageDisplay.setDetachable(true); + InputTextField.errorMessageDisplay.setDetachedTitle("Close"); InputTextField.errorMessageDisplay.setArrowIndent(5); } diff --git a/src/main/java/io/bitsquare/gui/components/Popups.java b/src/main/java/io/bitsquare/gui/components/Popups.java index ac8ab683b8..e3d26bf1f9 100644 --- a/src/main/java/io/bitsquare/gui/components/Popups.java +++ b/src/main/java/io/bitsquare/gui/components/Popups.java @@ -30,24 +30,37 @@ import javafx.application.Platform; import org.controlsfx.control.action.Action; import org.controlsfx.dialog.Dialog; +import org.controlsfx.dialog.DialogStyle; import org.controlsfx.dialog.Dialogs; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** * Collection of controlsfx Popups */ public class Popups { + private static final Logger log = LoggerFactory.getLogger(Popups.class); // Information public static void openInformationPopup(String title, String message) { - openInformationPopup(title, message, null); + openInformationPopup(title, message, null, null); } public static void openInformationPopup(String title, String message, String masthead) { + List actions = new ArrayList<>(); + actions.add(Dialog.Actions.CLOSE); + openInformationPopup(title, message, masthead, actions); + } + + public static void openInformationPopup(String title, String message, String masthead, List actions) { Dialogs.create() .owner(BitSquare.getPrimaryStage()) .title(title) .message(message) .masthead(masthead) + .actions(actions) + .style(DialogStyle.UNDECORATED) .showInformation(); } diff --git a/src/main/java/io/bitsquare/gui/components/btc/AddressTextField.java b/src/main/java/io/bitsquare/gui/components/btc/AddressTextField.java index 1e55ff0703..ee48cdd045 100644 --- a/src/main/java/io/bitsquare/gui/components/btc/AddressTextField.java +++ b/src/main/java/io/bitsquare/gui/components/btc/AddressTextField.java @@ -34,6 +34,7 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Label; +import javafx.scene.control.TextField; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.*; @@ -65,9 +66,10 @@ public class AddressTextField extends AnchorPane { /////////////////////////////////////////////////////////////////////////////////////////// public AddressTextField() { - Label addressLabel = new Label(); - addressLabel.setFocusTraversable(false); - addressLabel.setId("address-label"); + + TextField addressLabel = new TextField(); + addressLabel.setId("address-text-field"); + addressLabel.setEditable(false); addressLabel.textProperty().bind(address); addressLabel.setOnMouseClicked(mouseEvent -> { try { @@ -79,6 +81,11 @@ public class AddressTextField extends AnchorPane { "Perhaps you don't have one installed?"); } }); + addressLabel.focusTraversableProperty().set(focusTraversableProperty().get()); + focusedProperty().addListener((ov, oldValue, newValue) -> { + addressLabel.requestFocus(); + log.debug("foc"); + }); Label copyIcon = new Label(); copyIcon.setLayoutY(3); diff --git a/src/main/java/io/bitsquare/gui/components/btc/BalanceTextField.java b/src/main/java/io/bitsquare/gui/components/btc/BalanceTextField.java index 1ac016ec98..26f33fbfcc 100644 --- a/src/main/java/io/bitsquare/gui/components/btc/BalanceTextField.java +++ b/src/main/java/io/bitsquare/gui/components/btc/BalanceTextField.java @@ -28,7 +28,9 @@ import com.google.bitcoin.core.Coin; import com.google.bitcoin.core.TransactionConfidence; import javafx.scene.control.*; +import javafx.scene.effect.*; import javafx.scene.layout.*; +import javafx.scene.paint.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +42,9 @@ public class BalanceTextField extends AnchorPane { private final Tooltip progressIndicatorTooltip; private final ConfidenceProgressIndicator progressIndicator; + private final Effect fundedEffect = new DropShadow(BlurType.GAUSSIAN, Color.GREEN, 4, 0.0, 0, 0); + private final Effect notFundedEffect = new DropShadow(BlurType.GAUSSIAN, Color.ORANGERED, 4, 0.0, 0, 0); + /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -123,6 +128,10 @@ public class BalanceTextField extends AnchorPane { private void updateBalance(Coin balance) { balanceTextField.setText(BSFormatter.formatCoin(balance)); + if (balance.isPositive()) + balanceTextField.setEffect(fundedEffect); + else + balanceTextField.setEffect(notFundedEffect); } } diff --git a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferCB.java b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferCB.java index a92215bc1c..e7b286775b 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferCB.java +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferCB.java @@ -18,38 +18,79 @@ package io.bitsquare.gui.trade.createoffer; import io.bitsquare.gui.CachedCodeBehind; +import io.bitsquare.gui.MainController; +import io.bitsquare.gui.NavigationItem; import io.bitsquare.gui.components.InputTextField; import io.bitsquare.gui.components.Popups; import io.bitsquare.gui.components.btc.AddressTextField; import io.bitsquare.gui.components.btc.BalanceTextField; -import io.bitsquare.gui.components.confidence.ConfidenceProgressIndicator; +import io.bitsquare.gui.help.Help; +import io.bitsquare.gui.help.HelpId; import io.bitsquare.gui.trade.TradeController; +import io.bitsquare.gui.util.ImageUtil; import io.bitsquare.trade.orderbook.OrderBookFilter; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import java.util.ResourceBundle; import javax.inject.Inject; +import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.geometry.HPos; +import javafx.geometry.Insets; +import javafx.geometry.Orientation; +import javafx.geometry.Point2D; +import javafx.geometry.VPos; import javafx.scene.control.*; +import javafx.scene.image.*; +import javafx.scene.input.*; import javafx.scene.layout.*; +import javafx.scene.text.*; +import javafx.stage.Window; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; + +import org.controlsfx.control.PopOver; +import org.controlsfx.control.action.AbstractAction; +import org.controlsfx.control.action.Action; +import org.controlsfx.dialog.Dialog; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +// 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. + public class CreateOfferCB extends CachedCodeBehind { private static final Logger log = LoggerFactory.getLogger(CreateOfferCB.class); - @FXML private Label buyLabel, confirmationLabel, txTitleLabel, collateralLabel; + private boolean detailsVisible; + private boolean advancedScreenInited; + + private ImageView expand; + private ImageView collapse; + private PopOver totalToPayInfoPopover; + + @FXML private ScrollPane scrollPane; + @FXML private ImageView payFundsInfoIcon, showDetailsInfoIcon; + @FXML private TextFlow payFundsInfoTextFlow, showDetailsInfoLabel; + @FXML private Pane priceAmountPane, payFundsPane, showDetailsPane; + @FXML private Label buyLabel, priceAmountTitleLabel, addressLabel, + balanceLabel, payFundsTitleLabel, totalToPayLabel, totalToPayInfoIconLabel, + showDetailsTitleLabel, bankAccountTypeLabel, bankAccountCurrencyLabel, bankAccountCountyLabel, + acceptedCountriesLabel, acceptedCountriesLabelIcon, acceptedLanguagesLabel, acceptedLanguagesLabelIcon, + acceptedArbitratorsLabel, acceptedArbitratorsLabelIcon; + @FXML private Button showPaymentInfoScreenButton, showAdvancedSettingsButton, placeOfferButton; + @FXML private InputTextField amountTextField, minAmountTextField, priceTextField, volumeTextField; - @FXML private Button placeOfferButton, closeButton; - @FXML private TextField totalToPayTextField, collateralTextField, bankAccountTypeTextField, + @FXML private TextField acceptedArbitratorsTextField, totalToPayTextField, bankAccountTypeTextField, bankAccountCurrencyTextField, bankAccountCountyTextField, acceptedCountriesTextField, - acceptedLanguagesTextField, - totalFeesTextField, transactionIdTextField; - @FXML private ConfidenceProgressIndicator progressIndicator; + acceptedLanguagesTextField; @FXML private AddressTextField addressTextField; @FXML private BalanceTextField balanceTextField; @@ -59,7 +100,7 @@ public class CreateOfferCB extends CachedCodeBehind { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - CreateOfferCB(CreateOfferPM presentationModel) { + private CreateOfferCB(CreateOfferPM presentationModel) { super(presentationModel); } @@ -74,14 +115,15 @@ public class CreateOfferCB extends CachedCodeBehind { setupBindings(); setupListeners(); - configTextFieldValidators(); balanceTextField.setup(presentationModel.getWalletFacade(), presentationModel.address.get()); } + @SuppressWarnings("EmptyMethod") public void activate() { super.activate(); } + @SuppressWarnings("EmptyMethod") public void deactivate() { super.deactivate(); } @@ -90,7 +132,7 @@ public class CreateOfferCB extends CachedCodeBehind { public void terminate() { super.terminate(); - // Used to re-enable createOfferButton in OrderBookController + // Used to reset disable state of createOfferButton in OrderBookController if (parentController != null) ((TradeController) parentController).onCreateOfferViewRemoved(); } @@ -109,22 +151,91 @@ public class CreateOfferCB extends CachedCodeBehind { /////////////////////////////////////////////////////////////////////////////////////////// @FXML - public void onPlaceOffer() { - presentationModel.placeOffer(); + private void onPlaceOffer() { + presentationModel.onPlaceOffer(); } @FXML - public void onClose() { + private void onShowPayFundsScreen() { + priceAmountPane.setId("form-group-background"); + priceAmountTitleLabel.setId("form-group-title"); + + showPaymentInfoScreenButton.setVisible(false); + + payFundsPane.setVisible(true); + totalToPayLabel.setVisible(true); + totalToPayInfoIconLabel.setVisible(true); + totalToPayTextField.setVisible(true); + addressLabel.setVisible(true); + addressTextField.setVisible(true); + balanceLabel.setVisible(true); + balanceTextField.setVisible(true); + payFundsInfoIcon.setVisible(true); + payFundsInfoTextFlow.setVisible(true); + showAdvancedSettingsButton.setVisible(true); + + if (expand == null) { + expand = ImageUtil.getIconImageView(ImageUtil.EXPAND); + collapse = ImageUtil.getIconImageView(ImageUtil.COLLAPSE); + } + showAdvancedSettingsButton.setGraphic(expand); + + setupTotalToPayInfoIconLabel(); + + presentationModel.onShowPayFundsScreen(); + } + + @FXML + private void onToggleShowAdvancedSettings() { + detailsVisible = !detailsVisible; + if (detailsVisible) { + showAdvancedSettingsButton.setText("Hide advanced settings"); + showAdvancedSettingsButton.setGraphic(collapse); + showDetailsScreen(); + } + else { + showAdvancedSettingsButton.setText("Show advanced settings"); + showAdvancedSettingsButton.setGraphic(expand); + hideDetailsScreen(); + } + } + + @FXML + private void onOpenGeneralHelp() { + Help.openWindow(HelpId.CREATE_OFFER_GENERAL); + } + + @FXML + private void onOpenFundingHelp() { + Help.openWindow(HelpId.CREATE_OFFER_FUNDING); + } + + @FXML + private void onOpenAdvancedSettingsHelp() { + Help.openWindow(HelpId.CREATE_OFFER_ADVANCED); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Navigation + /////////////////////////////////////////////////////////////////////////////////////////// + + private void openSettings() { + MainController.GET_INSTANCE().loadViewAndGetChildController(NavigationItem.SETTINGS); + } + + private void close() { TabPane tabPane = ((TabPane) (root.getParent().getParent())); tabPane.getTabs().remove(tabPane.getSelectionModel().getSelectedItem()); } - /////////////////////////////////////////////////////////////////////////////////////////// // Private Methods /////////////////////////////////////////////////////////////////////////////////////////// private void setupListeners() { + scrollPane.setOnScroll(e -> InputTextField.hideErrorMessageDisplay()); + // focus out amountTextField.focusedProperty().addListener((o, oldValue, newValue) -> { presentationModel.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText()); @@ -172,14 +283,46 @@ public class CreateOfferCB extends CachedCodeBehind { } }); - - presentationModel.requestPlaceOfferFailed.addListener((o, oldValue, newValue) -> { - if (newValue && presentationModel.requestPlaceOfferErrorMessage.get() != null) { + presentationModel.requestPlaceOfferErrorMessage.addListener((o, oldValue, newValue) -> { + if (newValue != null) { Popups.openErrorPopup("Error", "An error occurred when placing the offer.\n" + presentationModel.requestPlaceOfferErrorMessage.get()); } }); + presentationModel.showTransactionPublishedScreen.addListener((o, oldValue, newValue) -> { + if (newValue != null) { + // Dialogs are a bit limited. There is no callback for the InformationDialog button click, so we added + // our own actions. + List actions = new ArrayList<>(); + actions.add(new AbstractAction("Copy transaction ID") { + @Override + public void handle(ActionEvent actionEvent) { + Clipboard clipboard = Clipboard.getSystemClipboard(); + ClipboardContent content = new ClipboardContent(); + content.putString(presentationModel.transactionId.get()); + clipboard.setContent(content); + } + }); + actions.add(new AbstractAction("Close") { + @Override + public void handle(ActionEvent actionEvent) { + try { + close(); + } catch (Exception e) { + e.printStackTrace(); + } + Dialog.Actions.CLOSE.handle(actionEvent); + } + }); + + Popups.openInformationPopup("Offer published", + "The Bitcoin network transaction ID for the offer payment is: " + + presentationModel.transactionId.get(), + "Your offer has been successfully published to the distributed orderbook.", + actions); + } + }); } private void setupBindings() { @@ -190,10 +333,7 @@ public class CreateOfferCB extends CachedCodeBehind { priceTextField.textProperty().bindBidirectional(presentationModel.price); volumeTextField.textProperty().bindBidirectional(presentationModel.volume); - collateralLabel.textProperty().bind(presentationModel.collateralLabel); - collateralTextField.textProperty().bind(presentationModel.collateral); totalToPayTextField.textProperty().bind(presentationModel.totalToPay); - totalFeesTextField.textProperty().bind(presentationModel.totalFees); addressTextField.amountAsCoinProperty().bind(presentationModel.totalToPayAsCoin); addressTextField.paymentLabelProperty().bind(presentationModel.paymentLabel); @@ -205,7 +345,7 @@ public class CreateOfferCB extends CachedCodeBehind { acceptedCountriesTextField.textProperty().bind(presentationModel.acceptedCountries); acceptedLanguagesTextField.textProperty().bind(presentationModel.acceptedLanguages); - transactionIdTextField.textProperty().bind(presentationModel.transactionId); + acceptedArbitratorsTextField.textProperty().bind(presentationModel.acceptedArbitrators); // Validation amountTextField.amountValidationResultProperty().bind(presentationModel.amountValidationResult); @@ -216,14 +356,129 @@ public class CreateOfferCB extends CachedCodeBehind { // buttons placeOfferButton.visibleProperty().bind(presentationModel.isPlaceOfferButtonVisible); placeOfferButton.disableProperty().bind(presentationModel.isPlaceOfferButtonDisabled); - closeButton.visibleProperty().bind(presentationModel.isCloseButtonVisible); + // closeButton.visibleProperty().bind(presentationModel.isCloseButtonVisible); } - private void configTextFieldValidators() { - Region referenceNode = (Region) amountTextField.getParent(); - amountTextField.setLayoutReference(referenceNode); - priceTextField.setLayoutReference(referenceNode); - volumeTextField.setLayoutReference(referenceNode); + private void showDetailsScreen() { + payFundsPane.setId("form-group-background"); + payFundsTitleLabel.setId("form-group-title"); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); + scrollPane.layout(); + + if (!advancedScreenInited) { + initEditIcons(); + advancedScreenInited = true; + } + + toggleDetailsScreen(true); } + + private void initEditIcons() { + advancedScreenInited = true; + acceptedCountriesLabelIcon.setId("clickable-icon"); + AwesomeDude.setIcon(acceptedCountriesLabelIcon, AwesomeIcon.EDIT_SIGN); + Tooltip.install(acceptedCountriesLabelIcon, new Tooltip("Open settings for editing")); + acceptedCountriesLabelIcon.setOnMouseClicked(e -> openSettings()); + + acceptedLanguagesLabelIcon.setId("clickable-icon"); + AwesomeDude.setIcon(acceptedLanguagesLabelIcon, AwesomeIcon.EDIT_SIGN); + Tooltip.install(acceptedLanguagesLabelIcon, new Tooltip("Open settings for editing")); + acceptedLanguagesLabelIcon.setOnMouseClicked(e -> openSettings()); + + acceptedArbitratorsLabelIcon.setId("clickable-icon"); + AwesomeDude.setIcon(acceptedArbitratorsLabelIcon, AwesomeIcon.EDIT_SIGN); + Tooltip.install(acceptedArbitratorsLabelIcon, new Tooltip("Open settings for editing")); + acceptedArbitratorsLabelIcon.setOnMouseClicked(e -> openSettings()); + } + + private void hideDetailsScreen() { + payFundsPane.setId("form-group-background-active"); + payFundsTitleLabel.setId("form-group-title-active"); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + scrollPane.layout(); + + toggleDetailsScreen(false); + } + + private void toggleDetailsScreen(boolean visible) { + showDetailsPane.setVisible(visible); + showDetailsTitleLabel.setVisible(visible); + + acceptedCountriesLabel.setVisible(visible); + acceptedCountriesLabelIcon.setVisible(visible); + acceptedCountriesTextField.setVisible(visible); + acceptedLanguagesLabel.setVisible(visible); + acceptedLanguagesLabelIcon.setVisible(visible); + acceptedLanguagesTextField.setVisible(visible); + acceptedArbitratorsLabel.setVisible(visible); + acceptedArbitratorsLabelIcon.setVisible(visible); + acceptedArbitratorsTextField.setVisible(visible); + + bankAccountTypeLabel.setVisible(visible); + bankAccountTypeTextField.setVisible(visible); + bankAccountCurrencyLabel.setVisible(visible); + bankAccountCurrencyTextField.setVisible(visible); + bankAccountCountyLabel.setVisible(visible); + bankAccountCountyTextField.setVisible(visible); + + showDetailsInfoIcon.setVisible(visible); + showDetailsInfoLabel.setVisible(visible); + } + + private void setupTotalToPayInfoIconLabel() { + totalToPayInfoIconLabel.setId("clickable-icon"); + AwesomeDude.setIcon(totalToPayInfoIconLabel, AwesomeIcon.QUESTION_SIGN); + + GridPane infoGridPane = new GridPane(); + infoGridPane.setHgap(5); + infoGridPane.setVgap(5); + infoGridPane.setPadding(new Insets(10, 10, 10, 10)); + + addPayInfoEntry(infoGridPane, 0, "Collateral (" + presentationModel.collateralLabel.get() + ")", + presentationModel.collateral.get()); + addPayInfoEntry(infoGridPane, 1, "Offer fee:", presentationModel.offerFee.get()); + addPayInfoEntry(infoGridPane, 2, "Bitcoin network fee:", presentationModel.networkFee.get()); + Separator separator = new Separator(); + separator.setOrientation(Orientation.HORIZONTAL); + separator.setStyle("-fx-background: #666;"); + GridPane.setConstraints(separator, 1, 3); + infoGridPane.getChildren().add(separator); + addPayInfoEntry(infoGridPane, 4, "Total:", presentationModel.totalToPay.get()); + + totalToPayInfoIconLabel.setOnMouseEntered(e -> { + if (totalToPayInfoIconLabel.getScene() != null) { + totalToPayInfoPopover = new PopOver(infoGridPane); + totalToPayInfoPopover.setDetachable(false); + totalToPayInfoPopover.setArrowIndent(5); + totalToPayInfoPopover.show(totalToPayInfoIconLabel.getScene().getWindow(), + getPopupPosition().getX(), + getPopupPosition().getY()); + } + }); + totalToPayInfoIconLabel.setOnMouseExited(e -> { + if (totalToPayInfoPopover != null) + totalToPayInfoPopover.hide(); + }); + } + + 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() + 20; + double y = point.getY() + window.getY() + Math.floor(totalToPayInfoIconLabel.getHeight() / 2); + return new Point2D(x, y); + } + } diff --git a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferModel.java b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferModel.java index 9eb01292d5..24929fd939 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferModel.java +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferModel.java @@ -17,10 +17,12 @@ package io.bitsquare.gui.trade.createoffer; +import io.bitsquare.arbitrator.Arbitrator; import io.bitsquare.bank.BankAccount; import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.FeePolicy; import io.bitsquare.btc.WalletFacade; +import io.bitsquare.btc.listeners.BalanceListener; import io.bitsquare.gui.UIModel; import io.bitsquare.locale.Country; import io.bitsquare.settings.Settings; @@ -48,6 +50,9 @@ import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +73,7 @@ class CreateOfferModel extends UIModel { private final User user; private final String offerId; - private Direction direction = null; + @Nullable private Direction direction = null; AddressEntry addressEntry; @@ -81,7 +86,7 @@ class CreateOfferModel extends UIModel { final LongProperty collateralAsLong = new SimpleLongProperty(); final BooleanProperty requestPlaceOfferSuccess = new SimpleBooleanProperty(); - final BooleanProperty requestPlaceOfferFailed = new SimpleBooleanProperty(); + final BooleanProperty isWalletFunded = new SimpleBooleanProperty(); final ObjectProperty amountAsCoin = new SimpleObjectProperty<>(); final ObjectProperty minAmountAsCoin = new SimpleObjectProperty<>(); @@ -89,10 +94,12 @@ class CreateOfferModel extends UIModel { final ObjectProperty volumeAsFiat = new SimpleObjectProperty<>(); final ObjectProperty totalToPayAsCoin = new SimpleObjectProperty<>(); final ObjectProperty collateralAsCoin = new SimpleObjectProperty<>(); - final ObjectProperty totalFeesAsCoin = new SimpleObjectProperty<>(); + final ObjectProperty offerFeeAsCoin = new SimpleObjectProperty<>(); + final ObjectProperty networkFeeAsCoin = new SimpleObjectProperty<>(); - ObservableList acceptedCountries = FXCollections.observableArrayList(); - ObservableList acceptedLanguages = FXCollections.observableArrayList(); + final ObservableList acceptedCountries = FXCollections.observableArrayList(); + final ObservableList acceptedLanguages = FXCollections.observableArrayList(); + final ObservableList acceptedArbitrators = FXCollections.observableArrayList(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -121,10 +128,20 @@ class CreateOfferModel extends UIModel { super.initialized(); // static data - totalFeesAsCoin.set(FeePolicy.CREATE_OFFER_FEE.add(FeePolicy.TX_FEE)); + offerFeeAsCoin.set(FeePolicy.CREATE_OFFER_FEE); + networkFeeAsCoin.set(FeePolicy.TX_FEE); - if (walletFacade != null && walletFacade.getWallet() != null) + if (walletFacade != null && walletFacade.getWallet() != null) { addressEntry = walletFacade.getAddressInfoByTradeID(offerId); + + walletFacade.addBalanceListener(new BalanceListener(addressEntry.getAddress()) { + @Override + public void onBalanceChanged(@NotNull Coin balance) { + updateBalance(balance); + } + }); + updateBalance(walletFacade.getBalanceForAddress(addressEntry.getAddress())); + } } @Override @@ -136,6 +153,7 @@ class CreateOfferModel extends UIModel { collateralAsLong.set(settings.getCollateral()); acceptedCountries.setAll(settings.getAcceptedCountries()); acceptedLanguages.setAll(settings.getAcceptedLanguageLocales()); + acceptedArbitrators.setAll(settings.getAcceptedArbitrators()); } if (user != null) { @@ -148,11 +166,13 @@ class CreateOfferModel extends UIModel { } } + @SuppressWarnings("EmptyMethod") @Override public void deactivate() { super.deactivate(); } + @SuppressWarnings("EmptyMethod") @Override public void terminate() { super.terminate(); @@ -171,13 +191,10 @@ class CreateOfferModel extends UIModel { amountAsCoin.get(), minAmountAsCoin.get(), (transaction) -> { - requestPlaceOfferSuccess.set(true); transactionId.set(transaction.getHashAsString()); + requestPlaceOfferSuccess.set(true); }, - (errorMessage) -> { - requestPlaceOfferFailed.set(true); - requestPlaceOfferErrorMessage.set(errorMessage); - } + (errorMessage) -> requestPlaceOfferErrorMessage.set(errorMessage) ); } @@ -213,8 +230,9 @@ class CreateOfferModel extends UIModel { void calculateTotalToPay() { calculateCollateral(); try { - if (collateralAsCoin.get() != null) - totalToPayAsCoin.set(collateralAsCoin.get().add(totalFeesAsCoin.get())); + if (collateralAsCoin.get() != null) { + totalToPayAsCoin.set(offerFeeAsCoin.get().add(networkFeeAsCoin.get()).add(collateralAsCoin.get())); + } } catch (Throwable t) { // Should be never reached log.error(t.toString()); @@ -238,16 +256,21 @@ class CreateOfferModel extends UIModel { /////////////////////////////////////////////////////////////////////////////////////////// boolean isMinAmountLessOrEqualAmount() { + //noinspection SimplifiableIfStatement if (minAmountAsCoin.get() != null && amountAsCoin.get() != null) return !minAmountAsCoin.get().isGreaterThan(amountAsCoin.get()); return true; } + private void updateBalance(@NotNull Coin balance) { + isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0); + } /////////////////////////////////////////////////////////////////////////////////////////// // Setter/Getter /////////////////////////////////////////////////////////////////////////////////////////// + @Nullable Direction getDirection() { return direction; } diff --git a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferPM.java b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferPM.java index d33c7d923e..a1a271d6a1 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferPM.java +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferPM.java @@ -41,6 +41,8 @@ import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import org.jetbrains.annotations.NotNull; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -50,8 +52,8 @@ import static javafx.beans.binding.Bindings.createStringBinding; class CreateOfferPM extends PresentationModel { private static final Logger log = LoggerFactory.getLogger(CreateOfferPM.class); - private BtcValidator btcValidator = new BtcValidator(); - private FiatValidator fiatValidator = new FiatValidator(); + private final BtcValidator btcValidator = new BtcValidator(); + private final FiatValidator fiatValidator = new FiatValidator(); final StringProperty amount = new SimpleStringProperty(); final StringProperty minAmount = new SimpleStringProperty(); @@ -61,25 +63,25 @@ class CreateOfferPM extends PresentationModel { final StringProperty totalToPay = new SimpleStringProperty(); final StringProperty directionLabel = new SimpleStringProperty(); final StringProperty collateralLabel = new SimpleStringProperty(); - final StringProperty totalFees = new SimpleStringProperty(); + final StringProperty offerFee = new SimpleStringProperty(); + final StringProperty networkFee = new SimpleStringProperty(); final StringProperty bankAccountType = new SimpleStringProperty(); final StringProperty bankAccountCurrency = new SimpleStringProperty(); final StringProperty bankAccountCounty = new SimpleStringProperty(); final StringProperty acceptedCountries = new SimpleStringProperty(); final StringProperty acceptedLanguages = new SimpleStringProperty(); + final StringProperty acceptedArbitrators = new SimpleStringProperty(); final StringProperty addressAsString = new SimpleStringProperty(); final StringProperty paymentLabel = new SimpleStringProperty(); final StringProperty transactionId = new SimpleStringProperty(); final StringProperty requestPlaceOfferErrorMessage = new SimpleStringProperty(); - final BooleanProperty isCloseButtonVisible = new SimpleBooleanProperty(); - final BooleanProperty isPlaceOfferButtonVisible = new SimpleBooleanProperty(true); + final BooleanProperty isPlaceOfferButtonVisible = new SimpleBooleanProperty(false); final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(true); final BooleanProperty showWarningAdjustedVolume = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidFiatDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty(); final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty(); - final BooleanProperty requestPlaceOfferFailed = new SimpleBooleanProperty(); final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(); final ObjectProperty minAmountValidationResult = new SimpleObjectProperty<>(); @@ -123,16 +125,19 @@ class CreateOfferPM extends PresentationModel { setupListeners(); } + @SuppressWarnings("EmptyMethod") @Override public void activate() { super.activate(); } + @SuppressWarnings("EmptyMethod") @Override public void deactivate() { super.deactivate(); } + @SuppressWarnings("EmptyMethod") @Override public void terminate() { super.terminate(); @@ -144,9 +149,9 @@ class CreateOfferPM extends PresentationModel { /////////////////////////////////////////////////////////////////////////////////////////// // setOrderBookFilter is a one time call - void setOrderBookFilter(OrderBookFilter orderBookFilter) { + void setOrderBookFilter(@NotNull OrderBookFilter orderBookFilter) { model.setDirection(orderBookFilter.getDirection()); - directionLabel.set(model.getDirection() == Direction.BUY ? "Buy:" : "Sell:"); + directionLabel.set(model.getDirection() == Direction.BUY ? "Buy Bitcoin" : "Sell Bitcoin"); // apply only if valid if (orderBookFilter.getAmount() != null && isBtcInputValid(orderBookFilter.getAmount().toPlainString()) @@ -165,13 +170,12 @@ class CreateOfferPM extends PresentationModel { // UI actions (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// - void placeOffer() { + void onPlaceOffer() { model.requestPlaceOfferErrorMessage.set(null); - model.requestPlaceOfferFailed.set(false); model.requestPlaceOfferSuccess.set(false); - + isPlaceOfferButtonDisabled.set(true); - isPlaceOfferButtonVisible.set(true); + // isPlaceOfferButtonVisible.set(true); model.placeOffer(); } @@ -181,6 +185,10 @@ class CreateOfferPM extends PresentationModel { // UI events (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// + public void onShowPayFundsScreen() { + isPlaceOfferButtonVisible.set(true); + } + // On focus out we do validation and apply the data to the model void onFocusOutAmountTextField(Boolean oldValue, Boolean newValue, String userInput) { if (oldValue && !newValue) { @@ -318,6 +326,10 @@ class CreateOfferPM extends PresentationModel { } validateInput(); }); + model.isWalletFunded.addListener((ov, oldValue, newValue) -> { + if (newValue) + validateInput(); + }); // Binding with Bindings.createObjectBinding does not work because of bi-directional binding model.amountAsCoin.addListener((ov, oldValue, newValue) -> amount.set(formatCoin(newValue))); @@ -325,9 +337,9 @@ class CreateOfferPM extends PresentationModel { model.priceAsFiat.addListener((ov, oldValue, newValue) -> price.set(formatFiat(newValue))); model.volumeAsFiat.addListener((ov, oldValue, newValue) -> volume.set(formatFiat(newValue))); - model.requestPlaceOfferFailed.addListener((ov, oldValue, newValue) -> { - isPlaceOfferButtonDisabled.set(!newValue); - requestPlaceOfferFailed.set(newValue); + model.requestPlaceOfferErrorMessage.addListener((ov, oldValue, newValue) -> { + if (newValue != null) + isPlaceOfferButtonDisabled.set(false); }); model.requestPlaceOfferSuccess.addListener((ov, oldValue, newValue) -> isPlaceOfferButtonVisible.set (!newValue)); @@ -337,6 +349,8 @@ class CreateOfferPM extends PresentationModel { .countryLocalesToString(model.acceptedCountries))); model.acceptedLanguages.addListener((Observable o) -> acceptedLanguages.set(BSFormatter .languageLocalesToString(model.acceptedLanguages))); + model.acceptedArbitrators.addListener((Observable o) -> acceptedArbitrators.set(BSFormatter + .arbitratorsToString(model.acceptedArbitrators))); } private void setupBindings() { @@ -349,16 +363,19 @@ class CreateOfferPM extends PresentationModel { (model.collateralAsLong.get()) + "):", model.collateralAsLong)); totalToPayAsCoin.bind(model.totalToPayAsCoin); - totalFees.bind(createStringBinding(() -> formatCoinWithCode(model.totalFeesAsCoin.get()), - model.totalFeesAsCoin)); + offerFee.bind(createStringBinding(() -> formatCoinWithCode(model.offerFeeAsCoin.get()), + model.offerFeeAsCoin)); + networkFee.bind(createStringBinding(() -> formatCoinWithCode(model.networkFeeAsCoin.get()), + model.offerFeeAsCoin)); + bankAccountType.bind(Bindings.createStringBinding(() -> Localisation.get(model.bankAccountType.get()), model.bankAccountType)); bankAccountCurrency.bind(model.bankAccountCurrency); bankAccountCounty.bind(model.bankAccountCounty); - isCloseButtonVisible.bind(model.requestPlaceOfferSuccess); requestPlaceOfferErrorMessage.bind(model.requestPlaceOfferErrorMessage); showTransactionPublishedScreen.bind(model.requestPlaceOfferSuccess); + transactionId.bind(model.transactionId); } private void calculateVolume() { @@ -406,7 +423,8 @@ class CreateOfferPM extends PresentationModel { isBtcInputValid(minAmount.get()).isValid && isBtcInputValid(price.get()).isValid && isBtcInputValid(volume.get()).isValid && - model.isMinAmountLessOrEqualAmount()) + model.isMinAmountLessOrEqualAmount() && + model.isWalletFunded.get()) ); } diff --git a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferView.fxml b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferView.fxml index d89074575a..8974a96137 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferView.fxml +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferView.fxml @@ -19,157 +19,350 @@ - - + + - + - - - - - - - + + + + + -