diff --git a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java index 4a5bc8f1658..85d35e19af1 100644 --- a/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/account/witness/AccountAgeWitnessService.java @@ -131,6 +131,10 @@ public String getDisplayString() { return String.format(displayString, daysUntilLimitLifted); } + public boolean isLimitLifted() { + return this == PEER_LIMIT_LIFTED || this == PEER_SIGNER || this == ARBITRATOR; + } + } private final KeyRing keyRing; diff --git a/core/src/main/java/bisq/core/api/CoreOffersService.java b/core/src/main/java/bisq/core/api/CoreOffersService.java index 04ce9c2ceb3..4649f4f0b31 100644 --- a/core/src/main/java/bisq/core/api/CoreOffersService.java +++ b/core/src/main/java/bisq/core/api/CoreOffersService.java @@ -327,7 +327,8 @@ private OfferPayload getMergedOfferPayload(OpenOffer openOffer, offer.getOfferPayload().getCountryCode(), offer.getOfferPayload().getAcceptedCountryCodes(), offer.getOfferPayload().getBankId(), - offer.getOfferPayload().getAcceptedBankIds()); + offer.getOfferPayload().getAcceptedBankIds(), + offer.getOfferPayload().getExtraDataMap()); log.info("Merging OfferPayload with {}", mutableOfferPayloadFields); return offerUtil.getMergedOfferPayload(openOffer, mutableOfferPayloadFields); } diff --git a/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java b/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java index 56700fd63aa..ec0fe502faf 100644 --- a/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java +++ b/core/src/main/java/bisq/core/offer/MutableOfferPayloadFields.java @@ -18,6 +18,7 @@ package bisq.core.offer; import java.util.List; +import java.util.Map; import lombok.Getter; import lombok.Setter; @@ -48,6 +49,8 @@ public final class MutableOfferPayloadFields { private final String bankId; @Nullable private final List acceptedBankIds; + @Nullable + private final Map extraDataMap; public MutableOfferPayloadFields(OfferPayload offerPayload) { this(offerPayload.getPrice(), @@ -60,7 +63,8 @@ public MutableOfferPayloadFields(OfferPayload offerPayload) { offerPayload.getCountryCode(), offerPayload.getAcceptedCountryCodes(), offerPayload.getBankId(), - offerPayload.getAcceptedBankIds()); + offerPayload.getAcceptedBankIds(), + offerPayload.getExtraDataMap()); } public MutableOfferPayloadFields(long price, @@ -73,7 +77,8 @@ public MutableOfferPayloadFields(long price, @Nullable String countryCode, @Nullable List acceptedCountryCodes, @Nullable String bankId, - @Nullable List acceptedBankIds) { + @Nullable List acceptedBankIds, + @Nullable Map extraDataMap) { this.price = price; this.marketPriceMargin = marketPriceMargin; this.useMarketBasedPrice = useMarketBasedPrice; @@ -85,5 +90,6 @@ public MutableOfferPayloadFields(long price, this.acceptedCountryCodes = acceptedCountryCodes; this.bankId = bankId; this.acceptedBankIds = acceptedBankIds; + this.extraDataMap = extraDataMap; } } diff --git a/core/src/main/java/bisq/core/offer/OfferUtil.java b/core/src/main/java/bisq/core/offer/OfferUtil.java index 91c894feb0e..fb76a69c89e 100644 --- a/core/src/main/java/bisq/core/offer/OfferUtil.java +++ b/core/src/main/java/bisq/core/offer/OfferUtil.java @@ -407,7 +407,7 @@ public OfferPayload getMergedOfferPayload(OpenOffer openOffer, originalOfferPayload.getUpperClosePrice(), originalOfferPayload.isPrivateOffer(), originalOfferPayload.getHashOfChallenge(), - originalOfferPayload.getExtraDataMap(), + mutableOfferPayloadFields.getExtraDataMap(), originalOfferPayload.getProtocolVersion()); } diff --git a/core/src/main/java/bisq/core/support/dispute/Dispute.java b/core/src/main/java/bisq/core/support/dispute/Dispute.java index 98ed154fbee..669f7d08540 100644 --- a/core/src/main/java/bisq/core/support/dispute/Dispute.java +++ b/core/src/main/java/bisq/core/support/dispute/Dispute.java @@ -148,6 +148,8 @@ public static protobuf.Dispute.State toProtoMessage(Dispute.State state) { private transient String uid; @Setter private transient long payoutTxConfirms = -1; + @Setter + private transient boolean payoutDone = false; private transient final BooleanProperty isClosedProperty = new SimpleBooleanProperty(); private transient final IntegerProperty badgeCountProperty = new SimpleIntegerProperty(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 0c7c1d863c5..95378f141be 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -354,6 +354,14 @@ offerbook.timeSinceSigning.info.signer=signed by peer and can sign peer accounts offerbook.timeSinceSigning.info.banned=account was banned offerbook.timeSinceSigning.daysSinceSigning={0} days offerbook.timeSinceSigning.daysSinceSigning.long={0} since signing +offerbook.timeSinceSigning.tooltip.accountLimit=Account limit: {0} +offerbook.timeSinceSigning.tooltip.accountLimitLifted=Account limit lifted +offerbook.timeSinceSigning.tooltip.info.unsigned=This account hasn't been signed yet +offerbook.timeSinceSigning.tooltip.info.signed=This account has been signed +offerbook.timeSinceSigning.tooltip.info.signedAndLifted=This account has been signed and can sign peer accounts +offerbook.timeSinceSigning.tooltip.checkmark.buyBtc=buy BTC from a signed account +offerbook.timeSinceSigning.tooltip.checkmark.wait=wait a minimum of {0} days +offerbook.timeSinceSigning.tooltip.learnMore=Learn more offerbook.xmrAutoConf=Is auto-confirm enabled offerbook.timeSinceSigning.help=When you successfully complete a trade with a peer who has a signed payment account, your payment account is signed.\n\ @@ -2626,6 +2634,9 @@ disputeSummaryWindow.close.txDetails=Spending: {0}\n\ disputeSummaryWindow.close.noPayout.headline=Close without any payout disputeSummaryWindow.close.noPayout.text=Do you want to close without doing any payout? +disputeSummaryWindow.close.alreadyPaid.headline=Payout already done +disputeSummaryWindow.close.alreadyPaid.text=Restart the client to do another payout for this dispute + emptyWalletWindow.headline={0} emergency wallet tool emptyWalletWindow.info=Please use that only in emergency case if you cannot access your fund from the UI.\n\n\ Please note that all open offers will be closed automatically when using this tool.\n\n\ diff --git a/desktop/package/29CDFD3B.asc b/desktop/package/29CDFD3B.asc index 18f37a913f8..96cf1950601 100644 --- a/desktop/package/29CDFD3B.asc +++ b/desktop/package/29CDFD3B.asc @@ -12,40 +12,100 @@ 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 -NYeOG8Xnm4CxuWqUu0FDMv6BhkMCTz2X4xcnbtI= -=9LRS +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 +Bt9fU4wDD1lOCBjI1JG+aQ== +=SPaw -----END PGP PUBLIC KEY BLOCK----- diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index 87d187683a3..8b0b830f901 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -609,6 +609,11 @@ tree-table-view:focused { -fx-font-size: 13; } +.bold-text, +.bold-text .text { + -fx-font-weight: bold; +} + /* Splash */ #splash { -fx-background-color: -bs-background-color; @@ -1916,6 +1921,21 @@ textfield */ -fx-effect: dropshadow(gaussian, -bs-text-color-transparent-dark, 44, 0, -1, 3); } +.account-status-title { + -fx-font-size: 0.769em; + -fx-font-family: "IBM Plex Sans Medium"; +} + +.account-status-inactive-info-item { + -fx-text-fill: -bs-color-gray-dim; + -fx-fill: -bs-color-gray-dim; +} + +.account-status-active-info-item { + -fx-text-fill: -fx-accent; + -fx-fill: -fx-accent; +} + #price-feed-combo { -fx-background-color: none; } diff --git a/desktop/src/main/java/bisq/desktop/components/AccountStatusTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/AccountStatusTooltipLabel.java new file mode 100644 index 00000000000..03cb3f8c9f4 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/AccountStatusTooltipLabel.java @@ -0,0 +1,164 @@ +/* + * This file is part of Bisq. + * + * Bisq 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. + * + * Bisq 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 Bisq. If not, see . + */ + +package bisq.desktop.components; + +import bisq.desktop.components.controlsfx.control.PopOver; +import bisq.desktop.main.offer.offerbook.OfferBookListItem; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; + +import bisq.core.account.sign.SignedWitnessService; +import bisq.core.locale.Res; +import bisq.core.offer.OfferRestrictions; +import bisq.core.util.coin.CoinFormatter; + +import bisq.common.UserThread; + +import de.jensd.fx.glyphs.materialdesignicons.MaterialDesignIcon; + +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.layout.VBox; +import javafx.scene.text.Text; + +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Pos; + +import java.util.concurrent.TimeUnit; + + +public class AccountStatusTooltipLabel extends AutoTooltipLabel { + + public static final int DEFAULT_WIDTH = 300; + private final Node textIcon; + private final OfferBookListItem.WitnessAgeData witnessAgeData; + private final String popupTitle; + private PopOver popOver; + private boolean keepPopOverVisible = false; + + public AccountStatusTooltipLabel(OfferBookListItem.WitnessAgeData witnessAgeData, + CoinFormatter formatter) { + super(witnessAgeData.getDisplayString()); + this.witnessAgeData = witnessAgeData; + this.textIcon = FormBuilder.getIcon(witnessAgeData.getIcon()); + this.popupTitle = witnessAgeData.isLimitLifted() + ? Res.get("offerbook.timeSinceSigning.tooltip.accountLimitLifted") + : Res.get("offerbook.timeSinceSigning.tooltip.accountLimit", formatter.formatCoinWithCode(OfferRestrictions.TOLERATED_SMALL_TRADE_AMOUNT)); + + positionAndActivateIcon(); + } + + private void positionAndActivateIcon() { + textIcon.setOpacity(0.4); + textIcon.getStyleClass().add("tooltip-icon"); + popOver = createPopOver(); + textIcon.setOnMouseEntered(e -> showPopup(textIcon)); + + textIcon.setOnMouseExited(e -> UserThread.runAfter(() -> { + if (!keepPopOverVisible) { + popOver.hide(); + } + }, 200, TimeUnit.MILLISECONDS) + ); + + setGraphic(textIcon); + setContentDisplay(ContentDisplay.RIGHT); + } + + private PopOver createPopOver() { + Label titleLabel = new Label(popupTitle); + titleLabel.setMaxWidth(DEFAULT_WIDTH); + titleLabel.setWrapText(true); + titleLabel.setPadding(new Insets(10, 10, 0, 10)); + titleLabel.getStyleClass().add("account-status-title"); + + Label infoLabel = new Label(witnessAgeData.getInfo()); + infoLabel.setMaxWidth(DEFAULT_WIDTH); + infoLabel.setWrapText(true); + infoLabel.setPadding(new Insets(0, 10, 4, 10)); + infoLabel.getStyleClass().add("small-text"); + + Label buyLabel = createDetailsItem( + Res.get("offerbook.timeSinceSigning.tooltip.checkmark.buyBtc"), + witnessAgeData.isAccountSigned() + ); + Label waitLabel = createDetailsItem( + Res.get("offerbook.timeSinceSigning.tooltip.checkmark.wait", SignedWitnessService.SIGNER_AGE_DAYS), + witnessAgeData.isLimitLifted() + ); + + Hyperlink learnMoreLink = new ExternalHyperlink(Res.get("offerbook.timeSinceSigning.tooltip.learnMore"), + null, + "0.769em"); + learnMoreLink.setMaxWidth(DEFAULT_WIDTH); + learnMoreLink.setWrapText(true); + learnMoreLink.setPadding(new Insets(10, 10, 2, 10)); + learnMoreLink.getStyleClass().addAll("very-small-text"); + learnMoreLink.setOnAction((e) -> GUIUtil.openWebPage("https://bisq.wiki/Account_limits")); + + VBox vBox = new VBox(2, titleLabel, infoLabel, buyLabel, waitLabel, learnMoreLink); + vBox.setPadding(new Insets(2, 0, 2, 0)); + vBox.setAlignment(Pos.CENTER_LEFT); + + + PopOver popOver = new PopOver(vBox); + popOver.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); + + vBox.setOnMouseEntered(mouseEvent -> keepPopOverVisible = true); + + vBox.setOnMouseExited(mouseEvent -> { + keepPopOverVisible = false; + popOver.hide(); + }); + + return popOver; + } + + private void showPopup(Node textIcon) { + Bounds bounds = textIcon.localToScreen(textIcon.getBoundsInLocal()); + popOver.show(textIcon, bounds.getMaxX() + 10, (bounds.getMinY() + bounds.getHeight() / 2) - 10); + } + + private Label createDetailsItem(String text, boolean active) { + Label label = new Label(text); + label.setMaxWidth(DEFAULT_WIDTH); + label.setWrapText(true); + label.setPadding(new Insets(0, 10, 0, 10)); + label.getStyleClass().add("small-text"); + if (active) { + label.setStyle("-fx-text-fill: -fx-accent"); + } else { + label.setStyle("-fx-text-fill: -bs-color-gray-dim"); + } + + Text icon = FormBuilder.getSmallIconForLabel(active ? + MaterialDesignIcon.CHECKBOX_MARKED_CIRCLE : MaterialDesignIcon.CLOSE_CIRCLE, label); + icon.setLayoutY(4); + + if (active) { + icon.getStyleClass().add("account-status-active-info-item"); + } else { + icon.getStyleClass().add("account-status-inactive-info-item"); + } + + return label; + } +} diff --git a/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java b/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java index 62f7cf12106..a6ce7483c74 100644 --- a/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java +++ b/desktop/src/main/java/bisq/desktop/components/ExternalHyperlink.java @@ -28,4 +28,8 @@ public ExternalHyperlink(String text) { public ExternalHyperlink(String text, String style) { super(text, MaterialDesignIcon.LINK, style); } + + public ExternalHyperlink(String text, String style, String iconSize) { + super(text, MaterialDesignIcon.LINK, style, iconSize); + } } diff --git a/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java b/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java index b4a24825e03..b3738ee74d0 100644 --- a/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java +++ b/desktop/src/main/java/bisq/desktop/components/HyperlinkWithIcon.java @@ -59,10 +59,10 @@ public HyperlinkWithIcon(String text, GlyphIcons icon) { this(text, icon, null); } - public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { + public HyperlinkWithIcon(String text, GlyphIcons icon, String style, String iconSize) { super(text); - Text textIcon = FormBuilder.getIcon(icon); + Text textIcon = FormBuilder.getIcon(icon, iconSize); textIcon.setOpacity(0.7); textIcon.getStyleClass().addAll("hyperlink", "no-underline"); @@ -76,6 +76,10 @@ public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { setIcon(textIcon); } + public HyperlinkWithIcon(String text, GlyphIcons icon, String style) { + this(text, icon, style, "1.231em"); + } + public void hideIcon() { setGraphic(null); } diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java index 9593cbc4302..0b2c8790bde 100644 --- a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/PopOver.java @@ -1,5 +1,5 @@ -/** - * Copyright (c) 2013, ControlsFX +/* + * Copyright (c) 2013, 2016 ControlsFX * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -24,14 +24,26 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + package bisq.desktop.components.controlsfx.control; -import static java.util.Objects.requireNonNull; -import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; import bisq.desktop.components.controlsfx.skin.PopOverSkin; + import javafx.animation.FadeTransition; + +import javafx.stage.Window; +import javafx.stage.WindowEvent; + +import javafx.scene.Node; +import javafx.scene.control.Label; +import javafx.scene.control.PopupControl; +import javafx.scene.control.Skin; +import javafx.scene.layout.StackPane; + +import javafx.geometry.Bounds; +import javafx.geometry.Insets; + import javafx.beans.InvalidationListener; -import javafx.beans.Observable; import javafx.beans.WeakInvalidationListener; import javafx.beans.property.BooleanProperty; import javafx.beans.property.DoubleProperty; @@ -42,37 +54,47 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.beans.value.WeakChangeListener; + import javafx.event.EventHandler; -import javafx.geometry.Bounds; -import javafx.geometry.Insets; -import javafx.scene.Node; -import javafx.scene.control.Label; -import javafx.scene.control.PopupControl; -import javafx.scene.control.Skin; -import javafx.scene.input.MouseEvent; -import javafx.stage.Window; -import javafx.stage.WindowEvent; +import javafx.event.WeakEventHandler; + import javafx.util.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.util.Objects.requireNonNull; +import static javafx.scene.input.MouseEvent.MOUSE_CLICKED; + /** * The PopOver control provides detailed information about an owning node in a * popup window. The popup window has a very lightweight appearance (no default * window decorations) and an arrow pointing at the owner. Due to the nature of * popup windows the PopOver will move around with the parent window when the * user drags it.
- *

+ *
Screenshot of PopOver

* The PopOver can be detached from the owning node by dragging it away from the * owner. It stops displaying an arrow and starts displaying a title and a close * icon.
*
- *

+ *
Screenshot of a detached PopOver

* The following image shows a popover with an accordion content node. PopOver * controls are automatically resizing themselves when the content node changes * its size.
*
- *

+ *
Screenshot of PopOver containing an Accordion

+ * For styling apply stylesheets to the root pane of the PopOver. + * + *

Example:

+ * + *
+ * PopOver popOver = new PopOver();
+ * popOver.getRoot().getStylesheets().add(...);
+ * 
+ * */ public class PopOver extends PopupControl { @@ -84,6 +106,12 @@ public class PopOver extends PopupControl { private double targetY; + private final SimpleBooleanProperty animated = new SimpleBooleanProperty(true); + private final ObjectProperty fadeInDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); + private final ObjectProperty fadeOutDuration = new SimpleObjectProperty<>(DEFAULT_FADE_DURATION); + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + /** * Creates a pop over with a label as the content node. */ @@ -92,30 +120,24 @@ public PopOver() { getStyleClass().add(DEFAULT_STYLE_CLASS); + getRoot().getStylesheets().add( + requireNonNull(PopOver.class.getResource("popover.css")).toExternalForm()); //$NON-NLS-1$ + setAnchorLocation(AnchorLocation.WINDOW_TOP_LEFT); - setOnHiding(new EventHandler() { - @Override - public void handle(WindowEvent evt) { - setDetached(false); - } - }); + setOnHiding(evt -> setDetached(false)); /* * Create some initial content. */ - Label label = new Label(""); //$NON-NLS-1$ + Label label = new Label("No content set"); //$NON-NLS-1$ label.setPrefSize(200, 200); label.setPadding(new Insets(4)); setContentNode(label); - ChangeListener repositionListener = new ChangeListener() { - @Override - public void changed(ObservableValue value, - Object oldObject, Object newObject) { - if (isShowing() && !isDetached()) { - show(getOwnerNode(), targetX, targetY); - adjustWindowLocation(); - } + InvalidationListener repositionListener = observable -> { + if (isShowing() && !isDetached()) { + show(getOwnerNode(), targetX, targetY); + adjustWindowLocation(); } }; @@ -123,12 +145,21 @@ public void changed(ObservableValue value, cornerRadius.addListener(repositionListener); arrowLocation.addListener(repositionListener); arrowIndent.addListener(repositionListener); + headerAlwaysVisible.addListener(repositionListener); + + /* + * A detached popover should of course not automatically hide itself. + */ + detached.addListener(it -> setAutoHide(!isDetached())); + + setAutoHide(true); } /** * Creates a pop over with the given node as the content node. * - * @param content The content shown by the pop over + * @param content + * The content shown by the pop over */ public PopOver(Node content) { this(); @@ -141,9 +172,28 @@ protected Skin createDefaultSkin() { return new PopOverSkin(this); } + private final StackPane root = new StackPane(); + + /** + * The root pane stores the content node of the popover. It is accessible + * via this method in order to support proper styling. + * + *

Example:

+ * + *
+     * PopOver popOver = new PopOver();
+     * popOver.getRoot().getStylesheets().add(...);
+     * 
+ * + * @return the root pane + */ + public final StackPane getRoot() { + return root; + } + // Content support. - private final ObjectProperty contentNode = new SimpleObjectProperty( + private final ObjectProperty contentNode = new SimpleObjectProperty<>( this, "contentNode") { //$NON-NLS-1$ @Override public void setValue(Node node) { @@ -151,7 +201,8 @@ public void setValue(Node node) { throw new IllegalArgumentException( "content node can not be null"); //$NON-NLS-1$ } - }; + } + }; /** @@ -186,41 +237,36 @@ public final void setContentNode(Node content) { contentNodeProperty().set(content); } - private InvalidationListener hideListener = new InvalidationListener() { - @Override - public void invalidated(Observable observable) { - if (!isDetached()) { - hide(Duration.ZERO); - } + private final InvalidationListener hideListener = observable -> { + if (!isDetached()) { + hide(Duration.ZERO); } }; - private WeakInvalidationListener weakHideListener = new WeakInvalidationListener( + private final WeakInvalidationListener weakHideListener = new WeakInvalidationListener( hideListener); - private ChangeListener xListener = new ChangeListener() { - @Override - public void changed(ObservableValue value, - Number oldX, Number newX) { - setX(getX() + (newX.doubleValue() - oldX.doubleValue())); + private final ChangeListener xListener = (value, oldX, newX) -> { + if (!isDetached()) { + setAnchorX(getAnchorX() + (newX.doubleValue() - oldX.doubleValue())); } }; - private WeakChangeListener weakXListener = new WeakChangeListener<>( + private final WeakChangeListener weakXListener = new WeakChangeListener<>( xListener); - private ChangeListener yListener = new ChangeListener() { - @Override - public void changed(ObservableValue value, - Number oldY, Number newY) { - setY(getY() + (newY.doubleValue() - oldY.doubleValue())); + private final ChangeListener yListener = (value, oldY, newY) -> { + if (!isDetached()) { + setAnchorY(getAnchorY() + (newY.doubleValue() - oldY.doubleValue())); } }; - private WeakChangeListener weakYListener = new WeakChangeListener<>( + private final WeakChangeListener weakYListener = new WeakChangeListener<>( yListener); private Window ownerWindow; + private final EventHandler closePopOverOnOwnerWindowCloseLambda = event -> ownerWindowClosing(); + private final WeakEventHandler closePopOverOnOwnerWindowClose = new WeakEventHandler<>(closePopOverOnOwnerWindowCloseLambda); /** * Shows the pop over in a position relative to the edges of the given owner @@ -286,6 +332,38 @@ public final void show(Node owner, double offset) { } } + /** {@inheritDoc} */ + @Override + public final void show(Window owner) { + super.show(owner); + ownerWindow = owner; + + if (isAnimated()) { + showFadeInAnimation(getFadeInDuration()); + } + + ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } + + /** {@inheritDoc} */ + @Override + public final void show(Window ownerWindow, double anchorX, double anchorY) { + super.show(ownerWindow, anchorX, anchorY); + this.ownerWindow = ownerWindow; + + if (isAnimated()) { + showFadeInAnimation(getFadeInDuration()); + } + + ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } + /** * Makes the pop over visible at the give location and associates it with * the given owner node. The x and y coordinate will be the target location @@ -300,7 +378,7 @@ public final void show(Node owner, double offset) { */ @Override public final void show(Node owner, double x, double y) { - show(owner, x, y, DEFAULT_FADE_DURATION); + show(owner, x, y, getFadeInDuration()); } /** @@ -315,14 +393,14 @@ public final void show(Node owner, double x, double y) { * @param y * the y coordinate for the pop over arrow tip * @param fadeInDuration - * the time it takes for the pop over to be fully visible + * the time it takes for the pop over to be fully visible. This duration takes precedence over the fade-in property without setting. */ public final void show(Node owner, double x, double y, Duration fadeInDuration) { /* - * Calling show() a second time without first closing the - * pop over causes it to be placed at the wrong location. + * Calling show() a second time without first closing the pop over + * causes it to be placed at the wrong location. */ if (ownerWindow != null && isShowing()) { super.hide(); @@ -360,18 +438,15 @@ public final void show(Node owner, double x, double y, /* * The user clicked somewhere into the transparent background. If - * this is the case the hide the window (when attached). + * this is the case then hide the window (when attached). */ - getScene().addEventHandler(MOUSE_CLICKED, - new EventHandler() { - public void handle(MouseEvent evt) { - if (evt.getTarget().equals(getScene().getRoot())) { - if (!isDetached()) { - hide(); - } - } - }; - }); + getScene().addEventHandler(MOUSE_CLICKED, mouseEvent -> { + if (mouseEvent.getTarget().equals(getScene().getRoot())) { + if (!isDetached()) { + hide(); + } + } + }); /* * Move the window so that the arrow will end up pointing at the @@ -382,6 +457,18 @@ public void handle(MouseEvent evt) { super.show(owner, x, y); + if (isAnimated()) { + showFadeInAnimation(fadeInDuration); + } + + // Bug fix - close popup when owner window is closing + ownerWindow.addEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.addEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } + + private void showFadeInAnimation(Duration fadeInDuration) { // Fade In Node skinNode = getSkin().getNode(); skinNode.setOpacity(0); @@ -392,6 +479,10 @@ public void handle(MouseEvent evt) { fadeIn.play(); } + private void ownerWindowClosing() { + hide(Duration.ZERO); + } + /** * Hides the pop over by quickly changing its opacity to 0. * @@ -399,7 +490,7 @@ public void handle(MouseEvent evt) { */ @Override public final void hide() { - hide(DEFAULT_FADE_DURATION); + hide(getFadeOutDuration()); } /** @@ -411,21 +502,32 @@ public final void hide() { * @since 1.0 */ public final void hide(Duration fadeOutDuration) { + log.info("hide:" + fadeOutDuration.toString()); + //We must remove EventFilter in order to prevent memory leak. + if (ownerWindow != null) { + ownerWindow.removeEventFilter(WindowEvent.WINDOW_CLOSE_REQUEST, + closePopOverOnOwnerWindowClose); + ownerWindow.removeEventFilter(WindowEvent.WINDOW_HIDING, + closePopOverOnOwnerWindowClose); + } if (fadeOutDuration == null) { fadeOutDuration = DEFAULT_FADE_DURATION; } if (isShowing()) { - // Fade Out - Node skinNode = getSkin().getNode(); - skinNode.setOpacity(0); - - FadeTransition fadeOut = new FadeTransition(fadeOutDuration, - skinNode); - fadeOut.setFromValue(1); - fadeOut.setToValue(0); - fadeOut.setOnFinished(evt -> super.hide()); - fadeOut.play(); + if (isAnimated()) { + // Fade Out + Node skinNode = getSkin().getNode(); + + FadeTransition fadeOut = new FadeTransition(fadeOutDuration, + skinNode); + fadeOut.setFromValue(skinNode.getOpacity()); + fadeOut.setToValue(0); + fadeOut.setOnFinished(evt -> super.hide()); + fadeOut.play(); + } else { + super.hide(); + } } } @@ -436,26 +538,26 @@ private void adjustWindowLocation() { case TOP_CENTER: case TOP_LEFT: case TOP_RIGHT: - setX(getX() + bounds.getMinX() - computeXOffset()); - setY(getY() + bounds.getMinY() + getArrowSize()); + setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset()); + setAnchorY(getAnchorY() + bounds.getMinY() + getArrowSize()); break; case LEFT_TOP: case LEFT_CENTER: case LEFT_BOTTOM: - setX(getX() + bounds.getMinX() + getArrowSize()); - setY(getY() + bounds.getMinY() - computeYOffset()); + setAnchorX(getAnchorX() + bounds.getMinX() + getArrowSize()); + setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset()); break; case BOTTOM_CENTER: case BOTTOM_LEFT: case BOTTOM_RIGHT: - setX(getX() + bounds.getMinX() - computeXOffset()); - setY(getY() - bounds.getMinY() - bounds.getMaxY() - 1); + setAnchorX(getAnchorX() + bounds.getMinX() - computeXOffset()); + setAnchorY(getAnchorY() - bounds.getMinY() - bounds.getMaxY() - 1); break; case RIGHT_TOP: case RIGHT_BOTTOM: case RIGHT_CENTER: - setX(getX() - bounds.getMinX() - bounds.getMaxX() - 1); - setY(getY() + bounds.getMinY() - computeYOffset()); + setAnchorX(getAnchorX() - bounds.getMinX() - bounds.getMaxX() - 1); + setAnchorY(getAnchorY() + bounds.getMinY() - computeYOffset()); break; } } @@ -508,6 +610,74 @@ public final void detach() { } } + // always show header + + private final BooleanProperty headerAlwaysVisible = new SimpleBooleanProperty(this, "headerAlwaysVisible"); //$NON-NLS-1$ + + /** + * Determines whether or not the {@link PopOver} header should remain visible, even while attached. + */ + public final BooleanProperty headerAlwaysVisibleProperty() { + return headerAlwaysVisible; + } + + /** + * Sets the value of the headerAlwaysVisible property. + * + * @param visible + * if true, then the header is visible even while attached + * + * @see #headerAlwaysVisibleProperty() + */ + public final void setHeaderAlwaysVisible(boolean visible) { + headerAlwaysVisible.setValue(visible); + } + + /** + * Returns the value of the detachable property. + * + * @return true if the header is visible even while attached + * + * @see #headerAlwaysVisibleProperty() + */ + public final boolean isHeaderAlwaysVisible() { + return headerAlwaysVisible.getValue(); + } + + // enable close button + + private final BooleanProperty closeButtonEnabled = new SimpleBooleanProperty(this, "closeButtonEnabled", true); //$NON-NLS-1$ + + /** + * Determines whether or not the header's close button should be available. + */ + public final BooleanProperty closeButtonEnabledProperty() { + return closeButtonEnabled; + } + + /** + * Sets the value of the closeButtonEnabled property. + * + * @param enabled + * if false, the pop over will not be closeable by the header's close button + * + * @see #closeButtonEnabledProperty() + */ + public final void setCloseButtonEnabled(boolean enabled) { + closeButtonEnabled.setValue(enabled); + } + + /** + * Returns the value of the closeButtonEnabled property. + * + * @return true if the header's close button is enabled + * + * @see #closeButtonEnabledProperty() + */ + public final boolean isCloseButtonEnabled() { + return closeButtonEnabled.getValue(); + } + // detach support private final BooleanProperty detachable = new SimpleBooleanProperty(this, @@ -561,7 +731,7 @@ public final BooleanProperty detachedProperty() { * Sets the value of the detached property. * * @param detached - * if true the pop over will change its appearance to "detached" + * if true the pop over will change its apperance to "detached" * mode * * @see #detachedProperty() @@ -701,46 +871,42 @@ public final void setCornerRadius(double radius) { // Detached stage title - private final StringProperty detachedTitle = new SimpleStringProperty(this, - "detachedTitle", "Info"); //$NON-NLS-1$ //$NON-NLS-2$ + private final StringProperty title = new SimpleStringProperty(this, "title", "No title set"); //$NON-NLS-1$ //$NON-NLS-2$ /** - * Stores the title to display when the pop over becomes detached. + * Stores the title to display in the PopOver's header. * - * @return the detached title property + * @return the title property */ - public final StringProperty detachedTitleProperty() { - return detachedTitle; + public final StringProperty titleProperty() { + return title; } /** - * Returns the value of the detached title property. + * Returns the value of the title property. * * @return the detached title - * - * @see #detachedTitleProperty() + * @see #titleProperty() */ - public final String getDetachedTitle() { - return detachedTitleProperty().get(); + public final String getTitle() { + return titleProperty().get(); } /** - * Sets the value of the detached title property. + * Sets the value of the title property. * - * @param title - * the title to use when detached - * - * @see #detachedTitleProperty() + * @param title the title to use when detached + * @see #titleProperty() */ - public final void setDetachedTitle(String title) { + public final void setTitle(String title) { if (title == null) { throw new IllegalArgumentException("title can not be null"); //$NON-NLS-1$ } - detachedTitleProperty().set(title); + titleProperty().set(title); } - private final ObjectProperty arrowLocation = new SimpleObjectProperty( + private final ObjectProperty arrowLocation = new SimpleObjectProperty<>( this, "arrowLocation", ArrowLocation.LEFT_TOP); //$NON-NLS-1$ /** @@ -782,6 +948,93 @@ public final ArrowLocation getArrowLocation() { * All possible arrow locations. */ public enum ArrowLocation { - LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT; + LEFT_TOP, LEFT_CENTER, LEFT_BOTTOM, RIGHT_TOP, RIGHT_CENTER, RIGHT_BOTTOM, TOP_LEFT, TOP_CENTER, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_CENTER, BOTTOM_RIGHT + } + + /** + * Stores the fade-in duration. This should be set before calling PopOver.show(..). + * + * @return the fade-in duration property + */ + public final ObjectProperty fadeInDurationProperty() { + return fadeInDuration; + } + + /** + * Stores the fade-out duration. + * + * @return the fade-out duration property + */ + public final ObjectProperty fadeOutDurationProperty() { + return fadeOutDuration; + } + + /** + * Returns the value of the fade-in duration property. + * + * @return the fade-in duration + * @see #fadeInDurationProperty() + */ + public final Duration getFadeInDuration() { + return fadeInDurationProperty().get(); + } + + /** + * Sets the value of the fade-in duration property. This should be set before calling PopOver.show(..). + * + * @param duration the requested fade-in duration + * @see #fadeInDurationProperty() + */ + public final void setFadeInDuration(Duration duration) { + fadeInDurationProperty().setValue(duration); + } + + /** + * Returns the value of the fade-out duration property. + * + * @return the fade-out duration + * @see #fadeOutDurationProperty() + */ + public final Duration getFadeOutDuration() { + return fadeOutDurationProperty().get(); + } + + /** + * Sets the value of the fade-out duration property. + * + * @param duration the requested fade-out duration + * @see #fadeOutDurationProperty() + */ + public final void setFadeOutDuration(Duration duration) { + fadeOutDurationProperty().setValue(duration); + } + + /** + * Stores the "animated" flag. If true then the PopOver will be shown / hidden with a short fade in / out animation. + * + * @return the "animated" property + */ + public final BooleanProperty animatedProperty() { + return animated; + } + + /** + * Returns the value of the "animated" property. + * + * @return true if the PopOver will be shown and hidden with a short fade animation + * @see #animatedProperty() + */ + public final boolean isAnimated() { + return animatedProperty().get(); + } + + /** + * Sets the value of the "animated" property. + * + * @param animated if true the PopOver will be shown and hidden with a short fade animation + * @see #animatedProperty() + */ + public final void setAnimated(boolean animated) { + animatedProperty().set(animated); } } diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css index ccc6d5e42f9..79f946fe6e3 100644 --- a/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/control/popover.css @@ -3,10 +3,10 @@ } .popover > .border { - -fx-stroke: linear-gradient(to bottom, rgba(0,0,0, .3), rgba(0, 0, 0, .7)) ; - -fx-stroke-width: 0.5; - -fx-fill: rgba(255.0,255.0,255.0, .95); - -fx-effect: dropshadow(gaussian, rgba(0,0,0,.2), 10.0, 0.5, 2.0, 2.0); + -fx-stroke: linear-gradient(to bottom, rgba(0, 0, 0, .3), rgba(0, 0, 0, .7)); + -fx-stroke-width: 1; + -fx-fill: rgba(255.0, 255.0, 255.0, .95); + -fx-effect: dropshadow(gaussian, rgba(0, 0, 0, .2), 10.0, 0.5, 2.0, 2.0); } .popover > .content { @@ -15,7 +15,7 @@ .popover > .detached { } -.popover > .content > .title > .text { +.popover > .content > .title > .text { -fx-padding: 6.0 6.0 0.0 6.0; -fx-text-fill: rgba(120, 120, 120, .8); -fx-font-weight: bold; @@ -26,11 +26,11 @@ } .popover > .content > .title > .icon > .graphics > .circle { - -fx-fill: gray ; - -fx-effect: innershadow(gaussian, rgba(0,0,0,.2), 3, 0.5, 1.0, 1.0); + -fx-fill: gray; + -fx-effect: innershadow(gaussian, rgba(0, 0, 0, .2), 3, 0.5, 1.0, 1.0); } .popover > .content > .title > .icon > .graphics > .line { - -fx-stroke: white ; + -fx-stroke: white; -fx-stroke-width: 2; } diff --git a/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java b/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java index 3ebdc843bf2..e449f847a1a 100644 --- a/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java +++ b/desktop/src/main/java/bisq/desktop/components/controlsfx/skin/PopOverSkin.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2013 - 2015, ControlsFX * All rights reserved. * @@ -26,24 +26,11 @@ */ package bisq.desktop.components.controlsfx.skin; -import static java.lang.Double.MAX_VALUE; -import static javafx.geometry.Pos.CENTER_LEFT; -import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY; -import static bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation.*; +import bisq.desktop.components.controlsfx.control.PopOver; +import bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation; -import java.util.ArrayList; -import java.util.List; +import javafx.stage.Window; -import javafx.beans.InvalidationListener; -import javafx.beans.Observable; -import javafx.beans.binding.Bindings; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.event.EventHandler; -import javafx.geometry.Point2D; -import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.control.Label; @@ -60,10 +47,28 @@ import javafx.scene.shape.PathElement; import javafx.scene.shape.QuadCurveTo; import javafx.scene.shape.VLineTo; -import javafx.stage.Window; -import bisq.desktop.components.controlsfx.control.PopOver; -import bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation; +import javafx.geometry.Point2D; +import javafx.geometry.Pos; + +import javafx.beans.InvalidationListener; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +import javafx.event.EventHandler; + +import java.util.ArrayList; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static bisq.desktop.components.controlsfx.control.PopOver.ArrowLocation.*; +import static java.lang.Double.MAX_VALUE; +import static javafx.geometry.Pos.CENTER_LEFT; +import static javafx.scene.control.ContentDisplay.GRAPHIC_ONLY; +import static javafx.scene.paint.Color.YELLOW; public class PopOverSkin implements Skin { @@ -74,27 +79,27 @@ public class PopOverSkin implements Skin { private boolean tornOff; - private Label title; - private Label closeIcon; + private final Path path; + private final Path clip; - private Path path; - private BorderPane content; - private StackPane titlePane; - private StackPane stackPane; + private final BorderPane content; + private final StackPane titlePane; + private final StackPane stackPane; private Point2D dragStartLocation; - private PopOver popOver; + private final PopOver popOver; + + private final Logger log = LoggerFactory.getLogger(this.getClass()); public PopOverSkin(final PopOver popOver) { this.popOver = popOver; - stackPane = new StackPane(); - stackPane.getStylesheets().add( - PopOver.class.getResource("popover.css").toExternalForm()); //$NON-NLS-1$ + stackPane = popOver.getRoot(); stackPane.setPickOnBounds(false); - stackPane.getStyleClass().add("popover"); //$NON-NLS-1$ + + Bindings.bindContent(stackPane.getStyleClass(), popOver.getStyleClass()); /* * The min width and height equal 2 * corner radius + 2 * arrow indent + @@ -110,26 +115,22 @@ public PopOverSkin(final PopOver popOver) { stackPane.minHeightProperty().bind(stackPane.minWidthProperty()); - title = new Label(); - title.textProperty().bind(popOver.detachedTitleProperty()); + Label title = new Label(); + title.textProperty().bind(popOver.titleProperty()); title.setMaxSize(MAX_VALUE, MAX_VALUE); title.setAlignment(Pos.CENTER); title.getStyleClass().add("text"); //$NON-NLS-1$ - closeIcon = new Label(); + Label closeIcon = new Label(); closeIcon.setGraphic(createCloseIcon()); closeIcon.setMaxSize(MAX_VALUE, MAX_VALUE); closeIcon.setContentDisplay(GRAPHIC_ONLY); - closeIcon.visibleProperty().bind(popOver.detachedProperty()); + closeIcon.visibleProperty().bind( + popOver.closeButtonEnabledProperty().and( + popOver.detachedProperty().or(popOver.headerAlwaysVisibleProperty()))); closeIcon.getStyleClass().add("icon"); //$NON-NLS-1$ closeIcon.setAlignment(CENTER_LEFT); - closeIcon.getGraphic().setOnMouseClicked( - new EventHandler() { - @Override - public void handle(MouseEvent evt) { - popOver.hide(); - } - }); + closeIcon.getGraphic().setOnMouseClicked(evt -> popOver.hide()); titlePane = new StackPane(); titlePane.getChildren().add(title); @@ -140,105 +141,125 @@ public void handle(MouseEvent evt) { content.setCenter(popOver.getContentNode()); content.getStyleClass().add("content"); //$NON-NLS-1$ - if (popOver.isDetached()) { + if (popOver.isDetached() || popOver.isHeaderAlwaysVisible()) { content.setTop(titlePane); + } + + if (popOver.isDetached()) { popOver.getStyleClass().add(DETACHED_STYLE_CLASS); content.getStyleClass().add(DETACHED_STYLE_CLASS); } - InvalidationListener updatePathListener = new InvalidationListener() { - - @Override - public void invalidated(Observable observable) { - updatePath(); + popOver.headerAlwaysVisibleProperty().addListener((o, oV, isVisible) -> { + if (isVisible) { + content.setTop(titlePane); + } else if (!popOver.isDetached()) { + content.setTop(null); } - }; + }); + InvalidationListener updatePathListener = observable -> updatePath(); getPopupWindow().xProperty().addListener(updatePathListener); getPopupWindow().yProperty().addListener(updatePathListener); - popOver.arrowLocationProperty().addListener(updatePathListener); + popOver.contentNodeProperty().addListener( + (value, oldContent, newContent) -> content + .setCenter(newContent)); + popOver.detachedProperty() + .addListener((value, oldDetached, newDetached) -> { + + if (newDetached) { + popOver.getStyleClass().add(DETACHED_STYLE_CLASS); + content.getStyleClass().add(DETACHED_STYLE_CLASS); + content.setTop(titlePane); + + switch (getSkinnable().getArrowLocation()) { + case LEFT_TOP: + case LEFT_CENTER: + case LEFT_BOTTOM: + popOver.setAnchorX( + popOver.getAnchorX() + popOver.getArrowSize()); + break; + case TOP_LEFT: + case TOP_CENTER: + case TOP_RIGHT: + popOver.setAnchorY( + popOver.getAnchorY() + popOver.getArrowSize()); + break; + default: + break; + } + } else { + popOver.getStyleClass().remove(DETACHED_STYLE_CLASS); + content.getStyleClass().remove(DETACHED_STYLE_CLASS); + + if (!popOver.isHeaderAlwaysVisible()) { + content.setTop(null); + } + } - popOver.contentNodeProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue value, - Node oldContent, Node newContent) { - content.setCenter(newContent); - } - }); + popOver.sizeToScene(); - popOver.detachedProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue value, - Boolean oldDetached, Boolean newDetached) { - - updatePath(); - - if (newDetached) { - popOver.getStyleClass().add(DETACHED_STYLE_CLASS); - content.getStyleClass().add(DETACHED_STYLE_CLASS); - content.setTop(titlePane); - } else { - popOver.getStyleClass().remove(DETACHED_STYLE_CLASS); - content.getStyleClass().remove(DETACHED_STYLE_CLASS); - content.setTop(null); - } - } - }); + updatePath(); + }); path = new Path(); path.getStyleClass().add("border"); //$NON-NLS-1$ path.setManaged(false); + clip = new Path(); + + /* + * The clip is a path and the path has to be filled with a color. + * Otherwise clipping will not work. + */ + clip.setFill(YELLOW); + createPathElements(); updatePath(); - final EventHandler mousePressedHandler = new EventHandler() { - public void handle(MouseEvent evt) { - if (popOver.isDetachable() || popOver.isDetached()) { - tornOff = false; + final EventHandler mousePressedHandler = evt -> { + log.info("mousePressed:" + popOver.isDetachable() + "," + popOver.isDetached()); + if (popOver.isDetachable() || popOver.isDetached()) { + tornOff = false; - xOffset = evt.getScreenX(); - yOffset = evt.getScreenY(); + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); - dragStartLocation = new Point2D(xOffset, yOffset); - } - }; + dragStartLocation = new Point2D(xOffset, yOffset); + } }; - final EventHandler mouseReleasedHandler = new EventHandler() { - public void handle(MouseEvent evt) { - if (tornOff && !getSkinnable().isDetached()) { - tornOff = false; - getSkinnable().detach(); - } - }; + final EventHandler mouseReleasedHandler = evt -> { + log.info("mouseReleased:tornOff" + tornOff + ", " + !getSkinnable().isDetached()); + if (tornOff && !getSkinnable().isDetached()) { + tornOff = false; + getSkinnable().detach(); + } }; - final EventHandler mouseDragHandler = new EventHandler() { - - public void handle(MouseEvent evt) { - if (popOver.isDetachable() || popOver.isDetached()) { - double deltaX = evt.getScreenX() - xOffset; - double deltaY = evt.getScreenY() - yOffset; + final EventHandler mouseDragHandler = evt -> { + log.info("mouseDrag:" + popOver.isDetachable() + "," + popOver.isDetached()); + if (popOver.isDetachable() || popOver.isDetached()) { + double deltaX = evt.getScreenX() - xOffset; + double deltaY = evt.getScreenY() - yOffset; - Window window = getSkinnable().getScene().getWindow(); + Window window = getSkinnable().getScene().getWindow(); - window.setX(window.getX() + deltaX); - window.setY(window.getY() + deltaY); + window.setX(window.getX() + deltaX); + window.setY(window.getY() + deltaY); - xOffset = evt.getScreenX(); - yOffset = evt.getScreenY(); + xOffset = evt.getScreenX(); + yOffset = evt.getScreenY(); - if (dragStartLocation.distance(xOffset, yOffset) > 20) { - tornOff = true; - updatePath(); - } else if (tornOff) { - tornOff = false; - updatePath(); - } + if (dragStartLocation.distance(xOffset, yOffset) > 20) { + tornOff = true; + updatePath(); + } else if (tornOff) { + tornOff = false; + updatePath(); } - }; + } }; stackPane.setOnMousePressed(mousePressedHandler); @@ -247,6 +268,8 @@ public void handle(MouseEvent evt) { stackPane.getChildren().add(path); stackPane.getChildren().add(content); + + content.setClip(clip); } @Override @@ -689,5 +712,6 @@ private void updatePath() { elements.add(topCurveTo); path.getElements().setAll(elements); + clip.getElements().setAll(elements); } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java index 80a986e16b5..dcb9f05495e 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookListItem.java @@ -40,6 +40,8 @@ import lombok.Value; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + import javax.annotation.Nullable; @Slf4j @@ -65,7 +67,7 @@ public class OfferBookListItem { // A -1 value indicates the seq-no has not been set by onAdded or onRemoved // since the most recent OfferBook view refresh. @Getter - private int sequenceNumber; + private final int sequenceNumber; // We cache the data once created for performance reasons. AccountAgeWitnessService calls can // be a bit expensive. @@ -76,7 +78,7 @@ public OfferBookListItem(Offer offer) { } public OfferBookListItem(Offer offer, - P2PDataStorage.ByteArray hashOfPayload, + @Nullable P2PDataStorage.ByteArray hashOfPayload, int sequenceNumber) { this.offer = offer; this.hashOfPayload = hashOfPayload; @@ -86,54 +88,40 @@ public OfferBookListItem(Offer offer, public WitnessAgeData getWitnessAgeData(AccountAgeWitnessService accountAgeWitnessService, SignedWitnessService signedWitnessService) { if (witnessAgeData == null) { - long ageInMs; - long daysSinceSignedAsLong = -1; - long accountAgeDaysAsLong = -1; - long accountAgeDaysNotYetSignedAsLong = -1; - String displayString; - String info; - GlyphIcons icon; - if (CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode())) { - // Altcoins - displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed"); - info = Res.get("shared.notSigned.noNeedAlts"); - icon = MaterialDesignIcon.INFORMATION_OUTLINE; + witnessAgeData = new WitnessAgeData(WitnessAgeData.TYPE_ALTCOINS); } else if (PaymentMethod.hasChargebackRisk(offer.getPaymentMethod(), offer.getCurrencyCode())) { // Fiat and signed witness required Optional optionalWitness = accountAgeWitnessService.findWitness(offer); - AccountAgeWitnessService.SignState signState = optionalWitness.map(accountAgeWitnessService::getSignState) + AccountAgeWitnessService.SignState signState = optionalWitness + .map(accountAgeWitnessService::getSignState) .orElse(AccountAgeWitnessService.SignState.UNSIGNED); - boolean isSignedAccountAgeWitness = optionalWitness.map(signedWitnessService::isSignedAccountAgeWitness) + + boolean isSignedAccountAgeWitness = optionalWitness + .map(signedWitnessService::isSignedAccountAgeWitness) .orElse(false); + if (isSignedAccountAgeWitness || !signState.equals(AccountAgeWitnessService.SignState.UNSIGNED)) { // either signed & limits lifted, or waiting for limits to be lifted // Or banned - daysSinceSignedAsLong = TimeUnit.MILLISECONDS.toDays(optionalWitness.map(witness -> - accountAgeWitnessService.getWitnessSignAge(witness, new Date())) - .orElse(0L)); - displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", daysSinceSignedAsLong); - info = Res.get("offerbook.timeSinceSigning.info", signState.getDisplayString()); + witnessAgeData = new WitnessAgeData( + signState.isLimitLifted() ? WitnessAgeData.TYPE_SIGNED_AND_LIMIT_LIFTED : WitnessAgeData.TYPE_SIGNED_OR_BANNED, + optionalWitness.map(witness -> accountAgeWitnessService.getWitnessSignAge(witness, new Date())).orElse(0L), + signState); } else { - // Unsigned case - ageInMs = optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date())) - .orElse(-1L); - accountAgeDaysNotYetSignedAsLong = ageInMs > -1 ? TimeUnit.MILLISECONDS.toDays(ageInMs) : 0; - displayString = Res.get("offerbook.timeSinceSigning.notSigned"); - info = Res.get("shared.notSigned", accountAgeDaysNotYetSignedAsLong); + witnessAgeData = new WitnessAgeData( + WitnessAgeData.TYPE_NOT_SIGNED, + optionalWitness.map(e -> accountAgeWitnessService.getAccountAge(e, new Date())).orElse(0L), + signState + ); } - - icon = GUIUtil.getIconForSignState(signState); } else { // Fiat, no signed witness required, we show account age - ageInMs = accountAgeWitnessService.getAccountAge(offer); - accountAgeDaysAsLong = ageInMs > -1 ? TimeUnit.MILLISECONDS.toDays(ageInMs) : 0; - displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", accountAgeDaysAsLong); - info = Res.get("shared.notSigned.noNeedDays", accountAgeDaysAsLong); - icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE; + witnessAgeData = new WitnessAgeData( + WitnessAgeData.TYPE_NOT_SIGNING_REQUIRED, + accountAgeWitnessService.getAccountAge(offer) + ); } - - witnessAgeData = new WitnessAgeData(displayString, info, icon, daysSinceSignedAsLong, accountAgeDaysNotYetSignedAsLong, accountAgeDaysAsLong); } return witnessAgeData; } @@ -149,49 +137,73 @@ public String toString() { } @Value - public static class WitnessAgeData { - private final String displayString; - private final String info; - private final GlyphIcons icon; - private final Long daysSinceSignedAsLong; - private final long accountAgeDaysNotYetSignedAsLong; - private final Long accountAgeDaysAsLong; + public static class WitnessAgeData implements Comparable { + String displayString; + String info; + GlyphIcons icon; // Used for sorting - private final Long type; + Long type; // Used for sorting - private final Long days; - - public WitnessAgeData(String displayString, - String info, - GlyphIcons icon, - long daysSinceSignedAsLong, - long accountAgeDaysNotYetSignedAsLong, - long accountAgeDaysAsLong) { - this.displayString = displayString; - this.info = info; - this.icon = icon; - this.daysSinceSignedAsLong = daysSinceSignedAsLong; - this.accountAgeDaysNotYetSignedAsLong = accountAgeDaysNotYetSignedAsLong; - this.accountAgeDaysAsLong = accountAgeDaysAsLong; - - if (daysSinceSignedAsLong > -1) { - // First we show signed accounts sorted by days - this.type = 3L; - this.days = daysSinceSignedAsLong; - } else if (accountAgeDaysNotYetSignedAsLong > -1) { - // Next group is not yet signed accounts sorted by account age - this.type = 2L; - this.days = accountAgeDaysNotYetSignedAsLong; - } else if (accountAgeDaysAsLong > -1) { - // Next group is not signing required accounts sorted by account age - this.type = 1L; - this.days = accountAgeDaysAsLong; + Long days; + + public static final long TYPE_SIGNED_AND_LIMIT_LIFTED = 4L; + public static final long TYPE_SIGNED_OR_BANNED = 3L; + public static final long TYPE_NOT_SIGNED = 2L; + public static final long TYPE_NOT_SIGNING_REQUIRED = 1L; + public static final long TYPE_ALTCOINS = 0L; + + public WitnessAgeData(long type) { + this(type, 0, null); + } + + public WitnessAgeData(long type, long days) { + this(type, days, null); + } + + public WitnessAgeData(long type, long age, AccountAgeWitnessService.SignState signState) { + this.type = type; + long days = age > -1 ? TimeUnit.MILLISECONDS.toDays(age) : 0; + this.days = days; + + if (type == TYPE_SIGNED_AND_LIMIT_LIFTED) { + this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days); + this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signedAndLifted"); + this.icon = GUIUtil.getIconForSignState(signState); + } else if (type == TYPE_SIGNED_OR_BANNED) { + this.displayString = Res.get("offerbook.timeSinceSigning.daysSinceSigning", days); + this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.signed"); + this.icon = GUIUtil.getIconForSignState(signState); + } else if (type == TYPE_NOT_SIGNED) { + this.displayString = Res.get("offerbook.timeSinceSigning.notSigned"); + this.info = Res.get("offerbook.timeSinceSigning.tooltip.info.unsigned"); + this.icon = GUIUtil.getIconForSignState(signState); + } else if (type == TYPE_NOT_SIGNING_REQUIRED) { + this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.ageDays", days); + this.info = Res.get("shared.notSigned.noNeedDays", days); + this.icon = MaterialDesignIcon.CHECKBOX_MARKED_OUTLINE; } else { - // No signing and age required (altcoins) - this.type = 0L; - this.days = 0L; + this.displayString = Res.get("offerbook.timeSinceSigning.notSigned.noNeed"); + this.info = Res.get("shared.notSigned.noNeedAlts"); + this.icon = MaterialDesignIcon.INFORMATION_OUTLINE; } } + + public boolean isAccountSigned() { + return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED || this.type == TYPE_SIGNED_OR_BANNED; + } + + public boolean isLimitLifted() { + return this.type == TYPE_SIGNED_AND_LIMIT_LIFTED; + } + + public boolean isSigningRequired() { + return this.type != TYPE_NOT_SIGNING_REQUIRED && this.type != TYPE_ALTCOINS; + } + + @Override + public int compareTo(@NotNull WitnessAgeData o) { + return (int) (this.type.equals(o.getType()) ? this.days - o.getDays() : this.type - o.getType()); + } } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java index e109d4b7ea2..c6f8b40ebf2 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/offerbook/OfferBookView.java @@ -20,6 +20,7 @@ import bisq.desktop.Navigation; import bisq.desktop.common.view.ActivatableViewAndModel; import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AccountStatusTooltipLabel; import bisq.desktop.components.AutoTooltipButton; import bisq.desktop.components.AutoTooltipLabel; import bisq.desktop.components.AutoTooltipSlideToggleButton; @@ -290,10 +291,7 @@ public void initialize() { }, Comparator.nullsFirst(Comparator.naturalOrder()))); - Comparator comparator = Comparator.comparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService).getType(), Comparator.nullsFirst(Comparator.naturalOrder())); - signingStateColumn.setComparator(comparator. - thenComparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService).getDays(), - Comparator.nullsFirst(Comparator.naturalOrder()))); + signingStateColumn.setComparator(Comparator.comparing(e -> e.getWitnessAgeData(accountAgeWitnessService, signedWitnessService), Comparator.nullsFirst(Comparator.naturalOrder()))); nrOfOffersLabel = new AutoTooltipLabel(""); nrOfOffersLabel.setId("num-offers"); @@ -1054,11 +1052,13 @@ public void updateItem(final OfferBookListItem item, boolean empty) { tableRow.setOnMousePressed(null); } else { button.setDefaultButton(false); - tableRow.setOnMousePressed(e -> { - // ugly hack to get the icon clickable when deactivated - if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) - onShowInfo(offer, canTakeOfferResult); - }); + if (!myOffer) { + tableRow.setOnMousePressed(e -> { + // ugly hack to get the icon clickable when deactivated + if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas)) + onShowInfo(offer, canTakeOfferResult); + }); + } } } @@ -1139,10 +1139,9 @@ public void updateItem(final OfferBookListItem item, boolean empty) { if (item != null && !empty) { var witnessAgeData = item.getWitnessAgeData(accountAgeWitnessService, signedWitnessService); - InfoAutoTooltipLabel label = new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), - witnessAgeData.getIcon(), - ContentDisplay.RIGHT, - witnessAgeData.getInfo()); + var label = witnessAgeData.isSigningRequired() + ? new AccountStatusTooltipLabel(witnessAgeData, formatter) + : new InfoAutoTooltipLabel(witnessAgeData.getDisplayString(), witnessAgeData.getIcon(), ContentDisplay.RIGHT, witnessAgeData.getInfo()); setGraphic(label); } else { setGraphic(null); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java index a6ac3ce0ee0..61b46a394ff 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/DisputeSummaryWindow.java @@ -673,6 +673,13 @@ private void addButtons(Contract contract) { } private void showPayoutTxConfirmation(Contract contract, DisputeResult disputeResult, ResultHandler resultHandler) { + if (dispute.isPayoutDone()) { + new Popup().headLine(Res.get("disputeSummaryWindow.close.alreadyPaid.headline")) + .confirmation(Res.get("disputeSummaryWindow.close.alreadyPaid.text")) + .closeButtonText(Res.get("shared.cancel")) + .show(); + } + Coin buyerPayoutAmount = disputeResult.getBuyerPayoutAmount(); String buyerPayoutAddressString = contract.getBuyerPayoutAddressString(); Coin sellerPayoutAmount = disputeResult.getSellerPayoutAmount(); @@ -734,6 +741,12 @@ private void doPayout(Coin buyerPayoutAmount, String buyerPayoutAddressString, String sellerPayoutAddressString, ResultHandler resultHandler) { + if (dispute.isPayoutDone()) { + log.error("Payout already processed, returning to avoid double payout for dispute of trade {}", + dispute.getTradeId()); + return; + } + dispute.setPayoutDone(true); try { Transaction tx = btcWalletService.createRefundPayoutTx(buyerPayoutAmount, sellerPayoutAmount,