Compare commits

...
Author SHA1 Message Date
layue13 b74b53fbf9 fix(ui): hard-fix JFXButton pressed feedback
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 3m28s
Check Codes / build (pull_request) Successful in 2m42s
Java CI / build (push) Failing after 3h0m6s
Java CI / build (pull_request) Failing after 3h0m8s
Stop JFXButtonSkin wiping Modena/CSS press fills to transparent; add
armed overlay (bg+border) and last-loaded press-feedback.css so every
icon/toolbar/launch button shows obvious :pressed/:armed state.
2026-11-07 20:57:18 +00:00
layue13 22d982a089 fix(ui): skill palette chrome + icon press feedback
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 2m59s
Check Codes / build (pull_request) Successful in 2m52s
Java CI / build (push) Failing after 3h0m2s
Java CI / build (pull_request) Failing after 3h0m2s
Pin Java title/frame surfaces to skill warm light/dark hex (kill Monet
pink/teal drift), override secondary/tertiary to orange-only, and add
visible pressed washes for toggle-icon / toolbar icon buttons.
2026-11-07 20:52:15 +00:00
layue13 07369a3aba fix(ui): golden-ratio chrome layout hierarchy
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 2m45s
Check Codes / build (pull_request) Successful in 3m5s
Java CI / build (push) Failing after 3h0m1s
Java CI / build (pull_request) Failing after 3h0m1s
Apply φ≈1.618 sidebar:content splits and φ spacing ladder for
title:body tool-density chrome (LayoutMetrics), keeping tactility.
2026-11-07 20:46:46 +00:00
layue13 e69464105a fix(ui): align launcher chrome to anthropic-style-cn skill
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 2m49s
Check Codes / build (pull_request) Successful in 2m55s
Java CI / build (push) Failing after 3h0m1s
Java CI / build (pull_request) Failing after 3h0m2s
Map skill light/dark surface/text/border pairs onto Monet vars, lock
warm orange CTA #D97757, skill motion (150/250/400 + cubic 0.16,1,0.3,1),
tool-density spacing, and keep Source Han sans/serif roles.
2026-11-07 20:43:01 +00:00
layue13 38d764c7d1 fix(ui): dual-mode tokens and tactile button feedback
Replace light-only hardcodes with Monet/Anthropic tokens for light and
dark, inject hover/press/focus washes from the active scheme, and restore
subtle scale+tone feedback without Material ripples.
2026-11-07 20:40:53 +00:00
layue13 69b415e5ef fix(ui): calm Material motion for Anthropic chrome
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 2m45s
Check Codes / build (pull_request) Successful in 4m56s
Java CI / build (push) Failing after 3h0m2s
Java CI / build (pull_request) Failing after 3h0m1s
Disable JFX ripples, replace emphasized/overshoot transitions with
short ease-out fades, and soften window open/minimize/close scaling.
2026-11-07 20:36:21 +00:00
layue13 64c63b2af0 feat(ui): Anthropic-inspired chrome with Source Han pairing
Calm olive-slate Monet seed, surface title bar, quieter cards, TDLM
branding, and hard-bound Source Han Sans Bold (titles) + Source Han
Serif (body) via UI-charset OFL subsets (~2MB).
2026-11-07 20:36:17 +00:00
浩 刘 3f67b6bda0 fix: store managed Java under the launcher workspace
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 2m48s
Java CI / build (push) Failing after 3h0m2s
Install Mojang/runtime downloads into <working-dir>/java beside
.minecraft instead of the user-global HMCL profile java store.
2026-11-07 20:35:13 +00:00
浩 刘 4cd783d6bb feat: offer Offline and Microsoft auth; keep authlib injector
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 4m59s
Java CI / build (push) Failing after 3h0m0s
Primary account creation entries are Offline and Microsoft. External
Yggdrasil / authlib-injector servers remain listed and addable for later
use. Offline creation is no longer redirected away or region-restricted.
2026-11-07 20:27:29 +00:00
浩 刘 4118030d9c feat: auto-fetch Mojang Java 25; strip April Fools, task list, unused locales
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 3m50s
Java CI / build (push) Failing after 3h0m1s
On first-capable startup, download java-runtime-epsilon via the existing
BMCLAPI-mirrored Mojang path when missing (progress dialog only; no prompt).

Remove launcher April Fools prompts/settings/UI tags, the top-right task
center page/button, dead NBT/April Fools images, and unused locale packs
(ar/de/es/ja/ru/uk) to shrink the distributable.
2026-11-07 20:19:40 +00:00
浩 刘 bcc5cda5c0 style: clear checkstyle leftovers from TDLM fork cuts
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Successful in 4m39s
Java CI / build (push) Failing after 3h0m3s
Remove unused/redundant imports, fix SettingsPage indentation, and
collapse extra blank lines so checkstyle/checkTranslations can pass on
Gitea Actions without reviewdog.
2026-11-07 19:33:20 +00:00
浩 刘 1147892830 ci: drop unmirrored reviewdog and github-script workflows
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 10m17s
Java CI / build (push) Failing after 3h1m7s
Gitea act resolves actions from git.nest.potatolab.uk; reviewdog/action-setup
and actions/github-script 404 there (even for conditional steps). Keep Gradle
checkstyle/checkTranslations only; disable PR comment and size-label flows.
2026-11-07 19:16:41 +00:00
浩 刘 a4fff896ea ci: install xvfb before Gradle build on self-hosted runners
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (push) Failing after 3h0m4s
Self-hosted Gitea Actions runners lack xvfb by default, so xvfb-run
fails with exit 127. Install it in the Java CI workflow before build.
2026-11-07 19:02:04 +00:00
layue13 5e2da98061 Merge pull request 'Remove official groups, multiplayer, downloads, help, and self-update' (#1)
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Check Codes / build (push) Failing after 2m20s
Java CI / build (push) Failing after 4m0s
Merge pull request #1: Remove official groups, multiplayer, downloads, help, and self-update

Server-client HMCL fork cleanup for TDLM.
2026-11-07 18:56:23 +00:00
93 changed files with 1465 additions and 11020 deletions

No files matched your search

+8 -27
View File
@@ -1,10 +1,11 @@
name: Check Codes Comment
# Disabled on this Gitea fork: the upstream flow depends on reviewdog
# (missing from git.nest.potatolab.uk) and a checkstyle-artifact produced by
# check-codes.yml. Re-enable only after those actions are mirrored or the
# comment path is rewritten without reviewdog.
on:
workflow_run:
workflows: ["Check Codes"]
types:
- completed
workflow_dispatch:
permissions:
actions: read
@@ -14,27 +15,7 @@ permissions:
jobs:
comment:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'failure'
if: false
steps:
- name: Checkout Script
uses: actions/checkout@v7
with:
sparse-checkout: |
.github/scripts/check-codes-comment.js
sparse-checkout-cone-mode: false
- name: Download Artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: checkstyle-artifact
path: ${{ runner.temp }}/checkstyle-artifact
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Review Comment
uses: actions/github-script@v9
env:
REPORTS_PATH: ${{ runner.temp }}/checkstyle-artifact/ALL_REPORTS.jsonl
PULL_REQUEST_NUMBER_PATH: ${{ runner.temp }}/checkstyle-artifact/PULL_REQUEST_NUMBER
with:
script: |
const script = require("./.github/scripts/check-codes-comment.js");
await script({ github, context, core });
- name: Disabled
run: echo "check-codes-comment is disabled on this fork"
+6 -26
View File
@@ -1,14 +1,17 @@
name: Check Codes
on:
workflow_dispatch:
push:
paths:
- '**.java'
- '**.properties'
- '.github/workflows/check-codes.yml'
pull_request:
paths:
- '**.java'
- '**.properties'
- '.github/workflows/check-codes.yml'
jobs:
build:
@@ -25,31 +28,8 @@ jobs:
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
# Keep only Gradle checks. reviewdog/action-setup is not mirrored on
# git.nest.potatolab.uk (404), so PR-comment artifact steps are omitted
# for Gitea Actions / act runners.
- name: Check Codes
run: ./gradlew checkstyle checkTranslations --no-daemon --parallel --stacktrace
- if: failure() && github.event_name == 'pull_request'
name: Setup reviewdog
uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0
with:
reviewdog_version: v0.21.0
- if: failure() && github.event_name == 'pull_request'
name: Generate Artifact
run: |
echo "${{ github.event.number }}" > PULL_REQUEST_NUMBER
> ALL_REPORTS.jsonl
find . -path "*/build/reports/checkstyle/*.xml" | while read -r xml_file; do
if [ -s "$xml_file" ]; then
echo "Processing $xml_file..."
reviewdog -f=checkstyle -name="checkstyle" -reporter=rdjsonl < "$xml_file" >> ALL_REPORTS.jsonl
else
echo "Skipping empty or missing report: $xml_file"
fi
done
- if: failure() && github.event_name == 'pull_request'
name: Upload Artifact
uses: actions/upload-artifact@v7
with:
name: checkstyle-artifact
path: |
ALL_REPORTS.jsonl
PULL_REQUEST_NUMBER
+4
View File
@@ -23,6 +23,10 @@ jobs:
uses: gradle/actions/setup-gradle@v5
with:
cache-cleanup: never
- name: Install Xvfb
run: |
sudo apt-get update
sudo apt-get install -y xvfb
- name: Build with Gradle
run: xvfb-run --auto-servernum ./gradlew build --no-daemon --parallel
env:
+6 -17
View File
@@ -1,11 +1,9 @@
name: PR Size Label
# Disabled on this Gitea fork: act resolves actions/github-script from
# git.nest.potatolab.uk and that mirror 404s. Re-enable after mirroring
# actions/github-script or replacing the labeler with a plain shell step.
on:
pull_request_target:
types:
- opened
- reopened
- synchronize
workflow_dispatch:
permissions:
@@ -16,16 +14,7 @@ permissions:
jobs:
label:
runs-on: ubuntu-latest
if: false
steps:
- name: Checkout scripts
uses: actions/checkout@v7
with:
sparse-checkout: |
.github/scripts/pr-size-label.js
sparse-checkout-cone-mode: false
- name: Label PR by filtered changed lines
uses: actions/github-script@v9
with:
script: |
const script = require("./.github/scripts/pr-size-label.js");
await script({ github, context, core });
- name: Disabled
run: echo "pr-size-label is disabled on this fork"
@@ -23,6 +23,10 @@ import javafx.scene.control.Label;
import javafx.scene.effect.DropShadow;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderStroke;
import javafx.scene.layout.BorderStrokeStyle;
import javafx.scene.layout.BorderWidths;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
@@ -30,8 +34,20 @@ import javafx.scene.shape.Shape;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
/// JFoenix button skin with Anthropic skill tactile press feedback.
///
/// Upstream skin rewrote Modena's cyan `:pressed` fill to transparent and thereby
/// also ate author CSS press backgrounds. This build keeps CSS fills, maps the
/// Modena cyan to a skill wash, and paints an unmistakable armed overlay
/// (background + border + opacity) on every JFXButton.
public class JFXButtonSkin extends ButtonSkin {
private static final Color SKILL_PRESS_FILL = Color.web("#D97757", 0.40);
private static final Color SKILL_PRESS_BORDER = Color.web("#C96442");
private static final Color SKILL_PRESS_DIM = Color.web("#141413", 0.28);
private final StackPane buttonContainer = new StackPane();
/// Full-bleed press veil — opacity toggled from armed/pressed, independent of CSS.
private final StackPane pressOverlay = new StackPane();
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
protected Node getMask() {
StackPane mask = new StackPane();
@@ -64,6 +80,17 @@ public class JFXButtonSkin extends ButtonSkin {
public JFXButtonSkin(JFXButton button) {
super(button);
this.pressOverlay.setMouseTransparent(true);
this.pressOverlay.setOpacity(0);
this.pressOverlay.setBackground(new Background(new BackgroundFill(
SKILL_PRESS_FILL, this.defaultRadii, Insets.EMPTY)));
this.pressOverlay.setBorder(new Border(new BorderStroke(
SKILL_PRESS_BORDER,
BorderStrokeStyle.SOLID,
this.defaultRadii,
new BorderWidths(2.0))));
this.getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
if (newVal) {
this.releaseManualRippler = this.buttonRippler.createManualRipple();
@@ -81,21 +108,23 @@ public class JFXButtonSkin extends ButtonSkin {
this.clickedAnimation.play();
}
}
this.updatePressOverlay(button);
});
this.buttonContainer.getChildren().add(this.buttonRippler);
this.buttonContainer.getChildren().addAll(this.buttonRippler, this.pressOverlay);
button.buttonTypeProperty().addListener((o, oldVal, newVal) -> this.updateButtonType(newVal));
button.setOnMousePressed((e) -> {
if (this.clickedAnimation != null) {
this.clickedAnimation.setRate(1.0F);
this.clickedAnimation.play();
}
this.updatePressOverlay(button);
});
button.setOnMouseReleased((e) -> {
if (this.clickedAnimation != null) {
this.clickedAnimation.setRate(-1.0F);
this.clickedAnimation.play();
}
this.updatePressOverlay(button);
});
ReadOnlyBooleanProperty focusVisibleProperty = FXUtils.focusVisibleProperty(button);
@@ -111,20 +140,34 @@ public class JFXButtonSkin extends ButtonSkin {
this.buttonRippler.hideOverlay();
}
});
button.pressedProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.hideOverlay());
button.pressedProperty().addListener((o, oldVal, newVal) -> {
this.buttonRippler.hideOverlay();
this.updatePressOverlay(button);
});
button.setPickOnBounds(false);
this.buttonContainer.setPickOnBounds(false);
this.buttonContainer.shapeProperty().bind(this.getSkinnable().shapeProperty());
this.buttonContainer.borderProperty().bind(this.getSkinnable().borderProperty());
// Mirror CSS background onto the container. Never call button.setBackground() here —
// that used to stomp author :pressed fills (and wiped Modena cyan to transparent).
this.buttonContainer.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
if (button.getBackground() == null || this.isJavaDefaultBackground(button.getBackground()) || this.isJavaDefaultClickedBackground(button.getBackground())) {
button.setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, null)));
Background background = button.getBackground();
if (background == null || background.getFills().isEmpty() || this.isJavaDefaultBackground(background)) {
return new Background(new BackgroundFill(Color.TRANSPARENT, this.defaultRadii, Insets.EMPTY));
}
if (this.isJavaDefaultClickedBackground(background)) {
CornerRadii radii = background.getFills().get(0).getRadii();
return new Background(new BackgroundFill(SKILL_PRESS_FILL,
radii != null ? radii : this.defaultRadii, Insets.EMPTY));
}
try {
return new Background(new BackgroundFill(this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getFill() : Color.TRANSPARENT, this.getSkinnable().getBackground() != null ? this.getSkinnable().getBackground().getFills().get(0).getRadii() : this.defaultRadii, Insets.EMPTY));
BackgroundFill fill = background.getFills().get(0);
return new Background(new BackgroundFill(
fill.getFill(),
fill.getRadii() != null ? fill.getRadii() : this.defaultRadii,
Insets.EMPTY));
} catch (Exception var3) {
return this.getSkinnable().getBackground();
return background;
}
}, this.getSkinnable().backgroundProperty()));
button.ripplerFillProperty().addListener((o, oldVal, newVal) -> this.buttonRippler.setRipplerFill(newVal));
@@ -134,6 +177,40 @@ public class JFXButtonSkin extends ButtonSkin {
this.updateButtonType(button.getButtonType());
this.updateChildren();
this.updatePressOverlay(button);
}
/// Shows a hard-to-miss press veil: orange wash + warm border; dimming on raised CTAs.
private void updatePressOverlay(JFXButton button) {
boolean active = !button.isDisabled() && (button.isArmed() || button.isPressed());
boolean raised = button.getButtonType() == JFXButton.ButtonType.RAISED
|| button.getStyleClass().contains("jfx-button-raised")
|| button.getStyleClass().contains("launch-button")
|| button.getStyleClass().contains("menu-button");
CornerRadii radii = this.defaultRadii;
Background bg = button.getBackground();
if (bg != null && !bg.getFills().isEmpty() && bg.getFills().get(0).getRadii() != null) {
radii = bg.getFills().get(0).getRadii();
}
Color fill = raised ? SKILL_PRESS_DIM : SKILL_PRESS_FILL;
this.pressOverlay.setBackground(new Background(new BackgroundFill(fill, radii, Insets.EMPTY)));
this.pressOverlay.setBorder(new Border(new BorderStroke(
SKILL_PRESS_BORDER,
BorderStrokeStyle.SOLID,
radii,
new BorderWidths(active ? 2.0 : 0.0))));
this.pressOverlay.setOpacity(active ? 1.0 : 0.0);
// Drive opacity via style class so CSS keeps ownership (no setOpacity stomping).
final String armedClass = "anthropic-armed";
if (active) {
if (!button.getStyleClass().contains(armedClass)) {
button.getStyleClass().add(armedClass);
}
} else {
button.getStyleClass().remove(armedClass);
}
}
protected void updateChildren() {
@@ -186,6 +263,7 @@ public class JFXButtonSkin extends ButtonSkin {
private boolean isJavaDefaultClickedBackground(Background background) {
try {
// Modena default armed cyan — replace with skill wash instead of wiping to transparent.
return "0x039ed3ff".equals(background.getFills().get(0).getFill().toString());
} catch (Exception var3) {
return false;
@@ -19,8 +19,8 @@ public final class Metadata {
private Metadata() {
}
public static final String NAME = "HMCL";
public static final String FULL_NAME = "Hello Minecraft! Launcher";
public static final String NAME = "TDLM";
public static final String FULL_NAME = "TDLM";
public static final String VERSION = System.getProperty("hmcl.version.override", JarUtils.getAttribute("hmcl.version", "@develop@"));
/// Explicit Application User Model ID used for Windows taskbar grouping and pinning.
@@ -455,9 +455,9 @@ public class HMCLGameInstance extends DefaultGameInstance {
return GameInstanceIconType.OPTIFINE.getIcon();
GameVersionNumber version = getVersion();
if (version.isAprilFools())
return GameInstanceIconType.APRIL_FOOLS.getIcon();
else if (version instanceof GameVersionNumber.LegacySnapshot || version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
if (version instanceof GameVersionNumber.LegacySnapshot
|| version.isAprilFools()
|| version instanceof GameVersionNumber.Release release && release.getEaType() != GameVersionNumber.Release.ReleaseType.GA)
return GameInstanceIconType.COMMAND.getIcon();
else if (version instanceof GameVersionNumber.Old)
return GameInstanceIconType.CRAFT_TABLE.getIcon();
@@ -61,8 +61,12 @@ public final class JavaManager {
"Semeru"
};
public static final HMCLJavaRepository REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_USER_HOME.resolve("java"));
public static final HMCLJavaRepository LOCAL_REPOSITORY = new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
/// Managed Mojang/Disco Java installs for this workspace (beside `.minecraft`), not the user-global HMCL profile.
public static final HMCLJavaRepository REPOSITORY =
new HMCLJavaRepository(Metadata.CURRENT_DIRECTORY.resolve("java"));
/// Optional portable installs under the launcher-local `.hmcl` directory.
public static final HMCLJavaRepository LOCAL_REPOSITORY =
new HMCLJavaRepository(Metadata.HMCL_LOCAL_HOME.resolve("java"));
public static String getMojangJavaPlatform(Platform platform) {
if (platform.getOperatingSystem() == OperatingSystem.WINDOWS) {
@@ -55,8 +55,12 @@ public final class Accounts {
public static final MicrosoftAccountFactory FACTORY_MICROSOFT = new MicrosoftAccountFactory(new MicrosoftService(OAUTH_CALLBACK));
/// Account factories offered when creating a new account from the UI.
///
/// Offline login remains loadable for existing accounts, but is no longer offered for creation.
public static final List<AccountFactory<?>> FACTORIES = List.of(FACTORY_MICROSOFT, FACTORY_AUTHLIB_INJECTOR);
/// Primary: Offline and Microsoft. Authlib-injector remains available for external Yggdrasil servers.
public static final List<AccountFactory<?>> FACTORIES = List.of(
FACTORY_OFFLINE,
FACTORY_MICROSOFT,
FACTORY_AUTHLIB_INJECTOR
);
// ==== login type / account factory mapping ====
private static final Map<String, AccountFactory<?>> type2factory = new HashMap<>();
@@ -36,7 +36,22 @@ public final class FontManager {
public static final double DEFAULT_FONT_SIZE = 12.0f;
/// Bundled Anthropic UI title face (Source Han Sans SC Bold subset).
public static final String TITLE_FONT_FAMILY = "Source Han Sans SC";
/// Bundled Anthropic UI body face (Source Han Serif SC Regular subset).
public static final String BODY_FONT_FAMILY = "Source Han Serif SC";
private static final String BUNDLED_TITLE_FONT = "/assets/fonts/SourceHanSansSC-Bold.otf";
private static final String BUNDLED_BODY_FONT = "/assets/fonts/SourceHanSerifSC-Regular.otf";
private static volatile boolean bundledFontsLoaded;
private static volatile @Nullable String loadedTitleFamily;
private static volatile @Nullable String loadedBodyFamily;
private static final Lazy<Font> DEFAULT_FONT = new Lazy<>(() -> {
ensureBundledFontsLoaded();
Font font;
// Recommended
@@ -70,6 +85,11 @@ public final class FontManager {
return font;
}
// Anthropic UI hard-bind: bundled Source Han Serif body face
if (loadedBodyFamily != null) {
return Font.font(loadedBodyFamily, DEFAULT_FONT_SIZE);
}
// Default
String fcMatchPattern;
@@ -83,8 +103,62 @@ public final class FontManager {
private static final ObjectProperty<FontReference> font = new SimpleObjectProperty<>();
static {
ensureBundledFontsLoaded();
updateFont();
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System"));
LOG.info("Font: " + (font.get() != null ? font.get().family() : "System")
+ ", title=" + Objects.requireNonNullElse(loadedTitleFamily, "unavailable")
+ ", body=" + Objects.requireNonNullElse(loadedBodyFamily, "unavailable"));
}
/// Registers the bundled Source Han subsets so CSS family names resolve.
public static void ensureBundledFontsLoaded() {
if (bundledFontsLoaded) {
return;
}
synchronized (FontManager.class) {
if (bundledFontsLoaded) {
return;
}
loadedTitleFamily = loadBundledFont(BUNDLED_TITLE_FONT, TITLE_FONT_FAMILY);
loadedBodyFamily = loadBundledFont(BUNDLED_BODY_FONT, BODY_FONT_FAMILY);
bundledFontsLoaded = true;
}
}
/// Returns the resolved title font family after bundled registration, or the hard-bind name.
public static String getTitleFontFamily() {
ensureBundledFontsLoaded();
return Objects.requireNonNullElse(loadedTitleFamily, TITLE_FONT_FAMILY);
}
/// Returns the resolved body font family after bundled registration, or the hard-bind name.
public static String getBodyFontFamily() {
ensureBundledFontsLoaded();
return Objects.requireNonNullElse(loadedBodyFamily, BODY_FONT_FAMILY);
}
private static @Nullable String loadBundledFont(String resourcePath, String expectedFamily) {
try (var input = FontManager.class.getResourceAsStream(resourcePath)) {
if (input == null) {
LOG.warning("Bundled font missing: " + resourcePath);
return null;
}
Font loaded = Font.loadFont(input, DEFAULT_FONT_SIZE);
if (loaded == null) {
LOG.warning("Failed to decode bundled font: " + resourcePath);
return null;
}
String family = loaded.getFamily();
LOG.info("Loaded bundled font " + resourcePath + " as family '" + family + "'");
if (!expectedFamily.equalsIgnoreCase(family)) {
LOG.warning("Bundled font family mismatch for " + resourcePath
+ ": expected '" + expectedFamily + "', got '" + family + "'");
}
return family;
} catch (Exception e) {
LOG.warning("Failed to load bundled font " + resourcePath, e);
return null;
}
}
private static void updateFont() {
@@ -96,7 +170,7 @@ public final class FontManager {
if (fontFamily == null) {
Font defaultFont = DEFAULT_FONT.get();
font.set(defaultFont != null ? new FontReference(defaultFont) : null);
font.set(defaultFont != null ? new FontReference(defaultFont) : new FontReference(BODY_FONT_FAMILY));
} else {
font.set(new FontReference(fontFamily));
}
@@ -22,7 +22,8 @@ public enum GameInstanceIconType {
NEO_FORGE("/assets/img/neoforge.png"),
FURNACE("/assets/img/furnace.png"),
QUILT("/assets/img/quilt.png"),
APRIL_FOOLS("/assets/img/april_fools.png"),
/// Retained for settings migration; UI no longer offers this icon.
APRIL_FOOLS("/assets/img/command.png"),
CLEANROOM("/assets/img/cleanroom.png"),
LEGACY_FABRIC("/assets/img/legacyfabric.png")
;
@@ -179,15 +179,6 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Whether April Fools features are disabled.
@SerializedName("disableAprilFools")
private final BooleanProperty disableAprilFools = new SimpleBooleanProperty(false);
/// Returns the April Fools disable property.
public BooleanProperty disableAprilFoolsProperty() {
return disableAprilFools;
}
/// The common Minecraft directory selection mode.
@SerializedName("commonDirectoryType")
private final ObjectProperty<EnumCommonDirectory> commonDirectoryType = new RawPreservingObjectProperty<>(EnumCommonDirectory.DEFAULT);
@@ -9,15 +9,12 @@ import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import org.jackhuang.hmcl.util.gson.JsonSchema;
import org.jackhuang.hmcl.util.gson.JsonSerializable;
import org.jackhuang.hmcl.util.gson.ObservableSetting;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
@@ -10,7 +10,6 @@ import javafx.scene.paint.Color;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorRole;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.theme.ResolvedTheme;
import org.jackhuang.hmcl.theme.ThemeColor;
import org.jackhuang.hmcl.theme.Themes;
@@ -26,6 +25,8 @@ public final class StyleSheets {
private static final int FONT_STYLE_SHEET_INDEX = 0;
private static final int THEME_STYLE_SHEET_INDEX = 1;
private static final int BRIGHTNESS_SHEET_INDEX = 2;
/// Loaded after root.css so solid :pressed fills beat Monet / Modena cyan.
private static final int PRESS_FEEDBACK_SHEET_INDEX = 4;
private static final ObservableList<String> stylesheets;
@@ -34,9 +35,15 @@ public final class StyleSheets {
getFontStyleSheet(),
getThemeStyleSheet(),
getBrightnessStyleSheet(),
"/assets/css/root.css"
"/assets/css/root.css",
"/assets/css/press-feedback.css"
};
stylesheets = FXCollections.observableList(Arrays.asList(array));
// Keep the constant honest for future inserts ahead of press-feedback.
if (stylesheets.size() <= PRESS_FEEDBACK_SHEET_INDEX
|| !"/assets/css/press-feedback.css".equals(stylesheets.get(PRESS_FEEDBACK_SHEET_INDEX))) {
throw new IllegalStateException("press-feedback.css must remain the last stylesheet");
}
FontManager.fontProperty().addListener(o -> stylesheets.set(FONT_STYLE_SHEET_INDEX, getFontStyleSheet()));
Themes.colorSchemeProperty().addListener(o -> {
@@ -50,51 +57,72 @@ public final class StyleSheets {
}
private static String getFontStyleSheet() {
final String defaultCss = "/assets/css/font.css";
FontManager.ensureBundledFontsLoaded();
final FontManager.FontReference font = FontManager.getFont();
if (font == null || "System".equals(font.family()))
return defaultCss;
String fontFamily = font.family();
String style = font.style();
String bodyFamily = FontManager.getBodyFontFamily();
String titleFamily = FontManager.getTitleFontFamily();
String weight = null;
String posture = null;
if (style != null) {
style = style.toLowerCase(Locale.ROOT);
// Explicit user / env override replaces body face only; titles stay Source Han Sans Bold.
if (font != null && !"System".equals(font.family())
&& !FontManager.BODY_FONT_FAMILY.equalsIgnoreCase(font.family())
&& !FontManager.TITLE_FONT_FAMILY.equalsIgnoreCase(font.family())) {
bodyFamily = font.family();
String style = font.style();
if (style != null) {
style = style.toLowerCase(Locale.ROOT);
if (style.contains("thin"))
weight = "100";
else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light"))
weight = "200";
else if (style.contains("medium"))
weight = "500";
else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold"))
weight = "600";
else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold"))
weight = "800";
else if (style.contains("black") || style.contains("heavy"))
weight = "900";
else if (style.contains("light"))
weight = "lighter";
else if (style.contains("bold"))
weight = "bold";
if (style.contains("thin"))
weight = "100";
else if (style.contains("extralight") || style.contains("extra light") || style.contains("ultralight") | style.contains("ultra light"))
weight = "200";
else if (style.contains("medium"))
weight = "500";
else if (style.contains("semibold") || style.contains("semi bold") || style.contains("demibold") || style.contains("demi bold"))
weight = "600";
else if (style.contains("extrabold") || style.contains("extra bold") || style.contains("ultrabold") || style.contains("ultra bold"))
weight = "800";
else if (style.contains("black") || style.contains("heavy"))
weight = "900";
else if (style.contains("light"))
weight = "lighter";
else if (style.contains("bold"))
weight = "bold";
posture = style.contains("italic") || style.contains("oblique") ? "italic" : null;
posture = style.contains("italic") || style.contains("oblique") ? "italic" : null;
}
}
StringBuilder builder = new StringBuilder();
builder.append(".root {");
builder.append("-fx-font-family:\"").append(fontFamily).append("\";");
builder.append("-fx-font-family:\"").append(bodyFamily).append("\", \"Source Han Serif\", serif;");
if (weight != null)
builder.append("-fx-font-weight:").append(weight).append(";");
if (posture != null)
builder.append("-fx-font-style:").append(posture).append(";");
builder.append("}\n");
builder.append('}');
builder.append("""
.jfx-decorator-title,
.jfx-tool-bar-second .label,
.title-label,
.class-title,
.class-title Text,
.jfx-layout-heading,
.launch-pane > .launch-button .label,
.announcement .title,
.method-title,
.radio-button-title-label,
.two-line-list-item > .title,
.advanced-list-item > .rippler-container > .container > .title,
.jfx-button-raised,
.jfx-button-raised * {
""");
builder.append("-fx-font-family:\"").append(titleFamily).append("\", \"Source Han Sans\", \"思源黑体\", sans-serif;");
builder.append("-fx-font-weight:bold;");
builder.append("}\n");
return toStyleSheetUri(builder.toString());
}
@@ -121,13 +149,133 @@ public final class StyleSheets {
.append(";\n");
}
private static void putHex(StringBuilder builder, String name, String hex) {
builder.append(" ").append(name).append(": ").append(hex).append(";\n");
}
/// Maps anthropic-style-cn skill base.css light/dark pairs onto Monet roles so existing CSS flips end-to-end.
private static void applySkillTokenOverrides(StringBuilder builder, boolean dark) {
// Skill accent (CTA / focus) — never Material blue.
final String accent = "#D97757";
final String accentWarm = "#C96442";
final String error = "#C0453A";
final String onAccent = "#FAF9F5";
if (dark) {
putHex(builder, "-monet-surface", "#1A1916");
putHex(builder, "-monet-background", "#1A1916");
putHex(builder, "-monet-surface-bright", "#2C2B26");
putHex(builder, "-monet-surface-dim", "#141413");
putHex(builder, "-monet-surface-container-lowest", "#141413");
putHex(builder, "-monet-surface-container-low", "#222119");
putHex(builder, "-monet-surface-container", "#222119");
putHex(builder, "-monet-surface-container-high", "#2C2B26");
putHex(builder, "-monet-surface-container-highest", "#2C2B26");
putHex(builder, "-monet-surface-variant", "#2C2B26");
putHex(builder, "-monet-on-surface", "#EAE7DC");
putHex(builder, "-monet-on-background", "#EAE7DC");
putHex(builder, "-monet-on-surface-variant", "#9D9A91");
putHex(builder, "-monet-outline", "#3A3830");
putHex(builder, "-monet-outline-variant", "#2E2D27");
putHex(builder, "-monet-inverse-surface", "#F5F3EC");
putHex(builder, "-monet-inverse-on-surface", "#141413");
putHex(builder, "-monet-secondary-container", "#2C2B26");
putHex(builder, "-monet-on-secondary-container", "#EAE7DC");
putHex(builder, "-anthropic-shadow", "rgba(0, 0, 0, 0.50)");
putHex(builder, "-anthropic-shadow-strong", "rgba(0, 0, 0, 0.65)");
putHex(builder, "-anthropic-scrollbar-track", "#222119");
putHex(builder, "-anthropic-scrollbar-thumb", "#5C5A54");
putHex(builder, "-anthropic-hover-wash", "rgba(234, 231, 220, 0.08)");
putHex(builder, "-anthropic-press-wash", "rgba(234, 231, 220, 0.14)");
} else {
putHex(builder, "-monet-surface", "#ECE9E0");
putHex(builder, "-monet-background", "#ECE9E0");
putHex(builder, "-monet-surface-bright", "#FDFCF8");
putHex(builder, "-monet-surface-dim", "#D8D5CC");
putHex(builder, "-monet-surface-container-lowest", "#FDFCF8");
putHex(builder, "-monet-surface-container-low", "#F5F3EC");
putHex(builder, "-monet-surface-container", "#F5F3EC");
putHex(builder, "-monet-surface-container-high", "#FDFCF8");
putHex(builder, "-monet-surface-container-highest", "#FDFCF8");
putHex(builder, "-monet-surface-variant", "#E8E6DC");
putHex(builder, "-monet-on-surface", "#141413");
putHex(builder, "-monet-on-background", "#141413");
putHex(builder, "-monet-on-surface-variant", "#6B6860");
putHex(builder, "-monet-outline", "#D8D5CC");
putHex(builder, "-monet-outline-variant", "#E8E6DC");
putHex(builder, "-monet-inverse-surface", "#141413");
putHex(builder, "-monet-inverse-on-surface", "#FAF9F5");
putHex(builder, "-monet-secondary-container", "#E8E6DC");
putHex(builder, "-monet-on-secondary-container", "#141413");
putHex(builder, "-anthropic-shadow", "rgba(20, 20, 19, 0.10)");
putHex(builder, "-anthropic-shadow-strong", "rgba(20, 20, 19, 0.16)");
putHex(builder, "-anthropic-scrollbar-track", "#E8E6DC");
putHex(builder, "-anthropic-scrollbar-thumb", "#9B9890");
putHex(builder, "-anthropic-hover-wash", "rgba(20, 20, 19, 0.06)");
putHex(builder, "-anthropic-press-wash", "rgba(20, 20, 19, 0.12)");
}
putHex(builder, "-monet-primary", accent);
putHex(builder, "-monet-primary-seed", accent);
putHex(builder, "-monet-on-primary", onAccent);
putHex(builder, "-monet-primary-container", accentWarm);
putHex(builder, "-monet-on-primary-container", onAccent);
putHex(builder, "-monet-primary-fixed", dark ? "#4A2E24" : "#F6E0D6");
putHex(builder, "-monet-primary-fixed-dim", dark ? "#C96442" : accent);
putHex(builder, "-monet-on-primary-fixed", dark ? "#FAF9F5" : "#141413");
putHex(builder, "-monet-on-primary-fixed-variant", dark ? "#EAE7DC" : "#6B6860");
putHex(builder, "-monet-inverse-primary", accent);
putHex(builder, "-monet-surface-tint", accent);
// Secondary / tertiary: warm orange family only — kill Monet teal/purple companions.
putHex(builder, "-monet-secondary", accentWarm);
putHex(builder, "-monet-on-secondary", onAccent);
putHex(builder, "-monet-secondary-fixed", dark ? "#3A2A22" : "#F0E4DC");
putHex(builder, "-monet-secondary-fixed-dim", dark ? accentWarm : accent);
putHex(builder, "-monet-on-secondary-fixed", dark ? "#EAE7DC" : "#141413");
putHex(builder, "-monet-on-secondary-fixed-variant", dark ? "#9D9A91" : "#6B6860");
putHex(builder, "-monet-tertiary", accent);
putHex(builder, "-monet-on-tertiary", onAccent);
putHex(builder, "-monet-tertiary-container", accentWarm);
putHex(builder, "-monet-on-tertiary-container", onAccent);
putHex(builder, "-monet-tertiary-fixed", dark ? "#4A2E24" : "#F6E0D6");
putHex(builder, "-monet-tertiary-fixed-dim", dark ? accentWarm : accent);
putHex(builder, "-monet-on-tertiary-fixed", dark ? "#FAF9F5" : "#141413");
putHex(builder, "-monet-on-tertiary-fixed-variant", dark ? "#EAE7DC" : "#6B6860");
putHex(builder, "-monet-error", error);
putHex(builder, "-monet-on-error", onAccent);
putHex(builder, "-monet-error-container", dark ? "#4A1A1F" : "#FCEBEB");
putHex(builder, "-monet-on-error-container", dark ? "#F9DEDC" : "#410E0B");
putHex(builder, "-anthropic-accent", accent);
putHex(builder, "-anthropic-accent-warm", accentWarm);
putHex(builder, "-anthropic-focus-ring", accent);
putHex(builder, "-anthropic-focus-glow", "rgba(217, 119, 87, 0.35)");
putHex(builder, "-anthropic-primary-hover", dark ? "#E08A6C" : accentWarm);
putHex(builder, "-anthropic-primary-press", dark ? "#EFA08A" : "#B4573A");
putHex(builder, "-anthropic-primary-container-hover", dark ? "#D97757" : "#B4573A");
putHex(builder, "-anthropic-primary-container-press", dark ? "#E08A6C" : "#9A4A32");
putHex(builder, "-anthropic-selected-wash", dark ? "rgba(217, 119, 87, 0.22)" : "rgba(217, 119, 87, 0.14)");
// Stronger wash for tiny icon buttons where 6–8% alpha is invisible.
putHex(builder, "-anthropic-icon-hover-wash", dark ? "rgba(234, 231, 220, 0.14)" : "rgba(20, 20, 19, 0.10)");
putHex(builder, "-anthropic-icon-press-wash", dark ? "rgba(217, 119, 87, 0.32)" : "rgba(217, 119, 87, 0.22)");
// Transparent helpers consumed by root.css
putHex(builder, "-monet-primary-transparent-50", accent + "80");
putHex(builder, "-monet-primary-transparent-80", accent + "CC");
putHex(builder, "-monet-surface-transparent-50", dark ? "#1A191680" : "#ECE9E080");
putHex(builder, "-monet-surface-transparent-80", dark ? "#1A1916CC" : "#ECE9E0CC");
putHex(builder, "-monet-surface-container-low-transparent-80", dark ? "#222119CC" : "#F5F3ECCC");
putHex(builder, "-monet-secondary-container-transparent-50", dark ? "#2C2B2680" : "#E8E6DC80");
putHex(builder, "-monet-secondary-container-transparent-80", dark ? "#2C2B26CC" : "#E8E6DCCC");
putHex(builder, "-monet-on-surface-variant-transparent-38", dark ? "#9D9A9161" : "#6B686061");
putHex(builder, "-monet-inverse-surface-transparent-80", dark ? "#F5F3ECCC" : "#141413CC");
}
private static String getThemeStyleSheet() {
final String blueCss = "/assets/css/blue.css";
if (ResolvedTheme.DEFAULT.equals(Themes.getTheme()))
return blueCss;
ColorScheme scheme = Themes.getColorScheme();
boolean dark = scheme.getBrightness() == Brightness.DARK;
StringBuilder builder = new StringBuilder();
builder.append("* {\n");
@@ -147,6 +295,9 @@ public final class StyleSheets {
addColor(builder, scheme, ColorRole.SECONDARY_CONTAINER, 0.8);
addColor(builder, scheme, ColorRole.INVERSE_SURFACE, 0.8);
// Skill authority: overwrite Monet surfaces/text/borders/accent for light+dark.
applySkillTokenOverrides(builder, dark);
builder.append("}\n");
return toStyleSheetUri(builder.toString());
}
@@ -24,10 +24,19 @@ public record ResolvedTheme(ThemeColor primaryColorSeed,
/// Converts these resolved values to a MonetFX color scheme.
///
/// Warm secondary / tertiary / neutral seeds keep Monet from inventing teal or purple
/// companions next to the orange accent; CSS + [SkillPalette] still pin exact skill hex.
///
/// @return the generated color scheme
public ColorScheme toColorScheme() {
boolean darkNeutrals = brightness == Brightness.DARK;
return ColorScheme.newBuilder()
.setPrimaryColorSeed(primaryColorSeed.color())
.setSecondaryColorSeed(SkillPalette.ACCENT_WARM)
.setTertiaryColorSeed(SkillPalette.ACCENT)
.setNeutralColorSeed(darkNeutrals ? SkillPalette.DARK_SURFACE : SkillPalette.LIGHT_SURFACE)
.setNeutralVariantColorSeed(darkNeutrals ? SkillPalette.DARK_OUTLINE : SkillPalette.LIGHT_OUTLINE)
.setErrorColorSeed(SkillPalette.ERROR)
.setColorStyle(colorStyle)
.setBrightness(brightness)
.setSpecVersion(ColorSpecVersion.SPEC_2025)
@@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.theme;
import javafx.scene.paint.Color;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorScheme;
import org.jetbrains.annotations.NotNullByDefault;
/// Anthropic-style-cn skill light/dark palette used for JavaFX chrome that bypasses CSS.
///
/// Monet's generated scheme from an orange seed still drifts pink / teal; these exact
/// skill tokens keep title bar, frame, and theme-color backgrounds aligned with
/// [org.jackhuang.hmcl.setting.StyleSheets] CSS overrides.
@NotNullByDefault
public final class SkillPalette {
public static final String ACCENT_HEX = "#D97757";
public static final String ACCENT_WARM_HEX = "#C96442";
public static final String ERROR_HEX = "#C0453A";
public static final String ON_ACCENT_HEX = "#FAF9F5";
public static final Color ACCENT = Color.web(ACCENT_HEX);
public static final Color ACCENT_WARM = Color.web(ACCENT_WARM_HEX);
public static final Color ERROR = Color.web(ERROR_HEX);
public static final Color ON_ACCENT = Color.web(ON_ACCENT_HEX);
public static final Color LIGHT_SURFACE = Color.web("#ECE9E0");
public static final Color LIGHT_SURFACE_RAISED = Color.web("#F5F3EC");
public static final Color LIGHT_SURFACE_OVERLAY = Color.web("#FDFCF8");
public static final Color LIGHT_SURFACE_VARIANT = Color.web("#E8E6DC");
public static final Color LIGHT_ON_SURFACE = Color.web("#141413");
public static final Color LIGHT_ON_SURFACE_VARIANT = Color.web("#6B6860");
public static final Color LIGHT_OUTLINE = Color.web("#D8D5CC");
public static final Color LIGHT_OUTLINE_VARIANT = Color.web("#E8E6DC");
public static final Color DARK_SURFACE = Color.web("#1A1916");
public static final Color DARK_SURFACE_RAISED = Color.web("#222119");
public static final Color DARK_SURFACE_OVERLAY = Color.web("#2C2B26");
public static final Color DARK_SURFACE_VARIANT = Color.web("#2C2B26");
public static final Color DARK_ON_SURFACE = Color.web("#EAE7DC");
public static final Color DARK_ON_SURFACE_VARIANT = Color.web("#9D9A91");
public static final Color DARK_OUTLINE = Color.web("#3A3830");
public static final Color DARK_OUTLINE_VARIANT = Color.web("#2E2D27");
private SkillPalette() {
}
public static boolean isDark(ColorScheme scheme) {
return scheme.getBrightness() == Brightness.DARK;
}
public static boolean isDark() {
return isDark(Themes.getColorScheme());
}
public static Color surface(boolean dark) {
return dark ? DARK_SURFACE : LIGHT_SURFACE;
}
public static Color surfaceContainer(boolean dark) {
return dark ? DARK_SURFACE_RAISED : LIGHT_SURFACE_RAISED;
}
public static Color surfaceContainerHigh(boolean dark) {
return dark ? DARK_SURFACE_OVERLAY : LIGHT_SURFACE_OVERLAY;
}
public static Color onSurface(boolean dark) {
return dark ? DARK_ON_SURFACE : LIGHT_ON_SURFACE;
}
public static Color onSurfaceVariant(boolean dark) {
return dark ? DARK_ON_SURFACE_VARIANT : LIGHT_ON_SURFACE_VARIANT;
}
public static Color outline(boolean dark) {
return dark ? DARK_OUTLINE : LIGHT_OUTLINE;
}
public static Color surface() {
return surface(isDark());
}
public static Color surfaceContainer() {
return surfaceContainer(isDark());
}
public static Color onSurface() {
return onSurface(isDark());
}
}
@@ -26,10 +26,13 @@ import java.util.Objects;
@JsonSerializable
public record ThemeColor(@NotNull String name, @NotNull Color color) {
public static final ThemeColor DEFAULT = new ThemeColor("blue", Color.web("#5C6BC0"));
/// Default Anthropic accent seed (skill: `--color-accent-orange`).
public static final ThemeColor DEFAULT = new ThemeColor("anthropic_orange", Color.web("#D97757"));
public static final List<ThemeColor> STANDARD_COLORS = List.of(
DEFAULT,
new ThemeColor("stone", Color.web("#3D453C")),
new ThemeColor("blue", Color.web("#5C6BC0")),
new ThemeColor("darker_blue", Color.web("#283593")),
new ThemeColor("green", Color.web("#43A047")),
new ThemeColor("orange", Color.web("#E67E22")),
@@ -29,7 +29,6 @@ import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorScheme;
import org.glavo.monetfx.ColorRole;
import org.glavo.monetfx.ColorStyle;
import org.glavo.monetfx.Contrast;
import org.glavo.monetfx.beans.property.ColorSchemeProperty;
@@ -800,7 +799,8 @@ public final class Themes {
opacity = background.opacity();
}
Paint themeColorPaint = appearance.toResolvedTheme(context).toColorScheme().getColor(ColorRole.SURFACE_CONTAINER);
boolean darkFallback = appearance.toResolvedTheme(context).toColorScheme().getBrightness() == Brightness.DARK;
Paint themeColorPaint = SkillPalette.surfaceContainer(darkFallback);
BackgroundType fallbackType = Objects.requireNonNullElse(
settings().backgroundFallbackTypeProperty().get(),
BackgroundType.BUILTIN);
@@ -960,7 +960,7 @@ public final class Themes {
/// Creates a JavaFX background from the current theme color scheme surface container.
private static LauncherBackground createThemeColorBackground(double opacity) {
return createPaintBackground(getColorScheme().getColor(ColorRole.SURFACE_CONTAINER), opacity);
return createPaintBackground(SkillPalette.surfaceContainer(), opacity);
}
/// Creates a JavaFX image background with a separately stored node opacity.
@@ -1103,10 +1103,11 @@ public final class Themes {
);
/// The title text fill derived from the current color scheme.
///
/// Title chrome uses surface tones (Anthropic-like calm bar), so fill stays on-surface
/// whether or not the bar is translucent.
private static final ObjectBinding<Color> titleFill = Bindings.createObjectBinding(
() -> titleBarTransparent.get()
? getColorScheme().getOnSurface()
: getColorScheme().getOnPrimaryContainer(),
() -> SkillPalette.onSurface(),
colorSchemeProperty(),
titleBarTransparent
);
@@ -8,7 +8,6 @@ import com.jfoenix.validation.base.ValidatorBase;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -22,6 +21,7 @@ import javafx.util.Duration;
import org.jackhuang.hmcl.Launcher;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.GameInstanceID;
import org.jackhuang.hmcl.game.GameJavaVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.game.ModpackHelper;
@@ -45,8 +45,6 @@ import org.jackhuang.hmcl.ui.instances.Instances;
import org.jackhuang.hmcl.upgrade.IntegrityChecker;
import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.function.ExceptionalRunnable;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.SupportedLocale;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -57,7 +55,6 @@ import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
@@ -78,8 +75,6 @@ public final class Controllers {
public static final String JAVA_INTERPRETED_MODE_TIP = "javaInterpretedMode";
/// The shown-tip key for the software-rendering warning.
public static final String SOFTWARE_RENDERING = "softwareRendering";
/// The shown-tip key storing the year of the April Fools prompt.
public static final String APRIL_FOOLS = "aprilFools";
/// The coordinator for the main window's scene graph and navigation stack.
private static @Nullable Decorator decorator;
@@ -259,8 +254,6 @@ public final class Controllers {
public static void initialize(Stage stage) {
LOG.info("Start initializing application");
LOG.info("April Fools: " + AprilFools.isEnabled());
if (System.getProperty("prism.lcdtext") == null) {
@Nullable String fontAntiAliasing = SettingsManager.userSettings().fontAntiAliasingProperty().get();
if ("lcd".equalsIgnoreCase(fontAntiAliasing)) {
@@ -280,7 +273,10 @@ public final class Controllers {
// Dedicated server client: always use the fixed default cache directory.
settings().commonDirectoryTypeProperty().set(EnumCommonDirectory.DEFAULT);
Lang.thread(JavaManager::initialize, "Search Java", true);
Lang.thread(() -> {
JavaManager.initialize();
FXUtils.runInFX(Controllers::tryEnsureRecommendedMojangJava);
}, "Search Java", true);
StyleSheets.init(mainScene);
@@ -363,54 +359,42 @@ public final class Controllers {
Controllers.dialog(agreementPane);
}
aprilFools:
if (AprilFools.isEnabled()) {
int currentYear = LocalDate.now().getYear();
if (state().getShownTips().get(APRIL_FOOLS) instanceof Number year && year.intValue() >= currentYear)
break aprilFools;
tryInstallBundledModpack(GameDirectoryManager.getSelectedRepository());
}
if (!I18n.getLocale().getLocale().getLanguage().equals("zh"))
break aprilFools;
/// Downloads Mojang Runtime Java 25 when missing on supported platforms.
///
/// No confirm dialog: if already installed, returns quietly; otherwise shows a progress dialog.
private static void tryEnsureRecommendedMojangJava() {
org.jackhuang.hmcl.util.platform.Platform platform =
org.jackhuang.hmcl.util.platform.Platform.SYSTEM_PLATFORM;
GameJavaVersion target = GameJavaVersion.JAVA_25;
@Nullable SupportedLocale lzh = SupportedLocale.getSupportedLocales().stream()
.filter(locale -> "lzh".equals(locale.getName()))
.findFirst().orElse(null);
if (lzh == null) {
LOG.warning("No supported locale found for lzh");
break aprilFools;
}
Runnable updateShowTips = () -> state().getShownTips().put(APRIL_FOOLS, currentYear);
Controllers.confirmWithCountdown(i18n("launcher.april_fools.switch_lzh"), null, 10,
MessageType.QUESTION, () -> {
Controllers.confirm(i18n("launcher.april_fools.switch_lzh.confirm"), null, MessageType.QUESTION, () -> {
LOG.info("Switching locale to " + lzh);
updateShowTips.run();
settings().languageProperty().set(lzh);
Controllers.onApplicationStop();
try {
FileSaver.waitForAllSaves();
} catch (InterruptedException ignored) {
// Ignore
}
try {
Restarter.restartSelf();
} catch (IOException e) {
LOG.warning("Failed to restart self", e);
}
Platform.exit();
}, updateShowTips);
}, updateShowTips);
if (!GameJavaVersion.getSupportedVersions(platform).contains(target)) {
return;
}
if (JavaManager.getMojangJavaPlatform(platform) == null) {
return;
}
if (JavaManager.REPOSITORY.isInstalled(platform, target)) {
return;
}
tryInstallBundledModpack(GameDirectoryManager.getSelectedRepository());
LOG.info("Recommended Mojang Java " + target.majorVersion()
+ " (" + target.component() + ") missing; starting automatic download");
Controllers.taskDialog(
JavaManager.getDownloadJavaTask(DownloadProviders.getDownloadProvider(), platform, target)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null) {
LOG.warning("Failed to download recommended Mojang Java " + target.majorVersion(), exception);
} else {
LOG.info("Installed Mojang Java " + target.majorVersion() + " at " + result.getBinary());
}
}),
i18n("java.download"),
TaskCancellationAction.NO_CANCEL
);
}
/// Installs the package found by [Metadata#findBundledModpackFile()] asynchronously, if present.
@@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: Copyright (C) 2020 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.ui;
/// Golden-ratio layout metrics for Anthropic tool-mode chrome.
///
/// Primary splits use φ ≈ 1.618 (sidebar:content = 1:φ). Spacing uses the φ ladder
/// from an 8px base so hierarchy stays dense — not landing-page whitespace.
public final class LayoutMetrics {
/// Golden ratio.
public static final double PHI = 1.618033988749895;
/// Sidebar share of a page: 1 / (1 + φ) ≈ 0.382 so sidebar:content = 1:φ.
public static final double SIDEBAR_FRACTION = 1.0 / (1.0 + PHI);
/// Design-minimum page width used when the node has not been laid out yet.
public static final double DESIGN_PAGE_WIDTH = 800.0;
/// Tool-density clamp: keep the rail usable without eating the content column.
public static final double SIDEBAR_MIN = 220.0;
/// φ share of [#DESIGN_PAGE_WIDTH] — exact 1:φ at the launcher minimum width.
public static final double SIDEBAR_MAX = Math.rint(DESIGN_PAGE_WIDTH * SIDEBAR_FRACTION); // 306
/// φ spacing ladder from an 8px base (…, 5, 8, 13, 21, 34).
public static final double SPACE_XS = 5.0;
public static final double SPACE_SM = 8.0;
public static final double SPACE_MD = 13.0;
public static final double SPACE_LG = 21.0;
public static final double SPACE_XL = 34.0;
/// Title-bar / touch target height (a11y floor from the skill).
public static final double TITLE_BAR = 44.0;
/// Primary CTA height on the home page (Fibonacci neighbour of title bar / φ body).
public static final double LAUNCH_HEIGHT = 55.0;
/// Primary CTA width (Fibonacci; pairs with the φ sidebar rail).
public static final double LAUNCH_WIDTH = 233.0;
private LayoutMetrics() {
}
/// Resolves the fixed sidebar width for a page of the given width.
///
/// @param pageWidth the animated page width in pixels; values ≤ 1 fall back to the design minimum
/// @return a width clamped to [{@link #SIDEBAR_MIN}, {@link #SIDEBAR_MAX}] on the 1:φ fraction
public static double sidebarWidth(double pageWidth) {
double width = pageWidth > 1.0 ? pageWidth : DESIGN_PAGE_WIDTH;
return Math.rint(Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, width * SIDEBAR_FRACTION)));
}
}
@@ -1,182 +0,0 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXSpinner;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.SkinBase;
import javafx.scene.control.Tooltip;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TaskExecutorDialogPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.TaskManager;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// Displays active tasks and pending completion actions.
@NotNullByDefault
public final class TaskManagerPage extends Control implements DecoratorPage {
/// Navigation metadata for the task list.
private final ReadOnlyObjectWrapper<State> state =
new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("task.list")));
/// Creates a page backed by the application-wide task manager.
public TaskManagerPage() {
getStyleClass().add("content-background");
}
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
@Override
protected SkinBase<TaskManagerPage> createDefaultSkin() {
return new Skin(this);
}
/// Binds a list view to active dialogs and pending completion actions.
@NotNullByDefault
private static final class Skin extends SkinBase<TaskManagerPage> {
/// Creates the task list and its empty-state message.
private Skin(TaskManagerPage control) {
super(control);
StackPane container = new StackPane();
container.setPadding(new Insets(10));
container.getStyleClass().add("notice-pane");
ComponentList root = new ComponentList();
root.getStyleClass().add("no-padding");
JFXListView<TaskExecutorDialogPane> list = new JFXListView<>();
list.setItems(TaskManager.getInstance().getTasks());
list.setCellFactory(view -> new TaskCell());
list.setPadding(Insets.EMPTY);
list.getStyleClass().add("no-horizontal-scrollbar");
FXUtils.ignoreEvent(list, KeyEvent.KEY_PRESSED, event -> event.getCode() == KeyCode.ESCAPE);
StackPane placeholder = new StackPane(new Label(i18n("task.list.empty")));
placeholder.getStyleClass().add("notice-pane");
list.setPlaceholder(placeholder);
ComponentList.setVgrow(list, Priority.ALWAYS);
root.getContent().setAll(list);
container.getChildren().setAll(root);
getChildren().setAll(container);
}
}
/// Displays a live task title and an action to restore progress or continue a completed task.
@NotNullByDefault
private static final class TaskCell extends ListCell<@Nullable TaskExecutorDialogPane> {
/// The list item whose title is bound to the current dialog.
private final TwoLineListItem content = new TwoLineListItem();
/// The row's shared hover and ripple feedback.
private final RipplerContainer graphic;
/// Centers running and pending indicators in a fixed-size slot to keep row titles aligned.
private final StackPane statusIcon = new StackPane();
/// Restores progress or invokes the pending completion action.
private final JFXButton actionButton = FXUtils.newToggleButton4(SVG.ARROW_FORWARD);
/// Describes the current row action.
private final Tooltip actionTooltip = new Tooltip();
/// Creates a row that activates the current task through its body or trailing action.
private TaskCell() {
content.getStyleClass().add("md-list-cell");
content.setPadding(new Insets(8));
statusIcon.setMinSize(32, 32);
statusIcon.setPrefSize(32, 32);
statusIcon.setMaxSize(32, 32);
statusIcon.setMouseTransparent(true);
content.setMinWidth(0);
TwoLineListItem.setMargin(statusIcon, new Insets(0, 8, 0, 0));
content.setLeading(statusIcon);
FXUtils.installFastTooltip(actionButton, actionTooltip);
actionButton.setOnAction(event -> activateTask());
TwoLineListItem.setMargin(actionButton, new Insets(0, 0, 0, 8));
content.setTrailing(actionButton);
graphic = new RipplerContainer(content);
graphic.setCursor(Cursor.HAND);
graphic.setOnAction(event -> activateTask());
}
/// Invokes a pending action or restores the current task's progress dialog.
private void activateTask() {
@Nullable TaskExecutorDialogPane pane = getItem();
if (pane != null) {
if (pane.pendingActionProperty().get() != null)
pane.runPendingAction();
else
pane.restore();
}
}
/// Rebinds a reused cell and updates its progress or pending-action presentation.
@Override
protected void updateItem(@Nullable TaskExecutorDialogPane pane, boolean empty) {
super.updateItem(pane, empty);
graphic.resetFeedback();
content.titleProperty().unbind();
content.setSubtitle(null);
if (empty || pane == null) {
content.setTitle("");
statusIcon.getChildren().clear();
setGraphic(null);
} else {
content.titleProperty().bind(pane.titleProperty());
@Nullable TaskExecutorDialogPane.PendingAction action = pane.pendingActionProperty().get();
actionButton.getStyleClass().removeAll("toggle-icon4", "jfx-button-border");
if (action != null) {
actionButton.getStyleClass().add("jfx-button-border");
statusIcon.getChildren().setAll(SVG.PENDING.createIcon(24));
content.setSubtitle(action.description());
actionButton.setGraphic(null);
actionButton.setText(action.text());
actionButton.setAccessibleText(action.text());
actionTooltip.setText(action.text());
} else {
actionButton.getStyleClass().add("toggle-icon4");
if (pane.queuedProperty().get()) {
statusIcon.getChildren().setAll(SVG.PENDING.createIcon(24));
content.setSubtitle(i18n("task.queued"));
} else if (statusIcon.getChildren().isEmpty() || !(statusIcon.getChildren().get(0) instanceof JFXSpinner)) {
JFXSpinner spinner = new JFXSpinner();
spinner.setRadius(12);
statusIcon.getChildren().setAll(spinner);
}
actionButton.setGraphic(SVG.ARROW_FORWARD.createIcon());
actionButton.setText(null);
actionButton.setAccessibleText(i18n("task.show"));
actionTooltip.setText(i18n("task.show"));
}
setGraphic(graphic);
}
}
}
}
@@ -6,14 +6,13 @@ import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.SettingsManager;
@@ -24,7 +23,6 @@ import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.RegionUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
import org.jetbrains.annotations.Nullable;
@@ -32,33 +30,13 @@ import org.jetbrains.annotations.UnmodifiableView;
import java.util.Locale;
import static org.jackhuang.hmcl.setting.SettingsManager.userSettings;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.createSelectedItemPropertyFor;
/// Displays account creation methods and the configured account list.
public final class AccountListPage extends DecoratorAnimatedPage implements DecoratorPage {
/// Whether offline and third-party account creation is restricted.
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(true);
static {
String property = System.getProperty("hmcl.offline.auth.restricted", "auto");
if ("false".equals(property)
|| "auto".equals(property) && "CN".equals(RegionUtils.MACHINE_REGION)
|| SettingsManager.userSettings().enableOfflineAccountProperty().get())
RESTRICTED.set(false);
else
userSettings().enableOfflineAccountProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) {
if (newValue) {
userSettings().enableOfflineAccountProperty().removeListener(this);
RESTRICTED.set(false);
}
}
});
}
/// Offline / authlib-injector creation is always allowed for this fork.
static final BooleanProperty RESTRICTED = new SimpleBooleanProperty(false);
/// The account entries derived from the configured accounts.
private final @UnmodifiableView ObservableList<AccountListItem> items;
@@ -78,6 +56,12 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
items = MappedObservableList.create(accounts, AccountListItem::new);
selectedAccount = createSelectedItemPropertyFor(items, Account.class);
AdvancedListItem offlineItem = new AdvancedListItem();
offlineItem.getStyleClass().add("navigation-drawer-item");
offlineItem.setTitle(i18n("account.methods.offline"));
offlineItem.setLeftIcon(SVG.PERSON);
offlineItem.setOnAction(e -> openCreateAccount(Accounts.FACTORY_OFFLINE));
AdvancedListItem microsoftItem = new AdvancedListItem();
microsoftItem.getStyleClass().add("navigation-drawer-item");
microsoftItem.setTitle(i18n("account.methods.microsoft"));
@@ -91,14 +75,14 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
});
InvalidationListener buildSidebarListener = observable -> {
boolean restricted = RESTRICTED.get();
getSidebar().clear();
getSidebar()
.startCategory(i18n("account.create").toUpperCase(Locale.ROOT))
.add(offlineItem)
.add(microsoftItem);
// External Yggdrasil / authlib-injector servers stay listed for future use.
for (AuthlibInjectorServer server : authServers) {
AdvancedListItem item = new AdvancedListItem();
item.getStyleClass().add("navigation-drawer-item");
@@ -111,24 +95,13 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
}
});
ObservableValue<String> title = BindingMapping.of(server, AuthlibInjectorServer::getName);
var title = BindingMapping.of(server, AuthlibInjectorServer::getName);
item.titleProperty().bind(title);
item.setSubtitle(server.getDisplayHostUrl());
Tooltip tooltip = new Tooltip();
if (restricted) {
tooltip.setText(i18n("account.login.restricted"));
item.setActionDisabled(true);
} else {
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
item.setOnAction(e -> {
if (SettingsManager.isUserGameAccountsReadOnly()) {
confirmOverwriteUserAccounts(() -> Controllers.dialog(new CreateAccountPane(server)));
} else {
Controllers.dialog(new CreateAccountPane(server));
}
});
}
tooltip.textProperty().bind(Bindings.format("%s (%s)", title, server.getUrl()));
item.setOnAction(e -> openCreateAccount(Accounts.getAccountFactoryByAuthlibInjectorServer(server)));
FXUtils.installFastTooltip(item, tooltip);
getSidebar().add(item);
@@ -139,7 +112,6 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
WeakInvalidationListener weakBuildSidebarListener = holder.weak(buildSidebarListener);
authServers.addListener(weakBuildSidebarListener);
RESTRICTED.addListener(weakBuildSidebarListener);
AdvancedListItem addAuthServerItem = new AdvancedListItem();
addAuthServerItem.getStyleClass().add("navigation-drawer-item");
@@ -172,6 +144,14 @@ public final class AccountListPage extends DecoratorAnimatedPage implements Deco
setContent(scrollPane);
}
private void openCreateAccount(AccountFactory<?> factory) {
if (SettingsManager.isUserGameAccountsReadOnly()) {
confirmOverwriteUserAccounts(() -> Controllers.dialog(new CreateAccountPane(factory)));
} else {
Controllers.dialog(new CreateAccountPane(factory));
}
}
/// Returns the account selection shared with the launcher.
public ObjectProperty<@Nullable Account> selectedAccountProperty() {
return selectedAccount;
@@ -81,20 +81,15 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
public CreateAccountPane(AccountFactory<?> factory) {
if (factory == null) {
if (AccountListPage.RESTRICTED.get()) {
showMethodSwitcher = false;
factory = Accounts.FACTORY_MICROSOFT;
} else {
showMethodSwitcher = true;
String preferred = settings().preferredLoginTypeProperty().get();
try {
factory = Accounts.getAccountFactory(preferred);
if (factory == Accounts.FACTORY_OFFLINE) {
factory = Accounts.FACTORY_MICROSOFT;
}
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_MICROSOFT;
showMethodSwitcher = true;
String preferred = settings().preferredLoginTypeProperty().get();
try {
factory = Accounts.getAccountFactory(preferred);
if (!Accounts.FACTORIES.contains(factory)) {
factory = Accounts.FACTORY_OFFLINE;
}
} catch (IllegalArgumentException e) {
factory = Accounts.FACTORY_OFFLINE;
}
} else {
showMethodSwitcher = false;
@@ -121,7 +121,7 @@ public enum ContainerAnimations implements TransitionPane.AnimationProducer {
/// Animates forward navigation with temporary incoming-page caching.
@Override
public Timeline animate(Pane container, Node previousNode, Node nextNode) {
double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
double offset = container.getWidth() > 0 ? container.getWidth() * 0.06 : 16;
return new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(nextNode.cacheProperty(), isCacheAllowed(nextNode), Interpolator.DISCRETE),
@@ -147,12 +147,12 @@ public enum ContainerAnimations implements TransitionPane.AnimationProducer {
}
},
/// @see <a href="https://m3.material.io/styles/motion/transitions/transition-patterns">Transitions - Material Design 3</a>
/// Soft reverse of [#FORWARD] (reduced travel distance for Anthropic-calm chrome).
BACKWARD {
/// Animates backward navigation with temporary incoming-page caching.
@Override
public Timeline animate(Pane container, Node previousNode, Node nextNode) {
double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
double offset = container.getWidth() > 0 ? container.getWidth() * 0.06 : 16;
return new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(nextNode.cacheProperty(), isCacheAllowed(nextNode), Interpolator.DISCRETE),
@@ -210,8 +210,8 @@ public enum ContainerAnimations implements TransitionPane.AnimationProducer {
new KeyValue(left.translateXProperty(), 0, DEFAULT_INTERPOLATOR),
new KeyValue(center.translateXProperty(), 0, DEFAULT_INTERPOLATOR)));
timeline.getKeyFrames().add(new KeyFrame(DEFAULT_DURATION_HALF,
new KeyValue(left.translateXProperty(), -30, DEFAULT_INTERPOLATOR),
new KeyValue(center.translateXProperty(), 30, DEFAULT_INTERPOLATOR)));
new KeyValue(left.translateXProperty(), -10, DEFAULT_INTERPOLATOR),
new KeyValue(center.translateXProperty(), 10, DEFAULT_INTERPOLATOR)));
timeline.getKeyFrames().add(new KeyFrame(DEFAULT_DURATION,
new KeyValue(left.cacheProperty(), left.isCache(), Interpolator.DISCRETE),
new KeyValue(center.cacheProperty(), center.isCache(), Interpolator.DISCRETE)));
@@ -233,8 +233,8 @@ public enum ContainerAnimations implements TransitionPane.AnimationProducer {
new KeyValue(left.cacheProperty(), isCacheAllowed(left), Interpolator.DISCRETE),
new KeyValue(center.cacheProperty(), cacheCenter, Interpolator.DISCRETE)));
timeline.getKeyFrames().add(new KeyFrame(DEFAULT_DURATION_HALF,
new KeyValue(left.translateXProperty(), -30, DEFAULT_INTERPOLATOR),
new KeyValue(center.translateXProperty(), 30, DEFAULT_INTERPOLATOR)));
new KeyValue(left.translateXProperty(), -10, DEFAULT_INTERPOLATOR),
new KeyValue(center.translateXProperty(), 10, DEFAULT_INTERPOLATOR)));
timeline.getKeyFrames().add(new KeyFrame(DEFAULT_DURATION,
new KeyValue(left.cacheProperty(), left.isCache(), Interpolator.DISCRETE),
new KeyValue(center.cacheProperty(), center.isCache(), Interpolator.DISCRETE),
@@ -22,6 +22,18 @@ public final class Motion {
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear.mp4">curve_linear.mp4</a>
public static final Interpolator LINEAR = Interpolator.LINEAR;
/// Anthropic skill default ease: `cubic-bezier(0.16, 1, 0.3, 1)` — transform/opacity only.
public static final Interpolator ANTHROPIC = new Cubic(0.16, 1.0, 0.3, 1.0);
/// Skill `--duration-fast` (150ms).
public static final Duration FAST = Duration.millis(150);
/// Skill `--duration-normal` (250ms).
public static final Duration NORMAL = Duration.millis(250);
/// Skill `--duration-slow` (400ms).
public static final Duration SLOW = Duration.millis(400);
/// The emphasizedAccelerate easing curve in the Material specification.
///
/// See also:
@@ -22,11 +22,11 @@ import org.jetbrains.annotations.Nullable;
@NotNullByDefault
public class TransitionPane extends StackPane {
/// The default easing curve for page transitions.
public static final Interpolator DEFAULT_INTERPOLATOR = Motion.EASE;
/// Skill ease: cubic-bezier(0.16, 1, 0.3, 1) — no Material overshoot.
public static final Interpolator DEFAULT_INTERPOLATOR = Motion.ANTHROPIC;
/// The default duration for page transitions.
public static final Duration DEFAULT_DURATION = Motion.SHORT4;
/// Skill `--duration-fast` (150ms).
public static final Duration DEFAULT_DURATION = Motion.FAST;
/// Half the default duration for transitions with two phases.
public static final Duration DEFAULT_DURATION_HALF = DEFAULT_DURATION.divide(2);
@@ -188,8 +188,9 @@ public final class TabController implements PageAware {
/// Mirrors the original slide-up and fade-in transition vertically.
TAB_DOWN(-1.0);
private static final Duration DURATION = Motion.MEDIUM4;
private static final Duration DURATION = Motion.NORMAL;
private static final Duration HALF_DURATION = DURATION.divide(2);
private static final Interpolator EASING = Motion.ANTHROPIC;
private final double direction;
@@ -200,22 +201,23 @@ public final class TabController implements PageAware {
@Override
public Animation animate(Pane container, Node previousNode, Node nextNode) {
double extent = container.getHeight();
double offset = (extent > 0 ? extent * 0.2 : 50) * direction;
// Small vertical drift + fade — avoids Material emphasized/overshoot motion.
double offset = (extent > 0 ? extent * 0.06 : 16) * direction;
var previousTranslation = previousNode.translateYProperty();
var nextTranslation = nextNode.translateYProperty();
return new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(nextNode.cacheProperty(), isCacheAllowed(nextNode), Interpolator.DISCRETE),
new KeyValue(previousTranslation, 0, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED),
new KeyValue(previousNode.opacityProperty(), 1, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED),
new KeyValue(nextNode.opacityProperty(), 0, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED),
new KeyValue(nextTranslation, offset, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED)),
new KeyValue(previousTranslation, 0, EASING),
new KeyValue(previousNode.opacityProperty(), 1, EASING),
new KeyValue(nextNode.opacityProperty(), 0, EASING),
new KeyValue(nextTranslation, offset, EASING)),
new KeyFrame(HALF_DURATION,
new KeyValue(previousNode.opacityProperty(), 0, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED)),
new KeyValue(previousNode.opacityProperty(), 0, EASING)),
new KeyFrame(DURATION,
new KeyValue(nextNode.cacheProperty(), nextNode.isCache(), Interpolator.DISCRETE),
new KeyValue(nextNode.opacityProperty(), 1, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED),
new KeyValue(nextTranslation, 0, Motion.EASE_IN_OUT_CUBIC_EMPHASIZED)));
new KeyValue(nextNode.opacityProperty(), 1, EASING),
new KeyValue(nextTranslation, 0, EASING)));
}
@Override
@@ -83,8 +83,8 @@ public final class Decorator {
/// The minimum width, in pixels, of the visible window content.
private static final double MIN_CONTENT_WIDTH = 800.0;
/// The minimum height, in pixels, including the 450-pixel background and 40-pixel title bar.
private static final double MIN_CONTENT_HEIGHT = 450.0 + 40.0;
/// The minimum height, in pixels, including the 450-pixel background and 44-pixel title bar.
private static final double MIN_CONTENT_HEIGHT = 450.0 + 44.0;
/// The minimum visible overlap, in pixels, used to accept a persisted window position.
private static final double MIN_VISIBLE_WINDOW_AREA = 20.0;
@@ -975,13 +975,13 @@ public final class Decorator {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 0, Motion.EASE),
new KeyValue(root.scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(root.scaleYProperty(), 0.8, Motion.EASE)),
new KeyFrame(Duration.millis(600),
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
new KeyValue(root.scaleXProperty(), 1, Motion.EASE),
new KeyValue(root.scaleYProperty(), 1, Motion.EASE)));
new KeyValue(root.opacityProperty(), 0, Motion.ANTHROPIC),
new KeyValue(root.scaleXProperty(), 0.985, Motion.ANTHROPIC),
new KeyValue(root.scaleYProperty(), 0.985, Motion.ANTHROPIC)),
new KeyFrame(Motion.NORMAL,
new KeyValue(root.opacityProperty(), 1, Motion.ANTHROPIC),
new KeyValue(root.scaleXProperty(), 1, Motion.ANTHROPIC),
new KeyValue(root.scaleYProperty(), 1, Motion.ANTHROPIC)));
playWindowAnimation(timeline, () -> {
resetRootTransform();
@@ -1003,15 +1003,11 @@ public final class Decorator {
if (!nativeDecorationEnabled && AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
new KeyValue(root.translateYProperty(), 0, Motion.EASE),
new KeyValue(root.scaleXProperty(), 1, Motion.EASE),
new KeyValue(root.scaleYProperty(), 1, Motion.EASE)),
new KeyFrame(Motion.SHORT4,
new KeyValue(root.opacityProperty(), 0, Motion.EASE),
new KeyValue(root.translateYProperty(), 200, Motion.EASE),
new KeyValue(root.scaleXProperty(), 0.4, Motion.EASE),
new KeyValue(root.scaleYProperty(), 0.4, Motion.EASE)));
new KeyValue(root.opacityProperty(), 1, Motion.ANTHROPIC),
new KeyValue(root.translateYProperty(), 0, Motion.ANTHROPIC)),
new KeyFrame(Motion.FAST,
new KeyValue(root.opacityProperty(), 0, Motion.ANTHROPIC),
new KeyValue(root.translateYProperty(), 24, Motion.ANTHROPIC)));
playWindowAnimation(timeline, () -> {
if (getStage() == currentStage) {
currentStage.setIconified(true);
@@ -1032,13 +1028,9 @@ public final class Decorator {
if (!nativeDecorationEnabled && AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
new KeyValue(root.scaleXProperty(), 1, Motion.EASE),
new KeyValue(root.scaleYProperty(), 1, Motion.EASE)),
new KeyFrame(Duration.millis(200),
new KeyValue(root.opacityProperty(), 0, Motion.EASE),
new KeyValue(root.scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(root.scaleYProperty(), 0.8, Motion.EASE)));
new KeyValue(root.opacityProperty(), 1, Motion.ANTHROPIC)),
new KeyFrame(Motion.FAST,
new KeyValue(root.opacityProperty(), 0, Motion.ANTHROPIC)));
playWindowAnimation(timeline, Launcher::stopApplication);
} else {
stopWindowAnimation();
@@ -1172,15 +1164,11 @@ public final class Decorator {
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(root.opacityProperty(), 0, Motion.EASE),
new KeyValue(root.translateYProperty(), 200, Motion.EASE),
new KeyValue(root.scaleXProperty(), 0.4, Motion.EASE),
new KeyValue(root.scaleYProperty(), 0.4, Motion.EASE)),
new KeyFrame(Motion.SHORT4,
new KeyValue(root.opacityProperty(), 1, Motion.EASE),
new KeyValue(root.translateYProperty(), 0, Motion.EASE),
new KeyValue(root.scaleXProperty(), 1, Motion.EASE),
new KeyValue(root.scaleYProperty(), 1, Motion.EASE)));
new KeyValue(root.opacityProperty(), 0, Motion.ANTHROPIC),
new KeyValue(root.translateYProperty(), 24, Motion.ANTHROPIC)),
new KeyFrame(Motion.FAST,
new KeyValue(root.opacityProperty(), 1, Motion.ANTHROPIC),
new KeyValue(root.translateYProperty(), 0, Motion.ANTHROPIC)));
playWindowAnimation(timeline, this::resetRootTransform);
}
@@ -2,16 +2,19 @@
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.ui.decorator;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.LayoutMetrics;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
/// Lays out a fixed-width sidebar and a growing content area as independent animation targets.
/// Lays out a φ-proportioned sidebar and a growing content area as independent animation targets.
///
/// Sidebar:content ≈ 1:φ (see [LayoutMetrics]), clamped for tool-mode density.
@NotNullByDefault
public class DecoratorAnimatedPage extends BorderPane {
@@ -25,7 +28,12 @@ public class DecoratorAnimatedPage extends BorderPane {
/// Creates both animation targets without requiring CSS or skin initialization.
public DecoratorAnimatedPage() {
getStyleClass().add("gray-background");
FXUtils.setLimitWidth(left, 200);
var sidebarWidth = Bindings.createDoubleBinding(
() -> LayoutMetrics.sidebarWidth(getWidth()),
widthProperty());
left.minWidthProperty().bind(sidebarWidth);
left.prefWidthProperty().bind(sidebarWidth);
left.maxWidthProperty().bind(sidebarWidth);
sidebar.setMinSize(0, 0);
left.setCenter(sidebar);
setLeft(left);
@@ -42,11 +50,11 @@ public class DecoratorAnimatedPage extends BorderPane {
return left.getBottom();
}
/// Replaces the bottom content, reserving its preferred height and a 12-pixel bottom gap.
/// Replaces the bottom content, reserving its preferred height and a φ-md bottom gap.
/// Passing null removes it without replacing the sidebar animation target.
public void setSidebarBottom(@Nullable Node node) {
if (node != null) {
BorderPane.setMargin(node, new Insets(0, 0, 12, 0));
BorderPane.setMargin(node, new Insets(0, 0, LayoutMetrics.SPACE_MD, 0));
}
left.setBottom(node);
}
@@ -3,7 +3,6 @@
package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXSpinner;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
@@ -24,22 +23,19 @@ import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.glavo.monetfx.ColorRole;
import org.glavo.monetfx.ColorScheme;
import org.jackhuang.hmcl.theme.SkillPalette;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.TaskManager;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import static org.jackhuang.hmcl.ui.animation.TransitionPane.DEFAULT_DURATION;
import static org.jackhuang.hmcl.ui.animation.TransitionPane.DEFAULT_INTERPOLATOR;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// Renders the clipped background, title bar, and navigation content owned by a [Decorator].
@NotNullByDefault
@@ -85,7 +81,7 @@ final class MainWindowPane extends StackPane {
() -> Themes.windowTransparentProperty().get()
? null
: new Background(new BackgroundFill(
Themes.getColorScheme().getColor(ColorRole.SURFACE_CONTAINER),
SkillPalette.surfaceContainer(),
CornerRadii.EMPTY,
Insets.EMPTY)),
Themes.windowTransparentProperty(),
@@ -170,42 +166,14 @@ final class MainWindowPane extends StackPane {
return backgroundNode;
}
/// Creates the task, minimize, and application-close buttons.
/// Creates the minimize and application-close buttons.
///
/// @return a size-limited top-right button row
private HBox createWindowButtons() {
HBox buttons = new HBox();
buttons.setAlignment(Pos.TOP_RIGHT);
decorator.capableDraggingWindow(buttons);
buttons.setMaxSize(Region.USE_PREF_SIZE, 40);
JFXButton tasksButton = new JFXButton();
tasksButton.setFocusTraversable(false);
tasksButton.setAccessibleText(i18n("task.list"));
tasksButton.getStyleClass().add("jfx-decorator-button");
tasksButton.setOnAction(event -> {
if (!(decorator.getNavigator().getCurrentPage() instanceof TaskManagerPage))
Controllers.navigate(new TaskManagerPage());
});
decorator.forbidDraggingWindow(tasksButton);
SVGPath tasksEmptyIcon = SVG.LIST_ALT.createIcon(Themes.titleFillProperty());
holder.onWeakChangeAndOperate(TaskManager.getInstance().statusProperty(), status -> {
switch (status) {
case IDLE -> tasksButton.setGraphic(tasksEmptyIcon);
case PENDING -> tasksButton.setGraphic(SVG.PENDING.createIcon(Themes.titleFillProperty()));
case RUNNING -> {
JFXSpinner tasksSpinner = new JFXSpinner();
tasksSpinner.getStyleClass().add("small-spinner");
tasksSpinner.setRadius(9);
tasksSpinner.setMouseTransparent(true);
FXUtils.setLimitWidth(tasksSpinner, 20);
FXUtils.setLimitHeight(tasksSpinner, 20);
tasksButton.setGraphic(tasksSpinner);
}
}
});
buttons.setMaxSize(Region.USE_PREF_SIZE, 44);
JFXButton minimizeButton = new JFXButton();
minimizeButton.setFocusTraversable(false);
@@ -221,24 +189,27 @@ final class MainWindowPane extends StackPane {
closeButton.setOnAction(event -> decorator.closeWindow());
decorator.forbidDraggingWindow(closeButton);
buttons.getChildren().setAll(tasksButton, minimizeButton, closeButton);
buttons.getChildren().setAll(minimizeButton, closeButton);
return buttons;
}
/// Recomputes the title-bar background for the current theme and transparency preference.
///
/// Uses surface / surface-container tones so the chrome stays calm and brand-forward
/// instead of a saturated primary strip.
private void updateTitleBarBackground() {
@Nullable ColorScheme colorScheme = Themes.getColorScheme();
if (colorScheme == null) {
titleBar.setBackground(null);
} else if (Themes.titleBarTransparentProperty().get()) {
Color surface = colorScheme.getSurface();
Color surface = SkillPalette.surface(SkillPalette.isDark(colorScheme));
titleBar.setBackground(new Background(new BackgroundFill(
new Color(surface.getRed(), surface.getGreen(), surface.getBlue(), 0.5),
CornerRadii.EMPTY,
Insets.EMPTY)));
} else {
titleBar.setBackground(new Background(new BackgroundFill(
colorScheme.getPrimaryContainer(),
SkillPalette.surfaceContainer(SkillPalette.isDark(colorScheme)),
CornerRadii.EMPTY,
Insets.EMPTY)));
}
@@ -358,7 +329,7 @@ final class MainWindowPane extends StackPane {
/// {@inheritDoc}
@Override
public Timeline animate(Pane container, Node previousNode, Node nextNode) {
return createTimeline(previousNode, nextNode, 50, -50);
return createTimeline(previousNode, nextNode, 18, -18);
}
/// {@inheritDoc}
@@ -382,7 +353,7 @@ final class MainWindowPane extends StackPane {
/// {@inheritDoc}
@Override
public Timeline animate(Pane container, Node previousNode, Node nextNode) {
return createTimeline(previousNode, nextNode, -50, 50);
return createTimeline(previousNode, nextNode, -18, 18);
}
/// {@inheritDoc}
@@ -145,7 +145,6 @@ public final class ModpackSelectionPage extends VBox implements WizardPage {
}, "", new URLValidator());
}
@Override
public void cleanup(SettingsMap settings) {
}
@@ -132,7 +132,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
ALL,
RELEASE,
SNAPSHOTS,
APRIL_FOOLS,
OLD
}
@@ -234,13 +233,8 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
imageView.setImage(GameInstanceIconType.GRASS.getIcon());
}
case SNAPSHOT, PENDING, UNOBFUSCATED -> {
if (versionType == ComponentRemoteVersion.Type.SNAPSHOT && gameVersion.isAprilFools()) {
tags.add(new Tag(i18n("instance.game.april_fools")));
imageView.setImage(GameInstanceIconType.APRIL_FOOLS.getIcon());
} else {
tags.add(new Tag(i18n("instance.game.snapshot")));
imageView.setImage(GameInstanceIconType.COMMAND.getIcon());
}
tags.add(new Tag(i18n("instance.game.snapshot")));
imageView.setImage(GameInstanceIconType.COMMAND.getIcon());
}
default -> {
tags.add(new Tag(i18n("instance.game.old")));
@@ -334,7 +328,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
VersionTypeFilter.ALL,
VersionTypeFilter.RELEASE,
VersionTypeFilter.SNAPSHOTS,
VersionTypeFilter.APRIL_FOOLS,
VersionTypeFilter.OLD
);
categoryField.getSelectionModel().select(VersionTypeFilter.RELEASE);
@@ -461,8 +454,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
case SNAPSHOTS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT
|| versionType == ComponentRemoteVersion.Type.PENDING
|| versionType == ComponentRemoteVersion.Type.UNOBFUSCATED;
case APRIL_FOOLS -> versionType == ComponentRemoteVersion.Type.SNAPSHOT
&& it.getGameVersion().isAprilFools();
case OLD -> versionType == ComponentRemoteVersion.Type.OLD;
// case ALL,
default -> true;
@@ -45,7 +45,6 @@ public class GameInstanceIconDialog extends JFXDialogLayout {
createIcon(GameInstanceIconType.CHEST),
createIcon(GameInstanceIconType.CHICKEN),
createIcon(GameInstanceIconType.COMMAND),
createIcon(GameInstanceIconType.APRIL_FOOLS),
createIcon(GameInstanceIconType.OPTIFINE),
createIcon(GameInstanceIconType.CRAFT_TABLE),
createIcon(GameInstanceIconType.FABRIC),
@@ -19,8 +19,6 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class HMCLLocalizedDownloadListPage extends DownloadListPage {
public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) {
return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddon.Type.RESOURCE_PACK, true, true);
}
@@ -10,14 +10,11 @@ import org.jackhuang.hmcl.game.GameComponentType;
import org.jackhuang.hmcl.game.HMCLGameInstance;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InstallerListPage extends ListPageBase<InstallerItem> {
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private @Nullable HMCLGameInstance gameInstance;
@@ -116,7 +113,6 @@ public class InstallerListPage extends ListPageBase<InstallerItem> {
}
}
private class InstallerListPageSkin extends ToolbarListPageSkin<InstallerItem, InstallerListPage> {
InstallerListPageSkin() {
@@ -78,7 +78,6 @@ public final class Instances {
installServerModpack();
}
public static void deleteInstance(HMCLGameInstance gameInstance) {
HMCLGameRepository repository = gameInstance.getRepository();
GameInstanceID instanceId = gameInstance.getId();
@@ -127,7 +126,6 @@ public final class Instances {
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName) || newVersionName.equals(instanceId.toString())));
}
public static void openFolder(HMCLGameInstance gameInstance) {
FXUtils.openFolder(gameInstance.getRunDirectory());
}
@@ -309,7 +309,6 @@ public final class ModListPage extends ListPageBase<ModListPage.ModInfoObject> i
}
}
public void rollback(LocalModFile from, LocalModFile to) {
try {
modManager.rollback(from, to);
@@ -187,7 +187,6 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
}
}
private void onOpenFolder() {
if (resourcePackDirectory != null) {
FXUtils.openFolder(resourcePackDirectory);
@@ -149,7 +149,6 @@ public final class WorldListPage extends ListPageBase<World> {
installWorld(res.get(0));
}
private void installWorld(Path zipFile) {
// Only accept one world file because user is required to confirm the new world name
// Or too many input dialogs are popped.
@@ -15,6 +15,7 @@ import javafx.scene.layout.*;
import org.jackhuang.hmcl.setting.ProxyType;
import org.jackhuang.hmcl.task.FetchTask;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.LayoutMetrics;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.util.Holder;
@@ -30,8 +31,8 @@ public class DownloadSettingsPage extends StackPane {
private final WeakListenerHolder holder = new WeakListenerHolder();
public DownloadSettingsPage() {
VBox content = new VBox(10);
content.setPadding(new Insets(10));
VBox content = new VBox(LayoutMetrics.SPACE_MD);
content.setPadding(new Insets(LayoutMetrics.SPACE_SM, LayoutMetrics.SPACE_MD, LayoutMetrics.SPACE_MD, LayoutMetrics.SPACE_MD));
content.setFillWidth(true);
ScrollPane scrollPane = new ScrollPane(content);
FXUtils.smoothScrolling(scrollPane);
@@ -27,6 +27,7 @@ import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.LayoutMetrics;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@@ -79,7 +80,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
state.setValue(new State(null, titleNode, false, false, true));
setPadding(new Insets(20));
// Tool-density φ ladder (skill): md/lg insets — not landing-page whitespace.
setPadding(new Insets(LayoutMetrics.SPACE_MD, LayoutMetrics.SPACE_MD, LayoutMetrics.SPACE_LG, LayoutMetrics.SPACE_MD));
@@ -186,8 +188,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
private void launch() {
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
Instances.launch(repository.getSelectedInstance());
@@ -198,9 +198,6 @@ public final class MainPage extends StackPane implements DecoratorPage {
Instances.installServerModpack();
}
@Override
public ReadOnlyObjectWrapper<State> stateProperty() {
return state;
@@ -237,5 +234,4 @@ public final class MainPage extends StackPane implements DecoratorPage {
return instances;
}
}
@@ -21,30 +21,19 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.Font;
import javafx.scene.text.FontSmoothingType;
import javafx.stage.FileChooser;
import org.glavo.monetfx.Brightness;
import org.glavo.monetfx.ColorStyle;
import org.glavo.uuid.UUIDs;
import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.theme.*;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.SafeStringConverter;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Locale;
@@ -55,7 +44,6 @@ import java.util.function.Supplier;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.setting.SettingsManager.userSettings;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// Configures launcher appearance, background, and launcher font settings.
@NotNullByDefault
@@ -1029,7 +1017,7 @@ public class PersonalizationPage extends StackPane {
}
}
vbox.getChildren().add(new Label("Hello Minecraft! Launcher"));
vbox.getChildren().add(new Label("TDLM"));
fontPane.getContent().add(vbox);
}
@@ -14,7 +14,7 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.LayoutMetrics;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.util.Lang;
@@ -34,7 +34,6 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.zip.GZIPInputStream;
@@ -49,103 +48,94 @@ public final class SettingsPage extends ScrollPane {
public SettingsPage() {
this.setFitToWidth(true);
VBox rootPane = new VBox(10);
rootPane.setPadding(new Insets(10));
// Title:body φ ladder — tight tool density (sm gap, md/lg insets).
VBox rootPane = new VBox(LayoutMetrics.SPACE_SM);
rootPane.setPadding(new Insets(LayoutMetrics.SPACE_SM, LayoutMetrics.SPACE_MD, LayoutMetrics.SPACE_MD, LayoutMetrics.SPACE_MD));
this.setContent(rootPane);
FXUtils.smoothScrolling(this);
{
ComponentList languagePaneList = new ComponentList();
{
ComponentList languagePaneList = new ComponentList();
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
{
var chooseLanguagePane = new LineSelectButton<SupportedLocale>();
chooseLanguagePane.setTitle(i18n("settings.launcher.language"));
chooseLanguagePane.setSubtitle(i18n("settings.take_effect_after_restart"));
SupportedLocale currentLocale = I18n.getLocale();
chooseLanguagePane.setNullSafeConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
});
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
chooseLanguagePane.valueProperty().bindBidirectional(settings().languageProperty());
SupportedLocale currentLocale = I18n.getLocale();
chooseLanguagePane.setNullSafeConverter(locale -> {
if (locale.isDefault())
return locale.getDisplayName(currentLocale);
else if (locale.isSameLanguage(currentLocale))
return locale.getDisplayName(locale);
else
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
});
chooseLanguagePane.setItems(SupportedLocale.getSupportedLocales());
chooseLanguagePane.valueProperty().bindBidirectional(settings().languageProperty());
languagePaneList.getContent().add(chooseLanguagePane);
languagePaneList.getContent().add(chooseLanguagePane);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.language")), languagePaneList);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.language")), languagePaneList);
}
{
ComponentList miscPaneList = new ComponentList();
{
ComponentList miscPaneList = new ComponentList();
BorderPane debugPane = new BorderPane();
{
LineSwitch disableAprilFools = new LineSwitch();
disableAprilFools.setTitle(i18n("settings.launcher.disable_april_fools"));
disableAprilFools.setSubtitle(i18n("settings.take_effect_after_restart"));
disableAprilFools.selectedProperty().bindBidirectional(settings().disableAprilFoolsProperty());
miscPaneList.getContent().add(disableAprilFools);
}
Label left = new Label(i18n("settings.launcher.debug"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
{
BorderPane debugPane = new BorderPane();
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
Label left = new Label(i18n("settings.launcher.debug"));
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
debugPane.setLeft(left);
SpinnerPane exportLogPane = new SpinnerPane();
JFXButton openLogFolderButton = new JFXButton(i18n("settings.launcher.launcher_log.reveal"));
openLogFolderButton.setOnAction(e -> openLogFolder());
openLogFolderButton.getStyleClass().add("jfx-button-border");
if (LOG.getLogFile() == null)
openLogFolderButton.setDisable(true);
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
SpinnerPane exportLogPane = new SpinnerPane();
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
JFXButton logButton = FXUtils.newBorderButton(i18n("settings.launcher.launcher_log.export"));
exportLogPane.setContent(logButton);
logButton.setOnAction(e -> {
exportLogPane.showSpinner();
onExportLogs().whenCompleteAsync((result, exception) -> {
exportLogPane.hideSpinner();
if (exception == null) {
Controllers.dialog(i18n("settings.launcher.launcher_log.export.success", result));
FXUtils.showFileInExplorer(result);
} else {
LOG.warning("Failed to export logs", exception);
Controllers.dialog(
i18n("settings.launcher.launcher_log.export.failed") + "\n" + StringUtils.getStackTrace(exception),
null,
MessageType.ERROR
);
}
}, Schedulers.javafx());
});
HBox buttonBox = new HBox();
buttonBox.setSpacing(10);
buttonBox.getChildren().addAll(openLogFolderButton, exportLogPane);
BorderPane.setAlignment(buttonBox, Pos.CENTER_RIGHT);
debugPane.setRight(buttonBox);
miscPaneList.getContent().add(debugPane);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.misc")), miscPaneList);
miscPaneList.getContent().add(debugPane);
}
rootPane.getChildren().addAll(ComponentList.createComponentListTitle(i18n("settings.launcher.misc")), miscPaneList);
}
}
private void openLogFolder() {
FXUtils.openFolder(LOG.getLogFile().getParent());
}
private static String getEntryName(Set<String> entryNames, String name) {
if (entryNames.add(name)) {
return name;
@@ -1,56 +0,0 @@
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.util.i18n.LocaleUtils;
import java.time.LocalDate;
import java.time.Month;
import java.util.List;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
/// April Fools' Day utilities.
///
/// This class provides methods to check if it is April Fools' Day or near April Fools' Day.
/// It also provides a method to check if April Fools is enabled.
///
/// @author Glavo
public final class AprilFools {
private static final boolean ENABLED;
static {
var date = LocalDate.now();
// Some countries/regions may oppose April Fools' Day for various reasons.
// Therefore, we use a regional whitelist to avoid risks.
// Currently, we have only listed a limited set of countries/regions for testing.
// We will investigate more countries/regions in the future to expand this list.
boolean supportedRegion = List.of(
"CN", "TW", "HK", "MO", "JP", "KR", "VN", "SG", "MY",
"ES", "DE", "FR", "GB", "RU", "UA", "US"
).contains(LocaleUtils.SYSTEM_DEFAULT.getCountry());
boolean aprilFoolsMode;
String value = System.getProperty("hmcl.april_fools", System.getenv("HMCL_APRIL_FOOLS"));
if ("true".equalsIgnoreCase(value))
aprilFoolsMode = true;
else if ("false".equalsIgnoreCase(value) || !supportedRegion)
aprilFoolsMode = false;
else
aprilFoolsMode = date.getMonth() == Month.APRIL && date.getDayOfMonth() == 1;
ENABLED = aprilFoolsMode && !settings().disableAprilFoolsProperty().get();
}
/// Whether April Fools is enabled.
///
/// This method returns true if April Fools is enabled.
public static boolean isEnabled() {
return ENABLED;
}
private AprilFools() {
}
}
@@ -3,7 +3,6 @@
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -1,35 +1,48 @@
/*
* Anthropic UI — dark brightness tokens (skill charcoal #1A1916 family).
* Loaded after the generated Monet theme sheet; keep AA contrast on surfaces/text.
*/
* {
-anthropic-shadow: rgba(0, 0, 0, 0.50);
-anthropic-shadow-strong: rgba(0, 0, 0, 0.65);
-anthropic-scrollbar-track: #222119;
-anthropic-scrollbar-thumb: #5C5A54;
-anthropic-hairline: -monet-outline-variant;
-anthropic-focus-ring: #D97757;
}
.hint {
-fx-hyperlink-fill: -monet-inverse-primary;
-fx-hyperlink-fill: -monet-primary;
}
.tag {
-fixed-warning-tag-background: #2c0b0e;
-fixed-warning-tag-background: #4A1A1F;
}
.log-window {
-fixed-log-background: #282828;
-fixed-log-toggle-selected: #DEE2E6;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #e3e3e3;
-fixed-log-trace-background: #495057;
-fixed-log-debug-background: #343A40;
-fixed-log-info-background: #212529;
-fixed-log-warn-background: #413c26;
-fixed-log-warn-text-fill: #fdf3aa;
-fixed-log-error-background: #4e3534;
-fixed-log-error-text-fill: #f9dedc;
-fixed-log-background: -monet-surface;
-fixed-log-toggle-selected: -monet-on-surface;
-fixed-log-toggle-unselected: -monet-on-surface-variant;
-fixed-log-text-fill: -monet-on-surface;
-fixed-log-trace-background: -monet-surface-container-high;
-fixed-log-debug-background: -monet-surface-container;
-fixed-log-info-background: -monet-surface;
-fixed-log-warn-background: #3F3A26;
-fixed-log-warn-text-fill: #F7E8A0;
-fixed-log-error-background: #4E3534;
-fixed-log-error-text-fill: #F9DEDC;
-fixed-log-fatal-background: #842029;
-fixed-log-selected: #3e3e3e;
-fixed-log-selected: -monet-surface-container-highest;
}
.addon-channel-icon.alpha .svg {
-fx-fill: #ff496e;
-fx-fill: #E08A8A;
}
.addon-channel-icon.beta .svg {
-fx-fill: #ffa347;
-fx-fill: #E08A6C;
}
.addon-channel-icon.release .svg {
-fx-fill: #1bd96a;
-fx-fill: #A3B58A;
}
@@ -1,3 +1,16 @@
/*
* Anthropic UI — light brightness tokens (skill-aligned warm neutrals).
* Loaded after the generated Monet theme sheet; keep AA contrast on surfaces/text.
*/
* {
-anthropic-shadow: rgba(20, 20, 19, 0.10);
-anthropic-shadow-strong: rgba(20, 20, 19, 0.16);
-anthropic-scrollbar-track: #E8E6DC;
-anthropic-scrollbar-thumb: #9B9890;
-anthropic-hairline: -monet-outline-variant;
-anthropic-focus-ring: #D97757;
}
.hint {
-fx-hyperlink-fill: -monet-primary;
}
@@ -7,29 +20,29 @@
}
.log-window {
-fixed-log-background: #ffffff;
-fixed-log-toggle-selected: #000000;
-fixed-log-toggle-unselected: #6C757D;
-fixed-log-text-fill: #1f1f1f;
-fixed-log-trace-background: #EEE9E0;
-fixed-log-debug-background: #EEE9E0;
-fixed-log-info-background: #FFFFFF;
-fixed-log-warn-background: #fef6d5;
-fixed-log-warn-text-fill: #3e2f00;
-fixed-log-error-background: #fcebeb;
-fixed-log-error-text-fill: #410e0b;
-fixed-log-background: -monet-surface;
-fixed-log-toggle-selected: -monet-on-surface;
-fixed-log-toggle-unselected: -monet-on-surface-variant;
-fixed-log-text-fill: -monet-on-surface;
-fixed-log-trace-background: -monet-surface-container;
-fixed-log-debug-background: -monet-surface-container-low;
-fixed-log-info-background: -monet-surface;
-fixed-log-warn-background: #F7F0D8;
-fixed-log-warn-text-fill: #3E2F00;
-fixed-log-error-background: #FCEBEB;
-fixed-log-error-text-fill: #410E0B;
-fixed-log-fatal-background: #F7A699;
-fixed-log-selected: #f2f2f2;
-fixed-log-selected: -monet-surface-container-highest;
}
.addon-channel-icon.alpha .svg {
-fx-fill: #cb2245;
-fx-fill: #C0453A;
}
.addon-channel-icon.beta .svg {
-fx-fill: #e08325;
-fx-fill: #C96442;
}
.addon-channel-icon.release .svg {
-fx-fill: #00af5c;
-fx-fill: #5F7A4A;
}
+50 -5
View File
@@ -1,8 +1,53 @@
/*
* SPDX-FileCopyrightText: Copyright (C) 2025 The HMCL Authors
* SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
* Anthropic skill type roles (CN+Latin):
* - Sans = UI / nav / buttons / chrome titles (Source Han Sans)
* - Serif = body / narrative copy (Source Han Serif)
*/
.root {
/* https://github.com/HMCL-dev/HMCL/pull/3423 */
-fx-font-family: -fx-base-font-family;
}
-fx-font-family: "Source Han Serif SC", "Source Han Serif", "思源宋体", serif;
}
/* UI chrome + interactive — sans */
.jfx-decorator-title,
.jfx-tool-bar-second .label,
.jfx-tool-bar Label,
.title-label,
.class-title,
.class-title Text,
.jfx-layout-heading,
.launch-pane > .launch-button .label,
.announcement .title,
.method-title,
.radio-button-title-label,
.two-line-list-item > .title,
.advanced-list-item > .rippler-container > .container > .title,
.iconed-item .iconed-item-container,
.iconed-menu-item .iconed-item-container,
.options-list-item .label,
.jfx-button,
.jfx-button *,
.jfx-button-raised,
.jfx-button-raised *,
.jfx-button-border,
.jfx-button-border *,
.tab-label,
.dialog-accept,
.dialog-cancel,
.line-component .title-label,
.menu-container .title-label {
-fx-font-family: "Source Han Sans SC", "Source Han Sans", "思源黑体", sans-serif;
-fx-font-weight: bold;
}
/* Body / secondary copy — serif */
.subtitle-label,
.two-line-list-item > .subtitle,
.advanced-list-item > .rippler-container > .container > .subtitle,
.announcement text,
.method-desc,
.options-list-item .label.subtitle,
.hint,
.html {
-fx-font-family: "Source Han Serif SC", "Source Han Serif", "思源宋体", serif;
-fx-font-weight: normal;
}
@@ -0,0 +1,113 @@
/*
* Anthropic skill — hard press feedback (loaded LAST, beats Monet/root).
* Forced :pressed / :armed change background AND border AND opacity.
* Solid hex only (no fragile lookups) so Modena cyan cannot win on specificity.
*/
.jfx-button:pressed,
.jfx-button:armed,
.toggle-icon4:pressed,
.toggle-icon4:armed,
.toggle-icon-tiny:pressed,
.toggle-icon-tiny:armed,
.announcement-close-button:pressed,
.announcement-close-button:armed,
.jfx-decorator-button:pressed,
.jfx-decorator-button:armed,
.jfx-tool-bar-button:pressed,
.jfx-tool-bar-button:armed,
.jfx-tool-bar .jfx-decorator-button:pressed,
.jfx-tool-bar .jfx-decorator-button:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-border-radius: 6px;
-fx-background-radius: 6px;
-fx-opacity: 0.84;
-fx-scale-x: 0.94;
-fx-scale-y: 0.94;
}
.jfx-button:hover,
.toggle-icon4:hover,
.toggle-icon-tiny:hover,
.announcement-close-button:hover,
.jfx-decorator-button:hover,
.jfx-tool-bar-button:hover {
-fx-background-color: rgba(217, 119, 87, 0.16);
-fx-border-color: rgba(201, 100, 66, 0.55);
-fx-border-width: 1px;
-fx-opacity: 1;
}
.jfx-button-raised:pressed,
.jfx-button-raised:armed,
.jfx-button-raised-round:pressed,
.jfx-button-raised-round:armed,
.launch-pane > .jfx-button:pressed,
.launch-pane > .jfx-button:armed {
-fx-background-color: #9A4A32;
-fx-border-color: #F5F3EC;
-fx-border-width: 2px;
-fx-opacity: 0.90;
-fx-scale-x: 0.96;
-fx-scale-y: 0.96;
}
.jfx-button-border:pressed,
.jfx-button-border:armed {
-fx-background-color: rgba(217, 119, 87, 0.28);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-opacity: 0.88;
-fx-scale-x: 0.96;
-fx-scale-y: 0.96;
}
.toggle-icon4:pressed,
.toggle-icon4:armed,
.toggle-icon-tiny:pressed,
.toggle-icon-tiny:armed,
.announcement-close-button:pressed,
.announcement-close-button:armed {
-fx-background-radius: 50px;
-fx-border-radius: 50px;
}
.jfx-button:disabled:pressed,
.jfx-button:disabled:armed,
.toggle-icon4:disabled:pressed,
.toggle-icon4:disabled:armed,
.toggle-icon-tiny:disabled:pressed,
.toggle-icon-tiny:disabled:armed,
.jfx-decorator-button:disabled:pressed,
.jfx-decorator-button:disabled:armed,
.jfx-tool-bar-button:disabled:pressed,
.jfx-tool-bar-button:disabled:armed {
-fx-opacity: 0.45;
-fx-background-color: transparent;
-fx-border-width: 0;
-fx-scale-x: 1;
-fx-scale-y: 1;
}
/* Skin-driven armed class — mirrors :pressed when Modena/CSS lose the race. */
.jfx-button.anthropic-armed,
.toggle-icon4.anthropic-armed,
.toggle-icon-tiny.anthropic-armed,
.jfx-decorator-button.anthropic-armed,
.jfx-tool-bar-button.anthropic-armed,
.announcement-close-button.anthropic-armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-opacity: 0.84;
}
.jfx-button-raised.anthropic-armed,
.launch-pane > .jfx-button.anthropic-armed {
-fx-background-color: #9A4A32;
-fx-border-color: #F5F3EC;
-fx-border-width: 2px;
-fx-opacity: 0.90;
}
+353 -99
View File
@@ -120,7 +120,7 @@
.radio-button-title-label {
-fx-font-size: 16px;
-fx-padding: 14.0 0.0 -20.0 0.0;
-fx-text-fill: rgba(0.0, 0.0, 0.0, 0.87);
-fx-text-fill: -monet-on-surface;
}
.announcement {
@@ -144,7 +144,8 @@
}
.rippler-container {
-jfx-feedback-fill: -monet-on-surface;
/* Anthropic chrome: no Material ripple / press ink */
-jfx-feedback-fill: transparent;
}
.rippler-container:press-feedback-enabled {
@@ -185,6 +186,18 @@
.advanced-list-item {
-jfx-selected-background-fill: -monet-secondary-container;
-fx-background-radius: 6px;
}
.advanced-list-item:hover > .rippler-container {
-fx-background-color: -anthropic-hover-wash;
-fx-background-radius: 6px;
}
.advanced-list-item:pressed > .rippler-container,
.advanced-list-item:armed > .rippler-container {
-fx-background-color: -anthropic-press-wash;
-fx-background-radius: 6px;
}
.advanced-list-item:selected > .rippler-container > .container > .title {
@@ -229,8 +242,9 @@
}
.class-title {
-fx-font-size: 12px;
-fx-padding: 8 16 8 16;
/* Title block of title:body ≈ 1:φ — md/sm padding under tool density */
-fx-font-size: 11px;
-fx-padding: 13 13 8 13;
}
.class-title Text {
@@ -245,7 +259,7 @@
}
.advanced-list-box-content {
-fx-padding: 12 0 0 0;
-fx-padding: 13 0 0 0;
-fx-spacing: 0;
}
@@ -258,8 +272,9 @@
}
.iconed-item .iconed-item-container {
-fx-padding: 10 16 10 16;
-fx-spacing: 10;
/* Body row under φ title:body — sm/md denser than landing */
-fx-padding: 8 13 8 13;
-fx-spacing: 13;
-fx-font-size: 14;
-fx-alignment: CENTER_LEFT;
}
@@ -274,14 +289,14 @@
}
.iconed-menu-item .iconed-item-container {
-fx-padding: 8 16 8 16;
-fx-spacing: 10;
-fx-padding: 8 13 8 13;
-fx-spacing: 13;
-fx-font-size: 12;
-fx-alignment: CENTER_LEFT;
}
.menu-container {
-fx-padding: 8 16 8 16;
-fx-padding: 8 13 8 13;
}
.popup-menu .scroll-bar .thumb {
@@ -392,7 +407,7 @@
}
.installer-item-wrapper:card {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
-fx-effect: dropshadow(gaussian, -anthropic-shadow, 8, 0.08, 0, 1);
}
.installer-item-wrapper .installer-item:list-item {
@@ -421,57 +436,82 @@
******************************************************************************/
.launch-pane {
-fx-max-height: 57px;
-fx-min-height: 57px;
-fx-max-width: 230px;
-fx-min-width: 230px;
/* φ/Fibonacci CTA: 55×233 pairs with title bar 44 and sidebar rail */
-fx-max-height: 55px;
-fx-min-height: 55px;
-fx-max-width: 233px;
-fx-min-width: 233px;
}
.launch-pane > .jfx-button {
-fx-translate-y: 1px;
-fx-max-height: 55px;
-fx-min-height: 55px;
-fx-background-color: -monet-primary-container;
-fx-max-height: 53px;
-fx-min-height: 53px;
-fx-background-color: -monet-primary;
-fx-opacity: 1; /* Override the opacity of disabled button */
-fx-cursor: hand;
-fx-scale-x: 1;
-fx-scale-y: 1;
}
.launch-pane > .jfx-button:hover {
-fx-background-color: -anthropic-primary-hover;
-fx-scale-x: 1.02;
-fx-scale-y: 1.02;
}
.launch-pane > .jfx-button:pressed,
.launch-pane > .jfx-button:armed {
-fx-background-color: #9A4A32;
-fx-border-color: #F5F3EC;
-fx-border-width: 2px;
-fx-opacity: 0.90;
-fx-scale-x: 0.96;
-fx-scale-y: 0.96;
}
.launch-pane > .jfx-button:focused {
-fx-border-color: -anthropic-focus-ring;
-fx-border-width: 1.5px;
}
.launch-pane > .jfx-button.launch-button {
-fx-max-width: 200px;
-fx-min-width: 200px;
-fx-border-width: 0 3px 0 0;
-fx-max-width: 201px;
-fx-min-width: 201px;
-fx-border-width: 0 1px 0 0;
-fx-border-color: linear-gradient(
to bottom,
transparent 0%,
transparent 20%,
-monet-on-surface-variant 20%,
-monet-on-surface-variant 80%,
transparent 80%,
to bottom,
transparent 0%,
transparent 22%,
-monet-on-primary 22%,
-monet-on-primary 78%,
transparent 78%,
transparent 100%
);
-fx-background-radius: 4px 0 0 4px;
-fx-background-radius: 8px 0 0 8px;
}
.launch-pane > .jfx-button.menu-button {
-fx-max-width: 27px;
-fx-min-width: 27px;
-fx-max-width: 32px;
-fx-min-width: 32px;
-fx-font-size: 15px;
-fx-background-radius: 0 4px 4px 0;
-fx-background-radius: 0 8px 8px 0;
}
.launch-pane > .jfx-button > StackPane > .jfx-rippler {
-jfx-rippler-fill: -monet-on-primary-container;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
-jfx-mask-type: CIRCLE;
-fx-padding: 0;
}
.launch-pane > .launch-button .label {
-fx-text-fill: -monet-on-primary-container;
-fx-font-size: 14px;
-fx-text-fill: -monet-on-primary;
-fx-font-size: 15px;
}
.launch-pane .svg {
-fx-fill: -monet-on-primary-container;
-fx-fill: -monet-on-primary;
}
/*******************************************************************************
@@ -498,7 +538,8 @@
}
.jfx-tab-pane .jfx-rippler {
-jfx-rippler-fill: white;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
}
.tab-label {
@@ -520,8 +561,8 @@
******************************************************************************/
.dialog-trigger {
-fx-background-color: WHITE;
-jfx-button-type: RAISED;
-fx-background-color: -monet-surface-container-highest;
-jfx-button-type: FLAT;
-fx-font-size: 14.0px;
}
@@ -657,7 +698,7 @@
******************************************************************************/
.icon {
-fx-fill: #FE774D;
-fx-fill: -monet-primary;
-fx-padding: 10.0;
-fx-cursor: hand;
}
@@ -671,17 +712,18 @@
.jfx-tool-bar {
-fx-font-size: 13.0;
-fx-pref-width: 100.0%;
-fx-min-height: 40px;
-fx-max-height: 40px;
-fx-pref-height: 40px;
-fx-min-height: 44px;
-fx-max-height: 44px;
-fx-pref-height: 44px;
}
.jfx-tool-bar .jfx-decorator-title {
-fx-font-weight: BOLD;
-fx-font-size: 16px;
}
.jfx-tool-bar.background {
-fx-background-color: -monet-primary-container;
-fx-background-color: -monet-surface-container;
}
/*.jfx-tool-bar HBox {*/
@@ -690,21 +732,21 @@
/*}*/
.jfx-tool-bar .jfx-decorator-button {
-fx-min-width: 40px;
-fx-max-width: 40px;
-fx-pref-width: 40px;
-fx-min-height: 40px;
-fx-max-height: 40px;
-fx-pref-height: 40px;
-fx-min-width: 44px;
-fx-max-width: 44px;
-fx-pref-width: 44px;
-fx-min-height: 44px;
-fx-max-height: 44px;
-fx-pref-height: 44px;
-fx-cursor: hand;
}
.jfx-tool-bar .jfx-decorator-button .svg {
-fx-fill: -monet-on-primary-container;
-fx-fill: -monet-on-surface;
}
.jfx-decorator-button .jfx-spinner .arc {
-fx-stroke: -monet-on-primary-container;
-fx-stroke: -monet-on-surface;
}
.jfx-tool-bar Label {
@@ -712,7 +754,7 @@
}
.jfx-tool-bar.gray-background Label {
-fx-text-fill: BLACK;
-fx-text-fill: -monet-on-surface;
}
.jfx-tool-bar Text {
@@ -732,15 +774,15 @@
}
.jfx-tool-bar-second {
-fx-pref-height: 42;
-fx-padding: 2 2 2 2;
-fx-background-color: -monet-primary-container;
-fx-pref-height: 48;
-fx-padding: 4 8 4 8;
-fx-background-color: -monet-surface-container;
-fx-alignment: CENTER-LEFT;
-fx-spacing: 8;
}
.jfx-tool-bar-second .label {
-fx-text-fill: -monet-on-primary-container;
-fx-text-fill: -monet-on-surface;
-fx-font-size: 16;
-fx-font-weight: bold;
}
@@ -758,12 +800,31 @@
-fx-min-height: -fx-toggle-icon4-size;
-fx-background-radius: 5px;
-fx-background-color: transparent;
-fx-opacity: 1;
-fx-scale-x: 1;
-fx-scale-y: 1;
-jfx-toggle-color: -monet-on-surface;
-jfx-untoggle-color: transparent;
}
.jfx-tool-bar-button:hover {
-fx-background-color: -anthropic-icon-hover-wash;
-fx-scale-x: 1.04;
-fx-scale-y: 1.04;
}
.jfx-tool-bar-button:pressed,
.jfx-tool-bar-button:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-opacity: 0.84;
-fx-scale-x: 0.94;
-fx-scale-y: 0.94;
}
.jfx-tool-bar-button .icon {
-fx-fill: rgb(204.0, 204.0, 51.0);
-fx-fill: -monet-on-surface;
-fx-padding: 10.0;
}
@@ -774,13 +835,35 @@
.jfx-decorator-button {
-fx-max-width: 40px;
-fx-background-radius: 5px;
-fx-background-radius: 8px;
-fx-max-height: 40px;
-fx-background-color: transparent;
}
.jfx-decorator-button:hover {
-fx-background-color: -anthropic-hover-wash;
-fx-scale-x: 1.04;
-fx-scale-y: 1.04;
}
.jfx-decorator-button:pressed,
.jfx-decorator-button:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-opacity: 0.84;
-fx-scale-x: 0.94;
-fx-scale-y: 0.94;
}
.jfx-decorator-button:focused {
-fx-border-color: -anthropic-focus-ring;
-fx-border-width: 1.5px;
-fx-border-radius: 8px;
}
.jfx-decorator-button .icon {
-fx-fill: rgb(204.0, 204.0, 51.0);
-fx-fill: -monet-on-surface;
-fx-padding: 0.0;
}
@@ -848,6 +931,11 @@
* *
*******************************************************************************/
.jfx-rippler {
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
}
.jfx-rippler:hover {
-fx-cursor: hand;
}
@@ -861,20 +949,80 @@
.jfx-button {
-jfx-disable-visual-focus: true;
-fx-cursor: hand;
-fx-background-radius: 6px;
-fx-background-color: transparent;
-fx-scale-x: 1;
-fx-scale-y: 1;
}
.jfx-button .jfx-rippler {
-jfx-rippler-fill: -monet-on-surface;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
-jfx-mask-type: CIRCLE;
-fx-padding: 0.0;
}
/* Calm tactile feedback: tone + border + opacity (no Material ripple). */
.jfx-button:hover {
-fx-background-color: -anthropic-hover-wash;
-fx-border-color: rgba(201, 100, 66, 0.45);
-fx-border-width: 1px;
-fx-scale-x: 1.02;
-fx-scale-y: 1.02;
}
.jfx-button:pressed,
.jfx-button:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-opacity: 0.84;
-fx-scale-x: 0.94;
-fx-scale-y: 0.94;
}
.jfx-button:focused {
-fx-border-color: -anthropic-focus-ring;
-fx-border-width: 2px;
-fx-border-radius: 6px;
-fx-border-insets: 0;
-fx-effect: dropshadow(gaussian, -anthropic-focus-glow, 0, 0, 0, 0);
}
.jfx-button:disabled,
.jfx-button:disabled:hover,
.jfx-button:disabled:pressed,
.jfx-button:disabled:armed {
-fx-scale-x: 1;
-fx-scale-y: 1;
-fx-opacity: 0.45;
-fx-background-color: transparent;
}
.jfx-button-raised {
-fx-background-color: -monet-primary;
-fx-background-radius: 6px;
}
.jfx-button-raised:hover {
-fx-background-color: -anthropic-primary-hover;
-fx-scale-x: 1.02;
-fx-scale-y: 1.02;
}
.jfx-button-raised:pressed,
.jfx-button-raised:armed {
-fx-background-color: #9A4A32;
-fx-border-color: #F5F3EC;
-fx-border-width: 2px;
-fx-opacity: 0.90;
-fx-scale-x: 0.96;
-fx-scale-y: 0.96;
}
.jfx-button-raised .jfx-rippler {
-jfx-rippler-fill: -monet-on-primary;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
-jfx-mask-type: CIRCLE;
-fx-padding: 0.0;
}
@@ -887,11 +1035,33 @@
.jfx-button-border {
-fx-background-color: transparent;
-fx-border-color: -monet-outline;
-fx-border-radius: 5px;
-fx-border-width: 0.2px;
-fx-border-radius: 6px;
-fx-border-width: 1px;
-fx-padding: 8px;
}
.jfx-button-border:hover {
-fx-background-color: -anthropic-hover-wash;
-fx-border-color: -monet-primary;
-fx-scale-x: 1.02;
-fx-scale-y: 1.02;
}
.jfx-button-border:pressed,
.jfx-button-border:armed {
-fx-background-color: rgba(217, 119, 87, 0.28);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-opacity: 0.88;
-fx-scale-x: 0.96;
-fx-scale-y: 0.96;
}
.jfx-button-border:focused {
-fx-border-color: -anthropic-focus-ring;
-fx-border-width: 1.5px;
}
.jfx-button-border, .jfx-button-border * {
-fx-text-fill: -monet-primary;
}
@@ -901,6 +1071,22 @@
-fx-background-radius: 50px;
}
.jfx-button-raised-round:hover {
-fx-background-color: -anthropic-primary-container-hover;
-fx-scale-x: 1.03;
-fx-scale-y: 1.03;
}
.jfx-button-raised-round:pressed,
.jfx-button-raised-round:armed {
-fx-background-color: #9A4A32;
-fx-border-color: #F5F3EC;
-fx-border-width: 2px;
-fx-opacity: 0.90;
-fx-scale-x: 0.96;
-fx-scale-y: 0.96;
}
.menu-up-down-button .label {
-fx-text-fill: -monet-on-surface;
}
@@ -993,7 +1179,7 @@
.list-cell:selected, .jfx-list-cell:selected,
.list-cell:hover, .jfx-list-cell:hover {
-fx-text-fill: black;
-fx-text-fill: -monet-on-surface;
}
.jfx-list-cell .jfx-rippler {
@@ -1040,31 +1226,31 @@
.options-list {
-fx-background-color: transparent;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
-fx-effect: dropshadow(gaussian, -anthropic-shadow, 4, 0.04, 0, 1);
}
.depth-0 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0), 0, 0, 0, 0);
-fx-effect: dropshadow(gaussian, transparent, 0, 0, 0, 0);
}
.depth-1 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
-fx-effect: dropshadow(gaussian, -anthropic-shadow, 8, 0.08, 0, 1);
}
.depth-2 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 15, 0.16, 0, 4);
-fx-effect: dropshadow(gaussian, -anthropic-shadow-strong, 12, 0.10, 0, 2);
}
.depth-3 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 20, 0.19, 0, 6);
-fx-effect: dropshadow(gaussian, -anthropic-shadow-strong, 16, 0.12, 0, 3);
}
.depth-4 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 25, 0.25, 0, 8);
-fx-effect: dropshadow(gaussian, -anthropic-shadow-strong, 20, 0.14, 0, 4);
}
.depth-5 {
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 30, 0.3, 0, 10);
-fx-effect: dropshadow(gaussian, -anthropic-shadow-strong, 24, 0.16, 0, 5);
}
.padding-8 {
@@ -1073,30 +1259,30 @@
.card {
-fx-background-color: -monet-surface-container-low-transparent-80;
-fx-background-radius: 4;
-fx-padding: 8px;
-fx-background-radius: 8;
-fx-padding: 13px;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
-fx-effect: dropshadow(gaussian, -anthropic-shadow, 6, 0.06, 0, 1);
}
.card-no-padding {
-fx-background-color: -monet-surface-container-low-transparent-80;
-fx-background-radius: 4;
-fx-background-radius: 8;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 10, 0.12, -1, 2);
-fx-effect: dropshadow(gaussian, -anthropic-shadow, 6, 0.06, 0, 1);
}
.card-non-transparent {
-fx-background-color: -monet-surface-container-low;
-fx-background-radius: 4;
-fx-padding: 8px;
-fx-background-radius: 8;
-fx-padding: 13px;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.26), 5, 0.06, -0.5, 1);
-fx-effect: dropshadow(gaussian, -anthropic-shadow, 4, 0.04, 0, 1);
}
.card-list {
-fx-padding: 10;
-fx-spacing: 10;
-fx-padding: 13;
-fx-spacing: 13;
}
.md-list-cell {
@@ -1132,7 +1318,8 @@
-fx-background-color: -monet-surface;
-fx-border-color: -monet-outline-variant;
-fx-border-width: 1 0 0 0;
-fx-padding: 10 16 10 16;
/* Body of title:body — sm vertical / md horizontal */
-fx-padding: 8 13 8 13;
-fx-font-size: 12;
}
@@ -1324,9 +1511,11 @@
.jfx-decorator-button {
-fx-pref-width: 50px;
-fx-background-radius: 50px;
-fx-background-radius: 8px;
-fx-pref-height: 50px;
-fx-background-color: transparent;
-fx-border-width: 0;
-fx-opacity: 1;
}
@@ -1340,17 +1529,38 @@
-fx-min-height: -fx-toggle-icon4-size;
-fx-background-radius: 50px;
-fx-background-color: transparent;
-fx-opacity: 1;
-fx-scale-x: 1;
-fx-scale-y: 1;
-jfx-toggle-color: -monet-primary;
-jfx-untoggle-color: transparent;
}
.toggle-icon4:hover {
-fx-background-color: -anthropic-icon-hover-wash;
-fx-scale-x: 1.06;
-fx-scale-y: 1.06;
}
.toggle-icon4:pressed,
.toggle-icon4:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-border-radius: 50px;
-fx-opacity: 0.84;
-fx-scale-x: 0.90;
-fx-scale-y: 0.90;
}
.toggle-icon4 .icon {
-fx-fill: rgb(204.0, 204.0, 51.0);
-fx-fill: -monet-on-surface;
-fx-padding: 10.0;
}
.toggle-icon4 .jfx-rippler {
-jfx-rippler-fill: -monet-primary;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
-jfx-mask-type: CIRCLE;
}
@@ -1365,12 +1575,33 @@
-fx-padding: 0;
-fx-background-radius: 25px;
-fx-background-color: transparent;
-fx-opacity: 1;
-fx-scale-x: 1;
-fx-scale-y: 1;
-jfx-toggle-color: -monet-primary;
-jfx-untoggle-color: transparent;
}
.toggle-icon-tiny:hover {
-fx-background-color: -anthropic-icon-hover-wash;
-fx-opacity: 0.92;
-fx-scale-x: 1.08;
-fx-scale-y: 1.08;
}
.toggle-icon-tiny:pressed,
.toggle-icon-tiny:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-border-radius: 50px;
-fx-opacity: 0.80;
-fx-scale-x: 0.86;
-fx-scale-y: 0.86;
}
.toggle-icon-tiny .icon {
-fx-fill: rgb(204.0, 204.0, 51.0);
-fx-fill: -monet-on-surface;
-fx-padding: 0;
}
@@ -1379,7 +1610,8 @@
}
.toggle-icon-tiny .jfx-rippler {
-jfx-rippler-fill: -monet-primary;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
-jfx-mask-type: CIRCLE;
}
@@ -1393,10 +1625,31 @@
-fx-min-height: -fx-announcement-close-button-size;
-fx-background-radius: 50px;
-fx-background-color: transparent;
-fx-opacity: 1;
-fx-scale-x: 1;
-fx-scale-y: 1;
}
.announcement-close-button:hover {
-fx-background-color: -anthropic-icon-hover-wash;
-fx-scale-x: 1.06;
-fx-scale-y: 1.06;
}
.announcement-close-button:pressed,
.announcement-close-button:armed {
-fx-background-color: rgba(217, 119, 87, 0.42);
-fx-border-color: #C96442;
-fx-border-width: 2px;
-fx-border-radius: 50px;
-fx-opacity: 0.84;
-fx-scale-x: 0.90;
-fx-scale-y: 0.90;
}
.announcement-close-button .jfx-rippler {
-jfx-rippler-fill: -monet-primary;
-jfx-rippler-disabled: true;
-jfx-rippler-fill: transparent;
-jfx-mask-type: CIRCLE;
}
@@ -1598,12 +1851,12 @@
}
.jfx-combo-box-warning {
-jfx-focus-color: #D34336;
-jfx-unfocus-color: #D34336;
-jfx-focus-color: -monet-error;
-jfx-unfocus-color: -monet-error;
}
.jfx-combo-box-warning .text {
-fx-fill: #D34336;
-fx-fill: -monet-error;
}
/*.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {*/
@@ -1781,15 +2034,16 @@
*******************************************************************************/
.jfx-decorator {
-fx-decorator-color: -monet-primary-container;
-fx-decorator-color: -monet-surface-container;
}
.jfx-decorator-drawer {
-fx-background-color: rgba(244, 244, 244, 0.5);
-fx-background-color: -monet-surface-transparent-80;
}
.jfx-decorator-title {
-fx-font-size: 14;
-fx-font-size: 16;
-fx-font-weight: bold;
}
.debug-border {
@@ -1798,11 +2052,11 @@
}
.content-background {
-fx-background-color: -monet-surface-transparent-50;
-fx-background-color: -monet-surface-transparent-80;
}
.gray-background {
-fx-background-color: -monet-surface-transparent-50;
-fx-background-color: -monet-surface-container-low-transparent-80;
}
/*******************************************************************************
@@ -1812,12 +2066,12 @@
******************************************************************************/
.scroll-bar:vertical > .track-background, .scroll-bar:horizontal > .track-background {
-fx-background-color: #F1F1F1;
-fx-background-color: -anthropic-scrollbar-track;
-fx-background-insets: 0.0;
}
.scroll-bar:vertical .thumb, .scroll-bar:horizontal .thumb {
-fx-background-color: #BCBCBC;
-fx-background-color: -anthropic-scrollbar-thumb;
-fx-background-insets: 0.0;
-fx-background-radius: 1.0;
}
@@ -1831,7 +2085,7 @@
}
.scroll-bar > .increment-button > .increment-arrow, .scroll-bar > .decrement-button > .decrement-arrow {
-fx-background-color: rgb(150.0, 150.0, 150.0);
-fx-background-color: -monet-on-surface-variant;
}
.scroll-bar > .increment-button > .increment-arrow {
@@ -0,0 +1,96 @@
Copyright 2014-2025 Adobe (http://www.adobe.com/), with Reserved Font
Name 'Source'. Source is a trademark of Adobe in the United States
and/or other countries.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 523 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 689 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 627 B

@@ -782,7 +782,6 @@ instance.empty.launch=No available instances.\nInstall the bundled server modpac
instance.empty.launch.goto_download=Install Modpack
instance.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to install the server modpack.
instance.game.all=All
instance.game.april_fools=April Fools
instance.game.old=Historical
instance.game.release=Release
instance.game.releases=Releases
@@ -911,8 +910,6 @@ launcher.agreement=ToS and EULA
launcher.agreement.accept=Accept
launcher.agreement.decline=Decline
launcher.agreement.hint=You must agree to the EULA to use this software.
launcher.april_fools.switch_lzh=HMCL Launcher now supports Classical Chinese, do you want to switch to Classical Chinese mode?\nYou can switch back to English in "置設 → 貫用 → 文".
launcher.april_fools.switch_lzh.confirm=Confirm switching to Classical Chinese mode?\nClick "OK" to exit HMCL and restart it in Classical Chinese mode; click "Cancel" to keep the current language and enter HMCL homepage.
launcher.background=Background Image
launcher.background.choose=Choose background image
launcher.background.builtin=Built-in
@@ -939,9 +936,9 @@ launcher.cache_directory.disabled=Disabled
launcher.cache_directory.invalid=Failed to create a cache directory, falling back to default.
launcher.crash=HMCL has encountered a fatal error! Please copy the following log and ask for help on GitHub or other Minecraft forums.
launcher.crash.java_internal_error=HMCL has encountered a fatal error because your Java is corrupted. Please uninstall your Java and download a suitable Java <a href="https://bell-sw.com/pages/downloads/#downloads">here</a>.
launcher.title=Hello Minecraft! Launcher v%s
launcher.title.modified=Hello Minecraft! Launcher v%s (Modified)
launcher.title.unofficial=Hello Minecraft! Launcher v%s (Unofficial Build)
launcher.title=TDLM v%s
launcher.title.modified=TDLM v%s (Modified)
launcher.title.unofficial=TDLM v%s (Unofficial Build)
launcher.update_java=Please update your Java version.
libraries.download=Downloading Libraries
@@ -1538,7 +1535,6 @@ settings.launcher.brightness.auto=Follow System Settings
settings.launcher.brightness.dark=Dark Mode
settings.launcher.brightness.light=Light Mode
settings.launcher.debug=Debug
settings.launcher.disable_april_fools=Do not enable April Fools features
settings.launcher.disable_auto_game_options=Do not switch game language
settings.launcher.download=Download
settings.launcher.download.threads=Download Threads
@@ -1643,8 +1639,6 @@ system.architecture=Architecture
system.operating_system=Operating System
task.background=Run in Background
task.list=Tasks
task.list.empty=No tasks
task.queued=Waiting for other tasks to finish
task.show=Show Progress
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -602,7 +602,6 @@ instance.empty.add=裝伺服器整合包
# instance.empty.launch.goto_download=
instance.empty.hint=無畢裝者。\n君且更他戲夾,抑擊斯以裝伺服器整合包。
instance.game.all=全覽
instance.game.april_fools=愚人節
instance.game.old=古典版
instance.game.release=當版
instance.game.releases=當版
@@ -731,8 +730,6 @@ launcher.agreement=客契與免責告示
launcher.agreement.accept=許
launcher.agreement.decline=拒
launcher.agreement.hint=將用而須許其客契與免責告示。
# launcher.april_fools.switch_lzh=
# launcher.april_fools.switch_lzh.confirm=
launcher.background=繪圖
launcher.background.choose=擇繪圖
# launcher.background.builtin=
@@ -759,9 +756,9 @@ launcher.cache_directory.disabled=禁 (恒用戲案夾之徑)
launcher.cache_directory.invalid=自訂之快取夾不能立。既復初。
launcher.crash=HMCL 有謬而弗能正。宜鈔下文而報謬于右下之鈕。
launcher.crash.java_internal_error=HMCL 不能行,以爪哇壞也。宜去是爪哇,而擊<a href="https://bell-sw.com/pages/downloads/#downloads">此</a>以置爪哇之適者。
launcher.title=Hello Minecraft! Launcher v%s
launcher.title.modified=Hello Minecraft! Launcher %s (已改)
launcher.title.unofficial=Hello Minecraft! Launcher %s (非官版)
launcher.title=TDLM v%s
launcher.title.modified=TDLM %s (已改)
launcher.title.unofficial=TDLM %s (非官版)
launcher.update_java=宜迭更爪哇。\n君可求助於 https://docs.hmcl.net/help.html。
libraries.download=引之所依
@@ -1351,7 +1348,6 @@ settings.launcher.appearance=外觀
# settings.launcher.brightness.dark=
# settings.launcher.brightness.light=
settings.launcher.debug=勘誤
# settings.launcher.disable_april_fools=
settings.launcher.disable_auto_game_options=弗自改戲文
settings.launcher.download=引
settings.launcher.download.threads=綫程數
@@ -1455,8 +1451,6 @@ system.architecture=大構
system.operating_system=械網
# task.background=
# task.list=
# task.list.empty=
# task.queued=
# task.show=
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -605,7 +605,6 @@ instance.empty.launch=没有可启动的游戏。\n请安装自带的服务器
instance.empty.launch.goto_download=安装整合包
instance.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处安装服务器整合包。
instance.game.all=全部
instance.game.april_fools=愚人节
instance.game.old=远古版
instance.game.release=正式版
instance.game.releases=正式版
@@ -734,8 +733,6 @@ launcher.agreement=用户协议与免责声明
launcher.agreement.accept=同意
launcher.agreement.decline=拒绝
launcher.agreement.hint=同意本软件的用户协议与免责声明以使用本软件。
launcher.april_fools.switch_lzh=HMCL 启动器现已支持文言文,我们诚邀你参与体验。是否要将界面语言切换至中文 (文言)?\n在切换语言后,你可以在“置設 → 貫用 → 文”中将语言切换回中文 (简体)。
launcher.april_fools.switch_lzh.confirm=确认切换语言?\n点击“确定”后 HMCL 将自动重启并将界面语言切换至中文 (文言);点击“取消”将继续使用中文 (简体) 进入 HMCL 主页。
launcher.background=背景图片
launcher.background.choose=选择背景图片
launcher.background.builtin=内置
@@ -762,9 +759,9 @@ launcher.cache_directory.disabled=禁用 (总是使用游戏文件夹路径)
launcher.cache_directory.invalid=无法创建自定义的缓存文件夹。已经恢复到默认设置。
launcher.crash=HMCL 遇到了无法处理的错误。请复制下列内容并点击右下角的按钮反馈问题。
launcher.crash.java_internal_error=HMCL 由于当前 Java 损坏而无法继续运行。请卸载当前 Java,点击 <a href="https://bell-sw.com/pages/downloads/#downloads">此处</a> 安装合适的 Java 版本。
launcher.title=Hello Minecraft! Launcher %s
launcher.title.modified=Hello Minecraft! Launcher %s (已修改)
launcher.title.unofficial=Hello Minecraft! Launcher %s (非官方构建)
launcher.title=TDLM %s
launcher.title.modified=TDLM %s (已修改)
launcher.title.unofficial=TDLM %s (非官方构建)
launcher.update_java=请更新你的 Java。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。
libraries.download=下载依赖库
@@ -1355,7 +1352,6 @@ settings.launcher.brightness.auto=跟随系统设置
settings.launcher.brightness.dark=深色模式
settings.launcher.brightness.light=浅色模式
settings.launcher.debug=调试
settings.launcher.disable_april_fools=不启用愚人节功能
settings.launcher.disable_auto_game_options=不自动切换游戏语言
settings.launcher.download=下载
settings.launcher.download.threads=下载线程数
@@ -1460,8 +1456,6 @@ system.architecture=架构
system.operating_system=操作系统
task.background=在后台运行
task.list=任务列表
task.list.empty=当前没有任务。
task.queued=等待其他任务完成
task.show=显示进度
@@ -599,7 +599,6 @@ instance.empty.launch=沒有可啟動的遊戲。\n請安裝內建的伺服器
instance.empty.launch.goto_download=安裝整合包
instance.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處安裝伺服器整合包。
instance.game.all=全部
instance.game.april_fools=愚人節
instance.game.old=遠古版
instance.game.release=正式版
instance.game.releases=正式版
@@ -728,8 +727,6 @@ launcher.agreement=使用者協議與免責宣告
launcher.agreement.accept=同意
launcher.agreement.decline=拒絕
launcher.agreement.hint=同意本軟體的使用者協議與免責宣告以使用本軟體。
launcher.april_fools.switch_lzh=HMCL 啟動器現已支援文言文,我們誠邀你參與體驗。是否要將介面語言切換至中文 (文言)?\n在切換語言後,你可以在「置設 → 貫用 → 文」中將語言切換回中文 (繁體)。
launcher.april_fools.switch_lzh.confirm=確認切換語言?\n點擊「確定」後 HMCL 將自動重新啟動並將介面語言切換至中文 (文言);點擊「取消」將繼續使用中文 (繁體) 進入 HMCL 主頁。
launcher.background=背景圖片
launcher.background.choose=選取背景圖片
launcher.background.builtin=內建
@@ -756,9 +753,9 @@ launcher.cache_directory.disabled=停用
launcher.cache_directory.invalid=無法建立自訂的快取目錄。已還原至預設設定。
launcher.crash=HMCL 遇到了無法處理的錯誤。請複製下列內容並透過 GitHub 或其他 Minecraft 論壇回報問題。
launcher.crash.java_internal_error=HMCL 由於目前 Java 損壞而無法繼續執行。請移除目前 Java,點擊 <a href="https://bell-sw.com/pages/downloads/#downloads">此處</a> 安裝合適的 Java 版本。
launcher.title=Hello Minecraft! Launcher %s
launcher.title.modified=Hello Minecraft! Launcher %s (已修改)
launcher.title.unofficial=Hello Minecraft! Launcher %s (非官方建置)
launcher.title=TDLM %s
launcher.title.modified=TDLM %s (已修改)
launcher.title.unofficial=TDLM %s (非官方建置)
launcher.update_java=請更新你的 Java
libraries.download=下載依賴庫
@@ -1349,7 +1346,6 @@ settings.launcher.brightness.auto=跟隨系統設定
settings.launcher.brightness.dark=深色模式
settings.launcher.brightness.light=淺色模式
settings.launcher.debug=除錯
settings.launcher.disable_april_fools=不啟用愚人節功能
settings.launcher.disable_auto_game_options=不自動切換遊戲語言
settings.launcher.download=下載
settings.launcher.download.threads=下載執行緒數
@@ -1454,8 +1450,6 @@ system.architecture=架構
system.operating_system=作業系統
task.background=在背景執行
task.list=任務列表
task.list.empty=目前沒有任務。
task.queued=等待其他任務完成
task.show=顯示進度
@@ -10,7 +10,6 @@ import com.google.gson.JsonParser;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.GameInstanceID;
import org.jackhuang.hmcl.game.GameInstanceManifest;
import org.jackhuang.hmcl.game.GameRepositoryDraft;
@@ -15,6 +15,8 @@ public final class ThemeColorTest {
@Test
public void testOf() {
assertEquals(new ThemeColor("#AABBCC", Color.web("#AABBCC")), ThemeColor.of("#AABBCC"));
assertEquals(new ThemeColor("anthropic_orange", Color.web("#D97757")), ThemeColor.of("anthropic_orange"));
assertEquals(new ThemeColor("stone", Color.web("#3D453C")), ThemeColor.of("stone"));
assertEquals(new ThemeColor("blue", Color.web("#5C6BC0")), ThemeColor.of("blue"));
assertEquals(new ThemeColor("darker_blue", Color.web("#283593")), ThemeColor.of("darker_blue"));
assertEquals(new ThemeColor("green", Color.web("#43A047")), ThemeColor.of("green"));
@@ -2,14 +2,12 @@
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXSpinner;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import org.jackhuang.hmcl.setting.LauncherSettings;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.task.Task;
@@ -17,8 +15,6 @@ import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.task.TaskListener;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.DialogUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.TaskManagerPage;
import org.jackhuang.hmcl.ui.decorator.Decorator;
import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.TaskManager;
@@ -34,7 +30,6 @@ import org.junit.jupiter.params.provider.ValueSource;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
@@ -73,7 +68,7 @@ final class TaskExecutorDialogPaneTest {
field.set(null, previousSettings);
}
/// Verifies dialog reuse and the task button's idle, running, and pending presentations.
/// Verifies dialog reuse and TaskManager status transitions without a task-list chrome button.
@Test
void restoresThroughMainWindow() throws Exception {
onFxThread(() -> {
@@ -86,25 +81,17 @@ final class TaskExecutorDialogPaneTest {
TaskExecutorDialogPane pane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
TaskExecutorDialogPane second = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
try {
ButtonBase tasksButton = decorator.getRoot().lookupAll(".jfx-decorator-button").stream()
.filter(node -> i18n("task.list").equals(node.getAccessibleText()))
.map(ButtonBase.class::cast).findFirst().orElseThrow();
Node icon = Objects.requireNonNull(tasksButton.getGraphic());
ControlledExecutor executor = new ControlledExecutor();
pane.setExecutor(executor, false);
executor.start();
Controllers.dialog(pane);
assertEquals(TaskManager.Status.IDLE, TaskManager.getInstance().statusProperty().get());
assertSame(icon, tasksButton.getGraphic());
Node progress = pane.getCenter();
pane.runInBackground();
assertEquals(TaskManager.Status.RUNNING, TaskManager.getInstance().statusProperty().get());
Node spinner = Objects.requireNonNull(tasksButton.getGraphic());
assertInstanceOf(JFXSpinner.class, spinner);
pane.restore();
pane.restore();
assertEquals(TaskManager.Status.IDLE, TaskManager.getInstance().statusProperty().get());
assertSame(icon, tasksButton.getGraphic());
assertTrue(DialogUtils.isShowing(pane));
assertSame(progress, pane.getCenter());
@Nullable JFXDialogPane stack = (JFXDialogPane) pane.getProperties()
@@ -119,38 +106,26 @@ final class TaskExecutorDialogPaneTest {
secondExecutor.start();
Controllers.dialog(second);
second.runInBackground();
Node nextSpinner = Objects.requireNonNull(tasksButton.getGraphic());
assertInstanceOf(JFXSpinner.class, nextSpinner);
assertNotSame(spinner, nextSpinner);
pane.runInBackground();
pane.restore();
assertEquals(TaskManager.Status.RUNNING, TaskManager.getInstance().statusProperty().get());
assertSame(nextSpinner, tasksButton.getGraphic());
AtomicInteger actions = new AtomicInteger();
pane.runInBackground();
assertFalse(pane.runOrDeferCompletionAction("Install", "Ready to install", actions::incrementAndGet));
assertEquals(TaskManager.Status.RUNNING, TaskManager.getInstance().statusProperty().get());
assertSame(nextSpinner, tasksButton.getGraphic());
second.restore();
assertEquals(TaskManager.Status.PENDING, TaskManager.getInstance().statusProperty().get());
SVGPath pendingIcon = assertInstanceOf(SVGPath.class, tasksButton.getGraphic());
assertEquals(SVG.PENDING.getPath(), pendingIcon.getContent());
second.runInBackground();
assertEquals(TaskManager.Status.RUNNING, TaskManager.getInstance().statusProperty().get());
Node resumedSpinner = assertInstanceOf(JFXSpinner.class, tasksButton.getGraphic());
assertNotSame(nextSpinner, resumedSpinner);
pane.runPendingAction();
assertEquals(1, actions.get());
assertEquals(TaskManager.Status.RUNNING, TaskManager.getInstance().statusProperty().get());
assertSame(resumedSpinner, tasksButton.getGraphic());
assertFalse(second.runOrDeferCompletionAction("Install", "Ready to install", actions::incrementAndGet));
assertEquals(TaskManager.Status.PENDING, TaskManager.getInstance().statusProperty().get());
assertEquals(SVG.PENDING.getPath(), assertInstanceOf(SVGPath.class, tasksButton.getGraphic()).getContent());
second.setExecutor(null);
assertEquals(TaskManager.Status.IDLE, TaskManager.getInstance().statusProperty().get());
assertSame(icon, tasksButton.getGraphic());
} finally {
DialogUtils.close(pane);
pane.setExecutor(null);
@@ -351,15 +326,10 @@ final class TaskExecutorDialogPaneTest {
});
}
/// Verifies a visible task list switches from progress to a retained, single-use install action.
/// Verifies a background task retains a single-use install action until runPendingAction.
@Test
void backgroundCompletionWaitsForInstallAction() throws Exception {
AtomicInteger installations = new AtomicInteger();
TaskManagerPage page = onFxThread(() -> {
TaskManagerPage result = new TaskManagerPage();
new Scene(result, 600, 400);
return result;
});
TaskExecutorDialogPane pane = onFxThread(() -> {
TaskExecutorDialogPane dialog = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL);
dialog.setTitle("Example modpack");
@@ -370,9 +340,6 @@ final class TaskExecutorDialogPaneTest {
DialogUtils.show(container, dialog);
executor.start();
dialog.runInBackground();
page.applyCss();
page.layout();
assertFalse(page.lookupAll(".jfx-spinner").isEmpty());
assertFalse(dialog.runOrDeferCompletionAction(i18n("button.install"), i18n("modpack.install.ready"),
installations::incrementAndGet));
executor.stop(true);
@@ -384,17 +351,7 @@ final class TaskExecutorDialogPaneTest {
assertFalse(DialogUtils.isShowing(pane));
assertTrue(TaskManager.getInstance().getTasks().contains(pane));
assertEquals(TaskManager.Status.PENDING, TaskManager.getInstance().statusProperty().get());
page.applyCss();
page.layout();
ButtonBase install = page.lookupAll(".jfx-button").stream()
.filter(ButtonBase.class::isInstance)
.map(ButtonBase.class::cast)
.filter(button -> i18n("button.install").equals(button.getText()))
.findFirst().orElseThrow();
assertTrue(page.lookupAll(".label").stream().anyMatch(node -> node instanceof Label label
&& i18n("modpack.install.ready").equals(label.getText())));
assertTrue(page.lookupAll(".jfx-spinner").isEmpty());
install.fire();
assertNotNull(pane.pendingActionProperty().get());
pane.runPendingAction();
assertEquals(1, installations.get());
assertNull(pane.pendingActionProperty().get());
@@ -18,9 +18,6 @@
"language": {
"type": "string"
},
"disableAprilFools": {
"type": "boolean"
},
"commonDirectoryType": {
"enum": [
"DEFAULT",