/*
 * Copyright (c) 1997, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javax.swing.plaf.basic;

import java.awt.event.*;
import java.awt.*;
import java.beans.*;
import java.util.Dictionary;
import java.util.Enumeration;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.*;
import sun.swing.DefaultLookup;
import sun.swing.SwingUtilities2;
import sun.swing.UIAction;


/**
 * A Basic L&amp;F implementation of SliderUI.
 *
 * @author Tom Santos
 */
public class BasicSliderUI extends SliderUI{
    // Old actions forward to an instance of this.
    private static final Actions SHARED_ACTION = new Actions();

    /** Positive scroll */
    public static final int POSITIVE_SCROLL = +1;
    /** Negative scroll */
    public static final int NEGATIVE_SCROLL = -1;
    /** Minimum scroll */
    public static final int MIN_SCROLL = -2;
    /** Maximum scroll */
    public static final int MAX_SCROLL = +2;

    /** Scroll timer */
    protected Timer scrollTimer;
    /** Slider */
    protected JSlider slider;

    /** Focus insets */
    protected Insets focusInsets = null;
    /** Inset cache */
    protected Insets insetCache = null;
    /** Left-to-right cache */
    protected boolean leftToRightCache = true;
    /** Focus rectangle */
    protected Rectangle focusRect = null;
    /** Content rectangle */
    protected Rectangle contentRect = null;
    /** Label rectangle */
    protected Rectangle labelRect = null;
    /** Tick rectangle */
    protected Rectangle tickRect = null;
    /** Track rectangle */
    protected Rectangle trackRect = null;
    /** Thumb rectangle */
    protected Rectangle thumbRect = null;

    /** The distance that the track is from the side of the control */
    protected int trackBuffer = 0;

    private transient boolean isDragging;

    /** Track listener */
    protected TrackListener trackListener;
    /** Change listener */
    protected ChangeListener changeListener;
    /** Component listener */
    protected ComponentListener componentListener;
    /** Focus listener */
    protected FocusListener focusListener;
    /** Scroll listener */
    protected ScrollListener scrollListener;
    /** Property chane listener */
    protected PropertyChangeListener propertyChangeListener;
    private Handler handler;
    private int lastValue;

    // Colors
    private Color shadowColor;
    private Color highlightColor;
    private Color focusColor;

    /**
     * Whther or not sameLabelBaselines is up to date.
     */
    private boolean checkedLabelBaselines;
    /**
     * Whether or not all the entries in the labeltable have the same
     * baseline.
     */
    private boolean sameLabelBaselines;

    /**
     * Returns the shadow color.
     * @return the shadow color
     */
    protected Color getShadowColor() {
        return shadowColor;
    }

    /**
     * Returns the highlight color.
     * @return the highlight color
     */
    protected Color getHighlightColor() {
        return highlightColor;
    }

    /**
     * Returns the focus color.
     * @return the focus color
     */
    protected Color getFocusColor() {
        return focusColor;
    }

    /**
     * Returns true if the user is dragging the slider.
     *
     * @return true if the user is dragging the slider
     * @since 1.5
     */
    protected boolean isDragging() {
        return isDragging;
    }

    /////////////////////////////////////////////////////////////////////////////
    // ComponentUI Interface Implementation methods
    /////////////////////////////////////////////////////////////////////////////
    /**
     * Creates a UI.
     * @param b a component
     * @return a UI
     */
    public static ComponentUI createUI(JComponent b)    {
        return new BasicSliderUI((JSlider)b);
    }

    /**
     * Constructs a {@code BasicSliderUI}.
     * @param b a slider
     */
    public BasicSliderUI(JSlider b)   {
    }

    /**
     * Installs a UI.
     * @param c a component
     */
    public void installUI(JComponent c)   {
        slider = (JSlider) c;

        checkedLabelBaselines = false;

        slider.setEnabled(slider.isEnabled());
        LookAndFeel.installProperty(slider, "opaque", Boolean.TRUE);

        isDragging = false;
        trackListener = createTrackListener( slider );
        changeListener = createChangeListener( slider );
        componentListener = createComponentListener( slider );
        focusListener = createFocusListener( slider );
        scrollListener = createScrollListener( slider );
        propertyChangeListener = createPropertyChangeListener( slider );

        installDefaults( slider );
        installListeners( slider );
        installKeyboardActions( slider );

        scrollTimer = new Timer( 100, scrollListener );
        scrollTimer.setInitialDelay( 300 );

        insetCache = slider.getInsets();
        leftToRightCache = BasicGraphicsUtils.isLeftToRight(slider);
        focusRect = new Rectangle();
        contentRect = new Rectangle();
        labelRect = new Rectangle();
        tickRect = new Rectangle();
        trackRect = new Rectangle();
        thumbRect = new Rectangle();
        lastValue = slider.getValue();

        calculateGeometry(); // This figures out where the labels, ticks, track, and thumb are.
    }

    /**
     * Uninstalls a UI.
     * @param c a component
     */
    public void uninstallUI(JComponent c) {
        if ( c != slider )
            throw new IllegalComponentStateException(
                                                    this + " was asked to deinstall() "
                                                    + c + " when it only knows about "
                                                    + slider + ".");

        scrollTimer.stop();
        scrollTimer = null;

        uninstallDefaults(slider);
        uninstallListeners( slider );
        uninstallKeyboardActions(slider);

        insetCache = null;
        leftToRightCache = true;
        focusRect = null;
        contentRect = null;
        labelRect = null;
        tickRect = null;
        trackRect = null;
        thumbRect = null;
        trackListener = null;
        changeListener = null;
        componentListener = null;
        focusListener = null;
        scrollListener = null;
        propertyChangeListener = null;
        slider = null;
    }

    /**
     * Installs the defaults.
     * @param slider a slider
     */
    protected void installDefaults( JSlider slider ) {
        LookAndFeel.installBorder(slider, "Slider.border");
        LookAndFeel.installColorsAndFont(slider, "Slider.background",
                                         "Slider.foreground", "Slider.font");
        highlightColor = UIManager.getColor("Slider.highlight");

        shadowColor = UIManager.getColor("Slider.shadow");
        focusColor = UIManager.getColor("Slider.focus");

        focusInsets = (Insets)UIManager.get( "Slider.focusInsets" );
        // use default if missing so that BasicSliderUI can be used in other
        // LAFs like Nimbus
        if (focusInsets == null) focusInsets = new InsetsUIResource(2,2,2,2);
    }

    /**
     * Uninstalls the defaults.
     * @param slider a slider
     */
    protected void uninstallDefaults(JSlider slider) {
        LookAndFeel.uninstallBorder(slider);

        focusInsets = null;
    }

    /**
     * Creates a track listener.
     * @return a track listener
     * @param slider a slider
     */
    protected TrackListener createTrackListener(JSlider slider) {
        return new TrackListener();
    }

    /**
     * Creates a change listener.
     * @return a change listener
     * @param slider a slider
     */
    protected ChangeListener createChangeListener(JSlider slider) {
        return getHandler();
    }

    /**
     * Creates a composite listener.
     * @return a composite listener
     * @param slider a slider
     */
    protected ComponentListener createComponentListener(JSlider slider) {
        return getHandler();
    }

    /**
     * Creates a focus listener.
     * @return a focus listener
     * @param slider a slider
     */
    protected FocusListener createFocusListener(JSlider slider) {
        return getHandler();
    }

    /**
     * Creates a scroll listener.
     * @return a scroll listener
     * @param slider a slider
     */
    protected ScrollListener createScrollListener( JSlider slider ) {
        return new ScrollListener();
    }

    /**
     * Creates a property change listener.
     * @return a property change listener
     * @param slider a slider
     */
    protected PropertyChangeListener createPropertyChangeListener(
            JSlider slider) {
        return getHandler();
    }

    private Handler getHandler() {
        if (handler == null) {
            handler = new Handler();
        }
        return handler;
    }

    /**
     * Installs listeners.
     * @param slider a slider
     */
    protected void installListeners( JSlider slider ) {
        slider.addMouseListener(trackListener);
        slider.addMouseMotionListener(trackListener);
        slider.addFocusListener(focusListener);
        slider.addComponentListener(componentListener);
        slider.addPropertyChangeListener( propertyChangeListener );
        slider.getModel().addChangeListener(changeListener);
    }

    /**
     * Uninstalls listeners.
     * @param slider a slider
     */
    protected void uninstallListeners( JSlider slider ) {
        slider.removeMouseListener(trackListener);
        slider.removeMouseMotionListener(trackListener);
        slider.removeFocusListener(focusListener);
        slider.removeComponentListener(componentListener);
        slider.removePropertyChangeListener( propertyChangeListener );
        slider.getModel().removeChangeListener(changeListener);
        handler = null;
    }

    /**
     * Installs keyboard actions.
     * @param slider a slider
     */
    protected void installKeyboardActions( JSlider slider ) {
        InputMap km = getInputMap(JComponent.WHEN_FOCUSED, slider);
        SwingUtilities.replaceUIInputMap(slider, JComponent.WHEN_FOCUSED, km);
        LazyActionMap.installLazyActionMap(slider, BasicSliderUI.class,
                "Slider.actionMap");
    }

    InputMap getInputMap(int condition, JSlider slider) {
        if (condition == JComponent.WHEN_FOCUSED) {
            InputMap keyMap = (InputMap)DefaultLookup.get(slider, this,
                  "Slider.focusInputMap");
            InputMap rtlKeyMap;

            if (slider.getComponentOrientation().isLeftToRight() ||
                ((rtlKeyMap = (InputMap)DefaultLookup.get(slider, this,
                          "Slider.focusInputMap.RightToLeft")) == null)) {
                return keyMap;
            } else {
                rtlKeyMap.setParent(keyMap);
                return rtlKeyMap;
            }
        }
        return null;
    }

    /**
     * Populates ComboBox's actions.
     */
    static void loadActionMap(LazyActionMap map) {
        map.put(new Actions(Actions.POSITIVE_UNIT_INCREMENT));
        map.put(new Actions(Actions.POSITIVE_BLOCK_INCREMENT));
        map.put(new Actions(Actions.NEGATIVE_UNIT_INCREMENT));
        map.put(new Actions(Actions.NEGATIVE_BLOCK_INCREMENT));
        map.put(new Actions(Actions.MIN_SCROLL_INCREMENT));
        map.put(new Actions(Actions.MAX_SCROLL_INCREMENT));
    }

    /**
     * Uninstalls keyboard actions.
     * @param slider a slider
     */
    protected void uninstallKeyboardActions( JSlider slider ) {
        SwingUtilities.replaceUIActionMap(slider, null);
        SwingUtilities.replaceUIInputMap(slider, JComponent.WHEN_FOCUSED,
                                         null);
    }


    /**
     * Returns the baseline.
     *
     * @throws NullPointerException {@inheritDoc}
     * @throws IllegalArgumentException {@inheritDoc}
     * @see javax.swing.JComponent#getBaseline(int, int)
     * @since 1.6
     */
    public int getBaseline(JComponent c, int width, int height) {
        super.getBaseline(c, width, height);
        if (slider.getPaintLabels() && labelsHaveSameBaselines()) {
            FontMetrics metrics = slider.getFontMetrics(slider.getFont());
            Insets insets = slider.getInsets();
            Dimension thumbSize = getThumbSize();
            if (slider.getOrientation() == JSlider.HORIZONTAL) {
                int tickLength = getTickLength();
                int contentHeight = height - insets.top - insets.bottom -
                    focusInsets.top - focusInsets.bottom;
                int thumbHeight = thumbSize.height;
                int centerSpacing = thumbHeight;
                if (slider.getPaintTicks()) {
                    centerSpacing += tickLength;
                }
                // Assume uniform labels.
                centerSpacing += getHeightOfTallestLabel();
                int trackY = insets.top + focusInsets.top +
                    (contentHeight - centerSpacing - 1) / 2;
                int trackHeight = thumbHeight;
                int tickY = trackY + trackHeight;
                int tickHeight = tickLength;
                if (!slider.getPaintTicks()) {
                    tickHeight = 0;
                }
                int labelY = tickY + tickHeight;
                return labelY + metrics.getAscent();
            }
            else { // vertical
                boolean inverted = slider.getInverted();
                Integer value = inverted ? getLowestValue() :
                                           getHighestValue();
                if (value != null) {
                    int thumbHeight = thumbSize.height;
                    int trackBuffer = Math.max(metrics.getHeight() / 2,
                                               thumbHeight / 2);
                    int contentY = focusInsets.top + insets.top;
                    int trackY = contentY + trackBuffer;
                    int trackHeight = height - focusInsets.top -
                        focusInsets.bottom - insets.top - insets.bottom -
                        trackBuffer - trackBuffer;
                    int yPosition = yPositionForValue(value, trackY,
                                                      trackHeight);
                    return yPosition - metrics.getHeight() / 2 +
                        metrics.getAscent();
                }
            }
        }
        return 0;
    }

    /**
     * Returns an enum indicating how the baseline of the component
     * changes as the size changes.
     *
     * @throws NullPointerException {@inheritDoc}
     * @see javax.swing.JComponent#getBaseline(int, int)
     * @since 1.6
     */
    public Component.BaselineResizeBehavior getBaselineResizeBehavior(
            JComponent c) {
        super.getBaselineResizeBehavior(c);
        // NOTE: BasicSpinner really provides for CENTER_OFFSET, but
        // the default min/pref size is smaller than it should be
        // so that getBaseline() doesn't implement the contract
        // for CENTER_OFFSET as defined in Component.
        return Component.BaselineResizeBehavior.OTHER;
    }

    /**
     * Returns true if all the labels from the label table have the same
     * baseline.
     *
     * @return true if all the labels from the label table have the
     *         same baseline
     * @since 1.6
     */
    protected boolean labelsHaveSameBaselines() {
        if (!checkedLabelBaselines) {
            checkedLabelBaselines = true;
            @SuppressWarnings("rawtypes")
            Dictionary dictionary = slider.getLabelTable();
            if (dictionary != null) {
                sameLabelBaselines = true;
                Enumeration<?> elements = dictionary.elements();
                int baseline = -1;
                while (elements.hasMoreElements()) {
                    JComponent label = (JComponent) elements.nextElement();
                    Dimension pref = label.getPreferredSize();
                    int labelBaseline = label.getBaseline(pref.width,
                                                          pref.height);
                    if (labelBaseline >= 0) {
                        if (baseline == -1) {
                            baseline = labelBaseline;
                        }
                        else if (baseline != labelBaseline) {
                            sameLabelBaselines = false;
                            break;
                        }
                    }
                    else {
                        sameLabelBaselines = false;
                        break;
                    }
                }
            }
            else {
                sameLabelBaselines = false;
            }
        }
        return sameLabelBaselines;
    }

    /**
     * Returns the preferred horizontal size.
     * @return the preferred horizontal size
     */
    public Dimension getPreferredHorizontalSize() {
        Dimension horizDim = (Dimension)DefaultLookup.get(slider,
                this, "Slider.horizontalSize");
        if (horizDim == null) {
            horizDim = new Dimension(200, 21);
        }
        return horizDim;
    }

    /**
     * Returns the preferred vertical size.
     * @return the preferred vertical size
     */
    public Dimension getPreferredVerticalSize() {
        Dimension vertDim = (Dimension)DefaultLookup.get(slider,
                this, "Slider.verticalSize");
        if (vertDim == null) {
            vertDim = new Dimension(21, 200);
        }
        return vertDim;
    }

    /**
     * Returns the minimum horizontal size.
     * @return the minimum horizontal size
     */
    public Dimension getMinimumHorizontalSize() {
        Dimension minHorizDim = (Dimension)DefaultLookup.get(slider,
                this, "Slider.minimumHorizontalSize");
        if (minHorizDim == null) {
            minHorizDim = new Dimension(36, 21);
        }
        return minHorizDim;
    }

    /**
     * Returns the minimum vertical size.
     * @return the minimum vertical size
     */
    public Dimension getMinimumVerticalSize() {
        Dimension minVertDim = (Dimension)DefaultLookup.get(slider,
                this, "Slider.minimumVerticalSize");
        if (minVertDim == null) {
            minVertDim = new Dimension(21, 36);
        }
        return minVertDim;
    }

    /**
     * Returns the preferred size.
     * @param c a component
     * @return the preferred size
     */
    public Dimension getPreferredSize(JComponent c)    {
        recalculateIfInsetsChanged();
        Dimension d;
        if ( slider.getOrientation() == JSlider.VERTICAL ) {
            d = new Dimension(getPreferredVerticalSize());
            d.width = insetCache.left + insetCache.right;
            d.width += focusInsets.left + focusInsets.right;
            d.width += trackRect.width + tickRect.width + labelRect.width;
        }
        else {
            d = new Dimension(getPreferredHorizontalSize());
            d.height = insetCache.top + insetCache.bottom;
            d.height += focusInsets.top + focusInsets.bottom;
            d.height += trackRect.height + tickRect.height + labelRect.height;
        }

        return d;
    }

    /**
     * Returns the minimum size.
     * @param c a component
     * @return the minimum size
     */
    public Dimension getMinimumSize(JComponent c)  {
        recalculateIfInsetsChanged();
        Dimension d;

        if ( slider.getOrientation() == JSlider.VERTICAL ) {
            d = new Dimension(getMinimumVerticalSize());
            d.width = insetCache.left + insetCache.right;
            d.width += focusInsets.left + focusInsets.right;
            d.width += trackRect.width + tickRect.width + labelRect.width;
        }
        else {
            d = new Dimension(getMinimumHorizontalSize());
            d.height = insetCache.top + insetCache.bottom;
            d.height += focusInsets.top + focusInsets.bottom;
            d.height += trackRect.height + tickRect.height + labelRect.height;
        }

        return d;
    }

    /**
     * Returns the maximum size.
     * @param c a component
     * @return the maximum size
     */
    public Dimension getMaximumSize(JComponent c) {
        Dimension d = getPreferredSize(c);
        if ( slider.getOrientation() == JSlider.VERTICAL ) {
            d.height = Short.MAX_VALUE;
        }
        else {
            d.width = Short.MAX_VALUE;
        }

        return d;
    }

    /**
     * Calculates the geometry.
     */
    protected void calculateGeometry() {
        calculateFocusRect();
        calculateContentRect();
        calculateThumbSize();
        calculateTrackBuffer();
        calculateTrackRect();
        calculateTickRect();
        calculateLabelRect();
        calculateThumbLocation();
    }

    /**
     * Calculates the focus rectangle.
     */
    protected void calculateFocusRect() {
        focusRect.x = insetCache.left;
        focusRect.y = insetCache.top;
        focusRect.width = slider.getWidth() - (insetCache.left + insetCache.right);
        focusRect.height = slider.getHeight() - (insetCache.top + insetCache.bottom);
    }

    /**
     * Calculates the thumb size rectangle.
     */
    protected void calculateThumbSize() {
        Dimension size = getThumbSize();
        thumbRect.setSize( size.width, size.height );
    }

    /**
     * Calculates the content rectangle.
     */
    protected void calculateContentRect() {
        contentRect.x = focusRect.x + focusInsets.left;
        contentRect.y = focusRect.y + focusInsets.top;
        contentRect.width = focusRect.width - (focusInsets.left + focusInsets.right);
        contentRect.height = focusRect.height - (focusInsets.top + focusInsets.bottom);
    }

    private int getTickSpacing() {
        int majorTickSpacing = slider.getMajorTickSpacing();
        int minorTickSpacing = slider.getMinorTickSpacing();

        int result;

        if (minorTickSpacing > 0) {
            result = minorTickSpacing;
        } else if (majorTickSpacing > 0) {
            result = majorTickSpacing;
        } else {
            result = 0;
        }

        return result;
    }

    /**
     * Calculates the thumb location.
     */
    protected void calculateThumbLocation() {
        if ( slider.getSnapToTicks() ) {
            int sliderValue = slider.getValue();
            int snappedValue = sliderValue;
            int tickSpacing = getTickSpacing();

            if ( tickSpacing != 0 ) {
                // If it's not on a tick, change the value
                if ( (sliderValue - slider.getMinimum()) % tickSpacing != 0 ) {
                    float temp = (float)(sliderValue - slider.getMinimum()) / (float)tickSpacing;
                    int whichTick = Math.round( temp );

                    // This is the fix for the bug #6401380
                    if (temp - (int)temp == .5 && sliderValue < lastValue) {
                      whichTick --;
                    }
                    snappedValue = slider.getMinimum() + (whichTick * tickSpacing);
                }

                if( snappedValue != sliderValue ) {
                    slider.setValue( snappedValue );
                }
            }
        }

        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            int valuePosition = xPositionForValue(slider.getValue());

            thumbRect.x = valuePosition - (thumbRect.width / 2);
            thumbRect.y = trackRect.y;
        }
        else {
            int valuePosition = yPositionForValue(slider.getValue());

            thumbRect.x = trackRect.x;
            thumbRect.y = valuePosition - (thumbRect.height / 2);
        }
    }

    /**
     * Calculates the track buffer.
     */
    protected void calculateTrackBuffer() {
        if ( slider.getPaintLabels() && slider.getLabelTable()  != null ) {
            Component highLabel = getHighestValueLabel();
            Component lowLabel = getLowestValueLabel();

            if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
                trackBuffer = Math.max( highLabel.getBounds().width, lowLabel.getBounds().width ) / 2;
                trackBuffer = Math.max( trackBuffer, thumbRect.width / 2 );
            }
            else {
                trackBuffer = Math.max( highLabel.getBounds().height, lowLabel.getBounds().height ) / 2;
                trackBuffer = Math.max( trackBuffer, thumbRect.height / 2 );
            }
        }
        else {
            if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
                trackBuffer = thumbRect.width / 2;
            }
            else {
                trackBuffer = thumbRect.height / 2;
            }
        }
    }

    /**
     * Calculates the track rectangle.
     */
    protected void calculateTrackRect() {
        int centerSpacing; // used to center sliders added using BorderLayout.CENTER (bug 4275631)
        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            centerSpacing = thumbRect.height;
            if ( slider.getPaintTicks() ) centerSpacing += getTickLength();
            if ( slider.getPaintLabels() ) centerSpacing += getHeightOfTallestLabel();
            trackRect.x = contentRect.x + trackBuffer;
            trackRect.y = contentRect.y + (contentRect.height - centerSpacing - 1)/2;
            trackRect.width = contentRect.width - (trackBuffer * 2);
            trackRect.height = thumbRect.height;
        }
        else {
            centerSpacing = thumbRect.width;
            if (BasicGraphicsUtils.isLeftToRight(slider)) {
                if ( slider.getPaintTicks() ) centerSpacing += getTickLength();
                if ( slider.getPaintLabels() ) centerSpacing += getWidthOfWidestLabel();
            } else {
                if ( slider.getPaintTicks() ) centerSpacing -= getTickLength();
                if ( slider.getPaintLabels() ) centerSpacing -= getWidthOfWidestLabel();
            }
            trackRect.x = contentRect.x + (contentRect.width - centerSpacing - 1)/2;
            trackRect.y = contentRect.y + trackBuffer;
            trackRect.width = thumbRect.width;
            trackRect.height = contentRect.height - (trackBuffer * 2);
        }

    }

    /**
     * Gets the height of the tick area for horizontal sliders and the width of
     * the tick area for vertical sliders. BasicSliderUI uses the returned value
     * to determine the tick area rectangle. If you want to give your ticks some
     * room, make this larger than you need and paint your ticks away from the
     * sides in paintTicks().
     *
     * @return an integer representing the height of the tick area for
     * horizontal sliders, and the width of the tick area for the vertical
     * sliders
     */
    protected int getTickLength() {
        return 8;
    }

    /**
     * Calculates the tick rectangle.
     */
    protected void calculateTickRect() {
        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            tickRect.x = trackRect.x;
            tickRect.y = trackRect.y + trackRect.height;
            tickRect.width = trackRect.width;
            tickRect.height = (slider.getPaintTicks()) ? getTickLength() : 0;
        }
        else {
            tickRect.width = (slider.getPaintTicks()) ? getTickLength() : 0;
            if(BasicGraphicsUtils.isLeftToRight(slider)) {
                tickRect.x = trackRect.x + trackRect.width;
            }
            else {
                tickRect.x = trackRect.x - tickRect.width;
            }
            tickRect.y = trackRect.y;
            tickRect.height = trackRect.height;
        }
    }

    /**
     * Calculates the label rectangle.
     */
    protected void calculateLabelRect() {
        if ( slider.getPaintLabels() ) {
            if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
                labelRect.x = tickRect.x - trackBuffer;
                labelRect.y = tickRect.y + tickRect.height;
                labelRect.width = tickRect.width + (trackBuffer * 2);
                labelRect.height = getHeightOfTallestLabel();
            }
            else {
                if(BasicGraphicsUtils.isLeftToRight(slider)) {
                    labelRect.x = tickRect.x + tickRect.width;
                    labelRect.width = getWidthOfWidestLabel();
                }
                else {
                    labelRect.width = getWidthOfWidestLabel();
                    labelRect.x = tickRect.x - labelRect.width;
                }
                labelRect.y = tickRect.y - trackBuffer;
                labelRect.height = tickRect.height + (trackBuffer * 2);
            }
        }
        else {
            if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
                labelRect.x = tickRect.x;
                labelRect.y = tickRect.y + tickRect.height;
                labelRect.width = tickRect.width;
                labelRect.height = 0;
            }
            else {
                if(BasicGraphicsUtils.isLeftToRight(slider)) {
                    labelRect.x = tickRect.x + tickRect.width;
                }
                else {
                    labelRect.x = tickRect.x;
                }
                labelRect.y = tickRect.y;
                labelRect.width = 0;
                labelRect.height = tickRect.height;
            }
        }
    }

    /**
     * Returns the thumb size.
     * @return the thumb size
     */
    protected Dimension getThumbSize() {
        Dimension size = new Dimension();

        if ( slider.getOrientation() == JSlider.VERTICAL ) {
            size.width = 20;
            size.height = 11;
        }
        else {
            size.width = 11;
            size.height = 20;
        }

        return size;
    }

    /**
     * A property change handler.
     */
    public class PropertyChangeHandler implements PropertyChangeListener {
        // NOTE: This class exists only for backward compatibility. All
        // its functionality has been moved into Handler. If you need to add
        // new functionality add it to the Handler, but make sure this
        // class calls into the Handler.
        /** {@inheritDoc} */
        public void propertyChange( PropertyChangeEvent e ) {
            getHandler().propertyChange(e);
        }
    }

    /**
     * Returns the width of the widest label.
     * @return the width of the widest label
     */
    protected int getWidthOfWidestLabel() {
        @SuppressWarnings("rawtypes")
        Dictionary dictionary = slider.getLabelTable();
        int widest = 0;
        if ( dictionary != null ) {
            Enumeration<?> keys = dictionary.keys();
            while ( keys.hasMoreElements() ) {
                JComponent label = (JComponent) dictionary.get(keys.nextElement());
                widest = Math.max( label.getPreferredSize().width, widest );
            }
        }
        return widest;
    }

    /**
     * Returns the height of the tallest label.
     * @return the height of the tallest label
     */
    protected int getHeightOfTallestLabel() {
        @SuppressWarnings("rawtypes")
        Dictionary dictionary = slider.getLabelTable();
        int tallest = 0;
        if ( dictionary != null ) {
            Enumeration<?> keys = dictionary.keys();
            while ( keys.hasMoreElements() ) {
                JComponent label = (JComponent) dictionary.get(keys.nextElement());
                tallest = Math.max( label.getPreferredSize().height, tallest );
            }
        }
        return tallest;
    }

    /**
     * Returns the width of the highest value label.
     * @return the width of the highest value label
     */
    protected int getWidthOfHighValueLabel() {
        Component label = getHighestValueLabel();
        int width = 0;

        if ( label != null ) {
            width = label.getPreferredSize().width;
        }

        return width;
    }

    /**
     * Returns the width of the lowest value label.
     * @return the width of the lowest value label
     */
    protected int getWidthOfLowValueLabel() {
        Component label = getLowestValueLabel();
        int width = 0;

        if ( label != null ) {
            width = label.getPreferredSize().width;
        }

        return width;
    }

    /**
     * Returns the height of the highest value label.
     * @return the height of the highest value label
     */
    protected int getHeightOfHighValueLabel() {
        Component label = getHighestValueLabel();
        int height = 0;

        if ( label != null ) {
            height = label.getPreferredSize().height;
        }

        return height;
    }

    /**
     * Returns the height of the lowest value label.
     * @return the height of the lowest value label
     */
    protected int getHeightOfLowValueLabel() {
        Component label = getLowestValueLabel();
        int height = 0;

        if ( label != null ) {
            height = label.getPreferredSize().height;
        }

        return height;
    }

    /**
     * Draws inverted.
     * @return the inverted-ness
     */
    protected boolean drawInverted() {
        if (slider.getOrientation()==JSlider.HORIZONTAL) {
            if(BasicGraphicsUtils.isLeftToRight(slider)) {
                return slider.getInverted();
            } else {
                return !slider.getInverted();
            }
        } else {
            return slider.getInverted();
        }
    }

    /**
     * Returns the biggest value that has an entry in the label table.
     *
     * @return biggest value that has an entry in the label table, or
     *         null.
     * @since 1.6
     */
    protected Integer getHighestValue() {
        @SuppressWarnings("rawtypes")
        Dictionary dictionary = slider.getLabelTable();

        if (dictionary == null) {
            return null;
        }

        Enumeration<?> keys = dictionary.keys();

        Integer max = null;

        while (keys.hasMoreElements()) {
            Integer i = (Integer) keys.nextElement();

            if (max == null || i > max) {
                max = i;
            }
        }

        return max;
    }

    /**
     * Returns the smallest value that has an entry in the label table.
     *
     * @return smallest value that has an entry in the label table, or
     * null.
     * @since 1.6
     */
    protected Integer getLowestValue() {
        @SuppressWarnings("rawtypes")
        Dictionary dictionary = slider.getLabelTable();

        if (dictionary == null) {
            return null;
        }

        Enumeration<?> keys = dictionary.keys();

        Integer min = null;

        while (keys.hasMoreElements()) {
            Integer i = (Integer) keys.nextElement();

            if (min == null || i < min) {
                min = i;
            }
        }

        return min;
    }


    /**
     * Returns the label that corresponds to the highest slider value in the
     * label table.
     *
     * @return the label that corresponds to the highest slider value in the
     * label table
     * @see JSlider#setLabelTable
     */
    protected Component getLowestValueLabel() {
        Integer min = getLowestValue();
        if (min != null) {
            return (Component)slider.getLabelTable().get(min);
        }
        return null;
    }

    /**
     * Returns the label that corresponds to the lowest slider value in the
     * label table.
     *
     * @return the label that corresponds to the lowest slider value in the
     * label table
     * @see JSlider#setLabelTable
     */
    protected Component getHighestValueLabel() {
        Integer max = getHighestValue();
        if (max != null) {
            return (Component)slider.getLabelTable().get(max);
        }
        return null;
    }

    public void paint( Graphics g, JComponent c )   {
        recalculateIfInsetsChanged();
        recalculateIfOrientationChanged();
        Rectangle clip = g.getClipBounds();

        if ( !clip.intersects(trackRect) && slider.getPaintTrack())
            calculateGeometry();

        if ( slider.getPaintTrack() && clip.intersects( trackRect ) ) {
            paintTrack( g );
        }
        if ( slider.getPaintTicks() && clip.intersects( tickRect ) ) {
            paintTicks( g );
        }
        if ( slider.getPaintLabels() && clip.intersects( labelRect ) ) {
            paintLabels( g );
        }
        if ( slider.hasFocus() && clip.intersects( focusRect ) ) {
            paintFocus( g );
        }
        if ( clip.intersects( thumbRect ) ) {
            paintThumb( g );
        }
    }

    /**
     * Recalculates if the insets have changed.
     */
    protected void recalculateIfInsetsChanged() {
        Insets newInsets = slider.getInsets();
        if ( !newInsets.equals( insetCache ) ) {
            insetCache = newInsets;
            calculateGeometry();
        }
    }

    /**
     * Recalculates if the orientation has changed.
     */
    protected void recalculateIfOrientationChanged() {
        boolean ltr = BasicGraphicsUtils.isLeftToRight(slider);
        if ( ltr!=leftToRightCache ) {
            leftToRightCache = ltr;
            calculateGeometry();
        }
    }

    /**
     * Paints focus.
     * @param g the graphics
     */
    public void paintFocus(Graphics g)  {
        g.setColor( getFocusColor() );

        BasicGraphicsUtils.drawDashedRect( g, focusRect.x, focusRect.y,
                                           focusRect.width, focusRect.height );
    }

    /**
     * Paints track.
     * @param g the graphics
     */
    public void paintTrack(Graphics g)  {

        Rectangle trackBounds = trackRect;

        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            int cy = (trackBounds.height / 2) - 2;
            int cw = trackBounds.width;

            g.translate(trackBounds.x, trackBounds.y + cy);

            g.setColor(getShadowColor());
            g.drawLine(0, 0, cw - 1, 0);
            g.drawLine(0, 1, 0, 2);
            g.setColor(getHighlightColor());
            g.drawLine(0, 3, cw, 3);
            g.drawLine(cw, 0, cw, 3);
            g.setColor(Color.black);
            g.drawLine(1, 1, cw-2, 1);

            g.translate(-trackBounds.x, -(trackBounds.y + cy));
        }
        else {
            int cx = (trackBounds.width / 2) - 2;
            int ch = trackBounds.height;

            g.translate(trackBounds.x + cx, trackBounds.y);

            g.setColor(getShadowColor());
            g.drawLine(0, 0, 0, ch - 1);
            g.drawLine(1, 0, 2, 0);
            g.setColor(getHighlightColor());
            g.drawLine(3, 0, 3, ch);
            g.drawLine(0, ch, 3, ch);
            g.setColor(Color.black);
            g.drawLine(1, 1, 1, ch-2);

            g.translate(-(trackBounds.x + cx), -trackBounds.y);
        }
    }

    /**
     * Paints ticks.
     * @param g the graphics
     */
    public void paintTicks(Graphics g)  {
        Rectangle tickBounds = tickRect;

        g.setColor(DefaultLookup.getColor(slider, this, "Slider.tickColor", Color.black));

        if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            g.translate(0, tickBounds.y);

            if (slider.getMinorTickSpacing() > 0) {
                int value = slider.getMinimum();

                while ( value <= slider.getMaximum() ) {
                    int xPos = xPositionForValue(value);
                    paintMinorTickForHorizSlider( g, tickBounds, xPos );

                    // Overflow checking
                    if (Integer.MAX_VALUE - slider.getMinorTickSpacing() < value) {
                        break;
                    }

                    value += slider.getMinorTickSpacing();
                }
            }

            if (slider.getMajorTickSpacing() > 0) {
                int value = slider.getMinimum();

                while ( value <= slider.getMaximum() ) {
                    int xPos = xPositionForValue(value);
                    paintMajorTickForHorizSlider( g, tickBounds, xPos );

                    // Overflow checking
                    if (Integer.MAX_VALUE - slider.getMajorTickSpacing() < value) {
                        break;
                    }

                    value += slider.getMajorTickSpacing();
                }
            }

            g.translate( 0, -tickBounds.y);
        } else {
            g.translate(tickBounds.x, 0);

            if (slider.getMinorTickSpacing() > 0) {
                int offset = 0;
                if(!BasicGraphicsUtils.isLeftToRight(slider)) {
                    offset = tickBounds.width - tickBounds.width / 2;
                    g.translate(offset, 0);
                }

                int value = slider.getMinimum();

                while (value <= slider.getMaximum()) {
                    int yPos = yPositionForValue(value);
                    paintMinorTickForVertSlider( g, tickBounds, yPos );

                    // Overflow checking
                    if (Integer.MAX_VALUE - slider.getMinorTickSpacing() < value) {
                        break;
                    }

                    value += slider.getMinorTickSpacing();
                }

                if(!BasicGraphicsUtils.isLeftToRight(slider)) {
                    g.translate(-offset, 0);
                }
            }

            if (slider.getMajorTickSpacing() > 0) {
                if(!BasicGraphicsUtils.isLeftToRight(slider)) {
                    g.translate(2, 0);
                }

                int value = slider.getMinimum();

                while (value <= slider.getMaximum()) {
                    int yPos = yPositionForValue(value);
                    paintMajorTickForVertSlider( g, tickBounds, yPos );

                    // Overflow checking
                    if (Integer.MAX_VALUE - slider.getMajorTickSpacing() < value) {
                        break;
                    }

                    value += slider.getMajorTickSpacing();
                }

                if(!BasicGraphicsUtils.isLeftToRight(slider)) {
                    g.translate(-2, 0);
                }
            }
            g.translate(-tickBounds.x, 0);
        }
    }

    /**
     * Paints minor tick for horizontal slider.
     * @param g the graphics
     * @param tickBounds the tick bounds
     * @param x the x coordinate
     */
    protected void paintMinorTickForHorizSlider( Graphics g, Rectangle tickBounds, int x ) {
        g.drawLine( x, 0, x, tickBounds.height / 2 - 1 );
    }

    /**
     * Paints major tick for horizontal slider.
     * @param g the graphics
     * @param tickBounds the tick bounds
     * @param x the x coordinate
     */
    protected void paintMajorTickForHorizSlider( Graphics g, Rectangle tickBounds, int x ) {
        g.drawLine( x, 0, x, tickBounds.height - 2 );
    }

    /**
     * Paints minor tick for vertical slider.
     * @param g the graphics
     * @param tickBounds the tick bounds
     * @param y the y coordinate
     */
    protected void paintMinorTickForVertSlider( Graphics g, Rectangle tickBounds, int y ) {
        g.drawLine( 0, y, tickBounds.width / 2 - 1, y );
    }

    /**
     * Paints major tick for vertical slider.
     * @param g the graphics
     * @param tickBounds the tick bounds
     * @param y the y coordinate
     */
    protected void paintMajorTickForVertSlider( Graphics g, Rectangle tickBounds, int y ) {
        g.drawLine( 0, y,  tickBounds.width - 2, y );
    }

    /**
     * Paints the labels.
     * @param g the graphics
     */
    public void paintLabels( Graphics g ) {
        Rectangle labelBounds = labelRect;

        @SuppressWarnings("rawtypes")
        Dictionary dictionary = slider.getLabelTable();
        if ( dictionary != null ) {
            Enumeration<?> keys = dictionary.keys();
            int minValue = slider.getMinimum();
            int maxValue = slider.getMaximum();
            boolean enabled = slider.isEnabled();
            while ( keys.hasMoreElements() ) {
                Integer key = (Integer)keys.nextElement();
                int value = key.intValue();
                if (value >= minValue && value <= maxValue) {
                    JComponent label = (JComponent) dictionary.get(key);
                    label.setEnabled(enabled);

                    if (label instanceof JLabel) {
                        Icon icon = label.isEnabled() ? ((JLabel) label).getIcon() : ((JLabel) label).getDisabledIcon();

                        if (icon instanceof ImageIcon) {
                            // Register Slider as an image observer. It allows to catch notifications about
                            // image changes (e.g. gif animation)
                            Toolkit.getDefaultToolkit().checkImage(((ImageIcon) icon).getImage(), -1, -1, slider);
                        }
                    }

                    if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
                        g.translate( 0, labelBounds.y );
                        paintHorizontalLabel( g, value, label );
                        g.translate( 0, -labelBounds.y );
                    }
                    else {
                        int offset = 0;
                        if (!BasicGraphicsUtils.isLeftToRight(slider)) {
                            offset = labelBounds.width -
                                label.getPreferredSize().width;
                        }
                        g.translate( labelBounds.x + offset, 0 );
                        paintVerticalLabel( g, value, label );
                        g.translate( -labelBounds.x - offset, 0 );
                    }
                }
            }
        }

    }

    /**
     * Called for every label in the label table. Used to draw the labels for
     * horizontal sliders. The graphics have been translated to labelRect.y
     * already.
     *
     * @param g the graphics context in which to paint
     * @param value the value of the slider
     * @param label the component label in the label table that needs to be
     * painted
     * @see JSlider#setLabelTable
     */
    protected void paintHorizontalLabel( Graphics g, int value, Component label ) {
        int labelCenter = xPositionForValue( value );
        int labelLeft = labelCenter - (label.getPreferredSize().width / 2);
        g.translate( labelLeft, 0 );
        label.paint( g );
        g.translate( -labelLeft, 0 );
    }

    /**
     * Called for every label in the label table. Used to draw the labels for
     * vertical sliders. The graphics have been translated to labelRect.x
     * already.
     *
     * @param g the graphics context in which to paint
     * @param value the value of the slider
     * @param label the component label in the label table that needs to be
     * painted
     * @see JSlider#setLabelTable
     */
    protected void paintVerticalLabel( Graphics g, int value, Component label ) {
        int labelCenter = yPositionForValue( value );
        int labelTop = labelCenter - (label.getPreferredSize().height / 2);
        g.translate( 0, labelTop );
        label.paint( g );
        g.translate( 0, -labelTop );
    }

    /**
     * Paints the thumb.
     * @param g the graphics
     */
    public void paintThumb(Graphics g)  {
        Rectangle knobBounds = thumbRect;
        int w = knobBounds.width;
        int h = knobBounds.height;

        g.translate(knobBounds.x, knobBounds.y);

        if ( slider.isEnabled() ) {
            g.setColor(slider.getBackground());
        }
        else {
            g.setColor(slider.getBackground().darker());
        }

        Boolean paintThumbArrowShape =
            (Boolean)slider.getClientProperty("Slider.paintThumbArrowShape");

        if ((!slider.getPaintTicks() && paintThumbArrowShape == null) ||
            paintThumbArrowShape == Boolean.FALSE) {

            // "plain" version
            g.fillRect(0, 0, w, h);

            g.setColor(Color.black);
            g.drawLine(0, h-1, w-1, h-1);
            g.drawLine(w-1, 0, w-1, h-1);

            g.setColor(highlightColor);
            g.drawLine(0, 0, 0, h-2);
            g.drawLine(1, 0, w-2, 0);

            g.setColor(shadowColor);
            g.drawLine(1, h-2, w-2, h-2);
            g.drawLine(w-2, 1, w-2, h-3);
        }
        else if ( slider.getOrientation() == JSlider.HORIZONTAL ) {
            int cw = w / 2;
            g.fillRect(1, 1, w-3, h-1-cw);
            Polygon p = new Polygon();
            p.addPoint(1, h-cw);
            p.addPoint(cw-1, h-1);
            p.addPoint(w-2, h-1-cw);
            g.fillPolygon(p);

            g.setColor(highlightColor);
            g.drawLine(0, 0, w-2, 0);
            g.drawLine(0, 1, 0, h-1-cw);
            g.drawLine(0, h-cw, cw-1, h-1);

            g.setColor(Color.black);
            g.drawLine(w-1, 0, w-1, h-2-cw);
            g.drawLine(w-1, h-1-cw, w-1-cw, h-1);

            g.setColor(shadowColor);
            g.drawLine(w-2, 1, w-2, h-2-cw);
            g.drawLine(w-2, h-1-cw, w-1-cw, h-2);
        }
        else {  // vertical
            int cw = h / 2;
            if(BasicGraphicsUtils.isLeftToRight(slider)) {
                  g.fillRect(1, 1, w-1-cw, h-3);
                  Polygon p = new Polygon();
                  p.addPoint(w-cw-1, 0);
                  p.addPoint(w-1, cw);
                  p.addPoint(w-1-cw, h-2);
                  g.fillPolygon(p);

                  g.setColor(highlightColor);
                  g.drawLine(0, 0, 0, h - 2);                  // left
                  g.drawLine(1, 0, w-1-cw, 0);                 // top
                  g.drawLine(w-cw-1, 0, w-1, cw);              // top slant

                  g.setColor(Color.black);
                  g.drawLine(0, h-1, w-2-cw, h-1);             // bottom
                  g.drawLine(w-1-cw, h-1, w-1, h-1-cw);        // bottom slant

                  g.setColor(shadowColor);
                  g.drawLine(1, h-2, w-2-cw,  h-2 );         // bottom
                  g.drawLine(w-1-cw, h-2, w-2, h-cw-1 );     // bottom slant
            }
            else {
                  g.fillRect(5, 1, w-1-cw, h-3);
                  Polygon p = new Polygon();
                  p.addPoint(cw, 0);
                  p.addPoint(0, cw);
                  p.addPoint(cw, h-2);
                  g.fillPolygon(p);

                  g.setColor(highlightColor);
                  g.drawLine(cw-1, 0, w-2, 0);             // top
                  g.drawLine(0, cw, cw, 0);                // top slant

                  g.setColor(Color.black);
                  g.drawLine(0, h-1-cw, cw, h-1 );         // bottom slant
                  g.drawLine(cw, h-1, w-1, h-1);           // bottom

                  g.setColor(shadowColor);
                  g.drawLine(cw, h-2, w-2,  h-2 );         // bottom
                  g.drawLine(w-1, 1, w-1,  h-2 );          // right
            }
        }

        g.translate(-knobBounds.x, -knobBounds.y);
    }

    // Used exclusively by setThumbLocation()
    private static Rectangle unionRect = new Rectangle();

    /**
     * Sets the thumb location.
     * @param x the x coordinate
     * @param y the y coordinate
     */
    public void setThumbLocation(int x, int y)  {
        unionRect.setBounds( thumbRect );

        thumbRect.setLocation( x, y );

        SwingUtilities.computeUnion( thumbRect.x, thumbRect.y, thumbRect.width, thumbRect.height, unionRect );
        slider.repaint( unionRect.x, unionRect.y, unionRect.width, unionRect.height );
    }

    /**
     * Scrolls by block.
     * @param direction the direction
     */
    public void scrollByBlock(int direction)    {
        synchronized(slider)    {
            int blockIncrement =
                (slider.getMaximum() - slider.getMinimum()) / 10;
            if (blockIncrement == 0) {
                blockIncrement = 1;
            }

            int tickSpacing = getTickSpacing();
            if (slider.getSnapToTicks()) {

                if (blockIncrement < tickSpacing) {
                    blockIncrement = tickSpacing;
                }
            }
            else {
                if (tickSpacing > 0) {
                    blockIncrement = tickSpacing;
                }
            }

            int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
            slider.setValue(slider.getValue() + delta);
        }
    }

    /**
     * Scrolls by unit.
     * @param direction the direction
     */
    public void scrollByUnit(int direction) {
        synchronized(slider)    {
            int delta = ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);

            if (slider.getSnapToTicks()) {
                delta *= getTickSpacing();
            }

            slider.setValue(slider.getValue() + delta);
        }
    }

    /**
     * This function is called when a mousePressed was detected in the track,
     * not in the thumb. The default behavior is to scroll by block. You can
     * override this method to stop it from scrolling or to add additional
     * behavior.
     *
     * @param dir the direction and number of blocks to scroll
     */
    protected void scrollDueToClickInTrack( int dir ) {
        scrollByBlock( dir );
    }

    /**
     * Returns the x position for a value.
     * @param value the value
     * @return the x position for a value
     */
    protected int xPositionForValue( int value )    {
        int min = slider.getMinimum();
        int max = slider.getMaximum();
        int trackLength = trackRect.width;
        double valueRange = (double)max - (double)min;
        double pixelsPerValue = (double)trackLength / valueRange;
        int trackLeft = trackRect.x;
        int trackRight = trackRect.x + (trackRect.width - 1);
        int xPosition;

        if ( !drawInverted() ) {
            xPosition = trackLeft;
            xPosition += Math.round( pixelsPerValue * ((double)value - min) );
        }
        else {
            xPosition = trackRight;
            xPosition -= Math.round( pixelsPerValue * ((double)value - min) );
        }

        xPosition = Math.max( trackLeft, xPosition );
        xPosition = Math.min( trackRight, xPosition );

        return xPosition;
    }

    /**
     * Returns the y position for a value.
     * @param value the value
     * @return the y position for a value
     */
    protected int yPositionForValue( int value )  {
        return yPositionForValue(value, trackRect.y, trackRect.height);
    }

    /**
     * Returns the y location for the specified value.  No checking is
     * done on the arguments.  In particular if <code>trackHeight</code> is
     * negative undefined results may occur.
     *
     * @param value the slider value to get the location for
     * @param trackY y-origin of the track
     * @param trackHeight the height of the track
     * @return the y location for the specified value of the slider
     * @since 1.6
     */
    protected int yPositionForValue(int value, int trackY, int trackHeight) {
        int min = slider.getMinimum();
        int max = slider.getMaximum();
        double valueRange = (double)max - (double)min;
        double pixelsPerValue = (double)trackHeight / valueRange;
        int trackBottom = trackY + (trackHeight - 1);
        int yPosition;

        if ( !drawInverted() ) {
            yPosition = trackY;
            yPosition += Math.round( pixelsPerValue * ((double)max - value ) );
        }
        else {
            yPosition = trackY;
            yPosition += Math.round( pixelsPerValue * ((double)value - min) );
        }

        yPosition = Math.max( trackY, yPosition );
        yPosition = Math.min( trackBottom, yPosition );

        return yPosition;
    }

    /**
     * Returns the value at the y position. If {@code yPos} is beyond the
     * track at the bottom or the top, this method sets the value to either
     * the minimum or maximum value of the slider, depending on if the slider
     * is inverted or not.
     *
     * @param yPos the location of the slider along the y axis
     * @return the value at the y position
     */
    public int valueForYPosition( int yPos ) {
        int value;
        final int minValue = slider.getMinimum();
        final int maxValue = slider.getMaximum();
        final int trackLength = trackRect.height;
        final int trackTop = trackRect.y;
        final int trackBottom = trackRect.y + (trackRect.height - 1);

        if ( yPos <= trackTop ) {
            value = drawInverted() ? minValue : maxValue;
        }
        else if ( yPos >= trackBottom ) {
            value = drawInverted() ? maxValue : minValue;
        }
        else {
            int distanceFromTrackTop = yPos - trackTop;
            double valueRange = (double)maxValue - (double)minValue;
            double valuePerPixel = valueRange / (double)trackLength;
            int valueFromTrackTop = (int)Math.round( distanceFromTrackTop * valuePerPixel );

            value = drawInverted() ? minValue + valueFromTrackTop : maxValue - valueFromTrackTop;
        }

        return value;
    }

    /**
     * Returns the value at the x position.  If {@code xPos} is beyond the
     * track at the left or the right, this method sets the value to either the
     * minimum or maximum value of the slider, depending on if the slider is
     * inverted or not.
     *
     * @param xPos the location of the slider along the x axis
     * @return the value of the x position
     */
    public int valueForXPosition( int xPos ) {
        int value;
        final int minValue = slider.getMinimum();
        final int maxValue = slider.getMaximum();
        final int trackLength = trackRect.width;
        final int trackLeft = trackRect.x;
        final int trackRight = trackRect.x + (trackRect.width - 1);

        if ( xPos <= trackLeft ) {
            value = drawInverted() ? maxValue : minValue;
        }
        else if ( xPos >= trackRight ) {
            value = drawInverted() ? minValue : maxValue;
        }
        else {
            int distanceFromTrackLeft = xPos - trackLeft;
            double valueRange = (double)maxValue - (double)minValue;
            double valuePerPixel = valueRange / (double)trackLength;
            int valueFromTrackLeft = (int)Math.round( distanceFromTrackLeft * valuePerPixel );

            value = drawInverted() ? maxValue - valueFromTrackLeft :
              minValue + valueFromTrackLeft;
        }

        return value;
    }


    private class Handler implements ChangeListener,
            ComponentListener, FocusListener, PropertyChangeListener {
        // Change Handler
        public void stateChanged(ChangeEvent e) {
            if (!isDragging) {
                calculateThumbLocation();
                slider.repaint();
            }
            lastValue = slider.getValue();
        }

        // Component Handler
        public void componentHidden(ComponentEvent e) { }
        public void componentMoved(ComponentEvent e) { }
        public void componentResized(ComponentEvent e) {
            calculateGeometry();
            slider.repaint();
        }
        public void componentShown(ComponentEvent e) { }

        // Focus Handler
        public void focusGained(FocusEvent e) { slider.repaint(); }
        public void focusLost(FocusEvent e) { slider.repaint(); }

        // Property Change Handler
        public void propertyChange(PropertyChangeEvent e) {
            String propertyName = e.getPropertyName();
            if (propertyName == "orientation" ||
                    propertyName == "inverted" ||
                    propertyName == "labelTable" ||
                    propertyName == "majorTickSpacing" ||
                    propertyName == "minorTickSpacing" ||
                    propertyName == "paintTicks" ||
                    propertyName == "paintTrack" ||
                    propertyName == "font" ||
                    SwingUtilities2.isScaleChanged(e) ||
                    propertyName == "paintLabels" ||
                    propertyName == "Slider.paintThumbArrowShape") {
                checkedLabelBaselines = false;
                calculateGeometry();
                slider.repaint();
            } else if (propertyName == "componentOrientation") {
                calculateGeometry();
                slider.repaint();
                InputMap km = getInputMap(JComponent.WHEN_FOCUSED, slider);
                SwingUtilities.replaceUIInputMap(slider,
                    JComponent.WHEN_FOCUSED, km);
            } else if (propertyName == "model") {
                ((BoundedRangeModel)e.getOldValue()).removeChangeListener(
                    changeListener);
                ((BoundedRangeModel)e.getNewValue()).addChangeListener(
                    changeListener);
                calculateThumbLocation();
                slider.repaint();
            }
        }
    }

    /////////////////////////////////////////////////////////////////////////
    /// Model Listener Class
    /////////////////////////////////////////////////////////////////////////
    /**
     * Data model listener.
     *
     * This class should be treated as a &quot;protected&quot; inner class.
     * Instantiate it only within subclasses of <code>Foo</code>.
     */
    public class ChangeHandler implements ChangeListener {
        // NOTE: This class exists only for backward compatibility. All
        // its functionality has been moved into Handler. If you need to add
        // new functionality add it to the Handler, but make sure this
        // class calls into the Handler.
        public void stateChanged(ChangeEvent e) {
            getHandler().stateChanged(e);
        }
    }

    /////////////////////////////////////////////////////////////////////////
    /// Track Listener Class
    /////////////////////////////////////////////////////////////////////////
    /**
     * Track mouse movements.
     *
     * This class should be treated as a &quot;protected&quot; inner class.
     * Instantiate it only within subclasses of <code>Foo</code>.
     */
    public class TrackListener extends MouseInputAdapter {
        /** The offset */
        protected transient int offset;
        /** Current mouse x. */
        protected transient int currentMouseX;
        /** Current mouse y. */
        protected transient int currentMouseY;

        /**
         * {@inheritDoc}
         */
        public void mouseReleased(MouseEvent e) {
            if (!slider.isEnabled()) {
                return;
            }

            offset = 0;
            scrollTimer.stop();

            isDragging = false;
            slider.setValueIsAdjusting(false);
            slider.repaint();
        }

        /**
        * If the mouse is pressed above the "thumb" component
        * then reduce the scrollbars value by one page ("page up"),
        * otherwise increase it by one page.  If there is no
        * thumb then page up if the mouse is in the upper half
        * of the track.
        */
        public void mousePressed(MouseEvent e) {
            if (!slider.isEnabled()) {
                return;
            }

            // We should recalculate geometry just before
            // calculation of the thumb movement direction.
            // It is important for the case, when JSlider
            // is a cell editor in JTable. See 6348946.
            calculateGeometry();

            currentMouseX = e.getX();
            currentMouseY = e.getY();

            if (slider.isRequestFocusEnabled()) {
                slider.requestFocus();
            }

            // Clicked in the Thumb area?
            if (thumbRect.contains(currentMouseX, currentMouseY)) {
                if (UIManager.getBoolean("Slider.onlyLeftMouseButtonDrag")
                        && !SwingUtilities.isLeftMouseButton(e)) {
                    return;
                }

                switch (slider.getOrientation()) {
                case JSlider.VERTICAL:
                    offset = currentMouseY - thumbRect.y;
                    break;
                case JSlider.HORIZONTAL:
                    offset = currentMouseX - thumbRect.x;
                    break;
                }
                isDragging = true;
                return;
            }

            if (!SwingUtilities.isLeftMouseButton(e)) {
                return;
            }

            isDragging = false;
            slider.setValueIsAdjusting(true);

            Dimension sbSize = slider.getSize();
            int direction = POSITIVE_SCROLL;

            switch (slider.getOrientation()) {
            case JSlider.VERTICAL:
                if ( thumbRect.isEmpty() ) {
                    int scrollbarCenter = sbSize.height / 2;
                    if ( !drawInverted() ) {
                        direction = (currentMouseY < scrollbarCenter) ?
                            POSITIVE_SCROLL : NEGATIVE_SCROLL;
                    }
                    else {
                        direction = (currentMouseY < scrollbarCenter) ?
                            NEGATIVE_SCROLL : POSITIVE_SCROLL;
                    }
                }
                else {
                    int thumbY = thumbRect.y;
                    if ( !drawInverted() ) {
                        direction = (currentMouseY < thumbY) ?
                            POSITIVE_SCROLL : NEGATIVE_SCROLL;
                    }
                    else {
                        direction = (currentMouseY < thumbY) ?
                            NEGATIVE_SCROLL : POSITIVE_SCROLL;
                    }
                }
                break;
            case JSlider.HORIZONTAL:
                if ( thumbRect.isEmpty() ) {
                    int scrollbarCenter = sbSize.width / 2;
                    if ( !drawInverted() ) {
                        direction = (currentMouseX < scrollbarCenter) ?
                            NEGATIVE_SCROLL : POSITIVE_SCROLL;
                    }
                    else {
                        direction = (currentMouseX < scrollbarCenter) ?
                            POSITIVE_SCROLL : NEGATIVE_SCROLL;
                    }
                }
                else {
                    int thumbX = thumbRect.x;
                    if ( !drawInverted() ) {
                        direction = (currentMouseX < thumbX) ?
                            NEGATIVE_SCROLL : POSITIVE_SCROLL;
                    }
                    else {
                        direction = (currentMouseX < thumbX) ?
                            POSITIVE_SCROLL : NEGATIVE_SCROLL;
                    }
                }
                break;
            }

            if (shouldScroll(direction)) {
                scrollDueToClickInTrack(direction);
            }
            if (shouldScroll(direction)) {
                scrollTimer.stop();
                scrollListener.setDirection(direction);
                scrollTimer.start();
            }
        }

        /**
         * Returns if scrolling should occur
         * @param direction the direction.
         * @return if scrolling should occur
         */
        public boolean shouldScroll(int direction) {
            Rectangle r = thumbRect;
            if (slider.getOrientation() == JSlider.VERTICAL) {
                if (drawInverted() ? direction < 0 : direction > 0) {
                    if (r.y  <= currentMouseY) {
                        return false;
                    }
                }
                else if (r.y + r.height >= currentMouseY) {
                    return false;
                }
            }
            else {
                if (drawInverted() ? direction < 0 : direction > 0) {
                    if (r.x + r.width  >= currentMouseX) {
                        return false;
                    }
                }
                else if (r.x <= currentMouseX) {
                    return false;
                }
            }

            if (direction > 0 && slider.getValue() + slider.getExtent() >=
                    slider.getMaximum()) {
                return false;
            }
            else if (direction < 0 && slider.getValue() <=
                    slider.getMinimum()) {
                return false;
            }

            return true;
        }

        /**
         * Set the models value to the position of the top/left
         * of the thumb relative to the origin of the track.
         */
        public void mouseDragged(MouseEvent e) {
            int thumbMiddle;

            if (!slider.isEnabled()) {
                return;
            }

            currentMouseX = e.getX();
            currentMouseY = e.getY();

            if (!isDragging) {
                return;
            }

            slider.setValueIsAdjusting(true);

            switch (slider.getOrientation()) {
            case JSlider.VERTICAL:
                int halfThumbHeight = thumbRect.height / 2;
                int thumbTop = e.getY() - offset;
                int trackTop = trackRect.y;
                int trackBottom = trackRect.y + (trackRect.height - 1);
                int vMax = yPositionForValue(slider.getMaximum() -
                                            slider.getExtent());

                if (drawInverted()) {
                    trackBottom = vMax;
                }
                else {
                    trackTop = vMax;
                }
                thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
                thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);

                setThumbLocation(thumbRect.x, thumbTop);

                thumbMiddle = thumbTop + halfThumbHeight;
                slider.setValue( valueForYPosition( thumbMiddle ) );
                break;
            case JSlider.HORIZONTAL:
                int halfThumbWidth = thumbRect.width / 2;
                int thumbLeft = e.getX() - offset;
                int trackLeft = trackRect.x;
                int trackRight = trackRect.x + (trackRect.width - 1);
                int hMax = xPositionForValue(slider.getMaximum() -
                                            slider.getExtent());

                if (drawInverted()) {
                    trackLeft = hMax;
                }
                else {
                    trackRight = hMax;
                }
                thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
                thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);

                setThumbLocation(thumbLeft, thumbRect.y);

                thumbMiddle = thumbLeft + halfThumbWidth;
                slider.setValue(valueForXPosition(thumbMiddle));
                break;
            }
        }

        /** {@inheritDoc} */
        public void mouseMoved(MouseEvent e) { }
    }

    /**
     * Scroll-event listener.
     *
     * This class should be treated as a &quot;protected&quot; inner class.
     * Instantiate it only within subclasses of <code>Foo</code>.
     */
    public class ScrollListener implements ActionListener {
        // changed this class to public to avoid bogus IllegalAccessException
        // bug in InternetExplorer browser.  It was protected.  Work around
        // for 4109432
        int direction = POSITIVE_SCROLL;
        boolean useBlockIncrement;

        /**
         * Constructs a {@code ScrollListener}
         */
        public ScrollListener() {
            direction = POSITIVE_SCROLL;
            useBlockIncrement = true;
        }

        /**
         * Constructs a {@code ScrollListener}
         * @param dir the direction
         * @param block whether or not to scroll by block
         */
        public ScrollListener(int dir, boolean block)   {
            direction = dir;
            useBlockIncrement = block;
        }

        /**
         * Sets the direction.
         * @param direction the new direction
         */
        public void setDirection(int direction) {
            this.direction = direction;
        }

        /**
         * Sets scrolling by block
         * @param block the new scroll by block value
         */
        public void setScrollByBlock(boolean block) {
            this.useBlockIncrement = block;
        }

        /** {@inheritDoc} */
        public void actionPerformed(ActionEvent e) {
            if (useBlockIncrement) {
                scrollByBlock(direction);
            }
            else {
                scrollByUnit(direction);
            }
            if (!trackListener.shouldScroll(direction)) {
                ((Timer)e.getSource()).stop();
            }
        }
    }

    /**
     * Listener for resizing events.
     * <p>
     * This class should be treated as a &quot;protected&quot; inner class.
     * Instantiate it only within subclasses of <code>Foo</code>.
     */
    public class ComponentHandler extends ComponentAdapter {
        // NOTE: This class exists only for backward compatibility. All
        // its functionality has been moved into Handler. If you need to add
        // new functionality add it to the Handler, but make sure this
        // class calls into the Handler.
        public void componentResized(ComponentEvent e)  {
            getHandler().componentResized(e);
        }
    }

    /**
     * Focus-change listener.
     * <p>
     * This class should be treated as a &quot;protected&quot; inner class.
     * Instantiate it only within subclasses of <code>Foo</code>.
     */
    public class FocusHandler implements FocusListener {
        // NOTE: This class exists only for backward compatibility. All
        // its functionality has been moved into Handler. If you need to add
        // new functionality add it to the Handler, but make sure this
        // class calls into the Handler.
        public void focusGained(FocusEvent e) {
            getHandler().focusGained(e);
        }

        public void focusLost(FocusEvent e) {
            getHandler().focusLost(e);
        }
    }

    /**
     * As of Java 2 platform v1.3 this undocumented class is no longer used.
     * The recommended approach to creating bindings is to use a
     * combination of an <code>ActionMap</code>, to contain the action,
     * and an <code>InputMap</code> to contain the mapping from KeyStroke
     * to action description. The InputMap is usually described in the
     * LookAndFeel tables.
     * <p>
     * Please refer to the key bindings specification for further details.
     * <p>
     * This class should be treated as a &quot;protected&quot; inner class.
     * Instantiate it only within subclasses of <code>Foo</code>.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    public class ActionScroller extends AbstractAction {
        // NOTE: This class exists only for backward compatibility. All
        // its functionality has been moved into Actions. If you need to add
        // new functionality add it to the Actions, but make sure this
        // class calls into the Actions.
        int dir;
        boolean block;
        JSlider slider;

        /**
         * Constructs an {@code ActionScroller}.
         * @param slider a slider
         * @param dir the direction
         * @param block block scrolling or not
         */
        public ActionScroller( JSlider slider, int dir, boolean block) {
            this.dir = dir;
            this.block = block;
            this.slider = slider;
        }

        /** {@inheritDoc} */
        public void actionPerformed(ActionEvent e) {
            SHARED_ACTION.scroll(slider, BasicSliderUI.this, dir, block);
        }

        /** {@inheritDoc} */
        public boolean isEnabled() {
            boolean b = true;
            if (slider != null) {
                b = slider.isEnabled();
            }
            return b;
        }

    }


    /**
     * A static version of the above.
     */
    @SuppressWarnings("serial") // Superclass is not serializable across versions
    static class SharedActionScroller extends AbstractAction {
        // NOTE: This class exists only for backward compatibility. All
        // its functionality has been moved into Actions. If you need to add
        // new functionality add it to the Actions, but make sure this
        // class calls into the Actions.
        int dir;
        boolean block;

        public SharedActionScroller(int dir, boolean block) {
            this.dir = dir;
            this.block = block;
        }

        public void actionPerformed(ActionEvent evt) {
            JSlider slider = (JSlider)evt.getSource();
            BasicSliderUI ui = (BasicSliderUI)BasicLookAndFeel.getUIOfType(
                    slider.getUI(), BasicSliderUI.class);
            if (ui == null) {
                return;
            }
            SHARED_ACTION.scroll(slider, ui, dir, block);
        }
    }

    private static class Actions extends UIAction {
        public static final String POSITIVE_UNIT_INCREMENT =
            "positiveUnitIncrement";
        public static final String POSITIVE_BLOCK_INCREMENT =
            "positiveBlockIncrement";
        public static final String NEGATIVE_UNIT_INCREMENT =
            "negativeUnitIncrement";
        public static final String NEGATIVE_BLOCK_INCREMENT =
            "negativeBlockIncrement";
        public static final String MIN_SCROLL_INCREMENT = "minScroll";
        public static final String MAX_SCROLL_INCREMENT = "maxScroll";


        Actions() {
            super(null);
        }

        public Actions(String name) {
            super(name);
        }

        public void actionPerformed(ActionEvent evt) {
            JSlider slider = (JSlider)evt.getSource();
            BasicSliderUI ui = (BasicSliderUI)BasicLookAndFeel.getUIOfType(
                     slider.getUI(), BasicSliderUI.class);
            String name = getName();

            if (ui == null) {
                return;
            }
            if (POSITIVE_UNIT_INCREMENT == name) {
                scroll(slider, ui, POSITIVE_SCROLL, false);
            } else if (NEGATIVE_UNIT_INCREMENT == name) {
                scroll(slider, ui, NEGATIVE_SCROLL, false);
            } else if (POSITIVE_BLOCK_INCREMENT == name) {
                scroll(slider, ui, POSITIVE_SCROLL, true);
            } else if (NEGATIVE_BLOCK_INCREMENT == name) {
                scroll(slider, ui, NEGATIVE_SCROLL, true);
            } else if (MIN_SCROLL_INCREMENT == name) {
                scroll(slider, ui, MIN_SCROLL, false);
            } else if (MAX_SCROLL_INCREMENT == name) {
                scroll(slider, ui, MAX_SCROLL, false);
            }
        }

        private void scroll(JSlider slider, BasicSliderUI ui, int direction,
                boolean isBlock) {
            boolean invert = slider.getInverted();

            if (direction == NEGATIVE_SCROLL || direction == POSITIVE_SCROLL) {
                if (invert) {
                    direction = (direction == POSITIVE_SCROLL) ?
                        NEGATIVE_SCROLL : POSITIVE_SCROLL;
                }

                if (isBlock) {
                    ui.scrollByBlock(direction);
                } else {
                    ui.scrollByUnit(direction);
                }
            } else {  // MIN or MAX
                if (invert) {
                    direction = (direction == MIN_SCROLL) ?
                        MAX_SCROLL : MIN_SCROLL;
                }

                slider.setValue((direction == MIN_SCROLL) ?
                    slider.getMinimum() : slider.getMaximum());
            }
        }
    }
}
