优化独立窗口弹窗 (#5060)

resolves #4942
This commit is contained in:
辞庐
2026-01-02 16:22:54 +08:00
committed by GitHub
parent 3842b6df32
commit fb942d1099
4 changed files with 197 additions and 114 deletions

View File

@@ -0,0 +1,155 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 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;
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<JFXDialog> 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<DialogCloseEvent> 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<? extends Boolean> 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<DialogCloseEvent>) 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();
}
}
}
}

View File

@@ -24,11 +24,11 @@ import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane; import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import javafx.scene.text.Text; import javafx.scene.text.Text;
import javafx.scene.text.TextFlow; 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.setting.StyleSheets;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel; import org.jackhuang.hmcl.util.Log4jLevel;
@@ -84,6 +85,7 @@ public class GameCrashWindow extends Stage {
private final ProcessListener.ExitType exitType; private final ProcessListener.ExitType exitType;
private final LaunchOptions launchOptions; private final LaunchOptions launchOptions;
private final View view; private final View view;
private final StackPane stackPane;
private final List<Log> logs; private final List<Log> logs;
@@ -106,9 +108,10 @@ public class GameCrashWindow extends Stage {
this.view = new View(); this.view = new View();
this.stackPane = new StackPane(view);
this.feedbackTextFlow.getChildren().addAll(FXUtils.parseSegment(i18n("game.crash.feedback"), Controllers::onHyperlinkAction)); 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()); StyleSheets.init(getScene());
setTitle(i18n("game.crash.title")); setTitle(i18n("game.crash.title"));
FXUtils.setIcon(this); FXUtils.setIcon(this);
@@ -297,19 +300,16 @@ public class GameCrashWindow extends Stage {
}); });
}) })
.handleAsync((result, exception) -> { .handleAsync((result, exception) -> {
Alert alert;
if (exception == null) { if (exception == null) {
FXUtils.showFileInExplorer(logFile); 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 { } else {
LOG.warning("Failed to export game crash info", exception); 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; return null;
}, Schedulers.javafx()); }, Schedulers.javafx());
} }

View File

@@ -33,7 +33,6 @@ import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.input.MouseButton; 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.game.Log;
import org.jackhuang.hmcl.setting.StyleSheets; import org.jackhuang.hmcl.setting.StyleSheets;
import org.jackhuang.hmcl.theme.Themes; 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.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.construct.SpinnerPane; import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.util.CircularArrayList; import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Log4jLevel;
import org.jackhuang.hmcl.util.platform.*; import org.jackhuang.hmcl.util.platform.*;
import org.jackhuang.hmcl.util.platform.windows.Dwmapi; import org.jackhuang.hmcl.util.platform.windows.Dwmapi;
import org.jackhuang.hmcl.util.platform.windows.WinConstants; 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.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.Lang.thread; 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.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/** /**
* @author huangyuhui * @author huangyuhui
@@ -199,6 +197,7 @@ public final class LogWindow extends Stage {
private final StringProperty[] buttonText = new StringProperty[LEVELS.length]; private final StringProperty[] buttonText = new StringProperty[LEVELS.length];
private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length]; private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length];
private final JFXComboBox<Integer> cboLines = new JFXComboBox<>(); private final JFXComboBox<Integer> cboLines = new JFXComboBox<>();
private final StackPane stackPane = new StackPane();
LogWindowImpl() { LogWindowImpl() {
getStyleClass().add("log-window"); getStyleClass().add("log-window");
@@ -241,9 +240,8 @@ public final class LogWindow extends Stage {
} }
Platform.runLater(() -> { Platform.runLater(() -> {
Alert 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();
alert.setTitle(i18n("settings.launcher.launcher_log.export")); DialogUtils.show(stackPane, dialog);
alert.showAndWait();
}); });
FXUtils.showFileInExplorer(logFile); FXUtils.showFileInExplorer(logFile);
@@ -267,9 +265,8 @@ public final class LogWindow extends Stage {
LOG.warning("Failed to create minecraft jstack dump", e); LOG.warning("Failed to create minecraft jstack dump", e);
Platform.runLater(() -> { Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR, i18n("logwindow.export_dump")); var dialog = new MessageDialogPane.Builder(i18n("logwindow.export_dump") + "\n" + StringUtils.getStackTrace(e), i18n("message.error"), MessageDialogPane.MessageType.ERROR).ok(null).build();
alert.setTitle(i18n("message.error")); DialogUtils.show(stackPane, dialog);
alert.showAndWait();
}); });
} }
@@ -300,7 +297,9 @@ public final class LogWindow extends Stage {
VBox vbox = new VBox(3); VBox vbox = new VBox(3);
vbox.setPadding(new Insets(3, 0, 3, 0)); vbox.setPadding(new Insets(3, 0, 3, 0));
getChildren().setAll(vbox); getSkinnable().stackPane.getChildren().setAll(vbox);
getChildren().setAll(getSkinnable().stackPane);
{ {
BorderPane borderPane = new BorderPane(); BorderPane borderPane = new BorderPane();

View File

@@ -17,18 +17,13 @@
*/ */
package org.jackhuang.hmcl.ui.decorator; package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXSnackbar; import com.jfoenix.controls.JFXSnackbar;
import javafx.animation.Interpolator; import javafx.animation.Interpolator;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.KeyValue; import javafx.animation.KeyValue;
import javafx.animation.Timeline; import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener; import javafx.beans.WeakInvalidationListener;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.image.Image; 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.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogUtils;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane; 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.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.JFXDialogPane;
import org.jackhuang.hmcl.ui.construct.Navigator;
import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.Refreshable;
import org.jackhuang.hmcl.ui.wizard.WizardProvider; import org.jackhuang.hmcl.ui.wizard.WizardProvider;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
@@ -66,7 +62,9 @@ import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; 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 java.util.stream.Stream;
import static java.util.stream.Collectors.toList; 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; import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DecoratorController { public class DecoratorController {
private static final String PROPERTY_DIALOG_CLOSE_HANDLER = DecoratorController.class.getName() + ".dialog.closeListener";
private final Decorator decorator; private final Decorator decorator;
private final Navigator navigator; private final Navigator navigator;
private JFXDialog dialog;
private JFXDialogPane dialogPane;
public DecoratorController(Stage stage, Node mainPage) { public DecoratorController(Stage stage, Node mainPage) {
decorator = new Decorator(stage); decorator = new Decorator(stage);
decorator.setOnCloseButtonAction(() -> { decorator.setOnCloseButtonAction(() -> {
@@ -134,19 +127,24 @@ public class DecoratorController {
// pass key events to current dialog / current page // pass key events to current dialog / current page
decorator.addEventFilter(KeyEvent.ANY, e -> { decorator.addEventFilter(KeyEvent.ANY, e -> {
if (!(e.getTarget() instanceof Node)) { if (!(e.getTarget() instanceof Node t)) {
return; // event source can't be determined return;
} }
Node newTarget; Node newTarget;
if (dialogPane != null && dialogPane.peek().isPresent()) {
newTarget = dialogPane.peek().get(); // current dialog JFXDialogPane currentDialogPane = null;
} else { if (decorator.getDrawerWrapper() != null) {
newTarget = navigator.getCurrentPage(); // current page 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; boolean needsRedirect = true;
Node t = (Node) e.getTarget();
while (t != null) { while (t != null) {
if (t == newTarget) { if (t == newTarget) {
// current event target is in newTarget // current event target is in newTarget
@@ -445,81 +443,12 @@ public class DecoratorController {
} }
// ==== Dialog ==== // ==== Dialog ====
public void showDialog(Node node) { public void showDialog(Node node) {
FXUtils.checkFxUserThread(); DialogUtils.show(decorator, node);
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<DialogCloseEvent> 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<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (newValue) {
dialog.requestFocus();
if (node instanceof DialogAware)
((DialogAware) node).onDialogShown();
observable.removeListener(this);
}
}
});
}
} }
@SuppressWarnings("unchecked")
private void closeDialog(Node node) { private void closeDialog(Node node) {
FXUtils.checkFxUserThread(); DialogUtils.close(node);
Optional.ofNullable(node.getProperties().get(PROPERTY_DIALOG_CLOSE_HANDLER))
.ifPresent(handler -> node.removeEventHandler(DialogCloseEvent.CLOSE, (EventHandler<DialogCloseEvent>) 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();
}
}
} }
// ==== Toast ==== // ==== Toast ====