diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index d709b9223..2e2985725 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -72,8 +72,6 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.*; import java.lang.ref.WeakReference; -import java.lang.reflect.Constructor; -import java.lang.reflect.Method; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Files; @@ -298,8 +296,12 @@ public final class FXUtils { ScrollUtils.addSmoothScrolling(scrollPane); } + private static final Duration TOOLTIP_FAST_SHOW_DELAY = Duration.millis(50); + private static final Duration TOOLTIP_SLOW_SHOW_DELAY = Duration.millis(500); + private static final Duration TOOLTIP_SHOW_DURATION = Duration.millis(5000); + public static void installFastTooltip(Node node, Tooltip tooltip) { - installTooltip(node, 50, 5000, 0, tooltip); + runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_FAST_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip)); } public static void installFastTooltip(Node node, String tooltip) { @@ -307,39 +309,13 @@ public final class FXUtils { } public static void installSlowTooltip(Node node, Tooltip tooltip) { - installTooltip(node, 500, 5000, 0, tooltip); + runInFX(() -> TooltipInstaller.INSTALLER.installTooltip(node, TOOLTIP_SLOW_SHOW_DELAY, TOOLTIP_SHOW_DURATION, Duration.ZERO, tooltip)); } public static void installSlowTooltip(Node node, String tooltip) { installSlowTooltip(node, new Tooltip(tooltip)); } - public static void installTooltip(Node node, double openDelay, double visibleDelay, double closeDelay, Tooltip tooltip) { - runInFX(() -> { - try { - // Java 8 - Class behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"); - Constructor behaviorConstructor = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class); - behaviorConstructor.setAccessible(true); - Object behavior = behaviorConstructor.newInstance(new Duration(openDelay), new Duration(visibleDelay), new Duration(closeDelay), false); - Method installMethod = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class); - installMethod.setAccessible(true); - installMethod.invoke(behavior, node, tooltip); - } catch (ReflectiveOperationException e) { - try { - // Java 9 - Tooltip.class.getMethod("setShowDelay", Duration.class).invoke(tooltip, new Duration(openDelay)); - Tooltip.class.getMethod("setShowDuration", Duration.class).invoke(tooltip, new Duration(visibleDelay)); - Tooltip.class.getMethod("setHideDelay", Duration.class).invoke(tooltip, new Duration(closeDelay)); - } catch (ReflectiveOperationException e2) { - e.addSuppressed(e2); - LOG.error("Cannot install tooltip", e); - } - Tooltip.install(node, tooltip); - } - }); - } - public static void playAnimation(Node node, String animationKey, Timeline timeline) { animationKey = "FXUTILS.ANIMATION." + animationKey; Object oldTimeline = node.getProperties().get(animationKey); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index 651542238..d3d74d24b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -65,7 +65,6 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -387,13 +386,13 @@ public class GameCrashWindow extends Stage { gameDir.getStyleClass().setAll("two-line-item-second-large"); gameDir.setTitle(i18n("game.directory")); gameDir.setSubtitle(launchOptions.getGameDir().getAbsolutePath()); - runInFX(() -> FXUtils.installFastTooltip(gameDir, i18n("game.directory"))); + FXUtils.installFastTooltip(gameDir, i18n("game.directory")); TwoLineListItem javaDir = new TwoLineListItem(); javaDir.getStyleClass().setAll("two-line-item-second-large"); javaDir.setTitle(i18n("settings.game.java_directory")); javaDir.setSubtitle(launchOptions.getJava().getBinary().toAbsolutePath().toString()); - runInFX(() -> FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory"))); + FXUtils.installFastTooltip(javaDir, i18n("settings.game.java_directory")); Label reasonTitle = new Label(i18n("game.crash.reason")); reasonTitle.getStyleClass().add("two-line-item-second-large-title"); @@ -424,7 +423,7 @@ public class GameCrashWindow extends Stage { JFXButton helpButton = FXUtils.newRaisedButton(i18n("help")); helpButton.setOnAction(e -> FXUtils.openLink("https://docs.hmcl.net/help.html")); - runInFX(() -> FXUtils.installFastTooltip(helpButton, i18n("logwindow.help"))); + FXUtils.installFastTooltip(helpButton, i18n("logwindow.help")); toolBar.setPadding(new Insets(8)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/TooltipInstaller.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/TooltipInstaller.java new file mode 100644 index 000000000..acc1530d1 --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/TooltipInstaller.java @@ -0,0 +1,123 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2024 huangyuhui 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 . + */ +package org.jackhuang.hmcl.ui; + +import javafx.scene.Node; +import javafx.scene.control.Tooltip; +import javafx.util.Duration; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; + +/** + * @author Glavo + */ +public class TooltipInstaller { + public static final TooltipInstaller INSTALLER; + + static { + TooltipInstaller installer = null; + + try { + installer = new NewInstaller(); + } catch (Throwable e) { + try { + installer = new OldInstaller(); + } catch (Throwable e2) { + e2.addSuppressed(e); + LOG.warning("Failed to initialize TooltipInstaller", e2); + } + } + + INSTALLER = installer != null ? installer : new TooltipInstaller(); + } + + TooltipInstaller() { + } + + public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) { + Tooltip.install(node, tooltip); + } + + // For Java 8 + private static final class OldInstaller extends TooltipInstaller { + private static final Constructor createTooltipBehavior; + private static final Method installTooltipBehavior; + + static { + try { + Class behaviorClass = Class.forName("javafx.scene.control.Tooltip$TooltipBehavior"); + createTooltipBehavior = behaviorClass.getDeclaredConstructor(Duration.class, Duration.class, Duration.class, boolean.class); + createTooltipBehavior.setAccessible(true); + installTooltipBehavior = behaviorClass.getDeclaredMethod("install", Node.class, Tooltip.class); + installTooltipBehavior.setAccessible(true); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) { + try { + Object behavior = createTooltipBehavior.newInstance(showDelay, showDuration, hideDelay, false); + installTooltipBehavior.invoke(behavior, node, tooltip); + } catch (ReflectiveOperationException e) { + LOG.warning("Failed to set tooltip show delay", e); + Tooltip.install(node, tooltip); + } + } + } + + // For Java 9+ + private static final class NewInstaller extends TooltipInstaller { + private static final MethodHandle setTooltipShowDelay; + private static final MethodHandle setTooltipShowDuration; + private static final MethodHandle setTooltipHideDelay; + + static { + try { + MethodHandles.Lookup lookup = MethodHandles.publicLookup(); + MethodType methodType = MethodType.methodType(void.class, Duration.class); + + setTooltipShowDelay = lookup.findVirtual(Tooltip.class, "setShowDelay", methodType); + setTooltipShowDuration = lookup.findVirtual(Tooltip.class, "setShowDuration", methodType); + setTooltipHideDelay = lookup.findVirtual(Tooltip.class, "setHideDelay", methodType); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + @Override + public void installTooltip(Node node, Duration showDelay, Duration showDuration, Duration hideDelay, Tooltip tooltip) { + try { + setTooltipShowDelay.invokeExact(tooltip, showDelay); + setTooltipShowDuration.invokeExact(tooltip, showDuration); + setTooltipHideDelay.invokeExact(tooltip, hideDelay); + } catch (Throwable e) { + LOG.warning("Failed to set tooltip show delay", e); + } + + Tooltip.install(node, tooltip); + } + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java index 1c3ff28c9..a5d8bee8b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/account/CreateAccountPane.java @@ -510,7 +510,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware { if (factory instanceof OfflineAccountFactory) { txtUsername.setPromptText(i18n("account.methods.offline.name.special_characters")); - runInFX(() -> FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters"))); + FXUtils.installFastTooltip(txtUsername, i18n("account.methods.offline.name.special_characters")); JFXHyperlink purchaseLink = new JFXHyperlink(i18n("account.methods.microsoft.purchase")); purchaseLink.setExternalLink(YggdrasilService.PURCHASE_URL); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 08999fb7a..7cded1d27 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -66,7 +66,6 @@ import java.util.stream.IntStream; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.FXUtils.SINE; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public final class MainPage extends StackPane implements DecoratorPage { @@ -224,7 +223,7 @@ public final class MainPage extends StackPane implements DecoratorPage { StackPane.setAlignment(svg, Pos.CENTER_RIGHT); graphic.getChildren().setAll(svg); graphic.setTranslateX(12); - runInFX(() -> FXUtils.installFastTooltip(menuButton, i18n("version.switch"))); + FXUtils.installFastTooltip(menuButton, i18n("version.switch")); menuButton.setGraphic(graphic); launchPane.getChildren().setAll(launchButton, separator, menuButton); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index abf24d6ce..21301dee7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -165,7 +165,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { downloadItem.setActionButtonVisible(false); downloadItem.setTitle(i18n("download")); downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage())); - runInFX(() -> FXUtils.installFastTooltip(downloadItem, i18n("download.hint"))); + FXUtils.installFastTooltip(downloadItem, i18n("download.hint")); // fifth item in left sidebar AdvancedListItem launcherSettingsItem = new AdvancedListItem(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java index ead269820..dffe893a3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java @@ -64,7 +64,6 @@ import java.util.Locale; import java.util.Optional; import java.util.stream.Collectors; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.ui.FXUtils.stringConverter; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor; @@ -302,7 +301,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP lblGameVersion.visibleProperty().bind(hasVersion); gameVersionField.managedProperty().bind(hasVersion); gameVersionField.visibleProperty().bind(hasVersion); - runInFX(() -> FXUtils.installFastTooltip(gameVersionField, i18n("search.enter"))); + FXUtils.installFastTooltip(gameVersionField, i18n("search.enter")); FXUtils.onChangeAndOperate(getSkinnable().version, version -> { if (StringUtils.isNotBlank(version.getVersion())) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java index f11880540..86e1a737b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java @@ -62,7 +62,6 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class DownloadPage extends Control implements DecoratorPage { @@ -241,14 +240,14 @@ public class DownloadPage extends Control implements DecoratorPage { openMcmodButton.setExternalLink(getSkinnable().translations.getMcmodUrl(getSkinnable().mod)); descriptionPane.getChildren().add(openMcmodButton); openMcmodButton.setMinWidth(Region.USE_PREF_SIZE); - runInFX(() -> FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod"))); + FXUtils.installFastTooltip(openMcmodButton, i18n("mods.mcmod")); } JFXHyperlink openUrlButton = new JFXHyperlink(control.page.getLocalizedOfficialPage()); openUrlButton.setExternalLink(getSkinnable().addon.getPageUrl()); descriptionPane.getChildren().add(openUrlButton); openUrlButton.setMinWidth(Region.USE_PREF_SIZE); - runInFX(() -> FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage())); + FXUtils.installFastTooltip(openUrlButton, control.page.getLocalizedOfficialPage()); } SpinnerPane spinnerPane = new SpinnerPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java index d2aebb890..15b826472 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItemSkin.java @@ -35,7 +35,6 @@ import org.jackhuang.hmcl.ui.construct.PopupMenu; import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.util.Lazy; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameListItemSkin extends SkinBase { @@ -82,7 +81,7 @@ public class GameListItemSkin extends SkinBase { btnUpgrade.setOnAction(e -> skinnable.update()); btnUpgrade.getStyleClass().add("toggle-icon4"); btnUpgrade.setGraphic(FXUtils.limitingSize(SVG.UPDATE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); - runInFX(() -> FXUtils.installFastTooltip(btnUpgrade, i18n("version.update"))); + FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")); right.getChildren().add(btnUpgrade); } @@ -92,7 +91,7 @@ public class GameListItemSkin extends SkinBase { btnLaunch.getStyleClass().add("toggle-icon4"); BorderPane.setAlignment(btnLaunch, Pos.CENTER); btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH_OUTLINE.createIcon(Theme.blackFill(), 24, 24), 24, 24)); - runInFX(() -> FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test"))); + FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test")); right.getChildren().add(btnLaunch); } @@ -105,7 +104,7 @@ public class GameListItemSkin extends SkinBase { btnManage.getStyleClass().add("toggle-icon4"); BorderPane.setAlignment(btnManage, Pos.CENTER); btnManage.setGraphic(FXUtils.limitingSize(SVG.DOTS_VERTICAL.createIcon(Theme.blackFill(), 24, 24), 24, 24)); - runInFX(() -> FXUtils.installFastTooltip(btnManage, i18n("settings.game.management"))); + FXUtils.installFastTooltip(btnManage, i18n("settings.game.management")); right.getChildren().add(btnManage); }