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