diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java index b0bb6a9a772..0220ddc28dc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/theme/Themes.java @@ -1151,10 +1151,28 @@ public void handle(WindowEvent event) { }); } } else if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS && MacOSNativeUtils.isSupported()) { - MacOSNativeUtils.setAppearance(darkModeProperty().get()); + // On macOS, calling NSApplication.setAppearance() overrides the system + // appearance, which locks the JavaFX colorScheme property and prevents it + // from following the system theme. Therefore, only set the appearance when + // the user explicitly chooses "light" or "dark" mode. When in "auto" (follow + // system) mode, leave the appearance unset so the JavaFX colorScheme tracks + // the system automatically. + Runnable syncMacAppearance = () -> { + String mode = settings().themeBrightnessModeProperty().get(); + if ("auto".equalsIgnoreCase(Objects.toString(mode, "").trim())) { + MacOSNativeUtils.resetAppearance(); + } else { + MacOSNativeUtils.setAppearance(darkModeProperty().get()); + } + }; - ChangeListener listener = FXUtils.onWeakChange(Themes.darkModeProperty(), MacOSNativeUtils::setAppearance); - stage.getProperties().put("Themes.applyNativeDarkMode.listener", listener); + syncMacAppearance.run(); + + // Re-evaluate when the user switches between auto / explicit brightness modes. + ChangeListener modeListener = FXUtils.onWeakChange( + settings().themeBrightnessModeProperty(), + mode -> syncMacAppearance.run()); + stage.getProperties().put("Themes.applyNativeDarkMode.modeListener", modeListener); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java index a747c1a34c9..2037fdc2a1b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java @@ -315,6 +315,8 @@ private static final class InstallerItemSkin extends SkinBase { Label statusLabel = new Label(); statusLabel.getStyleClass().add("installer-item-status"); statusLabel.setMouseTransparent(true); + statusLabel.setWrapText(true); + statusLabel.setMaxWidth(Double.MAX_VALUE); pane.getChildren().add(statusLabel); HBox.setHgrow(statusLabel, Priority.ALWAYS); statusLabel.textProperty().bind(Bindings.createStringBinding(() -> { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java index 9d83ce300fd..0fe8089dcb9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/MacOSNativeUtils.java @@ -96,6 +96,22 @@ public static void setAppearance(boolean dark, boolean highContrast) { } } + /// Resets the application appearance to `nil` so the system controls it. + /// Call this when switching to "follow system" mode so that the JavaFX + /// [colorScheme] property continues to track the system theme automatically. + public static void resetAppearance() { + if (nsApp == null) return; + + try { + var objc = ObjectiveCRuntime.INSTANCE; + + Pointer setSel = objc.sel_registerName("setAppearance:"); + objc.objc_msgSend(nsApp, setSel, (Pointer) null); + } catch (Throwable t) { + LOG.warning("Failed to reset macOS appearance", t); + } + } + private MacOSNativeUtils() { } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java index 185c7f61dd6..b8344f415ef 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/FloatScrollBarSkin.java @@ -53,6 +53,7 @@ public FloatScrollBarSkin(final ScrollBar scrollBar) { this.scrollBar = scrollBar; scrollBar.setPrefHeight(1e-18); scrollBar.setPrefWidth(1e-18); + scrollBar.setFocusTraversable(false); this.group = new Region() { Point2D dragStart; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java index f9f90e95904..9ef6166e0ca 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TwoLineListItem.java @@ -25,6 +25,7 @@ import javafx.css.PseudoClass; import javafx.geometry.Pos; import javafx.scene.control.Label; +import javafx.scene.control.OverrunStyle; import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; @@ -45,6 +46,10 @@ public TwoLineListItem() { lblTitle = new Label(); lblTitle.getStyleClass().add("title"); + lblTitle.setTextOverrun(OverrunStyle.ELLIPSIS); + lblTitle.setMaxWidth(Double.MAX_VALUE); + HBox.setHgrow(lblTitle, Priority.SOMETIMES); + lblTitle.setMinWidth(0); this.firstLine = new HBox(lblTitle); firstLine.getStyleClass().add("first-line"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java index b95dd77da72..8f859a83e90 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java @@ -133,7 +133,10 @@ public Cell(ListView listView) { StackPane rootPane = new StackPane(); rootPane.getStyleClass().add("advanced-list-item"); rootPane.getChildren().setAll(ripplerContainer); - rootPane.maxWidthProperty().bind(listView.widthProperty().subtract(5)); + // Allow items to fill the full width; the right padding prevents + // overlap with the floating scrollbar (6px wide at x=-6). + rootPane.maxWidthProperty().bind(listView.widthProperty().subtract(8)); + rootPane.setPadding(new Insets(0, 8, 0, 0)); FXUtils.onClicked(rootPane, () -> { GameItem item = getItem(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java index 01e8a32f5a1..b1597198aa5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/upgrade/ExecutableHeaderHelper.java @@ -116,6 +116,19 @@ public static void copyWithHeader(Path from, Path to) throws IOException { if (header.isPresent()) { out.write(ByteBuffer.wrap(header.get())); } + } else { + // Fallback: when the destination file has no recognizable suffix + // (e.g. the user removed the .sh extension), try to detect and + // preserve the header from the source file by checking all known + // suffixes. This prevents the update from turning an executable + // shell script into a plain .jar file. + for (String knownSuffix : suffix2header.keySet()) { + Optional header = readHeader(zip, knownSuffix); + if (header.isPresent()) { + out.write(ByteBuffer.wrap(header.get())); + break; + } + } } in.transferTo(detectHeaderLength(zip, in), Long.MAX_VALUE, out); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java index 8c1a8de4aea..ef757d4f063 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/LocalModFile.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.addon.mod; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import org.jackhuang.hmcl.addon.LocalAddonFile; @@ -75,14 +76,17 @@ protected void invalidated() { if (isOld()) return; Path path = LocalModFile.this.file.toAbsolutePath(); + boolean newValue = get(); try { - if (get()) + if (newValue) LocalModFile.this.file = modManager.enableMod(path); else LocalModFile.this.file = modManager.disableMod(path); } catch (IOException e) { LOG.error("Unable to invert state of mod file " + path, e); + // Revert so the UI stays consistent with the actual file state. + Platform.runLater(() -> set(!newValue)); } } };