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