diff --git a/src/main/java/io/bitsquare/di/BitSquareModule.java b/src/main/java/io/bitsquare/di/BitSquareModule.java index db36e389ff..6cb0ffc070 100644 --- a/src/main/java/io/bitsquare/di/BitSquareModule.java +++ b/src/main/java/io/bitsquare/di/BitSquareModule.java @@ -22,7 +22,6 @@ import io.bitsquare.btc.BlockChainFacade; import io.bitsquare.btc.FeePolicy; import io.bitsquare.btc.WalletFacade; import io.bitsquare.crypto.CryptoFacade; -import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.msg.BootstrappedPeerFactory; import io.bitsquare.msg.MessageFacade; import io.bitsquare.msg.P2PNode; @@ -64,7 +63,6 @@ public class BitSquareModule extends AbstractModule { bind(BootstrappedPeerFactory.class).asEagerSingleton(); bind(TradeManager.class).asEagerSingleton(); - bind(BSFormatter.class).asEagerSingleton(); //bind(String.class).annotatedWith(Names.named("networkType")).toInstance(WalletFacade.MAIN_NET); diff --git a/src/main/java/io/bitsquare/gui/components/ValidatingTextField.java b/src/main/java/io/bitsquare/gui/components/ValidatingTextField.java index c24c23181a..8f2dcaf54f 100644 --- a/src/main/java/io/bitsquare/gui/components/ValidatingTextField.java +++ b/src/main/java/io/bitsquare/gui/components/ValidatingTextField.java @@ -19,8 +19,8 @@ package io.bitsquare.gui.components; import io.bitsquare.gui.util.validation.InputValidator; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.control.*; @@ -43,15 +43,14 @@ import org.slf4j.LoggerFactory; */ public class ValidatingTextField extends TextField { private static final Logger log = LoggerFactory.getLogger(ValidatingTextField.class); - private static PopOver popOver; private Effect invalidEffect = new DropShadow(BlurType.GAUSSIAN, Color.RED, 4, 0.0, 0, 0); - private final BooleanProperty isValid = new SimpleBooleanProperty(true); - private InputValidator validator; - private boolean validateOnFocusOut = true; - private boolean needsValidationOnFocusOut; - private Region errorPopupLayoutReference; + final ObjectProperty amountValidationResult = new SimpleObjectProperty<>(new + InputValidator.ValidationResult(true)); + + private static PopOver popOver; + private Region errorPopupLayoutReference = this; /////////////////////////////////////////////////////////////////////////////////////////// @@ -62,15 +61,31 @@ public class ValidatingTextField extends TextField { if (popOver != null) popOver.hide(); } - - /////////////////////////////////////////////////////////////////////////////////////////// // Constructor /////////////////////////////////////////////////////////////////////////////////////////// public ValidatingTextField() { super(); - setupListeners(); + + amountValidationResult.addListener((ov, oldValue, newValue) -> { + if (newValue != null) { + setEffect(newValue.isValid ? null : invalidEffect); + + if (newValue.isValid) + hidePopover(); + else + applyErrorMessage(newValue); + } + }); + + sceneProperty().addListener((ov, oldValue, newValue) -> { + // we got removed from the scene + // lets hide an open popup + if (newValue == null) + hidePopover(); + }); + } @@ -78,21 +93,14 @@ public class ValidatingTextField extends TextField { // Public methods /////////////////////////////////////////////////////////////////////////////////////////// - public void reValidate() { - validate(getText()); - } - /////////////////////////////////////////////////////////////////////////////////////////// // Setters /////////////////////////////////////////////////////////////////////////////////////////// - public void setValidator(InputValidator validator) { - this.validator = validator; - } - /** - * @param errorPopupLayoutReference The node used as reference for positioning + * @param errorPopupLayoutReference The node used as reference for positioning. If not set explicitely the + * ValidatingTextField instance is used. */ public void setErrorPopupLayoutReference(Region errorPopupLayoutReference) { this.errorPopupLayoutReference = errorPopupLayoutReference; @@ -103,12 +111,8 @@ public class ValidatingTextField extends TextField { // Getters /////////////////////////////////////////////////////////////////////////////////////////// - public boolean getIsValid() { - return isValid.get(); - } - - public BooleanProperty isValidProperty() { - return isValid; + public ObjectProperty amountValidationResultProperty() { + return amountValidationResult; } @@ -116,40 +120,6 @@ public class ValidatingTextField extends TextField { // Private methods /////////////////////////////////////////////////////////////////////////////////////////// - private void setupListeners() { - sceneProperty().addListener((ov, oldValue, newValue) -> { - // we got removed from the scene - // lets hide an open popup - if (newValue == null) - hidePopover(); - }); - - textProperty().addListener((ov, oldValue, newValue) -> { - if (validator != null) { - if (!validateOnFocusOut) - validate(newValue); - else - needsValidationOnFocusOut = true; - } - }); - - focusedProperty().addListener((ov, oldValue, newValue) -> { - if (validateOnFocusOut && needsValidationOnFocusOut && - !newValue && getScene() != null && getScene().getWindow().isFocused()) - validate(getText()); - }); - - isValid.addListener((ov, oldValue, newValue) -> applyEffect(newValue)); - } - - private void validate(String input) { - if (input != null && validator != null) { - InputValidator.ValidationResult validationResult = validator.validate(input); - isValid.set(validationResult.isValid); - applyErrorMessage(validationResult); - } - } - private void applyErrorMessage(InputValidator.ValidationResult validationResult) { if (validationResult.isValid) { if (popOver != null) { @@ -160,35 +130,21 @@ public class ValidatingTextField extends TextField { if (popOver == null) createErrorPopOver(validationResult.errorMessage); else - setErrorMessage(validationResult.errorMessage); + ((Label) popOver.getContentNode()).setText(validationResult.errorMessage); popOver.show(getScene().getWindow(), getErrorPopupPosition().getX(), getErrorPopupPosition().getY()); } } - private void applyEffect(boolean isValid) { - setEffect(isValid ? null : invalidEffect); - } - private Point2D getErrorPopupPosition() { Window window = getScene().getWindow(); Point2D point; - double x; - if (errorPopupLayoutReference == null) { - point = localToScene(0, 0); - x = point.getX() + window.getX() + getWidth() + 20; - } - else { - point = errorPopupLayoutReference.localToScene(0, 0); - x = point.getX() + window.getX() + errorPopupLayoutReference.getWidth() + 20; - } + point = errorPopupLayoutReference.localToScene(0, 0); + double x = point.getX() + window.getX() + errorPopupLayoutReference.getWidth() + 20; double y = point.getY() + window.getY() + Math.floor(getHeight() / 2); return new Point2D(x, y); } - private static void setErrorMessage(String errorMessage) { - ((Label) popOver.getContentNode()).setText(errorMessage); - } private static void createErrorPopOver(String errorMessage) { Label errorLabel = new Label(errorMessage); @@ -196,8 +152,8 @@ public class ValidatingTextField extends TextField { errorLabel.setPadding(new Insets(0, 10, 0, 10)); popOver = new PopOver(errorLabel); - popOver.setAutoFix(true); - popOver.setDetachedTitle(""); + popOver.setDetachable(false); popOver.setArrowIndent(5); } + } \ No newline at end of file diff --git a/src/main/java/io/bitsquare/gui/trade/TradeController.java b/src/main/java/io/bitsquare/gui/trade/TradeController.java index 9cecebcad2..58b4c32a2b 100644 --- a/src/main/java/io/bitsquare/gui/trade/TradeController.java +++ b/src/main/java/io/bitsquare/gui/trade/TradeController.java @@ -75,6 +75,8 @@ public class TradeController extends CachedViewController { // TODO find better solution // Textfield focus out triggers validation, use runLater as quick fix... + + //TODO update to new verison ((TabPane) root).getSelectionModel().selectedIndexProperty().addListener((observableValue) -> Platform.runLater(ValidatingTextField::hidePopover)); } 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 f482203b71..d57fbc4b6d 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferCB.java +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferCB.java @@ -24,9 +24,6 @@ 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.trade.TradeController; -import io.bitsquare.gui.util.validation.BtcValidator; -import io.bitsquare.gui.util.validation.FiatValidator; -import io.bitsquare.gui.util.validation.ValidationHelper; import io.bitsquare.trade.orderbook.OrderBookFilter; import java.net.URL; @@ -78,8 +75,12 @@ public class CreateOfferCB extends CachedViewController { public void initialize(URL url, ResourceBundle rb) { super.initialize(url, rb); + //TODO handle in base class pm.onViewInitialized(); + setupBindings(); + setupListeners(); + configTextFieldValidators(); balanceTextField.setup(pm.getWalletFacade(), pm.address.get()); } @@ -87,21 +88,19 @@ public class CreateOfferCB extends CachedViewController { public void deactivate() { super.deactivate(); + //TODO handle in base class pm.deactivate(); //TODO check that again - ((TradeController) parentController).onCreateOfferViewRemoved(); + if (parentController != null) ((TradeController) parentController).onCreateOfferViewRemoved(); } @Override public void activate() { super.activate(); + //TODO handle in base class pm.activate(); - - setupBindings(); - setupListeners(); - setupTextFieldValidators(); } @@ -138,7 +137,7 @@ public class CreateOfferCB extends CachedViewController { private void setupListeners() { volumeTextField.focusedProperty().addListener((o, oldValue, newValue) -> { - pm.onFocusOutVolumeTextField(oldValue, newValue, volumeTextField.getText()); + pm.onFocusOutVolumeTextField(oldValue, newValue); volumeTextField.setText(pm.volume.get()); }); @@ -157,15 +156,6 @@ public class CreateOfferCB extends CachedViewController { minAmountTextField.setText(pm.minAmount.get()); }); - pm.needsInputValidation.addListener((o, oldValue, newValue) -> { - if (newValue) { - amountTextField.reValidate(); - minAmountTextField.reValidate(); - volumeTextField.reValidate(); - priceTextField.reValidate(); - } - }); - pm.showWarningInvalidBtcDecimalPlaces.addListener((o, oldValue, newValue) -> { if (newValue) { Popups.openWarningPopup("Warning", "The amount you have entered exceeds the number of allowed decimal" + @@ -182,11 +172,11 @@ public class CreateOfferCB extends CachedViewController { } }); - pm.showWarningInvalidBtcFractions.addListener((o, oldValue, newValue) -> { + pm.showWarningAdjustedVolume.addListener((o, oldValue, newValue) -> { if (newValue) { Popups.openWarningPopup("Warning", "The total volume you have entered leads to invalid fractional " + "Bitcoin amounts.\nThe amount has been adjusted and a new total volume be calculated from it."); - pm.showWarningInvalidBtcFractions.set(false); + pm.showWarningAdjustedVolume.set(false); volumeTextField.setText(pm.volume.get()); } }); @@ -224,46 +214,21 @@ public class CreateOfferCB extends CachedViewController { totalFeesTextField.textProperty().bind(pm.totalFees); transactionIdTextField.textProperty().bind(pm.transactionId); + amountTextField.amountValidationResultProperty().bind(pm.amountValidationResult); + minAmountTextField.amountValidationResultProperty().bind(pm.minAmountValidationResult); + priceTextField.amountValidationResultProperty().bind(pm.priceValidationResult); + volumeTextField.amountValidationResultProperty().bind(pm.volumeValidationResult); + placeOfferButton.visibleProperty().bind(pm.isPlaceOfferButtonVisible); placeOfferButton.disableProperty().bind(pm.isPlaceOfferButtonDisabled); closeButton.visibleProperty().bind(pm.isCloseButtonVisible); - - //TODO - /* progressIndicator.visibleProperty().bind(viewModel.isOfferPlacedScreen); - confirmationLabel.visibleProperty().bind(viewModel.isOfferPlacedScreen); - txTitleLabel.visibleProperty().bind(viewModel.isOfferPlacedScreen); - transactionIdTextField.visibleProperty().bind(viewModel.isOfferPlacedScreen); - */ - - // TODO - /* placeOfferButton.disableProperty().bind(amountTextField.isValidProperty() - .and(minAmountTextField.isValidProperty()) - .and(volumeTextField.isValidProperty()) - .and(priceTextField.isValidProperty()).not());*/ } - private void setupTextFieldValidators() { + private void configTextFieldValidators() { Region referenceNode = (Region) amountTextField.getParent(); - - BtcValidator amountValidator = new BtcValidator(); - amountTextField.setValidator(amountValidator); amountTextField.setErrorPopupLayoutReference(referenceNode); - - priceTextField.setValidator(new FiatValidator()); priceTextField.setErrorPopupLayoutReference(referenceNode); - - volumeTextField.setValidator(new FiatValidator()); volumeTextField.setErrorPopupLayoutReference(referenceNode); - - BtcValidator minAmountValidator = new BtcValidator(); - minAmountTextField.setValidator(minAmountValidator); - - ValidationHelper.setupMinAmountInRangeOfAmountValidation(amountTextField, - minAmountTextField, - pm.amount, - pm.minAmount, - amountValidator, - minAmountValidator); } } 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 15cd8ffb34..b267ebae05 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferModel.java +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferModel.java @@ -52,7 +52,7 @@ import org.slf4j.LoggerFactory; import static com.google.common.base.Preconditions.checkArgument; /** - * Domain for that UI element. + * Domain for that UI element. * Note that the create offer domain has a deeper scope in the application domain (TradeManager). * That model is just responsible for the domain specific parts displayed needed in that UI element. */ @@ -152,6 +152,15 @@ class CreateOfferModel { ); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Validation + /////////////////////////////////////////////////////////////////////////////////////////// + + boolean isMinAmountLessOrEqualAmount() { + if (minAmountAsCoin != null && amountAsCoin != null) + return !minAmountAsCoin.isGreaterThan(amountAsCoin); + return true; + } /////////////////////////////////////////////////////////////////////////////////////////// // Setter/Getter 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 8e679d04db..f2b00101cd 100644 --- a/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferPM.java +++ b/src/main/java/io/bitsquare/gui/trade/createoffer/CreateOfferPM.java @@ -19,6 +19,9 @@ package io.bitsquare.gui.trade.createoffer; import io.bitsquare.btc.WalletFacade; import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.validation.BtcValidator; +import io.bitsquare.gui.util.validation.FiatValidator; +import io.bitsquare.gui.util.validation.InputValidator; import io.bitsquare.locale.Localisation; import io.bitsquare.trade.Direction; import io.bitsquare.trade.orderbook.OrderBookFilter; @@ -46,6 +49,8 @@ class CreateOfferPM { private static final Logger log = LoggerFactory.getLogger(CreateOfferPM.class); private CreateOfferModel model; + private BtcValidator btcValidator = new BtcValidator(); + private FiatValidator fiatValidator = new FiatValidator(); final StringProperty amount = new SimpleStringProperty(); final StringProperty minAmount = new SimpleStringProperty(); @@ -69,19 +74,23 @@ class CreateOfferPM { final BooleanProperty isCloseButtonVisible = new SimpleBooleanProperty(); final BooleanProperty isPlaceOfferButtonVisible = new SimpleBooleanProperty(true); final BooleanProperty isPlaceOfferButtonDisabled = new SimpleBooleanProperty(); - final BooleanProperty needsInputValidation = new SimpleBooleanProperty(); - final BooleanProperty showWarningInvalidBtcFractions = new SimpleBooleanProperty(); + 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<>(); + final ObjectProperty priceValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty volumeValidationResult = new SimpleObjectProperty<>(); + final ObjectProperty totalToPayAsCoin = new SimpleObjectProperty<>(); final ObjectProperty
address = new SimpleObjectProperty<>(); /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor + // Constructor (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// CreateOfferPM(CreateOfferModel model) { @@ -90,7 +99,7 @@ class CreateOfferPM { /////////////////////////////////////////////////////////////////////////////////////////// - // Lifecycle + // Lifecycle (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// void onViewInitialized() { @@ -134,46 +143,54 @@ class CreateOfferPM { } void activate() { + //TODO handle in base class model.activate(); } + void deactivate() { + //TODO handle in base class model.deactivate(); } /////////////////////////////////////////////////////////////////////////////////////////// - // Public methods + // Public API methods (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// void setOrderBookFilter(OrderBookFilter orderBookFilter) { model.setDirection(orderBookFilter.getDirection()); - model.amountAsCoin = orderBookFilter.getAmount(); - model.minAmountAsCoin = orderBookFilter.getAmount(); + directionLabel.set(model.getDirection() == Direction.BUY ? "Buy:" : "Sell:"); + + if (orderBookFilter.getAmount() != null) { + model.amountAsCoin = orderBookFilter.getAmount(); + amount.set(formatCoin(model.amountAsCoin)); + + model.minAmountAsCoin = orderBookFilter.getAmount(); + minAmount.set(formatCoin(model.minAmountAsCoin)); + } // TODO use Fiat in orderBookFilter - model.priceAsFiat = parseToFiatWith2Decimals(String.valueOf(orderBookFilter.getPrice())); + if (orderBookFilter.getPrice() != 0) { + model.priceAsFiat = parseToFiatWith2Decimals(String.valueOf(orderBookFilter.getPrice())); + price.set(formatFiat(model.priceAsFiat)); + } - directionLabel.set(model.getDirection() == Direction.BUY ? "Buy:" : "Sell:"); - amount.set(formatCoin(model.amountAsCoin)); - minAmount.set(formatCoin(model.minAmountAsCoin)); - price.set(formatFiat(model.priceAsFiat)); } /////////////////////////////////////////////////////////////////////////////////////////// - // View Events + // UI actions (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// void placeOffer() { - model.amountAsCoin = parseToCoinWith4Decimals(amount.get()); - model.minAmountAsCoin = parseToCoinWith4Decimals(minAmount.get()); - model.priceAsFiat = parseToFiatWith2Decimals(price.get()); - model.minAmountAsCoin = parseToCoinWith4Decimals(minAmount.get()); + if (allInputsValid()) { - needsInputValidation.set(true); + model.amountAsCoin = parseToCoinWith4Decimals(amount.get()); + model.minAmountAsCoin = parseToCoinWith4Decimals(minAmount.get()); + model.priceAsFiat = parseToFiatWith2Decimals(price.get()); + model.minAmountAsCoin = parseToCoinWith4Decimals(minAmount.get()); - if (inputValid()) { model.placeOffer(); isPlaceOfferButtonDisabled.set(true); isPlaceOfferButtonVisible.set(true); @@ -183,79 +200,100 @@ class CreateOfferPM { void close() { } + /////////////////////////////////////////////////////////////////////////////////////////// - // + // UI events (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// - void setupInputListeners() { - - // bindBidirectional for amount, price, volume and minAmount - amount.addListener(ov -> { - model.amountAsCoin = parseToCoinWith4Decimals(amount.get()); - calculateVolume(); - calculateTotalToPay(); - calculateCollateral(); - - }); - - price.addListener(ov -> { - model.priceAsFiat = parseToFiatWith2Decimals(price.get()); - calculateVolume(); - calculateTotalToPay(); - calculateCollateral(); - }); - - volume.addListener(ov -> { - model.volumeAsFiat = parseToFiatWith2Decimals(volume.get()); - calculateAmount(); - calculateTotalToPay(); - calculateCollateral(); - }); - } + // when focus out we do validation and apply the data to the model void onFocusOutAmountTextField(Boolean oldValue, Boolean newValue) { - if (oldValue && !newValue) { - showWarningInvalidBtcDecimalPlaces.set(!hasBtcValidDecimals(amount.get())); - model.amountAsCoin = parseToCoinWith4Decimals(amount.get()); - amount.set(formatCoin(model.amountAsCoin)); - calculateVolume(); + InputValidator.ValidationResult result = isBtcInputValid(amount.get()); + boolean isValid = result.isValid; + amountValidationResult.set(result); + if (isValid) { + showWarningInvalidBtcDecimalPlaces.set(!hasBtcValidDecimals(amount.get())); + // only allow max 4 decimal places for btc values + model.amountAsCoin = parseToCoinWith4Decimals(amount.get()); + // reformat input to general btc format + amount.set(formatCoin(model.amountAsCoin)); + calculateVolume(); + + if (!model.isMinAmountLessOrEqualAmount()) { + amountValidationResult.set(new InputValidator.ValidationResult(false, + "Amount cannot be smaller than minimum amount.")); + } + else { + amountValidationResult.set(result); + if (minAmount.get() != null) + minAmountValidationResult.set(isBtcInputValid(minAmount.get())); + } + } } } void onFocusOutMinAmountTextField(Boolean oldValue, Boolean newValue) { if (oldValue && !newValue) { - showWarningInvalidBtcDecimalPlaces.set(!hasBtcValidDecimals(minAmount.get())); - model.minAmountAsCoin = parseToCoinWith4Decimals(minAmount.get()); - minAmount.set(formatCoin(model.minAmountAsCoin)); - } - } + InputValidator.ValidationResult result = isBtcInputValid(minAmount.get()); + boolean isValid = result.isValid; + minAmountValidationResult.set(result); + if (isValid) { + showWarningInvalidBtcDecimalPlaces.set(!hasBtcValidDecimals(minAmount.get())); + model.minAmountAsCoin = parseToCoinWith4Decimals(minAmount.get()); + minAmount.set(formatCoin(model.minAmountAsCoin)); - void onFocusOutVolumeTextField(Boolean oldValue, Boolean newValue, String volumeTextFieldText) { - if (oldValue && !newValue) { - showWarningInvalidFiatDecimalPlaces.set(!hasFiatValidDecimals(volume.get())); - model.volumeAsFiat = parseToFiatWith2Decimals(volume.get()); - volume.set(formatFiat(model.volumeAsFiat)); - calculateAmount(); - - showWarningInvalidBtcFractions.set(!formatFiat(parseToFiatWith2Decimals(volumeTextFieldText)).equals - (volume.get())); + if (!model.isMinAmountLessOrEqualAmount()) { + minAmountValidationResult.set(new InputValidator.ValidationResult(false, + "Minimum amount cannot be larger than amount.")); + } + else { + minAmountValidationResult.set(result); + if (amount.get() != null) + amountValidationResult.set(isBtcInputValid(amount.get())); + } + } } } void onFocusOutPriceTextField(Boolean oldValue, Boolean newValue) { if (oldValue && !newValue) { - showWarningInvalidFiatDecimalPlaces.set(!hasFiatValidDecimals(price.get())); - model.priceAsFiat = parseToFiatWith2Decimals(price.get()); - price.set(formatFiat(model.priceAsFiat)); - calculateVolume(); + InputValidator.ValidationResult result = isFiatInputValid(price.get()); + boolean isValid = result.isValid; + priceValidationResult.set(result); + if (isValid) { + showWarningInvalidFiatDecimalPlaces.set(!hasFiatValidDecimals(price.get())); + model.priceAsFiat = parseToFiatWith2Decimals(price.get()); + price.set(formatFiat(model.priceAsFiat)); + calculateVolume(); + } + } + } + + void onFocusOutVolumeTextField(Boolean oldValue, Boolean newValue) { + if (oldValue && !newValue) { + InputValidator.ValidationResult result = isBtcInputValid(volume.get()); + boolean isValid = result.isValid; + volumeValidationResult.set(result); + if (isValid) { + String origVolume = volume.get(); + showWarningInvalidFiatDecimalPlaces.set(!hasFiatValidDecimals(volume.get())); + model.volumeAsFiat = parseToFiatWith2Decimals(volume.get()); + + volume.set(formatFiat(model.volumeAsFiat)); + calculateAmount(); + + // must be after calculateAmount (btc value has been adjusted in case the calculation leads to + // invalid decimal places for the amount value + showWarningAdjustedVolume.set(!formatFiat(parseToFiatWith2Decimals(origVolume)).equals(volume.get())); + } } } /////////////////////////////////////////////////////////////////////////////////////////// - // Getters + // Getters (called by CB) /////////////////////////////////////////////////////////////////////////////////////////// WalletFacade getWalletFacade() { @@ -267,11 +305,45 @@ class CreateOfferPM { // Private /////////////////////////////////////////////////////////////////////////////////////////// - private boolean inputValid() { - //TODO - return true; + private void setupInputListeners() { + + // bindBidirectional for amount, price, volume and minAmount + // We do volume/amount calculation during input + amount.addListener((ov, oldValue, newValue) -> { + if (isBtcInputValid(newValue).isValid) { + model.amountAsCoin = parseToCoinWith4Decimals(newValue); + calculateVolume(); + calculateTotalToPay(); + calculateCollateral(); + } + }); + + price.addListener((ov, oldValue, newValue) -> { + if (isFiatInputValid(newValue).isValid) { + model.priceAsFiat = parseToFiatWith2Decimals(newValue); + calculateVolume(); + calculateTotalToPay(); + calculateCollateral(); + } + }); + + volume.addListener((ov, oldValue, newValue) -> { + if (isFiatInputValid(newValue).isValid) { + model.volumeAsFiat = parseToFiatWith2Decimals(newValue); + calculateAmount(); + calculateTotalToPay(); + calculateCollateral(); + } + }); } + + private boolean allInputsValid() { + return isBtcInputValid(amount.get()).isValid && isFiatInputValid(price.get()).isValid && isFiatInputValid + (volume.get()).isValid; + } + + //TODO move to model private void calculateVolume() { model.amountAsCoin = parseToCoinWith4Decimals(amount.get()); model.priceAsFiat = parseToFiatWith2Decimals(price.get()); @@ -282,6 +354,7 @@ class CreateOfferPM { } } + //TODO move to model private void calculateAmount() { model.volumeAsFiat = parseToFiatWith2Decimals(volume.get()); model.priceAsFiat = parseToFiatWith2Decimals(price.get()); @@ -295,8 +368,20 @@ class CreateOfferPM { calculateTotalToPay(); calculateCollateral(); } + + if (!model.isMinAmountLessOrEqualAmount()) { + amountValidationResult.set(new InputValidator.ValidationResult(false, + "Amount cannot be smaller than minimum amount.")); + } + else { + if (amount.get() != null) + amountValidationResult.set(isBtcInputValid(amount.get())); + if (minAmount.get() != null) + minAmountValidationResult.set(isBtcInputValid(minAmount.get())); + } } + //TODO move to model private void calculateTotalToPay() { calculateCollateral(); @@ -307,10 +392,27 @@ class CreateOfferPM { } } + //TODO move to model private void calculateCollateral() { if (model.amountAsCoin != null) { model.collateralAsCoin = model.amountAsCoin.multiply(model.collateralAsLong.get()).divide(1000); collateral.set(BSFormatter.formatCoinWithCode(model.collateralAsCoin)); } } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Package scope for testing + /////////////////////////////////////////////////////////////////////////////////////////// + + InputValidator.ValidationResult isBtcInputValid(String input) { + + return btcValidator.validate(input); + } + + InputValidator.ValidationResult isFiatInputValid(String input) { + + return fiatValidator.validate(input); + } + + } diff --git a/src/main/java/io/bitsquare/gui/util/validation/BtcValidator.java b/src/main/java/io/bitsquare/gui/util/validation/BtcValidator.java index 5d893dadec..1c9190d81d 100644 --- a/src/main/java/io/bitsquare/gui/util/validation/BtcValidator.java +++ b/src/main/java/io/bitsquare/gui/util/validation/BtcValidator.java @@ -80,8 +80,7 @@ public class BtcValidator extends NumberValidator { if (satoshis.scale() > 0) return new ValidationResult( false, - "Input results in a Bitcoin value with a fraction of the smallest unit (Satoshi).", - ErrorType.FRACTIONAL_SATOSHI); + "Input results in a Bitcoin value with a fraction of the smallest unit (Satoshi)."); else return new ValidationResult(true); } @@ -92,8 +91,7 @@ public class BtcValidator extends NumberValidator { if (satoshis.longValue() > NetworkParameters.MAX_MONEY.longValue()) return new ValidationResult( false, - "Input larger as maximum possible Bitcoin value is not allowed.", - ErrorType.EXCEEDS_MAX_BTC_VALUE); + "Input larger as maximum possible Bitcoin value is not allowed."); else return new ValidationResult(true); } diff --git a/src/main/java/io/bitsquare/gui/util/validation/FiatValidator.java b/src/main/java/io/bitsquare/gui/util/validation/FiatValidator.java index 2b24af5436..8a6dadc469 100644 --- a/src/main/java/io/bitsquare/gui/util/validation/FiatValidator.java +++ b/src/main/java/io/bitsquare/gui/util/validation/FiatValidator.java @@ -65,8 +65,7 @@ public class FiatValidator extends NumberValidator { if (d < MIN_FIAT_VALUE) return new ValidationResult( false, - "Input smaller as minimum possible Fiat value is not allowed..", - ErrorType.UNDERCUT_MIN_FIAT_VALUE); + "Input smaller as minimum possible Fiat value is not allowed.."); else return new ValidationResult(true); } @@ -76,8 +75,7 @@ public class FiatValidator extends NumberValidator { if (d > MAX_FIAT_VALUE) return new ValidationResult( false, - "Input larger as maximum possible Fiat value is not allowed.", - ErrorType.EXCEEDS_MAX_FIAT_VALUE); + "Input larger as maximum possible Fiat value is not allowed."); else return new ValidationResult(true); } diff --git a/src/main/java/io/bitsquare/gui/util/validation/InputValidator.java b/src/main/java/io/bitsquare/gui/util/validation/InputValidator.java index 60f9ddcf86..5e397e9d64 100644 --- a/src/main/java/io/bitsquare/gui/util/validation/InputValidator.java +++ b/src/main/java/io/bitsquare/gui/util/validation/InputValidator.java @@ -44,7 +44,7 @@ public abstract class InputValidator { protected ValidationResult validateIfNotEmpty(String input) { if (input == null || input.length() == 0) - return new ValidationResult(false, "Empty input is not allowed.", ErrorType.EMPTY_INPUT); + return new ValidationResult(false, "Empty input is not allowed."); else return new ValidationResult(true); } @@ -54,21 +54,6 @@ public abstract class InputValidator { } - /////////////////////////////////////////////////////////////////////////////////////////// - // ErrorType - /////////////////////////////////////////////////////////////////////////////////////////// - - public enum ErrorType { - EMPTY_INPUT, - NOT_A_NUMBER, - ZERO_NUMBER, - NEGATIVE_NUMBER, - FRACTIONAL_SATOSHI, - EXCEEDS_MAX_FIAT_VALUE, UNDERCUT_MIN_FIAT_VALUE, AMOUNT_LESS_THAN_MIN_AMOUNT, - MIN_AMOUNT_LARGER_THAN_MIN_AMOUNT, EXCEEDS_MAX_BTC_VALUE - } - - /////////////////////////////////////////////////////////////////////////////////////////// // ValidationResult /////////////////////////////////////////////////////////////////////////////////////////// @@ -76,16 +61,14 @@ public abstract class InputValidator { public static class ValidationResult { public final boolean isValid; public final String errorMessage; - public final ErrorType errorType; - public ValidationResult(boolean isValid, String errorMessage, ErrorType errorType) { + public ValidationResult(boolean isValid, String errorMessage) { this.isValid = isValid; this.errorMessage = errorMessage; - this.errorType = errorType; } - ValidationResult(boolean isValid) { - this(isValid, null, null); + public ValidationResult(boolean isValid) { + this(isValid, null); } public ValidationResult and(ValidationResult next) { @@ -100,7 +83,6 @@ public abstract class InputValidator { return "ValidationResult{" + "isValid=" + isValid + ", errorMessage='" + errorMessage + '\'' + - ", errorType=" + errorType + '}'; } } diff --git a/src/main/java/io/bitsquare/gui/util/validation/NumberValidator.java b/src/main/java/io/bitsquare/gui/util/validation/NumberValidator.java index 8b8b55e7ab..f94838546e 100644 --- a/src/main/java/io/bitsquare/gui/util/validation/NumberValidator.java +++ b/src/main/java/io/bitsquare/gui/util/validation/NumberValidator.java @@ -45,20 +45,20 @@ public abstract class NumberValidator extends InputValidator { Double.parseDouble(input); return new ValidationResult(true); } catch (Exception e) { - return new ValidationResult(false, "Input is not a valid number.", ErrorType.NOT_A_NUMBER); + return new ValidationResult(false, "Input is not a valid number."); } } protected ValidationResult validateIfNotZero(String input) { if (Double.parseDouble(input) == 0) - return new ValidationResult(false, "Input of 0 is not allowed.", ErrorType.ZERO_NUMBER); + return new ValidationResult(false, "Input of 0 is not allowed."); else return new ValidationResult(true); } protected ValidationResult validateIfNotNegative(String input) { if (Double.parseDouble(input) < 0) - return new ValidationResult(false, "A negative value is not allowed.", ErrorType.NEGATIVE_NUMBER); + return new ValidationResult(false, "A negative value is not allowed."); else return new ValidationResult(true); } diff --git a/src/main/java/io/bitsquare/gui/util/validation/ValidationHelper.java b/src/main/java/io/bitsquare/gui/util/validation/ValidationHelper.java deleted file mode 100644 index e5c3084823..0000000000 --- a/src/main/java/io/bitsquare/gui/util/validation/ValidationHelper.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 . - */ - -package io.bitsquare.gui.util.validation; - -import io.bitsquare.gui.components.ValidatingTextField; - -import javafx.beans.property.StringProperty; -import javafx.scene.control.*; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Helper class for setting up the validation and dependencies for minAmount and Amount. - * TODO Might be improved but does the job for now... - */ -public class ValidationHelper { - private static final Logger log = LoggerFactory.getLogger(ValidationHelper.class); - - /** - * Handles validation between minAmount and amount fields - * Min amount must not be larger as amount. - * Handles focus out events to display always the error popup from the field where the focus out happened. - */ - public static void setupMinAmountInRangeOfAmountValidation(ValidatingTextField amountTextField, - ValidatingTextField minAmountTextField, - StringProperty amount, - StringProperty minAmount, - BtcValidator amountValidator, - BtcValidator minAmountValidator) { - - - amountTextField.focusedProperty().addListener((ov, oldValue, newValue) -> { - // only on focus out and ignore focus loss from window - if (!newValue && amountTextField.getScene() != null && amountTextField.getScene().getWindow().isFocused()) - validateMinAmount(amountTextField, - minAmountTextField, - amount, - minAmount, - amountValidator, - minAmountValidator, - amountTextField); - }); - - minAmountTextField.focusedProperty().addListener((ov, oldValue, newValue) -> { - // only on focus out and ignore focus loss from window - if (!newValue && minAmountTextField.getScene() != null && - minAmountTextField.getScene().getWindow().isFocused()) - validateMinAmount(amountTextField, - minAmountTextField, - amount, - minAmount, - amountValidator, - minAmountValidator, - minAmountTextField); - }); - } - - private static void validateMinAmount(ValidatingTextField amountTextField, - ValidatingTextField minAmountTextField, - StringProperty amount, - StringProperty minAmount, - BtcValidator amountValidator, - BtcValidator minAmountValidator, - TextField currentTextField) { - amountValidator.overrideResult(null); - String amountCleaned = amount.get() != null ? amount.get().replace(",", ".").trim() : "0"; - String minAmountCleaned = minAmount.get() != null ? minAmount.get().replace(",", ".").trim() : "0"; - - if (!amountValidator.validate(amountCleaned).isValid) - return; - - minAmountValidator.overrideResult(null); - if (!minAmountValidator.validate(minAmountCleaned).isValid) - return; - - if (currentTextField == amountTextField) { - if (Double.parseDouble(amountCleaned) < Double.parseDouble(minAmountCleaned)) { - amountValidator.overrideResult(new NumberValidator.ValidationResult(false, - "Amount cannot be smaller than minimum amount.", - NumberValidator.ErrorType.AMOUNT_LESS_THAN_MIN_AMOUNT)); - amountTextField.reValidate(); - } - else { - amountValidator.overrideResult(null); - minAmountTextField.reValidate(); - } - } - else if (currentTextField == minAmountTextField) { - if (Double.parseDouble(minAmountCleaned) > Double.parseDouble(amountCleaned)) { - minAmountValidator.overrideResult(new NumberValidator.ValidationResult(false, - "Minimum amount cannot be larger than amount.", - NumberValidator.ErrorType.MIN_AMOUNT_LARGER_THAN_MIN_AMOUNT)); - minAmountTextField.reValidate(); - } - else { - minAmountValidator.overrideResult(null); - amountTextField.reValidate(); - } - } - } -} diff --git a/src/main/java/lighthouse/files/AppDirectory.java b/src/main/java/lighthouse/files/AppDirectory.java index 5d293aa2a3..16365ba973 100644 --- a/src/main/java/lighthouse/files/AppDirectory.java +++ b/src/main/java/lighthouse/files/AppDirectory.java @@ -10,15 +10,19 @@ import static com.google.common.base.Preconditions.checkNotNull; // TODO update to open source file when its released -/** Manages the directory where the app stores all its files. */ +/** + * Manages the directory where the app stores all its files. + */ public class AppDirectory { public static Path getUserDataDir() { String os = System.getProperty("os.name").toLowerCase(); if (os.contains("win")) { return Paths.get(System.getenv("APPDATA")); - } else if (os.contains("mac")) { + } + else if (os.contains("mac")) { return Paths.get(System.getProperty("user.home"), "Library", "Application Support"); - } else { + } + else { // Linux and other similar systems, we hope (not Android). return Paths.get(System.getProperty("user.home"), ".local", "share"); } @@ -30,7 +34,7 @@ public class AppDirectory { public static Path initAppDir(String appName) throws IOException { AppDirectory.appName = appName; - + Path dir = dir(); if (!Files.exists(dir)) Files.createDirectory(dir); @@ -39,7 +43,7 @@ public class AppDirectory { return dir; } - private static String appName; + private static String appName = ""; private static Path dir; diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 7cf77c0f54..92b1a2b249 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -23,7 +23,7 @@ --> - + diff --git a/src/test/java/io/bitsquare/gui/trade/createoffer/CreateOfferPMTest.java b/src/test/java/io/bitsquare/gui/trade/createoffer/CreateOfferPMTest.java index 500bc9d438..d2c5f904a8 100644 --- a/src/test/java/io/bitsquare/gui/trade/createoffer/CreateOfferPMTest.java +++ b/src/test/java/io/bitsquare/gui/trade/createoffer/CreateOfferPMTest.java @@ -22,10 +22,12 @@ import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.locale.Country; import com.google.bitcoin.core.Coin; +import com.google.bitcoin.core.NetworkParameters; import com.google.bitcoin.utils.Fiat; import java.util.Locale; +import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; @@ -36,15 +38,71 @@ import static org.junit.Assert.*; public class CreateOfferPMTest { private static final Logger log = LoggerFactory.getLogger(CreateOfferPMTest.class); - @Test - public void testBindings() { - CreateOfferModel model = new CreateOfferModel(null, null, null, null); + private CreateOfferModel model; + private CreateOfferPM presenter; + + @Before + public void setup() { + model = new CreateOfferModel(null, null, null, null); BSFormatter.setLocale(Locale.US); BSFormatter.setFiatCurrencyCode("USD"); - CreateOfferPM presenter = new CreateOfferPM(model); + presenter = new CreateOfferPM(model); presenter.onViewInitialized(); + } + + @Test + public void testIsBtcInputValid() { + assertTrue(presenter.isBtcInputValid("1").isValid); + assertTrue(presenter.isBtcInputValid("1,1").isValid); + assertTrue(presenter.isBtcInputValid("1.1").isValid); + assertTrue(presenter.isBtcInputValid(",1").isValid); + assertTrue(presenter.isBtcInputValid(".1").isValid); + assertTrue(presenter.isBtcInputValid("0.12345678").isValid); + assertTrue(presenter.isBtcInputValid(Coin.SATOSHI.toPlainString()).isValid); + assertTrue(presenter.isBtcInputValid(NetworkParameters.MAX_MONEY.toPlainString()).isValid); + + assertFalse(presenter.isBtcInputValid(null).isValid); + assertFalse(presenter.isBtcInputValid("").isValid); + assertFalse(presenter.isBtcInputValid("0").isValid); + assertFalse(presenter.isBtcInputValid("0.0").isValid); + assertFalse(presenter.isBtcInputValid("0,1,1").isValid); + assertFalse(presenter.isBtcInputValid("0.1.1").isValid); + assertFalse(presenter.isBtcInputValid("1,000.1").isValid); + assertFalse(presenter.isBtcInputValid("1.000,1").isValid); + assertFalse(presenter.isBtcInputValid("0.123456789").isValid); + assertFalse(presenter.isBtcInputValid("-1").isValid); + assertFalse(presenter.isBtcInputValid(String.valueOf(NetworkParameters.MAX_MONEY.longValue() + Coin.SATOSHI + .longValue())).isValid); + } + + @Test + public void testIsFiatInputValid() { + assertTrue(presenter.isFiatInputValid("1").isValid); + assertTrue(presenter.isFiatInputValid("1,1").isValid); + assertTrue(presenter.isFiatInputValid("1.1").isValid); + assertTrue(presenter.isFiatInputValid(",1").isValid); + assertTrue(presenter.isFiatInputValid(".1").isValid); + assertTrue(presenter.isFiatInputValid("0.01").isValid); + assertTrue(presenter.isFiatInputValid("1000000.00").isValid); + + assertFalse(presenter.isFiatInputValid(null).isValid); + assertFalse(presenter.isFiatInputValid("").isValid); + assertFalse(presenter.isFiatInputValid("0").isValid); + assertFalse(presenter.isFiatInputValid("-1").isValid); + assertFalse(presenter.isFiatInputValid("0.0").isValid); + assertFalse(presenter.isFiatInputValid("0,1,1").isValid); + assertFalse(presenter.isFiatInputValid("0.1.1").isValid); + assertFalse(presenter.isFiatInputValid("1,000.1").isValid); + assertFalse(presenter.isFiatInputValid("1.000,1").isValid); + assertFalse(presenter.isFiatInputValid("0.009").isValid); + assertFalse(presenter.isFiatInputValid("1000000.01").isValid); + } + + @Test + public void testBindings() { + model.collateralAsLong.set(100); presenter.price.set("500");