1 /*
   2  * Copyright (c) 2002, 2014, 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 package com.sun.java.swing.plaf.gtk;
  26 
  27 import java.awt.*;
  28 import java.awt.event.*;
  29 import java.awt.image.*;
  30 import javax.swing.*;
  31 import javax.swing.colorchooser.*;
  32 import javax.swing.event.*;
  33 import javax.swing.plaf.*;
  34 
  35 /**
  36  * A color chooser panel mimicking that of GTK's: a color wheel showing
  37  * hue and a triangle that varies saturation and brightness.
  38  *
  39  * @author Scott Violet
  40  */
  41 @SuppressWarnings("serial") // Superclass is not serializable across versions
  42 class GTKColorChooserPanel extends AbstractColorChooserPanel implements
  43               ChangeListener {
  44     private static final float PI_3 = (float)(Math.PI / 3);
  45 
  46     private ColorTriangle triangle;
  47     private JLabel lastLabel;
  48     private JLabel label;
  49 
  50     private JSpinner hueSpinner;
  51     private JSpinner saturationSpinner;
  52     private JSpinner valueSpinner;
  53 
  54     private JSpinner redSpinner;
  55     private JSpinner greenSpinner;
  56     private JSpinner blueSpinner;
  57 
  58     private JTextField colorNameTF;
  59 
  60     private boolean settingColor;
  61 
  62     // The colors are mirrored to avoid creep in adjusting an individual
  63     // value.
  64     private float hue;
  65     private float saturation;
  66     private float brightness;
  67 
  68 
  69 
  70     /**
  71      * Convenience method to transfer focus to the next child of component.
  72      */
  73     // PENDING: remove this when a variant of this is added to awt.
  74     static void compositeRequestFocus(Component component, boolean direction) {
  75         if (component instanceof Container) {
  76             Container container = (Container)component;
  77             if (container.isFocusCycleRoot()) {
  78                 FocusTraversalPolicy policy = container.
  79                                               getFocusTraversalPolicy();
  80                 Component comp = policy.getDefaultComponent(container);
  81                 if (comp!=null) {
  82                     comp.requestFocus();
  83                     return;
  84                 }
  85             }
  86             Container rootAncestor = container.getFocusCycleRootAncestor();
  87             if (rootAncestor!=null) {
  88                 FocusTraversalPolicy policy = rootAncestor.
  89                                                   getFocusTraversalPolicy();
  90                 Component comp;
  91 
  92                 if (direction) {
  93                     comp = policy.getComponentAfter(rootAncestor, container);
  94                 }
  95                 else {
  96                     comp = policy.getComponentBefore(rootAncestor, container);
  97                 }
  98                 if (comp != null) {
  99                     comp.requestFocus();
 100                     return;
 101                 }
 102             }
 103         }
 104         component.requestFocus();
 105     }
 106 
 107 
 108     /**
 109      * Returns a user presentable description of this GTKColorChooserPane.
 110      */
 111     public String getDisplayName() {
 112         return (String)UIManager.get("GTKColorChooserPanel.nameText");
 113     }
 114 
 115     /**
 116      * Returns the mnemonic to use with <code>getDisplayName</code>.
 117      */
 118     public int getMnemonic() {
 119         String m = (String)UIManager.get("GTKColorChooserPanel.mnemonic");
 120 
 121         if (m != null) {
 122             try {
 123                 int value = Integer.parseInt(m);
 124 
 125                 return value;
 126             } catch (NumberFormatException nfe) {}
 127         }
 128         return -1;
 129     }
 130 
 131     /**
 132      * Character to underline that represents the mnemonic.
 133      */
 134     public int getDisplayedMnemonicIndex() {
 135         String m = (String)UIManager.get(
 136                            "GTKColorChooserPanel.displayedMnemonicIndex");
 137 
 138         if (m != null) {
 139             try {
 140                 int value = Integer.parseInt(m);
 141 
 142                 return value;
 143             } catch (NumberFormatException nfe) {}
 144         }
 145         return -1;
 146     }
 147 
 148     public Icon getSmallDisplayIcon() {
 149         return null;
 150     }
 151 
 152     public Icon getLargeDisplayIcon() {
 153         return null;
 154     }
 155 
 156     public void uninstallChooserPanel(JColorChooser enclosingChooser) {
 157         super.uninstallChooserPanel(enclosingChooser);
 158         removeAll();
 159     }
 160 
 161     /**
 162      * Builds and configures the widgets for the GTKColorChooserPanel.
 163      */
 164     protected void buildChooser() {
 165         triangle = new ColorTriangle();
 166         triangle.setName("GTKColorChooserPanel.triangle");
 167 
 168         // PENDING: when we straighten out user setting opacity, this should
 169         // be changed.
 170         label = new OpaqueLabel();
 171         label.setName("GTKColorChooserPanel.colorWell");
 172         label.setOpaque(true);
 173         label.setMinimumSize(new Dimension(67, 32));
 174         label.setPreferredSize(new Dimension(67, 32));
 175         label.setMaximumSize(new Dimension(67, 32));
 176 
 177         // PENDING: when we straighten out user setting opacity, this should
 178         // be changed.
 179         lastLabel = new OpaqueLabel();
 180         lastLabel.setName("GTKColorChooserPanel.lastColorWell");
 181         lastLabel.setOpaque(true);
 182         lastLabel.setMinimumSize(new Dimension(67, 32));
 183         lastLabel.setPreferredSize(new Dimension(67, 32));
 184         lastLabel.setMaximumSize(new Dimension(67, 32));
 185 
 186         hueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 360, 1));
 187         configureSpinner(hueSpinner, "GTKColorChooserPanel.hueSpinner");
 188         saturationSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
 189         configureSpinner(saturationSpinner,
 190                          "GTKColorChooserPanel.saturationSpinner");
 191         valueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
 192         configureSpinner(valueSpinner, "GTKColorChooserPanel.valueSpinner");
 193         redSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
 194         configureSpinner(redSpinner, "GTKColorChooserPanel.redSpinner");
 195         greenSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
 196         configureSpinner(greenSpinner, "GTKColorChooserPanel.greenSpinner");
 197         blueSpinner = new JSpinner(new SpinnerNumberModel(0, 0, 255, 1));
 198         configureSpinner(blueSpinner, "GTKColorChooserPanel.blueSpinner");
 199 
 200         colorNameTF = new JTextField(8);
 201 
 202         setLayout(new GridBagLayout());
 203 
 204         add(this, "GTKColorChooserPanel.hue", hueSpinner, -1, -1);
 205         add(this, "GTKColorChooserPanel.red", redSpinner, -1, -1);
 206         add(this, "GTKColorChooserPanel.saturation", saturationSpinner, -1,-1);
 207         add(this, "GTKColorChooserPanel.green", greenSpinner, -1, -1);
 208         add(this, "GTKColorChooserPanel.value", valueSpinner, -1, -1);
 209         add(this, "GTKColorChooserPanel.blue", blueSpinner, -1, -1);
 210 
 211         add(new JSeparator(SwingConstants.HORIZONTAL), new
 212                   GridBagConstraints(1, 3, 4, 1, 1, 0,
 213                   GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
 214                   new Insets(14, 0, 0, 0), 0, 0));
 215 
 216         add(this, "GTKColorChooserPanel.colorName", colorNameTF, 0, 4);
 217 
 218         add(triangle, new GridBagConstraints(0, 0, 1, 5, 0, 0,
 219                       GridBagConstraints.LINE_START, GridBagConstraints.NONE,
 220                       new Insets(14, 20, 2, 9), 0, 0));
 221 
 222         Box hBox = Box.createHorizontalBox();
 223         hBox.add(lastLabel);
 224         hBox.add(label);
 225         add(hBox, new GridBagConstraints(0, 5, 1, 1, 0, 0,
 226                       GridBagConstraints.CENTER, GridBagConstraints.NONE,
 227                       new Insets(0, 0, 0, 0), 0, 0));
 228 
 229         add(new JSeparator(SwingConstants.HORIZONTAL), new
 230                   GridBagConstraints(0, 6, 5, 1, 1, 0,
 231                   GridBagConstraints.LINE_START, GridBagConstraints.HORIZONTAL,
 232                   new Insets(12, 0, 0, 0), 0, 0));
 233     }
 234 
 235     /**
 236      * Configures the spinner.
 237      */
 238     private void configureSpinner(JSpinner spinner, String name) {
 239         spinner.addChangeListener(this);
 240         spinner.setName(name);
 241         JComponent editor = spinner.getEditor();
 242         if (editor instanceof JSpinner.DefaultEditor) {
 243             JFormattedTextField ftf = ((JSpinner.DefaultEditor)editor).
 244                                                  getTextField();
 245 
 246             ftf.setFocusLostBehavior(JFormattedTextField.COMMIT_OR_REVERT);
 247         }
 248     }
 249 
 250     /**
 251      * Adds the widget creating a JLabel with the specified name.
 252      */
 253     private void add(Container parent, String key, JComponent widget,
 254                      int x, int y) {
 255         JLabel label = new JLabel(UIManager.getString(key + "Text",
 256                                                       getLocale()));
 257         String mnemonic = (String)UIManager.get(key + "Mnemonic", getLocale());
 258 
 259         if (mnemonic != null) {
 260             try {
 261                 label.setDisplayedMnemonic(Integer.parseInt(mnemonic));
 262             } catch (NumberFormatException nfe) {
 263             }
 264             String mnemonicIndex = (String)UIManager.get(key + "MnemonicIndex",
 265                                                     getLocale());
 266 
 267             if (mnemonicIndex != null) {
 268                 try {
 269                     label.setDisplayedMnemonicIndex(Integer.parseInt(
 270                                                         mnemonicIndex));
 271                 } catch (NumberFormatException nfe) {
 272                 }
 273             }
 274         }
 275         label.setLabelFor(widget);
 276         if (x < 0) {
 277             x = parent.getComponentCount() % 4;
 278         }
 279         if (y < 0) {
 280             y = parent.getComponentCount() / 4;
 281         }
 282         GridBagConstraints con = new GridBagConstraints(x + 1, y, 1, 1, 0, 0,
 283                    GridBagConstraints.FIRST_LINE_END, GridBagConstraints.NONE,
 284                    new Insets(4, 0, 0, 4), 0, 0);
 285         if (y == 0) {
 286             con.insets.top = 14;
 287         }
 288         parent.add(label, con);
 289         con.gridx++;
 290         parent.add(widget, con);
 291     }
 292 
 293     /**
 294      * Refreshes the display from the model.
 295      */
 296     public void updateChooser() {
 297         if (!settingColor) {
 298             lastLabel.setBackground(getColorFromModel());
 299             setColor(getColorFromModel(), true, true, false);
 300         }
 301     }
 302 
 303     /**
 304      * Resets the red component of the selected color.
 305      */
 306     private void setRed(int red) {
 307         setRGB(red << 16 | getColor().getGreen() << 8 | getColor().getBlue());
 308     }
 309 
 310     /**
 311      * Resets the green component of the selected color.
 312      */
 313     private void setGreen(int green) {
 314         setRGB(getColor().getRed() << 16 | green << 8 | getColor().getBlue());
 315     }
 316 
 317     /**
 318      * Resets the blue component of the selected color.
 319      */
 320     private void setBlue(int blue) {
 321         setRGB(getColor().getRed() << 16 | getColor().getGreen() << 8 | blue);
 322     }
 323 
 324     /**
 325      * Sets the hue of the selected color and updates the display if
 326      * necessary.
 327      */
 328     private void setHue(float hue, boolean update) {
 329         setHSB(hue, saturation, brightness);
 330         if (update) {
 331             settingColor = true;
 332             hueSpinner.setValue(Integer.valueOf((int)(hue * 360)));
 333             settingColor = false;
 334         }
 335     }
 336 
 337     /**
 338      * Returns the current amount of hue.
 339      */
 340     private float getHue() {
 341         return hue;
 342     }
 343 
 344     /**
 345      * Resets the saturation.
 346      */
 347     private void setSaturation(float saturation) {
 348         setHSB(hue, saturation, brightness);
 349     }
 350 
 351     /**
 352      * Returns the saturation.
 353      */
 354     private float getSaturation() {
 355         return saturation;
 356     }
 357 
 358     /**
 359      * Sets the brightness.
 360      */
 361     private void setBrightness(float brightness) {
 362         setHSB(hue, saturation, brightness);
 363     }
 364 
 365     /**
 366      * Returns the brightness.
 367      */
 368     private float getBrightness() {
 369         return brightness;
 370     }
 371 
 372     /**
 373      * Sets the saturation and brightness and updates the display if
 374      * necessary.
 375      */
 376     private void setSaturationAndBrightness(float s, float b, boolean update) {
 377         setHSB(hue, s, b);
 378         if (update) {
 379             settingColor = true;
 380             saturationSpinner.setValue(Integer.valueOf((int)(s * 255)));
 381             valueSpinner.setValue(Integer.valueOf((int)(b * 255)));
 382             settingColor = false;
 383         }
 384     }
 385 
 386     /**
 387      * Resets the rgb values.
 388      */
 389     private void setRGB(int rgb) {
 390         Color color = new Color(rgb);
 391 
 392         setColor(color, false, true, true);
 393 
 394         settingColor = true;
 395         hueSpinner.setValue(Integer.valueOf((int)(hue * 360)));
 396         saturationSpinner.setValue(Integer.valueOf((int)(saturation * 255)));
 397         valueSpinner.setValue(Integer.valueOf((int)(brightness * 255)));
 398         settingColor = false;
 399     }
 400 
 401     /**
 402      * Resets the hsb values.
 403      */
 404     private void setHSB(float h, float s, float b) {
 405         Color color = Color.getHSBColor(h, s, b);
 406 
 407         this.hue = h;
 408         this.saturation = s;
 409         this.brightness = b;
 410         setColor(color, false, false, true);
 411 
 412         settingColor = true;
 413         redSpinner.setValue(Integer.valueOf(color.getRed()));
 414         greenSpinner.setValue(Integer.valueOf(color.getGreen()));
 415         blueSpinner.setValue(Integer.valueOf(color.getBlue()));
 416         settingColor = false;
 417     }
 418 
 419 
 420     /**
 421      * Rests the color.
 422      *
 423      * @param color new Color
 424      * @param updateSpinners whether or not to update the spinners.
 425      * @param updateHSB if true, the hsb fields are updated based on the
 426      *                  new color
 427      * @param updateModel if true, the model is set.
 428      */
 429     private void setColor(Color color, boolean updateSpinners,
 430                           boolean updateHSB, boolean updateModel) {
 431         if (color == null) {
 432             color = Color.BLACK;
 433         }
 434 
 435         settingColor = true;
 436 
 437         if (updateHSB) {
 438             float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(),
 439                                          color.getBlue(), null);
 440             hue = hsb[0];
 441             saturation = hsb[1];
 442             brightness = hsb[2];
 443         }
 444 
 445         if (updateModel) {
 446             ColorSelectionModel model = getColorSelectionModel();
 447             if (model != null) {
 448                 model.setSelectedColor(color);
 449             }
 450         }
 451 
 452         triangle.setColor(hue, saturation, brightness);
 453         label.setBackground(color);
 454         // Force Integer to pad the string with 0's by adding 0x1000000 and
 455         // then removing the first character.
 456         String hexString = Integer.toHexString(
 457                   (color.getRGB() & 0xFFFFFF) | 0x1000000);
 458         colorNameTF.setText("#" + hexString.substring(1));
 459 
 460         if (updateSpinners) {
 461             redSpinner.setValue(Integer.valueOf(color.getRed()));
 462             greenSpinner.setValue(Integer.valueOf(color.getGreen()));
 463             blueSpinner.setValue(Integer.valueOf(color.getBlue()));
 464 
 465             hueSpinner.setValue(Integer.valueOf((int)(hue * 360)));
 466             saturationSpinner.setValue(Integer.valueOf((int)(saturation * 255)));
 467             valueSpinner.setValue(Integer.valueOf((int)(brightness * 255)));
 468         }
 469         settingColor = false;
 470     }
 471 
 472     public Color getColor() {
 473         return label.getBackground();
 474     }
 475 
 476     /**
 477      * ChangeListener method, updates the necessary display widgets.
 478      */
 479     public void stateChanged(ChangeEvent e) {
 480         if (settingColor) {
 481             return;
 482         }
 483         Color color = getColor();
 484 
 485         if (e.getSource() == hueSpinner) {
 486             setHue(((Number)hueSpinner.getValue()).floatValue() / 360, false);
 487         }
 488         else if (e.getSource() == saturationSpinner) {
 489             setSaturation(((Number)saturationSpinner.getValue()).
 490                           floatValue() / 255);
 491         }
 492         else if (e.getSource() == valueSpinner) {
 493             setBrightness(((Number)valueSpinner.getValue()).
 494                           floatValue() / 255);
 495         }
 496         else if (e.getSource() == redSpinner) {
 497             setRed(((Number)redSpinner.getValue()).intValue());
 498         }
 499         else if (e.getSource() == greenSpinner) {
 500             setGreen(((Number)greenSpinner.getValue()).intValue());
 501         }
 502         else if (e.getSource() == blueSpinner) {
 503             setBlue(((Number)blueSpinner.getValue()).intValue());
 504         }
 505     }
 506 
 507 
 508 
 509     /**
 510      * Flag indicating the angle, or hue, has changed and the triangle
 511      * needs to be recreated.
 512      */
 513     private static final int FLAGS_CHANGED_ANGLE = 1 << 0;
 514     /**
 515      * Indicates the wheel is being dragged.
 516      */
 517     private static final int FLAGS_DRAGGING = 1 << 1;
 518     /**
 519      * Indicates the triangle is being dragged.
 520      */
 521     private static final int FLAGS_DRAGGING_TRIANGLE = 1 << 2;
 522     /**
 523      * Indicates a color is being set and we should ignore setColor
 524      */
 525     private static final int FLAGS_SETTING_COLOR = 1 << 3;
 526     /**
 527      * Indicates the wheel has focus.
 528      */
 529     private static final int FLAGS_FOCUSED_WHEEL = 1 << 4;
 530     /**
 531      * Indicates the triangle has focus.
 532      */
 533     private static final int FLAGS_FOCUSED_TRIANGLE = 1 << 5;
 534 
 535 
 536     /**
 537      * Class responsible for rendering a color wheel and color triangle.
 538      */
 539     @SuppressWarnings("serial") // Superclass is not serializable across versions
 540     private class ColorTriangle extends JPanel {
 541         /**
 542          * Cached image of the wheel.
 543          */
 544         private Image wheelImage;
 545 
 546         /**
 547          * Cached image of the triangle.
 548          */
 549         private Image triangleImage;
 550 
 551         /**
 552          * Angle triangle is rotated by.
 553          */
 554         private double angle;
 555 
 556         /**
 557          * Boolean bitmask.
 558          */
 559         private int flags;
 560 
 561         /**
 562          * X location of selected color indicator.
 563          */
 564         private int circleX;
 565         /**
 566          * Y location of selected color indicator.
 567          */
 568         private int circleY;
 569 
 570 
 571         public ColorTriangle() {
 572             enableEvents(AWTEvent.FOCUS_EVENT_MASK);
 573             enableEvents(AWTEvent.MOUSE_EVENT_MASK);
 574             enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);
 575 
 576             setMinimumSize(new Dimension(getWheelRadius() * 2 + 2,
 577                                          getWheelRadius() * 2 + 2));
 578             setPreferredSize(new Dimension(getWheelRadius() * 2 + 2,
 579                                            getWheelRadius() * 2 + 2));
 580 
 581             // We want to handle tab ourself.
 582             setFocusTraversalKeysEnabled(false);
 583 
 584             // PENDING: this should come from the style.
 585             getInputMap().put(KeyStroke.getKeyStroke("UP"), "up");
 586             getInputMap().put(KeyStroke.getKeyStroke("DOWN"), "down");
 587             getInputMap().put(KeyStroke.getKeyStroke("LEFT"), "left");
 588             getInputMap().put(KeyStroke.getKeyStroke("RIGHT"), "right");
 589 
 590             getInputMap().put(KeyStroke.getKeyStroke("KP_UP"), "up");
 591             getInputMap().put(KeyStroke.getKeyStroke("KP_DOWN"), "down");
 592             getInputMap().put(KeyStroke.getKeyStroke("KP_LEFT"), "left");
 593             getInputMap().put(KeyStroke.getKeyStroke("KP_RIGHT"), "right");
 594 
 595             getInputMap().put(KeyStroke.getKeyStroke("TAB"), "focusNext");
 596             getInputMap().put(KeyStroke.getKeyStroke("shift TAB"),"focusLast");
 597 
 598             ActionMap map = (ActionMap)UIManager.get(
 599                                        "GTKColorChooserPanel.actionMap");
 600 
 601             if (map == null) {
 602                 map = new ActionMapUIResource();
 603                 map.put("left", new ColorAction("left", 2));
 604                 map.put("right", new ColorAction("right", 3));
 605                 map.put("up", new ColorAction("up", 0));
 606                 map.put("down", new ColorAction("down", 1));
 607                 map.put("focusNext", new ColorAction("focusNext", 4));
 608                 map.put("focusLast", new ColorAction("focusLast", 5));
 609                 UIManager.getLookAndFeelDefaults().put(
 610                              "GTKColorChooserPanel.actionMap", map);
 611             }
 612             SwingUtilities.replaceUIActionMap(this, map);
 613         }
 614 
 615         /**
 616          * Returns the GTKColorChooserPanel.
 617          */
 618         GTKColorChooserPanel getGTKColorChooserPanel() {
 619             return GTKColorChooserPanel.this;
 620         }
 621 
 622         /**
 623          * Gives focus to the wheel.
 624          */
 625         void focusWheel() {
 626             setFocusType(1);
 627         }
 628 
 629         /**
 630          * Gives focus to the triangle.
 631          */
 632         void focusTriangle() {
 633             setFocusType(2);
 634         }
 635 
 636         /**
 637          * Returns true if the wheel currently has focus.
 638          */
 639         boolean isWheelFocused() {
 640             return isSet(FLAGS_FOCUSED_WHEEL);
 641         }
 642 
 643         /**
 644          * Resets the selected color.
 645          */
 646         public void setColor(float h, float s, float b) {
 647             if (isSet(FLAGS_SETTING_COLOR)) {
 648                 return;
 649             }
 650 
 651             setAngleFromHue(h);
 652             setSaturationAndBrightness(s, b);
 653         }
 654 
 655         /**
 656          * Returns the selected color.
 657          */
 658         public Color getColor() {
 659             return GTKColorChooserPanel.this.getColor();
 660         }
 661 
 662         /**
 663          * Returns the x location of the selected color indicator.
 664          */
 665         int getColorX() {
 666             return circleX + getIndicatorSize() / 2 - getWheelXOrigin();
 667         }
 668 
 669         /**
 670          * Returns the y location of the selected color indicator.
 671          */
 672         int getColorY() {
 673             return circleY + getIndicatorSize() / 2 - getWheelYOrigin();
 674         }
 675 
 676         protected void processEvent(AWTEvent e) {
 677             if (e.getID() == MouseEvent.MOUSE_PRESSED ||
 678                    ((isSet(FLAGS_DRAGGING) ||isSet(FLAGS_DRAGGING_TRIANGLE)) &&
 679                    e.getID() == MouseEvent.MOUSE_DRAGGED)) {
 680                 // Assign focus to either the wheel or triangle and attempt
 681                 // to drag either the wheel or triangle.
 682                 int size = getWheelRadius();
 683                 int x = ((MouseEvent)e).getX() - size;
 684                 int y = ((MouseEvent)e).getY() - size;
 685 
 686                 if (!hasFocus()) {
 687                     requestFocus();
 688                 }
 689                 if (!isSet(FLAGS_DRAGGING_TRIANGLE) &&
 690                       adjustHue(x, y, e.getID() == MouseEvent.MOUSE_PRESSED)) {
 691                     setFlag(FLAGS_DRAGGING, true);
 692                     setFocusType(1);
 693                 }
 694                 else if (adjustSB(x, y, e.getID() ==
 695                                         MouseEvent.MOUSE_PRESSED)) {
 696                     setFlag(FLAGS_DRAGGING_TRIANGLE, true);
 697                     setFocusType(2);
 698                 }
 699                 else {
 700                     setFocusType(2);
 701                 }
 702             }
 703             else if (e.getID() == MouseEvent.MOUSE_RELEASED) {
 704                 // Stopped dragging
 705                 setFlag(FLAGS_DRAGGING_TRIANGLE, false);
 706                 setFlag(FLAGS_DRAGGING, false);
 707             }
 708             else if (e.getID() == FocusEvent.FOCUS_LOST) {
 709                 // Reset the flags to indicate no one has focus
 710                 setFocusType(0);
 711             }
 712             else if (e.getID() == FocusEvent.FOCUS_GAINED) {
 713                 // Gained focus, reassign focus to the wheel if no one
 714                 // currently has focus.
 715                 if (!isSet(FLAGS_FOCUSED_TRIANGLE) &&
 716                           !isSet(FLAGS_FOCUSED_WHEEL)) {
 717                     setFlag(FLAGS_FOCUSED_WHEEL, true);
 718                     setFocusType(1);
 719                 }
 720                 repaint();
 721             }
 722             super.processEvent(e);
 723         }
 724 
 725         public void paintComponent(Graphics g) {
 726             super.paintComponent(g);
 727 
 728             // Draw the wheel and triangle
 729             int size = getWheelRadius();
 730             int width = getWheelWidth();
 731             Image image = getImage(size);
 732             g.drawImage(image, getWheelXOrigin() - size,
 733                         getWheelYOrigin() - size, null);
 734 
 735             // Draw the focus indicator for the wheel
 736             if (hasFocus() && isSet(FLAGS_FOCUSED_WHEEL)) {
 737                 g.setColor(Color.BLACK);
 738                 g.drawOval(getWheelXOrigin() - size, getWheelYOrigin() - size,
 739                            2 * size, 2 * size);
 740                 g.drawOval(getWheelXOrigin() - size + width, getWheelYOrigin()-
 741                            size + width, 2 * (size - width), 2 *
 742                            (size - width));
 743             }
 744 
 745             // Draw a line on the wheel indicating the selected hue.
 746             if (Math.toDegrees(Math.PI * 2 - angle) <= 20 ||
 747                      Math.toDegrees(Math.PI * 2 - angle) >= 201) {
 748                 g.setColor(Color.WHITE);
 749             }
 750             else {
 751                 g.setColor(Color.BLACK);
 752             }
 753             int lineX0 = (int)(Math.cos(angle) * size);
 754             int lineY0 = (int)(Math.sin(angle) * size);
 755             int lineX1 = (int)(Math.cos(angle) * (size - width));
 756             int lineY1 = (int)(Math.sin(angle) * (size - width));
 757             g.drawLine(lineX0 + size, lineY0 + size, lineX1 + size,
 758                        lineY1 + size);
 759 
 760             // Draw the focus indicator on the triangle
 761             if (hasFocus() && isSet(FLAGS_FOCUSED_TRIANGLE)) {
 762                 Graphics g2 = g.create();
 763                 int innerR = getTriangleCircumscribedRadius();
 764                 int a = (int)(3 * innerR / Math.sqrt(3));
 765                 g2.translate(getWheelXOrigin(), getWheelYOrigin());
 766                 ((Graphics2D)g2).rotate(angle + Math.PI / 2);
 767                 g2.setColor(Color.BLACK);
 768                 g2.drawLine(0, -innerR, a / 2, innerR / 2);
 769                 g2.drawLine(a / 2, innerR / 2, -a / 2, innerR / 2);
 770                 g2.drawLine(-a / 2, innerR / 2, 0, -innerR);
 771                 g2.dispose();
 772             }
 773 
 774             // Draw the selected color indicator.
 775             g.setColor(Color.BLACK);
 776             g.drawOval(circleX, circleY, getIndicatorSize() - 1,
 777                        getIndicatorSize() - 1);
 778             g.setColor(Color.WHITE);
 779             g.drawOval(circleX + 1, circleY + 1, getIndicatorSize() - 3,
 780                        getIndicatorSize() - 3);
 781         }
 782 
 783         /**
 784          * Returns an image representing the triangle and wheel.
 785          */
 786         private Image getImage(int size) {
 787             if (!isSet(FLAGS_CHANGED_ANGLE) && wheelImage != null &&
 788                         wheelImage.getWidth(null) == size * 2) {
 789                 return wheelImage;
 790             }
 791             if (wheelImage == null || wheelImage.getWidth(null) != size) {
 792                 wheelImage = getWheelImage(size);
 793             }
 794             int innerR = getTriangleCircumscribedRadius();
 795             int triangleSize = (int)(innerR * 3.0 / 2.0);
 796             int a = (int)(2 * triangleSize / Math.sqrt(3));
 797             if (triangleImage == null || triangleImage.getWidth(null) != a) {
 798                 triangleImage = new BufferedImage(a, a,
 799                                                   BufferedImage.TYPE_INT_ARGB);
 800             }
 801             Graphics g = triangleImage.getGraphics();
 802             g.setColor(new Color(0, 0, 0, 0));
 803             g.fillRect(0, 0, a, a);
 804             g.translate(a / 2, 0);
 805             paintTriangle(g, triangleSize, getColor());
 806             g.translate(-a / 2, 0);
 807             g.dispose();
 808 
 809             g = wheelImage.getGraphics();
 810             g.setColor(new Color(0, 0, 0, 0));
 811             g.fillOval(getWheelWidth(), getWheelWidth(),
 812                        2 * (size - getWheelWidth()),
 813                        2 * (size - getWheelWidth()));
 814 
 815             double rotate = Math.toRadians(-30.0) + angle;
 816             g.translate(size, size);
 817             ((Graphics2D)g).rotate(rotate);
 818             g.drawImage(triangleImage, -a / 2,
 819                         getWheelWidth() - size, null);
 820             ((Graphics2D)g).rotate(-rotate);
 821             g.translate(a / 2, size - getWheelWidth());
 822 
 823             setFlag(FLAGS_CHANGED_ANGLE, false);
 824 
 825             return wheelImage;
 826         }
 827 
 828         private void paintTriangle(Graphics g, int size, Color color) {
 829             float[] colors = Color.RGBtoHSB(color.getRed(),
 830                                             color.getGreen(),
 831                                             color.getBlue(), null);
 832             float hue = colors[0];
 833             double dSize = (double)size;
 834             for (int y = 0; y < size; y++) {
 835                 int maxX = (int)(y * Math.tan(Math.toRadians(30.0)));
 836                 float factor = maxX * 2;
 837                 if (maxX > 0) {
 838                     float value = (float)(y / dSize);
 839                     for (int x = -maxX; x <= maxX; x++) {
 840                         float saturation = (float)x / factor + .5f;
 841                         g.setColor(Color.getHSBColor(hue, saturation, value));
 842                         g.fillRect(x, y, 1, 1);
 843                     }
 844                 }
 845                 else {
 846                     g.setColor(color);
 847                     g.fillRect(0, y, 1, 1);
 848                 }
 849             }
 850         }
 851 
 852         /**
 853          * Returns a color wheel image for the specified size.
 854          *
 855          * @param size Integer giving size of color wheel.
 856          * @return Color wheel image
 857          */
 858         private Image getWheelImage(int size) {
 859             int minSize = size - getWheelWidth();
 860             int doubleSize = size * 2;
 861             BufferedImage image = new BufferedImage(doubleSize, doubleSize,
 862                                               BufferedImage.TYPE_INT_ARGB);
 863 
 864             for (int y = -size; y < size; y++) {
 865                 int ySquared = y * y;
 866                 for (int x = -size; x < size; x++) {
 867                     double rad = Math.sqrt(ySquared + x * x);
 868 
 869                     if (rad < size && rad > minSize) {
 870                         int rgb = colorWheelLocationToRGB(x, y, rad) |
 871                               0xFF000000;
 872                         image.setRGB(x + size, y + size, rgb);
 873                     }
 874                 }
 875             }
 876             wheelImage = image;
 877             return wheelImage;
 878         }
 879 
 880         /**
 881          * Adjusts the saturation and brightness. <code>x</code> and
 882          * <code>y</code> give the location to adjust to and are relative
 883          * to the origin of the wheel/triangle.
 884          *
 885          * @param x X coordinate on the triangle to adjust to
 886          * @param y Y coordinate on the triangle to adjust to
 887          * @param checkLoc if true the location is checked to make sure
 888          *        it is contained in the triangle, if false the location is
 889          *        constrained to fit in the triangle.
 890          * @return true if the location is valid
 891          */
 892         boolean adjustSB(int x, int y, boolean checkLoc) {
 893             int innerR = getWheelRadius() - getWheelWidth();
 894             boolean resetXY = false;
 895             // Invert the axis.
 896             y = -y;
 897             if (checkLoc && (x < -innerR || x > innerR || y < -innerR ||
 898                              y > innerR)) {
 899                 return false;
 900             }
 901             // Rotate to origin and and verify x is valid.
 902             int triangleSize = innerR * 3 / 2;
 903             double x1 = Math.cos(angle) * x - Math.sin(angle) * y;
 904             double y1 = Math.sin(angle) * x + Math.cos(angle) * y;
 905             if (x1 < -(innerR / 2)) {
 906                 if (checkLoc) {
 907                     return false;
 908                 }
 909                 x1 = -innerR / 2;
 910                 resetXY = true;
 911             }
 912             else if ((int)x1 > innerR) {
 913                 if (checkLoc) {
 914                     return false;
 915                 }
 916                 x1 = innerR;
 917                 resetXY = true;
 918             }
 919             // Verify y location is valid.
 920             int maxY = (int)((triangleSize - x1 - innerR / 2.0) *
 921                              Math.tan(Math.toRadians(30.0)));
 922             if (y1 <= -maxY) {
 923                 if (checkLoc) {
 924                     return false;
 925                 }
 926                 y1 = -maxY;
 927                 resetXY = true;
 928             }
 929             else if (y1 > maxY) {
 930                 if (checkLoc) {
 931                     return false;
 932                 }
 933                 y1 = maxY;
 934                 resetXY = true;
 935             }
 936             // Rotate again to determine value and scale
 937             double x2 = Math.cos(Math.toRadians(-30.0)) * x1 -
 938                  Math.sin(Math.toRadians(-30.0)) * y1;
 939             double y2 = Math.sin(Math.toRadians(-30.0)) * x1 +
 940                  Math.cos(Math.toRadians(-30.0)) * y1;
 941             float value = Math.min(1.0f, (float)((innerR - y2) /
 942                                                 (double)triangleSize));
 943             float maxX = (float)(Math.tan(Math.toRadians(30)) * (innerR - y2));
 944             float saturation = Math.min(1.0f, (float)(x2 / maxX / 2 + .5));
 945 
 946             setFlag(FLAGS_SETTING_COLOR, true);
 947             if (resetXY) {
 948                 setSaturationAndBrightness(saturation, value);
 949             }
 950             else {
 951                 setSaturationAndBrightness(saturation, value, x +
 952                                       getWheelXOrigin(),getWheelYOrigin() - y);
 953             }
 954             GTKColorChooserPanel.this.setSaturationAndBrightness(saturation,
 955                                                                  value, true);
 956             setFlag(FLAGS_SETTING_COLOR, false);
 957             return true;
 958         }
 959 
 960         /**
 961          * Sets the saturation and brightness.
 962          */
 963         private void setSaturationAndBrightness(float s, float b) {
 964             int innerR = getTriangleCircumscribedRadius();
 965             int triangleSize = innerR * 3 / 2;
 966             double x = b * triangleSize;
 967             double maxY = x * Math.tan(Math.toRadians(30.0));
 968             double y = 2 * maxY * s - maxY;
 969             x = x - innerR;
 970             double x1 = Math.cos(Math.toRadians(-60.0) - angle) *
 971                         x - Math.sin(Math.toRadians(-60.0) - angle) * y;
 972             double y1 = Math.sin(Math.toRadians(-60.0) - angle) * x +
 973                         Math.cos(Math.toRadians(-60.0) - angle) * y;
 974             int newCircleX = (int)x1 + getWheelXOrigin();
 975             int newCircleY = getWheelYOrigin() - (int)y1;
 976 
 977             setSaturationAndBrightness(s, b, newCircleX, newCircleY);
 978         }
 979 
 980 
 981         /**
 982          * Sets the saturation and brightness.
 983          */
 984         private void setSaturationAndBrightness(float s, float b,
 985                                              int newCircleX, int newCircleY) {
 986             newCircleX -= getIndicatorSize() / 2;
 987             newCircleY -= getIndicatorSize() / 2;
 988 
 989             int minX = Math.min(newCircleX, circleX);
 990             int minY = Math.min(newCircleY, circleY);
 991 
 992             repaint(minX, minY, Math.max(circleX, newCircleX) - minX +
 993                     getIndicatorSize() + 1, Math.max(circleY, newCircleY) -
 994                     minY + getIndicatorSize() + 1);
 995             circleX = newCircleX;
 996             circleY = newCircleY;
 997         }
 998 
 999         /**
1000          * Adjusts the hue based on the passed in location.
1001          *
1002          * @param x X location to adjust to, relative to the origin of the
1003          *        wheel
1004          * @param y Y location to adjust to, relative to the origin of the
1005          *        wheel
1006          * @param check if true the location is checked to make sure
1007          *        it is contained in the wheel, if false the location is
1008          *        constrained to fit in the wheel
1009          * @return true if the location is valid.
1010          */
1011         private boolean adjustHue(int x, int y, boolean check) {
1012             double rad = Math.sqrt(x * x + y * y);
1013             int size = getWheelRadius();
1014 
1015             if (!check || (rad >= size - getWheelWidth() && rad < size)) {
1016                 // Map the location to an angle and reset hue
1017                 double angle;
1018                 if (x == 0) {
1019                     if (y > 0) {
1020                         angle = Math.PI / 2.0;
1021                     }
1022                     else {
1023                         angle = Math.PI + Math.PI / 2.0;
1024                     }
1025                 }
1026                 else {
1027                     angle = Math.atan((double)y / (double)x);
1028                     if (x < 0) {
1029                         angle += Math.PI;
1030                     }
1031                     else if (angle < 0) {
1032                         angle += 2 * Math.PI;
1033                     }
1034                 }
1035                 setFlag(FLAGS_SETTING_COLOR, true);
1036                 setHue((float)(1.0 - angle / Math.PI / 2), true);
1037                 setFlag(FLAGS_SETTING_COLOR, false);
1038                 setHueAngle(angle);
1039                 setSaturationAndBrightness(getSaturation(), getBrightness());
1040                 return true;
1041             }
1042             return false;
1043         }
1044 
1045         /**
1046          * Rotates the triangle to accommodate the passed in hue.
1047          */
1048         private void setAngleFromHue(float hue) {
1049             setHueAngle((1.0 - hue) * Math.PI * 2);
1050         }
1051 
1052         /**
1053          * Sets the angle representing the hue.
1054          */
1055         private void setHueAngle(double angle) {
1056             double oldAngle = this.angle;
1057 
1058             this.angle = angle;
1059             if (angle != oldAngle) {
1060                 setFlag(FLAGS_CHANGED_ANGLE, true);
1061                 repaint();
1062             }
1063         }
1064 
1065         /**
1066          * Returns the size of the color indicator.
1067          */
1068         private int getIndicatorSize() {
1069             return 8;
1070         }
1071 
1072         /**
1073          * Returns the circumscribed radius of the triangle.
1074          */
1075         private int getTriangleCircumscribedRadius() {
1076             return 72;
1077         }
1078 
1079         /**
1080          * Returns the x origin of the wheel and triangle.
1081          */
1082         private int getWheelXOrigin() {
1083             return 85;
1084         }
1085 
1086         /**
1087          * Returns y origin of the wheel and triangle.
1088          */
1089         private int getWheelYOrigin() {
1090             return 85;
1091         }
1092 
1093         /**
1094          * Returns the width of the wheel.
1095          */
1096         private int getWheelWidth() {
1097             return 13;
1098         }
1099 
1100         /**
1101          * Sets the focus to one of: 0 no one, 1 the wheel or 2 the triangle.
1102          */
1103         private void setFocusType(int type) {
1104             if (type == 0) {
1105                 setFlag(FLAGS_FOCUSED_WHEEL, false);
1106                 setFlag(FLAGS_FOCUSED_TRIANGLE, false);
1107                 repaint();
1108             }
1109             else {
1110                 int toSet = FLAGS_FOCUSED_WHEEL;
1111                 int toUnset = FLAGS_FOCUSED_TRIANGLE;
1112 
1113                 if (type == 2) {
1114                     toSet = FLAGS_FOCUSED_TRIANGLE;
1115                     toUnset = FLAGS_FOCUSED_WHEEL;
1116                 }
1117                 if (!isSet(toSet)) {
1118                     setFlag(toSet, true);
1119                     repaint();
1120                     setFlag(toUnset, false);
1121                 }
1122             }
1123         }
1124 
1125         /**
1126          * Returns the radius of the wheel.
1127          */
1128         private int getWheelRadius() {
1129             // As far as I can tell, GTK doesn't allow stretching this
1130             // widget
1131             return 85;
1132         }
1133 
1134         /**
1135          * Updates the flags bitmask.
1136          */
1137         private void setFlag(int flag, boolean value) {
1138             if (value) {
1139                 flags |= flag;
1140             }
1141             else {
1142                 flags &= ~flag;
1143             }
1144         }
1145 
1146         /**
1147          * Returns true if a particular flag has been set.
1148          */
1149         private boolean isSet(int flag) {
1150             return ((flags & flag) == flag);
1151         }
1152 
1153         /**
1154          * Returns the RGB color to use for the specified location. The
1155          * passed in point must be on the color wheel and be relative to the
1156          * origin of the color wheel.
1157          *
1158          * @param x X location to get color for
1159          * @param y Y location to get color for
1160          * @param rad Radius from center of color wheel
1161          * @return integer with red, green and blue components
1162          */
1163         private int colorWheelLocationToRGB(int x, int y, double rad) {
1164             double angle = Math.acos((double)x / rad);
1165             int rgb;
1166 
1167             if (angle < PI_3) {
1168                 if (y < 0) {
1169                     // FFFF00 - FF0000
1170                     rgb = 0xFF0000 | Math.min(255,
1171                                            (int)(255 * angle / PI_3)) << 8;
1172                 }
1173                 else {
1174                     // FF0000 - FF00FF
1175                     rgb = 0xFF0000 | Math.min(255,
1176                                            (int)(255 * angle / PI_3));
1177                 }
1178             }
1179             else if (angle < 2 * PI_3) {
1180                 angle -= PI_3;
1181                 if (y < 0) {
1182                     // 00FF00 - FFFF00
1183                     rgb = 0x00FF00 | Math.max(0, 255 -
1184                                            (int)(255 * angle / PI_3)) << 16;
1185                 }
1186                 else {
1187                     // FF00FF - 0000FF
1188                     rgb = 0x0000FF | Math.max(0, 255 -
1189                                            (int)(255 * angle / PI_3)) << 16;
1190                 }
1191             }
1192             else {
1193                 angle -= 2 * PI_3;
1194                 if (y < 0) {
1195                     // 00FFFF - 00FF00
1196                     rgb = 0x00FF00 | Math.min(255,
1197                                            (int)(255 * angle / PI_3));
1198                 }
1199                 else {
1200                     // 0000FF - 00FFFF
1201                     rgb = 0x0000FF | Math.min(255,
1202                                            (int)(255 * angle / PI_3)) << 8;
1203                 }
1204             }
1205             return rgb;
1206         }
1207 
1208         /**
1209          * Increments the hue.
1210          */
1211         void incrementHue(boolean positive) {
1212             float hue = triangle.getGTKColorChooserPanel().getHue();
1213 
1214             if (positive) {
1215                 hue += 1.0f / 360.0f;
1216             }
1217             else {
1218                 hue -= 1.0f / 360.0f;
1219             }
1220             if (hue > 1) {
1221                 hue -= 1;
1222             }
1223             else if (hue < 0) {
1224                 hue += 1;
1225             }
1226             getGTKColorChooserPanel().setHue(hue, true);
1227         }
1228     }
1229 
1230 
1231     /**
1232      * Action class used for colors.
1233      */
1234     @SuppressWarnings("serial") // Superclass is not serializable across versions
1235     private static class ColorAction extends AbstractAction {
1236         private int type;
1237 
1238         ColorAction(String name, int type) {
1239             super(name);
1240             this.type = type;
1241         }
1242 
1243         public void actionPerformed(ActionEvent e) {
1244             ColorTriangle triangle = (ColorTriangle)e.getSource();
1245 
1246             if (triangle.isWheelFocused()) {
1247                 float hue = triangle.getGTKColorChooserPanel().getHue();
1248 
1249                 switch (type) {
1250                 case 0:
1251                 case 2:
1252                     triangle.incrementHue(true);
1253                     break;
1254                 case 1:
1255                 case 3:
1256                     triangle.incrementHue(false);
1257                     break;
1258                 case 4:
1259                     triangle.focusTriangle();
1260                     break;
1261                 case 5:
1262                     compositeRequestFocus(triangle, false);
1263                     break;
1264                 }
1265             }
1266             else {
1267                 int xDelta = 0;
1268                 int yDelta = 0;
1269 
1270                 switch (type) {
1271                 case 0:
1272                     // up
1273                     yDelta--;
1274                     break;
1275                 case 1:
1276                     // down
1277                     yDelta++;
1278                     break;
1279                 case 2:
1280                     // left
1281                     xDelta--;
1282                     break;
1283                 case 3:
1284                     // right
1285                     xDelta++;
1286                     break;
1287                 case 4:
1288                     compositeRequestFocus(triangle, true);
1289                     return;
1290                 case 5:
1291                     triangle.focusWheel();
1292                     return;
1293                 }
1294                 triangle.adjustSB(triangle.getColorX() + xDelta,
1295                                   triangle.getColorY() + yDelta, true);
1296             }
1297         }
1298     }
1299 
1300     @SuppressWarnings("serial") // Superclass is not serializable across versions
1301     private class OpaqueLabel extends JLabel {
1302         public boolean isOpaque() {
1303             return true;
1304         }
1305     }
1306 }