001    /*
002     * @(#)ColorPicker.java  1.0  2008-03-01
003     *
004     * Copyright (c) 2008 Jeremy Wood
005     * E-mail: mickleness@gmail.com
006     * All rights reserved.
007     *
008     * The copyright of this software is owned by Jeremy Wood.
009     * You may not use, copy or modify this software, except in
010     * accordance with the license agreement you entered into with
011     * Jeremy Wood. For details see accompanying license terms.
012     */
013    
014    package com.colorpicker.swing;
015    
016    import com.colorpicker.swing.*;
017    import java.awt.*;
018    
019    import javax.swing.*;
020    import java.awt.event.*;
021    import java.beans.PropertyChangeEvent;
022    import java.beans.PropertyChangeListener;
023    import java.util.*;
024    import javax.swing.event.*;
025    
026    
027    
028    
029    /** This is a panel that offers a robust set of controls to pick a color.
030     * <P>This was originally intended to replace the <code>JColorChooser</code>.
031     * To use this class to create a color choosing dialog, simply call:
032     * <BR><code>ColorPicker.showDialog(frame, originalColor);</code>
033     * <P>However this panel is also resizable, and it can exist in other contexts.
034     * For example, you might try the following panel:
035     * <BR><code>ColorPicker picker = new ColorPicker(false, false);</code>
036     * <BR><code>picker.setPreferredSize(new Dimension(200,160));</code>
037     * <BR><code>picker.setMode(ColorPicker.HUE);</code>
038     * <P>This will create a miniature color picker that still lets the user choose
039     * from every available color, but it does not include all the buttons and
040     * numeric controls on the right side of the panel.  This might be ideal if you
041     * are working with limited space, or non-power-users who don't need the
042     * RGB values of a color.  The <code>main()</code> method of this class demonstrates
043     * possible ways you can customize a <code>ColorPicker</code> component.
044     * <P>To listen to color changes to this panel, you can add a <code>PropertyChangeListener</code>
045     * listening for changes to the <code>SELECTED_COLOR_PROPERTY</code>.  This will be triggered only
046     * when the RGB value of the selected color changes.
047     * <P>To listen to opacity changes to this panel, use a <code>PropertyChangeListener</code> listening
048     * for changes to the <code>OPACITY_PROPERTY</code>.
049     * 
050     * @version 1.3
051     * @author Jeremy Wood (With some updates from Frederic Roudaut to fit my needs)
052     */
053    public class ColorPicker extends JPanel {
054        private static final long serialVersionUID = 3L;
055            
056        /** The localized strings used in this (and related) panel(s). */
057        protected static ResourceBundle strings = ResourceBundle.getBundle("com.colorpicker.swing.ColorPicker");
058            
059        /** This demonstrates how to customize a small <code>ColorPicker</code> component.
060         */
061        public static void main(String[] args) {
062            final JFrame demo = new JFrame("Demo");
063            final JWindow palette = new JWindow(demo);
064            final ColorPicker picker = new ColorPicker(true,false);
065                    
066            final JComboBox comboBox = new JComboBox();
067            final JCheckBox alphaCheckbox = new JCheckBox("Include Alpha");
068            final JCheckBox hsbCheckbox = new JCheckBox("Include HSB Values");
069            final JCheckBox rgbCheckbox = new JCheckBox("Include RGB Values");
070            final JCheckBox modeCheckbox = new JCheckBox("Include Mode Controls",true);
071            final JButton button = new JButton("Show Dialog");
072                    
073            demo.getContentPane().setLayout(new GridBagLayout());
074            palette.getContentPane().setLayout(new GridBagLayout());
075                    
076            GridBagConstraints c = new GridBagConstraints();
077            c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 0;
078            c.insets = new Insets(5,5,5,5); c.anchor = GridBagConstraints.WEST;
079            palette.getContentPane().add(comboBox,c);
080            c.gridy++;
081            palette.getContentPane().add(alphaCheckbox,c);
082            c.gridy++;
083            palette.getContentPane().add(hsbCheckbox,c);
084            c.gridy++;
085            palette.getContentPane().add(rgbCheckbox,c);
086            c.gridy++;
087            palette.getContentPane().add(modeCheckbox,c);
088                    
089            c.gridy = 0;
090            c.weighty = 1; c.fill = GridBagConstraints.BOTH;
091            picker.setPreferredSize(new Dimension(220,200));
092            demo.getContentPane().add(picker,c);
093            c.gridy++; c.weighty = 0;
094            demo.getContentPane().add(picker.getExpertControls(),c);
095            c.gridy++; c.fill = GridBagConstraints.NONE;
096            demo.getContentPane().add(button,c);
097                    
098            comboBox.addItem("Hue");
099            comboBox.addItem("Saturation");
100            comboBox.addItem("Brightness");
101            comboBox.addItem("Red");
102            comboBox.addItem("Green");
103            comboBox.addItem("Blue");
104                    
105            ActionListener checkboxListener = new ActionListener() {
106                    public void actionPerformed(ActionEvent e) {
107                        Object src = e.getSource();
108                        if(src==alphaCheckbox) {
109                            picker.setOpacityVisible(alphaCheckbox.isSelected());
110                        } else if(src==hsbCheckbox) {
111                            picker.setHSBControlsVisible(hsbCheckbox.isSelected());
112                        } else if(src==rgbCheckbox) {
113                            picker.setRGBControlsVisible(rgbCheckbox.isSelected());
114                        } else if(src==modeCheckbox) {
115                            picker.setModeControlsVisible(modeCheckbox.isSelected());
116                        }
117                        demo.pack();
118                    }
119                };
120            picker.setOpacityVisible(false);
121            picker.setHSBControlsVisible(false);
122            picker.setRGBControlsVisible(false);
123            picker.setHexControlsVisible(false);
124            picker.setPreviewSwatchVisible(false);
125                    
126            picker.addPropertyChangeListener(MODE_PROPERTY, new PropertyChangeListener() {
127                    public void propertyChange(PropertyChangeEvent evt) {
128    
129                        int m = picker.getMode();
130                        if(m==HUE) {
131                            comboBox.setSelectedIndex(0);
132                        } else if(m==SAT) {
133                            comboBox.setSelectedIndex(1);
134                        } else if(m==BRI) {
135                            comboBox.setSelectedIndex(2);
136                        } else if(m==RED) {
137                            comboBox.setSelectedIndex(3);
138                        } else if(m==GREEN) {
139                            comboBox.setSelectedIndex(4);
140                        } else if(m==BLUE) {
141                            comboBox.setSelectedIndex(5);
142                        }
143                    }
144                });
145                    
146            alphaCheckbox.addActionListener(checkboxListener);
147            hsbCheckbox.addActionListener(checkboxListener);
148            rgbCheckbox.addActionListener(checkboxListener);
149            modeCheckbox.addActionListener(checkboxListener);
150            button.addActionListener(new ActionListener() {
151                    public void actionPerformed(ActionEvent e) {
152                        Color color = picker.getColor();
153                        color = ColorPicker.showDialog(demo, color, true);
154                        if(color!=null)
155                            picker.setColor(color);
156                    }
157                });
158    
159            comboBox.addActionListener(new ActionListener() {
160                    public void actionPerformed(ActionEvent e) {
161                        int i = ((JComboBox)e.getSource()).getSelectedIndex();
162                        if(i==0) {
163                            picker.setMode(ColorPicker.HUE);
164                        } else if(i==1) {
165                            picker.setMode(ColorPicker.SAT);
166                        } else if(i==2) {
167                            picker.setMode(ColorPicker.BRI);
168                        } else if(i==3) {
169                            picker.setMode(ColorPicker.RED);
170                        } else if(i==4) {
171                            picker.setMode(ColorPicker.GREEN);
172                        } else if(i==5) {
173                            picker.setMode(ColorPicker.BLUE);
174                        }
175                    }
176                });
177            comboBox.setSelectedIndex(2);
178    
179            palette.pack();
180            palette.setLocationRelativeTo(null);
181                    
182            demo.addComponentListener(new ComponentAdapter() {
183                    public void componentMoved(ComponentEvent e) {
184                        Point p = demo.getLocation();
185                        palette.setLocation(new Point(p.x-palette.getWidth()-10,p.y));
186                    }
187                });
188            demo.pack();
189            demo.setLocationRelativeTo(null);
190            demo.setVisible(true);
191            palette.setVisible(true);
192                    
193            demo.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
194        }
195            
196        /** This creates a modal dialog prompting the user to select a color.
197         * <P>This uses a generic dialog title: "Choose a Color", and does not include opacity.
198         * 
199         * @param owner the dialog this new dialog belongs to.  This must be a Frame or a Dialog.
200         * Java 1.6 supports Windows here, but this package is designed/compiled to work in Java 1.4,
201         * so an <code>IllegalArgumentException</code> will be thrown if this component is a <code>Window</code>.
202         * @param originalColor the color the <code>ColorPicker</code> initially points to.
203         * @return the <code>Color</code> the user chooses, or <code>null</code> if the user cancels the dialog.
204         */
205        public static Color showDialog(Window owner,Color originalColor) {
206            return showDialog(owner, null, originalColor, false );
207        }
208            
209        /** This creates a modal dialog prompting the user to select a color.
210         * <P>This uses a generic dialog title: "Choose a Color".
211         * 
212         * @param owner the dialog this new dialog belongs to.  This must be a Frame or a Dialog.
213         * Java 1.6 supports Windows here, but this package is designed/compiled to work in Java 1.4,
214         * so an <code>IllegalArgumentException</code> will be thrown if this component is a <code>Window</code>.
215         * @param originalColor the color the <code>ColorPicker</code> initially points to.
216         * @param includeOpacity whether to add a control for the opacity of the color.
217         * @return the <code>Color</code> the user chooses, or <code>null</code> if the user cancels the dialog.
218         */
219        public static Color showDialog(Window owner,Color originalColor,boolean includeOpacity) {
220            return showDialog(owner, null, originalColor, includeOpacity );
221        }
222    
223        /** This creates a modal dialog prompting the user to select a color.
224         * 
225         * @param owner the dialog this new dialog belongs to.  This must be a Frame or a Dialog.
226         * Java 1.6 supports Windows here, but this package is designed/compiled to work in Java 1.4,
227         * so an <code>IllegalArgumentException</code> will be thrown if this component is a <code>Window</code>.
228         * @param title the title for the dialog.
229         * @param originalColor the color the <code>ColorPicker</code> initially points to.
230         * @param includeOpacity whether to add a control for the opacity of the color.
231         * @return the <code>Color</code> the user chooses, or <code>null</code> if the user cancels the dialog.
232         */
233        public static Color showDialog(Window owner, String title,Color originalColor,boolean includeOpacity) {
234            ColorPickerDialog d;
235            if(owner instanceof Frame || owner==null) {
236                d = new ColorPickerDialog( (Frame)owner, originalColor, includeOpacity);
237            } else if(owner instanceof Dialog){
238                d = new ColorPickerDialog( (Dialog)owner, originalColor, includeOpacity);
239            } else {
240                throw new IllegalArgumentException("the owner ("+owner.getClass().getName()+") must be a java.awt.Frame or a java.awt.Dialog");
241            }
242                    
243            d.setTitle(title == null ? 
244                       strings.getObject("ColorPickerDialogTitle").toString() : 
245                       title);
246            d.pack();
247            d.setVisible(true);
248            return d.getColor();
249        }
250    
251        /** <code>PropertyChangeEvents</code> will be triggered for this property when the selected color
252         * changes.
253         * <P>(Events are only created when then RGB values of the color change.  This means, for example,
254         * that the change from HSB(0,0,0) to HSB(.4,0,0) will <i>not</i> generate events, because when the
255         * brightness stays zero the RGB color remains (0,0,0).  So although the hue moved around, the color
256         * is still black, so no events are created.)
257         * 
258         */
259        public static final String SELECTED_COLOR_PROPERTY = "selected color";
260    
261    
262        /** <code>PropertyChangeEvents</code> will be triggered when the Hexadecimal value changes.
263         * (Fix by frederic.roudaut@free.fr because of some problems with SELECTED_COLOR_PROPERTY) 
264         */
265        public static final String HEX_COLOR_PROPERTY = "hexadecimal value";
266    
267    
268        /** <code>PropertyChangeEvents</code> will be triggered for this property when <code>setModeControlsVisible()</code>
269         * is called.
270         */
271        public static final String MODE_CONTROLS_VISIBLE_PROPERTY = "mode controls visible";
272            
273        /** <code>PropertyChangeEvents</code> will be triggered when the opacity value is
274         * adjusted.
275         */
276        public static final String OPACITY_PROPERTY = "opacity";
277            
278        /** <code>PropertyChangeEvents</code> will be triggered when the mode changes.
279         * (That is, when the wheel switches from HUE, SAT, BRI, RED, GREEN, or BLUE modes.)
280         */
281        public static final String MODE_PROPERTY = "mode";
282            
283        /** Used to indicate when we're in "hue mode". */
284        protected static final int HUE = 0;
285        /** Used to indicate when we're in "brightness mode". */
286        protected static final int BRI = 1;
287        /** Used to indicate when we're in "saturation mode". */
288        protected static final int SAT = 2;
289        /** Used to indicate when we're in "red mode". */
290        protected static final int RED = 3;
291        /** Used to indicate when we're in "green mode". */
292        protected static final int GREEN = 4;
293        /** Used to indicate when we're in "blue mode". */
294        protected static final int BLUE = 5;
295            
296        /** The vertical slider */
297        private JSlider slider = new JSlider(JSlider.VERTICAL,0,100,0);
298            
299        ChangeListener changeListener = new ChangeListener() {
300                public void stateChanged(ChangeEvent e) {
301                    Object src = e.getSource();                     
302                    if(hue.contains(src) || sat.contains(src) || bri.contains(src)) {
303                        if(adjustingSpinners>0)
304                            return;
305                                    
306                        setHSB( hue.getFloatValue()/360f,
307                                sat.getFloatValue()/100f,
308                                bri.getFloatValue()/100f );
309                    } else if(red.contains(src) || green.contains(src) || blue.contains(src)) {
310                        if(adjustingSpinners>0)
311                            return;
312                                                                   
313                        setRGB( red.getIntValue(),
314                                green.getIntValue(),
315                                blue.getIntValue() );
316                    } else if(src==colorPanel) {
317                        if(adjustingColorPanel>0)
318                            return;
319                                    
320                        int mode = getMode();
321                        if(mode==HUE || mode==BRI || mode==SAT) {
322                            float[] hsb = colorPanel.getHSB();
323                            setHSB(hsb[0],hsb[1],hsb[2]);
324                        } else {
325                            int[] rgb = colorPanel.getRGB();
326                            setRGB(rgb[0],rgb[1],rgb[2]);
327                        }
328                    } else if(src==slider) {                            
329                        if(adjustingSlider>0)
330                            return;
331                                    
332                        int v = slider.getValue();
333                        Option option = getSelectedOption();
334                        option.setValue(v);
335                    } else if(alpha.contains(src)) {
336                        if(adjustingOpacity>0)
337                            return;
338                        int v = alpha.getIntValue();
339                        setOpacity( ((float)v)/255f );
340                    } else if(src==opacitySlider) {
341                        if(adjustingOpacity>0) return;
342                                    
343                        float newValue = ( ((float)opacitySlider.getValue())/255f );
344                        setOpacity(newValue);
345                    }
346                }
347            };
348            
349        ActionListener actionListener = new ActionListener() {
350                public void actionPerformed(ActionEvent e) {
351                    Object src = e.getSource();
352                    if(src==hue.radioButton) {
353                        setMode(HUE);
354                    } else if(src==bri.radioButton) {
355                        setMode(BRI);
356                    } else if(src==sat.radioButton) {
357                        setMode(SAT);
358                    } else if(src==red.radioButton) {
359                        setMode(RED);
360                    } else if(src==green.radioButton) {
361                        setMode(GREEN);
362                    } else if(src==blue.radioButton) {
363                        setMode(BLUE);
364                    }
365                }
366            };
367            
368        /** @return the currently selected <code>Option</code>
369         */
370        private Option getSelectedOption() {
371            int mode = getMode();
372            if(mode==HUE) {
373                return hue;
374            } else if(mode==SAT) {
375                return sat;
376            } else if(mode==BRI) {
377                return bri;
378            } else if(mode==RED) {
379                return red;
380            } else if(mode==GREEN) {
381                return green;
382            } else {
383                return blue;
384            }
385        }
386            
387        /** This thread will wait a second or two before committing the text in
388         * the hex TextField.  This gives the user a chance to finish typing...
389         * but if the user is just waiting for something to happen, this makes sure
390         * after a second or two something happens.
391         */
392        class HexUpdateThread extends Thread {
393            long myStamp;
394            String text;
395                    
396            public HexUpdateThread(long stamp,String s) {
397                myStamp = stamp;
398                text = s;
399            }
400                    
401            public void run() {
402                if(SwingUtilities.isEventDispatchThread()==false) {
403                    long WAIT = 1500;
404    
405                    while(System.currentTimeMillis()-myStamp<WAIT) {
406                        try {
407                            long delay = WAIT - (System.currentTimeMillis()-myStamp);
408                            if(delay<1) delay = 1;
409                            Thread.sleep( delay );
410                        } catch(Exception e) {
411                            Thread.yield();
412                        }
413                    }
414                    SwingUtilities.invokeLater(this);
415                    return;
416                }
417    
418                if(myStamp!=hexDocListener.lastTimeStamp) {
419                    //another event has come along and trumped this one
420                    return;
421                }
422                            
423                if(text.length()>6)
424                    text = text.substring(0,6);
425                while(text.length()<6) {
426                    text = text+"0";
427                }
428                if(hexField.getText().equals(text))
429                    return;
430                            
431                int pos = hexField.getCaretPosition();
432                hexField.setText(text);
433                hexField.setCaretPosition(pos);
434            }
435        }
436            
437        HexDocumentListener hexDocListener = new HexDocumentListener();
438    
439        class HexDocumentListener implements DocumentListener {
440            long lastTimeStamp;
441                    
442            public void changedUpdate(DocumentEvent e) {
443                lastTimeStamp = System.currentTimeMillis();
444                            
445                if(adjustingHexField>0)
446                    return;
447                            
448                String s = hexField.getText();
449                s = stripToHex(s);
450                if(s.length()==6) {
451                    //the user typed 6 digits: we can work with this:
452                    try {
453                        int i = Integer.parseInt(s,16);
454                        setRGB( ((i >> 16) & 0xff), ((i >> 8) & 0xff), ((i) & 0xff) );
455                        return;
456                    } catch(NumberFormatException e2) {
457                        //this shouldn't happen, since we already stripped out non-hex characters.
458                        e2.printStackTrace();
459                    }
460                }
461                Thread thread = new HexUpdateThread(lastTimeStamp,s);
462                thread.start();
463                while(System.currentTimeMillis()-lastTimeStamp==0) {
464                    Thread.yield();
465                }
466            }
467                    
468            /** Strips a string down to only uppercase hex-supported characters. */
469            private String stripToHex(String s) {
470                s = s.toUpperCase();
471                String s2 = "";
472                for(int a = 0; a<s.length(); a++) {
473                    char c = s.charAt(a);
474                    if(c=='0' || c=='1' || c=='2' || c=='3' || c=='4' || c=='5' ||
475                       c=='6' || c=='7' || c=='8' || c=='9' || c=='0' ||
476                       c=='A' || c=='B' || c=='C' || c=='D' || c=='E' || c=='F') {
477                        s2 = s2+c;
478                    }
479                }
480                return s2;
481            }
482    
483            public void insertUpdate(DocumentEvent e) {
484                changedUpdate(e);
485            }
486    
487            public void removeUpdate(DocumentEvent e) {
488                changedUpdate(e);
489            }
490        };
491    
492        private Option alpha = new Option(strings.getObject("alphaLabel").toString(), 255);
493        private Option hue = new Option(strings.getObject("hueLabel").toString(), 360);
494        private Option sat = new Option(strings.getObject("saturationLabel").toString(), 100);
495        private Option bri = new Option(strings.getObject("brightnessLabel").toString(), 100);
496        private Option red = new Option(strings.getObject("redLabel").toString(), 255);
497        private Option green = new Option(strings.getObject("greenLabel").toString(), 255);
498        private Option blue = new Option(strings.getObject("blueLabel").toString(), 255);
499        private ColorSwatch preview = new ColorSwatch(50);
500        private JLabel hexLabel = new JLabel(strings.getObject("hexLabel").toString());
501        private JTextField hexField = new JTextField("000000");
502            
503        /** Used to indicate when we're internally adjusting the value of the spinners.
504         * If this equals zero, then incoming events are triggered by the user and must be processed.
505         * If this is not equal to zero, then incoming events are triggered by another method
506         * that's already responding to the user's actions.
507         */
508        private int adjustingSpinners = 0;
509    
510        /** Used to indicate when we're internally adjusting the value of the slider.
511         * If this equals zero, then incoming events are triggered by the user and must be processed.
512         * If this is not equal to zero, then incoming events are triggered by another method
513         * that's already responding to the user's actions.
514         */
515        private int adjustingSlider = 0;
516    
517        /** Used to indicate when we're internally adjusting the selected color of the ColorPanel.
518         * If this equals zero, then incoming events are triggered by the user and must be processed.
519         * If this is not equal to zero, then incoming events are triggered by another method
520         * that's already responding to the user's actions.
521         */
522        private int adjustingColorPanel = 0;
523    
524        /** Used to indicate when we're internally adjusting the value of the hex field.
525         * If this equals zero, then incoming events are triggered by the user and must be processed.
526         * If this is not equal to zero, then incoming events are triggered by another method
527         * that's already responding to the user's actions.
528         */
529        private int adjustingHexField = 0;
530    
531        /** Used to indicate when we're internally adjusting the value of the opacity.
532         * If this equals zero, then incoming events are triggered by the user and must be processed.
533         * If this is not equal to zero, then incoming events are triggered by another method
534         * that's already responding to the user's actions.
535         */
536        private int adjustingOpacity = 0;
537            
538        /** The "expert" controls are the controls on the right side
539         * of this panel: the labels/spinners/radio buttons.
540         */
541        private JPanel expertControls = new JPanel(new GridBagLayout());
542            
543        private ColorPickerPanel colorPanel = new ColorPickerPanel();
544            
545        private JSlider opacitySlider = new JSlider(0,255,255);
546        private JLabel opacityLabel = new JLabel(strings.getObject("opacityLabel").toString());
547            
548        /** Create a new <code>ColorPicker</code> with all controls visible except opacity. */
549        public ColorPicker() {
550            this(true,false);
551        }
552            
553        /** Create a new <code>ColorPicker</code>.
554         * 
555         * @param showExpertControls the labels/spinners/buttons on the right side of a
556         * <code>ColorPicker</code> are optional.  This boolean will control whether they
557         * are shown or not.
558         * <P>It may be that your users will never need or want numeric control when
559         * they choose their colors, so hiding this may simplify your interface.
560         * @param includeOpacity whether the opacity controls will be shown
561         */
562        public ColorPicker(boolean showExpertControls,boolean includeOpacity) {
563            super(new GridBagLayout());
564            GridBagConstraints c = new GridBagConstraints();
565                    
566            Insets normalInsets = new Insets(3,3,3,3);
567                    
568            JPanel options = new JPanel(new GridBagLayout());
569            c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1;
570            c.insets = normalInsets;
571            ButtonGroup bg = new ButtonGroup();
572                    
573            //put them in order
574            Option[] optionsArray = new Option[] {
575                hue, sat, bri, red, green, blue
576            };
577                    
578            for(int a = 0; a<optionsArray.length; a++) {
579                if(a==3 || a==6) {
580                    c.insets = new Insets(normalInsets.top+10,normalInsets.left,normalInsets.bottom,normalInsets.right);
581                } else {
582                    c.insets = normalInsets;
583                }
584                c.anchor = GridBagConstraints.EAST;
585                c.fill = GridBagConstraints.NONE;
586                options.add(optionsArray[a].label,c);
587                c.gridx++;
588                c.anchor = GridBagConstraints.WEST;
589                c.fill = GridBagConstraints.HORIZONTAL;
590                if(optionsArray[a].spinner!=null) {
591                    options.add(optionsArray[a].spinner,c);
592                } else {
593                    options.add(optionsArray[a].slider,c);
594                }
595                c.gridx++; c.fill = GridBagConstraints.NONE;
596                options.add(optionsArray[a].radioButton,c);
597                c.gridy++;
598                c.gridx = 0;
599                bg.add(optionsArray[a].radioButton);
600            }
601            c.insets = new Insets(normalInsets.top+10,normalInsets.left,normalInsets.bottom,normalInsets.right);
602            c.anchor = GridBagConstraints.EAST; c.fill = GridBagConstraints.NONE;
603            options.add(hexLabel,c);
604            c.gridx++;
605            c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL;
606            options.add(hexField,c);
607            c.gridy++; c.gridx = 0;
608            c.anchor = GridBagConstraints.EAST; c.fill = GridBagConstraints.NONE;
609            options.add(alpha.label,c);
610            c.gridx++;
611            c.anchor = GridBagConstraints.WEST; c.fill = GridBagConstraints.HORIZONTAL;
612            options.add(alpha.spinner,c);
613                    
614            c.gridx = 0; c.gridy = 0; c.weightx = 1;
615            c.weighty = 1; c.fill = GridBagConstraints.BOTH;
616            c.anchor = GridBagConstraints.CENTER; c.insets = normalInsets;
617            c.gridwidth = 2;
618            add(colorPanel,c);
619                    
620            c.gridwidth = 1;
621            c.insets = normalInsets;
622            c.gridx+=2; c.weighty = 1; c.gridwidth = 1;
623            c.fill = GridBagConstraints.VERTICAL; c.weightx = 0;
624            add(slider,c);
625                    
626            c.gridx++; c.fill = GridBagConstraints.VERTICAL; c.gridheight = c.REMAINDER;
627            c.anchor = GridBagConstraints.CENTER; c.insets = new Insets(0,0,0,0);
628            add(expertControls,c);
629                    
630            c.gridx = 0; c.gridheight = 1;
631            c.gridy = 1; c.weightx = 0; c.weighty = 0;
632            c.insets = normalInsets; c.anchor = c.CENTER;
633            add(opacityLabel,c);
634            c.gridx++; c.gridwidth = 2;
635            c.weightx = 1; c.fill = c.HORIZONTAL;
636            add(opacitySlider,c);
637                    
638            c.gridx = 0; c.gridy = 0;
639            c.gridheight = 1; c.gridwidth = 1;
640            c.fill = GridBagConstraints.BOTH; 
641            c.weighty = 1; c.anchor = GridBagConstraints.CENTER; 
642            c.weightx = 1;
643            c.insets = new Insets(normalInsets.top,normalInsets.left+8,normalInsets.bottom+10,normalInsets.right+8);
644            expertControls.add(preview,c);
645            c.gridy++; c.weighty = 0; c.anchor = GridBagConstraints.CENTER;
646            c.insets = new Insets(normalInsets.top,normalInsets.left,0,normalInsets.right);
647            expertControls.add(options,c);
648                    
649            preview.setOpaque(true);
650    
651            colorPanel.setPreferredSize(new Dimension(expertControls.getPreferredSize().height, 
652                                                      expertControls.getPreferredSize().height));
653                                    
654            slider.addChangeListener(changeListener);               
655            colorPanel.addChangeListener(changeListener);
656            slider.setUI(new ColorPickerSliderUI(slider,this));
657            hexField.getDocument().addDocumentListener(hexDocListener);
658    
659            setMode(BRI);                           
660                    
661            setExpertControlsVisible(showExpertControls);
662                    
663            setOpacityVisible(includeOpacity);
664                    
665            opacitySlider.addChangeListener(changeListener);
666            
667            setOpacity(1);          
668    
669        }
670            
671        /** This controls whether the hex field (and label) are visible or not.
672         * <P>Note this lives inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
673         * has been called, then calling this method makes no difference: the hex controls will be hidden.
674         */
675        public void setHexControlsVisible(boolean b) {
676            hexLabel.setVisible(b);
677            hexField.setVisible(b);
678        }
679    
680        /** This controls whether the preview swatch visible or not.
681         * <P>Note this lives inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
682         * has been called, then calling this method makes no difference: the swatch will be hidden.
683         */
684        public void setPreviewSwatchVisible(boolean b) {
685            preview.setVisible(b);
686        }
687            
688        /** The labels/spinners/buttons on the right side of a <code>ColorPicker</code>
689         * are optional.  This method will control whether they are shown or not.
690         * <P>It may be that your users will never need or want numeric control when
691         * they choose their colors, so hiding this may simplify your interface.
692         * 
693         * @param b whether to show or hide the expert controls.
694         */
695        public void setExpertControlsVisible(boolean b) {
696            expertControls.setVisible(b);
697        }
698            
699        /** @return the current HSB coordinates of this <code>ColorPicker</code>.
700         * Each value is between [0,1].
701         * 
702         */
703        public float[] getHSB() {
704            return new float[] {
705                hue.getFloatValue()/360f,
706                sat.getFloatValue()/100f,
707                bri.getFloatValue()/100f
708            };
709        }
710    
711        /** @return the current RGB coordinates of this <code>ColorPicker</code>.
712         * Each value is between [0,255].
713         * 
714         */
715        public int[] getRGB() {
716            return new int[] {
717                red.getIntValue(),
718                green.getIntValue(),
719                blue.getIntValue()
720            };
721        }
722            
723        /** Returns the currently selected opacity (a float between 0 and 1). 
724         * 
725         * @return the currently selected opacity (a float between 0 and 1).
726         */
727        public float getOpacity() {
728            return ((float)opacitySlider.getValue())/255f;
729        }
730            
731        private float lastOpacity = 1;
732            
733        /** Sets the currently selected opacity.
734         * 
735         * @param v a float between 0 and 1.
736         */
737        public void setOpacity(float v) {
738            if(v<0 || v>1) 
739                throw new IllegalArgumentException("The opacity ("+v+") must be between 0 and 1.");
740            adjustingOpacity++;
741            try {
742                int i = (int)(255*v);
743                opacitySlider.setValue( i );
744                alpha.spinner.setValue( new Integer(i) );
745                if(lastOpacity!=v) {
746                    firePropertyChange(OPACITY_PROPERTY,new Float(lastOpacity),new Float(i));
747                    Color c = preview.getForeground();
748                    preview.setForeground(new Color(c.getRed(), c.getGreen(), c.getBlue(), i));
749                }
750                lastOpacity = v;
751            } finally {
752                adjustingOpacity--;
753            }
754        }
755            
756        /** Sets the mode of this <code>ColorPicker</code>.
757         * This is especially useful if this picker is in non-expert mode, so
758         * the radio buttons are not visible for the user to directly select.
759         * 
760         * @param mode must be HUE, SAT, BRI, RED, GREEN or BLUE.
761         */
762        public void setMode(int mode) {
763            if(!(mode==HUE || mode==SAT || mode==BRI || mode==RED || mode==GREEN || mode==BLUE))
764                throw new IllegalArgumentException("mode must be HUE, SAT, BRI, REd, GREEN, or BLUE");
765            putClientProperty(MODE_PROPERTY,new Integer(mode));
766            hue.radioButton.setSelected(mode==HUE);
767            sat.radioButton.setSelected(mode==SAT);
768            bri.radioButton.setSelected(mode==BRI);
769            red.radioButton.setSelected(mode==RED);
770            green.radioButton.setSelected(mode==GREEN);
771            blue.radioButton.setSelected(mode==BLUE);
772    
773            colorPanel.setMode(mode);
774            adjustingSlider++;
775            try {
776                slider.setValue(0);
777                Option option = getSelectedOption();
778                slider.setInverted(mode==HUE);
779                int max = option.getMaximum();
780                slider.setMaximum(max);
781                slider.setValue( option.getIntValue() );
782                slider.repaint();
783                            
784                if(mode==HUE || mode==SAT || mode==BRI) {
785                    setHSB( hue.getFloatValue()/360f,
786                            sat.getFloatValue()/100f,
787                            bri.getFloatValue()/100f );
788                } else {
789                    setRGB( red.getIntValue(),
790                            green.getIntValue(),
791                            blue.getIntValue() );
792                                    
793                }
794            } finally {
795                adjustingSlider--;
796            }
797        }
798            
799        /** This controls whether the radio buttons that adjust the mode are visible.
800         * <P>(These buttons appear next to the spinners in the expert controls.)
801         * <P>Note these live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
802         * has been called, then these will never be visible.
803         * 
804         * @param b
805         */
806        public void setModeControlsVisible(boolean b) {
807            hue.radioButton.setVisible(b && hue.isVisible());
808            sat.radioButton.setVisible(b && sat.isVisible());
809            bri.radioButton.setVisible(b && bri.isVisible());
810            red.radioButton.setVisible(b && red.isVisible());
811            green.radioButton.setVisible(b && green.isVisible());
812            blue.radioButton.setVisible(b && blue.isVisible());
813            putClientProperty(MODE_CONTROLS_VISIBLE_PROPERTY,new Boolean(b));
814        }
815            
816        /** @return the current mode of this <code>ColorPicker</code>.
817         * <BR>This will return <code>HUE</code>,  <code>SAT</code>,  <code>BRI</code>, 
818         * <code>RED</code>,  <code>GREEN</code>, or <code>BLUE</code>.
819         * <P>The default mode is <code>BRI</code>, because that provides the most
820         * aesthetic/recognizable color wheel.
821         */
822        public int getMode() {
823            Integer i = (Integer)getClientProperty(MODE_PROPERTY);
824            if(i==null) return -1;
825            return i.intValue();
826        }
827    
828        /** Sets the current color of this <code>ColorPicker</code>.
829         * This method simply calls <code>setRGB()</code> and <code>setOpacity()</code>.
830         * @param c the new color to use.
831         */
832        public void setColor(Color c) {
833            setRGB(c.getRed(),c.getGreen(),c.getBlue());
834            float opacity = ((float)c.getAlpha())/255f;
835            setOpacity(opacity);
836        }
837            
838        /** Sets the current color of this <code>ColorPicker</code>
839         * 
840         * @param r the red value.  Must be between [0,255].
841         * @param g the green value.  Must be between [0,255].
842         * @param b the blue value.  Must be between [0,255].
843         */
844        public void setRGB(int r,int g,int b) {
845            if(r<0 || r>255)
846                throw new IllegalArgumentException("The red value ("+r+") must be between [0,255].");
847            if(g<0 || g>255)
848                throw new IllegalArgumentException("The green value ("+g+") must be between [0,255].");
849            if(b<0 || b>255)
850                throw new IllegalArgumentException("The blue value ("+b+") must be between [0,255].");
851                    
852                    
853            Color lastColor = getColor();         
854            boolean updateRGBSpinners = adjustingSpinners==0;
855                    
856            adjustingSpinners++;
857            adjustingColorPanel++;
858            int alpha = this.alpha.getIntValue();
859            try {
860                if(updateRGBSpinners) {
861                    red.setValue(r);
862                    green.setValue(g);
863                    blue.setValue(b);
864                }
865                preview.setForeground(new Color(r,g,b, alpha));
866                float[] hsb = new float[3];
867                Color.RGBtoHSB(r, g, b, hsb);
868                hue.setValue( (int)(hsb[0]*360f+.49f));
869                sat.setValue( (int)(hsb[1]*100f+.49f));
870                bri.setValue( (int)(hsb[2]*100f+.49f));
871                colorPanel.setRGB(r, g, b);
872                updateHexField();
873                updateSlider();
874            } finally {
875                adjustingSpinners--;
876                adjustingColorPanel--;
877            }
878            Color newColor = getColor();
879            if(lastColor.equals(newColor)==false)           
880                firePropertyChange(SELECTED_COLOR_PROPERTY,lastColor,newColor);
881                    
882        }
883            
884        /** @return the current <code>Color</code> this <code>ColorPicker</code> has selected.
885         * <P>This is equivalent to:
886         * <BR><code>int[] i = getRGB();</code>
887         * <BR><code>return new Color(i[0], i[1], i[2], opacitySlider.getValue());</code>
888         */
889        public Color getColor() {       
890            int[] i = getRGB();
891            return new Color(i[0], i[1], i[2], opacitySlider.getValue());
892        }
893            
894        private void updateSlider() {
895            adjustingSlider++;
896            try {
897                int mode = getMode();
898                if(mode==HUE) {
899                    slider.setValue( hue.getIntValue() );
900                } else if(mode==SAT) {
901                    slider.setValue( sat.getIntValue() );
902                } else if(mode==BRI) {
903                    slider.setValue( bri.getIntValue() );
904                } else if(mode==RED) {
905                    slider.setValue( red.getIntValue() );
906                } else if(mode==GREEN) {
907                    slider.setValue( green.getIntValue() );
908                } else if(mode==BLUE) {
909                    slider.setValue( blue.getIntValue() );
910                }
911            } finally {
912                adjustingSlider--;
913            }
914            slider.repaint();
915        }
916            
917        /** This returns the panel with several rows of spinner controls.
918         * <P>Note you can also call methods such as <code>setRGBControlsVisible()</code> to adjust
919         * which controls are showing.
920         * <P>(This returns the panel this <code>ColorPicker</code> uses, so if you put it in
921         * another container, it will be removed from this <code>ColorPicker</code>.)
922         * @return the panel with several rows of spinner controls.
923         */
924        public JPanel getExpertControls() {
925            return expertControls;
926        }
927            
928        /** This shows or hides the RGB spinner controls.
929         * <P>Note these live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
930         * has been called, then calling this method makes no difference: the RGB controls will be hidden.
931         * 
932         * @param b whether the controls should be visible or not.
933         */
934        public void setRGBControlsVisible(boolean b) {
935            red.setVisible(b);
936            green.setVisible(b);
937            blue.setVisible(b);
938        }
939    
940        /** This shows or hides the HSB spinner controls.
941         * <P>Note these live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
942         * has been called, then calling this method makes no difference: the HSB controls will be hidden.
943         * 
944         * @param b whether the controls should be visible or not.
945         */
946        public void setHSBControlsVisible(boolean b) {
947            hue.setVisible(b);
948            sat.setVisible(b);
949            bri.setVisible(b);
950        }
951    
952        /** This shows or hides the alpha controls.
953         * <P>Note the alpha spinner live inside the "expert controls", so if <code>setExpertControlsVisible(false)</code>
954         * has been called, then this method does not affect that spinner.
955         * However, the opacity slider is <i>not</i> affected by the visibility of the export controls.
956         * @param b
957         */
958        public void setOpacityVisible(boolean b) {
959            opacityLabel.setVisible(b);
960            opacitySlider.setVisible(b);
961            alpha.label.setVisible(b);
962            alpha.spinner.setVisible(b);
963        }
964            
965        /** @return the <code>ColorPickerPanel</code> this <code>ColorPicker</code> displays. */
966        public ColorPickerPanel getColorPanel() {
967            return colorPanel;
968        }
969    
970        /** Sets the current color of this <code>ColorPicker</code>
971         * 
972         * @param h the hue value.
973         * @param s the saturation value.  Must be between [0,1].
974         * @param b the blue value.  Must be between [0,1].
975         */
976        public void setHSB(float h, float s, float b) {
977            if(Float.isInfinite(h) || Float.isNaN(h))
978                throw new IllegalArgumentException("The hue value ("+h+") is not a valid number.");
979            //hue is cyclic, so it can be any value:
980            while(h<0) h++;
981            while(h>1) h--;
982                    
983            if(s<0 || s>1)
984                throw new IllegalArgumentException("The saturation value ("+s+") must be between [0,1]");
985            if(b<0 || b>1)
986                throw new IllegalArgumentException("The brightness value ("+b+") must be between [0,1]");
987                    
988            Color lastColor = getColor();
989                    
990            boolean updateHSBSpinners = adjustingSpinners==0;
991            adjustingSpinners++;
992            adjustingColorPanel++;
993            try {
994                if(updateHSBSpinners) {
995                    hue.setValue( (int)(h*360f+.49f));
996                    sat.setValue( (int)(s*100f+.49f));
997                    bri.setValue( (int)(b*100f+.49f));
998                }
999                            
1000                Color c = new Color(Color.HSBtoRGB(h, s, b));
1001                int alpha = this.alpha.getIntValue();
1002                c = new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
1003                preview.setForeground(c);
1004                red.setValue(c.getRed());
1005                green.setValue(c.getGreen());
1006                blue.setValue(c.getBlue());
1007                colorPanel.setHSB(h, s, b);
1008                updateHexField();
1009                updateSlider();
1010                slider.repaint();
1011            } finally {
1012                adjustingSpinners--;
1013                adjustingColorPanel--;
1014            }
1015            Color newColor = getColor();
1016            if(lastColor.equals(newColor)==false)           
1017                firePropertyChange(SELECTED_COLOR_PROPERTY,lastColor,newColor);
1018        }
1019            
1020        private void updateHexField() {
1021            adjustingHexField++;
1022            try {
1023                int r = red.getIntValue();
1024                int g = green.getIntValue();
1025                int b = blue.getIntValue();
1026                            
1027                int i = (r << 16) + (g << 8) +b;
1028                String s = Integer.toHexString(i).toUpperCase();
1029                while(s.length()<6)
1030                    s = "0"+s;
1031                if(hexField.getText().equalsIgnoreCase(s)==false)
1032                    {
1033                        // Fix by frederic.roudaut@free.fr
1034                        firePropertyChange(HEX_COLOR_PROPERTY,hexField.getText(),s);
1035                        hexField.setText(s);
1036                    }
1037            } finally {
1038                adjustingHexField--;
1039            }
1040        }
1041    
1042        class Option {
1043            JRadioButton radioButton = new JRadioButton();
1044            JSpinner spinner;
1045            JSlider slider;
1046            JLabel label;
1047            public Option(String text,int max) {
1048                spinner = new JSpinner(new SpinnerNumberModel(0,0,max,5));
1049                spinner.addChangeListener(changeListener);
1050                            
1051                /*this tries out Tim Boudreaux's new slider UI.
1052                 * It's a good UI, but I think for the ColorPicker
1053                 * the numeric controls are more useful.
1054                 * That is: users who want click-and-drag control to choose
1055                 * their colors don't need any of these Option objects
1056                 * at all; only power users who may have specific RGB
1057                 * values in mind will use these controls: and when they do
1058                 * limiting them to a slider is unnecessary.
1059                 * That's my current position... of course it may
1060                 * not be true in the real world... :)
1061                 */
1062                //slider = new JSlider(0,max);
1063                //slider.addChangeListener(changeListener);
1064                //slider.setUI(new org.netbeans.paint.api.components.PopupSliderUI());
1065                                    
1066                label = new JLabel(text);
1067                radioButton.addActionListener(actionListener);
1068            }
1069                    
1070            public void setValue(int i) {
1071                if(slider!=null) {
1072                    slider.setValue(i);
1073                }
1074                if(spinner!=null) {
1075                    spinner.setValue(new Integer(i));
1076                }
1077            }
1078                    
1079            public int getMaximum() {
1080                if(slider!=null)
1081                    return slider.getMaximum();
1082                return ((Number) ((SpinnerNumberModel)spinner.getModel()).getMaximum() ).intValue();
1083            }
1084                    
1085            public boolean contains(Object src) {
1086                return (src==slider || src==spinner || src==radioButton || src==label);
1087            }
1088                    
1089            public float getFloatValue() {
1090                return getIntValue();
1091            }
1092                    
1093            public int getIntValue() {
1094                if(slider!=null)
1095                    return slider.getValue();
1096                return ((Number)spinner.getValue()).intValue();
1097            }
1098                    
1099            public boolean isVisible() {
1100                return label.isVisible();
1101            }
1102                    
1103            public void setVisible(boolean b) {
1104                boolean radioButtonsAllowed = true;
1105                Boolean z = (Boolean)getClientProperty(MODE_CONTROLS_VISIBLE_PROPERTY);
1106                if(z!=null) radioButtonsAllowed = z.booleanValue();
1107                            
1108                radioButton.setVisible(b && radioButtonsAllowed);
1109                if(slider!=null)
1110                    slider.setVisible(b);
1111                if(spinner!=null)
1112                    spinner.setVisible(b);
1113                label.setVisible(b);
1114            }
1115        }
1116    }