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 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[] 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()))));