diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/CircleProgressIndicatorApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/CircleProgressIndicatorApp.java index 340b3a4a..a309b923 100644 --- a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/CircleProgressIndicatorApp.java +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/CircleProgressIndicatorApp.java @@ -3,13 +3,17 @@ import com.dlsc.gemsfx.CircleProgressIndicator; import javafx.application.Application; import javafx.application.Platform; +import javafx.beans.binding.Bindings; import javafx.concurrent.Service; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Scene; import javafx.scene.control.CheckBox; import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; import javafx.scene.control.Separator; +import javafx.scene.control.Slider; +import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; @@ -36,12 +40,23 @@ public void start(Stage primaryStage) { String firstStyle = styles[0]; // add style progressIndicator.getStyleClass().add(firstStyle); + styleComboBox.setMaxWidth(Double.MAX_VALUE); styleComboBox.setValue(firstStyle); styleComboBox.valueProperty().addListener(it -> { progressIndicator.getStyleClass().removeAll(styles); progressIndicator.getStyleClass().add(styleComboBox.getValue()); }); + // start Angle + Label startAngleLabel = new Label("Start Angle"); + Slider startAngleSlider = new Slider(0, 360, 90); + progressIndicator.startAngleProperty().bind(startAngleSlider.valueProperty()); + Label startAngleValue = new Label(); + startAngleValue.setPrefWidth(30); + startAngleValue.textProperty().bind(Bindings.format("%.0f", startAngleSlider.valueProperty())); + HBox startAngleBox = new HBox(5, startAngleLabel, startAngleSlider, startAngleValue); + startAngleBox.setAlignment(Pos.CENTER_LEFT); + // graphic FontIcon graphic = new FontIcon(); CheckBox showGraphic = new CheckBox("Show Graphic"); @@ -61,7 +76,7 @@ public void start(Stage primaryStage) { indicatorWrapper.getStyleClass().add("indicator-wrapper"); VBox.setVgrow(indicatorWrapper, Priority.ALWAYS); - VBox bottom = new VBox(10, styleComboBox, showGraphic, customConverterBox); + VBox bottom = new VBox(10, showGraphic, customConverterBox, startAngleBox, styleComboBox); bottom.setAlignment(Pos.CENTER_LEFT); bottom.setMaxWidth(Region.USE_PREF_SIZE); @@ -72,7 +87,7 @@ public void start(Stage primaryStage) { containerBox.getChildren().addAll(indicatorWrapper, new Separator(), bottom); Scene scene = new Scene(containerBox, 330, 390); - scene.getStylesheets().add(Objects.requireNonNull(CircleProgressIndicatorApp.class.getResource("circle-progress-indicator-demo.css")).toExternalForm()); + scene.getStylesheets().add(Objects.requireNonNull(CircleProgressIndicatorApp.class.getResource("arc-progress-indicator-demo.css")).toExternalForm()); primaryStage.setScene(scene); primaryStage.setTitle("CircleProgressIndicator Demo"); primaryStage.show(); diff --git a/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SemiCircleProgressIndicatorApp.java b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SemiCircleProgressIndicatorApp.java new file mode 100644 index 00000000..64bad55b --- /dev/null +++ b/gemsfx-demo/src/main/java/com/dlsc/gemsfx/demo/SemiCircleProgressIndicatorApp.java @@ -0,0 +1,134 @@ +package com.dlsc.gemsfx.demo; + +import com.dlsc.gemsfx.SemiCircleProgressIndicator; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.concurrent.Service; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Scene; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Separator; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.stage.Stage; +import javafx.util.StringConverter; +import org.kordamp.ikonli.javafx.FontIcon; + +import java.util.Objects; + +public class SemiCircleProgressIndicatorApp extends Application { + + private StringConverter customConverter; + + @Override + public void start(Stage primaryStage) { + SemiCircleProgressIndicator progressIndicator = new SemiCircleProgressIndicator(); + delayAutoUpdateProgress(progressIndicator); + + // styles + String[] styles = new String[]{"bold-style", "thin-style", "sector-style", "default-style"}; + ComboBox styleComboBox = new ComboBox<>(); + styleComboBox.getItems().addAll(styles); + String firstStyle = styles[0]; + // add style + progressIndicator.getStyleClass().add(firstStyle); + styleComboBox.setValue(firstStyle); + styleComboBox.valueProperty().addListener(it -> { + progressIndicator.getStyleClass().removeAll(styles); + progressIndicator.getStyleClass().add(styleComboBox.getValue()); + }); + + // graphic + FontIcon graphic = new FontIcon(); + CheckBox showGraphic = new CheckBox("Show Graphic"); + showGraphic.selectedProperty().addListener((observable, oldValue, newValue) -> { + progressIndicator.setGraphic(newValue ? graphic : null); + }); + showGraphic.setSelected(true); + + // string converter + StringConverter defaultConvert = progressIndicator.getConverter(); + CheckBox customConverterBox = new CheckBox("Custom Converter"); + customConverterBox.selectedProperty().addListener((observable, oldValue, newValue) -> progressIndicator.setConverter(newValue ? getCustomConverter() : defaultConvert)); + customConverterBox.setSelected(true); + + // layout + StackPane indicatorWrapper = new StackPane(progressIndicator); + indicatorWrapper.getStyleClass().add("indicator-wrapper"); + VBox.setVgrow(indicatorWrapper, Priority.ALWAYS); + + VBox bottom = new VBox(10, styleComboBox, showGraphic, customConverterBox); + bottom.setAlignment(Pos.CENTER_LEFT); + bottom.setMaxWidth(Region.USE_PREF_SIZE); + + VBox containerBox = new VBox(20); + containerBox.getStyleClass().add("container-box"); + containerBox.setPadding(new Insets(20)); + containerBox.setAlignment(Pos.CENTER); + containerBox.getChildren().addAll(indicatorWrapper, new Separator(), bottom); + + Scene scene = new Scene(containerBox, 300, 390); + scene.getStylesheets().add(Objects.requireNonNull(SemiCircleProgressIndicatorApp.class.getResource("arc-progress-indicator-demo.css")).toExternalForm()); + primaryStage.setScene(scene); + primaryStage.setTitle("SemiCircleProgressIndicator"); + primaryStage.show(); + } + + private void delayAutoUpdateProgress(SemiCircleProgressIndicator graphicIndicator) { + Service service = new Service<>() { + @Override + protected javafx.concurrent.Task createTask() { + return new javafx.concurrent.Task<>() { + @Override + protected Void call() throws Exception { + for (int i = 0; i < 3000; i++) { + Thread.sleep(4500); + for (int j = 0; j <= 100; j++) { + updateProgress(j, 100); + Thread.sleep(50); + } + Thread.sleep(2000); + Platform.runLater(() -> updateProgress(-1, 100)); + } + return null; + } + }; + } + }; + graphicIndicator.progressProperty().bind(service.progressProperty()); + service.start(); + } + + private StringConverter getCustomConverter() { + if (customConverter == null) { + customConverter = new StringConverter<>() { + @Override + public String toString(Double progress) { + if (progress == null || progress < 0.0) { + return "Connecting"; + } + double percentage = progress * 100; + if (progress.intValue() == 1) { + return "Download Complete"; + } + return String.format("Downloading %.0f%%", percentage); + } + + @Override + public Double fromString(String string) { + return null; + } + }; + } + return customConverter; + } + + public static void main(String[] args) { + launch(args); + } + +} diff --git a/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/circle-progress-indicator-demo.css b/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/arc-progress-indicator-demo.css similarity index 60% rename from gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/circle-progress-indicator-demo.css rename to gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/arc-progress-indicator-demo.css index 85035838..bda2a7dc 100644 --- a/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/circle-progress-indicator-demo.css +++ b/gemsfx-demo/src/main/resources/com/dlsc/gemsfx/demo/arc-progress-indicator-demo.css @@ -1,57 +1,57 @@ -.circle-progress-indicator { +.arc-progress-indicator { /*-fx-font-family: Monospaced;*/ } -.circle-progress-indicator .progress-label { +.arc-progress-indicator .progress-label { -fx-font-size: 15px; } /** --- icons --- */ -.circle-progress-indicator .progress-label .ikonli-font-icon { +.arc-progress-indicator .progress-label .ikonli-font-icon { -fx-icon-code: mdi-download; -fx-icon-size: 16px; } -.circle-progress-indicator:indeterminate .progress-label .ikonli-font-icon { +.arc-progress-indicator:indeterminate .progress-label .ikonli-font-icon { -fx-icon-code: mdi-access-point-network; } -.circle-progress-indicator:completed .progress-label .ikonli-font-icon { +.arc-progress-indicator:completed .progress-label .ikonli-font-icon { -fx-icon-code: mdi-check-circle-outline; } /* --- bold style --- */ -.circle-progress-indicator.bold-style .track-circle { +.arc-progress-indicator.bold-style .track-circle { -fx-stroke-width: 10px; -fx-stroke: #dadada; } -.circle-progress-indicator.bold-style .progress-arc { +.arc-progress-indicator.bold-style .progress-arc { -fx-stroke-width: 5px; -fx-stroke: #67986e; } /** --- thin style --- */ -.circle-progress-indicator.thin-style .track-circle { +.arc-progress-indicator.thin-style .track-circle { -fx-stroke-width: 1px; -fx-stroke: #c9c9c9; } -.circle-progress-indicator.thin-style .progress-arc { +.arc-progress-indicator.thin-style .progress-arc { -fx-stroke-width: 1px; -fx-stroke: #f87328; } /** --- sector style --- */ -.circle-progress-indicator.sector-style { - -fx-arc-type: ROUND; +.arc-progress-indicator.sector-style { + -fx-progress-arc-type: ROUND; } -.circle-progress-indicator.sector-style .track-circle { +.arc-progress-indicator.sector-style .track-circle { -fx-stroke-width: 5px; } -.circle-progress-indicator.sector-style .progress-arc { +.arc-progress-indicator.sector-style .progress-arc { -fx-fill: #47bce833; -fx-stroke-width: 1px; } @@ -64,4 +64,4 @@ /** --- custom style --- */ .container-box { -fx-background-color: white; -} +} \ No newline at end of file diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/ArcProgressIndicator.java b/gemsfx/src/main/java/com/dlsc/gemsfx/ArcProgressIndicator.java new file mode 100644 index 00000000..211b3911 --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/ArcProgressIndicator.java @@ -0,0 +1,253 @@ +package com.dlsc.gemsfx; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.css.CssMetaData; +import javafx.css.Styleable; +import javafx.css.StyleableObjectProperty; +import javafx.css.StyleableProperty; +import javafx.css.converter.EnumConverter; +import javafx.scene.Node; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.shape.ArcType; +import javafx.util.StringConverter; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * ArcProgressIndicator is a visual control used to indicate the progress of a task. + * It represents progress in an arc form, with options to show determinate or indeterminate states. + *

+ * In a determinate state, the arc fills up based on the progress value, which ranges from 0.0 to 1.0, + * where 0.0 indicates no progress and 1.0 indicates completion. + *

+ * In an indeterminate state, the arc shows a cyclic animation, indicating that progress is ongoing + * but the exact status is unknown. This state is useful for tasks where the progress cannot be determined. + *

+ * The control also supports displaying a text or graphic inside the arc to provide additional + * information or visual feedback to the user. + *

+ * Usage examples include file downloads, file transfers, or any long-running tasks where + * visual feedback on progress is beneficial. + * + *

+ * Pseudo class: Beyond the inherited, indeterminate, and determinate pseudo-classes + * from ProgressIndicator, ArcProgressIndicator introduces a completed pseudo-class. + * This pseudo-class can be used in CSS to apply custom styles when the progress reaches 1.0 (100%). + * + *

+ * Tips: If you prefer not to instantiate the animation object during initialization, + * pass a 0.0 as the initial progress. This setup indicates no progress but avoids entering + * the indeterminate state, which would otherwise instantiate and start the animation. + * + *

Usage examples: + *

+ *     // Initializes with no progress and no animation.
+ *     ArcProgressIndicator progressIndicator = new ArcProgressIndicator(0.0);
+ * 
+ */ +public class ArcProgressIndicator extends ProgressIndicator { + + private static final String DEFAULT_STYLE_CLASS = "arc-progress-indicator"; + private static final ArcType DEFAULT_PROGRESS_ARC_TYPE = ArcType.OPEN; + private static final ArcType DEFAULT_TRACK_ARC_TYPE = ArcType.CHORD; + + private static final StringConverter DEFAULT_CONVERTER = new StringConverter<>() { + @Override + public String toString(Double progress) { + // indeterminate + if (progress == null || progress < 0.0) { + return ""; + } + // completed + if (progress == 1.0) { + return "Completed"; + } + return String.format("%.0f%%", progress * 100); + } + + @Override + public Double fromString(String string) { + return null; + } + }; + + public ArcProgressIndicator() { + this(INDETERMINATE_PROGRESS); + } + + public ArcProgressIndicator(double progress) { + super(progress); + getStyleClass().add(DEFAULT_STYLE_CLASS); + } + + @Override + public String getUserAgentStylesheet() { + return Objects.requireNonNull(ArcProgressIndicator.class.getResource("arc-progress-indicator.css")).toExternalForm(); + } + + private final ObjectProperty> converter = new SimpleObjectProperty<>(this, "converter", DEFAULT_CONVERTER); + + public final StringConverter getConverter() { + return converter.get(); + } + + /** + * The converter is used to convert the progress value to a string that is displayed + * + * @return the converter property + */ + public final ObjectProperty> converterProperty() { + return converter; + } + + public final void setConverter(StringConverter converter) { + converterProperty().set(converter); + } + + private final ObjectProperty graphic = new SimpleObjectProperty<>(this, "graphic"); + + /** + * The graphic property is used to display a custom node within the progress indicator. + * progress label's graphic property is bound to this property. + * + * @return the graphic property + */ + public final ObjectProperty graphicProperty() { + return graphic; + } + + public final Node getGraphic() { + return graphic.get(); + } + + public final void setGraphic(Node graphic) { + graphicProperty().set(graphic); + } + + private ObjectProperty progressArcType; + + /** + * The arc type property defines the type of the arc that is used to display the progress. + * + * @return the arc type property for the progress + */ + public final ObjectProperty progressArcTypeProperty() { + if (progressArcType == null) { + progressArcType = new StyleableObjectProperty<>(DEFAULT_PROGRESS_ARC_TYPE) { + @Override + public Object getBean() { + return this; + } + + @Override + public String getName() { + return "progressArcType"; + } + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.PROGRESS_ARC_TYPE; + } + }; + } + return progressArcType; + } + + public final ArcType getProgressArcType() { + return progressArcType == null ? DEFAULT_PROGRESS_ARC_TYPE : progressArcType.get(); + } + + public final void setProgressArcType(ArcType progressArcType) { + progressArcTypeProperty().set(progressArcType); + } + + private ObjectProperty trackArcType; + + /** + * The arc type property defines the type of the arc that is used to display the track. + * + * @return the arc type property for the track + */ + public final ObjectProperty trackArcTypeProperty() { + if (trackArcType == null) { + trackArcType = new StyleableObjectProperty<>(DEFAULT_TRACK_ARC_TYPE) { + @Override + public Object getBean() { + return this; + } + + @Override + public String getName() { + return "trackArcType"; + } + + @Override + public CssMetaData getCssMetaData() { + return StyleableProperties.TRACK_ARC_TYPE; + } + }; + } + return trackArcType; + } + + public final ArcType getTrackArcType() { + return trackArcType == null ? DEFAULT_TRACK_ARC_TYPE : trackArcType.get(); + } + + public final void setTrackArcType(ArcType trackArcType) { + trackArcTypeProperty().set(trackArcType); + } + + private static class StyleableProperties { + + private static final CssMetaData PROGRESS_ARC_TYPE = new CssMetaData<>( + "-fx-progress-arc-type", new EnumConverter<>(ArcType.class), DEFAULT_PROGRESS_ARC_TYPE) { + + @Override + public StyleableProperty getStyleableProperty(ArcProgressIndicator control) { + return (StyleableProperty) control.progressArcTypeProperty(); + } + + @Override + public boolean isSettable(ArcProgressIndicator control) { + return control.progressArcType == null || !control.progressArcType.isBound(); + } + }; + + private static final CssMetaData TRACK_ARC_TYPE = new CssMetaData<>( + "-fx-track-arc-type", new EnumConverter<>(ArcType.class), DEFAULT_TRACK_ARC_TYPE) { + + @Override + public StyleableProperty getStyleableProperty(ArcProgressIndicator control) { + return (StyleableProperty) control.trackArcTypeProperty(); + } + + @Override + public boolean isSettable(ArcProgressIndicator control) { + return control.trackArcType == null || !control.trackArcType.isBound(); + } + }; + + private static final List> STYLEABLES; + + static { + final List> styleables = new ArrayList<>(ProgressIndicator.getClassCssMetaData()); + Collections.addAll(styleables, PROGRESS_ARC_TYPE, TRACK_ARC_TYPE); + STYLEABLES = Collections.unmodifiableList(styleables); + } + } + + @Override + protected List> getControlCssMetaData() { + return getClassCssMetaData(); + } + + public static List> getClassCssMetaData() { + return ArcProgressIndicator.StyleableProperties.STYLEABLES; + } + +} diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/CircleProgressIndicator.java b/gemsfx/src/main/java/com/dlsc/gemsfx/CircleProgressIndicator.java index f2dfe9bc..0b9d3baf 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/CircleProgressIndicator.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/CircleProgressIndicator.java @@ -1,24 +1,9 @@ package com.dlsc.gemsfx; import com.dlsc.gemsfx.skins.CircleProgressIndicatorSkin; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.css.CssMetaData; -import javafx.css.Styleable; -import javafx.css.StyleableObjectProperty; -import javafx.css.StyleableProperty; -import javafx.css.converter.EnumConverter; -import javafx.scene.Node; -import javafx.scene.control.ProgressIndicator; +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; import javafx.scene.control.Skin; -import javafx.scene.shape.ArcType; -import javafx.util.StringConverter; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - /** * CircleProgressIndicator is a visual control used to indicate the progress of a task. @@ -35,6 +20,10 @@ *

* Usage examples include file downloads, file transfers, or any long-running tasks where * visual feedback on progress is beneficial. + *

+ * The CircleProgressIndicator extends ArcProgressIndicator and adds a start angle property + * that defines the starting angle of the arc used to display the progress. The default start angle is 90 degrees, + * which corresponds to the top of the circle. * *

* Pseudo class: Beyond the inherited , indeterminate and determinate pseudo-classes @@ -52,29 +41,10 @@ * CircleProgressIndicator progressIndicator = new CircleProgressIndicator(0.0); * */ -public class CircleProgressIndicator extends ProgressIndicator { +public class CircleProgressIndicator extends ArcProgressIndicator { private static final String DEFAULT_STYLE_CLASS = "circle-progress-indicator"; - private static final ArcType DEFAULT_ARC_TYPE = ArcType.OPEN; - private static final StringConverter DEFAULT_CONVERTER = new StringConverter<>() { - @Override - public String toString(Double progress) { - // indeterminate - if (progress == null || progress < 0.0) { - return ""; - } - // completed - if (progress == 1.0) { - return "Completed"; - } - return String.format("%.0f%%", progress * 100); - } - - @Override - public Double fromString(String string) { - return null; - } - }; + private static final double DEFAULT_START_ANGLE = 90.0; public CircleProgressIndicator() { this(INDETERMINATE_PROGRESS); @@ -91,118 +61,27 @@ protected Skin createDefaultSkin() { return new CircleProgressIndicatorSkin(this); } - @Override - public String getUserAgentStylesheet() { - return Objects.requireNonNull(CircleProgressIndicator.class.getResource("circle-progress-indicator.css")).toExternalForm(); - } - - private final ObjectProperty> converter = new SimpleObjectProperty<>(this, "converter", DEFAULT_CONVERTER); - - public final StringConverter getConverter() { - return converter.get(); - } - - /** - * The converter is used to convert the progress value to a string that is displayed - * - * @return the converter property - */ - public final ObjectProperty> converterProperty() { - return converter; - } - - public final void setConverter(StringConverter converter) { - converterProperty().set(converter); - } - - private final ObjectProperty graphic = new SimpleObjectProperty<>(this, "graphic"); + private DoubleProperty startAngle; /** - * The graphic property is used to display a custom node within the progress indicator. - * progress label's graphic property is bound to this property. + * The start angle property defines the starting angle of the arc that is used to display the progress. + * The default value is 90 degrees, which corresponds to the top of the circle. * - * @return the graphic property + * @return the start angle property */ - public final ObjectProperty graphicProperty() { - return graphic; - } - - public final Node getGraphic() { - return graphic.get(); - } - - public final void setGraphic(Node graphic) { - graphicProperty().set(graphic); - } - - private ObjectProperty arcType; - - /** - * The arc type property defines the type of the arc that is used to display the progress. - * - * @return the arc type property - */ - public final ObjectProperty arcTypeProperty() { - if (arcType == null) { - arcType = new StyleableObjectProperty<>(DEFAULT_ARC_TYPE) { - @Override - public Object getBean() { - return this; - } - - @Override - public String getName() { - return "arcType"; - } - - @Override - public CssMetaData getCssMetaData() { - return StyleableProperties.ARC_TYPE; - } - }; + public final DoubleProperty startAngleProperty() { + if (startAngle == null) { + startAngle = new SimpleDoubleProperty(this, "startAngle", DEFAULT_START_ANGLE); } - return arcType; + return startAngle; } - public final ArcType getArcType() { - return arcType == null ? DEFAULT_ARC_TYPE : arcType.get(); + public final double getStartAngle() { + return startAngle == null ? DEFAULT_START_ANGLE : startAngle.get(); } - public final void setArcType(ArcType arcType) { - arcTypeProperty().set(arcType); + public final void setStartAngle(double startAngle) { + startAngleProperty().set(startAngle); } - private static class StyleableProperties { - - private static final CssMetaData ARC_TYPE = new CssMetaData<>( - "-fx-arc-type", new EnumConverter<>(ArcType.class), DEFAULT_ARC_TYPE) { - - @Override - public StyleableProperty getStyleableProperty(CircleProgressIndicator control) { - return (StyleableProperty) control.arcTypeProperty(); - } - - @Override - public boolean isSettable(CircleProgressIndicator control) { - return control.arcType == null || !control.arcType.isBound(); - } - }; - - private static final List> STYLEABLES; - - static { - final List> styleables = new ArrayList<>(ProgressIndicator.getClassCssMetaData()); - styleables.add(ARC_TYPE); - STYLEABLES = Collections.unmodifiableList(styleables); - } - } - - @Override - protected List> getControlCssMetaData() { - return getClassCssMetaData(); - } - - public static List> getClassCssMetaData() { - return CircleProgressIndicator.StyleableProperties.STYLEABLES; - } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/SemiCircleProgressIndicator.java b/gemsfx/src/main/java/com/dlsc/gemsfx/SemiCircleProgressIndicator.java new file mode 100644 index 00000000..60606e01 --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/SemiCircleProgressIndicator.java @@ -0,0 +1,57 @@ +package com.dlsc.gemsfx; + +import com.dlsc.gemsfx.skins.SemiCircleProgressIndicatorSkin; +import javafx.scene.control.Skin; + +/** + * SemiCircleProgressIndicator is a visual control used to indicate the progress of a task. + * It represents progress in a semi-circular form, with options to show determinate or indeterminate states. + *

+ * In a determinate state, the semi-circle fills up based on the progress value, which ranges from 0.0 to 1.0, + * where 0.0 indicates no progress and 1.0 indicates completion. + *

+ * In an indeterminate state, the semi-circle shows a cyclic animation, indicating that progress is ongoing + * but the exact status is unknown. This state is useful for tasks where the progress cannot be determined. + *

+ * The control also supports displaying a text or graphic inside the semi-circle to provide additional + * information or visual feedback to the user. + *

+ * Usage examples include file downloads, file transfers, or any long-running tasks where + * visual feedback on progress is beneficial. + * + *

+ * Pseudo class: Beyond the inherited, indeterminate, and determinate pseudo-classes + * from ProgressIndicator, SemiCircleProgressIndicator introduces a completed pseudo-class. + * This pseudo-class can be used in CSS to apply custom styles when the progress reaches 1.0 (100%). + * + *

+ * Tips: If you prefer not to instantiate the animation object during initialization, + * pass a 0.0 as the initial progress. This setup indicates no progress but avoids entering + * the indeterminate state, which would otherwise instantiate and start the animation. + * + *

Usage examples: + *

+ *     // Initializes with no progress and no animation.
+ *     SemiCircleProgressIndicator progressIndicator = new SemiCircleProgressIndicator(0.0);
+ * 
+ */ +public class SemiCircleProgressIndicator extends ArcProgressIndicator { + + private static final String DEFAULT_STYLE_CLASS = "semi-circle-progress-indicator"; + + public SemiCircleProgressIndicator() { + this(INDETERMINATE_PROGRESS); + } + + public SemiCircleProgressIndicator(double progress) { + super(progress); + getStyleClass().add(DEFAULT_STYLE_CLASS); + setMinSize(26, 26); + } + + @Override + protected Skin createDefaultSkin() { + return new SemiCircleProgressIndicatorSkin(this); + } + +} diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/ArcProgressIndicatorSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/ArcProgressIndicatorSkin.java new file mode 100644 index 00000000..934ac5fe --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/ArcProgressIndicatorSkin.java @@ -0,0 +1,213 @@ +package com.dlsc.gemsfx.skins; + +import com.dlsc.gemsfx.ArcProgressIndicator; +import javafx.animation.Animation; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.DoubleBinding; +import javafx.css.PseudoClass; +import javafx.scene.control.Label; +import javafx.scene.control.SkinBase; +import javafx.scene.shape.Arc; +import javafx.scene.transform.Rotate; +import javafx.util.StringConverter; + +public abstract class ArcProgressIndicatorSkin extends SkinBase { + + private static final PseudoClass PSEUDO_CLASS_COMPLETED = PseudoClass.getPseudoClass("completed"); + protected final Label progressLabel = new Label(); + protected final Arc trackArc = new Arc(); + protected final Arc progressArc = new Arc(); + protected final Rotate rotate = new Rotate(); + protected DoubleBinding radiusBinding; + protected Timeline indeterminateAnimation; + + public ArcProgressIndicatorSkin(T control) { + super(control); + + initComponents(); + + registerListener(); + + updateProgress(); + } + + protected void initComponents() { + T control = getSkinnable(); + + // init the progress label + progressLabel.getStyleClass().add("progress-label"); + progressLabel.setWrapText(true); + progressLabel.graphicProperty().bind(control.graphicProperty()); + progressLabel.textProperty().bind(Bindings.createStringBinding(() -> { + double progress = control.getProgress(); + StringConverter converter = control.getConverter(); + return converter == null ? null : converter.toString(progress); + }, control.progressProperty(), control.converterProperty())); + progressLabel.managedProperty().bind(progressLabel.visibleProperty()); + progressLabel.visibleProperty().bind(control.graphicProperty().isNotNull().or(progressLabel.textProperty().isNotEmpty())); + + // calculate the radius of the circle based on the size of the control + radiusBinding = getRadiusBinding(control); + + // init the track arc + trackArc.getStyleClass().add("track-circle"); + trackArc.setManaged(false); + trackArc.radiusXProperty().bind(radiusBinding); + trackArc.radiusYProperty().bind(radiusBinding); + trackArc.typeProperty().bind(control.trackArcTypeProperty()); + + // init the progress arc + progressArc.getStyleClass().add("progress-arc"); + progressArc.setManaged(false); + progressArc.setLength(360); + progressArc.radiusXProperty().bind(radiusBinding); + progressArc.radiusYProperty().bind(radiusBinding); + progressArc.typeProperty().bind(control.progressArcTypeProperty()); + + getChildren().addAll(trackArc, progressArc, progressLabel); + } + + private void registerListener() { + T control = getSkinnable(); + + registerChangeListener(control.progressProperty(), it -> updateProgress()); + + registerChangeListener(control.visibleProperty(), it -> { + if (control.isVisible() && control.getProgress() < 0.0) { + playAnimation(); + } else { + pauseAnimation(); + } + }); + } + + private void updateProgress() { + T control = getSkinnable(); + double progress = control.getProgress(); + control.pseudoClassStateChanged(PSEUDO_CLASS_COMPLETED, progress == 1.0); + + if (progress < 0.0) { + if (control.isVisible()) { + playAnimation(); + } else { + pauseAnimation(); + } + } else { + stopAnimation(); + progressArc.setLength(getProgressMaxLength() * progress); + } + } + + protected void stopAnimation() { + progressArc.getTransforms().remove(rotate); + if (animationIsRunning()) { + indeterminateAnimation.stop(); + } + } + + private void pauseAnimation() { + if (animationIsRunning()) { + indeterminateAnimation.pause(); + } + } + + private void playAnimation() { + if (indeterminateAnimation == null) { + indeterminateAnimation = initIndeterminateAnimation(); + } + + if (indeterminateAnimation.getStatus() != Animation.Status.RUNNING) { + if (!progressArc.getTransforms().contains(rotate)) { + progressArc.getTransforms().add(rotate); + } + indeterminateAnimation.play(); + } + } + + private boolean animationIsRunning() { + return indeterminateAnimation != null && indeterminateAnimation.getStatus() == Animation.Status.RUNNING; + } + + @Override + protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { + double arcCenterX = computeAcrCenterX(contentX, contentWidth); + double arcCenterY = computeArcCenterY(contentY, contentHeight); + + // set the pivot point for the rotation + rotate.setPivotX(arcCenterX - progressArc.getLayoutX()); + rotate.setPivotY(arcCenterY - progressArc.getLayoutY()); + + // layout the arcs + trackArc.setCenterX(arcCenterX); + trackArc.setCenterY(arcCenterY); + progressArc.setCenterX(arcCenterX); + progressArc.setCenterY(arcCenterY); + trackArc.resize(contentWidth, contentHeight); + progressArc.resize(contentWidth, contentHeight); + + // layout the progress label + double maxStrokeWidth = Math.max(trackArc.getStrokeWidth(), progressArc.getStrokeWidth()); + double diameter = (radiusBinding.get() - maxStrokeWidth) * 2; + + double labelMaxWidth = computeLabelWidth(diameter); + double labelMaxHeight = computeLabelHeight(diameter); + + progressLabel.setMaxWidth(labelMaxWidth); + progressLabel.setMaxHeight(labelMaxHeight); + progressLabel.setPrefWidth(labelMaxWidth); + progressLabel.setPrefHeight(labelMaxHeight); + + double labelWidth = Math.min(progressLabel.prefWidth(diameter), diameter); + double labelHeight = Math.min(progressLabel.prefHeight(labelWidth), diameter); + + double labelX = computeLabelX(arcCenterX, labelWidth); + double labelY = computeLabelY(arcCenterY, labelHeight); + + progressLabel.resizeRelocate(labelX, labelY, labelWidth, labelHeight); + } + + protected double computeLabelWidth(double diameter) { + return diameter; + } + + protected double computeAcrCenterX(double contentX, double contentWidth) { + return contentX + contentWidth / 2; + } + + protected double computeLabelX(double arcCenterX, double labelWidth) { + return arcCenterX - (labelWidth / 2); + } + + /** + * Returns the height of the label. + */ + protected abstract double computeLabelHeight(double diameter); + + /** + * Returns the y-coordinate of the center of the progress arc / track arc. + */ + protected abstract double computeArcCenterY(double contentY, double contentHeight); + + + /** + * Returns the y-coordinate of the label. + */ + protected abstract double computeLabelY(double arcCenterY, double labelHeight); + + /** + * Initializes the animation that is used when the progress is indeterminate. + */ + protected abstract Timeline initIndeterminateAnimation(); + + /** + * Returns a binding that calculates the radius of the circle based on the size of the control. + */ + protected abstract DoubleBinding getRadiusBinding(T control); + + /** + * Returns the maximum length of the progress arc. + */ + protected abstract double getProgressMaxLength(); + +} diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/CircleProgressIndicatorSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/CircleProgressIndicatorSkin.java index bf4f8cfd..aa79b4c0 100644 --- a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/CircleProgressIndicatorSkin.java +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/CircleProgressIndicatorSkin.java @@ -7,136 +7,36 @@ import javafx.animation.Timeline; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; -import javafx.css.PseudoClass; import javafx.geometry.Insets; -import javafx.scene.control.Label; -import javafx.scene.control.SkinBase; -import javafx.scene.shape.Arc; -import javafx.scene.shape.ArcType; -import javafx.scene.shape.Circle; -import javafx.scene.transform.Rotate; import javafx.util.Duration; -import javafx.util.StringConverter; -public class CircleProgressIndicatorSkin extends SkinBase { - - private static final PseudoClass PSEUDO_CLASS_COMPLETED = PseudoClass.getPseudoClass("completed"); - private final Label progressLabel = new Label(); - private final Circle trackCircle = new Circle(); - private final Arc progressArc = new Arc(); - private final DoubleBinding radiusBinding; - private final Rotate rotate = new Rotate(); - private Timeline indeterminateAnimation; +public class CircleProgressIndicatorSkin extends ArcProgressIndicatorSkin { public CircleProgressIndicatorSkin(CircleProgressIndicator control) { super(control); - - // init the progress label - progressLabel.getStyleClass().add("progress-label"); - progressLabel.setWrapText(true); - progressLabel.graphicProperty().bind(control.graphicProperty()); - progressLabel.textProperty().bind(Bindings.createStringBinding(() -> { - double progress = control.getProgress(); - StringConverter converter = control.getConverter(); - return converter == null ? null : converter.toString(progress); - }, control.progressProperty(), control.converterProperty())); - progressLabel.managedProperty().bind(progressLabel.visibleProperty()); - progressLabel.visibleProperty().bind(control.graphicProperty().isNotNull().or(progressLabel.textProperty().isNotEmpty())); - - // calculate the radius of the circle based on the size of the control - radiusBinding = Bindings.createDoubleBinding(() -> { - Insets insets = control.getInsets() != null ? control.getInsets() : Insets.EMPTY; - double totalHorInset = insets.getLeft() + insets.getRight(); - double totalVerInset = insets.getTop() + insets.getBottom(); - double maxInset = Math.max(totalHorInset, totalVerInset); - double maxRadius = Math.max(trackCircle.getStrokeWidth(), progressArc.getStrokeWidth()); - return (Math.min(control.getWidth(), control.getHeight()) - maxInset - maxRadius) / 2; - }, control.widthProperty(), control.heightProperty(), control.insetsProperty(), trackCircle.strokeWidthProperty(), progressArc.strokeWidthProperty()); - - // init the track circle - trackCircle.getStyleClass().add("track-circle"); - trackCircle.setManaged(false); - trackCircle.radiusProperty().bind(radiusBinding); - - // init the progress arc - progressArc.getStyleClass().add("progress-arc"); - progressArc.setManaged(false); - progressArc.setStartAngle(90); - progressArc.setLength(360); - progressArc.radiusXProperty().bind(radiusBinding); - progressArc.radiusYProperty().bind(radiusBinding); - - getChildren().addAll(trackCircle, progressArc, progressLabel); - updateProgress(); - - registerListener(control); } - private void registerListener(CircleProgressIndicator control) { - registerChangeListener(control.progressProperty(), it -> updateProgress()); - registerChangeListener(control.visibleProperty(), it -> { - if (control.isVisible() && control.getProgress() < 0.0) { - playAnimation(); - } else { - pauseAnimation(); - } - }); - registerChangeListener(control.arcTypeProperty(), it -> { - ArcType arcType = control.getArcType(); - //trackArc.setType(arcType); - progressArc.setType(arcType); - }); - } - - private void updateProgress() { - CircleProgressIndicator control = getSkinnable(); - double progress = control.getProgress(); - control.pseudoClassStateChanged(PSEUDO_CLASS_COMPLETED, progress == 1.0); - - if (progress < 0.0) { - if (control.isVisible()) { - playAnimation(); - } else { - pauseAnimation(); - } - } else { - stopAnimation(); - progressArc.setLength(-360 * progress); - } - } - - private void stopAnimation() { - progressArc.getTransforms().remove(rotate); - if (animationIsRunning()) { - indeterminateAnimation.stop(); - } - } + @Override + protected void initComponents() { + super.initComponents(); - private void pauseAnimation() { - if (animationIsRunning()) { - indeterminateAnimation.pause(); - } + trackArc.setLength(360); + progressArc.startAngleProperty().bind(getSkinnable().startAngleProperty()); } - private void playAnimation() { - if (indeterminateAnimation == null) { - initIndeterminateAnimation(); - } - - if (indeterminateAnimation.getStatus() != Animation.Status.RUNNING) { - if (!progressArc.getTransforms().contains(rotate)) { - progressArc.getTransforms().add(rotate); - } - indeterminateAnimation.play(); - } + @Override + protected double getProgressMaxLength() { + return -360; } - private boolean animationIsRunning() { - return indeterminateAnimation != null && indeterminateAnimation.getStatus() == Animation.Status.RUNNING; + @Override + protected double computeLabelHeight(double diameter) { + return diameter; } - private void initIndeterminateAnimation() { - indeterminateAnimation = new Timeline( + @Override + protected Timeline initIndeterminateAnimation() { + Timeline timeline = new Timeline( new KeyFrame(Duration.ZERO, new KeyValue(rotate.angleProperty(), 0), new KeyValue(progressArc.lengthProperty(), 45)), @@ -147,42 +47,30 @@ private void initIndeterminateAnimation() { new KeyValue(rotate.angleProperty(), 360), new KeyValue(progressArc.lengthProperty(), 45)) ); - indeterminateAnimation.setCycleCount(Animation.INDEFINITE); + timeline.setCycleCount(Animation.INDEFINITE); + return timeline; } @Override - protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { - double centerX = contentX + contentWidth / 2; - double centerY = contentY + contentHeight / 2; - - // set the pivot point for the rotation - rotate.setPivotX(centerX - progressArc.getLayoutX()); - rotate.setPivotY(centerY - progressArc.getLayoutY()); - - // layout the arcs - trackCircle.setCenterX(centerX); - trackCircle.setCenterY(centerY); - progressArc.setCenterX(centerX); - progressArc.setCenterY(centerY); - trackCircle.resize(contentWidth, contentHeight); - progressArc.resize(contentWidth, contentHeight); - - // layout the progress label - double maxStrokeWidth = Math.max(trackCircle.getStrokeWidth(), progressArc.getStrokeWidth()); - double diameter = (radiusBinding.get() - maxStrokeWidth) * 2; - - progressLabel.setMaxWidth(diameter); - progressLabel.setMaxHeight(diameter); - progressLabel.setPrefWidth(diameter); - progressLabel.setPrefHeight(diameter); - - double labelWidth = Math.min(progressLabel.prefWidth(diameter), diameter); - double labelHeight = Math.min(progressLabel.prefHeight(labelWidth), diameter); + protected DoubleBinding getRadiusBinding(CircleProgressIndicator control) { + return Bindings.createDoubleBinding(() -> { + Insets insets = control.getInsets() != null ? control.getInsets() : Insets.EMPTY; + double totalHorInset = insets.getLeft() + insets.getRight(); + double totalVerInset = insets.getTop() + insets.getBottom(); + double maxInset = Math.max(totalHorInset, totalVerInset); + double maxRadius = Math.max(trackArc.getStrokeWidth(), progressArc.getStrokeWidth()); + return (Math.min(control.getWidth(), control.getHeight()) - maxInset - maxRadius) / 2; + }, control.widthProperty(), control.heightProperty(), control.insetsProperty(), trackArc.strokeWidthProperty(), progressArc.strokeWidthProperty()); + } - double labelX = centerX - (labelWidth / 2); - double labelY = centerY - (labelHeight / 2); + @Override + protected double computeArcCenterY(double contentY, double contentHeight) { + return contentY + contentHeight / 2; + } - progressLabel.resizeRelocate(labelX, labelY, labelWidth, labelHeight); + @Override + protected double computeLabelY(double arcCenterY, double labelHeight) { + return arcCenterY - (labelHeight / 2); } } diff --git a/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SemiCircleProgressIndicatorSkin.java b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SemiCircleProgressIndicatorSkin.java new file mode 100644 index 00000000..53cd82ae --- /dev/null +++ b/gemsfx/src/main/java/com/dlsc/gemsfx/skins/SemiCircleProgressIndicatorSkin.java @@ -0,0 +1,78 @@ +package com.dlsc.gemsfx.skins; + +import com.dlsc.gemsfx.SemiCircleProgressIndicator; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.DoubleBinding; +import javafx.geometry.Insets; +import javafx.util.Duration; + +public class SemiCircleProgressIndicatorSkin extends ArcProgressIndicatorSkin { + + public SemiCircleProgressIndicatorSkin(SemiCircleProgressIndicator control) { + super(control); + } + + @Override + protected void initComponents() { + super.initComponents(); + + trackArc.setStartAngle(0); + trackArc.setLength(180); + } + + @Override + protected double getProgressMaxLength() { + return -180; + } + + protected void stopAnimation() { + super.stopAnimation(); + progressArc.setStartAngle(180); + } + + @Override + protected double computeLabelHeight(double diameter) { + return diameter / 2; + } + + protected Timeline initIndeterminateAnimation() { + Timeline timeline = new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(progressArc.startAngleProperty(), 180), + new KeyValue(progressArc.lengthProperty(), 0)), + new KeyFrame(Duration.seconds(0.75), + new KeyValue(progressArc.startAngleProperty(), 90), + new KeyValue(progressArc.lengthProperty(), -60)), + new KeyFrame(Duration.seconds(1.5), + new KeyValue(progressArc.startAngleProperty(), 0), + new KeyValue(progressArc.lengthProperty(), 0)) + ); + timeline.setCycleCount(Animation.INDEFINITE); + return timeline; + } + + @Override + protected DoubleBinding getRadiusBinding(SemiCircleProgressIndicator control) { + return Bindings.createDoubleBinding(() -> { + Insets insets = control.getInsets() != null ? control.getInsets() : Insets.EMPTY; + double totalHorInset = insets.getLeft() + insets.getRight(); + double totalVerInset = insets.getTop() + insets.getBottom(); + double maxRadius = Math.max(trackArc.getStrokeWidth(), progressArc.getStrokeWidth()); + return (Math.min(control.getWidth() - totalHorInset - maxRadius, (control.getHeight() - totalVerInset - maxRadius) * 2)) / 2; + }, control.widthProperty(), control.heightProperty(), control.insetsProperty(), trackArc.strokeWidthProperty(), progressArc.strokeWidthProperty()); + } + + @Override + protected double computeArcCenterY(double contentY, double contentHeight) { + return contentY + contentHeight / 2 + radiusBinding.get() / 2; + } + + protected double computeLabelY(double centerY, double labelHeight) { + return centerY - labelHeight; + } + +} diff --git a/gemsfx/src/main/resources/com/dlsc/gemsfx/circle-progress-indicator.css b/gemsfx/src/main/resources/com/dlsc/gemsfx/arc-progress-indicator.css similarity index 59% rename from gemsfx/src/main/resources/com/dlsc/gemsfx/circle-progress-indicator.css rename to gemsfx/src/main/resources/com/dlsc/gemsfx/arc-progress-indicator.css index 88afc634..00a724f2 100644 --- a/gemsfx/src/main/resources/com/dlsc/gemsfx/circle-progress-indicator.css +++ b/gemsfx/src/main/resources/com/dlsc/gemsfx/arc-progress-indicator.css @@ -1,19 +1,19 @@ -.circle-progress-indicator { +.arc-progress-indicator { } -.circle-progress-indicator .track-circle { +.arc-progress-indicator .track-circle { -fx-stroke-width: 3px; -fx-stroke: -fx-box-border; -fx-fill: transparent; } -.circle-progress-indicator .progress-arc { +.arc-progress-indicator .progress-arc { -fx-stroke-width: 3px; -fx-stroke: -fx-accent; -fx-fill: transparent; } -.circle-progress-indicator .progress-label { +.arc-progress-indicator .progress-label { -fx-text-alignment: center; -fx-alignment: center; }