changed validation

This commit is contained in:
Manfred Karrer 2014-08-29 16:14:25 +02:00
parent ed5cc4d628
commit b91fe8273b
14 changed files with 320 additions and 365 deletions

View File

@ -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);

View File

@ -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<InputValidator.ValidationResult> 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<InputValidator.ValidationResult> 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);
}
}

View File

@ -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));
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> minAmountValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> priceValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Address> 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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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 +
'}';
}
}

View File

@ -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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}
}

View File

@ -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;

View File

@ -23,7 +23,7 @@
-->
</root>
<logger name="io.bitsquare" level="WARN"/>
<logger name="io.bitsquare" level="TRACE"/>
<logger name="com.google.bitcoin" level="WARN"/>
<logger name="net.tomp2p" level="WARN"/>

View File

@ -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");