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