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