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 }