From a2afdf34a531c9b9d0222edee5fdeda377a75318 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Mar 2026 21:22:57 +0800 Subject: [PATCH 1/6] update --- .../com/jfoenix/controls/JFXProgressBar.java | 20 ------------------- .../com/jfoenix/skins/JFXProgressBarSkin.java | 17 +--------------- HMCL/src/main/resources/assets/css/root.css | 7 +------ 3 files changed, 2 insertions(+), 42 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java b/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java index 7e6a6ae46b..38ff0a2607 100644 --- a/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java @@ -20,8 +20,6 @@ package com.jfoenix.controls; import com.jfoenix.skins.JFXProgressBarSkin; -import javafx.beans.property.DoubleProperty; -import javafx.beans.property.SimpleDoubleProperty; import javafx.scene.control.ProgressBar; import javafx.scene.control.Skin; @@ -55,22 +53,4 @@ private void initialize() { setPrefWidth(200); getStyleClass().add(DEFAULT_STYLE_CLASS); } - - - private DoubleProperty secondaryProgress; - - public DoubleProperty secondaryProgressProperty() { - if (secondaryProgress == null) { - secondaryProgress = new SimpleDoubleProperty(this, "secondaryProgress", INDETERMINATE_PROGRESS); - } - return secondaryProgress; - } - - public double getSecondaryProgress() { - return secondaryProgress == null ? INDETERMINATE_PROGRESS : secondaryProgress.get(); - } - - public void setSecondaryProgress(double secondaryProgress) { - secondaryProgressProperty().set(secondaryProgress); - } } diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java index bc844f62a7..b437871ec3 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java @@ -39,7 +39,6 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin { private StackPane track; - private StackPane secondaryBar; private StackPane bar; private double barWidth = 0; private double secondaryBarWidth = 0; @@ -54,11 +53,9 @@ public JFXProgressBarSkin(JFXProgressBar bar) { bar.widthProperty().addListener(observable -> { updateProgress(); - updateSecondaryProgress(); }); registerChangeListener(bar.progressProperty(), (obs) -> updateProgress()); - registerChangeListener(bar.secondaryProgressProperty(), obs -> updateSecondaryProgress()); registerChangeListener(bar.visibleProperty(), obs -> updateAnimation()); registerChangeListener(bar.parentProperty(), obs -> updateAnimation()); registerChangeListener(bar.sceneProperty(), obs -> updateAnimation()); @@ -75,21 +72,17 @@ public JFXProgressBarSkin(JFXProgressBar bar) { } protected void initialize() { - track = new StackPane(); track.getStyleClass().setAll("track"); bar = new StackPane(); bar.getStyleClass().setAll("bar"); - secondaryBar = new StackPane(); - secondaryBar.getStyleClass().setAll("secondary-bar"); - clip = new Region(); clip.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY))); bar.backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(bar.getBackground(), clip)); - getChildren().setAll(track, secondaryBar, bar); + getChildren().setAll(track, bar); } @Override @@ -120,7 +113,6 @@ protected double computeMaxHeight(double width, double topInset, double rightIns @Override protected void layoutChildren(double x, double y, double w, double h) { track.resizeRelocate(x, y, w, h); - secondaryBar.resizeRelocate(x, y, secondaryBarWidth, h); bar.resizeRelocate(x, y, getSkinnable().isIndeterminate() ? w : barWidth, h); clip.resizeRelocate(0, 0, w, h); @@ -138,13 +130,6 @@ protected void layoutChildren(double x, double y, double w, double h) { } } - protected void updateSecondaryProgress() { - final JFXProgressBar control = (JFXProgressBar) getSkinnable(); - secondaryBarWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2 - * Math.min(1, Math.max(0, control.getSecondaryProgress()))) / 2.0F; - control.requestLayout(); - } - boolean wasIndeterminate = false; protected void pauseTimeline(boolean pause) { diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 799882b6d8..85a511a4e6 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -923,14 +923,9 @@ -fx-padding: 1.5; } -.jfx-progress-bar > .secondary-bar, -.jfx-progress-bar:indeterminate > .secondary-bar { - -fx-background-color: -monet-secondary; -} - .jfx-progress-bar > .track, .jfx-progress-bar > .bar { - -fx-background-radius: 0; + -fx-background-radius: 3; -fx-background-insets: 0; } From 88eb2e151a1f76d0316b90e34700447f280ad72d Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Mar 2026 23:19:19 +0800 Subject: [PATCH 2/6] update --- .../com/jfoenix/skins/JFXProgressBarSkin.java | 246 +++++++++++------- HMCL/src/main/resources/assets/css/root.css | 16 +- .../jfoenix/skins/JFXProgressBarSkinTest.java | 149 +++++++++++ 3 files changed, 316 insertions(+), 95 deletions(-) create mode 100644 HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java index b437871ec3..f04c5ace6f 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java @@ -20,42 +20,55 @@ package com.jfoenix.skins; import com.jfoenix.controls.JFXProgressBar; -import com.jfoenix.utils.JFXNodeUtils; import com.jfoenix.utils.TreeShowingProperty; -import javafx.animation.*; -import javafx.geometry.Insets; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; import javafx.scene.Node; -import javafx.scene.control.ProgressIndicator; import javafx.scene.control.skin.ProgressIndicatorSkin; -import javafx.scene.layout.*; -import javafx.scene.paint.Color; +import javafx.scene.layout.Region; import javafx.util.Duration; -/// # Material Design ProgressBar Skin +/// # Material Design 3 ProgressBar Skin /// /// @author Shadi Shaheen /// @version 2.0 /// @since 2017-10-06 public class JFXProgressBarSkin extends ProgressIndicatorSkin { - private StackPane track; - private StackPane bar; - private double barWidth = 0; - private double secondaryBarWidth = 0; - private Timeline indeterminateTransition; - private Region clip; + private static final double INDICATOR_HEIGHT = 4; + private static final double DETERMINATE_MIN_ACTIVE_WIDTH = 4; + private static final double TRACK_GAP = 4; + private static final double STOP_INDICATOR_SIZE = 4; + + private static final double INDETERMINATE_INITIAL_OFFSET_FACTOR = 0.0; + private static final double INDETERMINATE_INITIAL_WIDTH_FACTOR = 0.22; + + private final Region leadingTrack = new Region(); + private final Region trailingTrack = new Region(); + private final Region activeIndicator = new Region(); + private final Region stopIndicator = new Region(); + private final DoubleProperty indeterminateOffsetFactor = new SimpleDoubleProperty(INDETERMINATE_INITIAL_OFFSET_FACTOR); + private final DoubleProperty indeterminateWidthFactor = new SimpleDoubleProperty(INDETERMINATE_INITIAL_WIDTH_FACTOR); private final TreeShowingProperty treeShowingProperty; + private Timeline indeterminateTransition; + public JFXProgressBarSkin(JFXProgressBar bar) { super(bar); this.treeShowingProperty = new TreeShowingProperty(bar); - bar.widthProperty().addListener(observable -> { - updateProgress(); - }); + initializeNodes(); + + indeterminateOffsetFactor.addListener(observable -> getSkinnable().requestLayout()); + indeterminateWidthFactor.addListener(observable -> getSkinnable().requestLayout()); + bar.widthProperty().addListener(observable -> updateProgress()); - registerChangeListener(bar.progressProperty(), (obs) -> updateProgress()); + registerChangeListener(bar.progressProperty(), obs -> updateProgress()); registerChangeListener(bar.visibleProperty(), obs -> updateAnimation()); registerChangeListener(bar.parentProperty(), obs -> updateAnimation()); registerChangeListener(bar.sceneProperty(), obs -> updateAnimation()); @@ -63,26 +76,29 @@ public JFXProgressBarSkin(JFXProgressBar bar) { unregisterChangeListeners(treeShowingProperty); unregisterChangeListeners(bar.indeterminateProperty()); - registerChangeListener(treeShowingProperty, obs -> this.updateAnimation()); + registerChangeListener(treeShowingProperty, obs -> updateAnimation()); registerChangeListener(bar.indeterminateProperty(), obs -> initialize()); initialize(); - - getSkinnable().requestLayout(); } - protected void initialize() { - track = new StackPane(); - track.getStyleClass().setAll("track"); - - bar = new StackPane(); - bar.getStyleClass().setAll("bar"); + private void initializeNodes() { + configureRegion(leadingTrack, "track"); + configureRegion(trailingTrack, "track"); + configureRegion(activeIndicator, "active-indicator"); + configureRegion(stopIndicator, "stop-indicator"); + getChildren().setAll(leadingTrack, trailingTrack, activeIndicator, stopIndicator); + } - clip = new Region(); - clip.setBackground(new Background(new BackgroundFill(Color.BLACK, CornerRadii.EMPTY, Insets.EMPTY))); - bar.backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(bar.getBackground(), clip)); + private void configureRegion(Region region, String styleClass) { + region.getStyleClass().setAll(styleClass); + region.setManaged(false); + } - getChildren().setAll(track, bar); + protected void initialize() { + resetIndeterminateGeometry(); + updateAnimation(); + updateProgress(); } @Override @@ -92,12 +108,13 @@ public double computeBaselineOffset(double topInset, double rightInset, double b @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { - return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset); + double prefWidth = getSkinnable().getPrefWidth(); + return leftInset + (prefWidth > 0 ? prefWidth : 100) + rightInset; } @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return topInset + bar.prefHeight(width) + bottomInset; + return topInset + INDICATOR_HEIGHT + bottomInset; } @Override @@ -107,103 +124,152 @@ protected double computeMaxWidth(double height, double topInset, double rightIns @Override protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { - return getSkinnable().prefHeight(width); + return topInset + INDICATOR_HEIGHT + bottomInset; } @Override protected void layoutChildren(double x, double y, double w, double h) { - track.resizeRelocate(x, y, w, h); - bar.resizeRelocate(x, y, getSkinnable().isIndeterminate() ? w : barWidth, h); - clip.resizeRelocate(0, 0, w, h); + double width = Math.max(0, w); + double height = Math.min(INDICATOR_HEIGHT, Math.max(0, h)); + double barY = y + Math.max(0, (h - height) / 2); if (getSkinnable().isIndeterminate()) { - createIndeterminateTimeline(); - if (JFXNodeUtils.isTreeShowing(getSkinnable())) { - indeterminateTransition.play(); - } - // apply clip - bar.setClip(clip); - } else if (indeterminateTransition != null) { - clearAnimation(); - // remove clip - bar.setClip(null); + layoutIndeterminate(x, barY, width, height); + } else { + layoutDeterminate(x, barY, width, height); } } - boolean wasIndeterminate = false; + private void layoutDeterminate(double x, double y, double width, double height) { + double progress = clamp(getSkinnable().getProgress(), 0, 1); + double activeWidth = progress >= 1 + ? width + : Math.min(width, Math.max(DETERMINATE_MIN_ACTIVE_WIDTH, progress * width)); + double trackX = x + activeWidth + TRACK_GAP; + double trackWidth = Math.max(0, width - activeWidth - TRACK_GAP); - protected void pauseTimeline(boolean pause) { - if (getSkinnable().isIndeterminate()) { - if (indeterminateTransition == null) { - createIndeterminateTimeline(); - } - if (pause) { - indeterminateTransition.pause(); - } else { - indeterminateTransition.play(); - } + layoutRegion(leadingTrack, 0, 0, 0, 0, false); + layoutRegion(activeIndicator, x, y, activeWidth, height, activeWidth > 0); + + boolean showTrack = progress < 1 && trackWidth > 0; + layoutRegion(trailingTrack, trackX, y, trackWidth, height, showTrack); + + double stopSize = Math.min(STOP_INDICATOR_SIZE, Math.min(trackWidth, height)); + double stopX = x + width - stopSize; + layoutRegion(stopIndicator, stopX, y, stopSize, stopSize, showTrack && stopSize > 0); + } + + private void layoutIndeterminate(double x, double y, double width, double height) { + double activeStart = indeterminateOffsetFactor.get() * width; + double activeWidth = Math.max(DETERMINATE_MIN_ACTIVE_WIDTH, indeterminateWidthFactor.get() * width); + double activeEnd = activeStart + activeWidth; + double visibleStart = clamp(activeStart, 0, width); + double visibleEnd = clamp(activeEnd, 0, width); + double visibleWidth = Math.max(0, visibleEnd - visibleStart); + + layoutRegion(stopIndicator, 0, 0, 0, 0, false); + + if (visibleWidth <= 0) { + layoutRegion(activeIndicator, 0, 0, 0, 0, false); + layoutRegion(leadingTrack, x, y, width, height, width > 0); + layoutRegion(trailingTrack, 0, 0, 0, 0, false); + return; + } + + double leftTrackWidth = Math.max(0, visibleStart - TRACK_GAP); + double rightTrackX = x + visibleEnd + TRACK_GAP; + double rightTrackWidth = Math.max(0, width - visibleEnd - TRACK_GAP); + + layoutRegion(leadingTrack, x, y, leftTrackWidth, height, leftTrackWidth > 0); + layoutRegion(activeIndicator, x + visibleStart, y, visibleWidth, height, true); + layoutRegion(trailingTrack, rightTrackX, y, rightTrackWidth, height, rightTrackWidth > 0); + } + + private void layoutRegion(Region region, double x, double y, double width, double height, boolean visible) { + region.setVisible(visible); + if (!visible) { + return; } + region.resizeRelocate(x, y, Math.max(0, width), Math.max(0, height)); } private void updateAnimation() { - final boolean isTreeShowing = treeShowingProperty.get(); - if (indeterminateTransition != null) { - pauseTimeline(!isTreeShowing); - } else if (isTreeShowing) { + if (!getSkinnable().isIndeterminate()) { + clearAnimation(); + return; + } + + if (indeterminateTransition == null) { createIndeterminateTimeline(); } + + if (treeShowingProperty.get()) { + indeterminateTransition.play(); + } else { + indeterminateTransition.pause(); + } } private void updateProgress() { - final ProgressIndicator control = getSkinnable(); - final boolean isIndeterminate = control.isIndeterminate(); - if (!(isIndeterminate && wasIndeterminate)) { - barWidth = ((int) (control.getWidth() - snappedLeftInset() - snappedRightInset()) * 2 - * Math.min(1, Math.max(0, control.getProgress()))) / 2.0F; - control.requestLayout(); - } - wasIndeterminate = isIndeterminate; + getSkinnable().requestLayout(); + } + + private void resetIndeterminateGeometry() { + indeterminateOffsetFactor.set(INDETERMINATE_INITIAL_OFFSET_FACTOR); + indeterminateWidthFactor.set(INDETERMINATE_INITIAL_WIDTH_FACTOR); } private void createIndeterminateTimeline() { - if (indeterminateTransition != null) { - clearAnimation(); - } - double dur = 1; - ProgressIndicator control = getSkinnable(); - final double w = control.getWidth() - (snappedLeftInset() + snappedRightInset()); - indeterminateTransition = new Timeline(new KeyFrame( - Duration.ZERO, - new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_IN), - new KeyValue(clip.translateXProperty(), -w / 2, Interpolator.LINEAR) - ), + clearAnimation(); + resetIndeterminateGeometry(); + + indeterminateTransition = new Timeline( + new KeyFrame( + Duration.ZERO, + new KeyValue(indeterminateOffsetFactor, 0.0, Interpolator.LINEAR), + new KeyValue(indeterminateWidthFactor, 0.22, Interpolator.EASE_BOTH) + ), + new KeyFrame( + Duration.seconds(0.45), + new KeyValue(indeterminateOffsetFactor, 0.18, Interpolator.EASE_BOTH), + new KeyValue(indeterminateWidthFactor, 0.42, Interpolator.EASE_BOTH) + ), new KeyFrame( - Duration.seconds(0.5 * dur), - new KeyValue(clip.scaleXProperty(), 0.4, Interpolator.LINEAR) + Duration.seconds(1.15), + new KeyValue(indeterminateOffsetFactor, 0.58, Interpolator.EASE_BOTH), + new KeyValue(indeterminateWidthFactor, 0.3, Interpolator.EASE_BOTH) ), new KeyFrame( - Duration.seconds(0.9 * dur), - new KeyValue(clip.translateXProperty(), w / 2, Interpolator.LINEAR) + Duration.seconds(1.6), + new KeyValue(indeterminateOffsetFactor, 0.92, Interpolator.EASE_IN), + new KeyValue(indeterminateWidthFactor, 0.18, Interpolator.EASE_OUT) ), new KeyFrame( - Duration.seconds(1 * dur), - new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_OUT) - )); + Duration.seconds(1.8), + new KeyValue(indeterminateOffsetFactor, 1.08, Interpolator.EASE_IN), + new KeyValue(indeterminateWidthFactor, 0.18, Interpolator.EASE_OUT) + ) + ); indeterminateTransition.setCycleCount(Timeline.INDEFINITE); } private void clearAnimation() { + if (indeterminateTransition == null) { + return; + } indeterminateTransition.stop(); indeterminateTransition.getKeyFrames().clear(); indeterminateTransition = null; } + private static double clamp(double value, double min, double max) { + return Math.max(min, Math.min(max, value)); + } + @Override public void dispose() { super.dispose(); treeShowingProperty.dispose(); - if (indeterminateTransition != null) { - clearAnimation(); - } + clearAnimation(); } } diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 85a511a4e6..5297f8bca7 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -913,19 +913,25 @@ * * *******************************************************************************/ +.jfx-progress-bar { + -fx-min-height: 4px; + -fx-pref-height: 4px; + -fx-max-height: 4px; +} + .jfx-progress-bar > .track { -fx-background-color: -monet-secondary-container; } -.jfx-progress-bar > .bar, -.jfx-progress-bar:indeterminate > .bar{ +.jfx-progress-bar > .active-indicator, +.jfx-progress-bar > .stop-indicator { -fx-background-color: -monet-primary; - -fx-padding: 1.5; } .jfx-progress-bar > .track, -.jfx-progress-bar > .bar { - -fx-background-radius: 3; +.jfx-progress-bar > .active-indicator, +.jfx-progress-bar > .stop-indicator { + -fx-background-radius: 999; -fx-background-insets: 0; } diff --git a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java new file mode 100644 index 0000000000..227189f994 --- /dev/null +++ b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java @@ -0,0 +1,149 @@ +package com.jfoenix.skins; + +import com.jfoenix.controls.JFXProgressBar; +import javafx.application.Platform; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import org.jackhuang.hmcl.JavaFXLauncher; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +class JFXProgressBarSkinTest { + private static final double TEST_WIDTH = 100; + private static final double TEST_HEIGHT = 4; + private static final double DELTA = 0.001; + + @BeforeAll + static void startJavaFx() { + JavaFXLauncher.start(); + assumeTrue(JavaFXLauncher.isStarted(), "JavaFX toolkit is unavailable in this environment"); + } + + @Test + void determinateZeroProgressUsesMinimumDotAndTrailingTrack() throws Exception { + runOnFxThreadAndWait(() -> { + LayoutProbe probe = createProbe(0); + + assertEquals(4.0, probe.activeIndicator().getWidth(), DELTA); + assertEquals(0.0, probe.activeIndicator().getLayoutX(), DELTA); + + List visibleTracks = probe.visibleTracks(); + assertEquals(1, visibleTracks.size()); + assertEquals(8.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(92.0, visibleTracks.get(0).getWidth(), DELTA); + + assertTrue(probe.stopIndicator().isVisible()); + assertEquals(96.0, probe.stopIndicator().getLayoutX(), DELTA); + assertEquals(4.0, probe.stopIndicator().getWidth(), DELTA); + }); + } + + @Test + void determinateCompleteHidesTrackAndStopIndicator() throws Exception { + runOnFxThreadAndWait(() -> { + LayoutProbe probe = createProbe(1.0); + + assertEquals(TEST_WIDTH, probe.activeIndicator().getWidth(), DELTA); + assertTrue(probe.visibleTracks().isEmpty()); + assertFalse(probe.stopIndicator().isVisible()); + }); + } + + @Test + void indeterminateStartsWithActiveIndicatorAndNoStopIndicator() throws Exception { + runOnFxThreadAndWait(() -> { + LayoutProbe probe = createProbe(ProgressIndicator.INDETERMINATE_PROGRESS); + + assertEquals(0.0, probe.activeIndicator().getLayoutX(), DELTA); + assertEquals(22.0, probe.activeIndicator().getWidth(), DELTA); + + List visibleTracks = probe.visibleTracks(); + assertEquals(1, visibleTracks.size()); + assertEquals(26.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(74.0, visibleTracks.get(0).getWidth(), DELTA); + + assertFalse(probe.stopIndicator().isVisible()); + }); + } + + private static LayoutProbe createProbe(double progress) { + JFXProgressBar progressBar = new JFXProgressBar(progress); + progressBar.setMinSize(TEST_WIDTH, TEST_HEIGHT); + progressBar.setPrefSize(TEST_WIDTH, TEST_HEIGHT); + progressBar.setMaxSize(TEST_WIDTH, TEST_HEIGHT); + + Pane root = new Pane(progressBar); + new Scene(root, TEST_WIDTH, 16); + + root.applyCss(); + progressBar.applyCss(); + progressBar.resizeRelocate(0, 6, TEST_WIDTH, TEST_HEIGHT); + progressBar.layout(); + + Region activeIndicator = lookupRegion(progressBar, ".active-indicator"); + Region stopIndicator = lookupRegion(progressBar, ".stop-indicator"); + List tracks = progressBar.lookupAll(".track").stream() + .map(Region.class::cast) + .sorted(Comparator.comparingDouble(Node::getLayoutX)) + .toList(); + + return new LayoutProbe(activeIndicator, tracks, stopIndicator); + } + + private static Region lookupRegion(JFXProgressBar progressBar, String selector) { + Node node = progressBar.lookup(selector); + assertNotNull(node, () -> "Missing node for selector " + selector); + return (Region) node; + } + + private static void runOnFxThreadAndWait(CheckedRunnable runnable) throws Exception { + if (Platform.isFxApplicationThread()) { + runnable.run(); + return; + } + + CountDownLatch latch = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + Platform.runLater(() -> { + try { + runnable.run(); + } catch (Throwable throwable) { + failure.set(throwable); + } finally { + latch.countDown(); + } + }); + + assertTrue(latch.await(5, TimeUnit.SECONDS), "Timed out waiting for JavaFX work to finish"); + if (failure.get() != null) { + throw new AssertionError(failure.get()); + } + } + + private record LayoutProbe(Region activeIndicator, List tracks, Region stopIndicator) { + private List visibleTracks() { + return tracks.stream().filter(Node::isVisible).toList(); + } + } + + @FunctionalInterface + private interface CheckedRunnable { + void run() throws Exception; + } +} + From c7d95baad1824a8bae9053a0b0fdb46a5041fb98 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Mar 2026 23:26:18 +0800 Subject: [PATCH 3/6] update --- .../com/jfoenix/skins/JFXProgressBarSkin.java | 12 +++++--- .../jfoenix/skins/JFXProgressBarSkinTest.java | 29 +++++++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java index f04c5ace6f..0f904a2cd9 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java @@ -142,14 +142,18 @@ protected void layoutChildren(double x, double y, double w, double h) { private void layoutDeterminate(double x, double y, double width, double height) { double progress = clamp(getSkinnable().getProgress(), 0, 1); - double activeWidth = progress >= 1 + boolean showActiveIndicator = progress > 0; + double gap = showActiveIndicator ? TRACK_GAP : 0; + double activeWidth = !showActiveIndicator + ? 0 + : progress >= 1 ? width : Math.min(width, Math.max(DETERMINATE_MIN_ACTIVE_WIDTH, progress * width)); - double trackX = x + activeWidth + TRACK_GAP; - double trackWidth = Math.max(0, width - activeWidth - TRACK_GAP); + double trackX = x + activeWidth + gap; + double trackWidth = Math.max(0, width - activeWidth - gap); layoutRegion(leadingTrack, 0, 0, 0, 0, false); - layoutRegion(activeIndicator, x, y, activeWidth, height, activeWidth > 0); + layoutRegion(activeIndicator, x, y, activeWidth, height, showActiveIndicator && activeWidth > 0); boolean showTrack = progress < 1 && trackWidth > 0; layoutRegion(trailingTrack, trackX, y, trackWidth, height, showTrack); diff --git a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java index 227189f994..06a9ecea47 100644 --- a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java +++ b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java @@ -35,17 +35,17 @@ static void startJavaFx() { } @Test - void determinateZeroProgressUsesMinimumDotAndTrailingTrack() throws Exception { + void determinateZeroProgressHidesActiveIndicatorAndUsesFullTrack() throws Exception { runOnFxThreadAndWait(() -> { LayoutProbe probe = createProbe(0); - assertEquals(4.0, probe.activeIndicator().getWidth(), DELTA); - assertEquals(0.0, probe.activeIndicator().getLayoutX(), DELTA); + assertFalse(probe.activeIndicator().isVisible()); + assertEquals(0.0, probe.activeIndicator().getWidth(), DELTA); List visibleTracks = probe.visibleTracks(); assertEquals(1, visibleTracks.size()); - assertEquals(8.0, visibleTracks.get(0).getLayoutX(), DELTA); - assertEquals(92.0, visibleTracks.get(0).getWidth(), DELTA); + assertEquals(0.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(100.0, visibleTracks.get(0).getWidth(), DELTA); assertTrue(probe.stopIndicator().isVisible()); assertEquals(96.0, probe.stopIndicator().getLayoutX(), DELTA); @@ -81,6 +81,24 @@ void indeterminateStartsWithActiveIndicatorAndNoStopIndicator() throws Exception }); } + @Test + void determinatePositiveProgressUsesMinimumVisibleWidth() throws Exception { + runOnFxThreadAndWait(() -> { + LayoutProbe probe = createProbe(0.01); + + assertTrue(probe.activeIndicator().isVisible()); + assertEquals(0.0, probe.activeIndicator().getLayoutX(), DELTA); + assertEquals(4.0, probe.activeIndicator().getWidth(), DELTA); + + List visibleTracks = probe.visibleTracks(); + assertEquals(1, visibleTracks.size()); + assertEquals(8.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(92.0, visibleTracks.get(0).getWidth(), DELTA); + + assertTrue(probe.stopIndicator().isVisible()); + }); + } + private static LayoutProbe createProbe(double progress) { JFXProgressBar progressBar = new JFXProgressBar(progress); progressBar.setMinSize(TEST_WIDTH, TEST_HEIGHT); @@ -146,4 +164,3 @@ private interface CheckedRunnable { void run() throws Exception; } } - From 739af067c3e44cc6e21ca11fa11656fd709b07da Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Mar 2026 23:32:56 +0800 Subject: [PATCH 4/6] update --- .../com/jfoenix/skins/JFXProgressBarSkin.java | 59 +++++++++------- .../jfoenix/skins/JFXProgressBarSkinTest.java | 67 ++++++++++++++++++- 2 files changed, 98 insertions(+), 28 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java index 0f904a2cd9..840db5f52d 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java @@ -44,15 +44,15 @@ public class JFXProgressBarSkin extends ProgressIndicatorSkin { private static final double TRACK_GAP = 4; private static final double STOP_INDICATOR_SIZE = 4; - private static final double INDETERMINATE_INITIAL_OFFSET_FACTOR = 0.0; - private static final double INDETERMINATE_INITIAL_WIDTH_FACTOR = 0.22; + private static final double INDETERMINATE_INITIAL_START_FACTOR = 0.0; + private static final double INDETERMINATE_INITIAL_END_FACTOR = 0.18; private final Region leadingTrack = new Region(); private final Region trailingTrack = new Region(); private final Region activeIndicator = new Region(); private final Region stopIndicator = new Region(); - private final DoubleProperty indeterminateOffsetFactor = new SimpleDoubleProperty(INDETERMINATE_INITIAL_OFFSET_FACTOR); - private final DoubleProperty indeterminateWidthFactor = new SimpleDoubleProperty(INDETERMINATE_INITIAL_WIDTH_FACTOR); + private final DoubleProperty indeterminateSegmentStartFactor = new SimpleDoubleProperty(INDETERMINATE_INITIAL_START_FACTOR); + private final DoubleProperty indeterminateSegmentEndFactor = new SimpleDoubleProperty(INDETERMINATE_INITIAL_END_FACTOR); private final TreeShowingProperty treeShowingProperty; private Timeline indeterminateTransition; @@ -64,8 +64,8 @@ public JFXProgressBarSkin(JFXProgressBar bar) { initializeNodes(); - indeterminateOffsetFactor.addListener(observable -> getSkinnable().requestLayout()); - indeterminateWidthFactor.addListener(observable -> getSkinnable().requestLayout()); + indeterminateSegmentStartFactor.addListener(observable -> getSkinnable().requestLayout()); + indeterminateSegmentEndFactor.addListener(observable -> getSkinnable().requestLayout()); bar.widthProperty().addListener(observable -> updateProgress()); registerChangeListener(bar.progressProperty(), obs -> updateProgress()); @@ -164,9 +164,8 @@ private void layoutDeterminate(double x, double y, double width, double height) } private void layoutIndeterminate(double x, double y, double width, double height) { - double activeStart = indeterminateOffsetFactor.get() * width; - double activeWidth = Math.max(DETERMINATE_MIN_ACTIVE_WIDTH, indeterminateWidthFactor.get() * width); - double activeEnd = activeStart + activeWidth; + double activeStart = indeterminateSegmentStartFactor.get() * width; + double activeEnd = indeterminateSegmentEndFactor.get() * width; double visibleStart = clamp(activeStart, 0, width); double visibleEnd = clamp(activeEnd, 0, width); double visibleWidth = Math.max(0, visibleEnd - visibleStart); @@ -219,8 +218,13 @@ private void updateProgress() { } private void resetIndeterminateGeometry() { - indeterminateOffsetFactor.set(INDETERMINATE_INITIAL_OFFSET_FACTOR); - indeterminateWidthFactor.set(INDETERMINATE_INITIAL_WIDTH_FACTOR); + indeterminateSegmentStartFactor.set(INDETERMINATE_INITIAL_START_FACTOR); + indeterminateSegmentEndFactor.set(INDETERMINATE_INITIAL_END_FACTOR); + } + + void setIndeterminateSegmentForTesting(double startFactor, double endFactor) { + indeterminateSegmentStartFactor.set(startFactor); + indeterminateSegmentEndFactor.set(Math.max(startFactor, endFactor)); } private void createIndeterminateTimeline() { @@ -230,28 +234,33 @@ private void createIndeterminateTimeline() { indeterminateTransition = new Timeline( new KeyFrame( Duration.ZERO, - new KeyValue(indeterminateOffsetFactor, 0.0, Interpolator.LINEAR), - new KeyValue(indeterminateWidthFactor, 0.22, Interpolator.EASE_BOTH) + new KeyValue(indeterminateSegmentStartFactor, 0.0, Interpolator.LINEAR), + new KeyValue(indeterminateSegmentEndFactor, 0.18, Interpolator.EASE_OUT) + ), + new KeyFrame( + Duration.seconds(0.42), + new KeyValue(indeterminateSegmentStartFactor, 0.0, Interpolator.EASE_BOTH), + new KeyValue(indeterminateSegmentEndFactor, 0.46, Interpolator.EASE_BOTH) ), new KeyFrame( - Duration.seconds(0.45), - new KeyValue(indeterminateOffsetFactor, 0.18, Interpolator.EASE_BOTH), - new KeyValue(indeterminateWidthFactor, 0.42, Interpolator.EASE_BOTH) + Duration.seconds(0.95), + new KeyValue(indeterminateSegmentStartFactor, 0.14, Interpolator.EASE_BOTH), + new KeyValue(indeterminateSegmentEndFactor, 0.76, Interpolator.EASE_BOTH) ), new KeyFrame( - Duration.seconds(1.15), - new KeyValue(indeterminateOffsetFactor, 0.58, Interpolator.EASE_BOTH), - new KeyValue(indeterminateWidthFactor, 0.3, Interpolator.EASE_BOTH) + Duration.seconds(1.42), + new KeyValue(indeterminateSegmentStartFactor, 0.44, Interpolator.EASE_BOTH), + new KeyValue(indeterminateSegmentEndFactor, 0.94, Interpolator.EASE_IN) ), new KeyFrame( - Duration.seconds(1.6), - new KeyValue(indeterminateOffsetFactor, 0.92, Interpolator.EASE_IN), - new KeyValue(indeterminateWidthFactor, 0.18, Interpolator.EASE_OUT) + Duration.seconds(1.78), + new KeyValue(indeterminateSegmentStartFactor, 0.82, Interpolator.EASE_IN), + new KeyValue(indeterminateSegmentEndFactor, 1.0, Interpolator.EASE_IN) ), new KeyFrame( - Duration.seconds(1.8), - new KeyValue(indeterminateOffsetFactor, 1.08, Interpolator.EASE_IN), - new KeyValue(indeterminateWidthFactor, 0.18, Interpolator.EASE_OUT) + Duration.seconds(2.0), + new KeyValue(indeterminateSegmentStartFactor, 1.02, Interpolator.EASE_IN), + new KeyValue(indeterminateSegmentEndFactor, 1.02, Interpolator.EASE_IN) ) ); indeterminateTransition.setCycleCount(Timeline.INDEFINITE); diff --git a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java index 06a9ecea47..1947e75ba8 100644 --- a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java +++ b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java @@ -70,12 +70,48 @@ void indeterminateStartsWithActiveIndicatorAndNoStopIndicator() throws Exception LayoutProbe probe = createProbe(ProgressIndicator.INDETERMINATE_PROGRESS); assertEquals(0.0, probe.activeIndicator().getLayoutX(), DELTA); - assertEquals(22.0, probe.activeIndicator().getWidth(), DELTA); + assertEquals(18.0, probe.activeIndicator().getWidth(), DELTA); List visibleTracks = probe.visibleTracks(); assertEquals(1, visibleTracks.size()); - assertEquals(26.0, visibleTracks.get(0).getLayoutX(), DELTA); - assertEquals(74.0, visibleTracks.get(0).getWidth(), DELTA); + assertEquals(22.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(78.0, visibleTracks.get(0).getWidth(), DELTA); + + assertFalse(probe.stopIndicator().isVisible()); + }); + } + + @Test + void indeterminateMidFlightShowsTracksOnBothSidesWithFourPixelGap() throws Exception { + runOnFxThreadAndWait(() -> { + LayoutProbe probe = createIndeterminateProbe(0.24, 0.68); + + assertEquals(24.0, probe.activeIndicator().getLayoutX(), DELTA); + assertEquals(44.0, probe.activeIndicator().getWidth(), DELTA); + + List visibleTracks = probe.visibleTracks(); + assertEquals(2, visibleTracks.size()); + assertEquals(0.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(20.0, visibleTracks.get(0).getWidth(), DELTA); + assertEquals(72.0, visibleTracks.get(1).getLayoutX(), DELTA); + assertEquals(28.0, visibleTracks.get(1).getWidth(), DELTA); + + assertFalse(probe.stopIndicator().isVisible()); + }); + } + + @Test + void indeterminateNearExitHidesTrailingTrack() throws Exception { + runOnFxThreadAndWait(() -> { + LayoutProbe probe = createIndeterminateProbe(0.86, 1.02); + + assertEquals(86.0, probe.activeIndicator().getLayoutX(), DELTA); + assertEquals(14.0, probe.activeIndicator().getWidth(), DELTA); + + List visibleTracks = probe.visibleTracks(); + assertEquals(1, visibleTracks.size()); + assertEquals(0.0, visibleTracks.get(0).getLayoutX(), DELTA); + assertEquals(82.0, visibleTracks.get(0).getWidth(), DELTA); assertFalse(probe.stopIndicator().isVisible()); }); @@ -153,6 +189,31 @@ private static void runOnFxThreadAndWait(CheckedRunnable runnable) throws Except } } + private static LayoutProbe createIndeterminateProbe(double startFactor, double endFactor) { + JFXProgressBar progressBar = new JFXProgressBar(ProgressIndicator.INDETERMINATE_PROGRESS); + progressBar.setMinSize(TEST_WIDTH, TEST_HEIGHT); + progressBar.setPrefSize(TEST_WIDTH, TEST_HEIGHT); + progressBar.setMaxSize(TEST_WIDTH, TEST_HEIGHT); + + Pane root = new Pane(progressBar); + new Scene(root, TEST_WIDTH, 16); + + root.applyCss(); + progressBar.applyCss(); + ((JFXProgressBarSkin) progressBar.getSkin()).setIndeterminateSegmentForTesting(startFactor, endFactor); + progressBar.resizeRelocate(0, 6, TEST_WIDTH, TEST_HEIGHT); + progressBar.layout(); + + Region activeIndicator = lookupRegion(progressBar, ".active-indicator"); + Region stopIndicator = lookupRegion(progressBar, ".stop-indicator"); + List tracks = progressBar.lookupAll(".track").stream() + .map(Region.class::cast) + .sorted(Comparator.comparingDouble(Node::getLayoutX)) + .toList(); + + return new LayoutProbe(activeIndicator, tracks, stopIndicator); + } + private record LayoutProbe(Region activeIndicator, List tracks, Region stopIndicator) { private List visibleTracks() { return tracks.stream().filter(Node::isVisible).toList(); From 945566de737c5e2eb7457f6ee50a7d4833b70591 Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Mar 2026 23:38:41 +0800 Subject: [PATCH 5/6] update --- .../main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java | 1 + 1 file changed, 1 insertion(+) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java index f5f7e52e16..d4982fc1c0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TaskListPane.java @@ -310,6 +310,7 @@ private Cell() { bar.minWidthProperty().bind(barWidth); bar.prefWidthProperty().bind(barWidth); bar.maxWidthProperty().bind(barWidth); + BorderPane.setMargin(bar, new Insets(2, 0, 0, 0)); setGraphic(pane); } From 6f5ea754160a2b6e56ee295b74824801562040bd Mon Sep 17 00:00:00 2001 From: Glavo Date: Sun, 15 Mar 2026 23:48:04 +0800 Subject: [PATCH 6/6] update --- HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java | 3 ++- .../test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java index 840db5f52d..215bfe72d7 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java @@ -156,11 +156,12 @@ private void layoutDeterminate(double x, double y, double width, double height) layoutRegion(activeIndicator, x, y, activeWidth, height, showActiveIndicator && activeWidth > 0); boolean showTrack = progress < 1 && trackWidth > 0; + boolean showStopIndicator = progress > 0 && showTrack; layoutRegion(trailingTrack, trackX, y, trackWidth, height, showTrack); double stopSize = Math.min(STOP_INDICATOR_SIZE, Math.min(trackWidth, height)); double stopX = x + width - stopSize; - layoutRegion(stopIndicator, stopX, y, stopSize, stopSize, showTrack && stopSize > 0); + layoutRegion(stopIndicator, stopX, y, stopSize, stopSize, showStopIndicator && stopSize > 0); } private void layoutIndeterminate(double x, double y, double width, double height) { diff --git a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java index 1947e75ba8..7d74355eab 100644 --- a/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java +++ b/HMCL/src/test/java/com/jfoenix/skins/JFXProgressBarSkinTest.java @@ -47,9 +47,7 @@ void determinateZeroProgressHidesActiveIndicatorAndUsesFullTrack() throws Except assertEquals(0.0, visibleTracks.get(0).getLayoutX(), DELTA); assertEquals(100.0, visibleTracks.get(0).getWidth(), DELTA); - assertTrue(probe.stopIndicator().isVisible()); - assertEquals(96.0, probe.stopIndicator().getLayoutX(), DELTA); - assertEquals(4.0, probe.stopIndicator().getWidth(), DELTA); + assertFalse(probe.stopIndicator().isVisible()); }); }