fix missing formatting updates

This commit is contained in:
Manfred Karrer 2014-08-29 22:11:59 +02:00
parent 3c67b08ff9
commit 096f4c01e7
12 changed files with 298 additions and 266 deletions

View file

@ -44,10 +44,9 @@ It is the JavaFX controller associated with the FXML view, but we don't use the
association with the classical MVC controller. It gets created by the JavaFX framework (FXMLLoader) and also the
setup with the FXML view is done by the framework.
It knows the presentation model but not the model.
It knows the presentation model (via Guice injection) but not the model.
#####Responsibility:
* Creates presentation model and passes model from Guice injection to the presenter (might be changed).
* Setup binding for updates from PM to view elements (also bidirectional for used for input data).
* Those binding are only simple bindings to plain presenter properties, no logical bindings.
* Listens to UI events (Actions) from UI controls and calls method in presentation model.
@ -62,7 +61,7 @@ It knows the presentation model but not the model.
It is the abstraction/presentation of the view.
Can be used for unit testing.
It knows the model but it does not know the CodeBehind (View)
It knows the model (via Guice injection) but it does not know the CodeBehind (View)
#####Responsibility:
* Holds the state of the view/CB

View file

@ -7,11 +7,16 @@ import java.util.ResourceBundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* If caching is used for loader we use the CachedViewController for turning the controller into sleep mode if not
* active and awake it at reactivation.
* * @param <T> The PresentationModel used in that class
*/
public class CachedCodeBehind<T extends PresentationModel> extends CodeBehind<T> {
private static final Logger log = LoggerFactory.getLogger(CachedCodeBehind.class);
public CachedCodeBehind(T pm) {
super(pm);
public CachedCodeBehind(T presentationModel) {
super(presentationModel);
}
/**
@ -35,7 +40,7 @@ public class CachedCodeBehind<T extends PresentationModel> extends CodeBehind<T>
});
activate();
pm.initialized();
presentationModel.initialized();
}
/**
@ -45,7 +50,7 @@ public class CachedCodeBehind<T extends PresentationModel> extends CodeBehind<T>
log.trace("Lifecycle: activate " + this.getClass().getSimpleName());
if (childController instanceof CachedViewController) ((CachedViewController) childController).activate();
pm.activate();
presentationModel.activate();
}
/**
@ -55,7 +60,7 @@ public class CachedCodeBehind<T extends PresentationModel> extends CodeBehind<T>
log.trace("Lifecycle: deactivate " + this.getClass().getSimpleName());
if (childController instanceof CachedViewController) ((CachedViewController) childController).deactivate();
pm.deactivate();
presentationModel.deactivate();
}
/**
@ -67,7 +72,7 @@ public class CachedCodeBehind<T extends PresentationModel> extends CodeBehind<T>
super.terminate();
deactivate();
pm.terminate();
presentationModel.terminate();
}
}

View file

@ -28,6 +28,8 @@ import org.slf4j.LoggerFactory;
* If caching is used for loader we use the CachedViewController for turning the controller into sleep mode if not
* active and awake it at reactivation.
*/
// for new PM pattern use CachedCodeBehind
@Deprecated
public abstract class CachedViewController extends ViewController {
private static final Logger log = LoggerFactory.getLogger(CachedViewController.class);

View file

@ -11,25 +11,26 @@ import javafx.scene.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Non caching version for code behind classes using the PM pattern
*
* @param <T> The PresentationModel used in that class
*/
public class CodeBehind<T extends PresentationModel> implements Initializable {
private static final Logger log = LoggerFactory.getLogger(CodeBehind.class);
protected T pm;
protected T presentationModel;
protected ViewController childController;
protected ViewController parentController;
@FXML protected Parent root;
public CodeBehind(T pm) {
this.pm = pm;
public CodeBehind(T presentationModel) {
this.presentationModel = presentationModel;
}
public CodeBehind() {
}
public T pm() {
return (T) pm;
}
/**
* Get called form GUI framework when the UI is ready.
*
@ -45,7 +46,7 @@ public class CodeBehind<T extends PresentationModel> implements Initializable {
if (oldValue != null && newValue == null) terminate();
});
pm.initialized();
presentationModel.initialized();
}
/**
@ -56,7 +57,7 @@ public class CodeBehind<T extends PresentationModel> implements Initializable {
log.trace("Lifecycle: terminate " + this.getClass().getSimpleName());
if (childController != null) childController.terminate();
pm.terminate();
presentationModel.terminate();
}
/**

View file

@ -2,11 +2,7 @@ package io.bitsquare.gui;
public class PresentationModel<T extends UIModel> {
private T model;
public T model() {
return (T) model;
}
protected T model;
public PresentationModel(T model) {
this.model = model;

View file

@ -31,6 +31,8 @@ import org.slf4j.LoggerFactory;
/**
* Base class for all controllers.
*/
// for new PM pattern use CodeBehind
@Deprecated
public abstract class ViewController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(ViewController.class);

View file

@ -35,11 +35,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* TextField with validation support. Validation is executed on the Validator object.
* In case of a invalid result we display a error message with a PopOver.
* The position is derived from the textField or if set from the errorPopupLayoutReference object.
* <p>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
* TextField with validation support.
* In case the isValid property in amountValidationResultProperty get set to false we display a red border and an error
* message within the errorMessageDisplay placed on the right of the text field.
* The errorMessageDisplay gets closed when the ValidatingTextField instance gets removed from the scene graph or when
* hideErrorMessageDisplay() is called.
* 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.
*/
public class ValidatingTextField extends TextField {
private static final Logger log = LoggerFactory.getLogger(ValidatingTextField.class);
@ -49,17 +51,17 @@ public class ValidatingTextField extends TextField {
final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>(new
InputValidator.ValidationResult(true));
private static PopOver popOver;
private Region errorPopupLayoutReference = this;
private static PopOver errorMessageDisplay;
private Region layoutReference = this;
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
public static void hidePopover() {
if (popOver != null)
popOver.hide();
public static void hideErrorMessageDisplay() {
if (errorMessageDisplay != null)
errorMessageDisplay.hide();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -73,17 +75,16 @@ public class ValidatingTextField extends TextField {
setEffect(newValue.isValid ? null : invalidEffect);
if (newValue.isValid)
hidePopover();
hideErrorMessageDisplay();
else
applyErrorMessage(newValue);
}
});
sceneProperty().addListener((ov, oldValue, newValue) -> {
// we got removed from the scene
// lets hide an open popup
// we got removed from the scene so hide the popup (if open)
if (newValue == null)
hidePopover();
hideErrorMessageDisplay();
});
}
@ -99,11 +100,11 @@ public class ValidatingTextField extends TextField {
///////////////////////////////////////////////////////////////////////////////////////////
/**
* @param errorPopupLayoutReference The node used as reference for positioning. If not set explicitely the
* ValidatingTextField instance is used.
* @param layoutReference The node used as reference for positioning. If not set explicitely the
* ValidatingTextField instance is used.
*/
public void setErrorPopupLayoutReference(Region errorPopupLayoutReference) {
this.errorPopupLayoutReference = errorPopupLayoutReference;
public void setLayoutReference(Region layoutReference) {
this.layoutReference = layoutReference;
}
@ -122,25 +123,26 @@ public class ValidatingTextField extends TextField {
private void applyErrorMessage(InputValidator.ValidationResult validationResult) {
if (validationResult.isValid) {
if (popOver != null) {
popOver.hide();
if (errorMessageDisplay != null) {
errorMessageDisplay.hide();
}
}
else {
if (popOver == null)
if (errorMessageDisplay == null)
createErrorPopOver(validationResult.errorMessage);
else
((Label) popOver.getContentNode()).setText(validationResult.errorMessage);
((Label) errorMessageDisplay.getContentNode()).setText(validationResult.errorMessage);
popOver.show(getScene().getWindow(), getErrorPopupPosition().getX(), getErrorPopupPosition().getY());
errorMessageDisplay.show(getScene().getWindow(), getErrorPopupPosition().getX(),
getErrorPopupPosition().getY());
}
}
private Point2D getErrorPopupPosition() {
Window window = getScene().getWindow();
Point2D point;
point = errorPopupLayoutReference.localToScene(0, 0);
double x = point.getX() + window.getX() + errorPopupLayoutReference.getWidth() + 20;
point = layoutReference.localToScene(0, 0);
double x = point.getX() + window.getX() + layoutReference.getWidth() + 20;
double y = point.getY() + window.getY() + Math.floor(getHeight() / 2);
return new Point2D(x, y);
}
@ -151,9 +153,9 @@ public class ValidatingTextField extends TextField {
errorLabel.setId("validation-error");
errorLabel.setPadding(new Insets(0, 10, 0, 10));
popOver = new PopOver(errorLabel);
popOver.setDetachable(false);
popOver.setArrowIndent(5);
ValidatingTextField.errorMessageDisplay = new PopOver(errorLabel);
ValidatingTextField.errorMessageDisplay.setDetachable(false);
ValidatingTextField.errorMessageDisplay.setArrowIndent(5);
}
}

View file

@ -78,7 +78,7 @@ public class TradeController extends CachedViewController {
//TODO update to new verison
((TabPane) root).getSelectionModel().selectedIndexProperty().addListener((observableValue) ->
Platform.runLater(ValidatingTextField::hidePopover));
Platform.runLater(ValidatingTextField::hideErrorMessageDisplay));
}

View file

@ -58,10 +58,9 @@ public class CreateOfferCB extends CachedCodeBehind<CreateOfferPM> {
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
//TODO find a better solution, handle at base class?
@Inject
public CreateOfferCB(CreateOfferModel model) {
super(new CreateOfferPM(model));
CreateOfferCB(CreateOfferPM presentationModel) {
super(presentationModel);
}
@ -76,7 +75,12 @@ public class CreateOfferCB extends CachedCodeBehind<CreateOfferPM> {
setupBindings();
setupListeners();
configTextFieldValidators();
balanceTextField.setup(pm().getWalletFacade(), pm().address.get());
balanceTextField.setup(presentationModel.getWalletFacade(), presentationModel.address.get());
}
@Override
public void activate() {
super.activate();
}
@Override
@ -87,13 +91,18 @@ public class CreateOfferCB extends CachedCodeBehind<CreateOfferPM> {
if (parentController != null) ((TradeController) parentController).onCreateOfferViewRemoved();
}
@Override
public void terminate() {
super.terminate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
// Public methods (called form other views/CB)
///////////////////////////////////////////////////////////////////////////////////////////
public void setOrderBookFilter(OrderBookFilter orderBookFilter) {
pm().setOrderBookFilter(orderBookFilter);
presentationModel.setOrderBookFilter(orderBookFilter);
}
@ -103,13 +112,11 @@ public class CreateOfferCB extends CachedCodeBehind<CreateOfferPM> {
@FXML
public void onPlaceOffer() {
pm().placeOffer();
presentationModel.placeOffer();
}
@FXML
public void onClose() {
pm().close();
TabPane tabPane = ((TabPane) (root.getParent().getParent()));
tabPane.getTabs().remove(tabPane.getSelectionModel().getSelectedItem());
}
@ -120,99 +127,104 @@ public class CreateOfferCB extends CachedCodeBehind<CreateOfferPM> {
///////////////////////////////////////////////////////////////////////////////////////////
private void setupListeners() {
volumeTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
pm().onFocusOutVolumeTextField(oldValue, newValue);
volumeTextField.setText(pm().volume.get());
});
// focus out
amountTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
pm().onFocusOutAmountTextField(oldValue, newValue);
amountTextField.setText(pm().amount.get());
});
priceTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
pm().onFocusOutPriceTextField(oldValue, newValue);
priceTextField.setText(pm().price.get());
presentationModel.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText());
amountTextField.setText(presentationModel.amount.get());
});
minAmountTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
pm().onFocusOutMinAmountTextField(oldValue, newValue);
minAmountTextField.setText(pm().minAmount.get());
presentationModel.onFocusOutMinAmountTextField(oldValue, newValue, minAmountTextField.getText());
minAmountTextField.setText(presentationModel.minAmount.get());
});
pm().showWarningInvalidBtcDecimalPlaces.addListener((o, oldValue, newValue) -> {
priceTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
presentationModel.onFocusOutPriceTextField(oldValue, newValue, priceTextField.getText());
priceTextField.setText(presentationModel.price.get());
});
volumeTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
presentationModel.onFocusOutVolumeTextField(oldValue, newValue, volumeTextField.getText());
volumeTextField.setText(presentationModel.volume.get());
});
// warnings
presentationModel.showWarningInvalidBtcDecimalPlaces.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup("Warning", "The amount you have entered exceeds the number of allowed decimal" +
" places.\nThe amount has been adjusted to 4 decimal places.");
pm().showWarningInvalidBtcDecimalPlaces.set(false);
presentationModel.showWarningInvalidBtcDecimalPlaces.set(false);
}
});
pm().showWarningInvalidFiatDecimalPlaces.addListener((o, oldValue, newValue) -> {
presentationModel.showWarningInvalidFiatDecimalPlaces.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup("Warning", "The amount you have entered exceeds the number of allowed decimal" +
" places.\nThe amount has been adjusted to 2 decimal places.");
pm().showWarningInvalidFiatDecimalPlaces.set(false);
presentationModel.showWarningInvalidFiatDecimalPlaces.set(false);
}
});
pm().showWarningAdjustedVolume.addListener((o, oldValue, newValue) -> {
presentationModel.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().showWarningAdjustedVolume.set(false);
volumeTextField.setText(pm().volume.get());
presentationModel.showWarningAdjustedVolume.set(false);
volumeTextField.setText(presentationModel.volume.get());
}
});
pm().requestPlaceOfferFailed.addListener((o, oldValue, newValue) -> {
presentationModel.requestPlaceOfferFailed.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openErrorPopup("Error", "An error occurred when placing the offer.\n" +
pm().requestPlaceOfferErrorMessage);
pm().requestPlaceOfferFailed.set(false);
presentationModel.requestPlaceOfferErrorMessage);
presentationModel.requestPlaceOfferFailed.set(false);
}
});
}
private void setupBindings() {
buyLabel.textProperty().bind(pm().directionLabel);
amountTextField.textProperty().bindBidirectional(pm().amount);
priceTextField.textProperty().bindBidirectional(pm().price);
volumeTextField.textProperty().bindBidirectional(pm().volume);
buyLabel.textProperty().bind(presentationModel.directionLabel);
minAmountTextField.textProperty().bindBidirectional(pm().minAmount);
collateralLabel.textProperty().bind(pm().collateralLabel);
collateralTextField.textProperty().bind(pm().collateral);
totalToPayTextField.textProperty().bind(pm().totalToPay);
amountTextField.textProperty().bindBidirectional(presentationModel.amount);
minAmountTextField.textProperty().bindBidirectional(presentationModel.minAmount);
priceTextField.textProperty().bindBidirectional(presentationModel.price);
volumeTextField.textProperty().bindBidirectional(presentationModel.volume);
addressTextField.amountAsCoinProperty().bind(pm().totalToPayAsCoin);
addressTextField.paymentLabelProperty().bind(pm().paymentLabel);
addressTextField.addressProperty().bind(pm().addressAsString);
collateralLabel.textProperty().bind(presentationModel.collateralLabel);
collateralTextField.textProperty().bind(presentationModel.collateral);
totalToPayTextField.textProperty().bind(presentationModel.totalToPay);
totalFeesTextField.textProperty().bind(presentationModel.totalFees);
bankAccountTypeTextField.textProperty().bind(pm().bankAccountType);
bankAccountCurrencyTextField.textProperty().bind(pm().bankAccountCurrency);
bankAccountCountyTextField.textProperty().bind(pm().bankAccountCounty);
addressTextField.amountAsCoinProperty().bind(presentationModel.totalToPayAsCoin);
addressTextField.paymentLabelProperty().bind(presentationModel.paymentLabel);
addressTextField.addressProperty().bind(presentationModel.addressAsString);
acceptedCountriesTextField.textProperty().bind(pm().acceptedCountries);
acceptedLanguagesTextField.textProperty().bind(pm().acceptedLanguages);
totalFeesTextField.textProperty().bind(pm().totalFees);
transactionIdTextField.textProperty().bind(pm().transactionId);
bankAccountTypeTextField.textProperty().bind(presentationModel.bankAccountType);
bankAccountCurrencyTextField.textProperty().bind(presentationModel.bankAccountCurrency);
bankAccountCountyTextField.textProperty().bind(presentationModel.bankAccountCounty);
amountTextField.amountValidationResultProperty().bind(pm().amountValidationResult);
minAmountTextField.amountValidationResultProperty().bind(pm().minAmountValidationResult);
priceTextField.amountValidationResultProperty().bind(pm().priceValidationResult);
volumeTextField.amountValidationResultProperty().bind(pm().volumeValidationResult);
acceptedCountriesTextField.textProperty().bind(presentationModel.acceptedCountries);
acceptedLanguagesTextField.textProperty().bind(presentationModel.acceptedLanguages);
transactionIdTextField.textProperty().bind(presentationModel.transactionId);
placeOfferButton.visibleProperty().bind(pm().isPlaceOfferButtonVisible);
placeOfferButton.disableProperty().bind(pm().isPlaceOfferButtonDisabled);
closeButton.visibleProperty().bind(pm().isCloseButtonVisible);
// Validation
amountTextField.amountValidationResultProperty().bind(presentationModel.amountValidationResult);
minAmountTextField.amountValidationResultProperty().bind(presentationModel.minAmountValidationResult);
priceTextField.amountValidationResultProperty().bind(presentationModel.priceValidationResult);
volumeTextField.amountValidationResultProperty().bind(presentationModel.volumeValidationResult);
// buttons
placeOfferButton.visibleProperty().bind(presentationModel.isPlaceOfferButtonVisible);
placeOfferButton.disableProperty().bind(presentationModel.isPlaceOfferButtonDisabled);
closeButton.visibleProperty().bind(presentationModel.isCloseButtonVisible);
}
private void configTextFieldValidators() {
Region referenceNode = (Region) amountTextField.getParent();
amountTextField.setErrorPopupLayoutReference(referenceNode);
priceTextField.setErrorPopupLayoutReference(referenceNode);
volumeTextField.setErrorPopupLayoutReference(referenceNode);
amountTextField.setLayoutReference(referenceNode);
priceTextField.setLayoutReference(referenceNode);
volumeTextField.setLayoutReference(referenceNode);
}
}

View file

@ -100,32 +100,18 @@ class CreateOfferModel extends UIModel {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public CreateOfferModel(TradeManager tradeManager, WalletFacade walletFacade, Settings settings, User user) {
CreateOfferModel(TradeManager tradeManager, WalletFacade walletFacade, Settings settings, User user) {
this.tradeManager = tradeManager;
this.walletFacade = walletFacade;
this.settings = settings;
this.user = user;
// static data
offerId = UUID.randomUUID().toString();
totalFeesAsCoin.setValue(FeePolicy.CREATE_OFFER_FEE.add(FeePolicy.TX_FEE));
//TODO just for unit testing, use mockito?
if (walletFacade != null && walletFacade.getWallet() != null)
addressEntry = walletFacade.getAddressInfoByTradeID(offerId);
collateralAsLong.setValue(settings.getCollateral());
BankAccount bankAccount = user.getCurrentBankAccount();
if (bankAccount != null) {
bankAccountType.setValue(bankAccount.getBankAccountType().toString());
bankAccountCurrency.setValue(bankAccount.getCurrency().getCurrencyCode());
bankAccountCounty.setValue(bankAccount.getCountry().getName());
}
acceptedCountries.setAll(settings.getAcceptedCountries());
acceptedLanguages.setAll(settings.getAcceptedLanguageLocales());
// Node: Don't do setup in constructor to make object creation faster
}
///////////////////////////////////////////////////////////////////////////////////////////
// Lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@ -133,11 +119,29 @@ class CreateOfferModel extends UIModel {
@Override
public void initialized() {
super.initialized();
// static data
totalFeesAsCoin.set(FeePolicy.CREATE_OFFER_FEE.add(FeePolicy.TX_FEE));
if (walletFacade != null && walletFacade.getWallet() != null)
addressEntry = walletFacade.getAddressInfoByTradeID(offerId);
}
@Override
public void activate() {
super.activate();
// might be changed after screen change
collateralAsLong.set(settings.getCollateral());
BankAccount bankAccount = user.getCurrentBankAccount();
if (bankAccount != null) {
bankAccountType.set(bankAccount.getBankAccountType().toString());
bankAccountCurrency.set(bankAccount.getCurrency().getCurrencyCode());
bankAccountCounty.set(bankAccount.getCountry().getName());
}
acceptedCountries.setAll(settings.getAcceptedCountries());
acceptedLanguages.setAll(settings.getAcceptedLanguageLocales());
}
@Override
@ -150,41 +154,43 @@ class CreateOfferModel extends UIModel {
super.terminate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Methods
///////////////////////////////////////////////////////////////////////////////////////////
void placeOffer() {
// data validation is done in the trade domain
tradeManager.requestPlaceOffer(offerId,
direction,
priceAsFiat.get(),
amountAsCoin.get(),
minAmountAsCoin.get(),
(transaction) -> {
requestPlaceOfferSuccess.setValue(true);
transactionId.setValue(transaction.getHashAsString());
requestPlaceOfferSuccess.set(true);
transactionId.set(transaction.getHashAsString());
},
(errorMessage) -> {
requestPlaceOfferFailed.setValue(true);
requestPlaceOfferErrorMessage.setValue(errorMessage);
requestPlaceOfferFailed.set(true);
requestPlaceOfferErrorMessage.set(errorMessage);
}
);
}
void calculateVolume() {
if (priceAsFiat.get() != null && amountAsCoin.get() != null /*&& !amountAsCoin.get().isZero()*/)
volumeAsFiat.setValue(new ExchangeRate(priceAsFiat.get()).coinToFiat(amountAsCoin.get()));
if (priceAsFiat.get() != null && amountAsCoin.get() != null /*&& !amountAsCoin.get().isZero()*/) {
volumeAsFiat.set(new ExchangeRate(priceAsFiat.get()).coinToFiat(amountAsCoin.get()));
}
}
void calculateAmount() {
if (volumeAsFiat.get() != null && priceAsFiat.get() != null/* && !volumeAsFiat.get().isZero() && !priceAsFiat
.get().isZero()*/) {
amountAsCoin.setValue(new ExchangeRate(priceAsFiat.get()).fiatToCoin(volumeAsFiat.get()));
amountAsCoin.set(new ExchangeRate(priceAsFiat.get()).fiatToCoin(volumeAsFiat.get()));
// If we got a btc value with more then 4 decimals we convert it to max 4 decimals
amountAsCoin.setValue(reduceto4Dezimals(amountAsCoin.get()));
amountAsCoin.set(reduceto4Dezimals(amountAsCoin.get()));
calculateTotalToPay();
calculateCollateral();
}
@ -193,17 +199,16 @@ class CreateOfferModel extends UIModel {
void calculateTotalToPay() {
calculateCollateral();
if (collateralAsCoin.get() != null) {
totalToPayAsCoin.setValue(collateralAsCoin.get().add(totalFeesAsCoin.get()));
}
if (collateralAsCoin.get() != null)
totalToPayAsCoin.set(collateralAsCoin.get().add(totalFeesAsCoin.get()));
}
void calculateCollateral() {
if (amountAsCoin.get() != null)
collateralAsCoin.setValue(amountAsCoin.get().multiply(collateralAsLong.get()).divide(1000));
collateralAsCoin.set(amountAsCoin.get().multiply(collateralAsLong.get()).divide(1000));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Validation
///////////////////////////////////////////////////////////////////////////////////////////
@ -214,6 +219,7 @@ class CreateOfferModel extends UIModel {
return true;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setter/Getter
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -30,6 +30,8 @@ import io.bitsquare.trade.orderbook.OrderBookFilter;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.Coin;
import javax.inject.Inject;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
@ -48,7 +50,6 @@ import static javafx.beans.binding.Bindings.createStringBinding;
class CreateOfferPM extends PresentationModel<CreateOfferModel> {
private static final Logger log = LoggerFactory.getLogger(CreateOfferPM.class);
private BtcValidator btcValidator = new BtcValidator();
private FiatValidator fiatValidator = new FiatValidator();
@ -85,29 +86,23 @@ class CreateOfferPM extends PresentationModel<CreateOfferModel> {
final ObjectProperty<InputValidator.ValidationResult> priceValidationResult = new SimpleObjectProperty<>();
final ObjectProperty<InputValidator.ValidationResult> volumeValidationResult = new SimpleObjectProperty<>();
// That is needed for the addressTextField
// Those are needed for the addressTextField
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Address> address = new SimpleObjectProperty<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor (called by CB)
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
CreateOfferPM(CreateOfferModel model) {
super(model);
paymentLabel.setValue("Bitsquare trade (" + model().getOfferId() + ")");
if (model().addressEntry != null) {
addressAsString.setValue(model().addressEntry.getAddress().toString());
address.setValue(model().addressEntry.getAddress());
}
setupModelBindings();
setupUIInputListeners();
// Node: Don't do setup in constructor to make object creation faster
}
///////////////////////////////////////////////////////////////////////////////////////////
// Lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@ -115,6 +110,17 @@ class CreateOfferPM extends PresentationModel<CreateOfferModel> {
@Override
public void initialized() {
super.initialized();
// static
paymentLabel.set("Bitsquare trade (" + model.getOfferId() + ")");
if (model.addressEntry != null) {
addressAsString.set(model.addressEntry.getAddress().toString());
address.set(model.addressEntry.getAddress());
}
setupBindings();
setupListeners();
}
@Override
@ -132,23 +138,27 @@ class CreateOfferPM extends PresentationModel<CreateOfferModel> {
super.terminate();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public API methods (called by CB)
///////////////////////////////////////////////////////////////////////////////////////////
// setOrderBookFilter is a one time call
void setOrderBookFilter(OrderBookFilter orderBookFilter) {
model().setDirection(orderBookFilter.getDirection());
directionLabel.setValue(model().getDirection() == Direction.BUY ? "Buy:" : "Sell:");
model.setDirection(orderBookFilter.getDirection());
directionLabel.set(model.getDirection() == Direction.BUY ? "Buy:" : "Sell:");
// apply only if valid
if (orderBookFilter.getAmount() != null && isBtcInputValid(orderBookFilter.getAmount().toPlainString())
.isValid) {
model().amountAsCoin.setValue(orderBookFilter.getAmount());
model().minAmountAsCoin.setValue(orderBookFilter.getAmount());
model.amountAsCoin.set(orderBookFilter.getAmount());
model.minAmountAsCoin.set(orderBookFilter.getAmount());
}
// TODO use Fiat in orderBookFilter
// apply only if valid
if (orderBookFilter.getPrice() != 0 && isBtcInputValid(String.valueOf(orderBookFilter.getPrice())).isValid)
model().priceAsFiat.setValue(parseToFiatWith2Decimals(String.valueOf(orderBookFilter.getPrice())));
model.priceAsFiat.set(parseToFiatWith2Decimals(String.valueOf(orderBookFilter.getPrice())));
}
@ -157,97 +167,96 @@ class CreateOfferPM extends PresentationModel<CreateOfferModel> {
///////////////////////////////////////////////////////////////////////////////////////////
void placeOffer() {
model().placeOffer();
isPlaceOfferButtonDisabled.setValue(true);
isPlaceOfferButtonVisible.setValue(true);
model.placeOffer();
isPlaceOfferButtonDisabled.set(true);
isPlaceOfferButtonVisible.set(true);
}
void close() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI events (called by CB)
///////////////////////////////////////////////////////////////////////////////////////////
// when focus out we do validation and apply the data to the model
void onFocusOutAmountTextField(Boolean oldValue, Boolean newValue) {
// On focus out we do validation and apply the data to the model
void onFocusOutAmountTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(amount.get());
boolean isValid = result.isValid;
amountValidationResult.setValue(result);
if (isValid) {
showWarningInvalidBtcDecimalPlaces.setValue(!hasBtcValidDecimals(amount.get()));
amountValidationResult.set(result);
if (result.isValid) {
showWarningInvalidBtcDecimalPlaces.set(!hasBtcValidDecimals(userInput));
// only allow max 4 decimal places for btc values
setAmountToModel();
// reformat input to general btc format
// reformat input
amount.set(formatCoin(model.amountAsCoin.get()));
calculateVolume();
if (!model().isMinAmountLessOrEqualAmount()) {
amountValidationResult.setValue(new InputValidator.ValidationResult(false,
// handle minAmount/amount relationship
if (!model.isMinAmountLessOrEqualAmount()) {
amountValidationResult.set(new InputValidator.ValidationResult(false,
"Amount cannot be smaller than minimum amount."));
}
else {
amountValidationResult.setValue(result);
amountValidationResult.set(result);
if (minAmount.get() != null)
minAmountValidationResult.setValue(isBtcInputValid(minAmount.get()));
minAmountValidationResult.set(isBtcInputValid(minAmount.get()));
}
}
}
}
void onFocusOutMinAmountTextField(Boolean oldValue, Boolean newValue) {
void onFocusOutMinAmountTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(minAmount.get());
boolean isValid = result.isValid;
minAmountValidationResult.setValue(result);
if (isValid) {
showWarningInvalidBtcDecimalPlaces.setValue(!hasBtcValidDecimals(minAmount.get()));
minAmountValidationResult.set(result);
if (result.isValid) {
showWarningInvalidBtcDecimalPlaces.set(!hasBtcValidDecimals(userInput));
setMinAmountToModel();
minAmount.set(formatCoin(model.minAmountAsCoin.get()));
if (!model().isMinAmountLessOrEqualAmount()) {
minAmountValidationResult.setValue(new InputValidator.ValidationResult(false,
if (!model.isMinAmountLessOrEqualAmount()) {
minAmountValidationResult.set(new InputValidator.ValidationResult(false,
"Minimum amount cannot be larger than amount."));
}
else {
minAmountValidationResult.setValue(result);
minAmountValidationResult.set(result);
if (amount.get() != null)
amountValidationResult.setValue(isBtcInputValid(amount.get()));
amountValidationResult.set(isBtcInputValid(amount.get()));
}
}
}
}
void onFocusOutPriceTextField(Boolean oldValue, Boolean newValue) {
void onFocusOutPriceTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isFiatInputValid(price.get());
boolean isValid = result.isValid;
priceValidationResult.setValue(result);
priceValidationResult.set(result);
if (isValid) {
showWarningInvalidFiatDecimalPlaces.setValue(!hasFiatValidDecimals(price.get()));
showWarningInvalidFiatDecimalPlaces.set(!hasFiatValidDecimals(userInput));
setPriceToModel();
price.set(formatFiat(model.priceAsFiat.get()));
calculateVolume();
}
}
}
void onFocusOutVolumeTextField(Boolean oldValue, Boolean newValue) {
void onFocusOutVolumeTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(volume.get());
boolean isValid = result.isValid;
volumeValidationResult.setValue(result);
if (isValid) {
String origVolume = volume.get();
showWarningInvalidFiatDecimalPlaces.setValue(!hasFiatValidDecimals(volume.get()));
volumeValidationResult.set(result);
if (result.isValid) {
showWarningInvalidFiatDecimalPlaces.set(!hasFiatValidDecimals(userInput));
setVolumeToModel();
volume.set(formatFiat(model.volumeAsFiat.get()));
calculateAmount();
// must be after calculateAmount (btc value has been adjusted in case the calculation leads to
// must be placed after calculateAmount (btc value has been adjusted in case the calculation leads to
// invalid decimal places for the amount value
showWarningAdjustedVolume.setValue(!formatFiat(parseToFiatWith2Decimals(origVolume)).equals(volume
showWarningAdjustedVolume.set(!formatFiat(parseToFiatWith2Decimals(userInput)).equals(volume
.get()));
}
}
@ -259,7 +268,7 @@ class CreateOfferPM extends PresentationModel<CreateOfferModel> {
///////////////////////////////////////////////////////////////////////////////////////////
WalletFacade getWalletFacade() {
return model().getWalletFacade();
return model.getWalletFacade();
}
@ -267,129 +276,127 @@ class CreateOfferPM extends PresentationModel<CreateOfferModel> {
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void setupUIInputListeners() {
private void setupListeners() {
// bindBidirectional for amount, price, volume and minAmount
// We do volume/amount calculation during input
// Bidirectional bindings are used for all input fields: amount, price, volume and minAmount
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener((ov, oldValue, newValue) -> {
if (isBtcInputValid(newValue).isValid) {
model().amountAsCoin.setValue(parseToCoinWith4Decimals(newValue));
setMinAmountToModel();
calculateVolume();
model().calculateTotalToPay();
model().calculateCollateral();
model.calculateTotalToPay();
model.calculateCollateral();
}
});
price.addListener((ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
model().priceAsFiat.setValue(parseToFiatWith2Decimals(newValue));
setPriceToModel();
calculateVolume();
model().calculateTotalToPay();
model().calculateCollateral();
model.calculateTotalToPay();
model.calculateCollateral();
}
});
volume.addListener((ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
model().volumeAsFiat.setValue(parseToFiatWith2Decimals(newValue));
setVolumeToModel();
setPriceToModel();
model().calculateAmount();
model().calculateTotalToPay();
model().calculateCollateral();
model.calculateAmount();
model.calculateTotalToPay();
model.calculateCollateral();
}
});
// Binding with Bindings.createObjectBinding does not work becaue of bi-directional binding in CB
model().amountAsCoin.addListener((ov, oldValue, newValue) -> amount.set(formatCoin(newValue)));
model().minAmountAsCoin.addListener((ov, oldValue, newValue) -> minAmount.set(formatCoin(newValue)));
model().priceAsFiat.addListener((ov, oldValue, newValue) -> price.set(formatFiat(newValue)));
model().volumeAsFiat.addListener((ov, oldValue, newValue) -> volume.set(formatFiat(newValue)));
}
private void setupModelBindings() {
totalToPay.bind(createStringBinding(() -> formatCoinWithCode(model().totalToPayAsCoin.get()),
model().totalToPayAsCoin));
collateral.bind(createStringBinding(() -> formatCoinWithCode(model().collateralAsCoin.get()),
model().collateralAsCoin));
collateralLabel.bind(Bindings.createStringBinding(() -> "Collateral (" + BSFormatter.formatCollateralPercent
(model().collateralAsLong.get()) + "):", model().collateralAsLong));
totalToPayAsCoin.bind(model().totalToPayAsCoin);
bankAccountType.bind(Bindings.createStringBinding(() -> Localisation.get(model().bankAccountType.get()),
model().bankAccountType));
bankAccountCurrency.bind(model().bankAccountCurrency);
bankAccountCounty.bind(model().bankAccountCounty);
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
model.amountAsCoin.addListener((ov, oldValue, newValue) -> amount.set(formatCoin(newValue)));
model.minAmountAsCoin.addListener((ov, oldValue, newValue) -> minAmount.set(formatCoin(newValue)));
model.priceAsFiat.addListener((ov, oldValue, newValue) -> price.set(formatFiat(newValue)));
model.volumeAsFiat.addListener((ov, oldValue, newValue) -> volume.set(formatFiat(newValue)));
// ObservableLists
model().acceptedCountries.addListener((Observable o) -> acceptedCountries.setValue(BSFormatter
.countryLocalesToString(model().acceptedCountries)));
model().acceptedLanguages.addListener((Observable o) -> acceptedLanguages.setValue(BSFormatter
.languageLocalesToString(model().acceptedLanguages)));
isCloseButtonVisible.bind(model().requestPlaceOfferSuccess);
requestPlaceOfferErrorMessage.bind(model().requestPlaceOfferErrorMessage);
requestPlaceOfferFailed.bind(model().requestPlaceOfferFailed);
showTransactionPublishedScreen.bind(model().requestPlaceOfferSuccess);
isPlaceOfferButtonDisabled.bind(Bindings.createBooleanBinding(() -> !model().requestPlaceOfferFailed.get(),
model().requestPlaceOfferFailed));
isPlaceOfferButtonVisible.bind(Bindings.createBooleanBinding(() -> !model().requestPlaceOfferSuccess.get(),
model().requestPlaceOfferSuccess));
model.acceptedCountries.addListener((Observable o) -> acceptedCountries.set(BSFormatter
.countryLocalesToString(model.acceptedCountries)));
model.acceptedLanguages.addListener((Observable o) -> acceptedLanguages.set(BSFormatter
.languageLocalesToString(model.acceptedLanguages)));
}
private void setupBindings() {
totalToPay.bind(createStringBinding(() -> formatCoinWithCode(model.totalToPayAsCoin.get()),
model.totalToPayAsCoin));
collateral.bind(createStringBinding(() -> formatCoinWithCode(model.collateralAsCoin.get()),
model.collateralAsCoin));
collateralLabel.bind(Bindings.createStringBinding(() -> "Collateral (" + BSFormatter.formatCollateralPercent
(model.collateralAsLong.get()) + "):", model.collateralAsLong));
totalToPayAsCoin.bind(model.totalToPayAsCoin);
totalFees.bind(createStringBinding(() -> formatCoinWithCode(model.totalFeesAsCoin.get()),
model.totalFeesAsCoin));
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);
requestPlaceOfferFailed.bind(model.requestPlaceOfferFailed);
showTransactionPublishedScreen.bind(model.requestPlaceOfferSuccess);
isPlaceOfferButtonDisabled.bind(Bindings.createBooleanBinding(() -> !model.requestPlaceOfferFailed.get(),
model.requestPlaceOfferFailed));
isPlaceOfferButtonVisible.bind(Bindings.createBooleanBinding(() -> !model.requestPlaceOfferSuccess.get(),
model.requestPlaceOfferSuccess));
}
private void calculateVolume() {
setAmountToModel();
setPriceToModel();
model().calculateVolume();
model.calculateVolume();
}
private void calculateAmount() {
setVolumeToModel();
setPriceToModel();
model().calculateAmount();
model.calculateAmount();
if (!model().isMinAmountLessOrEqualAmount()) {
amountValidationResult.setValue(new InputValidator.ValidationResult(false,
// Amount calculation could lead to amount/minAmount invalidation
if (!model.isMinAmountLessOrEqualAmount()) {
amountValidationResult.set(new InputValidator.ValidationResult(false,
"Amount cannot be smaller than minimum amount."));
}
else {
if (amount.get() != null)
amountValidationResult.setValue(isBtcInputValid(amount.get()));
amountValidationResult.set(isBtcInputValid(amount.get()));
if (minAmount.get() != null)
minAmountValidationResult.setValue(isBtcInputValid(minAmount.get()));
minAmountValidationResult.set(isBtcInputValid(minAmount.get()));
}
}
private void setAmountToModel() {
model().amountAsCoin.setValue(parseToCoinWith4Decimals(amount.get()));
model.amountAsCoin.set(parseToCoinWith4Decimals(amount.get()));
}
private void setMinAmountToModel() {
model().minAmountAsCoin.setValue(parseToCoinWith4Decimals(minAmount.get()));
model.minAmountAsCoin.set(parseToCoinWith4Decimals(minAmount.get()));
}
private void setPriceToModel() {
model().priceAsFiat.setValue(parseToFiatWith2Decimals(price.get()));
model.priceAsFiat.set(parseToFiatWith2Decimals(price.get()));
}
private void setVolumeToModel() {
model().volumeAsFiat.setValue(parseToFiatWith2Decimals(volume.get()));
model.volumeAsFiat.set(parseToFiatWith2Decimals(volume.get()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// Package scope for testing
///////////////////////////////////////////////////////////////////////////////////////////
InputValidator.ValidationResult isBtcInputValid(String input) {
private InputValidator.ValidationResult isBtcInputValid(String input) {
return btcValidator.validate(input);
}
InputValidator.ValidationResult isFiatInputValid(String input) {
private InputValidator.ValidationResult isFiatInputValid(String input) {
return fiatValidator.validate(input);
}

View file

@ -49,9 +49,9 @@
<Label GridPane.rowIndex="1" fx:id="buyLabel"/>
<HBox GridPane.rowIndex="1" GridPane.columnIndex="1" spacing="5" GridPane.hgrow="NEVER" alignment="CENTER_LEFT">
<ValidatingTextField fx:id="amountTextField" prefWidth="100.0" alignment="CENTER_RIGHT"/>
<Label text="BTC @"/>
<Label text="BTC for price of "/>
<ValidatingTextField fx:id="priceTextField" prefWidth="100.0" alignment="CENTER_RIGHT"/>
<Label text="EUR ="/>
<Label text="EUR/BTC ="/>
<ValidatingTextField fx:id="volumeTextField" prefWidth="100.0" alignment="CENTER_RIGHT"/>
<Label text="EUR in total"/>
</HBox>