001    /*
002     * @(#)ColorPickerPanel.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 java.util.*;
017    import javax.swing.event.*;
018    import java.awt.*;
019    import javax.swing.*;
020    import java.awt.event.*;
021    import java.awt.geom.*;
022    import java.awt.image.*;
023    import com.colorpicker.awt.*;
024    
025    /** This is the large graphic element in the <code>ColorPicker</code>
026     * that depicts a wide range of colors.
027     * <P>This panel can operate in 6 different modes.  In each mode a different
028     * property is held constant: hue, saturation, brightness, red, green, or blue.
029     * (Each property is identified with a constant in the <code>ColorPicker</code> class,
030     * such as: <code>ColorPicker.HUE</code> or <code>ColorPicker.GREEN</code>.)
031     * <P>In saturation and brightness mode, a wheel is used.  Although it doesn't
032     * use as many pixels as a square does: it is a very aesthetic model since the hue can
033     * wrap around in a complete circle.  (Also, on top of looks, this is how most
034     * people learn to think the color spectrum, so it has that advantage, too).
035     * In all other modes a square is used.
036     * <P>The user can click in this panel to select a new color.  The selected color is
037     * highlighted with a circle drawn around it.  Also once this
038     * component has the keyboard focus, the user can use the arrow keys to
039     * traverse the available colors.
040     * <P>Note this component is public and exists independently of the
041     * <code>ColorPicker</code> class.  The only way this class is dependent
042     * on the <code>ColorPicker</code> class is when the constants for the modes
043     * are used.
044     * <P>The graphic in this panel will be based on either the width or
045     * the height of this component: depending on which is smaller.
046     *
047     * @version 1.0
048     * @author Jeremy Wood
049     */
050    public class ColorPickerPanel extends JPanel {
051        private static final long serialVersionUID = 1L;
052            
053        /** The maximum size the graphic will be.  No matter
054         *  how big the panel becomes, the graphic will not exceed
055         *  this length.
056         *  <P>(This is enforced because only 1 BufferedImage is used
057         *  to render the graphic.  This image is created once at a fixed
058         *  size and is never replaced.)
059         */
060        public static int MAX_SIZE = 325;
061        private int mode = ColorPicker.BRI;
062        private Point point = new Point(0,0);
063        private Vector changeListeners;
064            
065        /* Floats from [0,1].  They must be kept distinct, because
066         * when you convert them to RGB coordinates HSB(0,0,0) and HSB (.5,0,0)
067         * and then convert them back to HSB coordinates, the hue always shifts back to zero.
068         */
069        float hue = -1, sat = -1, bri = -1;
070        int red = -1, green = -1, blue = -1;
071            
072        MouseInputListener mouseListener = new MouseInputAdapter() {
073                public void mousePressed(MouseEvent e) {
074                    requestFocus();
075                    Point p = e.getPoint();
076                    int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom));
077                    p.translate(-(getWidth()/2-size/2), -(getHeight()/2-size/2));
078                    if(mode==ColorPicker.BRI || mode==ColorPicker.SAT) {
079                        //the two circular views:
080                        double radius = ((double)size)/2.0;
081                        double x = p.getX()-size/2.0;
082                        double y = p.getY()-size/2.0;
083                        double r = Math.sqrt(x*x+y*y)/radius;
084                        double theta = Math.atan2(y,x)/(Math.PI*2.0);
085                                    
086                        if(r>1) r = 1;
087                                    
088                        if(mode==ColorPicker.BRI) {
089                            setHSB((float)(theta+.25f),
090                                   (float)(r),
091                                   bri);
092                        } else {
093                            setHSB((float)(theta+.25f),
094                                   sat,
095                                   (float)(r) );
096                        }
097                    } else if(mode==ColorPicker.HUE) {
098                        float s = ((float)p.x)/((float)size);
099                        float b = ((float)p.y)/((float)size);
100                        if(s<0) s = 0;
101                        if(s>1) s = 1;
102                        if(b<0) b = 0;
103                        if(b>1) b = 1;
104                        setHSB( hue,
105                                s,
106                                b );
107                    } else {
108                        int x2 = p.x*255/size;
109                        int y2 = p.y*255/size;
110                        if(x2<0) x2 = 0;
111                        if(x2>255) x2 = 255;
112                        if(y2<0) y2 = 0;
113                        if(y2>255) y2 = 255;
114                                    
115                        if(mode==ColorPicker.RED) {
116                            setRGB(red,x2,y2);
117                        } else if(mode==ColorPicker.GREEN) {
118                            setRGB(x2,green,y2);
119                        } else {
120                            setRGB(x2,y2,blue);
121                        }
122                    }
123                }
124    
125                public void mouseDragged(MouseEvent e) {
126                    mousePressed(e);
127                }
128            };
129            
130        KeyListener keyListener = new KeyAdapter() {
131                public void keyPressed(KeyEvent e) {
132                    int dx = 0;
133                    int dy = 0;
134                    if(e.getKeyCode()==KeyEvent.VK_LEFT) {
135                        dx = -1;
136                    } else if(e.getKeyCode()==KeyEvent.VK_RIGHT) {
137                        dx = 1;
138                    } else if(e.getKeyCode()==KeyEvent.VK_UP) {
139                        dy = -1;
140                    } else if(e.getKeyCode()==KeyEvent.VK_DOWN) {
141                        dy = 1;
142                    }
143                    int multiplier = 1;
144                    if(e.isShiftDown() && e.isAltDown()) {
145                        multiplier = 10;
146                    } else if(e.isShiftDown() || e.isAltDown()) {
147                        multiplier = 5;
148                    }
149                    if(dx!=0 || dy!=0) {
150                        int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom));
151                                    
152                        int offsetX = getWidth()/2-size/2;
153                        int offsetY = getHeight()/2-size/2;
154                        mouseListener.mousePressed(new MouseEvent(ColorPickerPanel.this,
155                                                                  MouseEvent.MOUSE_PRESSED,
156                                                                  System.currentTimeMillis(), 0,
157                                                                  point.x+multiplier*dx+offsetX,
158                                                                  point.y+multiplier*dy+offsetY,
159                                                                  1, false
160                                                                  ));
161                    }
162                }
163            };
164            
165        FocusListener focusListener = new FocusListener() {
166                public void focusGained(FocusEvent e) {
167                    repaint();
168                }
169                public void focusLost(FocusEvent e) {
170                    repaint();
171                }
172            };
173            
174        ComponentListener componentListener = new ComponentAdapter() {
175    
176                public void componentResized(ComponentEvent e) {
177    
178                    regeneratePoint();
179                    regenerateImage();
180                }
181                    
182            };
183            
184        BufferedImage image = new BufferedImage(MAX_SIZE, MAX_SIZE, BufferedImage.TYPE_INT_ARGB);
185            
186        /** Creates a new <code>ColorPickerPanel</code> */
187        public ColorPickerPanel() {
188            setMaximumSize(new Dimension(MAX_SIZE+imagePadding.left+imagePadding.right, 
189                                         MAX_SIZE+imagePadding.top+imagePadding.bottom));
190            setPreferredSize(new Dimension( (int)(MAX_SIZE*.75), (int)(MAX_SIZE*.75)));
191                    
192            setRGB(0,0,0);
193            addMouseListener(mouseListener);
194            addMouseMotionListener(mouseListener);
195                    
196            setFocusable(true);
197            addKeyListener(keyListener);
198            addFocusListener(focusListener);
199    
200            setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
201            addComponentListener(componentListener);
202        }
203            
204        /** This listener will be notified when the current HSB or RGB values
205         * change, depending on what mode the user is in.
206         */
207        public void addChangeListener(ChangeListener l) {
208            if(changeListeners==null)
209                changeListeners = new Vector();
210            if(changeListeners.contains(l))
211                return;
212            changeListeners.add(l);
213        }
214            
215        /** Remove a <code>ChangeListener</code> so it is no longer
216         * notified when the selected color changes.
217         */
218        public void removeChangeListener(ChangeListener l) {
219            if(changeListeners==null)
220                return;
221            changeListeners.remove(l);
222        }
223            
224        protected void fireChangeListeners() {
225            if(changeListeners==null)
226                return;
227            for(int a = 0; a<changeListeners.size(); a++) {
228                ChangeListener l = (ChangeListener)changeListeners.get(a);
229                try {
230                    l.stateChanged(new ChangeEvent(this));
231                } catch(RuntimeException e) {
232                    e.printStackTrace();
233                }
234            }
235        }
236            
237        Insets imagePadding = new Insets(6,6,6,6);
238            
239        public void paint(Graphics g) {
240            super.paint(g);
241    
242            Graphics2D g2 = (Graphics2D)g;
243            int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom));
244                    
245            g2.translate(getWidth()/2-size/2, getHeight()/2-size/2);
246            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
247    
248            Shape shape;
249                            
250            if(mode==ColorPicker.SAT || mode==ColorPicker.BRI) {
251                shape = new Ellipse2D.Float(0,0,size,size);
252            } else {
253                Rectangle r = new Rectangle(0,0,size,size);
254                shape = r;
255            }
256                    
257            if(hasFocus()) {
258                PaintUtils.paintFocus(g2,shape,5);
259            }
260                    
261            if(!(shape instanceof Rectangle)) {
262                //paint a circular shadow
263                g2.translate(2,2);
264                g2.setColor(new Color(0,0,0,20));
265                g2.fill(new Ellipse2D.Float(-2,-2,size+4,size+4));
266                g2.setColor(new Color(0,0,0,40));
267                g2.fill(new Ellipse2D.Float(-1,-1,size+2,size+2));
268                g2.setColor(new Color(0,0,0,80));
269                g2.fill(new Ellipse2D.Float(0,0,size,size));
270                g2.translate(-2,-2);
271            }
272                    
273            g2.drawImage(image, 0, 0, size, size, 0, 0, size, size, null);
274                    
275            if(shape instanceof Rectangle) {
276                Rectangle r = (Rectangle)shape;
277                PaintUtils.drawBevel(g2,r);
278            } else {
279                g2.setColor(new Color(0,0,0,120));
280                g2.draw(shape);
281            }
282                    
283            g2.setColor(Color.white);
284            g2.setStroke(new BasicStroke(1));
285            g2.draw(new Ellipse2D.Float(point.x-3,point.y-3,6,6));
286            g2.setColor(Color.black);
287            g2.draw(new Ellipse2D.Float(point.x-4,point.y-4,8,8));
288                    
289            g.translate(-imagePadding.left, -imagePadding.top);
290        }
291            
292        /** Set the mode of this panel.
293         * @param mode This must be one of the following constants from the <code>ColorPicker</code> class:
294         * <code>HUE</code>, <code>SAT</code>, <code>BRI</code>, <code>RED</code>, <code>GREEN</code>, or <code>BLUE</code>
295         */
296        public void setMode(int mode) {
297            if(!(mode==ColorPicker.HUE || mode==ColorPicker.SAT || mode==ColorPicker.BRI || 
298                 mode==ColorPicker.RED || mode==ColorPicker.GREEN || mode==ColorPicker.BLUE))
299                throw new IllegalArgumentException("The mode must be HUE, SAT, BRI, RED, GREEN, or BLUE.");
300                            
301            if(this.mode==mode)
302                return;
303            this.mode = mode;
304            regenerateImage();
305            regeneratePoint();
306        }
307            
308        /** Sets the selected color of this panel.
309         * <P>If this panel is in HUE, SAT, or BRI mode, then
310         * this method converts these values to HSB coordinates
311         * and calls <code>setHSB</code>.
312         * <P>This method may regenerate the graphic if necessary.
313         * 
314         * @param r the red value of the selected color.
315         * @param g the green value of the selected color.
316         * @param b the blue value of the selected color.
317         */
318        public void setRGB(int r,int g,int b) {                             
319                    
320            if(r<0 || r>255)
321                throw new IllegalArgumentException("The red value ("+r+") must be between [0,255].");
322            if(g<0 || g>255)
323                throw new IllegalArgumentException("The green value ("+g+") must be between [0,255].");
324            if(b<0 || b>255)
325                throw new IllegalArgumentException("The blue value ("+b+") must be between [0,255].");
326                    
327            if(red!=r || green!=g || blue!=b) {
328                if(mode==ColorPicker.RED || 
329                   mode==ColorPicker.GREEN ||
330                   mode==ColorPicker.BLUE) {
331                    int lastR = red;
332                    int lastG = green;
333                    int lastB = blue;
334                    red = r;
335                    green = g;
336                    blue = b;
337                                    
338                    if(mode==ColorPicker.RED) {
339                        if(lastR!=r) {
340                            regenerateImage();
341                        }
342                    } else if(mode==ColorPicker.GREEN) {
343                        if(lastG!=g) {
344                            regenerateImage();
345                        }
346                    } else if(mode==ColorPicker.BLUE) {
347                        if(lastB!=b) {
348                            regenerateImage();
349                        }
350                    }
351                } else {
352                    float[] hsb = new float[3];
353                    Color.RGBtoHSB(r, g, b, hsb);
354                    setHSB(hsb[0],hsb[1],hsb[2]);
355                    return;
356                }
357                regeneratePoint();
358                repaint();
359                fireChangeListeners();
360            }
361        }
362            
363        /** @return the HSB values of the selected color.
364         * Each value is between [0,1].
365         */
366        public float[] getHSB() {
367            return new float[] {hue, sat, bri};
368        }
369            
370        /** @return the RGB values of the selected color.
371         * Each value is between [0,255].
372         */
373        public int[] getRGB() {
374            return new int[] {red, green, blue};
375        }
376    
377        /** Sets the selected color of this panel.
378         * <P>If this panel is in RED, GREEN, or BLUE mode, then
379         * this method converts these values to RGB coordinates
380         * and calls <code>setRGB</code>.
381         * <P>This method may regenerate the graphic if necessary.
382         * 
383         * @param h the hue value of the selected color.
384         * @param s the saturation value of the selected color.
385         * @param b the brightness value of the selected color.
386         */
387        public void setHSB(float h,float s,float b) {
388            if(Float.isInfinite(h) || Float.isNaN(h))
389                throw new IllegalArgumentException("The hue value ("+h+") is not a valid number.");
390            //hue is cyclic, so it can be any value:
391            while(h<0) h++;
392            while(h>1) h--;
393                    
394            if(s<0 || s>1)
395                throw new IllegalArgumentException("The saturation value ("+s+") must be between [0,1]");
396            if(b<0 || b>1)
397                throw new IllegalArgumentException("The brightness value ("+b+") must be between [0,1]");
398                    
399            if(hue!=h || sat!=s || bri!=b) {
400                if(mode==ColorPicker.HUE || 
401                   mode==ColorPicker.BRI ||
402                   mode==ColorPicker.SAT) {
403                    float lastHue = hue;
404                    float lastBri = bri;
405                    float lastSat = sat;
406                    hue = h;
407                    sat = s;
408                    bri = b;
409                    if(mode==ColorPicker.HUE) {
410                        if(lastHue!=hue) {
411                            regenerateImage();
412                        }
413                    } else if(mode==ColorPicker.SAT) {
414                        if(lastSat!=sat) {
415                            regenerateImage();
416                        }
417                    } else if(mode==ColorPicker.BRI) {
418                        if(lastBri!=bri) {
419                            regenerateImage();
420                        }
421                    }
422                } else {
423    
424                    Color c = new Color(Color.HSBtoRGB(h, s, b));
425                    setRGB(c.getRed(), c.getGreen(), c.getBlue());
426                    return;
427                }
428                            
429    
430                Color c = new Color(Color.HSBtoRGB(hue, sat, bri));
431                red = c.getRed();
432                green = c.getGreen();
433                blue = c.getBlue();
434                            
435                regeneratePoint();
436                repaint();
437                fireChangeListeners();
438            }               
439        }
440            
441        /** Recalculates the (x,y) point used to indicate the selected color. */
442        private void regeneratePoint() {
443            int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom));
444            if(mode==ColorPicker.HUE || mode==ColorPicker.SAT || mode==ColorPicker.BRI) {
445                if(mode==ColorPicker.HUE) {
446                    point = new Point((int)(sat*size),(int)(bri*size));
447                } else if(mode==ColorPicker.SAT) {
448                    double theta = hue*2*Math.PI-Math.PI/2;
449                    if(theta<0) theta+=2*Math.PI;
450                                    
451                    double r = bri*size/2;
452                    point = new Point((int)(r*Math.cos(theta)+.5+size/2.0),(int)(r*Math.sin(theta)+.5+size/2.0));
453                } else if(mode==ColorPicker.BRI) {
454                    double theta = hue*2*Math.PI-Math.PI/2;
455                    if(theta<0) theta+=2*Math.PI;
456                    double r = sat*size/2;
457                    point = new Point((int)(r*Math.cos(theta)+.5+size/2.0),(int)(r*Math.sin(theta)+.5+size/2.0));
458                }
459            } else if(mode==ColorPicker.RED) {
460                point = new Point((int)(green*size/255f+.49f),
461                                  (int)(blue*size/255f+.49f) );
462            } else if(mode==ColorPicker.GREEN) {
463                point = new Point((int)(red*size/255f+.49f),
464                                  (int)(blue*size/255f+.49f) );
465            } else if(mode==ColorPicker.BLUE) {
466                point = new Point((int)(red*size/255f+.49f),
467                                  (int)(green*size/255f+.49f) );
468            }
469        }
470            
471        /** A row of pixel data we recycle every time we regenerate this image. */
472        private int[] row = new int[MAX_SIZE];
473        /** Regenerates the image. */
474        private synchronized void regenerateImage() {
475            int size = Math.min(MAX_SIZE, Math.min(getWidth()-imagePadding.left-imagePadding.right,getHeight()-imagePadding.top-imagePadding.bottom));
476                    
477            if(mode==ColorPicker.BRI || mode==ColorPicker.SAT) {
478                float bri2 = this.bri;
479                float sat2 = this.sat;
480                float radius = ((float)size)/2f;
481                float hue2;
482                float k = 1.2f; //the number of pixels to antialias
483                for(int y = 0; y<size; y++) {
484                    float y2 = (y-size/2f);
485                    for(int x = 0; x<size; x++) {
486                        float x2 = (x-size/2f);
487                        double theta = Math.atan2(y2,x2)-3*Math.PI/2.0;
488                        if(theta<0) theta+=2*Math.PI;
489                                            
490                        double r = Math.sqrt(x2*x2+y2*y2);
491                        if(r<=radius) {
492                            if(mode==ColorPicker.BRI) {
493                                hue2 = (float)(theta/(2*Math.PI));
494                                sat2 = (float)(r/radius);
495                            } else { //SAT
496                                hue2 = (float)(theta/(2*Math.PI));
497                                bri2 = (float)(r/radius);
498                            }
499                            row[x] = Color.HSBtoRGB(hue2, sat2, bri2);
500                            if(r>radius-k) {
501                                int alpha = (int)(255-255*(r-radius+k)/k);
502                                if(alpha<0) alpha = 0;
503                                if(alpha>255) alpha = 255;
504                                row[x] = row[x] & 0xffffff+(alpha << 24);
505                            }
506                        } else {
507                            row[x] = 0x00000000;
508                        }
509                    }
510                    image.getRaster().setDataElements(0, y, size, 1, row);
511                }
512            } else if(mode==ColorPicker.HUE) {
513                float hue2 = this.hue;
514                for(int y = 0; y<size; y++) {
515                    float y2 = ((float)y)/((float)size);
516                    for(int x = 0; x<size; x++) {
517                        float x2 = ((float)x)/((float)size);
518                        row[x] = Color.HSBtoRGB(hue2, x2, y2);
519                    }
520                    image.getRaster().setDataElements(0, y, image.getWidth(), 1, row);
521                }
522            } else { //mode is RED, GREEN, or BLUE
523                int red2 = red;
524                int green2 = green;
525                int blue2 = blue;
526                for(int y = 0; y<size; y++) {
527                    float y2 = ((float)y)/((float)size);
528                    for(int x = 0; x<size; x++) {
529                        float x2 = ((float)x)/((float)size);
530                        if(mode==ColorPicker.RED) {
531                            green2 = (int)(x2*255+.49);
532                            blue2 = (int)(y2*255+.49);
533                        } else if(mode==ColorPicker.GREEN) {
534                            red2 = (int)(x2*255+.49);
535                            blue2 = (int)(y2*255+.49);
536                        } else {
537                            red2 = (int)(x2*255+.49);
538                            green2 = (int)(y2*255+.49);
539                        }
540                        row[x] = 0xFF000000 + (red2 << 16) + (green2 << 8) + blue2;
541                    }
542                    image.getRaster().setDataElements(0, y, size, 1, row);
543                }
544            }
545            repaint();
546        }
547    }