MultiThumbSlider.java

/*
 * @(#)MultiThumbSlider.java
 *
 * $Date: 2015-01-04 21:15:07 -0500 (Sun, 04 Jan 2015) $
 *
 * Copyright (c) 2011 by Jeremy Wood.
 * All rights reserved.
 *
 * The copyright of this software is owned by Jeremy Wood. 
 * You may not use, copy or modify this software, except in  
 * accordance with the license agreement you entered into with  
 * Jeremy Wood. For details see accompanying license terms.
 * 
 * This software is probably, but not necessarily, discussed here:
 * https://javagraphics.java.net/
 * 
 * That site should also contain the most recent official version
 * of this software.  (See the SVN repository for more details.)
 */
package org.opentrafficsim.gui.multislider;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Vector;

import javax.swing.JComponent;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.plaf.ComponentUI;

/**
 * This JComponent resembles a <code>JSlider</code>, except there are at least two thumbs. A <code>JSlider</code> is
 * designed to modify one number within a certain range of values. By contrast a <code>MultiThumbSlider</code> actually
 * modifies a <i>table</i> of data. Each thumb in a <code>MultiThumbSlider</code> should be thought of as a key, and it
 * maps to an abstract value. In the case of the <code>GradientSlider</code>: each value is a
 * <code>java.awt.Color</code>. Other subclasses could come along that map to other abstract objects. (For example, a
 * <code>VolumeSlider</code> might map each thumb to a specific volume level. This type of widget would let the user
 * control fading in/out of an audio track.)
 * <P>
 * The slider graphically represents the domain from zero to one, so each thumb is always positioned within that domain.
 * If the user drags a thumb outside this domain: that thumb disappears.
 * <P>
 * There is always a selected thumb in each slider when this slider has the keyboard focus. The user can press the tab
 * key (or shift-tab) to transfer focus to different thumbs. Also the arrow keys can be used to control the selected
 * thumb.
 * <P>
 * The user can click and drag any thumb to a new location. If a thumb is dragged so it is less than zero or greater
 * than one: then that thumb is removed. If the user clicks between two existing thumbs: a new thumb is created if
 * <code>autoAdd</code> is set to <code>true</code>. (If <code>autoAdd</code> is set to false: nothing happens.)
 * <P>
 * There are unimplemented methods in this class: <code>doDoubleClick()</code> and <code>doPopup()</code>. The UI will
 * invoke these methods as needed; this gives the user a chance to edit the values represented at a particular point.
 * <P>
 * Also using the keyboard:
 * <ul>
 * <LI>In a horizontal slider, the user can press modifier+left or modifer+right to insert a new thumb to the left/right
 * of the currently selected thumb. (Where "modifier" refers to
 * <code>Toolkit.getDefaultTookit().getMenuShortcutKeyMask()</code>. On Mac this is META, and on Windows this is
 * CONTROL.) Likewise on a vertical slider the up/down arrow keys can be used to add thumbs.</li>
 * <LI>The delete/backspace key can be used to remove thumbs.</li>
 * <LI>In a horizontal slider, the down arrow key can be used to invoke <code>doPopup()</code>. This should invoke a
 * <code>JPopupMenu</code> that is keyboard accessible, so the user should be able to navigate this component without a
 * mouse. Likewise on a vertical slider the right arrow key should do the same.</li>
 * <LI>The space bar or return key invokes <code>doDoubleClick()</code>.</LI>
 * </ul>
 * <P>
 * Because thumbs can be abstractly inserted, the values each thumb represents should be tween-able. That is, if there
 * is a value at zero and a value at one, the call <code>getValue(.5f)</code> must return a value that is halfway
 * between those values.
 * <P>
 * Also note that although the thumbs must always be between zero and one: the minimum and maximum thumbs do not have to
 * be zero and one. The user can adjust them so the minimum thumb is, say, .2f, and the maximum thumb is .5f.
 * @param <T> the type of data each float maps to. For example: in the GradientSlider this value is a Color. Sometimes
 *            this property may be unnecessary. If this slider is only meant to store the relative position of thumbs,
 *            then you may set this to a trivial stub-like object like a String or Character.
 */
public class MultiThumbSlider<T> extends JComponent
{
    @SuppressWarnings("javadoc")
    private static final long serialVersionUID = 1L;

    /** A set of possible behaviors when one thumb collides with another. */
    public static enum Collision {
        /** When the user drags one thumb and it collides with another, nudge the other thumb as far as possible. */
        NUDGE_OTHER,
        /** When the user drags one thumb and it collides with another, skip over the other thumb. */
        JUMP_OVER_OTHER,
        /**
         * When the user drags one thumb and it collides with another, bump into the other thumb and don't allow any
         * more movement.
         */
        STOP_AGAINST
    };

    /** The property that controls whether clicking between thumbs automatically adds a thumb. */
    public static final String AUTOADD_PROPERTY = MultiThumbSlider.class.getName() + ".auto-add";

    /** The property that controls whether the user can remove a thumb (either by dragging or with the delete key). */
    public static final String REMOVAL_ALLOWED = MultiThumbSlider.class.getName() + ".removal-allowed";

    /** The property that is changed when <code>setSelectedThumb()</code> is called. */
    public static final String SELECTED_THUMB_PROPERTY = MultiThumbSlider.class.getName() + ".selected-thumb";

    /** The property that is changed when <code>setCollisionPolicy(c)</code> is called. */
    public static final String COLLISION_PROPERTY = MultiThumbSlider.class.getName() + ".collision";

    /** The property that is changed when <code>setInverted(b)</code> is called. */
    public static final String INVERTED_PROPERTY = MultiThumbSlider.class.getName() + ".inverted";

    /** The property that is changed when <code>setInverted(b)</code> is called. */
    public static final String THUMB_OVERLAP_PROPERTY = MultiThumbSlider.class.getName() + ".thumb-overlap";

    /** The property that is changed when <code>setMinimumThumbnailCount(b)</code> is called. */
    public static final String THUMB_MINIMUM_PROPERTY = MultiThumbSlider.class.getName() + ".thumb-minimum";

    /** The property that is changed when <code>setOrientation(i)</code> is called. */
    public static final String ORIENTATION_PROPERTY = MultiThumbSlider.class.getName() + ".orientation";

    /**
     * The property that is changed when <code>setValues()</code> is called. Note this is used when either the positions
     * or the values are updated, because they need to be updated at the same time to maintain an exact one-to-one
     * ratio.
     */
    public static final String VALUES_PROPERTY = MultiThumbSlider.class.getName() + ".values";

    /** The property that is changed when <code>setValueIsAdjusting(b)</code> is called. */
    public static final String ADJUST_PROPERTY = MultiThumbSlider.class.getName() + ".adjusting";

    /** The property that is changed when <code>setPaintTicks(b)</code> is called. */
    public static final String PAINT_TICKS_PROPERTY = MultiThumbSlider.class.getName() + ".paint ticks";

    /** The positions of the thumbs */
    protected float[] thumbPositions = new float[0];

    /** The values for each thumb */
    protected T[] values;

    /** ChangeListeners registered with this slider. */
    List<ChangeListener> changeListeners;

    /**
     * The orientation constant for a horizontal slider.
     */
    public static final int HORIZONTAL = SwingConstants.HORIZONTAL;

    /**
     * The orientation constant for a vertical slider.
     */
    public static final int VERTICAL = SwingConstants.VERTICAL;

    /**
     * Creates a new horizontal MultiThumbSlider.
     * @param thumbPositions an array of values from zero to one.
     * @param values an array of values, each value corresponds to a value in <code>thumbPositions</code>.
     */
    public MultiThumbSlider(float[] thumbPositions, T[] values)
    {
        this(HORIZONTAL, thumbPositions, values);
    }

    /**
     * Creates a new MultiThumbSlider.
     * @param orientation must be <code>HORIZONTAL</code> or <code>VERTICAL</code>
     * @param thumbPositions an array of values from zero to one.
     * @param values an array of values, each value corresponds to a value in <code>thumbPositions</code>.
     */
    public MultiThumbSlider(int orientation, float[] thumbPositions, T[] values)
    {
        setOrientation(orientation);
        setValues(thumbPositions, values);
        setFocusable(true);
        updateUI();
    }

    @SuppressWarnings({"unchecked", "javadoc"})
    public MultiThumbSliderUI<T> getUI()
    {
        return (MultiThumbSliderUI<T>) this.ui;
    }

    @Override
    public void updateUI()
    {
        String name = UIManager.getString("MultiThumbSliderUI");
        if (name == null)
        {
            if (JVM.isMac)
            {
                name = "org.opentrafficsim.gui.multislider.AquaMultiThumbSliderUI";
            }
            else if (JVM.isWindows)
            {
                name = "org.opentrafficsim.gui.multislider.VistaMultiThumbSliderUI";
            }
            else
            {
                name = "org.opentrafficsim.gui.multislider.DefaultMultiThumbSliderUI";
            }
        }
        try
        {
            Class<?> c = Class.forName(name);
            Constructor<?>[] constructors = c.getConstructors();
            for (int a = 0; a < constructors.length; a++)
            {
                Class<?>[] types = constructors[a].getParameterTypes();
                if (types.length == 1 && types[0].equals(MultiThumbSlider.class))
                {
                    MultiThumbSliderUI<T> ui = (MultiThumbSliderUI<T>) constructors[a].newInstance(new Object[]{this});
                    setUI(ui);
                    return;
                }
            }
        }
        catch (ClassNotFoundException e)
        {
            throw new RuntimeException("The class \"" + name + "\" could not be found.");
        }
        catch (Throwable t)
        {
            RuntimeException e = new RuntimeException("The class \"" + name + "\" could not be constructed.");
            e.initCause(t);
            throw e;
        }
    }

    public void setUI(MultiThumbSliderUI<T> ui)
    {
        super.setUI((ComponentUI) ui);
    }

    /**
     * This listener will be notified when the colors/positions of this slider are modified.
     * <P>
     * Note you can also listen to these events by listening to the <code>VALUES_PROPERTY</code>, but this mechanism is
     * provided as a convenience to resemble the <code>JSlider</code> model.
     * @param l the <code>ChangeListener</code> to add.
     */
    public void addChangeListener(ChangeListener l)
    {
        if (this.changeListeners == null)
            this.changeListeners = new Vector<ChangeListener>();
        if (this.changeListeners.contains(l))
            return;
        this.changeListeners.add(l);
    }

    /**
     * Removes a <code>ChangeListener</code> from this slider.
     */
    public void removeChangeListener(ChangeListener l)
    {
        if (this.changeListeners == null)
            return;
        this.changeListeners.remove(l);
    }

    /** Invokes all the ChangeListeners. */
    protected void fireChangeListeners()
    {
        if (this.changeListeners == null)
            return;
        for (int a = 0; a < this.changeListeners.size(); a++)
        {
            try
            {
                (this.changeListeners.get(a)).stateChanged(new ChangeEvent(this));
            }
            catch (Throwable t)
            {
                t.printStackTrace();
            }
        }
    }

    /**
     * Depending on which thumb is selected, this may shift the focus to the next available thumb, or it may shift the
     * focus to the next focusable <code>JComponent</code>.
     */
    @Override
    public void transferFocus()
    {
        transferFocus(true);
    }

    /**
     * Shifts the focus forward or backward. This may decide to select another thumb, or it may call
     * <code>super.transferFocus()</code> to let the next JComponent receive the focus.
     * @param forward whether we're shifting forward or backward
     */
    private void transferFocus(boolean forward)
    {
        int direction = (forward) ? 1 : -1;

        // because vertical sliders are technically inverted already:
        if (getOrientation() == VERTICAL)
            direction = direction * -1;

        // because inverted sliders are, well, inverted:
        if (isInverted())
            direction = direction * -1;

        int selectedThumb = getSelectedThumb();
        if (direction == 1)
        {
            if (selectedThumb != this.thumbPositions.length - 1)
            {
                setSelectedThumb(selectedThumb + 1);
                return;
            }
        }
        else
        {
            if (selectedThumb != 0)
            {
                setSelectedThumb(selectedThumb - 1);
                return;
            }
        }
        if (forward)
        {
            super.transferFocus();
        }
        else
        {
            super.transferFocusBackward();
        }
    }

    /**
     * Depending on which thumb is selected, this may shift the focus to the previous available thumb, or it may shift
     * the focus to the previous focusable <code>JComponent</code>.
     */
    @Override
    public void transferFocusBackward()
    {
        transferFocus(false);
    }

    /**
     * This creates a new value for insertion.
     * <P>
     * If the <code>pos</code> argument is outside the domain of thumbs, then a value still needs to be returned.
     * @param pos a position between zero and one
     * @return a value that corresponds to the position <code>pos</code>
     */
    public T createValueForInsertion(float pos)
    {
        throw new NullPointerException(
                "this method is undefined. Either auto-adding should be disabled, or this method needs to be overridden to return a value");
    }

    /**
     * Removes a specific thumb
     * @param thumbIndex the thumb index to remove.
     */
    public void removeThumb(int thumbIndex)
    {
        if (thumbIndex < 0 || thumbIndex > this.thumbPositions.length)
            throw new IllegalArgumentException("There is no thumb at index " + thumbIndex + " to remove.");

        float[] f = new float[this.thumbPositions.length - 1];
        T[] c = createSimilarArray(this.values, this.values.length - 1);
        System.arraycopy(this.thumbPositions, 0, f, 0, thumbIndex);
        System.arraycopy(this.values, 0, c, 0, thumbIndex);
        System.arraycopy(this.thumbPositions, thumbIndex + 1, f, thumbIndex, f.length - thumbIndex);
        System.arraycopy(this.values, thumbIndex + 1, c, thumbIndex, f.length - thumbIndex);
        setValues(f, c);
    }

    /** This is a kludgy casting trick to make our arrays mesh with generics. */
    private T[] createSimilarArray(T[] srcArray, int length)
    {
        Class<?> componentType = srcArray.getClass().getComponentType();
        return (T[]) Array.newInstance(componentType, length);
    }

    /**
     * An optional method subclasses can override to react to the user's double-click. When a thumb is double-clicked
     * the user is trying to edit the value for that thumb. A double-click probably suggests the user wants a detailed
     * set of controls to edit a value, such as a dialog.
     * <P>
     * Note this method will be called with arguments (-1,-1) if the space bar or return key is pressed.
     * <P>
     * By default this method does nothing, and returns <code>false</code>
     * <P>
     * Note the (x,y) information passed to this method is only provided so subclasses can position components (such as
     * a JPopupMenu). It can be assumed for a double-click event that the user has selected a thumb (since one click
     * will click/create a thumb) and intends to edit the currently selected thumb.
     * @param x the x-value of the mouse click location
     * @param y the y-value of the mouse click location
     * @return <code>true</code> if this event was consumed, or acted upon. <code>false</code> if this is unimplemented.
     */
    public boolean doDoubleClick(int x, int y)
    {
        return false;
    }

    /**
     * An optional method subclasses can override to react to the user's request for a contextual menu. When a thumb is
     * right-clicked the user is trying to edit the value for that thumb. A right-click probably suggests the user wants
     * very quick, simple options to adjust a thumb.
     * <P>
     * By default this method does nothing, and returns <code>false</code>
     * @param x the x-value of the mouse click location
     * @param y the y-value of the mouse click location
     * @return <code>true</code> if this event was consumed, or acted upon. <code>false</code> if this is unimplemented.
     */
    public boolean doPopup(int x, int y)
    {
        return false;
    }

    /**
     * Tells if tick marks are to be painted.
     * @return whether ticks should be painted on this slider.
     */
    public boolean isPaintTicks()
    {
        Boolean b = (Boolean) getClientProperty(PAINT_TICKS_PROPERTY);
        if (b == null)
            return false;
        return b;
    }

    /**
     * Turns on/off the painted tick marks for this slider.
     * <P>
     * This triggers a <code>PropertyChangeEvent</code> for <code>PAINT_TICKS_PROPERTY</code>.
     * @param b whether tick marks should be painted
     */
    public void setPaintTicks(boolean b)
    {
        putClientProperty(PAINT_TICKS_PROPERTY, b);
    }

    /**
     * This creats and inserts a thumb at a position indicated.
     * <P>
     * This method relies on the abstract <code>getValue(float)</code> to determine what value to put at the new thumb
     * location.
     * @param pos the new thumb position
     * @return the index of the newly created thumb
     */
    public int addThumb(float pos)
    {
        if (pos < 0 || pos > 1)
            throw new IllegalArgumentException("the new position (" + pos + ") must be between zero and one");
        T newValue = createValueForInsertion(pos);
        float[] f = new float[this.thumbPositions.length + 1];
        T[] c = createSimilarArray(this.values, this.values.length + 1);

        int newIndex = -1;
        if (pos < this.thumbPositions[0])
        {
            System.arraycopy(this.thumbPositions, 0, f, 1, this.thumbPositions.length);
            System.arraycopy(this.values, 0, c, 1, this.values.length);
            newIndex = 0;
            f[0] = pos;
            c[0] = newValue;
        }
        else if (pos > this.thumbPositions[this.thumbPositions.length - 1])
        {
            System.arraycopy(this.thumbPositions, 0, f, 0, this.thumbPositions.length);
            System.arraycopy(this.values, 0, c, 0, this.values.length);
            newIndex = f.length - 1;
            f[f.length - 1] = pos;
            c[c.length - 1] = newValue;
        }
        else
        {
            boolean addedYet = false;
            for (int a = 0; a < f.length; a++)
            {
                if (addedYet == false && this.thumbPositions[a] < pos)
                {
                    f[a] = this.thumbPositions[a];
                    c[a] = this.values[a];
                }
                else
                {
                    if (addedYet == false)
                    {
                        c[a] = newValue;
                        f[a] = pos;
                        addedYet = true;
                        newIndex = a;
                    }
                    else
                    {
                        f[a] = this.thumbPositions[a - 1];
                        c[a] = this.values[a - 1];
                    }
                }
            }
        }
        setValues(f, c);
        return newIndex;
    }

    /**
     * This is used to notify other objects when the user is in the process of adjusting values in this slider.
     * <P>
     * A listener may not want to act on certain changes until this property is <code>false</code> if it is expensive to
     * process certain changes.
     * <P>
     * This triggers a <code>PropertyChangeEvent</code> for <code>ADJUST_PROPERTY</code>.
     * @param b
     */
    public void setValueIsAdjusting(boolean b)
    {
        putClientProperty(ADJUST_PROPERTY, b);
    }

    /**
     * <code>true</code> if the user is current modifying this component.
     * @return the value of the <code>adjusting</code> property
     */
    public boolean isValueAdjusting()
    {
        Boolean b = (Boolean) getClientProperty(ADJUST_PROPERTY);
        if (b == null)
            return false;
        return b;
    }

    /**
     * The thumb positions for this slider.
     * <P>
     * There is a one-to-one correspondence between this array and the <code>getValues()</code> array.
     * <P>
     * This array is always sorted in ascending order.
     * @return an array of the positions of thumbs.
     */
    public float[] getThumbPositions()
    {
        float[] f = new float[this.thumbPositions.length];
        System.arraycopy(this.thumbPositions, 0, f, 0, f.length);
        return f;
    }

    /**
     * The values for thumbs for this slider.
     * <P>
     * There is a one-to-one correspondence between this array and the <code>getThumbPositions()</code> array.
     * @return an array of the values associated with each thumb.
     */
    public T[] getValues()
    {
        T[] c = createSimilarArray(this.values, this.values.length);
        System.arraycopy(this.values, 0, c, 0, c.length);
        return c;
    }

    /**
     * @param f an array of floats
     * @return a string representation of f
     */
    private static String toString(float[] f)
    {
        StringBuffer sb = new StringBuffer();
        sb.append('[');
        for (int a = 0; a < f.length; a++)
        {
            sb.append(Float.toString(f[a]));
            if (a != f.length - 1)
            {
                sb.append(", ");
            }
        }
        sb.append(']');
        return sb.toString();
    }

    /**
     * This assigns new positions/values for the thumbs in this slider. The two must be assigned at exactly the same
     * time, so there is always the same number of thumbs/sliders.
     * <P>
     * This triggers a <code>PropertyChangeEvent</code> for <code>VALUES_PROPERTY</code>, and possibly for the
     * <code>SELECTED_THUMB_PROPERTY</code> if that had to be adjusted, too.
     * @param thumbPositions an array of the new position of each thumb
     * @param values an array of the value associated with each thumb
     * @throws IllegalArgumentException if the size of the arrays are different, or if the thumbPositions array is not
     *             sorted in ascending order.
     */
    public void setValues(float[] thumbPositions, T[] values)
    {
        if (values.length != thumbPositions.length)
            throw new IllegalArgumentException("there number of positions (" + thumbPositions.length
                    + ") must equal the number of values (" + values.length + ")");

        for (int a = 0; a < values.length; a++)
        {
            if (values[a] == null)
                throw new NullPointerException();
            if (a > 0 && thumbPositions[a] < thumbPositions[a - 1])
                throw new IllegalArgumentException("the thumb positions must be ascending order ("
                        + toString(thumbPositions) + ")");
            if (thumbPositions[a] < 0 || thumbPositions[a] > 1)
                throw new IllegalArgumentException("illegal thumb value " + thumbPositions[a]
                        + " (must be between zero and one)");
        }

        // don't clone arrays and fire off events if
        // there really is no change here:
        if (thumbPositions.length == this.thumbPositions.length)
        {
            boolean equal = true;
            for (int a = 0; a < thumbPositions.length && equal; a++)
            {
                if (thumbPositions[a] != this.thumbPositions[a])
                    equal = false;
            }
            for (int a = 0; a < values.length && equal; a++)
            {
                if (!values[a].equals(this.values[a]))
                    equal = false;
            }
            if (equal)
                return; // no change! go home.
        }

        this.thumbPositions = new float[thumbPositions.length];
        System.arraycopy(thumbPositions, 0, this.thumbPositions, 0, thumbPositions.length);
        this.values = createSimilarArray(values, values.length);
        System.arraycopy(values, 0, this.values, 0, values.length);
        int oldThumb = getSelectedThumb();
        int newThumb = oldThumb;
        if (newThumb >= thumbPositions.length)
        {
            newThumb = thumbPositions.length - 1;
        }
        firePropertyChange(VALUES_PROPERTY, null, values);
        if (oldThumb != newThumb)
        {
            setSelectedThumb(newThumb);
        }
        fireChangeListeners();
    }

    /**
     * The number of thumbs in this slider.
     * @return the number of thumbs.
     */
    public int getThumbCount()
    {
        return this.thumbPositions.length;
    }

    /**
     * Assigns the currently selected thumb. A value of -1 indicates that no thumb is currently selected.
     * <P>
     * A slider should always have a selected thumb if it has the keyboard focus, though, so be careful when you modify
     * this.
     * <P>
     * This triggers a <code>PropertyChangeEvent</code> for <code>SELECTED_THUMB_PROPERTY</code>.
     * @param index the new selected thumb
     */
    public void setSelectedThumb(int index)
    {
        putClientProperty(SELECTED_THUMB_PROPERTY, new Integer(index));
    }

    /**
     * Returns the selected thumb index, or -1 if this component doesn't have the keyboard focus.
     * @return the selected thumb index
     */
    public int getSelectedThumb()
    {
        return getSelectedThumb(true);
    }

    /**
     * Returns the currently selected thumb index.
     * <P>
     * Note this might be -1, indicating that there is no selected thumb.
     * <P>
     * It is recommend you use the <code>getSelectedThumb()</code> method most of the time. This method is made public
     * so UI's can provide a better user experience as this component gains and loses focus.
     * @param ignoreIfUnfocused if this component doesn't have focus and this is <code>true</code>, then this returns
     *            -1. If this is <code>false</code> then this returns the internal value used to store the selected
     *            index, but the user may not realize this thumb is "selected".
     * @return the selected thumb
     */
    public int getSelectedThumb(boolean ignoreIfUnfocused)
    {
        if (hasFocus() == false && ignoreIfUnfocused)
            return -1;
        Integer i = (Integer) getClientProperty(SELECTED_THUMB_PROPERTY);
        if (i == null)
            return -1;
        return i.intValue();
    }

    /**
     * Controls whether thumbs are automatically added when the user clicks in a space that doesn't already have a
     * thumb.
     * @param b whether auto adding is active or not
     */
    public void setAutoAdding(boolean b)
    {
        putClientProperty(AUTOADD_PROPERTY, b);
    }

    /**
     * Whether thumbs are automatically added when the user clicks in a space that doesn't already have a thumb.
     */
    public boolean isAutoAdding()
    {
        Boolean b = (Boolean) getClientProperty(AUTOADD_PROPERTY);
        if (b == null)
            return true;
        return b;
    }

    /**
     * The orientation of this slider.
     * @return HORIZONTAL or VERTICAL
     */
    public int getOrientation()
    {
        Integer i = (Integer) getClientProperty(ORIENTATION_PROPERTY);
        if (i == null)
            return HORIZONTAL;
        return i;
    }

    /**
     * Reassign the orientation of this slider.
     * @param i must be HORIZONTAL or VERTICAL
     */
    public void setOrientation(int i)
    {
        if (!(i == SwingConstants.HORIZONTAL || i == SwingConstants.VERTICAL))
            throw new IllegalArgumentException("the orientation must be HORIZONTAL or VERTICAL");
        putClientProperty(ORIENTATION_PROPERTY, i);
    }

    /**
     * Whether this slider is inverted or not.
     */
    @SuppressWarnings("javadoc")
    public boolean isInverted()
    {
        Boolean b = (Boolean) getClientProperty(INVERTED_PROPERTY);
        if (b == null)
            return false;
        return b;
    }

    /**
     * Assigns whether this slider is inverted or not.
     * <P>
     * This triggers a <code>PropertyChangeEvent</code> for <code>INVERTED_PROPERTY</code>.
     */
    public void setInverted(boolean b)
    {
        putClientProperty(INVERTED_PROPERTY, b);
    }

    public Collision getCollisionPolicy()
    {
        Collision c = (Collision) getClientProperty(COLLISION_PROPERTY);
        if (c == null)
            c = Collision.JUMP_OVER_OTHER;
        return c;
    }

    public void setCollisionPolicy(Collision c)
    {
        putClientProperty(COLLISION_PROPERTY, c);
    }

    public boolean isThumbRemovalAllowed()
    {
        Boolean b = (Boolean) getClientProperty(REMOVAL_ALLOWED);
        if (b == null)
            b = true;
        return b;
    }

    public void setThumbRemovalAllowed(boolean b)
    {
        putClientProperty(REMOVAL_ALLOWED, b);
    }

    public void setMinimumThumbnailCount(int i)
    {
        putClientProperty(THUMB_MINIMUM_PROPERTY, i);
    }

    public int getMinimumThumbnailCount()
    {
        Integer i = (Integer) getClientProperty(THUMB_MINIMUM_PROPERTY);
        if (i == null)
            return 1;
        return i;
    }

    public void setThumbOverlap(boolean i)
    {
        putClientProperty(THUMB_OVERLAP_PROPERTY, i);
    }

    public boolean isThumbOverlap()
    {
        Boolean b = (Boolean) getClientProperty(THUMB_OVERLAP_PROPERTY);
        if (b == null)
            return false;
        return b;
    }
}