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