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