diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXSlider.java b/HMCL/src/main/java/com/jfoenix/controls/JFXSlider.java new file mode 100644 index 000000000..f84ecfbed --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXSlider.java @@ -0,0 +1,180 @@ +/* + * 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.converters.IndicatorPositionConverter; +import com.jfoenix.skins.JFXSliderSkin; +import javafx.beans.binding.StringBinding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.css.*; +import javafx.scene.control.Skin; +import javafx.scene.control.Slider; +import javafx.util.Callback; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// JFXSlider is the material design implementation of a slider. +/// +/// @author Bashir Elias & Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public class JFXSlider extends Slider { + + public JFXSlider() { + super(0, 100, 50); + initialize(); + } + + public JFXSlider(double min, double max, double value) { + super(min, max, value); + initialize(); + } + + @Override + protected Skin createDefaultSkin() { + return new JFXSliderSkin(this); + } + + private void initialize() { + getStyleClass().add(DEFAULT_STYLE_CLASS); + } + + public enum IndicatorPosition { + LEFT, RIGHT + } + + /*************************************************************************** + * * + * Properties * + * * + **************************************************************************/ + + /// String binding factory for the slider value. + /// Sets a custom string for the value text (by default, it shows the value rounded to the nearest whole number). + /// + /// + /// For example, to have the value displayed as a percentage (assuming the slider has a range of (0, 100)): + /// ```java + /// JFXSlider mySlider = ... + /// mySlider.setValueFactory(slider -> + /// Bindings.createStringBinding( + /// () -> ((int) slider.getValue()) + "%", + /// slider.valueProperty() + /// ) + /// ); + /// ``` + /// + /// NOTE: might be replaced later with a call back to create the animated thumb node + /// + /// @param callback a callback to create the string value binding + private ObjectProperty> valueFactory; + + public final ObjectProperty> valueFactoryProperty() { + if (valueFactory == null) { + valueFactory = new SimpleObjectProperty<>(this, "valueFactory"); + } + return valueFactory; + } + + /// @return the current slider value factory + public final Callback getValueFactory() { + return valueFactory == null ? null : valueFactory.get(); + } + + /// sets custom string binding for the slider text value + /// + /// @param valueFactory a callback to create the string value binding + public final void setValueFactory(final Callback valueFactory) { + this.valueFactoryProperty().set(valueFactory); + } + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + /// Initialize the style class to 'jfx-slider'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-slider"; + + /// indicates the position of the slider indicator, can be + /// either LEFT or RIGHT + private StyleableObjectProperty indicatorPosition; + + public StyleableObjectProperty indicatorPositionProperty() { + if (indicatorPosition == null) { + indicatorPosition = new SimpleStyleableObjectProperty<>( + StyleableProperties.INDICATOR_POSITION, + JFXSlider.this, + "indicatorPosition", + IndicatorPosition.LEFT); + } + return this.indicatorPosition; + } + + public IndicatorPosition getIndicatorPosition() { + return indicatorPosition == null ? IndicatorPosition.LEFT : indicatorPosition.get(); + } + + public void setIndicatorPosition(IndicatorPosition pos) { + indicatorPositionProperty().set(pos); + } + + private static final class StyleableProperties { + private static final CssMetaData INDICATOR_POSITION = new CssMetaData<>( + "-jfx-indicator-position", + IndicatorPositionConverter.getInstance(), + IndicatorPosition.LEFT) { + @Override + public boolean isSettable(JFXSlider control) { + return control.indicatorPosition == null || !control.indicatorPosition.isBound(); + } + + @Override + public StyleableProperty getStyleableProperty(JFXSlider control) { + return control.indicatorPositionProperty(); + } + }; + + private static final List> CHILD_STYLEABLES; + + static { + final List> styleables = new ArrayList<>( + Slider.getClassCssMetaData()); + Collections.addAll(styleables, INDICATOR_POSITION); + 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/JFXSliderSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXSliderSkin.java new file mode 100644 index 000000000..c754bac69 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXSliderSkin.java @@ -0,0 +1,291 @@ +/* + * 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.JFXSlider; +import com.jfoenix.controls.JFXSlider.IndicatorPosition; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.property.DoubleProperty; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.css.PseudoClass; +import javafx.geometry.Orientation; +import javafx.scene.Node; +import javafx.scene.chart.NumberAxis; +import javafx.scene.control.Slider; +import javafx.scene.control.skin.SliderSkin; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.Pane; +import javafx.scene.layout.StackPane; +import javafx.scene.text.Text; +import javafx.util.Duration; + +/// # Material Design Slider Skin +/// +/// rework of JFXSliderSkin by extending Java SliderSkin +/// this solves padding and resizing issues +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +public class JFXSliderSkin extends SliderSkin { + + private static final PseudoClass MIN_VALUE = PseudoClass.getPseudoClass("min"); + private static final PseudoClass MAX_VALUE = PseudoClass.getPseudoClass("max"); + + private final Pane mouseHandlerPane = new Pane(); + private final Text sliderValue; + private final StackPane coloredTrack; + private final StackPane thumb; + private final StackPane track; + private final StackPane animatedThumb; + private NumberAxis tickLine; + + private Timeline timeline; + + private double indicatorRotation; + private double horizontalRotation; + private double shifting; + + public JFXSliderSkin(JFXSlider slider) { + super(slider); + + track = (StackPane) getSkinnable().lookup(".track"); + thumb = (StackPane) getSkinnable().lookup(".thumb"); + tickLine = (NumberAxis) getSkinnable().lookup(".axis"); + if (tickLine != null) tickLine.setAnimated(false); + + coloredTrack = new StackPane(); + coloredTrack.getStyleClass().add("colored-track"); + coloredTrack.setMouseTransparent(true); + + sliderValue = new Text(); + sliderValue.getStyleClass().setAll("slider-value"); + + animatedThumb = new StackPane(); + animatedThumb.getStyleClass().add("animated-thumb"); + animatedThumb.getChildren().add(sliderValue); + animatedThumb.setMouseTransparent(true); + animatedThumb.setScaleX(0); + animatedThumb.setScaleY(0); + + thumb.layoutXProperty().addListener(x -> { + if (slider.getOrientation() == Orientation.VERTICAL) initAnimation(Orientation.VERTICAL); + }); + thumb.layoutYProperty().addListener(y -> { + if (slider.getOrientation() == Orientation.HORIZONTAL) initAnimation(Orientation.HORIZONTAL); + }); + + addJFXChildren(); + getChildren().addListener((ListChangeListener) c -> { + while (c.next()) { + if (c.wasAdded()) { + c.getAddedSubList().forEach(added -> { + if (added instanceof NumberAxis) { + tickLine = (NumberAxis) added; + tickLine.setAnimated(false); + } + }); + } + } + }); + registerChangeListener(slider.showTickMarksProperty(), e -> addJFXChildren()); + registerChangeListener(slider.showTickLabelsProperty(), e -> addJFXChildren()); + registerChangeListener(slider.valueFactoryProperty(), obs -> refreshSliderValueBinding()); + + initListeners(); + } + + private void addJFXChildren() { + ObservableList children = getChildren(); + Slider slider = getSkinnable(); + if ((slider.isShowTickMarks() || slider.isShowTickLabels()) && tickLine != null && !children.contains(tickLine)) { + children.add(0, tickLine); + } + if (children.contains(coloredTrack)) return; + children.add(children.indexOf(thumb), coloredTrack); + children.add(children.indexOf(thumb), animatedThumb); + children.add(0, mouseHandlerPane); + } + + private void refreshSliderValueBinding() { + sliderValue.textProperty().unbind(); + if (((JFXSlider) getSkinnable()).getValueFactory() != null) { + sliderValue.textProperty() + .bind(((JFXSlider) getSkinnable()).getValueFactory().call((JFXSlider) getSkinnable())); + } else { + sliderValue.textProperty().bind(Bindings.createStringBinding(() -> { + if (getSkinnable().getLabelFormatter() != null) { + return getSkinnable().getLabelFormatter().toString(getSkinnable().getValue()); + } else { + return String.valueOf(Math.round(getSkinnable().getValue())); + } + }, getSkinnable().valueProperty())); + } + } + + @Override + protected void layoutChildren(double x, double y, double w, double h) { + super.layoutChildren(x, y, w, h); + + if (timeline == null) { + initAnimation(getSkinnable().getOrientation()); + } + + double prefWidth = animatedThumb.prefWidth(-1); + animatedThumb.resize(prefWidth, animatedThumb.prefHeight(prefWidth)); + + boolean horizontal = getSkinnable().getOrientation() == Orientation.HORIZONTAL; + double width, height, layoutX, layoutY; + if (horizontal) { + width = thumb.getLayoutX() - snappedLeftInset(); + height = track.getHeight(); + layoutX = track.getLayoutX(); + layoutY = track.getLayoutY(); + animatedThumb.setLayoutX(thumb.getLayoutX() + thumb.getWidth() / 2 - animatedThumb.getWidth() / 2); + } else { + height = track.getLayoutBounds().getMaxY() + track.getLayoutY() - thumb.getLayoutY() - snappedBottomInset(); + width = track.getWidth(); + layoutX = track.getLayoutX(); + layoutY = thumb.getLayoutY(); + animatedThumb.setLayoutY(thumb.getLayoutY() + thumb.getHeight() / 2 - animatedThumb.getHeight() / 2); + } + + coloredTrack.resizeRelocate(layoutX, layoutY, width, height); + mouseHandlerPane.resizeRelocate(x, y, w, h); + } + + private void initializeVariables() { + shifting = 30 + thumb.getWidth(); + if (getSkinnable().getOrientation() != Orientation.HORIZONTAL) { + horizontalRotation = -90; + } + if (((JFXSlider) getSkinnable()).getIndicatorPosition() != IndicatorPosition.LEFT) { + indicatorRotation = 180; + shifting = -shifting; + } + final double rotationAngle = 45; + sliderValue.setRotate(rotationAngle + indicatorRotation + 3 * horizontalRotation); + animatedThumb.setRotate(-rotationAngle + indicatorRotation + horizontalRotation); + } + + private void initListeners() { + // delegate slider mouse events to track node + mouseHandlerPane.setOnMousePressed(this::delegateToTrack); + mouseHandlerPane.setOnMouseReleased(this::delegateToTrack); + mouseHandlerPane.setOnMouseDragged(this::delegateToTrack); + + // animate value node + track.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> { + timeline.setRate(1); + timeline.play(); + }); + track.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> { + timeline.setRate(-1); + timeline.play(); + }); + thumb.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> { + timeline.setRate(1); + timeline.play(); + }); + thumb.addEventHandler(MouseEvent.MOUSE_RELEASED, (event) -> { + timeline.setRate(-1); + timeline.play(); + }); + + refreshSliderValueBinding(); + updateValueStyleClass(); + + getSkinnable().valueProperty().addListener(observable -> updateValueStyleClass()); + } + + private void delegateToTrack(MouseEvent event) { + if (!event.isConsumed()) { + event.consume(); + track.fireEvent(event); + } + } + + private void updateValueStyleClass() { + getSkinnable().pseudoClassStateChanged(MIN_VALUE, getSkinnable().getMin() == getSkinnable().getValue()); + getSkinnable().pseudoClassStateChanged(MAX_VALUE, getSkinnable().getMax() == getSkinnable().getValue()); + } + + private void initAnimation(Orientation orientation) { + initializeVariables(); + + double thumbPos, thumbNewPos; + DoubleProperty layoutProperty; + + if (orientation == Orientation.HORIZONTAL) { + if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) { + thumbPos = thumb.getLayoutY() - thumb.getHeight(); + thumbNewPos = thumbPos - shifting; + } else { + double height = animatedThumb.prefHeight(animatedThumb.prefWidth(-1)); + thumbPos = thumb.getLayoutY() - height / 2; + thumbNewPos = thumb.getLayoutY() - height - thumb.getHeight(); + } + layoutProperty = animatedThumb.translateYProperty(); + } else { + if (((JFXSlider) getSkinnable()).getIndicatorPosition() == IndicatorPosition.RIGHT) { + thumbPos = thumb.getLayoutX() - thumb.getWidth(); + thumbNewPos = thumbPos - shifting; + } else { + double width = animatedThumb.prefWidth(-1); + thumbPos = thumb.getLayoutX() - width / 2; + thumbNewPos = thumb.getLayoutX() - width - thumb.getWidth(); + } + layoutProperty = animatedThumb.translateXProperty(); + } + + clearAnimation(); + + timeline = new Timeline( + new KeyFrame( + Duration.ZERO, + new KeyValue(animatedThumb.scaleXProperty(), 0, Interpolator.EASE_BOTH), + new KeyValue(animatedThumb.scaleYProperty(), 0, Interpolator.EASE_BOTH), + new KeyValue(layoutProperty, thumbPos, Interpolator.EASE_BOTH)), + new KeyFrame( + Duration.seconds(0.2), + new KeyValue(animatedThumb.scaleXProperty(), 1, Interpolator.EASE_BOTH), + new KeyValue(animatedThumb.scaleYProperty(), 1, Interpolator.EASE_BOTH), + new KeyValue(layoutProperty, thumbNewPos, Interpolator.EASE_BOTH))); + } + + @Override + public void dispose() { + super.dispose(); + clearAnimation(); + } + + private void clearAnimation() { + if (timeline != null) { + timeline.stop(); + timeline.getKeyFrames().clear(); + timeline = null; + } + } +} diff --git a/HMCL/src/main/resources/assets/css/root.css b/HMCL/src/main/resources/assets/css/root.css index 1bb680b8c..54708ea30 100644 --- a/HMCL/src/main/resources/assets/css/root.css +++ b/HMCL/src/main/resources/assets/css/root.css @@ -791,31 +791,39 @@ * * ******************************************************************************/ -.svg-slider .thumb { - -fx-stroke: -monet-on-surface; - -fx-fill: -monet-on-surface; +.jfx-slider .track, +.jfx-slider:vertical .track { + -fx-background-color: -monet-secondary-container; + -fx-background-radius: 5; + -fx-background-insets: 0; + -fx-pref-width: 2px; + -fx-pref-height: 2px; } -.jfx-slider:disabled { - -fx-opacity: 0.4; -} - -/******************************************************* -* * -* For the main demo sliders * -* * -*******************************************************/ - -.jfx-slider { - -jfx-indicator-position: right; -} - -.jfx-slider .track { - -fx-background-color: -monet-on-surface-variant-transparent-38; -} - -.jfx-slider .thumb { +.jfx-slider .thumb, +.jfx-slider:focused .thumb { -fx-background-color: -monet-primary; + -fx-background-radius: 20; + -fx-background-insets: 0; +} + +.jfx-slider .colored-track { + -fx-background-color: -monet-primary; + -fx-background-radius: 5 0 0 5; + -fx-background-insets: 0; +} + +.jfx-slider .slider-value { + -fx-stroke: -monet-inverse-on-surface; + -fx-font-size: 10; +} + +.jfx-slider .animated-thumb { + -fx-pref-width: 30px; + -fx-pref-height: 30px; + -fx-background-color: -monet-inverse-surface; + -fx-background-radius: 50% 50% 50% 0%; + -fx-background-insets: 0; } /*******************************************************************************