1 /*
   2  * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javafx.scene.control.skin;
  27 
  28 import com.sun.javafx.scene.NodeHelper;
  29 import com.sun.javafx.scene.ParentHelper;
  30 import com.sun.javafx.scene.control.CustomColorDialog;
  31 import com.sun.javafx.scene.control.skin.Utils;
  32 import com.sun.javafx.scene.traversal.Algorithm;
  33 import com.sun.javafx.scene.traversal.Direction;
  34 import com.sun.javafx.scene.traversal.ParentTraversalEngine;
  35 import com.sun.javafx.scene.traversal.TraversalContext;
  36 import javafx.collections.FXCollections;
  37 import javafx.collections.ListChangeListener;
  38 import javafx.collections.ObservableList;
  39 import javafx.event.ActionEvent;
  40 import javafx.event.Event;
  41 import javafx.event.EventHandler;
  42 import javafx.geometry.Bounds;
  43 import javafx.geometry.NodeOrientation;
  44 import javafx.geometry.Pos;
  45 import javafx.geometry.Side;
  46 import javafx.scene.Node;
  47 import javafx.scene.control.ColorPicker;
  48 import javafx.scene.control.ContextMenu;
  49 import javafx.scene.control.Hyperlink;
  50 import javafx.scene.control.Label;
  51 import javafx.scene.control.MenuItem;
  52 import javafx.scene.control.PopupControl;
  53 import javafx.scene.control.Separator;
  54 import javafx.scene.control.Tooltip;
  55 import javafx.scene.input.KeyCode;
  56 import javafx.scene.input.KeyEvent;
  57 import javafx.scene.input.MouseButton;
  58 import javafx.scene.input.MouseEvent;
  59 import javafx.scene.layout.GridPane;
  60 import javafx.scene.layout.Region;
  61 import javafx.scene.layout.StackPane;
  62 import javafx.scene.layout.VBox;
  63 import javafx.scene.paint.Color;
  64 import javafx.scene.shape.Rectangle;
  65 import javafx.scene.shape.StrokeType;
  66 
  67 import java.util.List;
  68 
  69 import static com.sun.javafx.scene.control.Properties.getColorPickerString;
  70 
  71 // Not public API - this is (presently) an implementation detail only
  72 class ColorPalette extends Region {
  73 
  74     private static final int SQUARE_SIZE = 15;
  75 
  76     // package protected for testing purposes
  77     ColorPickerGrid colorPickerGrid;
  78     final Hyperlink customColorLink = new Hyperlink(getColorPickerString("customColorLink"));
  79     CustomColorDialog customColorDialog = null;
  80 
  81     private ColorPicker colorPicker;
  82     private final GridPane standardColorGrid = new GridPane();
  83     private final GridPane customColorGrid = new GridPane();
  84     private final Separator separator = new Separator();
  85     private final Label customColorLabel = new Label(getColorPickerString("customColorLabel"));
  86 
  87     private PopupControl popupControl;
  88     private ColorSquare focusedSquare;
  89     private ContextMenu contextMenu = null;
  90 
  91     private Color mouseDragColor = null;
  92     private boolean dragDetected = false;
  93 
  94     // Metrics for custom colors
  95     private int customColorNumber = 0;
  96     private int customColorRows = 0;
  97     private int customColorLastRowLength = 0;
  98 
  99     private final ColorSquare hoverSquare = new ColorSquare();
 100 
 101     public ColorPalette(final ColorPicker colorPicker) {
 102         getStyleClass().add("color-palette-region");
 103         this.colorPicker = colorPicker;
 104         colorPickerGrid = new ColorPickerGrid();
 105         colorPickerGrid.getChildren().get(0).requestFocus();
 106         customColorLabel.setAlignment(Pos.CENTER_LEFT);
 107         customColorLink.setPrefWidth(colorPickerGrid.prefWidth(-1));
 108         customColorLink.setAlignment(Pos.CENTER);
 109         customColorLink.setFocusTraversable(true);
 110         customColorLink.setVisited(true); // so that it always appears blue
 111         customColorLink.setOnAction(new EventHandler<ActionEvent>() {
 112             @Override public void handle(ActionEvent t) {
 113                 if (customColorDialog == null) {
 114                     customColorDialog = new CustomColorDialog(popupControl);
 115                     customColorDialog.customColorProperty().addListener((ov, t1, t2) -> {
 116                         colorPicker.setValue(customColorDialog.customColorProperty().get());
 117                     });
 118                     customColorDialog.setOnSave(() -> {
 119                         Color customColor = customColorDialog.customColorProperty().get();
 120                         buildCustomColors();
 121                         colorPicker.getCustomColors().add(customColor);
 122                         updateSelection(customColor);
 123                         Event.fireEvent(colorPicker, new ActionEvent());
 124                         colorPicker.hide();
 125                     });
 126                     customColorDialog.setOnUse(() -> {
 127                         Event.fireEvent(colorPicker, new ActionEvent());
 128                         colorPicker.hide();
 129                     });
 130                 }
 131                 customColorDialog.setCurrentColor(colorPicker.valueProperty().get());
 132                 if (popupControl != null) popupControl.setAutoHide(false);
 133                 customColorDialog.show();
 134                  customColorDialog.setOnHidden(event -> {
 135                     if (popupControl != null) popupControl.setAutoHide(true);
 136                  });
 137             }
 138         });
 139 
 140         initNavigation();
 141 
 142         buildStandardColors();
 143         standardColorGrid.getStyleClass().add("color-picker-grid");
 144         standardColorGrid.setVisible(true);
 145         customColorGrid.getStyleClass().add("color-picker-grid");
 146         customColorGrid.setVisible(false);
 147         buildCustomColors();
 148         colorPicker.getCustomColors().addListener(new ListChangeListener<Color>() {
 149             @Override public void onChanged(Change<? extends Color> change) {
 150                 buildCustomColors();
 151             }
 152         });
 153 
 154         VBox paletteBox = new VBox();
 155         paletteBox.getStyleClass().add("color-palette");
 156         paletteBox.getChildren().addAll(standardColorGrid, colorPickerGrid, customColorLabel, customColorGrid, separator, customColorLink);
 157 
 158         hoverSquare.setMouseTransparent(true);
 159         hoverSquare.getStyleClass().addAll("hover-square");
 160         setFocusedSquare(null);
 161 
 162         getChildren().addAll(paletteBox, hoverSquare);
 163     }
 164 
 165     private void setFocusedSquare(ColorSquare square) {
 166         if (square == focusedSquare) {
 167             return;
 168         }
 169         focusedSquare = square;
 170 
 171         hoverSquare.setVisible(focusedSquare != null);
 172         if (focusedSquare == null) {
 173             return;
 174         }
 175 
 176         if (!focusedSquare.isFocused()) {
 177             focusedSquare.requestFocus();
 178         }
 179 
 180         hoverSquare.rectangle.setFill(focusedSquare.rectangle.getFill());
 181 
 182         Bounds b = square.localToScene(square.getLayoutBounds());
 183 
 184         double x = b.getMinX();
 185         double y = b.getMinY();
 186 
 187         double xAdjust;
 188         double scaleAdjust = hoverSquare.getScaleX() == 1.0 ? 0 : hoverSquare.getWidth() / 4.0;
 189 
 190         if (colorPicker.getEffectiveNodeOrientation() == NodeOrientation.RIGHT_TO_LEFT) {
 191             x = focusedSquare.getLayoutX();
 192             xAdjust = -focusedSquare.getWidth() + scaleAdjust;
 193         } else {
 194             xAdjust = focusedSquare.getWidth() / 2.0 + scaleAdjust;
 195         }
 196 
 197         hoverSquare.setLayoutX(snapPositionX(x) - xAdjust);
 198         hoverSquare.setLayoutY(snapPositionY(y) - focusedSquare.getHeight() / 2.0 + (hoverSquare.getScaleY() == 1.0 ? 0 : focusedSquare.getHeight() / 4.0));
 199     }
 200 
 201     private void buildStandardColors() {
 202         final Color[] STANDARD_COLORS = {
 203                 Color.TEAL,
 204                 Color.BLUE,
 205                 Color.NAVY,
 206                 Color.FUCHSIA,
 207                 Color.PURPLE,
 208                 Color.RED,
 209                 Color.MAROON,
 210                 Color.ORANGE,
 211                 Color.GOLD,
 212                 Color.YELLOW,
 213                 Color.OLIVE,
 214                 Color.GREEN
 215         };
 216 
 217         standardColorGrid.getChildren().clear();
 218 
 219         for (int i = 0; i < NUM_OF_COLUMNS; i++) {
 220             standardColorGrid.add(new ColorSquare(STANDARD_COLORS[i], i, ColorType.STANDARD), i, 0);
 221         }
 222         standardColorGrid.setVisible(true);
 223     }
 224 
 225     private void buildCustomColors() {
 226         final ObservableList<Color> customColors = colorPicker.getCustomColors();
 227         customColorNumber = customColors.size();
 228 
 229         customColorGrid.getChildren().clear();
 230         if (customColors.isEmpty()) {
 231             customColorLabel.setVisible(false);
 232             customColorLabel.setManaged(false);
 233             customColorGrid.setVisible(false);
 234             customColorGrid.setManaged(false);
 235             return;
 236         } else {
 237             customColorLabel.setVisible(true);
 238             customColorLabel.setManaged(true);
 239             customColorGrid.setVisible(true);
 240             customColorGrid.setManaged(true);
 241             if (contextMenu == null) {
 242                 MenuItem item = new MenuItem(getColorPickerString("removeColor"));
 243                 item.setOnAction(e -> {
 244                     ColorSquare square = (ColorSquare)contextMenu.getOwnerNode();
 245                     customColors.remove(square.rectangle.getFill());
 246                     buildCustomColors();
 247                 });
 248                 contextMenu = new ContextMenu(item);
 249             }
 250         }
 251 
 252         int customColumnIndex = 0;
 253         int customRowIndex = 0;
 254         int remainingSquares = customColors.size() % NUM_OF_COLUMNS;
 255         int numEmpty = (remainingSquares == 0) ? 0 : NUM_OF_COLUMNS - remainingSquares;
 256         customColorLastRowLength = remainingSquares == 0 ? 12 : remainingSquares;
 257 
 258         for (int i = 0; i < customColors.size(); i++) {
 259             Color c = customColors.get(i);
 260             ColorSquare square = new ColorSquare(c, i, ColorType.CUSTOM);
 261             square.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
 262                 if (e.getCode() == KeyCode.DELETE) {
 263                     customColors.remove(square.rectangle.getFill());
 264                     buildCustomColors();
 265                 }
 266             });
 267             customColorGrid.add(square, customColumnIndex, customRowIndex);
 268             customColumnIndex++;
 269             if (customColumnIndex == NUM_OF_COLUMNS) {
 270                 customColumnIndex = 0;
 271                 customRowIndex++;
 272             }
 273         }
 274         for (int i = 0; i < numEmpty; i++) {
 275             ColorSquare emptySquare = new ColorSquare();
 276             emptySquare.setDisable(true);
 277             customColorGrid.add(emptySquare, customColumnIndex, customRowIndex);
 278             customColumnIndex++;
 279         }
 280         customColorRows = customRowIndex + 1;
 281         requestLayout();
 282 
 283     }
 284 
 285     private void initNavigation() {
 286         setOnKeyPressed(ke -> {
 287             switch (ke.getCode()) {
 288                 case SPACE:
 289                 case ENTER:
 290                     processSelectKey(ke);
 291                     ke.consume();
 292                     break;
 293                 default: // no-op
 294             }
 295         });
 296 
 297         ParentHelper.setTraversalEngine(this, new ParentTraversalEngine(this, new Algorithm() {
 298             @Override
 299             public Node select(Node owner, Direction dir, TraversalContext context) {
 300                 final Node subsequentNode = context.selectInSubtree(context.getRoot(), owner, dir);
 301                 switch (dir) {
 302                     case NEXT:
 303                     case NEXT_IN_LINE:
 304                     case PREVIOUS:
 305                         return subsequentNode;
 306                     // Here, we need to intercept the standard algorithm in a few cases to get the desired traversal
 307                     // For right or left direction we want to continue on the next or previous row respectively
 308                     // For up and down, the custom color panel might be skipped by the standard algorithm (if not wide enough
 309                     // to be between the current color and custom color button), so we need to include it in the path explicitly.
 310                     case LEFT:
 311                     case RIGHT:
 312                     case UP:
 313                     case DOWN:
 314                         if (owner instanceof ColorSquare) {
 315                             Node result =  processArrow((ColorSquare)owner, dir);
 316                             return result != null ? result : subsequentNode;
 317                         } else {
 318                             return subsequentNode;
 319                         }
 320                 }
 321                 return null;
 322             }
 323 
 324             private Node processArrow(ColorSquare owner, Direction dir) {
 325                 int row = 0;
 326                 int column = 0;
 327 
 328                 if (owner.colorType == ColorType.STANDARD) {
 329                     row = 0;
 330                     column = owner.index;
 331                 } else {
 332                     row = owner.index / NUM_OF_COLUMNS;
 333                     column = owner.index % NUM_OF_COLUMNS;
 334                 }
 335 
 336                 // Adjust the direction according to color picker orientation
 337                 dir = dir.getDirectionForNodeOrientation(colorPicker.getEffectiveNodeOrientation());
 338                 // This returns true for all the cases which we need to override
 339                 if (isAtBorder(dir, row, column, (owner.colorType == ColorType.CUSTOM))) {
 340                     // There's no other node in the direction from the square, so we need to continue on some other row
 341                     // or cycle
 342                     int subsequentRow = row;
 343                     int subsequentColumn = column;
 344                     boolean subSequentSquareCustom = (owner.colorType == ColorType.CUSTOM);
 345                     boolean subSequentSquareStandard = (owner.colorType == ColorType.STANDARD);
 346                     switch (dir) {
 347                         case LEFT:
 348                         case RIGHT:
 349                             // The next row is either the first or the last, except when cycling in custom colors, the last row
 350                             // might have different number of columns
 351                             if (owner.colorType == ColorType.STANDARD) {
 352                                 subsequentRow = 0;
 353                                 subsequentColumn = (dir == Direction.LEFT)? NUM_OF_COLUMNS - 1 : 0;
 354                             }
 355                             else if (owner.colorType == ColorType.CUSTOM) {
 356                                 subsequentRow = Math.floorMod(dir == Direction.LEFT ? row - 1 : row + 1, customColorRows);
 357                                 subsequentColumn = dir == Direction.LEFT ? subsequentRow == customColorRows - 1 ?
 358                                         customColorLastRowLength - 1 : NUM_OF_COLUMNS - 1 : 0;
 359                             } else {
 360                                 subsequentRow = Math.floorMod(dir == Direction.LEFT ? row - 1 : row + 1, NUM_OF_ROWS);
 361                                 subsequentColumn = dir == Direction.LEFT ? NUM_OF_COLUMNS - 1 : 0;
 362                             }
 363                             break;
 364                         case UP: // custom color are not handled here
 365                             if (owner.colorType == ColorType.NORMAL && row == 0) {
 366                                 subSequentSquareStandard = true;
 367                             }
 368                             break;
 369                         case DOWN: // custom color are not handled here
 370                             if (customColorNumber > 0) {
 371                                 subSequentSquareCustom = true;
 372                                 subsequentRow = 0;
 373                                 subsequentColumn = customColorRows > 1 ? column : Math.min(customColorLastRowLength - 1, column);
 374                                 break;
 375                             } else {
 376                                 return null; // Let the default algorithm handle this
 377                             }
 378 
 379                     }
 380                     if (subSequentSquareCustom) {
 381                         return customColorGrid.getChildren().get(subsequentRow * NUM_OF_COLUMNS + subsequentColumn);
 382                     } else if (subSequentSquareStandard) {
 383                         return standardColorGrid.getChildren().get(subsequentColumn);
 384                     } else {
 385                         return colorPickerGrid.getChildren().get(subsequentRow * NUM_OF_COLUMNS + subsequentColumn);
 386                     }
 387                 }
 388                 return null;
 389             }
 390 
 391             private boolean isAtBorder(Direction dir, int row, int column, boolean custom) {
 392                 switch (dir) {
 393                     case LEFT:
 394                         return column == 0;
 395                     case RIGHT:
 396                         return custom && row == customColorRows - 1 ?
 397                                 column == customColorLastRowLength - 1 : column == NUM_OF_COLUMNS - 1;
 398                     case UP:
 399                         return !custom && row == 0;
 400                     case DOWN:
 401                         return !custom && row == NUM_OF_ROWS - 1;
 402                 }
 403                 return false;
 404             }
 405 
 406             @Override
 407             public Node selectFirst(TraversalContext context) {
 408                 return standardColorGrid.getChildren().get(0);
 409             }
 410 
 411             @Override
 412             public Node selectLast(TraversalContext context) {
 413                 return customColorLink;
 414             }
 415         }));
 416     }
 417 
 418     private void processSelectKey(KeyEvent ke) {
 419         if (focusedSquare != null) focusedSquare.selectColor(ke);
 420     }
 421 
 422     public void setPopupControl(PopupControl pc) {
 423         this.popupControl = pc;
 424     }
 425 
 426     public ColorPickerGrid getColorGrid() {
 427         return colorPickerGrid;
 428     }
 429 
 430     public boolean isCustomColorDialogShowing() {
 431         if (customColorDialog != null) return customColorDialog.isVisible();
 432         return false;
 433     }
 434 
 435 
 436     enum ColorType {
 437         NORMAL,
 438         STANDARD,
 439         CUSTOM
 440     };
 441 
 442     class ColorSquare extends StackPane {
 443         Rectangle rectangle;
 444         int index;
 445         boolean isEmpty;
 446         ColorType colorType = ColorType.NORMAL;
 447 
 448         public ColorSquare() {
 449             this(null, -1, ColorType.NORMAL);
 450         }
 451 
 452         public ColorSquare(Color color, int index) {
 453             this(color, index, ColorType.NORMAL);
 454         }
 455 
 456         public ColorSquare(Color color, int index, ColorType type) {
 457             // Add style class to handle selected color square
 458             getStyleClass().add("color-square");
 459             if (color != null) {
 460                 setFocusTraversable(true);
 461 
 462                 focusedProperty().addListener((s, ov, nv) -> {
 463                     setFocusedSquare(nv ? this : null);
 464                 });
 465 
 466                 addEventHandler(MouseEvent.MOUSE_ENTERED, event -> {
 467                     setFocusedSquare(ColorSquare.this);
 468                 });
 469                 addEventHandler(MouseEvent.MOUSE_EXITED, event -> {
 470                     setFocusedSquare(null);
 471                 });
 472 
 473                 addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
 474                     if (!dragDetected && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
 475                         if (!isEmpty) {
 476                             Color fill = (Color) rectangle.getFill();
 477                             colorPicker.setValue(fill);
 478                             colorPicker.fireEvent(new ActionEvent());
 479                             updateSelection(fill);
 480                             event.consume();
 481                         }
 482                         colorPicker.hide();
 483                     } else if (event.getButton() == MouseButton.SECONDARY ||
 484                             event.getButton() == MouseButton.MIDDLE) {
 485                         if ((colorType == ColorType.CUSTOM) && contextMenu != null) {
 486                             if (!contextMenu.isShowing()) {
 487                                 contextMenu.show(ColorSquare.this, Side.RIGHT, 0, 0);
 488                                 Utils.addMnemonics(contextMenu, ColorSquare.this.getScene(), NodeHelper.isShowMnemonics(colorPicker));
 489                             } else {
 490                                 contextMenu.hide();
 491                                 Utils.removeMnemonics(contextMenu, ColorSquare.this.getScene());
 492                             }
 493                         }
 494                     }
 495                 });
 496             }
 497             this.index = index;
 498             this.colorType = type;
 499             rectangle = new Rectangle(SQUARE_SIZE, SQUARE_SIZE);
 500             if (color == null) {
 501                 rectangle.setFill(Color.WHITE);
 502                 isEmpty = true;
 503             } else {
 504                 rectangle.setFill(color);
 505             }
 506 
 507             rectangle.setStrokeType(StrokeType.INSIDE);
 508 
 509             String tooltipStr = ColorPickerSkin.tooltipString(color);
 510             Tooltip.install(this, new Tooltip((tooltipStr == null) ? "" : tooltipStr));
 511 
 512             rectangle.getStyleClass().add("color-rect");
 513 
 514             getChildren().add(rectangle);
 515         }
 516 
 517         public void selectColor(KeyEvent event) {
 518             if (rectangle.getFill() != null) {
 519                 if (rectangle.getFill() instanceof Color) {
 520                     colorPicker.setValue((Color) rectangle.getFill());
 521                     colorPicker.fireEvent(new ActionEvent());
 522                 }
 523                 event.consume();
 524             }
 525             colorPicker.hide();
 526         }
 527     }
 528 
 529     // The skin can update selection if colorpicker value changes..
 530     public void updateSelection(Color color) {
 531         setFocusedSquare(null);
 532 
 533         // check standard colors
 534         for (Node n  : standardColorGrid.getChildren()) {
 535             ColorSquare c = (ColorSquare) n;
 536             if (c.rectangle.getFill().equals(color)) {
 537                 setFocusedSquare(c);
 538                 return;
 539             }
 540         }
 541 
 542         for (ColorSquare c : colorPickerGrid.getSquares()) {
 543             if (c.rectangle.getFill().equals(color)) {
 544                 setFocusedSquare(c);
 545                 return;
 546             }
 547         }
 548         // check custom colors
 549         for (Node n : customColorGrid.getChildren()) {
 550             ColorSquare c = (ColorSquare) n;
 551             if (c.rectangle.getFill().equals(color)) {
 552                 setFocusedSquare(c);
 553                 return;
 554             }
 555         }
 556     }
 557 
 558     class ColorPickerGrid extends GridPane {
 559 
 560         private final List<ColorSquare> squares;
 561 
 562         public ColorPickerGrid() {
 563             getStyleClass().add("color-picker-grid");
 564             setId("ColorCustomizerColorGrid");
 565             int columnIndex = 0, rowIndex = 0;
 566             squares = FXCollections.observableArrayList();
 567             final int numColors = RAW_VALUES.length / 3;
 568             Color[] colors = new Color[numColors];
 569             for (int i = 0; i < numColors; i++) {
 570                 colors[i] = new Color(RAW_VALUES[(i * 3)] / 255,
 571                         RAW_VALUES[(i * 3) + 1] / 255, RAW_VALUES[(i * 3) + 2] / 255,
 572                         1.0);
 573                 ColorSquare cs = new ColorSquare(colors[i], i);
 574                 squares.add(cs);
 575             }
 576 
 577             for (ColorSquare square : squares) {
 578                 add(square, columnIndex, rowIndex);
 579                 columnIndex++;
 580                 if (columnIndex == NUM_OF_COLUMNS) {
 581                     columnIndex = 0;
 582                     rowIndex++;
 583                 }
 584             }
 585             setOnMouseDragged(t -> {
 586                 if (!dragDetected) {
 587                     dragDetected = true;
 588                     mouseDragColor = colorPicker.getValue();
 589                 }
 590                 int xIndex = com.sun.javafx.util.Utils.clamp(0,
 591                         (int)t.getX()/(SQUARE_SIZE + 1), NUM_OF_COLUMNS - 1);
 592                 int yIndex = com.sun.javafx.util.Utils.clamp(0,
 593                         (int)t.getY()/(SQUARE_SIZE + 1), NUM_OF_ROWS - 1);
 594                 int index = xIndex + yIndex*NUM_OF_COLUMNS;
 595                 colorPicker.setValue((Color) squares.get(index).rectangle.getFill());
 596                 updateSelection(colorPicker.getValue());
 597             });
 598             addEventHandler(MouseEvent.MOUSE_RELEASED, t -> {
 599                 if(colorPickerGrid.getBoundsInLocal().contains(t.getX(), t.getY())) {
 600                     updateSelection(colorPicker.getValue());
 601                     colorPicker.fireEvent(new ActionEvent());
 602                     colorPicker.hide();
 603                 } else {
 604                     // restore color as mouse release happened outside the grid.
 605                     if (mouseDragColor != null) {
 606                         colorPicker.setValue(mouseDragColor);
 607                         updateSelection(mouseDragColor);
 608                     }
 609                 }
 610                 dragDetected = false;
 611             });
 612         }
 613 
 614         public List<ColorSquare> getSquares() {
 615             return squares;
 616         }
 617 
 618         @Override protected double computePrefWidth(double height) {
 619             return (SQUARE_SIZE + 1)*NUM_OF_COLUMNS;
 620         }
 621 
 622         @Override protected double computePrefHeight(double width) {
 623             return (SQUARE_SIZE + 1)*NUM_OF_ROWS;
 624         }
 625     }
 626 
 627     private static final int NUM_OF_COLUMNS = 12;
 628     private static double[] RAW_VALUES = {
 629             // WARNING: always make sure the number of colors is a divisable by NUM_OF_COLUMNS
 630             255, 255, 255, // first row
 631             242, 242, 242,
 632             230, 230, 230,
 633             204, 204, 204,
 634             179, 179, 179,
 635             153, 153, 153,
 636             128, 128, 128,
 637             102, 102, 102,
 638             77, 77, 77,
 639             51, 51, 51,
 640             26, 26, 26,
 641             0, 0, 0,
 642             0, 51, 51, // second row
 643             0, 26, 128,
 644             26, 0, 104,
 645             51, 0, 51,
 646             77, 0, 26,
 647             153, 0, 0,
 648             153, 51, 0,
 649             153, 77, 0,
 650             153, 102, 0,
 651             153, 153, 0,
 652             102, 102, 0,
 653             0, 51, 0,
 654             26, 77, 77, // third row
 655             26, 51, 153,
 656             51, 26, 128,
 657             77, 26, 77,
 658             102, 26, 51,
 659             179, 26, 26,
 660             179, 77, 26,
 661             179, 102, 26,
 662             179, 128, 26,
 663             179, 179, 26,
 664             128, 128, 26,
 665             26, 77, 26,
 666             51, 102, 102, // fourth row
 667             51, 77, 179,
 668             77, 51, 153,
 669             102, 51, 102,
 670             128, 51, 77,
 671             204, 51, 51,
 672             204, 102, 51,
 673             204, 128, 51,
 674             204, 153, 51,
 675             204, 204, 51,
 676             153, 153, 51,
 677             51, 102, 51,
 678             77, 128, 128, // fifth row
 679             77, 102, 204,
 680             102, 77, 179,
 681             128, 77, 128,
 682             153, 77, 102,
 683             230, 77, 77,
 684             230, 128, 77,
 685             230, 153, 77,
 686             230, 179, 77,
 687             230, 230, 77,
 688             179, 179, 77,
 689             77, 128, 77,
 690             102, 153, 153, // sixth row
 691             102, 128, 230,
 692             128, 102, 204,
 693             153, 102, 153,
 694             179, 102, 128,
 695             255, 102, 102,
 696             255, 153, 102,
 697             255, 179, 102,
 698             255, 204, 102,
 699             255, 255, 77,
 700             204, 204, 102,
 701             102, 153, 102,
 702             128, 179, 179, // seventh row
 703             128, 153, 255,
 704             153, 128, 230,
 705             179, 128, 179,
 706             204, 128, 153,
 707             255, 128, 128,
 708             255, 153, 128,
 709             255, 204, 128,
 710             255, 230, 102,
 711             255, 255, 102,
 712             230, 230, 128,
 713             128, 179, 128,
 714             153, 204, 204, // eigth row
 715             153, 179, 255,
 716             179, 153, 255,
 717             204, 153, 204,
 718             230, 153, 179,
 719             255, 153, 153,
 720             255, 179, 128,
 721             255, 204, 153,
 722             255, 230, 128,
 723             255, 255, 128,
 724             230, 230, 153,
 725             153, 204, 153,
 726             179, 230, 230, // ninth row
 727             179, 204, 255,
 728             204, 179, 255,
 729             230, 179, 230,
 730             230, 179, 204,
 731             255, 179, 179,
 732             255, 179, 153,
 733             255, 230, 179,
 734             255, 230, 153,
 735             255, 255, 153,
 736             230, 230, 179,
 737             179, 230, 179,
 738             204, 255, 255, // tenth row
 739             204, 230, 255,
 740             230, 204, 255,
 741             255, 204, 255,
 742             255, 204, 230,
 743             255, 204, 204,
 744             255, 204, 179,
 745             255, 230, 204,
 746             255, 255, 179,
 747             255, 255, 204,
 748             230, 230, 204,
 749             204, 255, 204
 750     };
 751 
 752     private static final int NUM_OF_COLORS = RAW_VALUES.length / 3;
 753     private static final int NUM_OF_ROWS = NUM_OF_COLORS / NUM_OF_COLUMNS;
 754 }