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 }