/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.main.portfolio.pendingtrades.steps;
import io.bitsquare.app.Log;
import io.bitsquare.arbitration.Dispute;
import io.bitsquare.common.Clock;
import io.bitsquare.gui.components.TitledGroupBg;
import io.bitsquare.gui.components.TxIdTextField;
import io.bitsquare.gui.components.paymentmethods.PaymentMethodForm;
import io.bitsquare.gui.main.overlays.popups.Popup;
import io.bitsquare.gui.main.portfolio.pendingtrades.PendingTradesViewModel;
import io.bitsquare.gui.main.portfolio.pendingtrades.TradeSubView;
import io.bitsquare.gui.util.Layout;
import io.bitsquare.trade.Trade;
import io.bitsquare.user.Preferences;
import javafx.beans.value.ChangeListener;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane;
import org.fxmisc.easybind.EasyBind;
import org.fxmisc.easybind.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Optional;
import static com.google.common.base.Preconditions.checkNotNull;
import static io.bitsquare.gui.util.FormBuilder.*;
public abstract class TradeStepView extends AnchorPane {
protected final Logger log = LoggerFactory.getLogger(this.getClass());
protected final PendingTradesViewModel model;
protected final Trade trade;
protected final Preferences preferences;
protected final GridPane gridPane;
private Subscription disputeStateSubscription;
private Subscription tradePeriodStateSubscription;
protected int gridRow = 0;
protected TitledGroupBg tradeInfoTitledGroupBg;
private TextField timeLeftTextField;
private ProgressBar timeLeftProgressBar;
private TxIdTextField txIdTextField;
protected TradeSubView.NotificationGroup notificationGroup;
private Subscription txIdSubscription;
private Clock.Listener clockListener;
private ChangeListener<String> errorMessageListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, Initialisation
///////////////////////////////////////////////////////////////////////////////////////////
protected TradeStepView(PendingTradesViewModel model) {
this.model = model;
preferences = model.dataModel.preferences;
trade = model.dataModel.getTrade();
checkNotNull(trade, "trade must not be null at TradeStepView");
gridPane = addGridPane(this);
AnchorPane.setLeftAnchor(this, 0d);
AnchorPane.setRightAnchor(this, 0d);
AnchorPane.setTopAnchor(this, -10d);
AnchorPane.setBottomAnchor(this, 0d);
addContent();
errorMessageListener = (observable, oldValue, newValue) -> {
if (newValue != null)
showSupportFields();
};
}
public void activate() {
if (txIdTextField != null) {
if (txIdSubscription != null)
txIdSubscription.unsubscribe();
txIdSubscription = EasyBind.subscribe(model.dataModel.txId, id -> {
if (!id.isEmpty())
txIdTextField.setup(id);
else
txIdTextField.cleanup();
});
}
trade.errorMessageProperty().addListener(errorMessageListener);
disputeStateSubscription = EasyBind.subscribe(trade.disputeStateProperty(), newValue -> {
if (newValue != null)
updateDisputeState(newValue);
});
tradePeriodStateSubscription = EasyBind.subscribe(trade.getTradePeriodStateProperty(), newValue -> {
if (newValue != null)
updateTradePeriodState(newValue);
});
clockListener = new Clock.Listener() {
@Override
public void onSecondTick() {
}
@Override
public void onMinuteTick() {
updateTimeLeft();
}
@Override
public void onMissedSecondTick(long missed) {
}
};
model.clock.addListener(clockListener);
}
public void deactivate() {
Log.traceCall();
if (txIdSubscription != null)
txIdSubscription.unsubscribe();
if (txIdTextField != null)
txIdTextField.cleanup();
if (errorMessageListener != null)
trade.errorMessageProperty().removeListener(errorMessageListener);
if (disputeStateSubscription != null)
disputeStateSubscription.unsubscribe();
if (tradePeriodStateSubscription != null)
tradePeriodStateSubscription.unsubscribe();
if (clockListener != null)
model.clock.removeListener(clockListener);
if (notificationGroup != null)
notificationGroup.button.setOnAction(null);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Content
///////////////////////////////////////////////////////////////////////////////////////////
protected void addContent() {
addTradeInfoBlock();
addInfoBlock();
}
protected void addTradeInfoBlock() {
tradeInfoTitledGroupBg = addTitledGroupBg(gridPane, gridRow, 4, "Trade information");
txIdTextField = addLabelTxIdTextField(gridPane, gridRow, "Deposit transaction ID:", Layout.FIRST_ROW_DISTANCE).second;
String id = model.dataModel.txId.get();
if (!id.isEmpty())
txIdTextField.setup(id);
else
txIdTextField.cleanup();
PaymentMethodForm.addAllowedPeriod(gridPane, ++gridRow, model.dataModel.getSellersPaymentAccountContractData(),
model.getDateForOpenDispute());
timeLeftTextField = addLabelTextField(gridPane, ++gridRow, "Remaining time:").second;
timeLeftProgressBar = new ProgressBar(0);
timeLeftProgressBar.setOpacity(0.7);
timeLeftProgressBar.setMinHeight(9);
timeLeftProgressBar.setMaxHeight(9);
timeLeftProgressBar.setMaxWidth(Double.MAX_VALUE);
GridPane.setRowIndex(timeLeftProgressBar, ++gridRow);
GridPane.setColumnIndex(timeLeftProgressBar, 1);
GridPane.setFillWidth(timeLeftProgressBar, true);
gridPane.getChildren().add(timeLeftProgressBar);
updateTimeLeft();
}
protected void addInfoBlock() {
addTitledGroupBg(gridPane, ++gridRow, 1, getInfoBlockTitle(), Layout.GROUP_DISTANCE);
addMultilineLabel(gridPane, gridRow, getInfoText(), Layout.FIRST_ROW_AND_GROUP_DISTANCE);
}
protected String getInfoText() {
return "";
}
protected String getInfoBlockTitle() {
return "";
}
private void updateTimeLeft() {
if (timeLeftTextField != null) {
String remainingTime = model.getRemainingTradeDurationAsWords();
timeLeftProgressBar.setProgress(model.getRemainingTradeDurationAsPercentage());
if (remainingTime != null) {
timeLeftTextField.setText(remainingTime);
if (model.showWarning() || model.showDispute()) {
timeLeftTextField.setStyle("-fx-text-fill: -bs-error-red");
timeLeftProgressBar.setStyle("-fx-accent: -bs-error-red;");
}
} else {
timeLeftTextField.setText("Trade not completed in time (" + model.getDateForOpenDispute() + ")");
timeLeftTextField.setStyle("-fx-text-fill: -bs-error-red");
timeLeftProgressBar.setStyle("-fx-accent: -bs-error-red;");
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Dispute/warning label and button
///////////////////////////////////////////////////////////////////////////////////////////
// We have the dispute button and text field on the left side, but we handle the content here as it
// is trade state specific
public void setNotificationGroup(TradeSubView.NotificationGroup notificationGroup) {
this.notificationGroup = notificationGroup;
}
private void showDisputeInfoLabel() {
if (notificationGroup != null)
notificationGroup.setLabelAndHeadlineVisible(true);
}
private void showOpenDisputeButton() {
if (notificationGroup != null) {
notificationGroup.setButtonVisible(true);
notificationGroup.button.setOnAction(e -> {
notificationGroup.button.setDisable(true);
onDisputeOpened();
model.dataModel.onOpenDispute();
});
}
}
protected void setWarningHeadline() {
if (notificationGroup != null) {
notificationGroup.titledGroupBg.setText("Warning");
}
}
protected void setInformationHeadline() {
if (notificationGroup != null) {
notificationGroup.titledGroupBg.setText("Notification");
}
}
protected void setOpenDisputeHeadline() {
if (notificationGroup != null) {
notificationGroup.titledGroupBg.setText("Open a dispute");
}
}
protected void setDisputeOpenedHeadline() {
if (notificationGroup != null) {
notificationGroup.titledGroupBg.setText("Dispute opened");
}
}
protected void setRequestSupportHeadline() {
if (notificationGroup != null) {
notificationGroup.titledGroupBg.setText("Open support ticket");
}
}
protected void setSupportOpenedHeadline() {
if (notificationGroup != null) {
notificationGroup.titledGroupBg.setText("Support ticket opened");
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Support
///////////////////////////////////////////////////////////////////////////////////////////
private void showSupportFields() {
if (notificationGroup != null) {
notificationGroup.button.setText("Request support");
notificationGroup.button.setId("open-support-button");
notificationGroup.button.setOnAction(e -> model.dataModel.onOpenSupportTicket());
}
new Popup().warning(trade.errorMessageProperty().getValue()
+ "\n\nPlease report the problem to your arbitrator.\n\n" +
"He will forward the information to the developers to investigate the problem.\n" +
"After the problem has be analyzed you will get back all the funds if funds was locked.\n" +
"There will be no arbitration fee charged in case of a software bug.")
.show();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Warning
///////////////////////////////////////////////////////////////////////////////////////////
private void showWarning() {
showDisputeInfoLabel();
if (notificationGroup != null)
notificationGroup.label.setText(getWarningText());
}
private void removeWarning() {
hideNotificationGroup();
}
protected String getWarningText() {
return "";
}
///////////////////////////////////////////////////////////////////////////////////////////
// Dispute
///////////////////////////////////////////////////////////////////////////////////////////
private void onOpenForDispute() {
showDisputeInfoLabel();
showOpenDisputeButton();
setOpenDisputeHeadline();
if (notificationGroup != null)
notificationGroup.label.setText(getOpenForDisputeText());
}
private void onDisputeOpened() {
showDisputeInfoLabel();
showOpenDisputeButton();
applyOnDisputeOpened();
setDisputeOpenedHeadline();
if (notificationGroup != null)
notificationGroup.button.setDisable(true);
}
protected String getOpenForDisputeText() {
return "";
}
protected void applyOnDisputeOpened() {
}
protected void hideNotificationGroup() {
notificationGroup.setLabelAndHeadlineVisible(false);
notificationGroup.setButtonVisible(false);
}
private void updateDisputeState(Trade.DisputeState disputeState) {
Optional<Dispute> ownDispute;
switch (disputeState) {
case NONE:
break;
case DISPUTE_REQUESTED:
onDisputeOpened();
ownDispute = model.dataModel.disputeManager.findOwnDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
String msg;
if (dispute.isSupportTicket()) {
setSupportOpenedHeadline();
msg = "You opened already a support ticket.\n" +
"Please communicate in the \"Support\" screen with the arbitrator.";
} else {
setDisputeOpenedHeadline();
msg = "You opened already a dispute.\n" +
"Please communicate in the \"Support\" screen with the arbitrator.";
}
if (notificationGroup != null)
notificationGroup.label.setText(msg);
});
break;
case DISPUTE_STARTED_BY_PEER:
onDisputeOpened();
ownDispute = model.dataModel.disputeManager.findOwnDispute(trade.getId());
ownDispute.ifPresent(dispute -> {
String msg;
if (dispute.isSupportTicket()) {
setSupportOpenedHeadline();
msg = "Your trading peer opened a support ticket due technical problems.\n" +
"Please communicate in the \"Support\" screen with the arbitrator.";
} else {
setDisputeOpenedHeadline();
msg = "Your trading peer opened a dispute.\n" +
"Please communicate in the \"Support\" screen with the arbitrator.";
}
if (notificationGroup != null)
notificationGroup.label.setText(msg);
});
break;
case DISPUTE_CLOSED:
break;
}
}
private void updateTradePeriodState(Trade.TradePeriodState tradePeriodState) {
if (trade.getDisputeState() != Trade.DisputeState.DISPUTE_REQUESTED &&
trade.getDisputeState() != Trade.DisputeState.DISPUTE_STARTED_BY_PEER) {
switch (tradePeriodState) {
case NORMAL:
break;
case HALF_REACHED:
if (trade.getState().getPhase().ordinal() < Trade.Phase.FIAT_RECEIVED.ordinal())
showWarning();
else
removeWarning();
break;
case TRADE_PERIOD_OVER:
onOpenForDispute();
break;
}
}
}
}