From fb942d10999f2b956fbf59a124df675bba92171b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BE=9E=E5=BA=90?= <109708109+CiiLu@users.noreply.github.com> Date: Fri, 2 Jan 2026 16:22:54 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=8B=AC=E7=AB=8B=E7=AA=97?= =?UTF-8?q?=E5=8F=A3=E5=BC=B9=E7=AA=97=20(#5060)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolves #4942 --- .../org/jackhuang/hmcl/ui/DialogUtils.java | 155 ++++++++++++++++++ .../jackhuang/hmcl/ui/GameCrashWindow.java | 18 +- .../java/org/jackhuang/hmcl/ui/LogWindow.java | 23 ++- .../ui/decorator/DecoratorController.java | 115 +++---------- 4 files changed, 197 insertions(+), 114 deletions(-) create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java new file mode 100644 index 000000000..faae25cbd --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/DialogUtils.java @@ -0,0 +1,155 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 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 com.jfoenix.controls.JFXDialog; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; +import javafx.event.EventHandler; +import javafx.scene.Node; +import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.construct.DialogAware; +import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; +import org.jackhuang.hmcl.ui.construct.JFXDialogPane; +import org.jackhuang.hmcl.ui.decorator.Decorator; +import org.jetbrains.annotations.Nullable; + +import java.util.Optional; +import java.util.function.Consumer; + +public final class DialogUtils { + private DialogUtils() { + } + + public static final String PROPERTY_DIALOG_INSTANCE = DialogUtils.class.getName() + ".dialog.instance"; + public static final String PROPERTY_DIALOG_PANE_INSTANCE = DialogUtils.class.getName() + ".dialog.pane.instance"; + public static final String PROPERTY_DIALOG_CLOSE_HANDLER = DialogUtils.class.getName() + ".dialog.closeListener"; + + public static final String PROPERTY_PARENT_PANE_REF = DialogUtils.class.getName() + ".dialog.parentPaneRef"; + public static final String PROPERTY_PARENT_DIALOG_REF = DialogUtils.class.getName() + ".dialog.parentDialogRef"; + + public static void show(Decorator decorator, Node content) { + if (decorator.getDrawerWrapper() == null) { + Platform.runLater(() -> show(decorator, content)); + return; + } + + show(decorator.getDrawerWrapper(), content, (dialog) -> { + JFXDialogPane pane = (JFXDialogPane) dialog.getContent(); + decorator.capableDraggingWindow(dialog); + decorator.forbidDraggingWindow(pane); + dialog.setDialogContainer(decorator.getDrawerWrapper()); + }); + } + + public static void show(StackPane container, Node content) { + show(container, content, null); + } + + public static void show(StackPane container, Node content, @Nullable Consumer onDialogCreated) { + FXUtils.checkFxUserThread(); + + JFXDialog dialog = (JFXDialog) container.getProperties().get(PROPERTY_DIALOG_INSTANCE); + JFXDialogPane dialogPane = (JFXDialogPane) container.getProperties().get(PROPERTY_DIALOG_PANE_INSTANCE); + + if (dialog == null) { + dialog = new JFXDialog(AnimationUtils.isAnimationEnabled() + ? JFXDialog.DialogTransition.CENTER + : JFXDialog.DialogTransition.NONE); + dialogPane = new JFXDialogPane(); + + dialog.setContent(dialogPane); + dialog.setDialogContainer(container); + dialog.setOverlayClose(false); + + container.getProperties().put(PROPERTY_DIALOG_INSTANCE, dialog); + container.getProperties().put(PROPERTY_DIALOG_PANE_INSTANCE, dialogPane); + + if (onDialogCreated != null) { + onDialogCreated.accept(dialog); + } + + dialog.show(); + } + + content.getProperties().put(PROPERTY_PARENT_PANE_REF, dialogPane); + content.getProperties().put(PROPERTY_PARENT_DIALOG_REF, dialog); + + dialogPane.push(content); + + EventHandler handler = event -> close(content); + content.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler); + content.addEventHandler(DialogCloseEvent.CLOSE, handler); + + handleDialogShown(dialog, content); + } + + private static void handleDialogShown(JFXDialog dialog, Node node) { + if (dialog.isVisible()) { + dialog.requestFocus(); + if (node instanceof DialogAware dialogAware) + dialogAware.onDialogShown(); + } else { + dialog.visibleProperty().addListener(new ChangeListener<>() { + @Override + public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { + if (newValue) { + dialog.requestFocus(); + if (node instanceof DialogAware dialogAware) + dialogAware.onDialogShown(); + observable.removeListener(this); + } + } + }); + } + } + + @SuppressWarnings("unchecked") + public static void close(Node content) { + FXUtils.checkFxUserThread(); + + Optional.ofNullable(content.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER)) + .ifPresent(handler -> content.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler) handler)); + + JFXDialogPane pane = (JFXDialogPane) content.getProperties().get(PROPERTY_PARENT_PANE_REF); + JFXDialog dialog = (JFXDialog) content.getProperties().get(PROPERTY_PARENT_DIALOG_REF); + + if (dialog != null && pane != null) { + if (pane.size() == 1 && pane.peek().orElse(null) == content) { + dialog.setOnDialogClosed(e -> pane.pop(content)); + dialog.close(); + + StackPane container = dialog.getDialogContainer(); + if (container != null) { + container.getProperties().remove(PROPERTY_DIALOG_INSTANCE); + container.getProperties().remove(PROPERTY_DIALOG_PANE_INSTANCE); + container.getProperties().remove(PROPERTY_PARENT_DIALOG_REF); + container.getProperties().remove(PROPERTY_PARENT_PANE_REF); + } + } else { + pane.pop(content); + } + + if (content instanceof DialogAware dialogAware) { + dialogAware.onDialogClosed(); + } + } + } +} 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 c0050bc2e..03c32f3e8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -24,11 +24,11 @@ import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.Scene; -import javafx.scene.control.Alert; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; +import javafx.scene.layout.StackPane; import javafx.scene.layout.VBox; import javafx.scene.text.Text; import javafx.scene.text.TextFlow; @@ -40,6 +40,7 @@ import org.jackhuang.hmcl.launch.ProcessListener; import org.jackhuang.hmcl.setting.StyleSheets; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Log4jLevel; @@ -84,6 +85,7 @@ public class GameCrashWindow extends Stage { private final ProcessListener.ExitType exitType; private final LaunchOptions launchOptions; private final View view; + private final StackPane stackPane; private final List logs; @@ -106,9 +108,10 @@ public class GameCrashWindow extends Stage { this.view = new View(); + this.stackPane = new StackPane(view); this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction)); - setScene(new Scene(view, 800, 480)); + setScene(new Scene(stackPane, 800, 480)); StyleSheets.init(getScene()); setTitle(i18n("game.crash.title")); FXUtils.setIcon(this); @@ -297,19 +300,16 @@ public class GameCrashWindow extends Stage { }); }) .handleAsync((result, exception) -> { - Alert alert; - if (exception == null) { FXUtils.showFileInExplorer(logFile); - alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile)); + var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build(); + DialogUtils.show(stackPane, dialog); } else { LOG.warning("Failed to export game crash info", exception); - alert = new Alert(Alert.AlertType.WARNING, i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception)); + var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build(); + DialogUtils.show(stackPane, dialog); } - alert.setTitle(i18n("settings.launcher.launcher_log.export")); - alert.showAndWait(); - return null; }, Schedulers.javafx()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index f5c71de7a..47394f98e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -33,7 +33,6 @@ import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; -import javafx.scene.control.Label; import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.input.MouseButton; @@ -44,11 +43,10 @@ import org.jackhuang.hmcl.game.GameDumpGenerator; import org.jackhuang.hmcl.game.Log; import org.jackhuang.hmcl.setting.StyleSheets; import org.jackhuang.hmcl.theme.Themes; +import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel; import org.jackhuang.hmcl.ui.construct.SpinnerPane; -import org.jackhuang.hmcl.util.CircularArrayList; -import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.Log4jLevel; +import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.platform.*; import org.jackhuang.hmcl.util.platform.windows.Dwmapi; import org.jackhuang.hmcl.util.platform.windows.WinConstants; @@ -65,8 +63,8 @@ import java.util.stream.Collectors; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.util.Lang.thread; -import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; +import static org.jackhuang.hmcl.util.logging.Logger.LOG; /** * @author huangyuhui @@ -199,6 +197,7 @@ public final class LogWindow extends Stage { private final StringProperty[] buttonText = new StringProperty[LEVELS.length]; private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length]; private final JFXComboBox cboLines = new JFXComboBox<>(); + private final StackPane stackPane = new StackPane(); LogWindowImpl() { getStyleClass().add("log-window"); @@ -241,9 +240,8 @@ public final class LogWindow extends Stage { } Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.INFORMATION, i18n("settings.launcher.launcher_log.export.success", logFile)); - alert.setTitle(i18n("settings.launcher.launcher_log.export")); - alert.showAndWait(); + var dialog = new MessageDialogPane.Builder(i18n("settings.launcher.launcher_log.export.success", logFile), i18n("message.success"), MessageDialogPane.MessageType.SUCCESS).ok(null).build(); + DialogUtils.show(stackPane, dialog); }); FXUtils.showFileInExplorer(logFile); @@ -267,9 +265,8 @@ public final class LogWindow extends Stage { LOG.warning("Failed to create minecraft jstack dump", e); Platform.runLater(() -> { - Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump")); - alert.setTitle(i18n("message.error")); - alert.showAndWait(); + var dialog = new MessageDialogPane.Builder(i18n("logwindow.export_dump") + "\n" + StringUtils.getStackTrace(e), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build(); + DialogUtils.show(stackPane, dialog); }); } @@ -300,7 +297,9 @@ public final class LogWindow extends Stage { VBox vbox = new VBox(3); vbox.setPadding(new Insets(3, 0, 3, 0)); - getChildren().setAll(vbox); + getSkinnable().stackPane.getChildren().setAll(vbox); + getChildren().setAll(getSkinnable().stackPane); + { BorderPane borderPane = new BorderPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index a13bde94e..64601aa68 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -17,18 +17,13 @@ */ package org.jackhuang.hmcl.ui.decorator; -import com.jfoenix.controls.JFXDialog; import com.jfoenix.controls.JFXSnackbar; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; -import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; -import javafx.event.EventHandler; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.image.Image; @@ -48,14 +43,15 @@ import org.jackhuang.hmcl.setting.EnumBackgroundImage; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; +import org.jackhuang.hmcl.ui.DialogUtils; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane; -import org.jackhuang.hmcl.ui.animation.*; +import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.animation.TransitionPane.AnimationProducer; -import org.jackhuang.hmcl.ui.construct.DialogAware; -import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; -import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.JFXDialogPane; +import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.util.Lang; @@ -66,7 +62,9 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.*; +import java.util.List; +import java.util.Locale; +import java.util.Random; import java.util.stream.Stream; import static java.util.stream.Collectors.toList; @@ -77,14 +75,9 @@ import static org.jackhuang.hmcl.util.io.FileUtils.getExtension; import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class DecoratorController { - private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener"; - private final Decorator decorator; private final Navigator navigator; - private JFXDialog dialog; - private JFXDialogPane dialogPane; - public DecoratorController(Stage stage, Node mainPage) { decorator = new Decorator(stage); decorator.setOnCloseButtonAction(() -> { @@ -134,19 +127,24 @@ public class DecoratorController { // pass key events to current dialog / current page decorator.addEventFilter(KeyEvent.ANY, e -> { - if (!(e.getTarget() instanceof Node)) { - return; // event source can't be determined + if (!(e.getTarget() instanceof Node t)) { + return; } Node newTarget; - if (dialogPane != null && dialogPane.peek().isPresent()) { - newTarget = dialogPane.peek().get(); // current dialog - } else { - newTarget = navigator.getCurrentPage(); // current page + + JFXDialogPane currentDialogPane = null; + if (decorator.getDrawerWrapper() != null) { + currentDialogPane = (JFXDialogPane) decorator.getDrawerWrapper().getProperties().get(DialogUtils.PROPERTY_DIALOG_PANE_INSTANCE); } + if (currentDialogPane != null && currentDialogPane.peek().isPresent()) { + newTarget = currentDialogPane.peek().get(); + } else { + newTarget = navigator.getCurrentPage(); + } boolean needsRedirect = true; - Node t = (Node) e.getTarget(); + while (t != null) { if (t == newTarget) { // current event target is in newTarget @@ -445,81 +443,12 @@ public class DecoratorController { } // ==== Dialog ==== - public void showDialog(Node node) { - FXUtils.checkFxUserThread(); - - if (dialog == null) { - if (decorator.getDrawerWrapper() == null) { - // Sometimes showDialog will be invoked before decorator was initialized. - // Keep trying again. - Platform.runLater(() -> showDialog(node)); - return; - } - dialog = new JFXDialog(AnimationUtils.isAnimationEnabled() - ? JFXDialog.DialogTransition.CENTER - : JFXDialog.DialogTransition.NONE); - dialogPane = new JFXDialogPane(); - - dialog.setContent(dialogPane); - decorator.capableDraggingWindow(dialog); - decorator.forbidDraggingWindow(dialogPane); - dialog.setDialogContainer(decorator.getDrawerWrapper()); - dialog.setOverlayClose(false); - dialog.show(); - - navigator.setDisable(true); - } - dialogPane.push(node); - - EventHandler handler = event -> closeDialog(node); - node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler); - node.addEventHandler(DialogCloseEvent.CLOSE, handler); - - if (dialog.isVisible()) { - dialog.requestFocus(); - if (node instanceof DialogAware) - ((DialogAware) node).onDialogShown(); - } else { - dialog.visibleProperty().addListener(new ChangeListener() { - @Override - public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) { - if (newValue) { - dialog.requestFocus(); - if (node instanceof DialogAware) - ((DialogAware) node).onDialogShown(); - observable.removeListener(this); - } - } - }); - } + DialogUtils.show(decorator, node); } - @SuppressWarnings("unchecked") private void closeDialog(Node node) { - FXUtils.checkFxUserThread(); - - Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER)) - .ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler) handler)); - - if (dialog != null) { - JFXDialogPane pane = dialogPane; - - if (pane.size() == 1 && pane.peek().orElse(null) == node) { - dialog.setOnDialogClosed(e -> pane.pop(node)); - dialog.close(); - dialog = null; - dialogPane = null; - - navigator.setDisable(false); - } else { - pane.pop(node); - } - - if (node instanceof DialogAware) { - ((DialogAware) node).onDialogClosed(); - } - } + DialogUtils.close(node); } // ==== Toast ====