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 }