1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.oracle.javafx.scenebuilder.kit.util.control.paintpicker.colorpicker;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.util.control.paintpicker.PaintPicker.Mode;
  35 import com.oracle.javafx.scenebuilder.kit.util.control.paintpicker.PaintPickerController;
  36 import com.oracle.javafx.scenebuilder.kit.util.control.paintpicker.gradientpicker.GradientPicker;
  37 import com.oracle.javafx.scenebuilder.kit.util.control.paintpicker.gradientpicker.GradientPickerStop;
  38 
  39 import java.io.IOException;
  40 import java.util.logging.Level;
  41 import java.util.logging.Logger;
  42 
  43 import javafx.beans.value.ChangeListener;
  44 import javafx.event.ActionEvent;
  45 import javafx.fxml.FXMLLoader;
  46 import javafx.scene.control.ScrollPane;
  47 import javafx.fxml.FXML;
  48 import javafx.geometry.Bounds;
  49 import javafx.scene.control.Slider;
  50 import javafx.scene.control.TextField;
  51 import javafx.scene.input.MouseEvent;
  52 import javafx.scene.layout.Region;
  53 import javafx.scene.layout.StackPane;
  54 import javafx.scene.layout.VBox;
  55 import javafx.scene.paint.Color;
  56 import javafx.scene.paint.LinearGradient;
  57 import javafx.scene.paint.Paint;
  58 import javafx.scene.paint.RadialGradient;
  59 import javafx.scene.shape.Circle;
  60 
  61 /**
  62  * Controller class for the color part of the paint editor.
  63  */
  64 public class ColorPicker extends VBox {
  65 
  66     @FXML
  67     private Region chip_region;
  68     @FXML
  69     private Region alpha_region;
  70     @FXML
  71     private ScrollPane picker_scrollpane;
  72     @FXML
  73     private Region picker_region;
  74     @FXML
  75     private StackPane picker_handle_stackpane;
  76     @FXML
  77     private Circle picker_handle_chip_circle;
  78     @FXML
  79     private Slider hue_slider;
  80     @FXML
  81     private Slider alpha_slider;
  82     @FXML
  83     private TextField hue_textfield;
  84     @FXML
  85     private TextField saturation_textfield;
  86     @FXML
  87     private TextField brightness_textfield;
  88     @FXML
  89     private TextField red_textfield;
  90     @FXML
  91     private TextField green_textfield;
  92     @FXML
  93     private TextField blue_textfield;
  94     @FXML
  95     private TextField alpha_textfield;
  96     @FXML
  97     private TextField hexa_textfield;
  98 
  99     private final PaintPickerController paintPickerController;
 100     private boolean updating = false;
 101 
 102     public ColorPicker(PaintPickerController pe) {
 103         paintPickerController = pe;
 104         initialize();
 105     }
 106 
 107     public Color getValue() {
 108         double hue = Double.valueOf(hue_textfield.getText());
 109         double saturation = Double.valueOf(saturation_textfield.getText()) / 100.0;
 110         double brightness = Double.valueOf(brightness_textfield.getText()) / 100.0;
 111         double alpha = Double.valueOf(alpha_textfield.getText());
 112         return Color.hsb(hue, saturation, brightness, alpha);
 113     }
 114 
 115     public void updateUI(final Color color) {
 116         double hue = color.getHue();
 117         double saturation = color.getSaturation();
 118         double brightness = color.getBrightness();
 119         double alpha = color.getOpacity();
 120         updateUI(hue, saturation, brightness, alpha);
 121     }
 122 
 123     /**
 124      * Private
 125      */
 126     private void initialize() {
 127 
 128         final FXMLLoader loader = new FXMLLoader();
 129         loader.setLocation(ColorPicker.class.getResource("ColorPicker.fxml")); //NOI18N
 130         loader.setController(this);
 131         loader.setRoot(this);
 132         try {
 133             loader.load();
 134         } catch (IOException ex) {
 135             Logger.getLogger(ColorPicker.class.getName()).log(Level.SEVERE, null, ex);
 136         }
 137 
 138         assert hue_slider != null;
 139         assert picker_region != null;
 140         assert hue_textfield != null;
 141         assert saturation_textfield != null;
 142         assert brightness_textfield != null;
 143         assert alpha_textfield != null;
 144         assert red_textfield != null;
 145         assert green_textfield != null;
 146         assert blue_textfield != null;
 147         assert alpha_slider != null;
 148 
 149         hue_slider.setStyle(makeHueSliderCSS()); // Make the grad for hue slider
 150 
 151         // Investigate why height + width listeners do not work
 152         // Indeed, the picker_handle_stackpane bounds may still be null at this point
 153         // UPDATE BELOW TO BE CALLED ONCE ONLY AT DISPLAY TIME
 154         picker_region.boundsInParentProperty().addListener((ChangeListener<Bounds>) (ov, oldb, newb) -> {
 155             picker_scrollpane.setHvalue(0.5);
 156             picker_scrollpane.setVvalue(0.5);
 157             // Init time only
 158             final Paint paint = paintPickerController.getPaintProperty();
 159             if (paint instanceof Color) {
 160                 updateUI((Color) paint);
 161             } else if (paint instanceof LinearGradient
 162                     || paint instanceof RadialGradient) {
 163                 final GradientPicker gradientPicker = paintPickerController.getGradientPicker();
 164                 final GradientPickerStop gradientPickerStop = gradientPicker.getSelectedStop();
 165                 // Update the color preview with the color of the selected stop
 166                 if (gradientPickerStop != null) {
 167                     updateUI(gradientPickerStop.getColor());
 168                 }
 169             }
 170         });
 171 
 172         final ChangeListener<Boolean> onHSBFocusedChange = (ov, oldValue, newValue) -> {
 173             if (newValue == false) {
 174                 // Update UI
 175                 final Color color = updateUI_OnHSBChange();
 176                 // Update model
 177                 setPaintProperty(color);
 178             }
 179         };
 180         final ChangeListener<Boolean> onRGBFocusedChange = (ov, oldValue, newValue) -> {
 181             if (newValue == false) {
 182                 // Update UI
 183                 final Color color = updateUI_OnRGBChange();
 184                 // Update model
 185                 setPaintProperty(color);
 186             }
 187         };
 188         final ChangeListener<Boolean> onHexaFocusedChange = (ov, oldValue, newValue) -> {
 189             if (newValue == false) {
 190                 try {
 191                     // Update UI
 192                     final Color color = updateUI_OnHexaChange();
 193                     // Update model
 194                     setPaintProperty(color);
 195                 } catch (IllegalArgumentException iae) {
 196                     handleHexaException();
 197                 }
 198             }
 199         };
 200 
 201         // TextField ON FOCUS LOST event handler
 202         hue_textfield.focusedProperty().addListener(onHSBFocusedChange);
 203         saturation_textfield.focusedProperty().addListener(onHSBFocusedChange);
 204         brightness_textfield.focusedProperty().addListener(onHSBFocusedChange);
 205         alpha_textfield.focusedProperty().addListener(onHSBFocusedChange);
 206         red_textfield.focusedProperty().addListener(onRGBFocusedChange);
 207         green_textfield.focusedProperty().addListener(onRGBFocusedChange);
 208         blue_textfield.focusedProperty().addListener(onRGBFocusedChange);
 209         hexa_textfield.focusedProperty().addListener(onHexaFocusedChange);
 210 
 211         // Slider ON VALUE CHANGE event handler
 212         hue_slider.valueProperty().addListener((ChangeListener<Number>) (ov, oldValue, newValue) -> {
 213             if (updating == true) {
 214                 return;
 215             }
 216             double hue = newValue.doubleValue();
 217             // retrieve HSB TextFields values
 218             double saturation = Double.valueOf(saturation_textfield.getText()) / 100.0;
 219             double brightness = Double.valueOf(brightness_textfield.getText()) / 100.0;
 220             double alpha = Double.valueOf(alpha_textfield.getText());
 221             // Update UI
 222             final Color color = updateUI(hue, saturation, brightness, alpha);
 223             // Update model
 224             setPaintProperty(color);
 225         });
 226         alpha_slider.valueProperty().addListener((ChangeListener<Number>) (ov, oldValue, newValue) -> {
 227             if (updating == true) {
 228                 return;
 229             }
 230             double alpha = newValue.doubleValue();
 231             // retrieve HSB TextFields values
 232             double hue = Double.valueOf(hue_textfield.getText());
 233             double saturation = Double.valueOf(saturation_textfield.getText()) / 100.0;
 234             double brightness = Double.valueOf(brightness_textfield.getText()) / 100.0;
 235             // Update UI
 236             final Color color = updateUI(hue, saturation, brightness, alpha);
 237             // Update model
 238             setPaintProperty(color);
 239         });
 240 
 241         final ChangeListener<Boolean> liveUpdateListener = (ov, oldValue, newValue) -> paintPickerController.setLiveUpdate(newValue);
 242         picker_region.pressedProperty().addListener(liveUpdateListener);
 243         hue_slider.pressedProperty().addListener(liveUpdateListener);
 244         alpha_slider.pressedProperty().addListener(liveUpdateListener);
 245     }
 246 
 247     /**
 248      * When updating the color picker, we may update :
 249      * - either the color of the paint picker itself (Color mode)
 250      * - or the color of the selected stop (LinearGradient or RadialGradient mode)
 251      *
 252      * @param color
 253      */
 254     private void setPaintProperty(Color color) {
 255         final Mode mode = paintPickerController.getMode();
 256         final Paint paint;
 257         switch (mode) {
 258             case COLOR:
 259                 paint = color;
 260                 break;
 261             case LINEAR:
 262             case RADIAL:
 263                 final GradientPicker gradientPicker = paintPickerController.getGradientPicker();
 264                 final GradientPickerStop gradientPickerStop = gradientPicker.getSelectedStop();
 265                 // Set the color of the selected stop
 266                 if (gradientPickerStop != null) {
 267                     gradientPickerStop.setColor(color);
 268                 }
 269                 // Update gradient preview
 270                 paint = gradientPicker.getValue(mode);
 271                 gradientPicker.updatePreview(paint);
 272                 break;
 273             default:
 274                 paint = null;
 275                 break;
 276         }
 277         paintPickerController.setPaintProperty(paint);
 278     }
 279 
 280     @FXML
 281     void onActionHue(ActionEvent event) {
 282         onHSBChange(event);
 283         event.consume();
 284     }
 285 
 286     @FXML
 287     void onActionSaturation(ActionEvent event) {
 288         onHSBChange(event);
 289         event.consume();
 290     }
 291 
 292     @FXML
 293     void onActionBrightness(ActionEvent event) {
 294         onHSBChange(event);
 295         event.consume();
 296     }
 297 
 298     @FXML
 299     void onActionAlpha(ActionEvent event) {
 300         onHSBChange(event);
 301         event.consume();
 302     }
 303 
 304     @FXML
 305     void onActionRed(ActionEvent event) {
 306         onRGBChange(event);
 307         event.consume();
 308     }
 309 
 310     @FXML
 311     void onActionGreen(ActionEvent event) {
 312         onRGBChange(event);
 313         event.consume();
 314     }
 315 
 316     @FXML
 317     void onActionBlue(ActionEvent event) {
 318         onRGBChange(event);
 319         event.consume();
 320     }
 321 
 322     @FXML
 323     void onActionHexa(ActionEvent event) {
 324         onHexaChange(event);
 325         event.consume();
 326     }
 327 
 328     private void onHSBChange(ActionEvent event) {
 329         // Update UI
 330         final Color color = updateUI_OnHSBChange();
 331         final Object source = event.getSource();
 332         assert source instanceof TextField;
 333         ((TextField) source).selectAll();
 334         // Update model
 335         setPaintProperty(color);
 336     }
 337 
 338     private void onRGBChange(ActionEvent event) {
 339         // Update UI
 340         final Color color = updateUI_OnRGBChange();
 341         final Object source = event.getSource();
 342         assert source instanceof TextField;
 343         ((TextField) source).selectAll();
 344         // Update model
 345         setPaintProperty(color);
 346     }
 347 
 348     private void onHexaChange(ActionEvent event) {
 349         try {
 350             // Update UI
 351             final Color color = updateUI_OnHexaChange();
 352             final Object source = event.getSource();
 353             assert source instanceof TextField;
 354             ((TextField) source).selectAll();
 355             // Update model
 356             setPaintProperty(color);
 357         } catch (IllegalArgumentException iae) {
 358             handleHexaException();
 359         }
 360     }
 361 
 362     @FXML
 363     void onPickerRegionPressed(MouseEvent e) {
 364         double mx = e.getX();
 365         double my = e.getY();
 366         // Update UI
 367         final Color color = updateUI_OnPickerChange(mx, my);
 368         // Update model
 369         setPaintProperty(color);
 370     }
 371 
 372     @FXML
 373     void onPickerRegionDragged(MouseEvent e) {
 374         double mx = e.getX();
 375         double my = e.getY();
 376         // Update UI
 377         final Color color = updateUI_OnPickerChange(mx, my);
 378         // Update model
 379         setPaintProperty(color);
 380     }
 381 
 382     private Color updateUI_OnPickerChange(double x, double y) {
 383         double w = picker_region.getWidth();
 384         double h = picker_region.getHeight();
 385         double hue = Double.valueOf(hue_textfield.getText());
 386         double saturation = x / w;
 387         double brightness = 1.0 - (y / h);
 388         double alpha = Double.valueOf(alpha_textfield.getText());
 389         return updateUI(hue, saturation, brightness, alpha);
 390     }
 391 
 392     private Color updateUI_OnHSBChange() {
 393         // retrieve HSB TextFields values
 394         double hue = Double.valueOf(hue_textfield.getText());
 395         double saturation = Double.valueOf(saturation_textfield.getText()) / 100.0;
 396         double brightness = Double.valueOf(brightness_textfield.getText()) / 100.0;
 397         double alpha = Double.valueOf(alpha_textfield.getText());
 398         return updateUI(hue, saturation, brightness, alpha);
 399     }
 400 
 401     private Color updateUI_OnRGBChange() {
 402         // retrieve RGB TextFields values
 403         int red = Double.valueOf(red_textfield.getText()).intValue();
 404         int green = Double.valueOf(green_textfield.getText()).intValue();
 405         int blue = Double.valueOf(blue_textfield.getText()).intValue();
 406         // retrieve HSB values from RGB values
 407         final Color color = Color.rgb(red, green, blue);
 408         double hue = color.getHue();
 409         double saturation = color.getSaturation();
 410         double brightness = color.getBrightness();
 411         double alpha = Double.valueOf(alpha_textfield.getText());
 412         return updateUI(hue, saturation, brightness, alpha);
 413     }
 414 
 415     private Color updateUI_OnHexaChange() {
 416         // retrieve Hexa TextField value
 417         final String hexa = hexa_textfield.getText().trim();
 418         final Color color = Color.web(hexa);
 419         double hue = color.getHue();
 420         double saturation = color.getSaturation();
 421         double brightness = color.getBrightness();
 422         double alpha = Double.valueOf(alpha_textfield.getText());
 423         return updateUI(hue, saturation, brightness, alpha);
 424     }
 425 
 426     private Color updateUI(double hue, double saturation, double brightness, double alpha) {
 427 
 428         updating = true;
 429 
 430         // update the HSB values so they are within range
 431         hue = PaintPickerController.clamp(0, hue, 360);
 432         saturation = PaintPickerController.clamp(0, saturation, 1);
 433         brightness = PaintPickerController.clamp(0, brightness, 1);
 434         alpha = PaintPickerController.clamp(0, alpha, 1);
 435         // make an rgb color from the hsb
 436         final Color color = Color.hsb(hue, saturation, brightness, alpha);
 437         int red = (int) (color.getRed() * 255);
 438         int green = (int) (color.getGreen() * 255);
 439         int blue = (int) (color.getBlue() * 255);
 440         final String hexa = String.format("#%02x%02x%02x", red, green, blue); //NOI18N
 441 
 442         // Set TextFields value
 443         hue_textfield.setText(String.valueOf((int) hue));
 444         saturation_textfield.setText(String.valueOf((int) (saturation * 100)));
 445         brightness_textfield.setText(String.valueOf((int) (brightness * 100)));
 446         double alpha_rounded = round(alpha, 100); // 2 decimals rounding
 447         alpha_textfield.setText(Double.toString(alpha_rounded));
 448         red_textfield.setText(Integer.toString(red));
 449         green_textfield.setText(Integer.toString(green));
 450         blue_textfield.setText(Integer.toString(blue));
 451         hexa_textfield.setText(hexa);
 452 
 453         // Set the background color of the chips
 454         final StringBuilder sb = new StringBuilder();
 455         sb.append("hsb("); //NOI18N
 456         sb.append(hue);
 457         sb.append(", "); //NOI18N
 458         sb.append(saturation * 100);
 459         sb.append("%, "); //NOI18N
 460         sb.append(brightness * 100);
 461         sb.append("%, "); //NOI18N
 462         sb.append(alpha);
 463         sb.append(")"); //NOI18N
 464         final String hsbCssValue = sb.toString();
 465         final String chipStyle = "-fx-background-color: " + hsbCssValue; //NOI18N
 466         chip_region.setStyle(chipStyle);
 467         picker_handle_chip_circle.setFill(Color.rgb(red, green, blue));
 468         final String alphaChipStyle = "-fx-background-color: " //NOI18N
 469                 + "linear-gradient(to right, transparent, " + hsbCssValue + ")"; //NOI18N
 470         alpha_region.setStyle(alphaChipStyle);
 471 
 472         // Set the background color of the picker region
 473         // (force saturation and brightness to 100% - don't add opacity)
 474         final String pickerRegionStyle = "-fx-background-color: hsb(" //NOI18N
 475                 + hue + ", 100%, 100%, 1.0);"; //NOI18N
 476         picker_region.setStyle(pickerRegionStyle);
 477 
 478         // Position the picker dot
 479         double xSat = picker_region.getWidth() * saturation; // Saturation is on x axis
 480         double yBri = picker_region.getHeight() * (1.0 - brightness); // Brightness is on y axis (reversed as white is top)
 481         double xPos = (picker_region.getBoundsInParent().getMinX() + xSat) - picker_handle_stackpane.getWidth() / 2;
 482         double yPos = (picker_region.getBoundsInParent().getMinY() + yBri) - picker_handle_stackpane.getHeight() / 2;
 483         picker_handle_stackpane.setLayoutX(xPos);
 484         picker_handle_stackpane.setLayoutY(yPos);
 485 
 486         // Set the Sliders value
 487         hue_slider.adjustValue(hue);
 488         alpha_slider.adjustValue(alpha);
 489 
 490         updating = false;
 491         return color;
 492     }
 493 
 494     private String makeHueSliderCSS() {
 495         final StringBuilder sb = new StringBuilder();
 496         sb.append("-fx-background-color: linear-gradient(to right "); //NOI18N
 497         for (int i = 0; i < 12; i++) { // max 12 gradient stops
 498             sb.append(", hsb("); //NOI18N
 499             sb.append(i * (360 / 11));
 500             sb.append(", 100%, 100%)"); //NOI18N
 501         }
 502         sb.append(");"); //NOI18N
 503         return sb.toString();
 504     }
 505 
 506     private double round(double value, int roundingFactor) {
 507         double doubleRounded = Math.round(value * roundingFactor);
 508         return doubleRounded / roundingFactor;
 509     }
 510 
 511     private void handleHexaException() {
 512         paintPickerController.getDelegate().handleError(
 513                 "log.warning.color.creation.error.hexadecimal",
 514                 hexa_textfield.getText().trim());
 515         // Update UI to previous value
 516         final Color color;
 517         switch (paintPickerController.getMode()) {
 518             case COLOR:
 519                 assert paintPickerController.getPaintProperty() instanceof Color;
 520                 color = (Color) paintPickerController.getPaintProperty();
 521                 break;
 522             case LINEAR:
 523             case RADIAL:
 524                 final GradientPicker gradientPicker = paintPickerController.getGradientPicker();
 525                 if (gradientPicker.getGradientStops().isEmpty() == false) {
 526                     final GradientPickerStop stop = paintPickerController.getGradientPicker().getSelectedStop();
 527                     assert stop != null;
 528                     color = stop.getColor();
 529                 } else {
 530                     color = Color.BLACK;
 531                 }
 532                 break;
 533             default:
 534                 color = null;
 535                 assert false;
 536         }
 537         assert color != null;
 538         updateUI(color);
 539         hexa_textfield.selectAll();
 540     }
 541 }