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