1 /*
   2  * Copyright (c) 2012, 2017, 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             emptySquare.setDisable(true);
 248             customColorGrid.add(emptySquare, customColumnIndex, customRowIndex);
 249             customColumnIndex++;
 250         }
 251         customColorRows = customRowIndex + 1;
 252         requestLayout();
 253 
 254     }
 255 
 256     private void initNavigation() {
 257         setOnKeyPressed(ke -> {
 258             switch (ke.getCode()) {
 259                 case SPACE:
 260                 case ENTER:
 261                     processSelectKey(ke);
 262                     ke.consume();
 263                     break;
 264                 default: // no-op
 265             }
 266         });
 267 
 268         ParentHelper.setTraversalEngine(this, new ParentTraversalEngine(this, new Algorithm() {
 269             @Override
 270             public Node select(Node owner, Direction dir, TraversalContext context) {
 271                 final Node subsequentNode = context.selectInSubtree(context.getRoot(), owner, dir);
 272                 switch (dir) {
 273                     case NEXT:
 274                     case NEXT_IN_LINE:
 275                     case PREVIOUS:
 276                         return subsequentNode;
 277                     // Here, we need to intercept the standard algorithm in a few cases to get the desired traversal
 278                     // For right or left direction we want to continue on the next or previous row respectively
 279                     // For up and down, the custom color panel might be skipped by the standard algorithm (if not wide enough
 280                     // to be between the current color and custom color button), so we need to include it in the path explicitly.
 281                     case LEFT:
 282                     case RIGHT:
 283                     case UP:
 284                     case DOWN:
 285                         if (owner instanceof ColorSquare) {
 286                             Node result =  processArrow((ColorSquare)owner, dir);
 287                             return result != null ? result : subsequentNode;
 288                         } else {
 289                             return subsequentNode;
 290                         }
 291                 }
 292                 return null;
 293             }
 294 
 295             private Node processArrow(ColorSquare owner, Direction dir) {
 296                 final int row = owner.index / NUM_OF_COLUMNS;
 297                 final int column = owner.index % NUM_OF_COLUMNS;
 298 
 299                 // Adjust the direction according to color picker orientation
 300                 dir = dir.getDirectionForNodeOrientation(colorPicker.getEffectiveNodeOrientation());
 301                 // This returns true for all the cases which we need to override
 302                 if (isAtBorder(dir, row, column, owner.isCustom)) {
 303                     // There's no other node in the direction from the square, so we need to continue on some other row
 304                     // or cycle
 305                     int subsequentRow = row;
 306                     int subsequentColumn = column;
 307                     boolean subSequentSquareCustom = owner.isCustom;
 308                     switch (dir) {
 309                         case LEFT:
 310                         case RIGHT:
 311                             // The next row is either the first or the last, except when cycling in custom colors, the last row
 312                             // might have different number of columns
 313                             if (owner.isCustom) {
 314                                 subsequentRow = Math.floorMod(dir == Direction.LEFT ? row - 1 : row + 1, customColorRows);
 315                                 subsequentColumn = dir == Direction.LEFT ? subsequentRow == customColorRows - 1 ?
 316                                         customColorLastRowLength - 1 : NUM_OF_COLUMNS - 1 : 0;
 317                             } else {
 318                                 subsequentRow = Math.floorMod(dir == Direction.LEFT ? row - 1 : row + 1, NUM_OF_ROWS);
 319                                 subsequentColumn = dir == Direction.LEFT ? NUM_OF_COLUMNS - 1 : 0;
 320                             }
 321                             break;
 322                         case UP: // custom color are not handled here
 323                             subsequentRow = NUM_OF_ROWS - 1;
 324                             break;
 325                         case DOWN: // custom color are not handled here
 326                             if (customColorNumber > 0) {
 327                                 subSequentSquareCustom = true;
 328                                 subsequentRow = 0;
 329                                 subsequentColumn = customColorRows > 1 ? column : Math.min(customColorLastRowLength - 1, column);
 330                                 break;
 331                             } else {
 332                                 return null; // Let the default algorith handle this
 333                             }
 334 
 335                     }
 336                     if (subSequentSquareCustom) {
 337                         return customColorGrid.getChildren().get(subsequentRow * NUM_OF_COLUMNS + subsequentColumn);
 338                     } else {
 339                         return colorPickerGrid.getChildren().get(subsequentRow * NUM_OF_COLUMNS + subsequentColumn);
 340                     }
 341                 }
 342                 return null;
 343             }
 344 
 345             private boolean isAtBorder(Direction dir, int row, int column, boolean custom) {
 346                 switch (dir) {
 347                     case LEFT:
 348                         return column == 0;
 349                     case RIGHT:
 350                         return custom && row == customColorRows - 1 ?
 351                                 column == customColorLastRowLength - 1 : column == NUM_OF_COLUMNS - 1;
 352                     case UP:
 353                         return !custom && row == 0;
 354                     case DOWN:
 355                         return !custom && row == NUM_OF_ROWS - 1;
 356                 }
 357                 return false;
 358             }
 359 
 360             @Override
 361             public Node selectFirst(TraversalContext context) {
 362                 return colorPickerGrid.getChildren().get(0);
 363             }
 364 
 365             @Override
 366             public Node selectLast(TraversalContext context) {
 367                 return customColorLink;
 368             }
 369         }));
 370     }
 371 
 372     private void processSelectKey(KeyEvent ke) {
 373         if (focusedSquare != null) focusedSquare.selectColor(ke);
 374     }
 375 
 376     public void setPopupControl(PopupControl pc) {
 377         this.popupControl = pc;
 378     }
 379 
 380     public ColorPickerGrid getColorGrid() {
 381         return colorPickerGrid;
 382     }
 383 
 384     public boolean isCustomColorDialogShowing() {
 385         if (customColorDialog != null) return customColorDialog.isVisible();
 386         return false;
 387     }
 388 
 389     class ColorSquare extends StackPane {
 390         Rectangle rectangle;
 391         int index;
 392         boolean isEmpty;
 393         boolean isCustom;
 394 
 395         public ColorSquare() {
 396             this(null, -1, false);
 397         }
 398 
 399         public ColorSquare(Color color, int index) {
 400             this(color, index, false);
 401         }
 402 
 403         public ColorSquare(Color color, int index, boolean isCustom) {
 404             // Add style class to handle selected color square
 405             getStyleClass().add("color-square");
 406             if (color != null) {
 407                 setFocusTraversable(true);
 408 
 409                 focusedProperty().addListener((s, ov, nv) -> {
 410                     setFocusedSquare(nv ? this : null);
 411                 });
 412 
 413                 addEventHandler(MouseEvent.MOUSE_ENTERED, event -> {
 414                     setFocusedSquare(ColorSquare.this);
 415                 });
 416                 addEventHandler(MouseEvent.MOUSE_EXITED, event -> {
 417                     setFocusedSquare(null);
 418                 });
 419 
 420                 addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
 421                     if (!dragDetected && event.getButton() == MouseButton.PRIMARY && event.getClickCount() == 1) {
 422                         if (!isEmpty) {
 423                             Color fill = (Color) rectangle.getFill();
 424                             colorPicker.setValue(fill);
 425                             colorPicker.fireEvent(new ActionEvent());
 426                             updateSelection(fill);
 427                             event.consume();
 428                         }
 429                         colorPicker.hide();
 430                     } else if (event.getButton() == MouseButton.SECONDARY ||
 431                             event.getButton() == MouseButton.MIDDLE) {
 432                         if (isCustom && contextMenu != null) {
 433                             if (!contextMenu.isShowing()) {
 434                                 contextMenu.show(ColorSquare.this, Side.RIGHT, 0, 0);
 435                                 Utils.addMnemonics(contextMenu, ColorSquare.this.getScene(), NodeHelper.isShowMnemonics(colorPicker));
 436                             } else {
 437                                 contextMenu.hide();
 438                                 Utils.removeMnemonics(contextMenu, ColorSquare.this.getScene());
 439                             }
 440                         }
 441                     }
 442                 });
 443             }
 444             this.index = index;
 445             this.isCustom = isCustom;
 446             rectangle = new Rectangle(SQUARE_SIZE, SQUARE_SIZE);
 447             if (color == null) {
 448                 rectangle.setFill(Color.WHITE);
 449                 isEmpty = true;
 450             } else {
 451                 rectangle.setFill(color);
 452             }
 453 
 454             rectangle.setStrokeType(StrokeType.INSIDE);
 455 
 456             String tooltipStr = ColorPickerSkin.tooltipString(color);
 457             Tooltip.install(this, new Tooltip((tooltipStr == null) ? "" : tooltipStr));
 458 
 459             rectangle.getStyleClass().add("color-rect");
 460 
 461             getChildren().add(rectangle);
 462         }
 463 
 464         public void selectColor(KeyEvent event) {
 465             if (rectangle.getFill() != null) {
 466                 if (rectangle.getFill() instanceof Color) {
 467                     colorPicker.setValue((Color) rectangle.getFill());
 468                     colorPicker.fireEvent(new ActionEvent());
 469                 }
 470                 event.consume();
 471             }
 472             colorPicker.hide();
 473         }
 474     }
 475 
 476     // The skin can update selection if colorpicker value changes..
 477     public void updateSelection(Color color) {
 478         setFocusedSquare(null);
 479 
 480         for (ColorSquare c : colorPickerGrid.getSquares()) {
 481             if (c.rectangle.getFill().equals(color)) {
 482                 setFocusedSquare(c);
 483                 return;
 484             }
 485         }
 486         // check custom colors
 487         for (Node n : customColorGrid.getChildren()) {
 488             ColorSquare c = (ColorSquare) n;
 489             if (c.rectangle.getFill().equals(color)) {
 490                 setFocusedSquare(c);
 491                 return;
 492             }
 493         }
 494     }
 495 
 496     class ColorPickerGrid extends GridPane {
 497 
 498         private final List<ColorSquare> squares;
 499 
 500         public ColorPickerGrid() {
 501             getStyleClass().add("color-picker-grid");
 502             setId("ColorCustomizerColorGrid");
 503             int columnIndex = 0, rowIndex = 0;
 504             squares = FXCollections.observableArrayList();
 505             final int numColors = RAW_VALUES.length / 3;
 506             Color[] colors = new Color[numColors];
 507             for (int i = 0; i < numColors; i++) {
 508                 colors[i] = new Color(RAW_VALUES[(i * 3)] / 255,
 509                         RAW_VALUES[(i * 3) + 1] / 255, RAW_VALUES[(i * 3) + 2] / 255,
 510                         1.0);
 511                 ColorSquare cs = new ColorSquare(colors[i], i);
 512                 squares.add(cs);
 513             }
 514 
 515             for (ColorSquare square : squares) {
 516                 add(square, columnIndex, rowIndex);
 517                 columnIndex++;
 518                 if (columnIndex == NUM_OF_COLUMNS) {
 519                     columnIndex = 0;
 520                     rowIndex++;
 521                 }
 522             }
 523             setOnMouseDragged(t -> {
 524                 if (!dragDetected) {
 525                     dragDetected = true;
 526                     mouseDragColor = colorPicker.getValue();
 527                 }
 528                 int xIndex = com.sun.javafx.util.Utils.clamp(0,
 529                         (int)t.getX()/(SQUARE_SIZE + 1), NUM_OF_COLUMNS - 1);
 530                 int yIndex = com.sun.javafx.util.Utils.clamp(0,
 531                         (int)t.getY()/(SQUARE_SIZE + 1), NUM_OF_ROWS - 1);
 532                 int index = xIndex + yIndex*NUM_OF_COLUMNS;
 533                 colorPicker.setValue((Color) squares.get(index).rectangle.getFill());
 534                 updateSelection(colorPicker.getValue());
 535             });
 536             addEventHandler(MouseEvent.MOUSE_RELEASED, t -> {
 537                 if(colorPickerGrid.getBoundsInLocal().contains(t.getX(), t.getY())) {
 538                     updateSelection(colorPicker.getValue());
 539                     colorPicker.fireEvent(new ActionEvent());
 540                     colorPicker.hide();
 541                 } else {
 542                     // restore color as mouse release happened outside the grid.
 543                     if (mouseDragColor != null) {
 544                         colorPicker.setValue(mouseDragColor);
 545                         updateSelection(mouseDragColor);
 546                     }
 547                 }
 548                 dragDetected = false;
 549             });
 550         }
 551 
 552         public List<ColorSquare> getSquares() {
 553             return squares;
 554         }
 555 
 556         @Override protected double computePrefWidth(double height) {
 557             return (SQUARE_SIZE + 1)*NUM_OF_COLUMNS;
 558         }
 559 
 560         @Override protected double computePrefHeight(double width) {
 561             return (SQUARE_SIZE + 1)*NUM_OF_ROWS;
 562         }
 563     }
 564 
 565     private static final int NUM_OF_COLUMNS = 12;
 566     private static double[] RAW_VALUES = {
 567             // WARNING: always make sure the number of colors is a divisable by NUM_OF_COLUMNS
 568             255, 255, 255, // first row
 569             242, 242, 242,
 570             230, 230, 230,
 571             204, 204, 204,
 572             179, 179, 179,
 573             153, 153, 153,
 574             128, 128, 128,
 575             102, 102, 102,
 576             77, 77, 77,
 577             51, 51, 51,
 578             26, 26, 26,
 579             0, 0, 0,
 580             0, 51, 51, // second row
 581             0, 26, 128,
 582             26, 0, 104,
 583             51, 0, 51,
 584             77, 0, 26,
 585             153, 0, 0,
 586             153, 51, 0,
 587             153, 77, 0,
 588             153, 102, 0,
 589             153, 153, 0,
 590             102, 102, 0,
 591             0, 51, 0,
 592             26, 77, 77, // third row
 593             26, 51, 153,
 594             51, 26, 128,
 595             77, 26, 77,
 596             102, 26, 51,
 597             179, 26, 26,
 598             179, 77, 26,
 599             179, 102, 26,
 600             179, 128, 26,
 601             179, 179, 26,
 602             128, 128, 26,
 603             26, 77, 26,
 604             51, 102, 102, // fourth row
 605             51, 77, 179,
 606             77, 51, 153,
 607             102, 51, 102,
 608             128, 51, 77,
 609             204, 51, 51,
 610             204, 102, 51,
 611             204, 128, 51,
 612             204, 153, 51,
 613             204, 204, 51,
 614             153, 153, 51,
 615             51, 102, 51,
 616             77, 128, 128, // fifth row
 617             77, 102, 204,
 618             102, 77, 179,
 619             128, 77, 128,
 620             153, 77, 102,
 621             230, 77, 77,
 622             230, 128, 77,
 623             230, 153, 77,
 624             230, 179, 77,
 625             230, 230, 77,
 626             179, 179, 77,
 627             77, 128, 77,
 628             102, 153, 153, // sixth row
 629             102, 128, 230,
 630             128, 102, 204,
 631             153, 102, 153,
 632             179, 102, 128,
 633             255, 102, 102,
 634             255, 153, 102,
 635             255, 179, 102,
 636             255, 204, 102,
 637             255, 255, 77,
 638             204, 204, 102,
 639             102, 153, 102,
 640             128, 179, 179, // seventh row
 641             128, 153, 255,
 642             153, 128, 230,
 643             179, 128, 179,
 644             204, 128, 153,
 645             255, 128, 128,
 646             255, 153, 128,
 647             255, 204, 128,
 648             255, 230, 102,
 649             255, 255, 102,
 650             230, 230, 128,
 651             128, 179, 128,
 652             153, 204, 204, // eigth row
 653             153, 179, 255,
 654             179, 153, 255,
 655             204, 153, 204,
 656             230, 153, 179,
 657             255, 153, 153,
 658             255, 179, 128,
 659             255, 204, 153,
 660             255, 230, 128,
 661             255, 255, 128,
 662             230, 230, 153,
 663             153, 204, 153,
 664             179, 230, 230, // ninth row
 665             179, 204, 255,
 666             204, 179, 255,
 667             230, 179, 230,
 668             230, 179, 204,
 669             255, 179, 179,
 670             255, 179, 153,
 671             255, 230, 179,
 672             255, 230, 153,
 673             255, 255, 153,
 674             230, 230, 179,
 675             179, 230, 179,
 676             204, 255, 255, // tenth row
 677             204, 230, 255,
 678             230, 204, 255,
 679             255, 204, 255,
 680             255, 204, 230,
 681             255, 204, 204,
 682             255, 204, 179,
 683             255, 230, 204,
 684             255, 255, 179,
 685             255, 255, 204,
 686             230, 230, 204,
 687             204, 255, 204
 688     };
 689 
 690     private static final int NUM_OF_COLORS = RAW_VALUES.length / 3;
 691     private static final int NUM_OF_ROWS = NUM_OF_COLORS / NUM_OF_COLUMNS;
 692 }