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 9593cbc430..0b2c8790bd 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.SimpleObjectProperty; 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 class PopOver extends PopupControl { 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 class PopOver extends PopupControl { 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 @@ public class PopOver extends PopupControl { 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 class PopOver extends PopupControl { throw new IllegalArgumentException( "content node can not be null"); //$NON-NLS-1$ } - }; + } + }; /** @@ -186,41 +237,36 @@ public class PopOver extends PopupControl { 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 class PopOver extends PopupControl { } } + /** {@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 class PopOver extends PopupControl { */ @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 class PopOver extends PopupControl { * @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 class PopOver extends PopupControl { /* * 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 class PopOver extends PopupControl { 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 class PopOver extends PopupControl { fadeIn.play(); } + private void ownerWindowClosing() { + hide(Duration.ZERO); + } + /** * Hides the pop over by quickly changing its opacity to 0. * @@ -399,7 +490,7 @@ public class PopOver extends PopupControl { */ @Override public final void hide() { - hide(DEFAULT_FADE_DURATION); + hide(getFadeOutDuration()); } /** @@ -411,21 +502,32 @@ public class PopOver extends PopupControl { * @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); + if (isAnimated()) { + // Fade Out + Node skinNode = getSkin().getNode(); - FadeTransition fadeOut = new FadeTransition(fadeOutDuration, - skinNode); - fadeOut.setFromValue(1); - fadeOut.setToValue(0); - fadeOut.setOnFinished(evt -> super.hide()); - fadeOut.play(); + 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 @@ public class PopOver extends PopupControl { 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 class PopOver extends PopupControl { } } + // 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 class PopOver extends PopupControl { * 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 class PopOver extends PopupControl { // 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 class PopOver extends PopupControl { * 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 ccc6d5e42f..79f946fe6e 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 3ebdc843bf..e449f847a1 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.Path; 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 class PopOverSkin implements Skin { 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 class PopOverSkin implements Skin { 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) -> { - popOver.contentNodeProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue value, - Node oldContent, Node newContent) { - content.setCenter(newContent); - } - }); + if (newDetached) { + popOver.getStyleClass().add(DETACHED_STYLE_CLASS); + content.getStyleClass().add(DETACHED_STYLE_CLASS); + content.setTop(titlePane); - popOver.detachedProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue value, - Boolean oldDetached, Boolean newDetached) { + 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); - updatePath(); + if (!popOver.isHeaderAlwaysVisible()) { + content.setTop(null); + } + } - 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); - } - } - }); + popOver.sizeToScene(); + + 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() { + 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; - public void handle(MouseEvent evt) { - 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 class PopOverSkin implements Skin { stackPane.getChildren().add(path); stackPane.getChildren().add(content); + + content.setClip(clip); } @Override @@ -689,5 +712,6 @@ public class PopOverSkin implements Skin { elements.add(topCurveTo); path.getElements().setAll(elements); + clip.getElements().setAll(elements); } }