001    /*
002     * $Id: JXMultiThumbSlider.java 3310 2009-03-26 10:25:24Z kleopatra $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     * 
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     * 
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    
022    package org.jdesktop.swingx;
023    
024    import org.jdesktop.swingx.multislider.DefaultMultiThumbModel;
025    import org.jdesktop.swingx.multislider.MultiThumbModel;
026    import org.jdesktop.swingx.multislider.ThumbDataEvent;
027    import org.jdesktop.swingx.multislider.ThumbDataListener;
028    import org.jdesktop.swingx.multislider.ThumbListener;
029    import org.jdesktop.swingx.multislider.ThumbRenderer;
030    import org.jdesktop.swingx.multislider.TrackRenderer;
031    import org.jdesktop.swingx.plaf.MultiThumbSliderAddon;
032    import org.jdesktop.swingx.plaf.LookAndFeelAddons;
033    import org.jdesktop.swingx.plaf.MultiThumbSliderUI;
034    
035    import javax.swing.*;
036    import javax.swing.event.MouseInputAdapter;
037    import java.awt.*;
038    import java.awt.event.MouseEvent;
039    import java.util.ArrayList;
040    import java.util.List;
041    
042    /**
043     * <p>A slider which can have multiple control points or <i>Thumbs</i></p>
044     * <p>The thumbs each represent a value between the minimum and maximum values
045     * of the slider.  Thumbs can pass each other when being dragged.  Thumbs have
046     * no default visual representation. To customize the look of the thumbs and the
047     * track behind the thumbs you must provide a ThumbRenderer and a TrackRenderer 
048     * implementation. To listen for changes to the thumbs you must provide an 
049     * implemention of ThumbDataListener.
050     * 
051     * TODOs:
052     * move public inner classes (interfaces, etc) to subpackage
053     * add min/maxvalue convenience methods to jxmultithumbslider
054     * add plafs for windows, mac, and basic (if necessary)
055     * make way to properly control the height.
056     * hide the inner thumb component
057     *
058     * @author joshy
059     */
060    public class JXMultiThumbSlider<E> extends JComponent {
061        static {
062            LookAndFeelAddons.contribute(new MultiThumbSliderAddon());
063        }
064    
065        public static final String uiClassID = "MultiThumbSliderUI";
066        
067        /** Creates a new instance of JMultiThumbSlider */
068        public JXMultiThumbSlider() {
069            thumbs = new ArrayList<ThumbComp>();
070            setLayout(null);
071            
072            tdl = new ThumbHandler();
073            
074            setModel(new DefaultMultiThumbModel<E>());
075            MultiThumbMouseListener mia = new MultiThumbMouseListener();
076            addMouseListener(mia);
077            addMouseMotionListener(mia);
078            
079            Dimension dim = new Dimension(60,16);
080            setPreferredSize(dim);
081            setSize(dim);
082            setMinimumSize(new Dimension(30,16));
083            updateUI();
084        }
085        
086        public MultiThumbSliderUI getUI() {
087            return (MultiThumbSliderUI)ui;
088        }
089        
090        public void setUI(MultiThumbSliderUI ui) {
091            super.setUI(ui);
092        }
093        
094        @Override
095        public void updateUI() {
096            setUI((MultiThumbSliderUI)LookAndFeelAddons.getUI(this, MultiThumbSliderUI.class));
097            invalidate();
098        }
099        
100        /**
101         * {@inheritDoc}
102         */
103        @Override
104        public String getUIClassID() {
105            return uiClassID;
106        }
107    
108        private ThumbDataListener tdl;
109        
110        private List<ThumbComp> thumbs;
111        
112        private ThumbRenderer thumbRenderer;
113        
114        private TrackRenderer trackRenderer;
115        
116        private MultiThumbModel<E> model;
117    
118        private List<ThumbListener> listeners = new ArrayList<ThumbListener>();
119            
120        private ThumbComp selected;
121        
122        @Override
123        protected void paintComponent(Graphics g) {
124            if(isVisible()) {
125                if(trackRenderer != null) {
126                    JComponent comp = trackRenderer.getRendererComponent(this);
127                    add(comp);
128                    comp.paint(g);
129                    remove(comp);
130                } else {
131                    paintRange((Graphics2D)g);
132                }
133            }
134        }
135    
136        private void paintRange(Graphics2D g) {
137            g.setColor(Color.blue);
138            g.fillRect(0,0,getWidth(),getHeight());
139        }    
140        
141        private float getThumbValue(int thumbIndex) {
142            return getModel().getThumbAt(thumbIndex).getPosition();
143        }
144        
145        private float getThumbValue(ThumbComp thumb) {
146            return getThumbValue(thumbs.indexOf(thumb));
147        }
148        
149        private int getThumbIndex(ThumbComp thumb) {
150            return thumbs.indexOf(thumb);
151        }
152        
153        private void clipThumbPosition(ThumbComp thumb) {
154            if(getThumbValue(thumb) < getModel().getMinimumValue()) {
155                getModel().getThumbAt(getThumbIndex(thumb)).setPosition(
156                    getModel().getMinimumValue());
157            }
158            if(getThumbValue(thumb) > getModel().getMaximumValue()) {
159                getModel().getThumbAt(getThumbIndex(thumb)).setPosition(
160                getModel().getMaximumValue());
161            }
162        }
163            
164        public ThumbRenderer getThumbRenderer() {
165            return thumbRenderer;
166        }
167    
168        public void setThumbRenderer(ThumbRenderer thumbRenderer) {
169            this.thumbRenderer = thumbRenderer;
170        }
171    
172        public TrackRenderer getTrackRenderer() {
173            return trackRenderer;
174        }
175    
176        public void setTrackRenderer(TrackRenderer trackRenderer) {
177            this.trackRenderer = trackRenderer;
178        }
179        
180        public float getMinimumValue() {
181            return getModel().getMinimumValue();
182        }
183        
184        public void setMinimumValue(float minimumValue) {
185            getModel().setMinimumValue(minimumValue);
186        }
187        
188        public float getMaximumValue() {
189            return getModel().getMaximumValue();
190        }
191        
192        public void setMaximumValue(float maximumValue) {
193            getModel().setMaximumValue(maximumValue);
194        }
195    
196        private void setThumbPositionByX(ThumbComp selected) {    
197            float range = getModel().getMaximumValue()-getModel().getMinimumValue();
198            int x = selected.getX();
199            // adjust to the center of the thumb
200            x += selected.getWidth()/2;
201            // adjust for the leading space on the slider
202            x -= selected.getWidth()/2;
203            
204            int w = getWidth();
205            // adjust for the leading and trailing space on the slider
206            w -= selected.getWidth();
207            float delta = ((float)x)/((float)w);
208            int thumb_index = getThumbIndex(selected);
209            float value = delta*range;
210            getModel().getThumbAt(thumb_index).setPosition(value);
211            //getModel().setPositionAt(thumb_index,value);
212            clipThumbPosition(selected);
213        }
214        
215        private void setThumbXByPosition(ThumbComp thumb, float pos) {
216            float lp = getWidth()-thumb.getWidth();
217            float lu = getModel().getMaximumValue()-getModel().getMinimumValue();
218            float tp = (pos*lp)/lu;
219            thumb.setLocation((int)tp-thumb.getWidth()/2 + thumb.getWidth()/2, thumb.getY());
220        }
221        
222        private void recalc() {
223            for(ThumbComp th : thumbs) {
224                setThumbXByPosition(th,getModel().getThumbAt(getThumbIndex(th)).getPosition());
225                //getPositionAt(getThumbIndex(th)));
226            }
227        }
228        
229        @Override
230        public void setBounds(int x, int y, int w, int h) {
231            super.setBounds(x,y,w,h);
232            recalc();
233        }
234    
235        public JComponent getSelectedThumb() {
236            return selected;
237        }
238        
239        public int getSelectedIndex() {
240            return getThumbIndex(selected);
241        }
242            
243        public MultiThumbModel<E> getModel() {
244            return model;
245        }
246    
247        public void setModel(MultiThumbModel<E> model) {
248            if(this.model != null) {
249                this.model.removeThumbDataListener(tdl);
250            }
251            this.model = model;
252            this.model.addThumbDataListener(tdl);        
253        }
254        
255        public void addMultiThumbListener(ThumbListener listener) {
256            listeners.add(listener);
257        }
258    
259        private class MultiThumbMouseListener extends MouseInputAdapter {
260            @Override
261            public void mousePressed(MouseEvent evt) {
262                ThumbComp handle = findHandle(evt);
263                if(handle != null) {
264                    selected = handle;
265                    selected.setSelected(true);
266                    int thumb_index = getThumbIndex(selected);
267                    for(ThumbListener tl : listeners) {
268                        tl.thumbSelected(thumb_index);
269                    }
270                    repaint();
271                } else {
272                    selected = null;
273                    for(ThumbListener tl : listeners) {
274                        tl.thumbSelected(-1);
275                    }
276                    repaint();
277                }
278                for(ThumbListener tl : listeners) {
279                    tl.mousePressed(evt);
280                }
281            }
282    
283            @Override
284            public void mouseReleased(MouseEvent evt) {
285                if(selected != null) {
286                    selected.setSelected(false);
287                }
288            }
289                
290            @Override
291            public void mouseDragged(MouseEvent evt) {
292                if(selected != null) {
293                    int nx = (int)evt.getPoint().getX()- selected.getWidth()/2;
294                    if(nx < 0) {
295                        nx = 0;
296                    }
297                    if(nx > getWidth()-selected.getWidth()) {
298                        nx = getWidth()-selected.getWidth();
299                    }
300                    selected.setLocation(nx,(int)selected.getLocation().getY());
301                    setThumbPositionByX(selected);
302                    int thumb_index = getThumbIndex(selected);
303                    //log.fine("still dragging: " + thumb_index);
304                    for(ThumbListener mtl : listeners) {
305                        mtl.thumbMoved(thumb_index,getModel().getThumbAt(thumb_index).getPosition());
306                        //getPositionAt(thumb_index));
307                    }
308                    repaint();
309                }
310            }
311    
312            
313            private ThumbComp findHandle(MouseEvent evt) {
314                for(ThumbComp hand : thumbs) {
315                    Point p2 = new Point();
316                    p2.setLocation(evt.getPoint().getX() - hand.getX(),
317                        evt.getPoint().getY() - hand.getY());
318                    if(hand.contains(p2)) {
319                        return hand;
320                    }
321                }
322                return null;
323            }
324        }
325        
326        private static class ThumbComp extends JComponent {
327            
328            private JXMultiThumbSlider<?> slider;
329            
330            public ThumbComp(JXMultiThumbSlider<?> slider) {
331                this.slider = slider;
332                Dimension dim = new Dimension(10,10);//slider.getHeight());
333                /*if(slider.getThumbRenderer() != null) {
334                    JComponent comp = getRenderer();
335                    dim = comp.getPreferredSize();
336                }*/
337                setSize(dim);
338                setMinimumSize(dim);
339                setPreferredSize(dim);
340                setMaximumSize(dim);
341                setBackground(Color.white);
342            }
343            
344            @Override
345            public void paintComponent(Graphics g) {
346                if(slider.getThumbRenderer() != null) {
347                    JComponent comp = getRenderer();
348                    comp.setSize(this.getSize());
349                    comp.paint(g);
350                } else {
351                    g.setColor(getBackground());
352                    g.fillRect(0,0,getWidth(),getHeight());
353                    if(isSelected()) {
354                        g.setColor(Color.black);
355                        g.drawRect(0,0,getWidth()-1,getHeight()-1);
356                    }
357                }
358            }
359    
360            private JComponent getRenderer() {
361                return slider.getThumbRenderer().
362                        getThumbRendererComponent(slider,slider.getThumbIndex(this),isSelected());
363            }
364            
365            private boolean selected;
366    
367            public boolean isSelected() {
368                return selected;
369            }
370    
371            public void setSelected(boolean selected) {
372                this.selected = selected;
373            }
374        }
375    
376        private class ThumbHandler implements ThumbDataListener {
377    
378            public void positionChanged(ThumbDataEvent e) {
379                ThumbComp comp = thumbs.get(e.getIndex());
380                clipThumbPosition(comp);
381                setThumbXByPosition(comp, e.getThumb().getPosition());
382                repaint();
383            }
384    
385            public void thumbAdded(ThumbDataEvent evt) {
386                ThumbComp thumb = new ThumbComp(JXMultiThumbSlider.this);
387                thumb.setLocation(0, 0);
388                add(thumb);
389                thumbs.add(evt.getIndex(), thumb);
390                clipThumbPosition(thumb);
391                setThumbXByPosition(thumb, evt.getThumb().getPosition());
392                repaint();
393            }
394    
395            public void thumbRemoved(ThumbDataEvent evt) {
396                ThumbComp thumb = thumbs.get(evt.getIndex());
397                remove(thumb);
398                thumbs.remove(thumb);
399                repaint();
400            }
401    
402            public void valueChanged(ThumbDataEvent e) {
403                repaint();
404            }
405        }
406    
407    
408    }