Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
import org.jackhuang.hmcl.ui.construct.JFXTooltip;
import org.jackhuang.hmcl.ui.construct.MenuSeparator;
import org.jackhuang.hmcl.ui.construct.PopupMenu;
import org.jackhuang.hmcl.ui.image.ImageLoader;
Expand Down Expand Up @@ -471,31 +472,30 @@ public static void smoothScrolling(VirtualFlow<?> virtualFlow) {
}
}

private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50);
private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(250);
private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500);
private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000);

public static void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) {
public static void installTooltip(Node node, Duration showDelay, Duration showDuration, JFXTooltip tooltip) {
tooltip.setShowDelay(showDelay);
tooltip.setShowDuration(showDuration);
tooltip.setHideDelay(hideDelay);
Tooltip.install(node, tooltip);
tooltip.install(node);
}

public static void installFastTooltip(Node node, Tooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
public static void installFastTooltip(Node node, JFXTooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, tooltip));
}

public static void installFastTooltip(Node node, String tooltip) {
installFastTooltip(node, new Tooltip(tooltip));
public static void installFastTooltip(Node node, String tooltipText) {
installFastTooltip(node, new JFXTooltip(tooltipText));
}

public static void installSlowTooltip(Node node, Tooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip));
public static void installSlowTooltip(Node node, JFXTooltip tooltip) {
runInFX(() -> installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, tooltip));
}

public static void installSlowTooltip(Node node, String tooltip) {
installSlowTooltip(node, new Tooltip(tooltip));
public static void installSlowTooltip(Node node, String tooltipText) {
installSlowTooltip(node, new JFXTooltip(tooltipText));
}

public static void playAnimation(Node node, String animationKey, Animation animation) {
Expand Down Expand Up @@ -1321,18 +1321,18 @@ public static void showTooltipWhenTruncated(Labeled labeled) {
if (textTruncatedProperty != null) {
ChangeListener<Boolean> listener = (observable, oldValue, newValue) -> {
var label = (Labeled) ((ReadOnlyProperty<?>) observable).getBean();
var tooltip = (Tooltip) label.getProperties().get(LABEL_FULL_TEXT_PROP_KEY);
var tooltip = (JFXTooltip) label.getProperties().get(LABEL_FULL_TEXT_PROP_KEY);

if (newValue) {
if (tooltip == null) {
tooltip = new Tooltip();
tooltip = new JFXTooltip();
tooltip.textProperty().bind(label.textProperty());
label.getProperties().put(LABEL_FULL_TEXT_PROP_KEY, tooltip);
}

FXUtils.installFastTooltip(label, tooltip);
} else if (tooltip != null) {
Tooltip.uninstall(label, tooltip);
tooltip.uninstall();
}
};
listener.changed(textTruncatedProperty, false, textTruncatedProperty.get());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Tooltip;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
Expand All @@ -32,6 +31,7 @@
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.JFXTooltip;
import org.jackhuang.hmcl.util.javafx.BindingMapping;

import static javafx.beans.binding.Bindings.createStringBinding;
Expand All @@ -40,7 +40,7 @@
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;

public class AccountAdvancedListItem extends AdvancedListItem {
private final Tooltip tooltip;
private final JFXTooltip tooltip;
private final Canvas canvas;

private final ObjectProperty<Account> account = new SimpleObjectProperty<Account>() {
Expand Down Expand Up @@ -73,7 +73,7 @@ public AccountAdvancedListItem() {
}

public AccountAdvancedListItem(Account account) {
tooltip = new Tooltip();
tooltip = new JFXTooltip();
FXUtils.installFastTooltip(this, tooltip);

canvas = new Canvas(32, 32);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
Expand All @@ -41,6 +40,7 @@
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.JFXTooltip;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.javafx.BindingMapping;

Expand Down Expand Up @@ -75,7 +75,7 @@ public AccountListItemSkin(AccountListItem skinnable) {
subtitle.getStyleClass().add("subtitle");
subtitle.textProperty().bind(skinnable.subtitleProperty());
if (skinnable.getAccount() instanceof AuthlibInjectorAccount) {
Tooltip tooltip = new Tooltip();
JFXTooltip tooltip = new JFXTooltip();
AuthlibInjectorServer server = ((AuthlibInjectorAccount) skinnable.getAccount()).getServer();
tooltip.textProperty().bind(BindingMapping.of(server, AuthlibInjectorServer::toString));
FXUtils.installSlowTooltip(subtitle, tooltip);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,14 @@
package org.jackhuang.hmcl.ui.account;

import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ListProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
Expand All @@ -43,6 +36,7 @@
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.ClassTitle;
import org.jackhuang.hmcl.ui.construct.JFXTooltip;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
Expand Down Expand Up @@ -157,7 +151,7 @@ public AccountListPageSkin(AccountListPage skinnable) {
LOG.warning("Unparsable authlib-injector server url " + server.getUrl(), e);
}
item.subtitleProperty().set(host);
Tooltip tooltip = new Tooltip();
JFXTooltip tooltip = new JFXTooltip();
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
FXUtils.installFastTooltip(item, tooltip);

Expand Down
149 changes: 149 additions & 0 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/JFXTooltip.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;

import javafx.animation.FadeTransition;
import javafx.animation.PauseTransition;
import javafx.beans.property.StringProperty;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Popup;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;

public class JFXTooltip {
// https://api.flutter.dev/flutter/material/Tooltip-class.html
private static final double FADE_IN_MS = AnimationUtils.isAnimationEnabled() ? 150 : 0;
private static final double FADE_OUT_MS = AnimationUtils.isAnimationEnabled() ? 75 : 0;

private final Popup popup;
private final Label label;
private final PauseTransition showDelayTransition;
private final PauseTransition showDurationTransition;

private final FadeTransition fadeIn;
private final FadeTransition fadeOut;

private double mouseX;
private double mouseY;

private EventHandler<MouseEvent> enteredHandler;
private EventHandler<MouseEvent> exitedHandler;
private EventHandler<MouseEvent> pressedHandler;
private Node attachedNode;

public JFXTooltip() {
this("");
}

public JFXTooltip(String text) {
popup = new Popup();
popup.setAutoHide(false);

label = new Label(text);
StackPane root = new StackPane(label);
root.getStyleClass().add("jfx-tooltip");
root.setMouseTransparent(true);

popup.getContent().add(root);

fadeIn = new FadeTransition(Duration.millis(FADE_IN_MS), root);
fadeIn.setFromValue(0.0);
fadeIn.setToValue(1.0);

fadeOut = new FadeTransition(Duration.millis(FADE_OUT_MS), root);
fadeOut.setFromValue(1.0);
fadeOut.setToValue(0.0);
fadeOut.setOnFinished(event -> popup.hide());

showDelayTransition = new PauseTransition(Duration.millis(500));
showDurationTransition = new PauseTransition(Duration.millis(5000));
showDurationTransition.setOnFinished(e -> hideTooltip());
}

public void setShowDelay(Duration delay) {
this.showDelayTransition.setDuration(delay);
}

public void setShowDuration(Duration duration) {
this.showDurationTransition.setDuration(duration);
}

public final StringProperty textProperty() {
return label.textProperty();
}

public final void setText(String value) {
label.setText(value);
}

private void hideTooltip() {
showDelayTransition.stop();
showDurationTransition.stop();
if (popup.isShowing()) {
fadeIn.stop();
fadeOut.playFromStart();
}
}

public void install(Node targetNode) {
if (attachedNode != null) {
uninstall();
}
this.attachedNode = targetNode;

Comment on lines +107 to +112
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

install(Node) always calls uninstall() whenever attachedNode != null, even if the tooltip is being (re)installed onto the same node. In list cell updateItem paths (e.g. where FXUtils.installSlowTooltip(left, tooltip) is called repeatedly), this causes unnecessary handler churn and transition resets. Consider making install idempotent for the same targetNode (early-return if attachedNode == targetNode) and/or avoid re-registering handlers unless the target node actually changes.

Copilot uses AI. Check for mistakes.
enteredHandler = event -> {
mouseX = event.getScreenX();
mouseY = event.getScreenY();
showDelayTransition.playFromStart();
};

exitedHandler = event -> hideTooltip();
pressedHandler = event -> hideTooltip();

targetNode.addEventHandler(MouseEvent.MOUSE_ENTERED, enteredHandler);
targetNode.addEventHandler(MouseEvent.MOUSE_EXITED, exitedHandler);
targetNode.addEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler);

showDelayTransition.setOnFinished(e -> {
if (targetNode.getScene() != null && targetNode.getScene().getWindow() != null) {
fadeOut.stop();
popup.show(targetNode.getScene().getWindow(), mouseX + 5, mouseY);
fadeIn.playFromStart();
showDurationTransition.playFromStart();
}
});
}

public void uninstall() {
if (attachedNode != null) {
hideTooltip();
attachedNode.removeEventHandler(MouseEvent.MOUSE_ENTERED, enteredHandler);
attachedNode.removeEventHandler(MouseEvent.MOUSE_EXITED, exitedHandler);
attachedNode.removeEventHandler(MouseEvent.MOUSE_PRESSED, pressedHandler);

attachedNode = null;
enteredHandler = null;
exitedHandler = null;
pressedHandler = null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import javafx.scene.Node;
import javafx.scene.control.ListCell;
import javafx.scene.control.Skin;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
Expand All @@ -41,6 +40,7 @@
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.construct.JFXTooltip;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
Expand All @@ -49,16 +49,18 @@
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.Platform;
import org.jackhuang.hmcl.util.platform.UnsupportedPlatformException;
import org.jackhuang.hmcl.util.tree.ArchiveFileTree;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
Expand Down Expand Up @@ -221,7 +223,7 @@ private static final class JavaItemCell extends ListCell<JavaRuntime> {

private SVG removeIcon;
private final StackPane removeIconPane;
private final Tooltip removeTooltip = new Tooltip();
private final JFXTooltip removeTooltip = new JFXTooltip();

JavaItemCell(JFXListView<JavaRuntime> listView) {
BorderPane root = new BorderPane();
Expand Down
Loading
Loading