swingx-master/swingx-core/src/main/java/org/jdesktop/swingx/JXMultiThumbSlider.java
/*
 * $Id$
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.jdesktop.swingx;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JComponent;
import javax.swing.event.MouseInputAdapter;

import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.multislider.DefaultMultiThumbModel;
import org.jdesktop.swingx.multislider.MultiThumbModel;
import org.jdesktop.swingx.multislider.ThumbDataEvent;
import org.jdesktop.swingx.multislider.ThumbDataListener;
import org.jdesktop.swingx.multislider.ThumbListener;
import org.jdesktop.swingx.multislider.ThumbRenderer;
import org.jdesktop.swingx.multislider.TrackRenderer;
import org.jdesktop.swingx.plaf.LookAndFeelAddons;
import org.jdesktop.swingx.plaf.MultiThumbSliderAddon;
import org.jdesktop.swingx.plaf.MultiThumbSliderUI;

/**
 * <p>A slider which can have multiple control points or <i>Thumbs</i></p>
 * <p>The thumbs each represent a value between the minimum and maximum values
 * of the slider.  Thumbs can pass each other when being dragged.  Thumbs have
 * no default visual representation. To customize the look of the thumbs and the
 * track behind the thumbs you must provide a ThumbRenderer and a TrackRenderer 
 * implementation. To listen for changes to the thumbs you must provide an 
 * implementation of ThumbDataListener.
 * 
 * TODOs:
 * add min/maxvalue convenience methods to jxmultithumbslider
 * add plafs for windows, mac, and basic (if necessary)
 * make way to properly control the height.
 * hide the inner thumb component
 *
 * @author joshy
 */
@JavaBean
public class JXMultiThumbSlider<E> extends JComponent {
    public static final String uiClassID = "MultiThumbSliderUI";
    
    private ThumbDataListener tdl;
    
    private List<ThumbComp> thumbs;
    
    private ThumbRenderer thumbRenderer;
    
    private TrackRenderer trackRenderer;
    
    private MultiThumbModel<E> model;
    
    private List<ThumbListener> listeners = new ArrayList<ThumbListener>();
    
    private ThumbComp selected;
    
    static {
        LookAndFeelAddons.contribute(new MultiThumbSliderAddon());
    }
    
    /** Creates a new instance of JMultiThumbSlider */
    public JXMultiThumbSlider() {
        thumbs = new ArrayList<ThumbComp>();
        setLayout(null);
        
        tdl = new ThumbHandler();
        
        setModel(new DefaultMultiThumbModel<E>());
        MultiThumbMouseListener mia = new MultiThumbMouseListener();
        addMouseListener(mia);
        addMouseMotionListener(mia);
        
        Dimension dim = new Dimension(60,16);
        setPreferredSize(dim);
        setSize(dim);
        setMinimumSize(new Dimension(30,16));
        updateUI();
    }
    
    /**
     * {@inheritDoc}
     */
    @Override
    public String getUIClassID() {
        return uiClassID;
    }
    
    public MultiThumbSliderUI getUI() {
        return (MultiThumbSliderUI)ui;
    }
    
    public void setUI(MultiThumbSliderUI ui) {
        super.setUI(ui);
    }
    
    @Override
    public void updateUI() {
        setUI((MultiThumbSliderUI)LookAndFeelAddons.getUI(this, MultiThumbSliderUI.class));
        invalidate();
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        if(isVisible()) {
            if(trackRenderer != null) {
                JComponent comp = trackRenderer.getRendererComponent(this);
                add(comp);
                comp.paint(g);
                remove(comp);
            } else {
                paintRange((Graphics2D)g);
            }
        }
    }

    private void paintRange(Graphics2D g) {
        g.setColor(Color.blue);
        g.fillRect(0,0,getWidth(),getHeight());
    }    
    
    private float getThumbValue(int thumbIndex) {
        return getModel().getThumbAt(thumbIndex).getPosition();
    }
    
    private float getThumbValue(ThumbComp thumb) {
        return getThumbValue(thumbs.indexOf(thumb));
    }
    
    private int getThumbIndex(ThumbComp thumb) {
        return thumbs.indexOf(thumb);
    }
    
    private void clipThumbPosition(ThumbComp thumb) {
        if(getThumbValue(thumb) < getModel().getMinimumValue()) {
            getModel().getThumbAt(getThumbIndex(thumb)).setPosition(
                getModel().getMinimumValue());
        }
        if(getThumbValue(thumb) > getModel().getMaximumValue()) {
            getModel().getThumbAt(getThumbIndex(thumb)).setPosition(
            getModel().getMaximumValue());
        }
    }
        
    public ThumbRenderer getThumbRenderer() {
        return thumbRenderer;
    }

    public void setThumbRenderer(ThumbRenderer thumbRenderer) {
        this.thumbRenderer = thumbRenderer;
    }

    public TrackRenderer getTrackRenderer() {
        return trackRenderer;
    }

    public void setTrackRenderer(TrackRenderer trackRenderer) {
        this.trackRenderer = trackRenderer;
    }
    
    public float getMinimumValue() {
        return getModel().getMinimumValue();
    }
    
    public void setMinimumValue(float minimumValue) {
        getModel().setMinimumValue(minimumValue);
    }
    
    public float getMaximumValue() {
        return getModel().getMaximumValue();
    }
    
    public void setMaximumValue(float maximumValue) {
        getModel().setMaximumValue(maximumValue);
    }

    private void setThumbPositionByX(ThumbComp selected) {    
        float range = getModel().getMaximumValue()-getModel().getMinimumValue();
        int x = selected.getX();
        // adjust to the center of the thumb
        x += selected.getWidth()/2;
        // adjust for the leading space on the slider
        x -= selected.getWidth()/2;
        
        int w = getWidth();
        // adjust for the leading and trailing space on the slider
        w -= selected.getWidth();
        float delta = ((float)x)/((float)w);
        int thumb_index = getThumbIndex(selected);
        float value = delta*range;
        getModel().getThumbAt(thumb_index).setPosition(value);
        //getModel().setPositionAt(thumb_index,value);
        clipThumbPosition(selected);
    }
    
    private void setThumbXByPosition(ThumbComp thumb, float pos) {
        float lp = getWidth()-thumb.getWidth();
        float lu = getModel().getMaximumValue()-getModel().getMinimumValue();
        float tp = (pos*lp)/lu;
        thumb.setLocation((int)tp-thumb.getWidth()/2 + thumb.getWidth()/2, thumb.getY());
    }
    
    private void recalc() {
        for(ThumbComp th : thumbs) {
            setThumbXByPosition(th,getModel().getThumbAt(getThumbIndex(th)).getPosition());
            //getPositionAt(getThumbIndex(th)));
        }
    }
    
    @Override
    public void setBounds(int x, int y, int w, int h) {
        super.setBounds(x,y,w,h);
        recalc();
    }

    public JComponent getSelectedThumb() {
        return selected;
    }
    
    public int getSelectedIndex() {
        return getThumbIndex(selected);
    }
        
    public MultiThumbModel<E> getModel() {
        return model;
    }

    public void setModel(MultiThumbModel<E> model) {
        if(this.model != null) {
            this.model.removeThumbDataListener(tdl);
        }
        this.model = model;
        this.model.addThumbDataListener(tdl);        
    }
    
    public void addMultiThumbListener(ThumbListener listener) {
        listeners.add(listener);
    }

    private class MultiThumbMouseListener extends MouseInputAdapter {
        @Override
        public void mousePressed(MouseEvent evt) {
            ThumbComp handle = findHandle(evt);
            if(handle != null) {
                selected = handle;
                selected.setSelected(true);
                int thumb_index = getThumbIndex(selected);
                for(ThumbListener tl : listeners) {
                    tl.thumbSelected(thumb_index);
                }
                repaint();
            } else {
                selected = null;
                for(ThumbListener tl : listeners) {
                    tl.thumbSelected(-1);
                }
                repaint();
            }
            for(ThumbListener tl : listeners) {
                tl.mousePressed(evt);
            }
        }

        @Override
        public void mouseReleased(MouseEvent evt) {
            if(selected != null) {
                selected.setSelected(false);
            }
        }
            
        @Override
        public void mouseDragged(MouseEvent evt) {
            if(selected != null) {
                int nx = (int)evt.getPoint().getX()- selected.getWidth()/2;
                if(nx < 0) {
                    nx = 0;
                }
                if(nx > getWidth()-selected.getWidth()) {
                    nx = getWidth()-selected.getWidth();
                }
                selected.setLocation(nx,(int)selected.getLocation().getY());
                setThumbPositionByX(selected);
                int thumb_index = getThumbIndex(selected);
                //log.fine("still dragging: " + thumb_index);
                for(ThumbListener mtl : listeners) {
                    mtl.thumbMoved(thumb_index,getModel().getThumbAt(thumb_index).getPosition());
                    //getPositionAt(thumb_index));
                }
                repaint();
            }
        }

        
        private ThumbComp findHandle(MouseEvent evt) {
            for(ThumbComp hand : thumbs) {
                Point p2 = new Point();
                p2.setLocation(evt.getPoint().getX() - hand.getX(),
                    evt.getPoint().getY() - hand.getY());
                if(hand.contains(p2)) {
                    return hand;
                }
            }
            return null;
        }
    }
    
    private static class ThumbComp extends JComponent {
        
        private JXMultiThumbSlider<?> slider;
        
        public ThumbComp(JXMultiThumbSlider<?> slider) {
            this.slider = slider;
            Dimension dim = new Dimension(10,10);//slider.getHeight());
            /*if(slider.getThumbRenderer() != null) {
                JComponent comp = getRenderer();
                dim = comp.getPreferredSize();
            }*/
            setSize(dim);
            setMinimumSize(dim);
            setPreferredSize(dim);
            setMaximumSize(dim);
            setBackground(Color.white);
        }
        
        @Override
        public void paintComponent(Graphics g) {
            if(slider.getThumbRenderer() != null) {
                JComponent comp = getRenderer();
                comp.setSize(this.getSize());
                comp.paint(g);
            } else {
                g.setColor(getBackground());
                g.fillRect(0,0,getWidth(),getHeight());
                if(isSelected()) {
                    g.setColor(Color.black);
                    g.drawRect(0,0,getWidth()-1,getHeight()-1);
                }
            }
        }

        private JComponent getRenderer() {
            return slider.getThumbRenderer().
                    getThumbRendererComponent(slider,slider.getThumbIndex(this),isSelected());
        }
        
        private boolean selected;

        public boolean isSelected() {
            return selected;
        }

        public void setSelected(boolean selected) {
            this.selected = selected;
        }
    }

    private class ThumbHandler implements ThumbDataListener {
        @Override
        public void positionChanged(ThumbDataEvent e) {
            ThumbComp comp = thumbs.get(e.getIndex());
            clipThumbPosition(comp);
            setThumbXByPosition(comp, e.getThumb().getPosition());
            repaint();
        }

        @Override
        public void thumbAdded(ThumbDataEvent evt) {
            ThumbComp thumb = new ThumbComp(JXMultiThumbSlider.this);
            thumb.setLocation(0, 0);
            add(thumb);
            thumbs.add(evt.getIndex(), thumb);
            clipThumbPosition(thumb);
            setThumbXByPosition(thumb, evt.getThumb().getPosition());
            repaint();
        }

        @Override
        public void thumbRemoved(ThumbDataEvent evt) {
            ThumbComp thumb = thumbs.get(evt.getIndex());
            remove(thumb);
            thumbs.remove(thumb);
            repaint();
        }

        @Override
        public void valueChanged(ThumbDataEvent e) {
            repaint();
        }
    }
}
