From dd56b3a4e70b558785e5ed3f65870439139653cd Mon Sep 17 00:00:00 2001 From: Steven Barclay Date: Thu, 5 Sep 2019 18:49:41 +0100 Subject: [PATCH] Prevent tooltip popover flicker upon mouseover Provide a wrapper for PopOver components used as tooltips, to debounce the 'MouseEntered' and 'MouseExited' events used to show/hide it, in order to prevent it flickering open/closed in a loop. This fixes #3016. To this end, use a ..->HIDDEN->SHOWING->SHOWN->HIDING->.. state field, together with a target visibility boolean field, where the transition between HIDDEN and SHOWN incurs a small fixed delay. --- .../components/AutoTooltipTableColumn.java | 43 ++++------- .../components/InfoAutoTooltipLabel.java | 42 ++++------- .../components/InfoInputTextField.java | 28 ++----- .../desktop/components/InfoTextField.java | 29 ++------ .../desktop/components/PopOverWrapper.java | 73 +++++++++++++++++++ 5 files changed, 113 insertions(+), 102 deletions(-) create mode 100644 desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java diff --git a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java index 906a751e2cb..aa6cb17af20 100644 --- a/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java +++ b/desktop/src/main/java/bisq/desktop/components/AutoTooltipTableColumn.java @@ -17,7 +17,6 @@ package bisq.desktop.components; -import bisq.common.UserThread; import bisq.desktop.components.controlsfx.control.PopOver; import de.jensd.fx.fontawesome.AwesomeDude; @@ -28,14 +27,10 @@ import javafx.scene.control.TableColumn; import javafx.scene.layout.HBox; -import java.util.concurrent.TimeUnit; - public class AutoTooltipTableColumn extends TableColumn { private Label helpIcon; - private Boolean hidePopover; - private PopOver infoPopover; - + private PopOverWrapper popoverWrapper = new PopOverWrapper(); public AutoTooltipTableColumn(String text) { super(); @@ -53,46 +48,36 @@ public void setTitle(String title) { } public void setTitleWithHelpText(String title, String help) { - - final AutoTooltipLabel label = new AutoTooltipLabel(title); - helpIcon = new Label(); AwesomeDude.setIcon(helpIcon, AwesomeIcon.QUESTION_SIGN, "1em"); helpIcon.setOpacity(0.4); - helpIcon.setOnMouseEntered(e -> { - hidePopover = false; - final Label helpLabel = new Label(help); - helpLabel.setMaxWidth(300); - helpLabel.setWrapText(true); - showInfoPopOver(helpLabel); - }); - helpIcon.setOnMouseExited(e -> { - if (infoPopover != null) - infoPopover.hide(); - hidePopover = true; - UserThread.runAfter(() -> { - if (hidePopover) { - infoPopover.hide(); - hidePopover = false; - } - }, 250, TimeUnit.MILLISECONDS); - }); + helpIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(help))); + helpIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); + final AutoTooltipLabel label = new AutoTooltipLabel(title); final HBox hBox = new HBox(label, helpIcon); hBox.setStyle("-fx-alignment: center-left"); hBox.setSpacing(4); setGraphic(hBox); } - private void showInfoPopOver(Node node) { + private PopOver createInfoPopOver(String help) { + Label helpLabel = new Label(help); + helpLabel.setMaxWidth(300); + helpLabel.setWrapText(true); + return createInfoPopOver(helpLabel); + } + + private PopOver createInfoPopOver(Node node) { node.getStyleClass().add("default-text"); - infoPopover = new PopOver(node); + PopOver infoPopover = new PopOver(node); if (helpIcon.getScene() != null) { infoPopover.setDetachable(false); infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); infoPopover.show(helpIcon, -10); } + return infoPopover; } } diff --git a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java index 0f08bdf92e7..add5cf9dbfd 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoAutoTooltipLabel.java @@ -19,8 +19,6 @@ import bisq.desktop.components.controlsfx.control.PopOver; -import bisq.common.UserThread; - import de.jensd.fx.fontawesome.AwesomeIcon; import de.jensd.fx.glyphs.GlyphIcons; @@ -30,16 +28,13 @@ import javafx.geometry.Insets; -import java.util.concurrent.TimeUnit; - import static bisq.desktop.util.FormBuilder.getIcon; public class InfoAutoTooltipLabel extends AutoTooltipLabel { public static final int DEFAULT_WIDTH = 300; private Node textIcon; - private Boolean hidePopover; - private PopOver infoPopover; + private PopOverWrapper popoverWrapper = new PopOverWrapper(); private ContentDisplay contentDisplay; public InfoAutoTooltipLabel(String text, GlyphIcons icon, ContentDisplay contentDisplay, String info) { @@ -82,42 +77,31 @@ public void hideIcon() { private void positionAndActivateIcon(ContentDisplay contentDisplay, String info, double width) { textIcon.setOpacity(0.4); textIcon.getStyleClass().add("tooltip-icon"); - - textIcon.setOnMouseEntered(e -> { - hidePopover = false; - final Label helpLabel = new Label(info); - helpLabel.setMaxWidth(width); - helpLabel.setWrapText(true); - helpLabel.setPadding(new Insets(10)); - showInfoPopOver(helpLabel); - }); - - textIcon.setOnMouseExited(e -> { - if (infoPopover != null) - infoPopover.hide(); - hidePopover = true; - UserThread.runAfter(() -> { - if (hidePopover) { - infoPopover.hide(); - hidePopover = false; - } - }, 250, TimeUnit.MILLISECONDS); - }); + textIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createInfoPopOver(info, width))); + textIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); setGraphic(textIcon); setContentDisplay(contentDisplay); } + private PopOver createInfoPopOver(String info, double width) { + Label helpLabel = new Label(info); + helpLabel.setMaxWidth(width); + helpLabel.setWrapText(true); + helpLabel.setPadding(new Insets(10)); + return createInfoPopOver(helpLabel); + } - private void showInfoPopOver(Node node) { + private PopOver createInfoPopOver(Node node) { node.getStyleClass().add("default-text"); - infoPopover = new PopOver(node); + PopOver infoPopover = new PopOver(node); if (textIcon.getScene() != null) { infoPopover.setDetachable(false); infoPopover.setArrowLocation(PopOver.ArrowLocation.LEFT_CENTER); infoPopover.show(textIcon, -10); } + return infoPopover; } } diff --git a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java index 120fb9454d7..15531f9015c 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoInputTextField.java @@ -17,7 +17,6 @@ package bisq.desktop.components; -import bisq.common.UserThread; import bisq.desktop.components.controlsfx.control.PopOver; import de.jensd.fx.fontawesome.AwesomeIcon; @@ -29,8 +28,6 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import java.util.concurrent.TimeUnit; - import lombok.Getter; import static bisq.desktop.util.FormBuilder.getIcon; @@ -49,8 +46,7 @@ public class InfoInputTextField extends AnchorPane { private final Label privacyIcon; private Label currentIcon; - private PopOver popover; - private boolean hidePopover; + private PopOverWrapper popoverWrapper = new PopOverWrapper(); public InfoInputTextField() { this(0); @@ -161,28 +157,15 @@ private void setActionHandlers(Node node) { currentIcon.setVisible(true); // As we don't use binding here we need to recreate it on mouse over to reflect the current state - currentIcon.setOnMouseEntered(e -> { - hidePopover = false; - showPopOver(node); - }); - currentIcon.setOnMouseExited(e -> { - if (popover != null) - popover.hide(); - hidePopover = true; - UserThread.runAfter(() -> { - if (hidePopover) { - popover.hide(); - hidePopover = false; - } - }, 250, TimeUnit.MILLISECONDS); - }); + currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node))); + currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); } } - private void showPopOver(Node node) { + private PopOver createPopOver(Node node) { node.getStyleClass().add("default-text"); - popover = new PopOver(node); + PopOver popover = new PopOver(node); if (currentIcon.getScene() != null) { popover.setDetachable(false); popover.setArrowLocation(PopOver.ArrowLocation.LEFT_TOP); @@ -190,5 +173,6 @@ private void showPopOver(Node node) { popover.show(currentIcon, -17); } + return popover; } } diff --git a/desktop/src/main/java/bisq/desktop/components/InfoTextField.java b/desktop/src/main/java/bisq/desktop/components/InfoTextField.java index cc87dd7cb4d..fe253b2f2b8 100644 --- a/desktop/src/main/java/bisq/desktop/components/InfoTextField.java +++ b/desktop/src/main/java/bisq/desktop/components/InfoTextField.java @@ -34,8 +34,6 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; -import java.util.concurrent.TimeUnit; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,13 +51,12 @@ public class InfoTextField extends AnchorPane { private final StringProperty text = new SimpleStringProperty(); protected final Label infoIcon; private Label currentIcon; - private Boolean hidePopover; - private PopOver popover; + private PopOverWrapper popoverWrapper = new PopOverWrapper(); private PopOver.ArrowLocation arrowLocation; public InfoTextField() { - arrowLocation = PopOver.ArrowLocation.RIGHT_TOP;; + arrowLocation = PopOver.ArrowLocation.RIGHT_TOP; textField = new BisqTextField(); textField.setLabelFloat(true); textField.setEditable(false); @@ -124,27 +121,14 @@ private void setActionHandlers(Node node) { currentIcon.setVisible(true); // As we don't use binding here we need to recreate it on mouse over to reflect the current state - currentIcon.setOnMouseEntered(e -> { - hidePopover = false; - showPopOver(node); - }); - currentIcon.setOnMouseExited(e -> { - if (popover != null) - popover.hide(); - hidePopover = true; - UserThread.runAfter(() -> { - if (hidePopover) { - popover.hide(); - hidePopover = false; - } - }, 250, TimeUnit.MILLISECONDS); - }); + currentIcon.setOnMouseEntered(e -> popoverWrapper.showPopOver(() -> createPopOver(node))); + currentIcon.setOnMouseExited(e -> popoverWrapper.hidePopOver()); } - private void showPopOver(Node node) { + private PopOver createPopOver(Node node) { node.getStyleClass().add("default-text"); - popover = new PopOver(node); + PopOver popover = new PopOver(node); if (currentIcon.getScene() != null) { popover.setDetachable(false); popover.setArrowLocation(arrowLocation); @@ -152,6 +136,7 @@ private void showPopOver(Node node) { popover.show(currentIcon, -17); } + return popover; } /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java b/desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java new file mode 100644 index 00000000000..fce42467b5e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/components/PopOverWrapper.java @@ -0,0 +1,73 @@ +/* + * 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.common.UserThread; + +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +public class PopOverWrapper { + + private PopOver popover; + private Supplier popoverSupplier; + private boolean hidePopover; + private PopOverState state = PopOverState.HIDDEN; + + enum PopOverState { + HIDDEN, SHOWING, SHOWN, HIDING + } + + public void showPopOver(Supplier popoverSupplier) { + this.popoverSupplier = popoverSupplier; + hidePopover = false; + + if (state == PopOverState.HIDDEN) { + state = PopOverState.SHOWING; + popover = popoverSupplier.get(); + + UserThread.runAfter(() -> { + state = PopOverState.SHOWN; + if (hidePopover) { + // For some reason, this can result in a brief flicker when invoked + // from a 'runAfter' callback, rather than directly. So make the delay + // very short (25ms) so that we don't reach here often: + hidePopOver(); + } + }, 25, TimeUnit.MILLISECONDS); + } + } + + public void hidePopOver() { + hidePopover = true; + + if (state == PopOverState.SHOWN) { + state = PopOverState.HIDING; + popover.hide(); + + UserThread.runAfter(() -> { + state = PopOverState.HIDDEN; + if (!hidePopover) { + showPopOver(popoverSupplier); + } + }, 250, TimeUnit.MILLISECONDS); + } + } +}