From 010abf037e768c9c0fc60d484e694c91180e6249 Mon Sep 17 00:00:00 2001 From: Glavo Date: Thu, 19 Feb 2026 05:08:33 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20JFXProgressBar=20=E5=92=8C?= =?UTF-8?q?=20JFXSpinner=20(#5565)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- HMCL/build.gradle.kts | 1 + .../com/jfoenix/controls/JFXProgressBar.java | 76 ++++ .../java/com/jfoenix/controls/JFXSpinner.java | 168 +++++++++ .../com/jfoenix/skins/JFXProgressBarSkin.java | 224 ++++++++++++ .../com/jfoenix/skins/JFXSpinnerSkin.java | 330 ++++++++++++++++++ .../java/com/jfoenix/utils/JFXNodeUtils.java | 76 ++++ .../jfoenix/utils/TreeShowingProperty.java | 143 ++++++++ HMCL/src/main/resources/assets/css/root.css | 51 +-- 8 files changed, 1045 insertions(+), 24 deletions(-) create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXSpinner.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java create mode 100644 HMCL/src/main/java/com/jfoenix/skins/JFXSpinnerSkin.java create mode 100644 HMCL/src/main/java/com/jfoenix/utils/TreeShowingProperty.java diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts index 96275f51b..3f0750f99 100644 --- a/HMCL/build.gradle.kts +++ b/HMCL/build.gradle.kts @@ -130,6 +130,7 @@ val addOpens = listOf( "javafx.base/javafx.beans.property", "javafx.graphics/javafx.css", "javafx.graphics/javafx.stage", + "javafx.graphics/javafx.scene", "javafx.graphics/com.sun.glass.ui", "javafx.graphics/com.sun.javafx.stage", "javafx.graphics/com.sun.javafx.util", diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java b/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java new file mode 100644 index 000000000..7e6a6ae46 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXProgressBar.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +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; + +/// JFXProgressBar is the material design implementation of a progress bar. +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public class JFXProgressBar extends ProgressBar { + /// Initialize the style class to 'jfx-progress-bar'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-progress-bar"; + + public JFXProgressBar() { + initialize(); + } + + public JFXProgressBar(double progress) { + super(progress); + initialize(); + } + + @Override + protected Skin createDefaultSkin() { + return new JFXProgressBarSkin(this); + } + + 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/controls/JFXSpinner.java b/HMCL/src/main/java/com/jfoenix/controls/JFXSpinner.java new file mode 100644 index 000000000..f48d88431 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXSpinner.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.skins.JFXSpinnerSkin; +import javafx.css.CssMetaData; +import javafx.css.SimpleStyleableDoubleProperty; +import javafx.css.Styleable; +import javafx.css.StyleableDoubleProperty; +import javafx.css.converter.SizeConverter; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.Skin; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// JFXSpinner is the material design implementation of a loading spinner. +/// +/// @author Bashir Elias & Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public class JFXSpinner extends ProgressIndicator { + + public static final double INDETERMINATE_PROGRESS = ProgressIndicator.INDETERMINATE_PROGRESS; + + public JFXSpinner() { + this(INDETERMINATE_PROGRESS); + } + + public JFXSpinner(double progress) { + super(progress); + init(); + } + + private void init() { + getStyleClass().add(DEFAULT_STYLE_CLASS); + } + + @Override + protected Skin createDefaultSkin() { + return new JFXSpinnerSkin(this); + } + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + /// Initialize the style class to 'jfx-spinner'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-spinner"; + + private static final double DEFAULT_RADIUS = 12.0; + + /** + * specifies the radius of the spinner node, by default it's set to `12.0` + */ + private StyleableDoubleProperty radius; + + public final StyleableDoubleProperty radiusProperty() { + if (this.radius == null) { + this.radius = new SimpleStyleableDoubleProperty(StyleableProperties.RADIUS, + JFXSpinner.this, + "radius", + DEFAULT_RADIUS); + } + return this.radius; + } + + public final double getRadius() { + return radius != null ? radius.get() : DEFAULT_RADIUS; + } + + public final void setRadius(final double radius) { + this.radiusProperty().set(radius); + } + + /// specifies from which angle the spinner should start spinning + private StyleableDoubleProperty startingAngle; + + public final StyleableDoubleProperty startingAngleProperty() { + if (this.startingAngle == null) { + startingAngle = new SimpleStyleableDoubleProperty(StyleableProperties.STARTING_ANGLE, + JFXSpinner.this, + "startingAngle", + 0.0); + } + return this.startingAngle; + } + + public final double getStartingAngle() { + return startingAngle != null ? startingAngle.get() : 0.0; + } + + public final void setStartingAngle(final double startingAngle) { + this.startingAngleProperty().set(startingAngle); + } + + private static final class StyleableProperties { + private static final CssMetaData RADIUS = + new CssMetaData<>("-jfx-radius", + SizeConverter.getInstance(), DEFAULT_RADIUS) { + @Override + public boolean isSettable(JFXSpinner control) { + return control.radius == null || !control.radius.isBound(); + } + + @Override + public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) { + return control.radius; + } + }; + + private static final CssMetaData STARTING_ANGLE = + new CssMetaData<>("-jfx-starting-angle", + SizeConverter.getInstance(), 0.0) { + @Override + public boolean isSettable(JFXSpinner control) { + return control.startingAngle == null || !control.startingAngle.isBound(); + } + + @Override + public StyleableDoubleProperty getStyleableProperty(JFXSpinner control) { + return control.startingAngle; + } + }; + + private static final List> CHILD_STYLEABLES; + + static { + final List> styleables = + new ArrayList<>(ProgressIndicator.getClassCssMetaData()); + Collections.addAll(styleables, RADIUS, STARTING_ANGLE); + CHILD_STYLEABLES = List.copyOf(styleables); + } + } + + @Override + public List> getControlCssMetaData() { + return getClassCssMetaData(); + } + + public static List> getClassCssMetaData() { + return StyleableProperties.CHILD_STYLEABLES; + } + +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java new file mode 100644 index 000000000..bc844f62a --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXProgressBarSkin.java @@ -0,0 +1,224 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +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.scene.Node; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.skin.ProgressIndicatorSkin; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.util.Duration; + +/// # Material Design ProgressBar Skin +/// +/// @author Shadi Shaheen +/// @version 2.0 +/// @since 2017-10-06 +public class JFXProgressBarSkin extends ProgressIndicatorSkin { + + private StackPane track; + private StackPane secondaryBar; + private StackPane bar; + private double barWidth = 0; + private double secondaryBarWidth = 0; + private Timeline indeterminateTransition; + private Region clip; + private final TreeShowingProperty treeShowingProperty; + + public JFXProgressBarSkin(JFXProgressBar bar) { + super(bar); + + this.treeShowingProperty = new TreeShowingProperty(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()); + + unregisterChangeListeners(treeShowingProperty); + unregisterChangeListeners(bar.indeterminateProperty()); + + registerChangeListener(treeShowingProperty, obs -> this.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"); + + 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); + } + + @Override + public double computeBaselineOffset(double topInset, double rightInset, double bottomInset, double leftInset) { + return Node.BASELINE_OFFSET_SAME_AS_HEIGHT; + } + + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return Math.max(100, leftInset + bar.prefWidth(getSkinnable().getWidth()) + rightInset); + } + + @Override + protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return topInset + bar.prefHeight(width) + bottomInset; + } + + @Override + protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + return getSkinnable().prefWidth(height); + } + + @Override + protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + return getSkinnable().prefHeight(width); + } + + @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); + + 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); + } + } + + 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) { + if (getSkinnable().isIndeterminate()) { + if (indeterminateTransition == null) { + createIndeterminateTimeline(); + } + if (pause) { + indeterminateTransition.pause(); + } else { + indeterminateTransition.play(); + } + } + } + + private void updateAnimation() { + final boolean isTreeShowing = treeShowingProperty.get(); + if (indeterminateTransition != null) { + pauseTimeline(!isTreeShowing); + } else if (isTreeShowing) { + createIndeterminateTimeline(); + } + } + + 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; + } + + 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) + ), + new KeyFrame( + Duration.seconds(0.5 * dur), + new KeyValue(clip.scaleXProperty(), 0.4, Interpolator.LINEAR) + ), + new KeyFrame( + Duration.seconds(0.9 * dur), + new KeyValue(clip.translateXProperty(), w / 2, Interpolator.LINEAR) + ), + new KeyFrame( + Duration.seconds(1 * dur), + new KeyValue(clip.scaleXProperty(), 0.0, Interpolator.EASE_OUT) + )); + indeterminateTransition.setCycleCount(Timeline.INDEFINITE); + } + + private void clearAnimation() { + indeterminateTransition.stop(); + indeterminateTransition.getKeyFrames().clear(); + indeterminateTransition = null; + } + + @Override + public void dispose() { + super.dispose(); + treeShowingProperty.dispose(); + if (indeterminateTransition != null) { + clearAnimation(); + } + } +} diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXSpinnerSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXSpinnerSkin.java new file mode 100644 index 000000000..e0ab5da12 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXSpinnerSkin.java @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.skins; + +import com.jfoenix.controls.JFXSpinner; +import com.jfoenix.utils.JFXNodeUtils; +import com.jfoenix.utils.TreeShowingProperty; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.scene.Group; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.control.SkinBase; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.Arc; +import javafx.scene.shape.Rectangle; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import javafx.util.Duration; + +/// JFXSpinner material design skin +/// +/// @author Shadi Shaheen & Gerard Moubarak +/// @version 1.0 +/// @since 2017-09-25 +public class JFXSpinnerSkin extends SkinBase { + + private static final Color GREEN_COLOR = Color.valueOf("#0F9D58"); + private static final Color RED_COLOR = Color.valueOf("#db4437"); + private static final Color YELLOW_COLOR = Color.valueOf("#f4b400"); + private static final Color BLUE_COLOR = Color.valueOf("#4285f4"); + + private JFXSpinner control; + private final TreeShowingProperty treeShowingProperty; + private boolean isValid = false; + + private Timeline timeline; + private Arc arc; + private Arc track; + private final StackPane arcPane; + private final Rectangle fillRect; + private double arcLength = -1; + private final Text text; + + public JFXSpinnerSkin(JFXSpinner control) { + super(control); + + this.control = control; + this.treeShowingProperty = new TreeShowingProperty(control); + + arc = new Arc(); + arc.setManaged(false); + arc.setStartAngle(0); + arc.setLength(180); + arc.getStyleClass().setAll("arc"); + arc.setFill(Color.TRANSPARENT); + arc.setStrokeWidth(3); + arc.setStrokeLineCap(StrokeLineCap.ROUND); + + track = new Arc(); + track.setManaged(false); + track.setStartAngle(0); + track.setLength(360); + track.setStrokeWidth(3); + track.getStyleClass().setAll("track"); + track.setFill(Color.TRANSPARENT); + + fillRect = new Rectangle(); + fillRect.setFill(Color.TRANSPARENT); + text = new Text(); + text.setStyle("-fx-font-size:null"); + text.getStyleClass().setAll("text", "percentage"); + final Group group = new Group(fillRect, track, arc, text); + group.setManaged(false); + arcPane = new StackPane(group); + arcPane.setPrefSize(50, 50); + getChildren().setAll(arcPane); + + // register listeners + registerChangeListener(control.indeterminateProperty(), obs -> initialize()); + registerChangeListener(control.progressProperty(), obs -> updateProgress()); + registerChangeListener(treeShowingProperty, obs -> updateAnimation()); + registerChangeListener(control.sceneProperty(), obs -> updateAnimation()); + } + + private void initialize() { + if (getSkinnable().isIndeterminate()) { + if (timeline == null) { + createTransition(); + if (JFXNodeUtils.isTreeShowing(getSkinnable())) { + timeline.play(); + } + } + } else { + clearAnimation(); + arc.setStartAngle(90); + updateProgress(); + } + } + + private KeyFrame[] getKeyFrames(double angle, double duration, Paint color) { + KeyFrame[] frames = new KeyFrame[4]; + frames[0] = new KeyFrame(Duration.seconds(duration), + new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR), + new KeyValue(arc.startAngleProperty(), + angle + 45 + control.getStartingAngle(), + Interpolator.LINEAR)); + frames[1] = new KeyFrame(Duration.seconds(duration + 0.4), + new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR), + new KeyValue(arc.startAngleProperty(), + angle + 90 + control.getStartingAngle(), + Interpolator.LINEAR)); + frames[2] = new KeyFrame(Duration.seconds(duration + 0.7), + new KeyValue(arc.lengthProperty(), 250, Interpolator.LINEAR), + new KeyValue(arc.startAngleProperty(), + angle + 135 + control.getStartingAngle(), + Interpolator.LINEAR)); + frames[3] = new KeyFrame(Duration.seconds(duration + 1.1), + new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR), + new KeyValue(arc.startAngleProperty(), + angle + 435 + control.getStartingAngle(), + Interpolator.LINEAR), + new KeyValue(arc.strokeProperty(), color, Interpolator.EASE_BOTH)); + return frames; + } + + private void pauseTimeline(boolean pause) { + if (getSkinnable().isIndeterminate()) { + if (timeline == null) { + createTransition(); + } + if (pause) { + timeline.pause(); + } else { + timeline.play(); + } + } + } + + private void updateAnimation() { + final boolean isTreeShowing = treeShowingProperty.get(); + if (timeline != null) { + pauseTimeline(!isTreeShowing); + } else if (isTreeShowing) { + createTransition(); + } + } + + private double computeSize() { + return control.getRadius() * 2 + arc.getStrokeWidth() * 2; + } + + @Override + protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + if (Region.USE_COMPUTED_SIZE == control.getRadius()) { + return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset); + } else { + return computeSize(); + } + } + + @Override + protected double computeMaxWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + if (Region.USE_COMPUTED_SIZE == control.getRadius()) { + return super.computeMaxWidth(height, topInset, rightInset, bottomInset, leftInset); + } else { + return computeSize(); + } + } + + @Override + protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { + if (Region.USE_COMPUTED_SIZE == control.getRadius()) { + return arcPane.prefWidth(-1); + } else { + return computeSize(); + } + } + + @Override + protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { + if (Region.USE_COMPUTED_SIZE == control.getRadius()) { + return arcPane.prefHeight(-1); + } else { + return computeSize(); + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { + final double strokeWidth = arc.getStrokeWidth(); + final double radius = Math.min(contentWidth, contentHeight) / 2 - strokeWidth / 2; + final double arcSize = snapSizeX(radius * 2 + strokeWidth); + + arcPane.resizeRelocate((contentWidth - arcSize) / 2 + 1, (contentHeight - arcSize) / 2 + 1, arcSize, arcSize); + updateArcLayout(radius, arcSize); + + fillRect.setWidth(arcSize); + fillRect.setHeight(arcSize); + + if (!isValid) { + initialize(); + isValid = true; + } + + if (!getSkinnable().isIndeterminate()) { + arc.setLength(arcLength); + if (text.isVisible()) { + final double progress = control.getProgress(); + int intProgress = (int) Math.round(progress * 100.0); + Font font = text.getFont(); + text.setFont(Font.font(font.getFamily(), radius / 1.7)); + text.setText((progress > 1 ? 100 : intProgress) + "%"); + text.relocate((arcSize - text.getLayoutBounds().getWidth()) / 2, (arcSize - text.getLayoutBounds().getHeight()) / 2); + } + } + } + + private void updateArcLayout(double radius, double arcSize) { + arc.setRadiusX(radius); + arc.setRadiusY(radius); + arc.setCenterX(arcSize / 2); + arc.setCenterY(arcSize / 2); + + track.setRadiusX(radius); + track.setRadiusY(radius); + track.setCenterX(arcSize / 2); + track.setCenterY(arcSize / 2); + track.setStrokeWidth(arc.getStrokeWidth()); + } + + boolean wasIndeterminate = false; + + protected void updateProgress() { + final ProgressIndicator control = getSkinnable(); + final boolean isIndeterminate = control.isIndeterminate(); + if (!(isIndeterminate && wasIndeterminate)) { + arcLength = -360 * control.getProgress(); + control.requestLayout(); + } + wasIndeterminate = isIndeterminate; + } + + private void createTransition() { + if (!getSkinnable().isIndeterminate()) return; + final Paint initialColor = arc.getStroke(); + if (initialColor == null) { + arc.setStroke(BLUE_COLOR); + } + + KeyFrame[] blueFrame = getKeyFrames(0, 0, initialColor == null ? BLUE_COLOR : initialColor); + KeyFrame[] redFrame = getKeyFrames(450, 1.4, initialColor == null ? RED_COLOR : initialColor); + KeyFrame[] yellowFrame = getKeyFrames(900, 2.8, initialColor == null ? YELLOW_COLOR : initialColor); + KeyFrame[] greenFrame = getKeyFrames(1350, 4.2, initialColor == null ? GREEN_COLOR : initialColor); + + KeyFrame endingFrame = new KeyFrame(Duration.seconds(5.6), + new KeyValue(arc.lengthProperty(), 5, Interpolator.LINEAR), + new KeyValue(arc.startAngleProperty(), + 1845 + control.getStartingAngle(), + Interpolator.LINEAR)); + + if (timeline != null) { + timeline.stop(); + timeline.getKeyFrames().clear(); + } + timeline = new Timeline(blueFrame[0], + blueFrame[1], + blueFrame[2], + blueFrame[3], + redFrame[0], + redFrame[1], + redFrame[2], + redFrame[3], + yellowFrame[0], + yellowFrame[1], + yellowFrame[2], + yellowFrame[3], + greenFrame[0], + greenFrame[1], + greenFrame[2], + greenFrame[3], + endingFrame); + timeline.setCycleCount(Timeline.INDEFINITE); + timeline.setDelay(Duration.ZERO); + timeline.playFromStart(); + } + + private void clearAnimation() { + if (timeline != null) { + timeline.stop(); + timeline.getKeyFrames().clear(); + timeline = null; + } + } + + @Override + public void dispose() { + super.dispose(); + treeShowingProperty.dispose(); + clearAnimation(); + arc = null; + track = null; + control = null; + } +} diff --git a/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java b/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java index 2a08dcfa0..cc9154a9f 100644 --- a/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java +++ b/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java @@ -19,13 +19,25 @@ package com.jfoenix.utils; +import javafx.beans.value.ObservableBooleanValue; +import javafx.scene.Node; +import javafx.scene.Scene; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.Region; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; +import javafx.stage.Window; +import org.jetbrains.annotations.NotNull; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; import java.util.Locale; +import java.util.function.Function; + +import static org.jackhuang.hmcl.util.logging.Logger.LOG; /// @author Shadi Shaheen /// @version 1.0 @@ -58,6 +70,70 @@ public final class JFXNodeUtils { } } + private static final @NotNull Function treeVisiblePropertyGetter = initTreeVisiblePropertyGetter(); + + private static @NotNull Function initTreeVisiblePropertyGetter() { + + MethodHandles.Lookup lookup; + try { + lookup = MethodHandles.privateLookupIn(Node.class, MethodHandles.lookup()); + } catch (IllegalAccessException e) { + LOG.warning("Failed to get private lookup for Node", e); + return JFXNodeUtils::defaultTreeVisibleProperty; + } + + try { + Method treeVisiblePropertyMethod = Node.class.getDeclaredMethod("treeVisibleProperty"); + if (!ObservableBooleanValue.class.isAssignableFrom(treeVisiblePropertyMethod.getReturnType())) { + LOG.warning("Node.treeVisibleProperty() does not return ObservableBooleanValue: " + treeVisiblePropertyMethod.getReturnType()); + return JFXNodeUtils::defaultTreeVisibleProperty; + } + + MethodHandle handle = lookup.unreflect(treeVisiblePropertyMethod) + .asType(MethodType.methodType(ObservableBooleanValue.class, Node.class)); + return item -> { + try { + return (ObservableBooleanValue) handle.invokeExact((Node) item); + } catch (RuntimeException | Error e) { + throw e; + } catch (Throwable e) { + throw new AssertionError("Unreachable", e); + } + }; + } catch (Exception e) { + LOG.warning("Failed to get method handle for Node.treeVisibleProperty()", e); + return JFXNodeUtils::defaultTreeVisibleProperty; + } + } + + /// If `Node.treeVisibleProperty()` does not exist, use `Node.visibleProperty()` as a fallback + private static @NotNull ObservableBooleanValue defaultTreeVisibleProperty(Node item) { + return item.visibleProperty(); + } + + public static @NotNull ObservableBooleanValue treeVisibleProperty(Node item) { + return treeVisiblePropertyGetter.apply(item); + } + + public static boolean isTreeVisible(Node item) { + return treeVisibleProperty(item).getValue(); + } + + public static boolean isTreeShowing(Node node) { + if (node == null) + return false; + + Scene scene = node.getScene(); + if (scene == null) + return false; + + Window window = scene.getWindow(); + if (window == null || !window.isShowing()) + return false; + + return isTreeVisible(node); + } + private JFXNodeUtils() { } diff --git a/HMCL/src/main/java/com/jfoenix/utils/TreeShowingProperty.java b/HMCL/src/main/java/com/jfoenix/utils/TreeShowingProperty.java new file mode 100644 index 000000000..6050282af --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/utils/TreeShowingProperty.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +package com.jfoenix.utils; + +import javafx.beans.property.ReadOnlyBooleanPropertyBase; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableBooleanValue; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.stage.Window; + +/** + * Used to observe changes in tree showing status for a {@link Node}. For a Node's tree to be showing + * it must be visible, its ancestors must be visible, the node must be part of a {@link Scene} and + * the scene must have a {@link Window} which is currently showing.

+ * + * This class provides the exact same functionality as {@link JFXNodeUtils#isTreeShowing(Node)} in + * an observable form. + */ +public class TreeShowingProperty extends ReadOnlyBooleanPropertyBase { + private final ChangeListener windowShowingChangedListener = (obs, old, current) -> updateTreeShowing(); + private final ChangeListener sceneWindowChangedListener = (obs, old, current) -> windowChanged(old, current); + private final ChangeListener nodeSceneChangedListener = (obs, old, current) -> sceneChanged(old, current); + + private final Node node; + private final ObservableBooleanValue treeVisibleProperty; + + private boolean valid; + private boolean treeShowing; + + /** + * Constructs a new instance. + * + * @param node a {@link Node} for which the tree showing status should be observed, cannot be null + */ + public TreeShowingProperty(Node node) { + this.node = node; + this.treeVisibleProperty = JFXNodeUtils.treeVisibleProperty(node); + + this.node.sceneProperty().addListener(nodeSceneChangedListener); + this.treeVisibleProperty.addListener(windowShowingChangedListener); + + sceneChanged(null, node.getScene()); + } + + @Override + public Object getBean() { + return node; + } + + @Override + public String getName() { + return "treeShowing"; + } + + /** + * Cleans up any listeners that this class may have registered on the {@link Node} + * that was supplied at construction. + */ + public void dispose() { + node.sceneProperty().removeListener(nodeSceneChangedListener); + + if (treeVisibleProperty != null) + treeVisibleProperty.removeListener(windowShowingChangedListener); + + valid = false; // prevents unregistration from triggering an invalidation notification + sceneChanged(node.getScene(), null); + } + + protected void invalidate() { + if (valid) { + valid = false; + fireValueChangedEvent(); + } + } + + @Override + public boolean get() { + if (!valid) { + updateTreeShowing(); + valid = true; + } + + return treeShowing; + } + + private void sceneChanged(Scene oldScene, Scene newScene) { + if (oldScene != null) { + oldScene.windowProperty().removeListener(sceneWindowChangedListener); + } + if (newScene != null) { + newScene.windowProperty().addListener(sceneWindowChangedListener); + } + + windowChanged( + oldScene == null ? null : oldScene.getWindow(), + newScene == null ? null : newScene.getWindow() + ); + } + + private void windowChanged(Window oldWindow, Window newWindow) { + if (oldWindow != null) { + oldWindow.showingProperty().removeListener(windowShowingChangedListener); + } + if (newWindow != null) { + newWindow.showingProperty().addListener(windowShowingChangedListener); + } + + updateTreeShowing(); + } + + private void updateTreeShowing() { + boolean newValue = JFXNodeUtils.isTreeShowing(node); + + if (newValue != treeShowing) { + treeShowing = newValue; + invalidate(); + } + } +} diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 7bcdb9810..1bb680b8c 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -904,23 +904,28 @@ * * *******************************************************************************/ -.jfx-progress-bar { - -fx-pref-width: 500.0; -} - -.jfx-progress-bar > .track, .jfx-progress-bar > .bar { - -fx-background-radius: 0; - -fx-background-insets: 0; -} - .jfx-progress-bar > .track { -fx-background-color: -monet-secondary-container; } -.jfx-progress-bar > .bar { +.jfx-progress-bar > .bar, +.jfx-progress-bar:indeterminate > .bar{ -fx-background-color: -monet-primary; + -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-insets: 0; +} + + /******************************************************************************* * * * JFX Textfield * @@ -1439,21 +1444,19 @@ * * *******************************************************************************/ -.jfx-spinner > .arc { - -fx-stroke-width: 3.0; - -fx-fill: transparent; +.jfx-spinner .arc { -fx-stroke: -monet-primary-container; } -.jfx-spinner .arc { - -fx-stroke-line-cap: round; +.jfx-spinner:determinate .percentage { + -fx-fill: -monet-on-surface-variant; } .first-spinner { -jfx-radius: 20; } -.first-spinner > .arc { +.first-spinner .arc { -fx-stroke-width: 5.0; } @@ -1461,7 +1464,7 @@ -jfx-radius: 10; } -.small-spinner > .arc { +.small-spinner .arc { -fx-stroke-width: 3.0; } @@ -1469,7 +1472,7 @@ -jfx-radius: 10; } -.small-spinner-pane .jfx-spinner > .arc { +.small-spinner-pane .jfx-spinner .arc { -fx-stroke-width: 3.0; } @@ -1477,7 +1480,7 @@ -jfx-radius: 30; } -.second-spinner > .arc { +.second-spinner .arc { -fx-stroke-width: 5.0; } @@ -1485,7 +1488,7 @@ -jfx-radius: 40; } -.third-spinner > .arc { +.third-spinner .arc { -fx-stroke-width: 5.0; } @@ -1493,7 +1496,7 @@ -jfx-radius: 50; } -.fourth-spinner > .arc { +.fourth-spinner .arc { -fx-stroke-width: 5.0; } @@ -1501,7 +1504,7 @@ -jfx-radius: 60; } -.fifth-spinner > .arc { +.fifth-spinner .arc { -fx-stroke-width: 5.0; } @@ -1509,7 +1512,7 @@ -jfx-radius: 70; } -.sixth-spinner > .arc { +.sixth-spinner .arc { -fx-stroke-width: 5.0; } @@ -1517,7 +1520,7 @@ -jfx-radius: 80; } -.seventh-spinner > .arc { +.seventh-spinner .arc { -fx-stroke-width: 5.0; }