diff --git a/HMCL/build.gradle.kts b/HMCL/build.gradle.kts
index 89f52fab0..7718b3969 100644
--- a/HMCL/build.gradle.kts
+++ b/HMCL/build.gradle.kts
@@ -117,10 +117,6 @@ tasks.checkstyleMain {
exclude("**/org/jackhuang/hmcl/ui/image/apng/**")
}
-tasks.compileJava {
- options.compilerArgs.add("--add-exports=java.base/jdk.internal.loader=ALL-UNNAMED")
-}
-
val addOpens = listOf(
"java.base/java.lang",
"java.base/java.lang.reflect",
@@ -128,8 +124,11 @@ val addOpens = listOf(
"javafx.base/com.sun.javafx.binding",
"javafx.base/com.sun.javafx.event",
"javafx.base/com.sun.javafx.runtime",
+ "javafx.base/javafx.beans.property",
"javafx.graphics/javafx.css",
+ "javafx.graphics/javafx.stage",
"javafx.graphics/com.sun.javafx.stage",
+ "javafx.graphics/com.sun.javafx.util",
"javafx.graphics/com.sun.prism",
"javafx.controls/com.sun.javafx.scene.control",
"javafx.controls/com.sun.javafx.scene.control.behavior",
@@ -137,6 +136,10 @@ val addOpens = listOf(
"jdk.attach/sun.tools.attach",
)
+tasks.compileJava {
+ options.compilerArgs.addAll(addOpens.map { "--add-exports=$it=ALL-UNNAMED" })
+}
+
val hmclProperties = buildList {
add("hmcl.version" to project.version.toString())
add("hmcl.add-opens" to addOpens.joinToString(" "))
diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXClippedPane.java b/HMCL/src/main/java/com/jfoenix/controls/JFXClippedPane.java
new file mode 100644
index 000000000..4dc67de30
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/controls/JFXClippedPane.java
@@ -0,0 +1,60 @@
+/*
+ * 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.utils.JFXNodeUtils;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+
+/**
+ * JFXClippedPane is a StackPane that clips its content if exceeding the pane bounds.
+ *
+ * @author Shadi Shaheen
+ * @version 1.0
+ * @since 2018-06-02
+ */
+public class JFXClippedPane extends StackPane {
+
+ private final Region clip = new Region();
+
+ public JFXClippedPane() {
+ super();
+ init();
+ }
+
+ public JFXClippedPane(Node... children) {
+ super(children);
+ init();
+ }
+
+ private void init() {
+ setClip(clip);
+ clip.setBackground(new Background(new BackgroundFill(Color.BLACK, new CornerRadii(2), Insets.EMPTY)));
+ backgroundProperty().addListener(observable -> JFXNodeUtils.updateBackground(getBackground(), clip));
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+ clip.resizeRelocate(0, 0, getWidth(), getHeight());
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXColorPicker.java b/HMCL/src/main/java/com/jfoenix/controls/JFXColorPicker.java
new file mode 100644
index 000000000..18f32ca94
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/controls/JFXColorPicker.java
@@ -0,0 +1,145 @@
+/*
+ * 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.JFXColorPickerSkin;
+import javafx.css.CssMetaData;
+import javafx.css.SimpleStyleableBooleanProperty;
+import javafx.css.Styleable;
+import javafx.css.StyleableBooleanProperty;
+import javafx.css.converter.BooleanConverter;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.Skin;
+import javafx.scene.paint.Color;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * JFXColorPicker is the metrial design implementation of color picker.
+ *
+ * @author Shadi Shaheen
+ * @version 1.0
+ * @since 2016-03-09
+ */
+public class JFXColorPicker extends ColorPicker {
+
+ /**
+ * {@inheritDoc}
+ */
+ public JFXColorPicker() {
+ initialize();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public JFXColorPicker(Color color) {
+ super(color);
+ initialize();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ protected Skin> createDefaultSkin() {
+ return new JFXColorPickerSkin(this);
+ }
+
+ private void initialize() {
+ this.getStyleClass().add(DEFAULT_STYLE_CLASS);
+ }
+
+ /**
+ * Initialize the style class to 'jfx-color-picker'.
+ *
+ * This is the selector class from which CSS can be used to style
+ * this control.
+ */
+ private static final String DEFAULT_STYLE_CLASS = "jfx-color-picker";
+
+ private double[] preDefinedColors = null;
+
+ public double[] getPreDefinedColors() {
+ return preDefinedColors;
+ }
+
+ public void setPreDefinedColors(double[] preDefinedColors) {
+ this.preDefinedColors = preDefinedColors;
+ }
+
+ /**
+ * disable animation on button action
+ */
+ private final StyleableBooleanProperty disableAnimation = new SimpleStyleableBooleanProperty(StyleableProperties.DISABLE_ANIMATION,
+ JFXColorPicker.this,
+ "disableAnimation",
+ false);
+
+ public final StyleableBooleanProperty disableAnimationProperty() {
+ return this.disableAnimation;
+ }
+
+ public final Boolean isDisableAnimation() {
+ return disableAnimation != null && this.disableAnimationProperty().get();
+ }
+
+ public final void setDisableAnimation(final Boolean disabled) {
+ this.disableAnimationProperty().set(disabled);
+ }
+
+ private static final class StyleableProperties {
+
+ private static final CssMetaData DISABLE_ANIMATION =
+ new CssMetaData("-jfx-disable-animation",
+ BooleanConverter.getInstance(), false) {
+ @Override
+ public boolean isSettable(JFXColorPicker control) {
+ return control.disableAnimation == null || !control.disableAnimation.isBound();
+ }
+
+ @Override
+ public StyleableBooleanProperty getStyleableProperty(JFXColorPicker control) {
+ return control.disableAnimationProperty();
+ }
+ };
+
+
+ private static final List> CHILD_STYLEABLES;
+
+ static {
+ final List> styleables =
+ new ArrayList<>(ColorPicker.getClassCssMetaData());
+ Collections.addAll(styleables, DISABLE_ANIMATION);
+ CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
+ }
+ }
+
+ @Override
+ public List> getControlCssMetaData() {
+ return getClassCssMetaData();
+ }
+
+ public static List> getClassCssMetaData() {
+ return StyleableProperties.CHILD_STYLEABLES;
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXRippler.java b/HMCL/src/main/java/com/jfoenix/controls/JFXRippler.java
new file mode 100644
index 000000000..99937c7e5
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/controls/JFXRippler.java
@@ -0,0 +1,800 @@
+/*
+ * 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.RipplerMaskTypeConverter;
+import com.jfoenix.utils.JFXNodeUtils;
+import javafx.animation.*;
+import javafx.beans.DefaultProperty;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.css.*;
+import javafx.css.converter.BooleanConverter;
+import javafx.css.converter.PaintConverter;
+import javafx.css.converter.SizeConverter;
+import javafx.geometry.Bounds;
+import javafx.scene.CacheHint;
+import javafx.scene.Group;
+import javafx.scene.Node;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Pane;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.paint.Paint;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
+import javafx.util.Duration;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * JFXRippler is the material design implementation of a ripple effect.
+ * the ripple effect can be applied to any node in the scene. JFXRippler is
+ * a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
+ *
+ * UPDATE NOTES:
+ * - fireEventProgrammatically(Event) method has been removed as the ripple controller is
+ * the control itself, so you can trigger manual ripple by firing mouse event on the control
+ * instead of JFXRippler
+ *
+ * @author Shadi Shaheen
+ * @version 1.0
+ * @since 2016-03-09
+ */
+@DefaultProperty(value = "control")
+public class JFXRippler extends StackPane {
+ public enum RipplerPos {
+ FRONT, BACK
+ }
+
+ public enum RipplerMask {
+ CIRCLE, RECT, FIT
+ }
+
+ protected RippleGenerator rippler;
+ protected Pane ripplerPane;
+ protected Node control;
+
+ protected static final double RIPPLE_MAX_RADIUS = 300;
+
+ private boolean enabled = true;
+ private boolean forceOverlay = false;
+ private final Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825,
+ 0.3025,
+ 0.0875,
+ 0.9975); //0.1, 0.54, 0.28, 0.95);
+
+ /// creates empty rippler node
+ public JFXRippler() {
+ this(null, RipplerMask.RECT, RipplerPos.FRONT);
+ }
+
+ /// creates a rippler for the specified control
+ public JFXRippler(Node control) {
+ this(control, RipplerMask.RECT, RipplerPos.FRONT);
+ }
+
+ /// creates a rippler for the specified control
+ ///
+ /// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
+ public JFXRippler(Node control, RipplerPos pos) {
+ this(control, RipplerMask.RECT, pos);
+ }
+
+ /// creates a rippler for the specified control and apply the specified mask to it
+ ///
+ /// @param mask can be either rectangle/cricle
+ public JFXRippler(Node control, RipplerMask mask) {
+ this(control, mask, RipplerPos.FRONT);
+ }
+
+ /// creates a rippler for the specified control, mask and position.
+ ///
+ /// @param mask can be either rectangle/cricle
+ /// @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
+ public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
+ initialize();
+
+ setMaskType(mask);
+ setPosition(pos);
+ createRippleUI();
+ setControl(control);
+
+ // listen to control position changed
+ position.addListener(observable -> updateControlPosition());
+
+ setPickOnBounds(false);
+ setCache(true);
+ setCacheHint(CacheHint.SPEED);
+ setCacheShape(true);
+ }
+
+ protected final void createRippleUI() {
+ // create rippler panels
+ rippler = new RippleGenerator();
+ ripplerPane = new StackPane();
+ ripplerPane.setMouseTransparent(true);
+ ripplerPane.getChildren().add(rippler);
+ getChildren().add(ripplerPane);
+ }
+
+ /***************************************************************************
+ * *
+ * Setters / Getters *
+ * *
+ **************************************************************************/
+
+ public void setControl(Node control) {
+ if (control != null) {
+ this.control = control;
+ // position control
+ positionControl(control);
+ // add control listeners to generate / release ripples
+ initControlListeners();
+ }
+ }
+
+ // Override this method to create JFXRippler for a control outside the ripple
+ protected void positionControl(Node control) {
+ if (this.position.get() == RipplerPos.BACK) {
+ getChildren().add(control);
+ } else {
+ getChildren().add(0, control);
+ }
+ }
+
+ protected void updateControlPosition() {
+ if (this.position.get() == RipplerPos.BACK) {
+ ripplerPane.toBack();
+ } else {
+ ripplerPane.toFront();
+ }
+ }
+
+ public Node getControl() {
+ return control;
+ }
+
+ public void setEnabled(boolean enable) {
+ this.enabled = enable;
+ }
+
+ // methods that can be changed by extending the rippler class
+
+ /// generate the clipping mask
+ ///
+ /// @return the mask node
+ protected Node getMask() {
+ double borderWidth = ripplerPane.getBorder() != null ? ripplerPane.getBorder().getInsets().getTop() : 0;
+ Bounds bounds = control.getBoundsInParent();
+ double width = control.getLayoutBounds().getWidth();
+ double height = control.getLayoutBounds().getHeight();
+ double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
+ double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
+ double diffMaxX = Math.abs(control.getBoundsInLocal().getMaxX() - control.getLayoutBounds().getMaxX());
+ double diffMaxY = Math.abs(control.getBoundsInLocal().getMaxY() - control.getLayoutBounds().getMaxY());
+ Node mask;
+ switch (getMaskType()) {
+ case RECT:
+ mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
+ bounds.getMinY() + diffMinY - snappedTopInset(),
+ width - 2 * borderWidth,
+ height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
+ break;
+ case CIRCLE:
+ double radius = Math.min((width / 2) - 2 * borderWidth, (height / 2) - 2 * borderWidth);
+ mask = new Circle((bounds.getMinX() + diffMinX + bounds.getMaxX() - diffMaxX) / 2 - snappedLeftInset(),
+ (bounds.getMinY() + diffMinY + bounds.getMaxY() - diffMaxY) / 2 - snappedTopInset(),
+ radius,
+ Color.BLUE);
+ break;
+ case FIT:
+ mask = new Region();
+ if (control instanceof Shape) {
+ ((Region) mask).setShape((Shape) control);
+ } else if (control instanceof Region) {
+ ((Region) mask).setShape(((Region) control).getShape());
+ JFXNodeUtils.updateBackground(((Region) control).getBackground(), (Region) mask);
+ }
+ mask.resize(width, height);
+ mask.relocate(bounds.getMinX() + diffMinX, bounds.getMinY() + diffMinY);
+ break;
+ default:
+ mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
+ bounds.getMinY() + diffMinY - snappedTopInset(),
+ width - 2 * borderWidth,
+ height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
+ break;
+ }
+ return mask;
+ }
+
+ /**
+ * compute the ripple radius
+ *
+ * @return the ripple radius size
+ */
+ protected double computeRippleRadius() {
+ double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
+ double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
+ return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
+ }
+
+ protected void setOverLayBounds(Rectangle overlay) {
+ overlay.setWidth(control.getLayoutBounds().getWidth());
+ overlay.setHeight(control.getLayoutBounds().getHeight());
+ }
+
+ /**
+ * init mouse listeners on the control
+ */
+ protected void initControlListeners() {
+ // if the control got resized the overlay rect must be rest
+ control.layoutBoundsProperty().addListener(observable -> resetRippler());
+ if (getChildren().contains(control)) {
+ control.boundsInParentProperty().addListener(observable -> resetRippler());
+ }
+ control.addEventHandler(MouseEvent.MOUSE_PRESSED,
+ (event) -> createRipple(event.getX(), event.getY()));
+ // create fade out transition for the ripple
+ control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple());
+ }
+
+ /**
+ * creates Ripple effect
+ */
+ protected void createRipple(double x, double y) {
+ if (!isRipplerDisabled()) {
+ rippler.setGeneratorCenterX(x);
+ rippler.setGeneratorCenterY(y);
+ rippler.createRipple();
+ }
+ }
+
+ protected void releaseRipple() {
+ rippler.releaseRipple();
+ }
+
+ /**
+ * creates Ripple effect in the center of the control
+ *
+ * @return a runnable to release the ripple when needed
+ */
+ public Runnable createManualRipple() {
+ if (!isRipplerDisabled()) {
+ rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
+ rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
+ rippler.createRipple();
+ return () -> {
+ // create fade out transition for the ripple
+ releaseRipple();
+ };
+ }
+ return () -> {
+ };
+ }
+
+ /// show/hide the ripple overlay
+ ///
+ /// @param forceOverlay used to hold the overlay after ripple action
+ public void setOverlayVisible(boolean visible, boolean forceOverlay) {
+ this.forceOverlay = forceOverlay;
+ setOverlayVisible(visible);
+ }
+
+ /// show/hide the ripple overlay
+ /// NOTE: setting overlay visibility to false will reset forceOverlay to false
+ public void setOverlayVisible(boolean visible) {
+ if (visible) {
+ showOverlay();
+ } else {
+ forceOverlay = false;
+ hideOverlay();
+ }
+ }
+
+ /**
+ * this method will be set to private in future versions of JFoenix,
+ * user the method {@link #setOverlayVisible(boolean)}
+ */
+ public void showOverlay() {
+ if (rippler.overlayRect != null) {
+ rippler.overlayRect.outAnimation.stop();
+ }
+ rippler.createOverlay();
+ rippler.overlayRect.inAnimation.play();
+ }
+
+ public void hideOverlay() {
+ if (!forceOverlay) {
+ if (rippler.overlayRect != null) {
+ rippler.overlayRect.inAnimation.stop();
+ }
+ if (rippler.overlayRect != null) {
+ rippler.overlayRect.outAnimation.play();
+ }
+ } else {
+ System.err.println("Ripple Overlay is forced!");
+ }
+ }
+
+ /**
+ * Generates ripples on the screen every 0.3 seconds or whenever
+ * the createRipple method is called. Ripples grow and fade out
+ * over 0.6 seconds
+ */
+ protected final class RippleGenerator extends Group {
+
+ private double generatorCenterX = 0;
+ private double generatorCenterY = 0;
+ private OverLayRipple overlayRect;
+ private final AtomicBoolean generating = new AtomicBoolean(false);
+ private boolean cacheRipplerClip = false;
+ private boolean resetClip = false;
+ private final Queue ripplesQueue = new LinkedList<>();
+
+ RippleGenerator() {
+ // improve in performance, by preventing
+ // redrawing the parent when the ripple effect is triggered
+ this.setManaged(false);
+ this.setCache(true);
+ this.setCacheHint(CacheHint.SPEED);
+ }
+
+ void createRipple() {
+ if (enabled) {
+ if (!generating.getAndSet(true)) {
+ // create overlay once then change its color later
+ createOverlay();
+ if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
+ this.setClip(getMask());
+ }
+ this.resetClip = false;
+
+ // create the ripple effect
+ final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
+ getChildren().add(ripple);
+ ripplesQueue.add(ripple);
+
+ // animate the ripple
+ overlayRect.outAnimation.stop();
+ overlayRect.inAnimation.play();
+ ripple.inAnimation.play();
+ }
+ }
+ }
+
+ private void releaseRipple() {
+ Ripple ripple = ripplesQueue.poll();
+ if (ripple != null) {
+ ripple.inAnimation.stop();
+ ripple.outAnimation = new Timeline(
+ new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
+ , ripple.outKeyValues));
+ ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
+ ripple.outAnimation.play();
+ if (generating.getAndSet(false)) {
+ if (overlayRect != null) {
+ overlayRect.inAnimation.stop();
+ if (!forceOverlay) {
+ overlayRect.outAnimation.play();
+ }
+ }
+ }
+ }
+ }
+
+ void cacheRippleClip(boolean cached) {
+ cacheRipplerClip = cached;
+ }
+
+ void createOverlay() {
+ if (overlayRect == null) {
+ overlayRect = new OverLayRipple();
+ overlayRect.setClip(getMask());
+ getChildren().add(0, overlayRect);
+ overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> {
+ if (ripplerFill.get() instanceof Color) {
+ return new Color(((Color) ripplerFill.get()).getRed(),
+ ((Color) ripplerFill.get()).getGreen(),
+ ((Color) ripplerFill.get()).getBlue(),
+ 0.2);
+ } else {
+ return Color.TRANSPARENT;
+ }
+ }, ripplerFill));
+ }
+ }
+
+ void setGeneratorCenterX(double generatorCenterX) {
+ this.generatorCenterX = generatorCenterX;
+ }
+
+ void setGeneratorCenterY(double generatorCenterY) {
+ this.generatorCenterY = generatorCenterY;
+ }
+
+ private final class OverLayRipple extends Rectangle {
+ // Overlay ripple animations
+ Animation inAnimation = new Timeline(new KeyFrame(Duration.millis(300),
+ new KeyValue(opacityProperty(), 1, Interpolator.EASE_IN)));
+
+ Animation outAnimation = new Timeline(new KeyFrame(Duration.millis(300),
+ new KeyValue(opacityProperty(), 0, Interpolator.EASE_OUT)));
+
+ OverLayRipple() {
+ super();
+ setOverLayBounds(this);
+ this.getStyleClass().add("jfx-rippler-overlay");
+ // update initial position
+ if (JFXRippler.this.getChildrenUnmodifiable().contains(control)) {
+ double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
+ double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
+ Bounds bounds = control.getBoundsInParent();
+ this.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
+ this.setY(bounds.getMinY() + diffMinY - snappedTopInset());
+ }
+ // set initial attributes
+ setOpacity(0);
+ setCache(true);
+ setCacheHint(CacheHint.SPEED);
+ setCacheShape(true);
+ setManaged(false);
+ }
+ }
+
+ private final class Ripple extends Circle {
+
+ KeyValue[] outKeyValues;
+ Animation outAnimation = null;
+ Animation inAnimation = null;
+
+ private Ripple(double centerX, double centerY) {
+ super(centerX,
+ centerY,
+ ripplerRadius.get().doubleValue() == Region.USE_COMPUTED_SIZE ?
+ computeRippleRadius() : ripplerRadius.get().doubleValue(), null);
+ setCache(true);
+ setCacheHint(CacheHint.SPEED);
+ setCacheShape(true);
+ setManaged(false);
+ setSmooth(true);
+
+ KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
+ outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
+
+ inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, rippleInterpolator);
+ inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, rippleInterpolator);
+
+ outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, rippleInterpolator);
+ outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, rippleInterpolator);
+ outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, rippleInterpolator);
+
+ if (isRipplerRecenter()) {
+ double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
+ double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
+ inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
+ Math.signum(dx) * Math.min(Math.abs(dx),
+ this.getRadius() / 2),
+ rippleInterpolator);
+ inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
+ Math.signum(dy) * Math.min(Math.abs(dy),
+ this.getRadius() / 2),
+ rippleInterpolator);
+ }
+ inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
+ new KeyValue(scaleXProperty(),
+ 0,
+ rippleInterpolator),
+ new KeyValue(scaleYProperty(),
+ 0,
+ rippleInterpolator),
+ new KeyValue(translateXProperty(),
+ 0,
+ rippleInterpolator),
+ new KeyValue(translateYProperty(),
+ 0,
+ rippleInterpolator),
+ new KeyValue(opacityProperty(),
+ 1,
+ rippleInterpolator)
+ ), new KeyFrame(Duration.millis(900), inKeyValues));
+
+ setScaleX(0);
+ setScaleY(0);
+ if (ripplerFill.get() instanceof Color) {
+ Color circleColor = new Color(((Color) ripplerFill.get()).getRed(),
+ ((Color) ripplerFill.get()).getGreen(),
+ ((Color) ripplerFill.get()).getBlue(),
+ 0.3);
+ setStroke(circleColor);
+ setFill(circleColor);
+ } else {
+ setStroke(ripplerFill.get());
+ setFill(ripplerFill.get());
+ }
+ }
+ }
+
+ public void clear() {
+ getChildren().clear();
+ rippler.overlayRect = null;
+ generating.set(false);
+ }
+ }
+
+ private void resetOverLay() {
+ if (rippler.overlayRect != null) {
+ rippler.overlayRect.inAnimation.stop();
+ final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
+ rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
+ rippler.overlayRect.outAnimation.play();
+ rippler.overlayRect = null;
+ }
+ }
+
+ private void resetClip() {
+ this.rippler.resetClip = true;
+ }
+
+ protected void resetRippler() {
+ resetOverLay();
+ resetClip();
+ }
+
+ /***************************************************************************
+ * *
+ * Stylesheet Handling *
+ * *
+ **************************************************************************/
+
+ /**
+ * Initialize the style class to 'jfx-rippler'.
+ *
+ * This is the selector class from which CSS can be used to style
+ * this control.
+ */
+ private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";
+
+ private void initialize() {
+ this.getStyleClass().add(DEFAULT_STYLE_CLASS);
+ }
+
+ /**
+ * the ripple recenter property, by default it's false.
+ * if true the ripple effect will show gravitational pull to the center of its control
+ */
+ private final StyleableObjectProperty ripplerRecenter = new SimpleStyleableObjectProperty<>(
+ StyleableProperties.RIPPLER_RECENTER,
+ JFXRippler.this,
+ "ripplerRecenter",
+ false);
+
+ public Boolean isRipplerRecenter() {
+ return ripplerRecenter != null && ripplerRecenter.get();
+ }
+
+ public StyleableObjectProperty ripplerRecenterProperty() {
+ return this.ripplerRecenter;
+ }
+
+ public void setRipplerRecenter(Boolean radius) {
+ this.ripplerRecenter.set(radius);
+ }
+
+ /**
+ * the ripple radius size, by default it will be automatically computed.
+ */
+ private final StyleableObjectProperty ripplerRadius = new SimpleStyleableObjectProperty<>(
+ StyleableProperties.RIPPLER_RADIUS,
+ JFXRippler.this,
+ "ripplerRadius",
+ Region.USE_COMPUTED_SIZE);
+
+ public Number getRipplerRadius() {
+ return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
+ }
+
+ public StyleableObjectProperty ripplerRadiusProperty() {
+ return this.ripplerRadius;
+ }
+
+ public void setRipplerRadius(Number radius) {
+ this.ripplerRadius.set(radius);
+ }
+
+ /**
+ * the default color of the ripple effect
+ */
+ private final StyleableObjectProperty ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
+ JFXRippler.this,
+ "ripplerFill",
+ Color.rgb(0,
+ 200,
+ 255));
+
+ public Paint getRipplerFill() {
+ return ripplerFill == null ? Color.rgb(0, 200, 255) : ripplerFill.get();
+ }
+
+ public StyleableObjectProperty ripplerFillProperty() {
+ return this.ripplerFill;
+ }
+
+ public void setRipplerFill(Paint color) {
+ this.ripplerFill.set(color);
+ }
+
+ /// mask property used for clipping the rippler.
+ /// can be either CIRCLE/RECT
+ private final StyleableObjectProperty maskType = new SimpleStyleableObjectProperty<>(
+ StyleableProperties.MASK_TYPE,
+ JFXRippler.this,
+ "maskType",
+ RipplerMask.RECT);
+
+ public RipplerMask getMaskType() {
+ return maskType == null ? RipplerMask.RECT : maskType.get();
+ }
+
+ public StyleableObjectProperty maskTypeProperty() {
+ return this.maskType;
+ }
+
+ public void setMaskType(RipplerMask type) {
+ this.maskType.set(type);
+ }
+
+ /**
+ * the ripple disable, by default it's false.
+ * if true the ripple effect will be hidden
+ */
+ private final StyleableBooleanProperty ripplerDisabled = new SimpleStyleableBooleanProperty(
+ StyleableProperties.RIPPLER_DISABLED,
+ JFXRippler.this,
+ "ripplerDisabled",
+ false);
+
+ public Boolean isRipplerDisabled() {
+ return ripplerDisabled != null && ripplerDisabled.get();
+ }
+
+ public StyleableBooleanProperty ripplerDisabledProperty() {
+ return this.ripplerDisabled;
+ }
+
+ public void setRipplerDisabled(Boolean disabled) {
+ this.ripplerDisabled.set(disabled);
+ }
+
+
+ /**
+ * indicates whether the ripple effect is infront of or behind the node
+ */
+ protected ObjectProperty position = new SimpleObjectProperty<>();
+
+ public void setPosition(RipplerPos pos) {
+ this.position.set(pos);
+ }
+
+ public RipplerPos getPosition() {
+ return position == null ? RipplerPos.FRONT : position.get();
+ }
+
+ public ObjectProperty positionProperty() {
+ return this.position;
+ }
+
+ private static final class StyleableProperties {
+ private static final CssMetaData RIPPLER_RECENTER =
+ new CssMetaData<>("-jfx-rippler-recenter",
+ BooleanConverter.getInstance(), false) {
+ @Override
+ public boolean isSettable(JFXRippler control) {
+ return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(JFXRippler control) {
+ return control.ripplerRecenterProperty();
+ }
+ };
+ private static final CssMetaData RIPPLER_DISABLED =
+ new CssMetaData<>("-jfx-rippler-disabled",
+ BooleanConverter.getInstance(), false) {
+ @Override
+ public boolean isSettable(JFXRippler control) {
+ return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(JFXRippler control) {
+ return control.ripplerDisabledProperty();
+ }
+ };
+ private static final CssMetaData RIPPLER_FILL =
+ new CssMetaData<>("-jfx-rippler-fill",
+ PaintConverter.getInstance(), Color.rgb(0, 200, 255)) {
+ @Override
+ public boolean isSettable(JFXRippler control) {
+ return control.ripplerFill == null || !control.ripplerFill.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(JFXRippler control) {
+ return control.ripplerFillProperty();
+ }
+ };
+ private static final CssMetaData RIPPLER_RADIUS =
+ new CssMetaData<>("-jfx-rippler-radius",
+ SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
+ @Override
+ public boolean isSettable(JFXRippler control) {
+ return control.ripplerRadius == null || !control.ripplerRadius.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(JFXRippler control) {
+ return control.ripplerRadiusProperty();
+ }
+ };
+ private static final CssMetaData MASK_TYPE =
+ new CssMetaData<>("-jfx-mask-type",
+ RipplerMaskTypeConverter.getInstance(), RipplerMask.RECT) {
+ @Override
+ public boolean isSettable(JFXRippler control) {
+ return control.maskType == null || !control.maskType.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(JFXRippler control) {
+ return control.maskTypeProperty();
+ }
+ };
+
+ private static final List> STYLEABLES;
+
+ static {
+ final List> styleables =
+ new ArrayList<>(StackPane.getClassCssMetaData());
+ Collections.addAll(styleables,
+ RIPPLER_RECENTER,
+ RIPPLER_RADIUS,
+ RIPPLER_FILL,
+ MASK_TYPE,
+ RIPPLER_DISABLED
+ );
+ STYLEABLES = Collections.unmodifiableList(styleables);
+ }
+ }
+
+ @Override
+ public List> getCssMetaData() {
+ return getClassCssMetaData();
+ }
+
+ public static List> getClassCssMetaData() {
+ return StyleableProperties.STYLEABLES;
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/controls/behavior/JFXGenericPickerBehavior.java b/HMCL/src/main/java/com/jfoenix/controls/behavior/JFXGenericPickerBehavior.java
new file mode 100644
index 000000000..ca7b67a41
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/controls/behavior/JFXGenericPickerBehavior.java
@@ -0,0 +1,46 @@
+/*
+ * 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.behavior;
+
+import com.sun.javafx.scene.control.behavior.ComboBoxBaseBehavior;
+import javafx.scene.control.ComboBoxBase;
+import javafx.scene.control.PopupControl;
+
+/**
+ * @author Shadi Shaheen
+ * @version 2.0
+ * @since 2017-10-05
+ */
+public class JFXGenericPickerBehavior extends ComboBoxBaseBehavior {
+
+ public JFXGenericPickerBehavior(ComboBoxBase var1) {
+ super(var1);
+ }
+
+ public void onAutoHide(PopupControl var1) {
+ if (!var1.isShowing() && this.getNode().isShowing()) {
+ this.getNode().hide();
+ }
+ if (!this.getNode().isShowing()) {
+ super.onAutoHide(var1);
+ }
+ }
+
+}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXColorPalette.java b/HMCL/src/main/java/com/jfoenix/skins/JFXColorPalette.java
new file mode 100644
index 000000000..b27384d3d
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXColorPalette.java
@@ -0,0 +1,629 @@
+/*
+ * 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.JFXButton;
+import com.jfoenix.controls.JFXColorPicker;
+import com.jfoenix.utils.JFXNodeUtils;
+import javafx.collections.FXCollections;
+import javafx.collections.ListChangeListener.Change;
+import javafx.collections.ObservableList;
+import javafx.event.ActionEvent;
+import javafx.event.Event;
+import javafx.geometry.Bounds;
+import javafx.geometry.Insets;
+import javafx.geometry.NodeOrientation;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.Label;
+import javafx.scene.control.PopupControl;
+import javafx.scene.control.Tooltip;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseButton;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.StrokeType;
+
+import java.util.List;
+
+import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
+
+/**
+ * @author Shadi Shaheen FUTURE WORK: this UI will get re-designed to match material design guidlines
+ */
+final class JFXColorPalette extends Region {
+
+ private static final int SQUARE_SIZE = 15;
+
+ // package protected for testing purposes
+ JFXColorGrid colorPickerGrid;
+ final JFXButton customColorLink = new JFXButton(i18n("color.custom"));
+ JFXCustomColorPickerDialog customColorDialog = null;
+
+ private final JFXColorPicker colorPicker;
+ private final GridPane customColorGrid = new GridPane();
+ private final Label customColorLabel = new Label(i18n("color.recent"));
+
+ private PopupControl popupControl;
+ private ColorSquare focusedSquare;
+
+ private Color mouseDragColor = null;
+ private boolean dragDetected = false;
+
+ private final ColorSquare hoverSquare = new ColorSquare();
+
+ public JFXColorPalette(final JFXColorPicker colorPicker) {
+ getStyleClass().add("color-palette-region");
+ this.colorPicker = colorPicker;
+ colorPickerGrid = new JFXColorGrid();
+ colorPickerGrid.getChildren().get(0).requestFocus();
+ customColorLabel.setAlignment(Pos.CENTER_LEFT);
+ customColorLink.setPrefWidth(colorPickerGrid.prefWidth(-1));
+ customColorLink.setAlignment(Pos.CENTER);
+ customColorLink.setFocusTraversable(true);
+ customColorLink.setOnAction(ev -> {
+ if (customColorDialog == null) {
+ customColorDialog = new JFXCustomColorPickerDialog(popupControl);
+ customColorDialog.customColorProperty().addListener((ov, t1, t2) -> {
+ colorPicker.setValue(customColorDialog.customColorProperty().get());
+ });
+ customColorDialog.setOnSave(() -> {
+ Color customColor = customColorDialog.customColorProperty().get();
+ buildCustomColors();
+ colorPicker.getCustomColors().add(customColor);
+ updateSelection(customColor);
+ Event.fireEvent(colorPicker, new ActionEvent());
+ colorPicker.hide();
+ });
+ }
+ customColorDialog.setCurrentColor(colorPicker.valueProperty().get());
+ if (popupControl != null) {
+ popupControl.setAutoHide(false);
+ }
+ customColorDialog.show();
+ customColorDialog.setOnHidden(event -> {
+ if (popupControl != null) {
+ popupControl.setAutoHide(true);
+ }
+ });
+ });
+
+ initNavigation();
+ customColorGrid.getStyleClass().add("color-picker-grid");
+ customColorGrid.setVisible(false);
+
+ buildCustomColors();
+
+ colorPicker.getCustomColors().addListener((Change extends Color> change) -> buildCustomColors());
+ VBox paletteBox = new VBox();
+ paletteBox.getStyleClass().add("color-palette");
+ paletteBox.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
+ paletteBox.setBorder(new Border(new BorderStroke(Color.valueOf("#9E9E9E"),
+ BorderStrokeStyle.SOLID,
+ CornerRadii.EMPTY,
+ BorderWidths.DEFAULT)));
+ paletteBox.getChildren().addAll(colorPickerGrid);
+ if (colorPicker.getPreDefinedColors() == null) {
+ paletteBox.getChildren().addAll(customColorLabel, customColorGrid, customColorLink);
+ }
+
+ hoverSquare.setMouseTransparent(true);
+ hoverSquare.getStyleClass().addAll("hover-square");
+ setFocusedSquare(null);
+
+ getChildren().addAll(paletteBox, hoverSquare);
+ }
+
+ private void setFocusedSquare(ColorSquare square) {
+ hoverSquare.setVisible(square != null);
+
+ if (square == focusedSquare) {
+ return;
+ }
+ focusedSquare = square;
+
+ hoverSquare.setVisible(focusedSquare != null);
+ if (focusedSquare == null) {
+ return;
+ }
+
+ if (!focusedSquare.isFocused()) {
+ focusedSquare.requestFocus();
+ }
+
+ hoverSquare.rectangle.setFill(focusedSquare.rectangle.getFill());
+
+ Bounds b = square.localToScene(square.getLayoutBounds());
+
+ double x = b.getMinX();
+ double y = b.getMinY();
+
+ double xAdjust;
+ double scaleAdjust = hoverSquare.getScaleX() == 1.0 ? 0 : hoverSquare.getWidth() / 4.0;
+
+ if (colorPicker.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
+ x = focusedSquare.getLayoutX();
+ xAdjust = -focusedSquare.getWidth() + scaleAdjust;
+ } else {
+ xAdjust = focusedSquare.getWidth() / 2.0 + scaleAdjust;
+ }
+
+ hoverSquare.setLayoutX(snapPositionX(x) - xAdjust);
+ hoverSquare.setLayoutY(snapPositionY(y) - focusedSquare.getHeight() / 2.0 + (hoverSquare.getScaleY() == 1.0 ? 0 : focusedSquare.getHeight() / 4.0));
+ }
+
+ private void buildCustomColors() {
+ final ObservableList customColors = colorPicker.getCustomColors();
+ customColorGrid.getChildren().clear();
+ if (customColors.isEmpty()) {
+ customColorLabel.setVisible(false);
+ customColorLabel.setManaged(false);
+ customColorGrid.setVisible(false);
+ customColorGrid.setManaged(false);
+ return;
+ } else {
+ customColorLabel.setVisible(true);
+ customColorLabel.setManaged(true);
+ customColorGrid.setVisible(true);
+ customColorGrid.setManaged(true);
+ }
+
+ int customColumnIndex = 0;
+ int customRowIndex = 0;
+ int remainingSquares = customColors.size() % NUM_OF_COLUMNS;
+ int numEmpty = (remainingSquares == 0) ? 0 : NUM_OF_COLUMNS - remainingSquares;
+
+ for (int i = 0; i < customColors.size(); i++) {
+ Color c = customColors.get(i);
+ ColorSquare square = new ColorSquare(c, i, true);
+ customColorGrid.add(square, customColumnIndex, customRowIndex);
+ customColumnIndex++;
+ if (customColumnIndex == NUM_OF_COLUMNS) {
+ customColumnIndex = 0;
+ customRowIndex++;
+ }
+ }
+ for (int i = 0; i < numEmpty; i++) {
+ ColorSquare emptySquare = new ColorSquare();
+ customColorGrid.add(emptySquare, customColumnIndex, customRowIndex);
+ customColumnIndex++;
+ }
+ requestLayout();
+ }
+
+ private void initNavigation() {
+ setOnKeyPressed(ke -> {
+ switch (ke.getCode()) {
+ case SPACE:
+ case ENTER:
+ // select the focused color
+ if (focusedSquare != null) {
+ focusedSquare.selectColor(ke);
+ }
+ ke.consume();
+ break;
+ default: // no-op
+ }
+ });
+ }
+
+ public void setPopupControl(PopupControl pc) {
+ this.popupControl = pc;
+ }
+
+ public JFXColorGrid getColorGrid() {
+ return colorPickerGrid;
+ }
+
+ public boolean isCustomColorDialogShowing() {
+ return customColorDialog != null && customColorDialog.isVisible();
+ }
+
+ class ColorSquare extends StackPane {
+ Rectangle rectangle;
+ boolean isEmpty;
+
+ public ColorSquare() {
+ this(null, -1, false);
+ }
+
+ public ColorSquare(Color color, int index) {
+ this(color, index, false);
+ }
+
+ public ColorSquare(Color color, int index, boolean isCustom) {
+ // Add style class to handle selected color square
+ getStyleClass().add("color-square");
+ if (color != null) {
+ setFocusTraversable(true);
+ focusedProperty().addListener((s, ov, nv) -> setFocusedSquare(nv ? this : null));
+ addEventHandler(MouseEvent.MOUSE_ENTERED, event -> setFocusedSquare(ColorSquare.this));
+ addEventHandler(MouseEvent.MOUSE_EXITED, event -> setFocusedSquare(null));
+ addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
+ if (!dragDetected && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
+ if (!isEmpty) {
+ Color fill = (Color) rectangle.getFill();
+ colorPicker.setValue(fill);
+ colorPicker.fireEvent(new ActionEvent());
+ updateSelection(fill);
+ event.consume();
+ }
+ colorPicker.hide();
+ }
+ });
+ }
+ rectangle = new Rectangle(SQUARE_SIZE, SQUARE_SIZE);
+ if (color == null) {
+ rectangle.setFill(Color.WHITE);
+ isEmpty = true;
+ } else {
+ rectangle.setFill(color);
+ }
+
+ rectangle.setStrokeType(StrokeType.INSIDE);
+
+ String tooltipStr = JFXNodeUtils.colorToHex(color);
+ Tooltip.install(this, new Tooltip((tooltipStr == null) ? "" : tooltipStr));
+
+ rectangle.getStyleClass().add("color-rect");
+ getChildren().add(rectangle);
+ }
+
+ public void selectColor(KeyEvent event) {
+ if (rectangle.getFill() != null) {
+ if (rectangle.getFill() instanceof Color) {
+ colorPicker.setValue((Color) rectangle.getFill());
+ colorPicker.fireEvent(new ActionEvent());
+ }
+ event.consume();
+ }
+ colorPicker.hide();
+ }
+ }
+
+ // The skin can update selection if colorpicker value changes..
+ public void updateSelection(Color color) {
+ setFocusedSquare(null);
+
+ for (ColorSquare c : colorPickerGrid.getSquares()) {
+ if (c.rectangle.getFill().equals(color)) {
+ setFocusedSquare(c);
+ return;
+ }
+ }
+ // check custom colors
+ for (Node n : customColorGrid.getChildren()) {
+ ColorSquare c = (ColorSquare) n;
+ if (c.rectangle.getFill().equals(color)) {
+ setFocusedSquare(c);
+ return;
+ }
+ }
+ }
+
+ class JFXColorGrid extends GridPane {
+
+ private final List squares;
+ final int NUM_OF_COLORS;
+ final int NUM_OF_ROWS;
+
+ public JFXColorGrid() {
+ getStyleClass().add("color-picker-grid");
+ setId("ColorCustomizerColorGrid");
+ int columnIndex = 0;
+ int rowIndex = 0;
+ squares = FXCollections.observableArrayList();
+ double[] limitedColors = colorPicker.getPreDefinedColors();
+ limitedColors = limitedColors == null ? RAW_VALUES : limitedColors;
+ NUM_OF_COLORS = limitedColors.length / 3;
+ NUM_OF_ROWS = (int) Math.ceil((double) NUM_OF_COLORS / (double) NUM_OF_COLUMNS);
+ final int numColors = limitedColors.length / 3;
+ Color[] colors = new Color[numColors];
+ for (int i = 0; i < numColors; i++) {
+ colors[i] = new Color(limitedColors[i * 3] / 255,
+ limitedColors[(i * 3) + 1] / 255, limitedColors[(i * 3) + 2] / 255,
+ 1.0);
+ ColorSquare cs = new ColorSquare(colors[i], i);
+ squares.add(cs);
+ }
+
+ for (ColorSquare square : squares) {
+ add(square, columnIndex, rowIndex);
+ columnIndex++;
+ if (columnIndex == NUM_OF_COLUMNS) {
+ columnIndex = 0;
+ rowIndex++;
+ }
+ }
+ setOnMouseDragged(t -> {
+ if (!dragDetected) {
+ dragDetected = true;
+ mouseDragColor = colorPicker.getValue();
+ }
+ int xIndex = clamp(0,
+ (int) t.getX() / (SQUARE_SIZE + 1), NUM_OF_COLUMNS - 1);
+ int yIndex = clamp(0,
+ (int) t.getY() / (SQUARE_SIZE + 1), NUM_OF_ROWS - 1);
+ int index = xIndex + yIndex * NUM_OF_COLUMNS;
+ colorPicker.setValue((Color) squares.get(index).rectangle.getFill());
+ updateSelection(colorPicker.getValue());
+ });
+ addEventHandler(MouseEvent.MOUSE_RELEASED, t -> {
+ if (colorPickerGrid.getBoundsInLocal().contains(t.getX(), t.getY())) {
+ updateSelection(colorPicker.getValue());
+ colorPicker.fireEvent(new ActionEvent());
+ colorPicker.hide();
+ } else {
+ // restore color as mouse release happened outside the grid.
+ if (mouseDragColor != null) {
+ colorPicker.setValue(mouseDragColor);
+ updateSelection(mouseDragColor);
+ }
+ }
+ dragDetected = false;
+ });
+ }
+
+ public List getSquares() {
+ return squares;
+ }
+
+ @Override
+ protected double computePrefWidth(double height) {
+ return (SQUARE_SIZE + 1) * NUM_OF_COLUMNS;
+ }
+
+ @Override
+ protected double computePrefHeight(double width) {
+ return (SQUARE_SIZE + 1) * NUM_OF_ROWS;
+ }
+ }
+
+ private static final int NUM_OF_COLUMNS = 10;
+ private static final double[] RAW_VALUES = {
+ // WARNING: always make sure the number of colors is a divisable by NUM_OF_COLUMNS
+ 250, 250, 250, // first row
+ 245, 245, 245,
+ 238, 238, 238,
+ 224, 224, 224,
+ 189, 189, 189,
+ 158, 158, 158,
+ 117, 117, 117,
+ 97, 97, 97,
+ 66, 66, 66,
+ 33, 33, 33,
+ // second row
+ 236, 239, 241,
+ 207, 216, 220,
+ 176, 190, 197,
+ 144, 164, 174,
+ 120, 144, 156,
+ 96, 125, 139,
+ 84, 110, 122,
+ 69, 90, 100,
+ 55, 71, 79,
+ 38, 50, 56,
+ // third row
+ 255, 235, 238,
+ 255, 205, 210,
+ 239, 154, 154,
+ 229, 115, 115,
+ 239, 83, 80,
+ 244, 67, 54,
+ 229, 57, 53,
+ 211, 47, 47,
+ 198, 40, 40,
+ 183, 28, 28,
+ // forth row
+ 252, 228, 236,
+ 248, 187, 208,
+ 244, 143, 177,
+ 240, 98, 146,
+ 236, 64, 122,
+ 233, 30, 99,
+ 216, 27, 96,
+ 194, 24, 91,
+ 173, 20, 87,
+ 136, 14, 79,
+ // fifth row
+ 243, 229, 245,
+ 225, 190, 231,
+ 206, 147, 216,
+ 186, 104, 200,
+ 171, 71, 188,
+ 156, 39, 176,
+ 142, 36, 170,
+ 123, 31, 162,
+ 106, 27, 154,
+ 74, 20, 140,
+ // sixth row
+ 237, 231, 246,
+ 209, 196, 233,
+ 179, 157, 219,
+ 149, 117, 205,
+ 126, 87, 194,
+ 103, 58, 183,
+ 94, 53, 177,
+ 81, 45, 168,
+ 69, 39, 160,
+ 49, 27, 146,
+ // seventh row
+ 232, 234, 246,
+ 197, 202, 233,
+ 159, 168, 218,
+ 121, 134, 203,
+ 92, 107, 192,
+ 63, 81, 181,
+ 57, 73, 171,
+ 48, 63, 159,
+ 40, 53, 147,
+ 26, 35, 126,
+ // eigth row
+ 227, 242, 253,
+ 187, 222, 251,
+ 144, 202, 249,
+ 100, 181, 246,
+ 66, 165, 245,
+ 33, 150, 243,
+ 30, 136, 229,
+ 25, 118, 210,
+ 21, 101, 192,
+ 13, 71, 161,
+ // ninth row
+ 225, 245, 254,
+ 179, 229, 252,
+ 129, 212, 250,
+ 79, 195, 247,
+ 41, 182, 246,
+ 3, 169, 244,
+ 3, 155, 229,
+ 2, 136, 209,
+ 2, 119, 189,
+ 1, 87, 155,
+ // tenth row
+ 224, 247, 250,
+ 178, 235, 242,
+ 128, 222, 234,
+ 77, 208, 225,
+ 38, 198, 218,
+ 0, 188, 212,
+ 0, 172, 193,
+ 0, 151, 167,
+ 0, 131, 143,
+ 0, 96, 100,
+ // eleventh row
+ 224, 242, 241,
+ 178, 223, 219,
+ 128, 203, 196,
+ 77, 182, 172,
+ 38, 166, 154,
+ 0, 150, 136,
+ 0, 137, 123,
+ 0, 121, 107,
+ 0, 105, 92,
+ 0, 77, 64,
+ // twelfth row
+ 232, 245, 233,
+ 200, 230, 201,
+ 165, 214, 167,
+ 129, 199, 132,
+ 102, 187, 106,
+ 76, 175, 80,
+ 67, 160, 71,
+ 56, 142, 60,
+ 46, 125, 50,
+ 27, 94, 32,
+
+ // thirteenth row
+ 241, 248, 233,
+ 220, 237, 200,
+ 197, 225, 165,
+ 174, 213, 129,
+ 156, 204, 101,
+ 139, 195, 74,
+ 124, 179, 66,
+ 104, 159, 56,
+ 85, 139, 47,
+ 51, 105, 30,
+ // fourteenth row
+ 249, 251, 231,
+ 240, 244, 195,
+ 230, 238, 156,
+ 220, 231, 117,
+ 212, 225, 87,
+ 205, 220, 57,
+ 192, 202, 51,
+ 175, 180, 43,
+ 158, 157, 36,
+ 130, 119, 23,
+
+ // fifteenth row
+ 255, 253, 231,
+ 255, 249, 196,
+ 255, 245, 157,
+ 255, 241, 118,
+ 255, 238, 88,
+ 255, 235, 59,
+ 253, 216, 53,
+ 251, 192, 45,
+ 249, 168, 37,
+ 245, 127, 23,
+
+ // sixteenth row
+ 255, 248, 225,
+ 255, 236, 179,
+ 255, 224, 130,
+ 255, 213, 79,
+ 255, 202, 40,
+ 255, 193, 7,
+ 255, 179, 0,
+ 255, 160, 0,
+ 255, 143, 0,
+ 255, 111, 0,
+
+ // seventeenth row
+ 255, 243, 224,
+ 255, 224, 178,
+ 255, 204, 128,
+ 255, 183, 77,
+ 255, 167, 38,
+ 255, 152, 0,
+ 251, 140, 0,
+ 245, 124, 0,
+ 239, 108, 0,
+ 230, 81, 0,
+
+ // eighteenth row
+ 251, 233, 231,
+ 255, 204, 188,
+ 255, 171, 145,
+ 255, 138, 101,
+ 255, 112, 67,
+ 255, 87, 34,
+ 244, 81, 30,
+ 230, 74, 25,
+ 216, 67, 21,
+ 191, 54, 12,
+
+ // nineteenth row
+ 239, 235, 233,
+ 215, 204, 200,
+ 188, 170, 164,
+ 161, 136, 127,
+ 141, 110, 99,
+ 121, 85, 72,
+ 109, 76, 65,
+ 93, 64, 55,
+ 78, 52, 46,
+ 62, 39, 35,
+ };
+
+ private static int clamp(int min, int value, int max) {
+ if (value < min) {
+ return min;
+ }
+ if (value > max) {
+ return max;
+ }
+ return value;
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXColorPickerSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXColorPickerSkin.java
new file mode 100644
index 000000000..d814d99bc
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXColorPickerSkin.java
@@ -0,0 +1,251 @@
+/*
+ * 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.JFXClippedPane;
+import com.jfoenix.controls.JFXColorPicker;
+import com.jfoenix.controls.JFXRippler;
+import com.jfoenix.effects.JFXDepthManager;
+import com.jfoenix.utils.JFXNodeUtils;
+import javafx.animation.Interpolator;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.css.*;
+import javafx.css.converter.BooleanConverter;
+import javafx.geometry.Insets;
+import javafx.scene.Node;
+import javafx.scene.control.ColorPicker;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextField;
+import javafx.scene.control.skin.ComboBoxPopupControl;
+import javafx.scene.layout.Background;
+import javafx.scene.layout.BackgroundFill;
+import javafx.scene.layout.CornerRadii;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Shadi Shaheen
+ */
+public final class JFXColorPickerSkin extends JFXGenericPickerSkin {
+
+ private final Label displayNode;
+ private final JFXClippedPane colorBox;
+ private JFXColorPalette popupContent;
+ StyleableBooleanProperty colorLabelVisible = new SimpleStyleableBooleanProperty(StyleableProperties.COLOR_LABEL_VISIBLE,
+ JFXColorPickerSkin.this,
+ "colorLabelVisible",
+ true);
+
+ public JFXColorPickerSkin(final ColorPicker colorPicker) {
+ super(colorPicker);
+
+ // create displayNode
+ displayNode = new Label("");
+ displayNode.getStyleClass().add("color-label");
+ displayNode.setMouseTransparent(true);
+
+ // label graphic
+ colorBox = new JFXClippedPane(displayNode);
+ colorBox.getStyleClass().add("color-box");
+ colorBox.setManaged(false);
+ initColor();
+ final JFXRippler rippler = new JFXRippler(colorBox, JFXRippler.RipplerMask.FIT);
+ rippler.ripplerFillProperty().bind(displayNode.textFillProperty());
+ getChildren().setAll(rippler);
+ JFXDepthManager.setDepth(getSkinnable(), 1);
+ getSkinnable().setPickOnBounds(false);
+
+ colorPicker.focusedProperty().addListener(observable -> {
+ if (colorPicker.isFocused()) {
+ if (!getSkinnable().isPressed()) {
+ rippler.setOverlayVisible(true);
+ }
+ } else {
+ rippler.setOverlayVisible(false);
+ }
+ });
+
+ // add listeners
+ registerChangeListener(colorPicker.valueProperty(), obs -> updateColor());
+
+ colorLabelVisible.addListener(invalidate -> {
+ if (colorLabelVisible.get()) {
+ displayNode.setText(JFXNodeUtils.colorToHex(getSkinnable().getValue()));
+ } else {
+ displayNode.setText("");
+ }
+ });
+ }
+
+ @Override
+ protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
+ double width = 100;
+ String displayNodeText = displayNode.getText();
+ displayNode.setText("#DDDDDD");
+ width = Math.max(width, super.computePrefWidth(height, topInset, rightInset, bottomInset, leftInset));
+ displayNode.setText(displayNodeText);
+ return width + rightInset + leftInset;
+ }
+
+ @Override
+ protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
+ if (colorBox == null) {
+ reflectUpdateDisplayArea();
+ }
+ return topInset + colorBox.prefHeight(width) + bottomInset;
+ }
+
+ @Override
+ protected void layoutChildren(double x, double y, double w, double h) {
+ super.layoutChildren(x, y, w, h);
+ double hInsets = snappedLeftInset() + snappedRightInset();
+ double vInsets = snappedTopInset() + snappedBottomInset();
+ double width = w + hInsets;
+ double height = h + vInsets;
+ colorBox.resizeRelocate(0, 0, width, height);
+ }
+
+ @Override
+ protected Node getPopupContent() {
+ if (popupContent == null) {
+ popupContent = new JFXColorPalette((JFXColorPicker) getSkinnable());
+ }
+ return popupContent;
+ }
+
+ @Override
+ public void show() {
+ super.show();
+ final ColorPicker colorPicker = (ColorPicker) getSkinnable();
+ popupContent.updateSelection(colorPicker.getValue());
+ }
+
+ @Override
+ public Node getDisplayNode() {
+ return displayNode;
+ }
+
+ private void updateColor() {
+ final ColorPicker colorPicker = (ColorPicker) getSkinnable();
+ Color color = colorPicker.getValue();
+ Color circleColor = color == null ? Color.WHITE : color;
+ // update picker box color
+ if (((JFXColorPicker) getSkinnable()).isDisableAnimation()) {
+ JFXNodeUtils.updateBackground(colorBox.getBackground(), colorBox, circleColor);
+ } else {
+ Circle colorCircle = new Circle();
+ colorCircle.setFill(circleColor);
+ colorCircle.setManaged(false);
+ colorCircle.setLayoutX(colorBox.getWidth() / 4);
+ colorCircle.setLayoutY(colorBox.getHeight() / 2);
+ colorBox.getChildren().add(colorCircle);
+ Timeline animateColor = new Timeline(new KeyFrame(Duration.millis(240),
+ new KeyValue(colorCircle.radiusProperty(),
+ 200,
+ Interpolator.EASE_BOTH)));
+ animateColor.setOnFinished((finish) -> {
+ JFXNodeUtils.updateBackground(colorBox.getBackground(), colorBox, colorCircle.getFill());
+ colorBox.getChildren().remove(colorCircle);
+ });
+ animateColor.play();
+ }
+ // update label color
+ displayNode.setTextFill(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
+ "rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
+ if (colorLabelVisible.get()) {
+ displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
+ } else {
+ displayNode.setText("");
+ }
+ }
+
+ private void initColor() {
+ final ColorPicker colorPicker = (ColorPicker) getSkinnable();
+ Color color = colorPicker.getValue();
+ Color circleColor = color == null ? Color.WHITE : color;
+ // update picker box color
+ colorBox.setBackground(new Background(new BackgroundFill(circleColor, new CornerRadii(3), Insets.EMPTY)));
+ // update label color
+ displayNode.setTextFill(circleColor.grayscale().getRed() < 0.5 ? Color.valueOf(
+ "rgba(255, 255, 255, 0.87)") : Color.valueOf("rgba(0, 0, 0, 0.87)"));
+ if (colorLabelVisible.get()) {
+ displayNode.setText(JFXNodeUtils.colorToHex(circleColor));
+ } else {
+ displayNode.setText("");
+ }
+ }
+
+ /***************************************************************************
+ * *
+ * Stylesheet Handling *
+ * *
+ **************************************************************************/
+
+ private static final class StyleableProperties {
+ private static final CssMetaData COLOR_LABEL_VISIBLE =
+ new CssMetaData("-fx-color-label-visible",
+ BooleanConverter.getInstance(), Boolean.TRUE) {
+
+ @Override
+ public boolean isSettable(ColorPicker n) {
+ final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
+ return skin.colorLabelVisible == null || !skin.colorLabelVisible.isBound();
+ }
+
+ @Override
+ public StyleableProperty getStyleableProperty(ColorPicker n) {
+ final JFXColorPickerSkin skin = (JFXColorPickerSkin) n.getSkin();
+ return skin.colorLabelVisible;
+ }
+ };
+ private static final List> STYLEABLES;
+
+ static {
+ final List> styleables =
+ new ArrayList<>(ComboBoxPopupControl.getClassCssMetaData());
+ styleables.add(COLOR_LABEL_VISIBLE);
+ STYLEABLES = Collections.unmodifiableList(styleables);
+ }
+ }
+
+ public static List> getClassCssMetaData() {
+ return StyleableProperties.STYLEABLES;
+ }
+
+ @Override
+ public List> getCssMetaData() {
+ return getClassCssMetaData();
+ }
+
+ protected TextField getEditor() {
+ return null;
+ }
+
+ protected javafx.util.StringConverter getConverter() {
+ return null;
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXColorPickerUI.java b/HMCL/src/main/java/com/jfoenix/skins/JFXColorPickerUI.java
new file mode 100644
index 000000000..d060daf97
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXColorPickerUI.java
@@ -0,0 +1,629 @@
+/*
+ * 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.effects.JFXDepthManager;
+import com.jfoenix.transitions.CachedTransition;
+import javafx.animation.Animation.Status;
+import javafx.animation.*;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.geometry.Point2D;
+import javafx.scene.Node;
+import javafx.scene.effect.ColorAdjust;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.image.PixelWriter;
+import javafx.scene.image.WritableImage;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Circle;
+import javafx.scene.shape.Path;
+import javafx.util.Duration;
+
+/**
+ * @author Shadi Shaheen & Bassel El Mabsout this UI allows the user to pick a color using HSL color system
+ */
+final class JFXColorPickerUI extends Pane {
+
+ private CachedTransition selectorTransition;
+ private int pickerSize = 400;
+ // sl circle selector size
+ private final int selectorSize = 20;
+ private final double centerX;
+ private final double centerY;
+ private final double huesRadius;
+ private final double slRadius;
+ private double currentHue = 0;
+
+ private final ImageView huesCircleView;
+ private final ImageView slCircleView;
+ private final Pane colorSelector;
+ private final Pane selector;
+ private CurveTransition colorsTransition;
+
+ public JFXColorPickerUI(int pickerSize) {
+ JFXDepthManager.setDepth(this, 1);
+
+ this.pickerSize = pickerSize;
+ this.centerX = (double) pickerSize / 2;
+ this.centerY = (double) pickerSize / 2;
+ final double pickerRadius = (double) pickerSize / 2;
+ this.huesRadius = pickerRadius * 0.9;
+ final double huesSmallR = pickerRadius * 0.8;
+ final double huesLargeR = pickerRadius;
+ this.slRadius = pickerRadius * 0.7;
+
+ // Create Hues Circle
+ huesCircleView = new ImageView(getHuesCircle(pickerSize, pickerSize));
+ // clip to smooth the edges
+ Circle outterCircle = new Circle(centerX, centerY, huesLargeR - 2);
+ Circle innterCircle = new Circle(centerX, centerY, huesSmallR + 2);
+ huesCircleView.setClip(Path.subtract(outterCircle, innterCircle));
+ this.getChildren().add(huesCircleView);
+
+ // create Hues Circle Selector
+ Circle r1 = new Circle(pickerRadius - huesSmallR);
+ Circle r2 = new Circle(pickerRadius - huesRadius);
+ colorSelector = new Pane();
+ colorSelector.setStyle("-fx-border-color:#424242; -fx-border-width:1px; -fx-background-color:rgba(255, 255, 255, 0.87);");
+ colorSelector.setPrefSize(pickerRadius - huesSmallR, pickerRadius - huesSmallR);
+ colorSelector.setShape(Path.subtract(r1, r2));
+ colorSelector.setCache(true);
+ colorSelector.setMouseTransparent(true);
+ colorSelector.setPickOnBounds(false);
+ this.getChildren().add(colorSelector);
+
+ // add Hues Selection Listeners
+ huesCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
+ if (colorsTransition != null) {
+ colorsTransition.stop();
+ }
+ double dx = event.getX() - centerX;
+ double dy = event.getY() - centerY;
+ double theta = Math.atan2(dy, dx);
+ double x = centerX + huesRadius * Math.cos(theta);
+ double y = centerY + huesRadius * Math.sin(theta);
+ colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
+ colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
+ colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
+ });
+ huesCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
+ double dx = event.getX() - centerX;
+ double dy = event.getY() - centerY;
+ double theta = Math.atan2(dy, dx);
+ double x = centerX + huesRadius * Math.cos(theta);
+ double y = centerY + huesRadius * Math.sin(theta);
+ colorsTransition = new CurveTransition(new Point2D(colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
+ colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2),
+ new Point2D(x, y));
+ colorsTransition.play();
+ });
+ colorSelector.translateXProperty()
+ .addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (newVal.intValue() + colorSelector.getPrefWidth() / 2),
+ (int) (colorSelector.getTranslateY() + colorSelector
+ .getPrefHeight() / 2)));
+ colorSelector.translateYProperty()
+ .addListener((o, oldVal, newVal) -> updateHSLCircleColor((int) (colorSelector.getTranslateX() + colorSelector
+ .getPrefWidth() / 2), (int) (newVal.intValue() + colorSelector.getPrefHeight() / 2)));
+
+
+ // Create SL Circle
+ slCircleView = new ImageView(getSLCricle(pickerSize, pickerSize));
+ slCircleView.setClip(new Circle(centerX, centerY, slRadius - 2));
+ slCircleView.setPickOnBounds(false);
+ this.getChildren().add(slCircleView);
+
+ // create SL Circle Selector
+ selector = new Pane();
+ Circle c1 = new Circle(selectorSize / 2);
+ Circle c2 = new Circle((selectorSize / 2) * 0.5);
+ selector.setShape(Path.subtract(c1, c2));
+ selector.setStyle(
+ "-fx-border-color:#424242; -fx-border-width:1px;-fx-background-color:rgba(255, 255, 255, 0.87);");
+ selector.setPrefSize(selectorSize, selectorSize);
+ selector.setMinSize(selectorSize, selectorSize);
+ selector.setMaxSize(selectorSize, selectorSize);
+ selector.setCache(true);
+ selector.setMouseTransparent(true);
+ this.getChildren().add(selector);
+
+
+ // add SL selection Listeners
+ slCircleView.addEventHandler(MouseEvent.MOUSE_DRAGGED, (event) -> {
+ if (selectorTransition != null) {
+ selectorTransition.stop();
+ }
+ if (Math.pow(event.getX() - centerX, 2) + Math.pow(event.getY() - centerY, 2) < Math.pow(slRadius - 2, 2)) {
+ selector.setTranslateX(event.getX() - selector.getPrefWidth() / 2);
+ selector.setTranslateY(event.getY() - selector.getPrefHeight() / 2);
+ } else {
+ double dx = event.getX() - centerX;
+ double dy = event.getY() - centerY;
+ double theta = Math.atan2(dy, dx);
+ double x = centerX + (slRadius - 2) * Math.cos(theta);
+ double y = centerY + (slRadius - 2) * Math.sin(theta);
+ selector.setTranslateX(x - selector.getPrefWidth() / 2);
+ selector.setTranslateY(y - selector.getPrefHeight() / 2);
+ }
+ });
+ slCircleView.addEventHandler(MouseEvent.MOUSE_PRESSED, (event) -> {
+ selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
+ new KeyValue(selector.translateXProperty(),
+ event.getX() - selector.getPrefWidth() / 2,
+ Interpolator.EASE_BOTH),
+ new KeyValue(selector.translateYProperty(),
+ event.getY() - selector.getPrefHeight() / 2,
+ Interpolator.EASE_BOTH)))) {
+ {
+ setCycleDuration(Duration.millis(160));
+ setDelay(Duration.seconds(0));
+ }
+ };
+ selectorTransition.play();
+ });
+ // add slCircleView listener
+ selector.translateXProperty()
+ .addListener((o, oldVal, newVal) -> setColorAtLocation(newVal.intValue() + selectorSize / 2,
+ (int) selector.getTranslateY() + selectorSize / 2));
+ selector.translateYProperty()
+ .addListener((o, oldVal, newVal) -> setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
+ newVal.intValue() + selectorSize / 2));
+
+
+ // initial color selection
+ double dx = 20 - centerX;
+ double dy = 20 - centerY;
+ double theta = Math.atan2(dy, dx);
+ double x = centerX + huesRadius * Math.cos(theta);
+ double y = centerY + huesRadius * Math.sin(theta);
+ colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(dy, dx)));
+ colorSelector.setTranslateX(x - colorSelector.getPrefWidth() / 2);
+ colorSelector.setTranslateY(y - colorSelector.getPrefHeight() / 2);
+ selector.setTranslateX(centerX - selector.getPrefWidth() / 2);
+ selector.setTranslateY(centerY - selector.getPrefHeight() / 2);
+ }
+
+ /**
+ * List of Color Nodes that needs to be updated when picking a color
+ */
+ private final ObservableList colorNodes = FXCollections.observableArrayList();
+
+ public void addColorSelectionNode(Node... nodes) {
+ colorNodes.addAll(nodes);
+ }
+
+ public void removeColorSelectionNode(Node... nodes) {
+ colorNodes.removeAll(nodes);
+ }
+
+ private void updateHSLCircleColor(int x, int y) {
+ // transform color to HSL space
+ Color color = huesCircleView.getImage().getPixelReader().getColor(x, y);
+ double max = Math.max(color.getRed(), Math.max(color.getGreen(), color.getBlue()));
+ double min = Math.min(color.getRed(), Math.min(color.getGreen(), color.getBlue()));
+ double hue = 0;
+ if (max != min) {
+ double d = max - min;
+ if (max == color.getRed()) {
+ hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
+ } else if (max == color.getGreen()) {
+ hue = (color.getBlue() - color.getRed()) / d + 2;
+ } else if (max == color.getBlue()) {
+ hue = (color.getRed() - color.getGreen()) / d + 4;
+ }
+ hue /= 6;
+ }
+ currentHue = map(hue, 0, 1, 0, 255);
+
+ // refresh the HSL circle
+ refreshHSLCircle();
+ }
+
+ private void refreshHSLCircle() {
+ ColorAdjust colorAdjust = new ColorAdjust();
+ colorAdjust.setHue(map(currentHue + (currentHue < 127.5 ? 1 : -1) * 127.5, 0, 255, -1, 1));
+ slCircleView.setEffect(colorAdjust);
+ setColorAtLocation((int) selector.getTranslateX() + selectorSize / 2,
+ (int) selector.getTranslateY() + selectorSize / 2);
+ }
+
+
+ /**
+ * this method is used to move selectors to a certain color
+ */
+ private boolean allowColorChange = true;
+ private ParallelTransition pTrans;
+
+ public void moveToColor(Color color) {
+ allowColorChange = false;
+ double max = Math.max(color.getRed(),
+ Math.max(color.getGreen(), color.getBlue())), min = Math.min(color.getRed(),
+ Math.min(color.getGreen(),
+ color.getBlue()));
+ double hue = 0;
+ double l = (max + min) / 2;
+ double s = 0;
+ if (max == min) {
+ hue = s = 0; // achromatic
+ } else {
+ double d = max - min;
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
+ if (max == color.getRed()) {
+ hue = (color.getGreen() - color.getBlue()) / d + (color.getGreen() < color.getBlue() ? 6 : 0);
+ } else if (max == color.getGreen()) {
+ hue = (color.getBlue() - color.getRed()) / d + 2;
+ } else if (max == color.getBlue()) {
+ hue = (color.getRed() - color.getGreen()) / d + 4;
+ }
+ hue /= 6;
+ }
+ currentHue = map(hue, 0, 1, 0, 255);
+
+ // Animate Hue
+ double theta = map(currentHue, 0, 255, -Math.PI, Math.PI);
+ double x = centerX + huesRadius * Math.cos(theta);
+ double y = centerY + huesRadius * Math.sin(theta);
+ colorsTransition = new CurveTransition(
+ new Point2D(
+ colorSelector.getTranslateX() + colorSelector.getPrefWidth() / 2,
+ colorSelector.getTranslateY() + colorSelector.getPrefHeight() / 2
+ ),
+ new Point2D(x, y));
+
+ // Animate SL
+ s = map(s, 0, 1, 0, 255);
+ l = map(l, 0, 1, 0, 255);
+ Point2D point = getPointFromSL((int) s, (int) l, slRadius);
+ double pX = centerX - point.getX();
+ double pY = centerY - point.getY();
+
+ double endPointX;
+ double endPointY;
+ if (Math.pow(pX - centerX, 2) + Math.pow(pY - centerY, 2) < Math.pow(slRadius - 2, 2)) {
+ endPointX = pX - selector.getPrefWidth() / 2;
+ endPointY = pY - selector.getPrefHeight() / 2;
+ } else {
+ double dx = pX - centerX;
+ double dy = pY - centerY;
+ theta = Math.atan2(dy, dx);
+ x = centerX + (slRadius - 2) * Math.cos(theta);
+ y = centerY + (slRadius - 2) * Math.sin(theta);
+ endPointX = x - selector.getPrefWidth() / 2;
+ endPointY = y - selector.getPrefHeight() / 2;
+ }
+ selectorTransition = new CachedTransition(selector, new Timeline(new KeyFrame(Duration.millis(1000),
+ new KeyValue(selector.translateXProperty(),
+ endPointX,
+ Interpolator.EASE_BOTH),
+ new KeyValue(selector.translateYProperty(),
+ endPointY,
+ Interpolator.EASE_BOTH)))) {
+ {
+ setCycleDuration(Duration.millis(160));
+ setDelay(Duration.seconds(0));
+ }
+ };
+
+ if (pTrans != null) {
+ pTrans.stop();
+ }
+ pTrans = new ParallelTransition(colorsTransition, selectorTransition);
+ pTrans.setOnFinished((finish) -> {
+ if (pTrans.getStatus() == Status.STOPPED) {
+ allowColorChange = true;
+ }
+ });
+ pTrans.play();
+
+ refreshHSLCircle();
+ }
+
+ private void setColorAtLocation(int x, int y) {
+ if (allowColorChange) {
+ Color color = getColorAtLocation(x, y);
+ String colorString = "rgb(" + color.getRed() * 255 + "," + color.getGreen() * 255 + "," + color.getBlue() * 255 + ");";
+ for (Node node : colorNodes)
+ node.setStyle("-fx-background-color:" + colorString + "; -fx-fill:" + colorString + ";");
+ }
+ }
+
+ private Color getColorAtLocation(double x, double y) {
+ double dy = x - centerX;
+ double dx = y - centerY;
+ return getColor(dx, dy);
+ }
+
+ private Image getHuesCircle(int width, int height) {
+ WritableImage raster = new WritableImage(width, height);
+ PixelWriter pixelWriter = raster.getPixelWriter();
+ Point2D center = new Point2D((double) width / 2, (double) height / 2);
+ double rsmall = 0.8 * width / 2;
+ double rbig = (double) width / 2;
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ double dx = x - center.getX();
+ double dy = y - center.getY();
+ double distance = Math.sqrt((dx * dx) + (dy * dy));
+ double o = Math.atan2(dy, dx);
+ if (distance > rsmall && distance < rbig) {
+ double H = map(o, -Math.PI, Math.PI, 0, 255);
+ double S = 255;
+ double L = 152;
+ pixelWriter.setColor(x, y, HSL2RGB(H, S, L));
+ }
+ }
+ }
+ return raster;
+ }
+
+ private Image getSLCricle(int width, int height) {
+ WritableImage raster = new WritableImage(width, height);
+ PixelWriter pixelWriter = raster.getPixelWriter();
+ Point2D center = new Point2D((double) width / 2, (double) height / 2);
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ double dy = x - center.getX();
+ double dx = y - center.getY();
+ pixelWriter.setColor(x, y, getColor(dx, dy));
+ }
+ }
+ return raster;
+ }
+
+ private double clamp(double from, double small, double big) {
+ return Math.min(Math.max(from, small), big);
+ }
+
+ private Color getColor(double dx, double dy) {
+ double distance = Math.sqrt((dx * dx) + (dy * dy));
+ double rverysmall = 0.65 * ((double) pickerSize / 2);
+ Color pixelColor = Color.BLUE;
+
+ if (distance <= rverysmall * 1.1) {
+ double angle = -Math.PI / 2.;
+ double angle1 = angle + 2 * Math.PI / 3.;
+ double angle2 = angle1 + 2 * Math.PI / 3.;
+ double x1 = rverysmall * Math.sin(angle1);
+ double y1 = rverysmall * Math.cos(angle1);
+ double x2 = rverysmall * Math.sin(angle2);
+ double y2 = rverysmall * Math.cos(angle2);
+ dx += 0.01;
+ double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(x2, y2), new Point2D(dx, dy));
+ double xArc = circle[0];
+ double yArc = 0;
+ double arcR = circle[2];
+ double Arco = Math.atan2(dx - xArc, dy - yArc);
+ double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
+ double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
+
+ double finalX = xArc > 0 ? xArc - arcR : xArc + arcR;
+
+ double saturation = map(finalX, -rverysmall, rverysmall, 255, 0);
+
+ double lightness = 255;
+ double diffAngle = Arco2 - Arco1;
+ double diffArco = Arco - Arco1;
+ if (dx < x1) {
+ diffAngle = diffAngle < 0 ? 2 * Math.PI + diffAngle : diffAngle;
+ diffAngle = Math.abs(2 * Math.PI - diffAngle);
+ diffArco = diffArco < 0 ? 2 * Math.PI + diffArco : diffArco;
+ diffArco = Math.abs(2 * Math.PI - diffArco);
+ }
+ lightness = map(diffArco, 0, diffAngle, 0, 255);
+
+
+ if (distance > rverysmall) {
+ saturation = 255 - saturation;
+ if (lightness < 0 && dy < 0) {
+ lightness = 255;
+ }
+ }
+ lightness = clamp(lightness, 0, 255);
+ if ((saturation < 10 && dx < x1) || (saturation > 240 && dx > x1)) {
+ saturation = 255 - saturation;
+ }
+ saturation = clamp(saturation, 0, 255);
+ pixelColor = HSL2RGB(currentHue, saturation, lightness);
+ }
+ return pixelColor;
+ }
+
+ /***************************************************************************
+ * *
+ * Hues Animation *
+ * *
+ **************************************************************************/
+
+ private final class CurveTransition extends Transition {
+ Point2D from;
+ double fromTheta;
+ double toTheta;
+
+ public CurveTransition(Point2D from, Point2D to) {
+ this.from = from;
+ double fromDx = from.getX() - centerX;
+ double fromDy = from.getY() - centerY;
+ fromTheta = Math.atan2(fromDy, fromDx);
+ double toDx = to.getX() - centerX;
+ double toDy = to.getY() - centerY;
+ toTheta = Math.atan2(toDy, toDx);
+ setInterpolator(Interpolator.EASE_BOTH);
+ setDelay(Duration.millis(0));
+ setCycleDuration(Duration.millis(240));
+ }
+
+ @Override
+ protected void interpolate(double frac) {
+ double dif = Math.min(Math.abs(toTheta - fromTheta), 2 * Math.PI - Math.abs(toTheta - fromTheta));
+ if (dif == 2 * Math.PI - Math.abs(toTheta - fromTheta)) {
+ int dir = -1;
+ if (toTheta < fromTheta) {
+ dir = 1;
+ }
+ dif = dir * dif;
+ } else {
+ dif = toTheta - fromTheta;
+ }
+
+ Point2D newP = rotate(from, new Point2D(centerX, centerY), frac * dif);
+ colorSelector.setRotate(90 + Math.toDegrees(Math.atan2(newP.getY() - centerY, newP.getX() - centerX)));
+ colorSelector.setTranslateX(newP.getX() - colorSelector.getPrefWidth() / 2);
+ colorSelector.setTranslateY(newP.getY() - colorSelector.getPrefHeight() / 2);
+ }
+ }
+
+ /***************************************************************************
+ * *
+ * Util methods *
+ * *
+ **************************************************************************/
+
+ private double map(double val, double min1, double max1, double min2, double max2) {
+ return min2 + (max2 - min2) * ((val - min1) / (max1 - min1));
+ }
+
+ private Color HSL2RGB(double hue, double sat, double lum) {
+ hue = map(hue, 0, 255, 0, 359);
+ sat = map(sat, 0, 255, 0, 1);
+ lum = map(lum, 0, 255, 0, 1);
+ double v;
+ double red, green, blue;
+ double m;
+ double sv;
+ int sextant;
+ double fract, vsf, mid1, mid2;
+
+ red = lum; // default to gray
+ green = lum;
+ blue = lum;
+ v = (lum <= 0.5) ? (lum * (1.0 + sat)) : (lum + sat - lum * sat);
+ m = lum + lum - v;
+ sv = (v - m) / v;
+ hue /= 60.0; //get into range 0..6
+ sextant = (int) Math.floor(hue); // int32 rounds up or down.
+ fract = hue - sextant;
+ vsf = v * sv * fract;
+ mid1 = m + vsf;
+ mid2 = v - vsf;
+
+ if (v > 0) {
+ switch (sextant) {
+ case 0:
+ red = v;
+ green = mid1;
+ blue = m;
+ break;
+ case 1:
+ red = mid2;
+ green = v;
+ blue = m;
+ break;
+ case 2:
+ red = m;
+ green = v;
+ blue = mid1;
+ break;
+ case 3:
+ red = m;
+ green = mid2;
+ blue = v;
+ break;
+ case 4:
+ red = mid1;
+ green = m;
+ blue = v;
+ break;
+ case 5:
+ red = v;
+ green = m;
+ blue = mid2;
+ break;
+ }
+ }
+ return new Color(red, green, blue, 1);
+ }
+
+ private double[] circleFrom3Points(Point2D a, Point2D b, Point2D c) {
+ double ax, ay, bx, by, cx, cy, x1, y11, dx1, dy1, x2, y2, dx2, dy2, ox, oy, dx, dy, radius; // Variables Used and to Declared
+ ax = a.getX();
+ ay = a.getY(); //first Point X and Y
+ bx = b.getX();
+ by = b.getY(); // Second Point X and Y
+ cx = c.getX();
+ cy = c.getY(); // Third Point X and Y
+
+ ////****************Following are Basic Procedure**********************///
+ x1 = (bx + ax) / 2;
+ y11 = (by + ay) / 2;
+ dy1 = bx - ax;
+ dx1 = -(by - ay);
+
+ x2 = (cx + bx) / 2;
+ y2 = (cy + by) / 2;
+ dy2 = cx - bx;
+ dx2 = -(cy - by);
+
+ ox = (y11 * dx1 * dx2 + x2 * dx1 * dy2 - x1 * dy1 * dx2 - y2 * dx1 * dx2) / (dx1 * dy2 - dy1 * dx2);
+ oy = (ox - x1) * dy1 / dx1 + y11;
+
+ dx = ox - ax;
+ dy = oy - ay;
+ radius = Math.sqrt(dx * dx + dy * dy);
+ return new double[]{ox, oy, radius};
+ }
+
+ private Point2D getPointFromSL(int saturation, int lightness, double radius) {
+ double dy = map(saturation, 0, 255, -radius, radius);
+ double angle = 0.;
+ double angle1 = angle + 2 * Math.PI / 3.;
+ double angle2 = angle1 + 2 * Math.PI / 3.;
+ double x1 = radius * Math.sin(angle1);
+ double y1 = radius * Math.cos(angle1);
+ double x2 = radius * Math.sin(angle2);
+ double y2 = radius * Math.cos(angle2);
+ double dx = 0;
+ double[] circle = circleFrom3Points(new Point2D(x1, y1), new Point2D(dx, dy), new Point2D(x2, y2));
+ double xArc = circle[0];
+ double yArc = circle[1];
+ double arcR = circle[2];
+ double Arco1 = Math.atan2(x1 - xArc, y1 - yArc);
+ double Arco2 = Math.atan2(x2 - xArc, y2 - yArc);
+ double ArcoFinal = map(lightness, 0, 255, Arco2, Arco1);
+ double finalX = xArc + arcR * Math.sin(ArcoFinal);
+ double finalY = yArc + arcR * Math.cos(ArcoFinal);
+ if (dy < y1) {
+ ArcoFinal = map(lightness, 0, 255, Arco1, Arco2 + 2 * Math.PI);
+ finalX = -xArc - arcR * Math.sin(ArcoFinal);
+ finalY = yArc + arcR * Math.cos(ArcoFinal);
+ }
+ return new Point2D(finalX, finalY);
+ }
+
+ private Point2D rotate(Point2D a, Point2D center, double angle) {
+ double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
+ .sin(angle);
+ double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
+ .cos(angle);
+ return new Point2D(resultX, resultY);
+ }
+
+}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXCustomColorPicker.java b/HMCL/src/main/java/com/jfoenix/skins/JFXCustomColorPicker.java
new file mode 100644
index 000000000..196ed1e5d
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXCustomColorPicker.java
@@ -0,0 +1,520 @@
+/*
+ * 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.effects.JFXDepthManager;
+import com.jfoenix.transitions.CachedTransition;
+import javafx.animation.KeyFrame;
+import javafx.animation.KeyValue;
+import javafx.animation.Timeline;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.DoubleProperty;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleDoubleProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.geometry.Point2D;
+import javafx.scene.Node;
+import javafx.scene.effect.DropShadow;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Pane;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.*;
+import javafx.scene.transform.Rotate;
+import javafx.util.Duration;
+
+import java.util.ArrayList;
+
+import static javafx.animation.Interpolator.EASE_BOTH;
+
+/// @author Shadi Shaheen
+final class JFXCustomColorPicker extends Pane {
+
+ ObjectProperty selectedPath = new SimpleObjectProperty<>();
+ private MoveTo startPoint;
+ private CubicCurveTo curve0To;
+ private CubicCurveTo outerCircleCurveTo;
+ private CubicCurveTo curve1To;
+ private CubicCurveTo innerCircleCurveTo;
+ private final ArrayList curves = new ArrayList<>();
+
+ private final double distance = 200;
+ private final double centerX = distance;
+ private final double centerY = distance;
+ private final double radius = 110;
+
+ private static final int shapesNumber = 13;
+ private final ArrayList shapes = new ArrayList<>();
+ private CachedTransition showAnimation;
+ private final JFXColorPickerUI hslColorPicker;
+
+ public JFXCustomColorPicker() {
+ this.setPickOnBounds(false);
+ this.setMinSize(distance * 2, distance * 2);
+
+ final DoubleProperty rotationAngle = new SimpleDoubleProperty(2.1);
+
+ // draw recent colors shape using cubic curves
+ init(rotationAngle, centerX + 53, centerY + 162);
+
+ hslColorPicker = new JFXColorPickerUI((int) distance);
+ hslColorPicker.setLayoutX(centerX - distance / 2);
+ hslColorPicker.setLayoutY(centerY - distance / 2);
+ this.getChildren().add(hslColorPicker);
+ // add recent colors shapes
+ final int shapesStartIndex = this.getChildren().size();
+ final int shapesEndIndex = shapesStartIndex + shapesNumber;
+ for (int i = 0; i < shapesNumber; i++) {
+ final double angle = 2 * i * Math.PI / shapesNumber;
+ final RecentColorPath path = new RecentColorPath(startPoint,
+ curve0To,
+ outerCircleCurveTo,
+ curve1To,
+ innerCircleCurveTo);
+ shapes.add(path);
+ path.setPickOnBounds(false);
+ final Rotate rotate = new Rotate(Math.toDegrees(angle), centerX, centerY);
+ path.getTransforms().add(rotate);
+ this.getChildren().add(shapesStartIndex, path);
+ path.setFill(Color.valueOf(getDefaultColor(i)));
+ path.setFocusTraversable(true);
+ path.addEventHandler(MouseEvent.MOUSE_CLICKED, (event) -> {
+ path.requestFocus();
+ selectedPath.set(path);
+ });
+ }
+
+ // add selection listeners
+ selectedPath.addListener((o, oldVal, newVal) -> {
+ if (oldVal != null) {
+ hslColorPicker.removeColorSelectionNode(oldVal);
+ oldVal.playTransition(-1);
+ }
+ // re-arrange children
+ while (this.getChildren().indexOf(newVal) != shapesEndIndex - 1) {
+ final Node temp = this.getChildren().get(shapesEndIndex - 1);
+ this.getChildren().remove(shapesEndIndex - 1);
+ this.getChildren().add(shapesStartIndex, temp);
+ }
+ // update path fill according to the color picker
+ newVal.setStroke(Color.rgb(255, 255, 255, 0.87));
+ newVal.playTransition(1);
+ hslColorPicker.moveToColor((Color) newVal.getFill());
+ hslColorPicker.addColorSelectionNode(newVal);
+ });
+ // init selection
+ selectedPath.set((RecentColorPath) this.getChildren().get(shapesStartIndex));
+ }
+
+ public int getShapesNumber() {
+ return shapesNumber;
+ }
+
+ public int getSelectedIndex() {
+ if (selectedPath.get() != null) {
+ return shapes.indexOf(selectedPath.get());
+ }
+ return -1;
+ }
+
+ public void setColor(final Color color) {
+ shapes.get(getSelectedIndex()).setFill(color);
+ hslColorPicker.moveToColor(color);
+ }
+
+ public Color getColor(final int index) {
+ if (index >= 0 && index < shapes.size()) {
+ return (Color) shapes.get(index).getFill();
+ } else {
+ return Color.WHITE;
+ }
+ }
+
+ public void preAnimate() {
+ final CubicCurve firstCurve = curves.get(0);
+ final double x = firstCurve.getStartX();
+ final double y = firstCurve.getStartY();
+ firstCurve.setStartX(centerX);
+ firstCurve.setStartY(centerY);
+
+ final CubicCurve secondCurve = curves.get(1);
+ final double x1 = secondCurve.getStartX();
+ final double y1 = secondCurve.getStartY();
+ secondCurve.setStartX(centerX);
+ secondCurve.setStartY(centerY);
+
+ final double cx1 = firstCurve.getControlX1();
+ final double cy1 = firstCurve.getControlY1();
+ firstCurve.setControlX1(centerX + radius);
+ firstCurve.setControlY1(centerY + radius / 2);
+
+ final KeyFrame keyFrame = new KeyFrame(Duration.millis(1000),
+ new KeyValue(firstCurve.startXProperty(), x, EASE_BOTH),
+ new KeyValue(firstCurve.startYProperty(), y, EASE_BOTH),
+ new KeyValue(secondCurve.startXProperty(), x1, EASE_BOTH),
+ new KeyValue(secondCurve.startYProperty(), y1, EASE_BOTH),
+ new KeyValue(firstCurve.controlX1Property(), cx1, EASE_BOTH),
+ new KeyValue(firstCurve.controlY1Property(), cy1, EASE_BOTH)
+ );
+ final Timeline timeline = new Timeline(keyFrame);
+ showAnimation = new CachedTransition(this, timeline) {
+ {
+ setCycleDuration(Duration.millis(240));
+ setDelay(Duration.millis(0));
+ }
+ };
+ }
+
+ public void animate() {
+ showAnimation.play();
+ }
+
+ private void init(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
+
+ final Circle innerCircle = new Circle(centerX, centerY, radius, Color.TRANSPARENT);
+ final Circle outerCircle = new Circle(centerX, centerY, radius * 2, Color.web("blue", 0.5));
+
+ // Create a composite shape of 4 cubic curves
+ // create 2 cubic curves of the shape
+ createQuadraticCurve(rotationAngle, initControlX1, initControlY1);
+
+ // inner circle curve
+ final CubicCurve innerCircleCurve = new CubicCurve();
+ innerCircleCurve.startXProperty().bind(curves.get(0).startXProperty());
+ innerCircleCurve.startYProperty().bind(curves.get(0).startYProperty());
+ innerCircleCurve.endXProperty().bind(curves.get(1).startXProperty());
+ innerCircleCurve.endYProperty().bind(curves.get(1).startYProperty());
+ curves.get(0).startXProperty().addListener((o, oldVal, newVal) -> {
+ final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
+ curves.get(0).getStartY(),
+ innerCircle,
+ shapesNumber,
+ -1);
+ innerCircleCurve.setControlX1(controlPoint.getX());
+ innerCircleCurve.setControlY1(controlPoint.getY());
+ });
+ curves.get(0).startYProperty().addListener((o, oldVal, newVal) -> {
+ final Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
+ newVal.doubleValue(),
+ innerCircle,
+ shapesNumber,
+ -1);
+ innerCircleCurve.setControlX1(controlPoint.getX());
+ innerCircleCurve.setControlY1(controlPoint.getY());
+ });
+ curves.get(1).startXProperty().addListener((o, oldVal, newVal) -> {
+ final Point2D controlPoint = makeControlPoint(newVal.doubleValue(),
+ curves.get(1).getStartY(),
+ innerCircle,
+ shapesNumber,
+ 1);
+ innerCircleCurve.setControlX2(controlPoint.getX());
+ innerCircleCurve.setControlY2(controlPoint.getY());
+ });
+ curves.get(1).startYProperty().addListener((o, oldVal, newVal) -> {
+ final Point2D controlPoint = makeControlPoint(curves.get(1).getStartX(),
+ newVal.doubleValue(),
+ innerCircle,
+ shapesNumber,
+ 1);
+ innerCircleCurve.setControlX2(controlPoint.getX());
+ innerCircleCurve.setControlY2(controlPoint.getY());
+ });
+ Point2D controlPoint = makeControlPoint(curves.get(0).getStartX(),
+ curves.get(0).getStartY(),
+ innerCircle,
+ shapesNumber,
+ -1);
+ innerCircleCurve.setControlX1(controlPoint.getX());
+ innerCircleCurve.setControlY1(controlPoint.getY());
+ controlPoint = makeControlPoint(curves.get(1).getStartX(),
+ curves.get(1).getStartY(),
+ innerCircle,
+ shapesNumber,
+ 1);
+ innerCircleCurve.setControlX2(controlPoint.getX());
+ innerCircleCurve.setControlY2(controlPoint.getY());
+
+ // outer circle curve
+ final CubicCurve outerCircleCurve = new CubicCurve();
+ outerCircleCurve.startXProperty().bind(curves.get(0).endXProperty());
+ outerCircleCurve.startYProperty().bind(curves.get(0).endYProperty());
+ outerCircleCurve.endXProperty().bind(curves.get(1).endXProperty());
+ outerCircleCurve.endYProperty().bind(curves.get(1).endYProperty());
+ controlPoint = makeControlPoint(curves.get(0).getEndX(),
+ curves.get(0).getEndY(),
+ outerCircle,
+ shapesNumber,
+ -1);
+ outerCircleCurve.setControlX1(controlPoint.getX());
+ outerCircleCurve.setControlY1(controlPoint.getY());
+ controlPoint = makeControlPoint(curves.get(1).getEndX(), curves.get(1).getEndY(), outerCircle, shapesNumber, 1);
+ outerCircleCurve.setControlX2(controlPoint.getX());
+ outerCircleCurve.setControlY2(controlPoint.getY());
+
+ startPoint = new MoveTo();
+ startPoint.xProperty().bind(curves.get(0).startXProperty());
+ startPoint.yProperty().bind(curves.get(0).startYProperty());
+
+ curve0To = new CubicCurveTo();
+ curve0To.controlX1Property().bind(curves.get(0).controlX1Property());
+ curve0To.controlY1Property().bind(curves.get(0).controlY1Property());
+ curve0To.controlX2Property().bind(curves.get(0).controlX2Property());
+ curve0To.controlY2Property().bind(curves.get(0).controlY2Property());
+ curve0To.xProperty().bind(curves.get(0).endXProperty());
+ curve0To.yProperty().bind(curves.get(0).endYProperty());
+
+ outerCircleCurveTo = new CubicCurveTo();
+ outerCircleCurveTo.controlX1Property().bind(outerCircleCurve.controlX1Property());
+ outerCircleCurveTo.controlY1Property().bind(outerCircleCurve.controlY1Property());
+ outerCircleCurveTo.controlX2Property().bind(outerCircleCurve.controlX2Property());
+ outerCircleCurveTo.controlY2Property().bind(outerCircleCurve.controlY2Property());
+ outerCircleCurveTo.xProperty().bind(outerCircleCurve.endXProperty());
+ outerCircleCurveTo.yProperty().bind(outerCircleCurve.endYProperty());
+
+ curve1To = new CubicCurveTo();
+ curve1To.controlX1Property().bind(curves.get(1).controlX2Property());
+ curve1To.controlY1Property().bind(curves.get(1).controlY2Property());
+ curve1To.controlX2Property().bind(curves.get(1).controlX1Property());
+ curve1To.controlY2Property().bind(curves.get(1).controlY1Property());
+ curve1To.xProperty().bind(curves.get(1).startXProperty());
+ curve1To.yProperty().bind(curves.get(1).startYProperty());
+
+ innerCircleCurveTo = new CubicCurveTo();
+ innerCircleCurveTo.controlX1Property().bind(innerCircleCurve.controlX2Property());
+ innerCircleCurveTo.controlY1Property().bind(innerCircleCurve.controlY2Property());
+ innerCircleCurveTo.controlX2Property().bind(innerCircleCurve.controlX1Property());
+ innerCircleCurveTo.controlY2Property().bind(innerCircleCurve.controlY1Property());
+ innerCircleCurveTo.xProperty().bind(innerCircleCurve.startXProperty());
+ innerCircleCurveTo.yProperty().bind(innerCircleCurve.startYProperty());
+ }
+
+ private void createQuadraticCurve(final DoubleProperty rotationAngle, final double initControlX1, final double initControlY1) {
+ for (int i = 0; i < 2; i++) {
+
+ double angle = 2 * i * Math.PI / shapesNumber;
+ double xOffset = radius * Math.cos(angle);
+ double yOffset = radius * Math.sin(angle);
+ final double startx = centerX + xOffset;
+ final double starty = centerY + yOffset;
+
+ final double diffStartCenterX = startx - centerX;
+ final double diffStartCenterY = starty - centerY;
+ final double sinRotAngle = Math.sin(rotationAngle.get());
+ final double cosRotAngle = Math.cos(rotationAngle.get());
+ final double startXR = cosRotAngle * diffStartCenterX - sinRotAngle * diffStartCenterY + centerX;
+ final double startYR = sinRotAngle * diffStartCenterX + cosRotAngle * diffStartCenterY + centerY;
+
+ angle = 2 * i * Math.PI / shapesNumber;
+ xOffset = distance * Math.cos(angle);
+ yOffset = distance * Math.sin(angle);
+
+ final double endx = centerX + xOffset;
+ final double endy = centerY + yOffset;
+
+ final CubicCurve curvedLine = new CubicCurve();
+ curvedLine.setStartX(startXR);
+ curvedLine.setStartY(startYR);
+ curvedLine.setControlX1(startXR);
+ curvedLine.setControlY1(startYR);
+ curvedLine.setControlX2(endx);
+ curvedLine.setControlY2(endy);
+ curvedLine.setEndX(endx);
+ curvedLine.setEndY(endy);
+ curvedLine.setStroke(Color.FORESTGREEN);
+ curvedLine.setStrokeWidth(1);
+ curvedLine.setStrokeLineCap(StrokeLineCap.ROUND);
+ curvedLine.setFill(Color.TRANSPARENT);
+ curvedLine.setMouseTransparent(true);
+ rotationAngle.addListener((o, oldVal, newVal) -> {
+ final double newstartXR = ((cosRotAngle * diffStartCenterX) - (sinRotAngle * diffStartCenterY)) + centerX;
+ final double newstartYR = (sinRotAngle * diffStartCenterX) + (cosRotAngle * diffStartCenterY) + centerY;
+ curvedLine.setStartX(newstartXR);
+ curvedLine.setStartY(newstartYR);
+ });
+
+ curves.add(curvedLine);
+
+ if (i == 0) {
+ curvedLine.setControlX1(initControlX1);
+ curvedLine.setControlY1(initControlY1);
+ } else {
+ final CubicCurve firstCurve = curves.get(0);
+ final double curveTheta = 2 * curves.indexOf(curvedLine) * Math.PI / shapesNumber;
+
+ curvedLine.controlX1Property().bind(Bindings.createDoubleBinding(() -> {
+ final double a = firstCurve.getControlX1() - centerX;
+ final double b = Math.sin(curveTheta) * (firstCurve.getControlY1() - centerY);
+ return ((Math.cos(curveTheta) * a) - b) + centerX;
+ }, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
+
+ curvedLine.controlY1Property().bind(Bindings.createDoubleBinding(() -> {
+ final double a = Math.sin(curveTheta) * (firstCurve.getControlX1() - centerX);
+ final double b = Math.cos(curveTheta) * (firstCurve.getControlY1() - centerY);
+ return a + b + centerY;
+ }, firstCurve.controlX1Property(), firstCurve.controlY1Property()));
+
+
+ curvedLine.controlX2Property().bind(Bindings.createDoubleBinding(() -> {
+ final double a = firstCurve.getControlX2() - centerX;
+ final double b = firstCurve.getControlY2() - centerY;
+ return ((Math.cos(curveTheta) * a) - (Math.sin(curveTheta) * b)) + centerX;
+ }, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
+
+ curvedLine.controlY2Property().bind(Bindings.createDoubleBinding(() -> {
+ final double a = Math.sin(curveTheta) * (firstCurve.getControlX2() - centerX);
+ final double b = Math.cos(curveTheta) * (firstCurve.getControlY2() - centerY);
+ return a + b + centerY;
+ }, firstCurve.controlX2Property(), firstCurve.controlY2Property()));
+ }
+ }
+ }
+
+ private String getDefaultColor(final int i) {
+ String color = "#FFFFFF";
+ switch (i) {
+ case 0:
+ color = "#8F3F7E";
+ break;
+ case 1:
+ color = "#B5305F";
+ break;
+ case 2:
+ color = "#CE584A";
+ break;
+ case 3:
+ color = "#DB8D5C";
+ break;
+ case 4:
+ color = "#DA854E";
+ break;
+ case 5:
+ color = "#E9AB44";
+ break;
+ case 6:
+ color = "#FEE435";
+ break;
+ case 7:
+ color = "#99C286";
+ break;
+ case 8:
+ color = "#01A05E";
+ break;
+ case 9:
+ color = "#4A8895";
+ break;
+ case 10:
+ color = "#16669B";
+ break;
+ case 11:
+ color = "#2F65A5";
+ break;
+ case 12:
+ color = "#4E6A9C";
+ break;
+ default:
+ break;
+ }
+ return color;
+ }
+
+ private Point2D rotate(final Point2D a, final Point2D center, final double angle) {
+ final double resultX = center.getX() + (a.getX() - center.getX()) * Math.cos(angle) - (a.getY() - center.getY()) * Math
+ .sin(angle);
+ final double resultY = center.getY() + (a.getX() - center.getX()) * Math.sin(angle) + (a.getY() - center.getY()) * Math
+ .cos(angle);
+ return new Point2D(resultX, resultY);
+ }
+
+ private Point2D makeControlPoint(final double endX, final double endY, final Circle circle, final int numSegments, int direction) {
+ final double controlPointDistance = (4.0 / 3.0) * Math.tan(Math.PI / (2 * numSegments)) * circle.getRadius();
+ final Point2D center = new Point2D(circle.getCenterX(), circle.getCenterY());
+ final Point2D end = new Point2D(endX, endY);
+ Point2D perp = rotate(center, end, direction * Math.PI / 2.);
+ Point2D diff = perp.subtract(end);
+ diff = diff.normalize();
+ diff = scale(diff, controlPointDistance);
+ return end.add(diff);
+ }
+
+ private Point2D scale(final Point2D a, final double scale) {
+ return new Point2D(a.getX() * scale, a.getY() * scale);
+ }
+
+ final class RecentColorPath extends Path {
+ PathClickTransition transition;
+
+ RecentColorPath(final PathElement... elements) {
+ super(elements);
+ this.setStrokeLineCap(StrokeLineCap.ROUND);
+ this.setStrokeWidth(0);
+ this.setStrokeType(StrokeType.CENTERED);
+ this.setCache(true);
+ JFXDepthManager.setDepth(this, 2);
+ this.transition = new PathClickTransition(this);
+ }
+
+ void playTransition(final double rate) {
+ transition.setRate(rate);
+ transition.play();
+ }
+ }
+
+ private final class PathClickTransition extends CachedTransition {
+ PathClickTransition(final Path path) {
+ super(JFXCustomColorPicker.this, new Timeline(
+ new KeyFrame(Duration.ZERO,
+ new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
+ JFXDepthManager.getShadowAt(2).radiusProperty().get(),
+ EASE_BOTH),
+ new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
+ JFXDepthManager.getShadowAt(2).spreadProperty().get(),
+ EASE_BOTH),
+ new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
+ JFXDepthManager.getShadowAt(2).offsetXProperty().get(),
+ EASE_BOTH),
+ new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
+ JFXDepthManager.getShadowAt(2).offsetYProperty().get(),
+ EASE_BOTH),
+ new KeyValue(path.strokeWidthProperty(), 0, EASE_BOTH)
+ ),
+ new KeyFrame(Duration.millis(1000),
+ new KeyValue(((DropShadow) path.getEffect()).radiusProperty(),
+ JFXDepthManager.getShadowAt(5).radiusProperty().get(),
+ EASE_BOTH),
+ new KeyValue(((DropShadow) path.getEffect()).spreadProperty(),
+ JFXDepthManager.getShadowAt(5).spreadProperty().get(),
+ EASE_BOTH),
+ new KeyValue(((DropShadow) path.getEffect()).offsetXProperty(),
+ JFXDepthManager.getShadowAt(5).offsetXProperty().get(),
+ EASE_BOTH),
+ new KeyValue(((DropShadow) path.getEffect()).offsetYProperty(),
+ JFXDepthManager.getShadowAt(5).offsetYProperty().get(),
+ EASE_BOTH),
+ new KeyValue(path.strokeWidthProperty(), 2, EASE_BOTH)
+ )
+ )
+ );
+ // reduce the number to increase the shifting , increase number to reduce shifting
+ setCycleDuration(Duration.millis(120));
+ setDelay(Duration.seconds(0));
+ setAutoReverse(false);
+ }
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXCustomColorPickerDialog.java b/HMCL/src/main/java/com/jfoenix/skins/JFXCustomColorPickerDialog.java
new file mode 100644
index 000000000..82d251bf1
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXCustomColorPickerDialog.java
@@ -0,0 +1,420 @@
+/*
+ * 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.*;
+import com.jfoenix.svg.SVGGlyph;
+import com.jfoenix.transitions.JFXFillTransition;
+import javafx.animation.*;
+import javafx.application.Platform;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.EventHandler;
+import javafx.geometry.Insets;
+import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.control.Tab;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+import javafx.stage.*;
+import javafx.util.Duration;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author Shadi Shaheen
+ */
+public class JFXCustomColorPickerDialog extends StackPane {
+
+ public static final String rgbFieldStyle = "-fx-background-color:TRANSPARENT;-fx-font-weight: BOLD;-fx-prompt-text-fill: #808080; -fx-alignment: top-left ; -fx-max-width: 300;";
+ private final Stage dialog = new Stage();
+ // used for concurrency control and preventing FX-thread over use
+ private final AtomicInteger concurrencyController = new AtomicInteger(-1);
+
+ private final ObjectProperty currentColorProperty = new SimpleObjectProperty<>(Color.WHITE);
+ private final ObjectProperty customColorProperty = new SimpleObjectProperty<>(Color.TRANSPARENT);
+ private Runnable onSave;
+
+ private final Scene customScene;
+ private final JFXCustomColorPicker curvedColorPicker;
+ private ParallelTransition paraTransition;
+ private final JFXDecorator pickerDecorator;
+ private boolean systemChange = false;
+ private boolean userChange = false;
+ private boolean initOnce = true;
+ private final Runnable initRun;
+
+ public JFXCustomColorPickerDialog(Window owner) {
+ getStyleClass().add("custom-color-dialog");
+ if (owner != null) {
+ dialog.initOwner(owner);
+ }
+ dialog.initModality(Modality.APPLICATION_MODAL);
+ dialog.initStyle(StageStyle.TRANSPARENT);
+ dialog.setResizable(false);
+
+ // create JFX Decorator
+ pickerDecorator = new JFXDecorator(dialog, this, false, false, false);
+ pickerDecorator.setOnCloseButtonAction(this::updateColor);
+ pickerDecorator.setPickOnBounds(false);
+ customScene = new Scene(pickerDecorator, Color.TRANSPARENT);
+ if (owner != null) {
+ final Scene ownerScene = owner.getScene();
+ if (ownerScene != null) {
+ if (ownerScene.getUserAgentStylesheet() != null) {
+ customScene.setUserAgentStylesheet(ownerScene.getUserAgentStylesheet());
+ }
+ customScene.getStylesheets().addAll(ownerScene.getStylesheets());
+ }
+ }
+ curvedColorPicker = new JFXCustomColorPicker();
+
+ StackPane pane = new StackPane(curvedColorPicker);
+ pane.setPadding(new Insets(18));
+
+ VBox container = new VBox();
+ container.getChildren().add(pane);
+
+ JFXTabPane tabs = new JFXTabPane();
+
+ JFXTextField rgbField = new JFXTextField();
+ JFXTextField hsbField = new JFXTextField();
+ JFXTextField hexField = new JFXTextField();
+
+ rgbField.setStyle(rgbFieldStyle);
+ rgbField.setPromptText("RGB Color");
+ rgbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
+
+ hsbField.setStyle(
+ "-fx-background-color:TRANSPARENT;-fx-font-weight: BOLD;-fx-prompt-text-fill: #808080; -fx-alignment: top-left ; -fx-max-width: 300;");
+ hsbField.setPromptText("HSB Color");
+ hsbField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
+
+ hexField.setStyle(
+ "-fx-background-color:TRANSPARENT;-fx-font-weight: BOLD;-fx-prompt-text-fill: #808080; -fx-alignment: top-left ; -fx-max-width: 300;");
+ hexField.setPromptText("#HEX Color");
+ hexField.textProperty().addListener((o, oldVal, newVal) -> updateColorFromUserInput(newVal));
+
+ StackPane tabContent = new StackPane();
+ tabContent.getChildren().add(rgbField);
+ tabContent.setMinHeight(100);
+
+ Tab rgbTab = new Tab("RGB");
+ rgbTab.setContent(tabContent);
+ Tab hsbTab = new Tab("HSB");
+ hsbTab.setContent(hsbField);
+ Tab hexTab = new Tab("HEX");
+ hexTab.setContent(hexField);
+
+ tabs.getTabs().add(rgbTab);
+ tabs.getTabs().add(hsbTab);
+ tabs.getTabs().add(hexTab);
+
+ curvedColorPicker.selectedPath.addListener((o, oldVal, newVal) -> {
+ if (paraTransition != null) {
+ paraTransition.stop();
+ }
+ Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
+ pane.backgroundProperty().unbind();
+ tabsHeader.backgroundProperty().unbind();
+ JFXFillTransition fillTransition = new JFXFillTransition(Duration.millis(240),
+ pane,
+ (Color) oldVal.getFill(),
+ (Color) newVal.getFill());
+ JFXFillTransition tabsFillTransition = new JFXFillTransition(Duration.millis(240),
+ tabsHeader,
+ (Color) oldVal.getFill(),
+ (Color) newVal.getFill());
+ paraTransition = new ParallelTransition(fillTransition, tabsFillTransition);
+ paraTransition.setOnFinished((finish) -> {
+ tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
+ return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
+ }, newVal.fillProperty()));
+ pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
+ return new Background(new BackgroundFill(newVal.getFill(), CornerRadii.EMPTY, Insets.EMPTY));
+ }, newVal.fillProperty()));
+ });
+ paraTransition.play();
+ });
+
+ initRun = () -> {
+ // change tabs labels font color according to the selected color
+ pane.backgroundProperty().addListener((o, oldVal, newVal) -> {
+ if (concurrencyController.getAndSet(1) == -1) {
+ Color fontColor = ((Color) newVal.getFills().get(0).getFill()).grayscale()
+ .getRed() > 0.5 ? Color.valueOf(
+ "rgba(40, 40, 40, 0.87)") : Color.valueOf("rgba(255, 255, 255, 0.87)");
+ for (Node tabNode : tabs.lookupAll(".tab")) {
+ for (Node node : tabNode.lookupAll(".tab-label")) {
+ ((Label) node).setTextFill(fontColor);
+ }
+ for (Node node : tabNode.lookupAll(".jfx-rippler")) {
+ ((JFXRippler) node).setRipplerFill(fontColor);
+ }
+ }
+ ((Pane) tabs.lookup(".tab-selected-line")).setBackground(new Background(new BackgroundFill(fontColor, CornerRadii.EMPTY, Insets.EMPTY)));
+ pickerDecorator.lookupAll(".jfx-decorator-button").forEach(button -> {
+ ((JFXButton) button).setRipplerFill(fontColor);
+ ((SVGGlyph) ((JFXButton) button).getGraphic()).setFill(fontColor);
+ });
+
+ Color newColor = (Color) newVal.getFills().get(0).getFill();
+ String hex = String.format("#%02X%02X%02X",
+ (int) (newColor.getRed() * 255),
+ (int) (newColor.getGreen() * 255),
+ (int) (newColor.getBlue() * 255));
+ String rgb = String.format("rgba(%d, %d, %d, 1)",
+ (int) (newColor.getRed() * 255),
+ (int) (newColor.getGreen() * 255),
+ (int) (newColor.getBlue() * 255));
+ String hsb = String.format("hsl(%d, %d%%, %d%%)",
+ (int) (newColor.getHue()),
+ (int) (newColor.getSaturation() * 100),
+ (int) (newColor.getBrightness() * 100));
+
+ if (!userChange) {
+ systemChange = true;
+ rgbField.setText(rgb);
+ hsbField.setText(hsb);
+ hexField.setText(hex);
+ systemChange = false;
+ }
+ concurrencyController.getAndSet(-1);
+ }
+ });
+
+ // initial selected colors
+ Platform.runLater(() -> {
+ pane.setBackground(new Background(new BackgroundFill(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
+ CornerRadii.EMPTY,
+ Insets.EMPTY)));
+ ((Region) tabs.lookup(".tab-header-background")).setBackground(new Background(new BackgroundFill(
+ curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()),
+ CornerRadii.EMPTY,
+ Insets.EMPTY)));
+ Region tabsHeader = (Region) tabs.lookup(".tab-header-background");
+ pane.backgroundProperty().unbind();
+ tabsHeader.backgroundProperty().unbind();
+ tabsHeader.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
+ return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
+ CornerRadii.EMPTY,
+ Insets.EMPTY));
+ }, curvedColorPicker.selectedPath.get().fillProperty()));
+ pane.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
+ return new Background(new BackgroundFill(curvedColorPicker.selectedPath.get().getFill(),
+ CornerRadii.EMPTY,
+ Insets.EMPTY));
+ }, curvedColorPicker.selectedPath.get().fillProperty()));
+
+ // bind text field line color
+ rgbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
+ return pane.getBackground().getFills().get(0).getFill();
+ }, pane.backgroundProperty()));
+ hsbField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
+ return pane.getBackground().getFills().get(0).getFill();
+ }, pane.backgroundProperty()));
+ hexField.focusColorProperty().bind(Bindings.createObjectBinding(() -> {
+ return pane.getBackground().getFills().get(0).getFill();
+ }, pane.backgroundProperty()));
+
+
+ ((Pane) pickerDecorator.lookup(".jfx-decorator-buttons-container")).backgroundProperty()
+ .bind(Bindings.createObjectBinding(() -> {
+ return new Background(new BackgroundFill(
+ pane.getBackground()
+ .getFills()
+ .get(0)
+ .getFill(),
+ CornerRadii.EMPTY,
+ Insets.EMPTY));
+ }, pane.backgroundProperty()));
+
+ ((Pane) pickerDecorator.lookup(".jfx-decorator-content-container")).borderProperty()
+ .bind(Bindings.createObjectBinding(() -> {
+ return new Border(new BorderStroke(
+ pane.getBackground()
+ .getFills()
+ .get(0)
+ .getFill(),
+ BorderStrokeStyle.SOLID,
+ CornerRadii.EMPTY,
+ new BorderWidths(0,
+ 4,
+ 4,
+ 4)));
+ }, pane.backgroundProperty()));
+ });
+ };
+
+
+ container.getChildren().add(tabs);
+
+ this.getChildren().add(container);
+ this.setPadding(new Insets(0));
+
+ dialog.setScene(customScene);
+ final EventHandler keyEventListener = key -> {
+ switch (key.getCode()) {
+ case ESCAPE:
+ close();
+ break;
+ case ENTER:
+ updateColor();
+ break;
+ default:
+ break;
+ }
+ };
+ dialog.addEventHandler(KeyEvent.ANY, keyEventListener);
+ }
+
+ private void updateColor() {
+ close();
+ this.customColorProperty.set(curvedColorPicker.getColor(curvedColorPicker.getSelectedIndex()));
+ this.onSave.run();
+ }
+
+ private void updateColorFromUserInput(String colorWebString) {
+ if (!systemChange) {
+ userChange = true;
+ try {
+ curvedColorPicker.setColor(Color.valueOf(colorWebString));
+ } catch (IllegalArgumentException ignored) {
+ // if color is not valid then do nothing
+ }
+ userChange = false;
+ }
+ }
+
+ private void close() {
+ dialog.setScene(null);
+ dialog.close();
+ }
+
+ public void setCurrentColor(Color currentColor) {
+ this.currentColorProperty.set(currentColor);
+ }
+
+ Color getCurrentColor() {
+ return currentColorProperty.get();
+ }
+
+ ObjectProperty customColorProperty() {
+ return customColorProperty;
+ }
+
+ void setCustomColor(Color color) {
+ customColorProperty.set(color);
+ }
+
+ Color getCustomColor() {
+ return customColorProperty.get();
+ }
+
+ public Runnable getOnSave() {
+ return onSave;
+ }
+
+ public void setOnSave(Runnable onSave) {
+ this.onSave = onSave;
+ }
+
+ public void setOnHidden(EventHandler onHidden) {
+ dialog.setOnHidden(onHidden);
+ }
+
+ public void show() {
+ dialog.setOpacity(0);
+ // pickerDecorator.setOpacity(0);
+ if (dialog.getOwner() != null) {
+ dialog.widthProperty().addListener(positionAdjuster);
+ dialog.heightProperty().addListener(positionAdjuster);
+ positionAdjuster.invalidated(null);
+ }
+ if (dialog.getScene() == null) {
+ dialog.setScene(customScene);
+ }
+ curvedColorPicker.preAnimate();
+ dialog.show();
+ if (initOnce) {
+ initRun.run();
+ initOnce = false;
+ }
+
+ Timeline timeline = new Timeline(new KeyFrame(Duration.millis(120),
+ new KeyValue(dialog.opacityProperty(),
+ 1,
+ Interpolator.EASE_BOTH)));
+ timeline.setOnFinished((finish) -> curvedColorPicker.animate());
+ timeline.play();
+ }
+
+
+ // add option to show color picker using JFX Dialog
+ private InvalidationListener positionAdjuster = new InvalidationListener() {
+ @Override
+ public void invalidated(Observable ignored) {
+ if (Double.isNaN(dialog.getWidth()) || Double.isNaN(dialog.getHeight())) {
+ return;
+ }
+ dialog.widthProperty().removeListener(positionAdjuster);
+ dialog.heightProperty().removeListener(positionAdjuster);
+ fixPosition();
+ }
+ };
+
+ private void fixPosition() {
+ Window w = dialog.getOwner();
+ Screen s = com.sun.javafx.util.Utils.getScreen(w);
+ Rectangle2D sb = s.getBounds();
+ double xR = w.getX() + w.getWidth();
+ double xL = w.getX() - dialog.getWidth();
+ double x;
+ double y;
+ if (sb.getMaxX() >= xR + dialog.getWidth()) {
+ x = xR;
+ } else if (sb.getMinX() <= xL) {
+ x = xL;
+ } else {
+ x = Math.max(sb.getMinX(), sb.getMaxX() - dialog.getWidth());
+ }
+ y = Math.max(sb.getMinY(), Math.min(sb.getMaxY() - dialog.getHeight(), w.getY()));
+ dialog.setX(x);
+ dialog.setY(y);
+ }
+
+ @Override
+ public void layoutChildren() {
+ super.layoutChildren();
+ if (dialog.getMinWidth() > 0 && dialog.getMinHeight() > 0) {
+ return;
+ }
+ double minWidth = Math.max(0, computeMinWidth(getHeight()) + (dialog.getWidth() - customScene.getWidth()));
+ double minHeight = Math.max(0, computeMinHeight(getWidth()) + (dialog.getHeight() - customScene.getHeight()));
+ dialog.setMinWidth(minWidth);
+ dialog.setMinHeight(minHeight);
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXGenericPickerSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXGenericPickerSkin.java
new file mode 100644
index 000000000..bc61774dd
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/skins/JFXGenericPickerSkin.java
@@ -0,0 +1,242 @@
+/*
+ * 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.adapters.ReflectionHelper;
+import com.jfoenix.controls.behavior.JFXGenericPickerBehavior;
+import com.sun.javafx.binding.ExpressionHelper;
+import com.sun.javafx.event.EventHandlerManager;
+import com.sun.javafx.stage.WindowEventDispatcher;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanPropertyBase;
+import javafx.beans.value.ChangeListener;
+import javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.scene.control.ComboBoxBase;
+import javafx.scene.control.PopupControl;
+import javafx.scene.control.TextField;
+import javafx.scene.control.skin.ComboBoxBaseSkin;
+import javafx.scene.control.skin.ComboBoxPopupControl;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Pane;
+import javafx.stage.Window;
+
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.VarHandle;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import static org.jackhuang.hmcl.util.logging.Logger.LOG;
+
+public abstract class JFXGenericPickerSkin extends ComboBoxPopupControl {
+
+ private final EventHandler mouseEnteredEventHandler;
+ private final EventHandler mousePressedEventHandler;
+ private final EventHandler mouseReleasedEventHandler;
+ private final EventHandler mouseExitedEventHandler;
+
+ protected JFXGenericPickerBehavior behavior;
+
+ // reference of the arrow button node in getChildren (not the actual field)
+ protected Pane arrowButton;
+ protected PopupControl popup;
+
+ public JFXGenericPickerSkin(ComboBoxBase comboBoxBase) {
+ super(comboBoxBase);
+ behavior = new JFXGenericPickerBehavior(comboBoxBase);
+
+ removeParentFakeFocusListener(comboBoxBase);
+
+ this.mouseEnteredEventHandler = event -> behavior.mouseEntered(event);
+ this.mousePressedEventHandler = event -> {
+ behavior.mousePressed(event);
+ event.consume();
+ };
+ this.mouseReleasedEventHandler = event -> {
+ behavior.mouseReleased(event);
+ event.consume();
+ };
+ this.mouseExitedEventHandler = event -> behavior.mouseExited(event);
+
+ arrowButton = (Pane) getChildren().get(0);
+
+ parentArrowEventHandlerTerminator.accept("mouseEnteredEventHandler", MouseEvent.MOUSE_ENTERED);
+ parentArrowEventHandlerTerminator.accept("mousePressedEventHandler", MouseEvent.MOUSE_PRESSED);
+ parentArrowEventHandlerTerminator.accept("mouseReleasedEventHandler", MouseEvent.MOUSE_RELEASED);
+ parentArrowEventHandlerTerminator.accept("mouseExitedEventHandler", MouseEvent.MOUSE_EXITED);
+ this.unregisterChangeListeners(comboBoxBase.editableProperty());
+
+ updateArrowButtonListeners();
+ registerChangeListener(comboBoxBase.editableProperty(), obs -> {
+ updateArrowButtonListeners();
+ reflectUpdateDisplayArea();
+ });
+
+ removeParentPopupHandlers();
+
+ popup = ReflectionHelper.getFieldContent(ComboBoxPopupControl.class, this, "popup");
+ }
+
+ @Override
+ public void dispose() {
+ super.dispose();
+ if (this.behavior != null) {
+ this.behavior.dispose();
+ }
+ }
+
+
+ /***************************************************************************
+ * *
+ * Reflections internal API *
+ * *
+ **************************************************************************/
+
+ private final BiConsumer> parentArrowEventHandlerTerminator = (handlerName, eventType) -> {
+ try {
+ EventHandler handler = ReflectionHelper.getFieldContent(ComboBoxBaseSkin.class, this, handlerName);
+ arrowButton.removeEventHandler(eventType, handler);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ };
+
+ private static final VarHandle READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER =
+ findVarHandle(ReadOnlyBooleanPropertyBase.class, "helper", ExpressionHelper.class);
+
+ /// @author Glavo
+ private static VarHandle findVarHandle(Class> targetClass, String fieldName, Class> type) {
+ try {
+ return MethodHandles.privateLookupIn(targetClass, MethodHandles.lookup()).findVarHandle(targetClass, fieldName, type);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ LOG.warning("Failed to get var handle", e);
+ return null;
+ }
+ }
+
+ private void removeParentFakeFocusListener(ComboBoxBase comboBoxBase) {
+ // handle FakeFocusField cast exception
+ try {
+ final ReadOnlyBooleanProperty focusedProperty = comboBoxBase.focusedProperty();
+ //noinspection unchecked
+ ExpressionHelper value = (ExpressionHelper) READ_ONLY_BOOLEAN_PROPERTY_BASE_HELPER.get(focusedProperty);
+ ChangeListener super Boolean>[] changeListeners = ReflectionHelper.getFieldContent(value.getClass(), value, "changeListeners");
+ // remove parent focus listener to prevent editor class cast exception
+ for (int i = changeListeners.length - 1; i > 0; i--) {
+ if (changeListeners[i] != null && changeListeners[i].getClass().getName().contains("ComboBoxPopupControl")) {
+ focusedProperty.removeListener(changeListeners[i]);
+ break;
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void removeParentPopupHandlers() {
+ try {
+ PopupControl popup = ReflectionHelper.invoke(ComboBoxPopupControl.class, this, "getPopup");
+ popup.setOnAutoHide(event -> behavior.onAutoHide(popup));
+ WindowEventDispatcher dispatcher = ReflectionHelper.invoke(Window.class, popup, "getInternalEventDispatcher");
+ Map compositeEventHandlersMap = ReflectionHelper.getFieldContent(EventHandlerManager.class, dispatcher.getEventHandlerManager(), "eventHandlerMap");
+ compositeEventHandlersMap.remove(MouseEvent.MOUSE_CLICKED);
+// CompositeEventHandler compositeEventHandler = (CompositeEventHandler) compositeEventHandlersMap.get(MouseEvent.MOUSE_CLICKED);
+// Object obj = fieldConsumer.apply(()->CompositeEventHandler.class.getDeclaredField("firstRecord"),compositeEventHandler);
+// EventHandler handler = (EventHandler) fieldConsumer.apply(() -> obj.getClass().getDeclaredField("eventHandler"), obj);
+// popup.removeEventHandler(MouseEvent.MOUSE_CLICKED, handler);
+ popup.addEventHandler(MouseEvent.MOUSE_CLICKED, click -> behavior.onAutoHide(popup));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void updateArrowButtonListeners() {
+ if (getSkinnable().isEditable()) {
+ arrowButton.addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
+ arrowButton.addEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
+ arrowButton.addEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
+ arrowButton.addEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
+ } else {
+ arrowButton.removeEventHandler(MouseEvent.MOUSE_ENTERED, mouseEnteredEventHandler);
+ arrowButton.removeEventHandler(MouseEvent.MOUSE_PRESSED, mousePressedEventHandler);
+ arrowButton.removeEventHandler(MouseEvent.MOUSE_RELEASED, mouseReleasedEventHandler);
+ arrowButton.removeEventHandler(MouseEvent.MOUSE_EXITED, mouseExitedEventHandler);
+ }
+ }
+
+
+ /***************************************************************************
+ * *
+ * Reflections internal API for ComboBoxPopupControl *
+ * *
+ **************************************************************************/
+
+ private final HashMap parentCachedMethods = new HashMap<>();
+
+ Function methodSupplier = name -> {
+ if (!parentCachedMethods.containsKey(name)) {
+ try {
+ Method method = ComboBoxPopupControl.class.getDeclaredMethod(name);
+ method.setAccessible(true);
+ parentCachedMethods.put(name, method);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return parentCachedMethods.get(name);
+ };
+
+ final Consumer methodInvoker = method -> {
+ try {
+ method.invoke(this);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ };
+
+ final Function methodReturnInvoker = method -> {
+ try {
+ return method.invoke(this);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return null;
+ };
+
+ protected void reflectUpdateDisplayArea() {
+ methodInvoker.accept(methodSupplier.apply("updateDisplayArea"));
+ }
+
+ protected void reflectSetTextFromTextFieldIntoComboBoxValue() {
+ methodInvoker.accept(methodSupplier.apply("setTextFromTextFieldIntoComboBoxValue"));
+ }
+
+ protected TextField reflectGetEditableInputNode() {
+ return (TextField) methodReturnInvoker.apply(methodSupplier.apply("getEditableInputNode"));
+ }
+
+ protected void reflectUpdateDisplayNode() {
+ methodInvoker.accept(methodSupplier.apply("updateDisplayNode"));
+ }
+}
diff --git a/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java b/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java
new file mode 100644
index 000000000..2a08dcfa0
--- /dev/null
+++ b/HMCL/src/main/java/com/jfoenix/utils/JFXNodeUtils.java
@@ -0,0 +1,64 @@
+/*
+ * 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.utils;
+
+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 java.util.Locale;
+
+/// @author Shadi Shaheen
+/// @version 1.0
+/// @since 2017-02-11
+public final class JFXNodeUtils {
+
+ public static void updateBackground(Background newBackground, Region nodeToUpdate) {
+ updateBackground(newBackground, nodeToUpdate, Color.BLACK);
+ }
+
+ public static void updateBackground(Background newBackground, Region nodeToUpdate, Paint fill) {
+ if (newBackground != null && !newBackground.getFills().isEmpty()) {
+ final BackgroundFill[] fills = new BackgroundFill[newBackground.getFills().size()];
+ for (int i = 0; i < newBackground.getFills().size(); i++) {
+ BackgroundFill bf = newBackground.getFills().get(i);
+ fills[i] = new BackgroundFill(fill, bf.getRadii(), bf.getInsets());
+ }
+ nodeToUpdate.setBackground(new Background(fills));
+ }
+ }
+
+ public static String colorToHex(Color c) {
+ if (c != null) {
+ return String.format((Locale) null, "#%02X%02X%02X",
+ Math.round(c.getRed() * 255),
+ Math.round(c.getGreen() * 255),
+ Math.round(c.getBlue() * 255));
+ } else {
+ return null;
+ }
+ }
+
+ private JFXNodeUtils() {
+ }
+
+}
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java
index d20502727..dc7c61d10 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/MultiFileItem.java
@@ -17,6 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;
+import com.jfoenix.controls.JFXColorPicker;
import com.jfoenix.controls.JFXRadioButton;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
@@ -301,7 +302,7 @@ public final class MultiFileItem extends VBox {
}
public static final class PaintOption extends Option {
- private final ColorPicker colorPicker = new ColorPicker();
+ private final ColorPicker colorPicker = new JFXColorPicker();
public PaintOption(String title, T data) {
super(title, data);
diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java
index 67a2678b0..3f0d75845 100644
--- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java
+++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/PersonalizationPage.java
@@ -17,10 +17,7 @@
*/
package org.jackhuang.hmcl.ui.main;
-import com.jfoenix.controls.JFXButton;
-import com.jfoenix.controls.JFXComboBox;
-import com.jfoenix.controls.JFXSlider;
-import com.jfoenix.controls.JFXTextField;
+import com.jfoenix.controls.*;
import com.jfoenix.effects.JFXDepthManager;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
@@ -93,7 +90,7 @@ public class PersonalizationPage extends StackPane {
themeColorPickerContainer.setMinHeight(30);
themePane.setRight(themeColorPickerContainer);
- ColorPicker picker = new ColorPicker(Color.web(Theme.getTheme().getColor()));
+ ColorPicker picker = new JFXColorPicker(Color.web(Theme.getTheme().getColor()));
picker.getCustomColors().setAll(Theme.SUGGESTED_COLORS);
picker.setOnAction(e ->
config().setTheme(Theme.custom(Theme.getColorDisplayName(picker.getValue()))));