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
6 changes: 0 additions & 6 deletions HMCL/src/main/java/org/jackhuang/hmcl/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,8 @@
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.AsyncTaskExecutor;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WindowsNativeUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.upgrade.UpdateChecker;
import org.jackhuang.hmcl.upgrade.UpdateHandler;
Expand Down Expand Up @@ -142,12 +140,8 @@ public void start(Stage primaryStage) {
Platform.setImplicitExit(false);
Controllers.initialize(primaryStage);

if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS)
Themes.applyNativeDarkMode(primaryStage);

UpdateChecker.init();

WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(primaryStage);
primaryStage.show();
});
} catch (Throwable e) {
Expand Down
75 changes: 71 additions & 4 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.Motion;
Expand Down Expand Up @@ -96,6 +97,9 @@ public final class Controllers {
private static Lazy<RootPage> rootPage = new Lazy<>(RootPage::new);
/// The coordinator for the main window's scene graph and navigation stack.
private static @Nullable Decorator decorator;
/// Whether a transparency change has already queued a main-window style check.
private static boolean windowStyleUpdateQueued;

private static DownloadPage downloadPage;
private static Lazy<AccountListPage> accountListPage = new Lazy<>(() -> {
AccountListPage accountListPage = new AccountListPage();
Expand Down Expand Up @@ -210,6 +214,63 @@ public static void onApplicationStop() {
}
}

/// Installs application identity and native lifecycle integration on a new main stage.
///
/// @param stage the unshown main stage
private static void configureMainStage(Stage stage) {
stage.setOnCloseRequest(event -> Launcher.stopApplication());
FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) {
Themes.applyNativeDarkMode(stage);
}
WindowsNativeUtils.installWindowsAppUserModelRelaunchProperties(stage);
}

/// Replaces the main stage when transparency requires a different decoration style.
///
/// The retained scene, navigation, dialogs, normal bounds, and window state are preserved. A hidden window
/// remains hidden. This method must run on the JavaFX application thread after theme bindings have updated.
private static void updateMainWindowStyle() {
@Nullable Decorator currentDecorator = decorator;
if (currentDecorator == null || !currentDecorator.isStageStyleOutdated()) {
return;
}
@Nullable Stage previousStage = currentDecorator.getStage();
if (previousStage == null) {
return;
}

boolean showing = previousStage.isShowing();
boolean maximized = previousStage.isMaximized();
boolean fullScreen = previousStage.isFullScreen();
boolean iconified = previousStage.isIconified();
@Nullable Node focusOwner = previousStage.getScene().getFocusOwner();

Stage replacement = new Stage();
configureMainStage(replacement);
replacement.setTitle(previousStage.getTitle());
replacement.getIcons().setAll(previousStage.getIcons());
replacement.setOnCloseRequest(previousStage.getOnCloseRequest());
replacement.setResizable(previousStage.isResizable());
replacement.setAlwaysOnTop(previousStage.isAlwaysOnTop());
replacement.setFullScreenExitHint(previousStage.getFullScreenExitHint());
replacement.setFullScreenExitKeyCombination(previousStage.getFullScreenExitKeyCombination());

currentDecorator.detachStage();
previousStage.hide();
currentDecorator.attachStage(replacement);
replacement.setMaximized(maximized);
replacement.setFullScreen(fullScreen);
replacement.setIconified(iconified);
if (showing) {
replacement.show();
}
if (focusOwner != null) {
focusOwner.requestFocus();
}
}

/// Initializes the main application stage, scene graph, and background services.
///
/// @param stage the primary application stage, which must not have been shown
Expand All @@ -230,10 +291,19 @@ public static void initialize(Stage stage) {
}
}

stage.setOnCloseRequest(e -> Launcher.stopApplication());
configureMainStage(stage);

decorator = new Decorator(getRootPage());
Scene mainScene = decorator.attachStage(stage);
Themes.windowTransparentProperty().addListener((observable, oldValue, newValue) -> {
if (!windowStyleUpdateQueued) {
windowStyleUpdateQueued = true;
Platform.runLater(() -> {
windowStyleUpdateQueued = false;
updateMainWindowStyle();
});
}
});
getRootPage().getMainPage().showUpdateProperty().bind(UpdateChecker.checkingUpdateProperty().not().and(UpdateChecker.outdatedProperty()));
getRootPage().getMainPage().showUpdateDialogProperty().bind(
decorator.backableProperty().not()
Expand All @@ -251,9 +321,6 @@ public static void initialize(Stage stage) {

StyleSheets.init(mainScene);

FXUtils.setIcon(stage);
stage.setTitle(Metadata.FULL_TITLE);

if (!Architecture.SYSTEM_ARCH.isX86() && SettingsManager.userState().platformPromptVersionProperty().get() < 1) {
Runnable continueAction = () -> {
UserState userState = userState();
Expand Down
92 changes: 71 additions & 21 deletions HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorDnD;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogUtils;
import org.jackhuang.hmcl.ui.FXUtils;
Expand Down Expand Up @@ -129,6 +130,12 @@ public final class Decorator {
/// The navigation stack rendered in the main window.
private final Navigator navigator = new Navigator();

/// Reflective access to system decoration, or `null` on unsupported runtimes.
private final @Nullable NativeWindowDecoration nativeDecoration = NativeWindowDecoration.create();

/// Whether the attached stage delegates window decoration and gestures to the platform.
private boolean nativeDecorationEnabled;

/// The clipped main-window content hosted inside [#shadowContainer].
private final MainWindowPane mainWindowPane;

Expand Down Expand Up @@ -287,7 +294,7 @@ public Parent getRoot() {

/// Returns the insets reserved outside the main-window content.
///
/// @return the root's shadow insets for a normal window, or [Insets#EMPTY] while maximized or full-screen
/// @return the custom shadow insets, or [Insets#EMPTY] with native decoration, maximization, or full-screen
public Insets getWindowInsets() {
return root.getPadding();
}
Expand Down Expand Up @@ -318,9 +325,10 @@ private Insets getResizeInsets() {
///
/// @param edgeToEdge whether the attached stage is maximized or full-screen
private void updateWindowDecoration(boolean edgeToEdge) {
root.setPadding(edgeToEdge ? Insets.EMPTY : SHADOW_INSETS);
shadowContainer.setEffect(edgeToEdge ? null : windowShadow);
mainWindowPane.setWindowEdgeToEdge(edgeToEdge);
boolean customDecoration = !nativeDecorationEnabled && !edgeToEdge;
root.setPadding(customDecoration ? SHADOW_INSETS : Insets.EMPTY);
shadowContainer.setEffect(customDecoration ? windowShadow : null);
mainWindowPane.setWindowCornersRounded(customDecoration);
}

/// Returns the pane on which application dialogs are stacked.
Expand Down Expand Up @@ -405,7 +413,14 @@ public void startWizard(WizardProvider wizardProvider, @Nullable String category
///
/// @param node the drag-enabled node
public void capableDraggingWindow(Node node) {
node.addEventHandler(MouseEvent.MOUSE_MOVED, event -> allowMove = true);
if (nativeDecoration != null) {
nativeDecoration.setDraggable(node, true);
}
node.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
if (!nativeDecorationEnabled) {
allowMove = true;
}
});
node.addEventHandler(MouseEvent.MOUSE_EXITED, event -> {
if (!dragging) {
allowMove = false;
Expand All @@ -425,9 +440,14 @@ void registerTitleBar(Node node) {
///
/// @param node the node that must consume drag eligibility
public void forbidDraggingWindow(Node node) {
if (nativeDecoration != null) {
nativeDecoration.setDraggable(node, false);
}
node.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
allowMove = false;
event.consume();
if (!nativeDecorationEnabled) {
allowMove = false;
event.consume();
}
});
}

Expand All @@ -436,7 +456,8 @@ public void forbidDraggingWindow(Node node) {
/// @param event the title-bar click event
private void onTitleBarDoubleClick(MouseEvent event) {
@Nullable Stage currentStage = stage;
if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
if (nativeDecorationEnabled
|| OperatingSystem.CURRENT_OS == OperatingSystem.MACOS
|| currentStage == null
|| event.getButton() != MouseButton.PRIMARY
|| event.getClickCount() != 2) {
Expand All @@ -452,7 +473,7 @@ private void onTitleBarDoubleClick(MouseEvent event) {
/// @param event the title-bar drag event
private void onTitleBarDragged(MouseEvent event) {
@Nullable Stage currentStage = stage;
if (currentStage == null || dragging || !currentStage.isMaximized()) {
if (nativeDecorationEnabled || currentStage == null || dragging || !currentStage.isMaximized()) {
return;
}

Expand Down Expand Up @@ -531,7 +552,7 @@ private void resizeStage(double newWidth, double newHeight) {
/// @param event the pointer movement event
private void onMouseMoved(MouseEvent event) {
@Nullable Stage currentStage = stage;
if (currentStage == null
if (nativeDecorationEnabled || currentStage == null
|| currentStage.isIconified()
|| currentStage.isFullScreen()
|| currentStage.isMaximized()
Expand Down Expand Up @@ -597,7 +618,7 @@ private void onMouseReleased(MouseEvent event) {
/// @param event the primary-button drag event
private void onMouseDragged(MouseEvent event) {
@Nullable Stage currentStage = stage;
if (currentStage == null
if (nativeDecorationEnabled || currentStage == null
|| currentStage.isIconified()
|| currentStage.isFullScreen()
|| currentStage.isMaximized()
Expand Down Expand Up @@ -712,27 +733,51 @@ private void initializeStageBounds(Stage targetStage) {
contentHeight.set(initialContentHeight);
}

/// Returns the preferred style for the current runtime and effective transparency setting.
///
/// @return the system style when available and opaque, otherwise the custom transparent style
private StageStyle preferredStageStyle() {
return nativeDecoration != null && !Themes.windowTransparentProperty().get()
? nativeDecoration.style : StageStyle.TRANSPARENT;
}

/// Returns whether the current stage must be replaced to apply the effective transparency setting.
///
/// @return `true` if a stage is attached with a different style from the current preference
public boolean isStageStyleOutdated() {
return stage != null && stage.getStyle() != preferredStageStyle();
}

/// Attaches the retained scene and native window behavior to `newStage`.
///
/// If the root has no scene, this method reuses `newStage`'s scene and replaces its root, or creates a
/// transparent scene when the stage has none. If the retained scene belongs to another stage, it is detached
/// from that stage before being installed on `newStage`. A newly attached stage receives the persisted normal
/// content bounds and minimum size adjusted for the normal decoration insets. The stage and retained scene use
/// transparent styles required by the custom decoration. Any active window animation is cancelled and reset.
/// When window animations are enabled, each subsequent showing starts the opening animation. This method does
/// content bounds and minimum size adjusted for the normal decoration insets. Opaque windows use the system
/// decoration when supported; other windows use the custom transparent decoration. Any active window animation
/// is cancelled and reset. Custom window animations run only with custom decoration. This method does
/// not show the stage.
///
/// @param newStage the stage to attach, which must be accessed on the JavaFX application thread
/// @return the scene installed on `newStage`
/// @throws IllegalStateException if `newStage` has already been shown with a non-transparent style, or if the
/// @throws IllegalStateException if `newStage` has already been shown with a different style, or if the
/// retained root or scene cannot be transferred from its current owner
public Scene attachStage(Stage newStage) {
FXUtils.checkFxUserThread();
stopWindowAnimation();

if (newStage.getStyle() != StageStyle.TRANSPARENT) {
newStage.initStyle(StageStyle.TRANSPARENT);
StageStyle style = preferredStageStyle();
if (newStage.getStyle() != style) {
newStage.initStyle(style);
}
nativeDecorationEnabled = nativeDecoration != null && style == nativeDecoration.style;
if (nativeDecoration != null) {
nativeDecoration.setContent(null);
if (nativeDecorationEnabled) {
nativeDecoration.configureStage(newStage);
}
}
mainWindowPane.setNativeDecoration(nativeDecorationEnabled ? nativeDecoration : null);

@Nullable Scene retainedScene = root.getScene();
Scene scene;
Expand All @@ -759,7 +804,7 @@ public Scene attachStage(Stage newStage) {
}
}

scene.setFill(Color.TRANSPARENT);
scene.setFill(nativeDecorationEnabled ? Color.WHITE : Color.TRANSPARENT);

if (newStage.getScene() != scene) {
newStage.setScene(scene);
Expand Down Expand Up @@ -880,7 +925,7 @@ void setNavigationDirection(Navigation.NavigationDirection navigationDirection)
/// Calling this method replaces any active minimize, restore, close, or earlier opening animation. If window
/// animations are disabled, it restores the stable root transform without starting an animation.
private void playOpenAnimation() {
if (!AnimationUtils.playWindowAnimation()) {
if (nativeDecorationEnabled || !AnimationUtils.playWindowAnimation()) {
stopWindowAnimation();
Controllers.trimHeap();
return;
Expand Down Expand Up @@ -915,7 +960,7 @@ void minimizeWindow() {
return;
}

if (AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
if (!nativeDecorationEnabled && AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
Expand Down Expand Up @@ -946,7 +991,7 @@ void minimizeWindow() {

/// Closes the application, using the configured window animation when enabled.
void closeWindow() {
if (AnimationUtils.playWindowAnimation()) {
if (!nativeDecorationEnabled && AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
Expand Down Expand Up @@ -1074,6 +1119,11 @@ private void setupInputRouting() {

/// Restores the root node's transform after an animated minimization.
private void playRestoreAnimation() {
if (nativeDecorationEnabled) {
stopWindowAnimation();
return;
}

Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 0, Motion.EASE),
Expand Down
Loading