/*
 * @(#)MultiThumbSliderUI.java
 *
 * $Date: 2014-06-06 20:04:49 +0200 (P, 06 jún. 2014) $
 *
 * 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 com.bric.plaf;

import com.bric.swing.MultiThumbSlider;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import static java.awt.RenderingHints.KEY_ANTIALIASING;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_OFF;
import static java.awt.RenderingHints.VALUE_ANTIALIAS_ON;

/**
 * This is the abstract UI for <code>MultiThumbSliders</code>
 */
public abstract class MultiThumbSliderUI extends ComponentUI implements MouseListener, MouseMotionListener {
    protected final MultiThumbSlider slider;

    /**
     * The maximum width returned by <code>getMaximumSize()</code>.
     * (or if the slider is vertical, this is the maximum height.)
     */
    static final int MAX_LENGTH = 300;

    /**
     * The minimum width returned by <code>getMinimumSize()</code>.
     * (or if the slider is vertical, this is the minimum height.)
     */
    static final int MIN_LENGTH = 50;

    /**
     * The maximum width returned by <code>getPreferredSize()</code>.
     * (or if the slider is vertical, this is the preferred height.)
     */
    static final int PREF_LENGTH = 140;

    /**
     * The height of a horizontal slider -- or width of a vertical slider.
     */
    static final int DEPTH = 15;

    /**
     * The pixel position of the thumbs.  This may be x or y coordinates, depending on
     * whether this slider is horizontal or vertical
     */
    int[] thumbPositions = new int[0];

    /**
     * A float from zero to one, indicating whether that thumb should be highlighted
     * or not.
     */
    protected float[] thumbIndications = new float[0];

    /**
     * This is used by the animating thread.  The field indication is updated until it equals this value.
     */
    private float indicationGoal = 0;

    /**
     * The overall indication of the thumbs.  At one they should be opaque,
     * at zero they should be transparent.
     */
    float indication = 0;

    /**
     * The rectangle the track should be painted in.
     */
    protected Rectangle trackRect = new Rectangle(0, 0, 0, 0);

    protected MultiThumbSliderUI(MultiThumbSlider slider) {
        this.slider = slider;
    }

    @Override
    public Dimension getMaximumSize(JComponent s) {
        MultiThumbSlider mySlider = (MultiThumbSlider) s;
        if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            return new Dimension(MAX_LENGTH, DEPTH);
        }
        return new Dimension(DEPTH, MAX_LENGTH);
    }

    @Override
    public Dimension getMinimumSize(JComponent s) {
        MultiThumbSlider mySlider = (MultiThumbSlider) s;
        if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            return new Dimension(MIN_LENGTH, DEPTH);
        }
        return new Dimension(DEPTH, MIN_LENGTH);
    }

    @Override
    public Dimension getPreferredSize(JComponent s) {
        MultiThumbSlider mySlider = (MultiThumbSlider) s;
        if (mySlider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            return new Dimension(PREF_LENGTH, DEPTH);
        }
        return new Dimension(DEPTH, PREF_LENGTH);
    }

    /**
     * This records the positions/values of each thumb.
     * This is used when the mouse is pressed, so as the mouse
     * is dragged values can get replaced and rearranged freely.
     * (Including removing and adding thumbs)
     */
    class State {
        Object[] values;
        float[] positions;
        int selectedThumb;

        public State() {
            values = slider.getValues();
            positions = slider.getThumbPositions();
            selectedThumb = slider.getSelectedThumb(false);
        }

        public State(State s) {
            selectedThumb = s.selectedThumb;
            positions = new float[s.positions.length];
            values = new Object[s.values.length];
            System.arraycopy(s.positions, 0, positions, 0, positions.length);
            System.arraycopy(s.values, 0, values, 0, values.length);
        }

        /**
         * Strip values outside of [0,1]
         */
        private void polish() {
            while (positions[0] < 0) {
                float[] f2 = new float[positions.length - 1];
                System.arraycopy(positions, 1, f2, 0, positions.length - 1);
                Object[] c2 = new Object[values.length - 1];
                System.arraycopy(values, 1, c2, 0, positions.length - 1);
                positions = f2;
                values = c2;
                selectedThumb++;
            }
            while (positions[positions.length - 1] > 1) {
                float[] f2 = new float[positions.length - 1];
                System.arraycopy(positions, 0, f2, 0, positions.length - 1);
                Object[] c2 = new Object[values.length - 1];
                System.arraycopy(values, 0, c2, 0, positions.length - 1);
                positions = f2;
                values = c2;
                selectedThumb--;
            }
            if (selectedThumb >= positions.length) {
                selectedThumb = -1;
            }
        }

        /**
         * Make the slider reflect this object
         */
        public void install() {
            polish();

            slider.setValues(positions, values);
            slider.setSelectedThumb(selectedThumb);
        }

        public void removeThumb(int index) {
            float[] f = new float[positions.length - 1];
            Object[] c = new Object[values.length - 1];
            System.arraycopy(positions, 0, f, 0, index);
            System.arraycopy(values, 0, c, 0, index);
            System.arraycopy(positions, index + 1, f, index, f.length - index);
            System.arraycopy(values, index + 1, c, index, f.length - index);
            positions = f;
            values = c;
            selectedThumb = -1;
        }
    }

    Thread animatingThread = null;

    final Runnable animatingRunnable = new Runnable() {
        @Override
        public void run() {
            boolean finished = false;
            while (!finished) {
                synchronized (MultiThumbSliderUI.this) {
                    finished = true;
                    for (int a = 0; a < thumbIndications.length; a++) {
                        if (a != slider.getSelectedThumb()) {
                            if (a == currentIndicatedThumb) {
                                if (thumbIndications[a] < 1) {
                                    thumbIndications[a] = Math.min(1, thumbIndications[a] + 0.025f);
                                    finished = false;
                                }
                            } else {
                                if (thumbIndications[a] > 0) {
                                    thumbIndications[a] = Math.max(0, thumbIndications[a] - 0.025f);
                                    finished = false;
                                }
                            }
                        } else {
                            //the selected thumb is painted as selected,
                            //so there's no indication to animate.
                            //just set the indication to whatever it should
                            //be and move on.  No repainting.
                            if (a == currentIndicatedThumb) {
                                thumbIndications[a] = 1;
                            } else {
                                thumbIndications[a] = 0;
                            }
                        }
                    }
                    if (indicationGoal > indication + 0.01f) {
                        if (indication < 0.99f) {
                            indication = Math.min(1, indication + 0.1f);
                            finished = false;
                        }
                    } else if (indicationGoal < indication - 0.01f) {
                        if (indication > 0.01f) {
                            indication = Math.max(0, indication - 0.1f);
                            finished = false;
                        }
                    }
                }
                if (!finished) {
                    slider.repaint();
                }

                //rest a little bit
                long t = System.currentTimeMillis();
                while (System.currentTimeMillis() - t < 20) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        Thread.yield();
                    }
                }
            }
        }
    };

    private int currentIndicatedThumb = -1;
    private boolean mouseInside = false;
    private boolean mouseIsDown = false;
    private State pressedState;
    private int dx, dy;

    @Override
    public void mousePressed(MouseEvent e) {
        dx = 0;
        dy = 0;

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

        if (e.getClickCount() >= 2) {
            if (slider.doDoubleClick(e.getX(), e.getY())) {
                e.consume();
                return;
            }
        } else if (e.isPopupTrigger()) {
            int x = e.getX();
            int y = e.getY();
            if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
                if (x < trackRect.x || x > trackRect.x + trackRect.width) {
                    return;
                }
                y = trackRect.y + trackRect.height;
            } else {
                if (y < trackRect.y || y > trackRect.y + trackRect.height) {
                    return;
                }
                x = trackRect.x + trackRect.width;
            }
            if (slider.doPopup(x, y)) {
                e.consume();
                return;
            }
        }
        mouseIsDown = true;
        mouseMoved(e);

        if (e.getSource() != slider) {
            throw new RuntimeException("only install this UI on the GradientSlider it was constructed with");
        }
        slider.requestFocus();

        int index = getIndex(e);
        if (index != -1) {
            if (slider.getOrientation() == SwingConstants.HORIZONTAL) {
                dx = -e.getX() + thumbPositions[index];
            } else {
                dy = -e.getY() + thumbPositions[index];
            }
        }

        if (index != -1) {
            slider.setSelectedThumb(index);
            e.consume();
        } else {
            if (slider.isAutoAdding()) {
                float k;

                int v;
                if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
                    v = e.getX();
                } else {
                    v = e.getY();
                }

                if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
                    k = ((float) (v - trackRect.x)) / ((float) trackRect.width);
                    if (slider.isInverted()) {
                        k = 1 - k;
                    }
                } else {
                    k = ((float) (v - trackRect.y)) / ((float) trackRect.height);
                    if (!slider.isInverted()) {
                        k = 1 - k;
                    }
                }
                if (k > 0 && k < 1) {
                    int added = slider.addThumb(k);
                    slider.setSelectedThumb(added);
                }
                e.consume();
            } else {
                if (slider.getSelectedThumb() != -1) {
                    slider.setSelectedThumb(-1);
                    e.consume();
                }
            }
        }
        pressedState = new State();
    }

    private int getIndex(MouseEvent e) {
        int v;
        if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            v = e.getX();
            if (v < trackRect.x - getClickLocationTolerance() + 1 || v > trackRect.x + trackRect.width + getClickLocationTolerance() - 1) {
                return -1; // didn't click in the track;
            }
        } else {
            v = e.getY();
            if (v < trackRect.y - getClickLocationTolerance() + 1 || v > trackRect.y + trackRect.height + getClickLocationTolerance() - 1) {
                return -1;
            }
        }
        int min = Math.abs(v - thumbPositions[0]);
        int minIndex = 0;
        for (int a = 1; a < thumbPositions.length; a++) {
            int distance = Math.abs(v - thumbPositions[a]);
            if (distance < min) {
                min = distance;
                minIndex = a;
            }
        }
        if (min < getClickLocationTolerance()) {
            return minIndex;
        }
        return -1;
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        mouseMoved(e);
    }

    @Override
    public void mouseExited(MouseEvent e) {
        setCurrentIndicatedThumb(-1);
        setMouseInside(false);
    }

    @Override
    public void mouseClicked(MouseEvent e) {
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        if (!slider.isEnabled()) {
            return;
        }

        int i = getIndex(e);
        setCurrentIndicatedThumb(i);
        boolean b = (e.getX() >= 0 && e.getX() < slider.getWidth() && e.getY() >= 0 && e.getY() < slider.getHeight());
        if (mouseIsDown) {
            b = true;
        }
        setMouseInside(b);
    }

    private void setCurrentIndicatedThumb(int i) {
        if (getProperty(slider, "MultiThumbSlider.indicateThumb", "true").equals("false")) {
            //never activate a specific thumb
            i = -1;
        }
        currentIndicatedThumb = i;
        boolean finished = true;
        for (int a = 0; a < thumbIndications.length; a++) {
            if (a == currentIndicatedThumb) {
                if (thumbIndications[a] != 1) {
                    finished = false;
                }
            } else {
                if (thumbIndications[a] != 0) {
                    finished = false;
                }
            }
        }
        if (!finished) {
            synchronized (this) {
                if (animatingThread == null || !animatingThread.isAlive()) {
                    animatingThread = new Thread(animatingRunnable);
                    animatingThread.start();
                }
            }
        }
    }

    private void setMouseInside(boolean b) {
        mouseInside = b;
        updateIndication();
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        if (!slider.isEnabled()) {
            return;
        }

        e.translatePoint(dx, dy);

        mouseMoved(e);
        if (pressedState != null && pressedState.selectedThumb != -1) {
            slider.setValueIsAdjusting(true);

            State newState = new State(pressedState);
            float v;
            boolean outside;
            if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
                v = ((float) (e.getX() - trackRect.x)) / ((float) trackRect.width);
                if (slider.isInverted()) {
                    v = 1 - v;
                }
                outside = (e.getY() < trackRect.y - 10) || (e.getY() > trackRect.y + trackRect.height + 10);

                //don't whack the thumb off the slider if you happen to be *near* the edge:
                if (e.getX() > trackRect.x - 10 && e.getX() < trackRect.x + trackRect.width + 10) {
                    if (v < 0) {
                        v = 0;
                    }
                    if (v > 1) {
                        v = 1;
                    }
                }
            } else {
                v = ((float) (e.getY() - trackRect.y)) / ((float) trackRect.height);
                if (!slider.isInverted()) {
                    v = 1 - v;
                }
                outside = (e.getX() < trackRect.x - 10) || (e.getX() > trackRect.x + trackRect.width + 10);

                if (e.getY() > trackRect.y - 10 && e.getY() < trackRect.y + trackRect.height + 10) {
                    if (v < 0) {
                        v = 0;
                    }
                    if (v > 1) {
                        v = 1;
                    }
                }
            }
            if (newState.positions.length <= 2) {
                outside = false; //I don't care if you are outside: no removing!
            }
            newState.positions[newState.selectedThumb] = v;

            //because we delegate mouseReleased() to this method:
            if (outside) {
                newState.removeThumb(newState.selectedThumb);
            }
            if (validatePositions(newState)) {
                newState.install();
            }
            e.consume();
        }
    }

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

        mouseIsDown = false;
        if (pressedState != null && slider.getThumbCount() <= pressedState.positions.length) {
            mouseDragged(e); //go ahead and commit this final location
        }
        if (slider.isValueAdjusting()) {
            slider.setValueIsAdjusting(false);
        }

        if (e.isPopupTrigger() && slider.doPopup(e.getX(), e.getY())) {
            //on windows popuptriggers happen on mouseRelease
            e.consume();
            return;
        }
    }

    /**
     * This retrieves a property.
     * If the component has this property manually set (by calling
     * <code>component.putClientProperty()</code>), then that value will be returned.
     * Otherwise this method refers to <code>UIManager.get()</code>.  If that
     * value is missing, this returns <code>defaultValue</code>
     *
     * @param jc
     * @param propertyName the property name
     * @param defaultValue if no other value is found, this is returned
     * @return the property value
     */
    public static String getProperty(JComponent jc, String propertyName, String defaultValue) {
        Object jcValue = jc.getClientProperty(propertyName);
        if (jcValue != null) {
            return jcValue.toString();
        }
        Object uiValue = UIManager.get(propertyName);
        if (uiValue != null) {
            return uiValue.toString();
        }
        return defaultValue;
    }

    /**
     * How many pixels can you deviate from a thumb and and still "click" it.
     */
    public abstract int getClickLocationTolerance();

    /**
     * Makes sure the thumbs are in the right order.
     *
     * @param state
     * @return true if the thumbs are valid.  False if there are two
     * thumbs with the same value (this is not allowed)
     */
    protected static boolean validatePositions(State state) {
        float[] p = state.positions;
        Object[] c = state.values;

        /* Don't let the user position a thumb outside of
          [0,1] if there are only 2 colors:
          colors outside [0,1] are deleted, and we can't delete
          colors so we get less than 2.
         */
        if (p.length <= 2) {
            /* Since the user can only manipulate 1 thumb at a time,
              only 1 thumb should be outside the domain of [0,1].
              So we *don't* have to reorganize c when we change p
             */
            for (int a = 0; a < p.length; a++) {
                if (p[a] < 0) {
                    p[a] = 0;
                } else if (p[a] > 1) {
                    p[a] = 1;
                }
            }
        }

        //validate the new positions:
        boolean checkAgain = true;
        while (checkAgain) {
            checkAgain = false;
            for (int a = 0; a < p.length - 1; a++) {
                if (p[a] == p[a + 1]) {
                    return false; //we can't make two equal
                }
                if (p[a] > p[a + 1]) {
                    checkAgain = true;

                    float swap1 = p[a];
                    p[a] = p[a + 1];
                    p[a + 1] = swap1;
                    Object swap2 = c[a];
                    c[a] = c[a + 1];
                    c[a + 1] = swap2;

                    if (a == state.selectedThumb) {
                        state.selectedThumb = a + 1;
                    } else if (a + 1 == state.selectedThumb) {
                        state.selectedThumb = a;
                    }
                }
            }
        }

        return true;
    }

    final FocusListener focusListener = new FocusListener() {
        @Override
        public void focusLost(FocusEvent e) {
            Component c = (Component) e.getSource();
            if (getProperty(slider, "MultiThumbSlider.indicateComponent", "true").equals("true")) {
                slider.setSelectedThumb(-1);
            }
            updateIndication();
            c.repaint();
        }

        @Override
        public void focusGained(FocusEvent e) {
            Component c = (Component) e.getSource();
            int i = slider.getSelectedThumb(false);
            if (i == -1) {
                int direction = 1;
                if (slider.getOrientation() == MultiThumbSlider.VERTICAL) {
                    direction *= -1;
                }
                if (slider.isInverted()) {
                    direction *= -1;
                }
                slider.setSelectedThumb((direction == 1) ? 0 : slider.getThumbCount() - 1);
            }
            updateIndication();
            c.repaint();
        }
    };

    /**
     * This will try to add a thumb between index1 and index2.
     * <P>This method will not add a thumb if there is already a very
     * small distance between these two endpoints
     *
     * @param index1
     * @param index2
     * @return true if a new thumb was added
     */
    protected boolean addThumb(int index1, int index2) {
        float pos1 = 0;
        float pos2 = 1;
        int min;
        int max;
        if (index1 < index2) {
            min = index1;
            max = index2;
        } else {
            min = index2;
            max = index1;
        }
        float[] positions = slider.getThumbPositions();
        if (min >= 0) {
            pos1 = positions[min];
        }
        if (max < positions.length) {
            pos2 = positions[max];
        }

        if (pos2 - pos1 < 0.05) {
            return false;
        }

        float newPosition = (pos1 + pos2) / 2.0f;
        slider.setSelectedThumb(slider.addThumb(newPosition));


        return true;
    }

    final KeyListener keyListener = new KeyListener() {
        @Override
        public void keyPressed(KeyEvent e) {
            if (!slider.isEnabled()) {
                return;
            }

            if (e.getSource() != slider) {
                throw new RuntimeException("only install this UI on the GradientSlider it was constructed with");
            }
            int i = slider.getSelectedThumb();
            int code = e.getKeyCode();
            int orientation = slider.getOrientation();
            if (i != -1 &&
                    (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_LEFT) &&
                    orientation == MultiThumbSlider.HORIZONTAL &&
                    e.getModifiersEx() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()) {
                //insert a new thumb
                int i2;
                if ((code == KeyEvent.VK_RIGHT && !slider.isInverted()) ||
                        (code == KeyEvent.VK_LEFT && slider.isInverted())) {
                    i2 = i + 1;
                } else {
                    i2 = i - 1;
                }
                addThumb(i, i2);
                e.consume();
                return;
            } else if (i != -1 &&
                    (code == KeyEvent.VK_UP || code == KeyEvent.VK_DOWN) &&
                    orientation == MultiThumbSlider.VERTICAL &&
                    e.getModifiersEx() == Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()) {
                //insert a new thumb
                int i2;
                if ((code == KeyEvent.VK_UP && !slider.isInverted()) ||
                        (code == KeyEvent.VK_DOWN && slider.isInverted())) {
                    i2 = i + 1;
                } else {
                    i2 = i - 1;
                }
                addThumb(i, i2);
                e.consume();
                return;
            } else if (code == KeyEvent.VK_DOWN &&
                    orientation == MultiThumbSlider.HORIZONTAL &&
                    i != -1) {
                //popup up!
                int x = slider.isInverted() ?
                        (int) (trackRect.x + trackRect.width * (1 - slider.getThumbPositions()[i])) :
                        (int) (trackRect.x + trackRect.width * slider.getThumbPositions()[i]);
                int y = trackRect.y + trackRect.height;
                if (slider.doPopup(x, y)) {
                    e.consume();
                    return;
                }
            } else if (code == KeyEvent.VK_RIGHT &&
                    orientation == MultiThumbSlider.VERTICAL &&
                    i != -1) {
                //popup up!
                int y = slider.isInverted() ?
                        (int) (trackRect.y + trackRect.height * slider.getThumbPositions()[i]) :
                        (int) (trackRect.y + trackRect.height * (1 - slider.getThumbPositions()[i]));
                int x = trackRect.x + trackRect.width;
                if (slider.doPopup(x, y)) {
                    e.consume();
                    return;
                }
            }
            if (i != -1) {
                //move the selected thumb
                if (code == KeyEvent.VK_RIGHT || code == KeyEvent.VK_DOWN) {
                    nudge(i, 1);
                    e.consume();
                } else if (code == KeyEvent.VK_LEFT || code == KeyEvent.VK_UP) {
                    nudge(i, -1);
                    e.consume();
                } else if (code == KeyEvent.VK_DELETE || code == KeyEvent.VK_BACK_SPACE) {
                    if (slider.getThumbCount() > 2) {
                        slider.removeThumb(i);
                        e.consume();
                    }
                } else if (code == KeyEvent.VK_SPACE || code == KeyEvent.VK_ENTER) {
                    slider.doDoubleClick(-1, -1);
                }
            }
        }

        @Override
        public void keyReleased(KeyEvent e) {
        }

        @Override
        public void keyTyped(KeyEvent e) {
        }
    };

    final PropertyChangeListener propertyListener = new PropertyChangeListener() {

        @Override
        public void propertyChange(PropertyChangeEvent e) {
            String name = e.getPropertyName();
            switch (name) {
                case MultiThumbSlider.VALUES_PROPERTY:
                case MultiThumbSlider.ORIENTATION_PROPERTY:
                case MultiThumbSlider.INVERTED_PROPERTY:
                    calculateGeometry();
                    slider.repaint();
                    break;
                case MultiThumbSlider.SELECTED_THUMB_PROPERTY:
                case MultiThumbSlider.PAINT_TICKS_PROPERTY:
                    slider.repaint();
                    break;
                case "MultiThumbSlider.indicateComponent":
                    setMouseInside(mouseInside);
                    slider.repaint();
                    break;
            }
        }

    };

    final ComponentListener compListener = new ComponentListener() {

        @Override
        public void componentHidden(ComponentEvent e) {
        }

        @Override
        public void componentMoved(ComponentEvent e) {
        }

        @Override
        public void componentResized(ComponentEvent e) {
            calculateGeometry();
            Component c = (Component) e.getSource();
            c.repaint();
        }

        @Override
        public void componentShown(ComponentEvent e) {
        }
    };

    protected void updateIndication() {
        synchronized (this) {
            if (slider.isEnabled() && (slider.hasFocus() || mouseInside)) {
                indicationGoal = 1;
            } else {
                indicationGoal = 0;
            }

            if (getProperty(slider, "MultiThumbSlider.indicateComponent", "true").equals("false")) {
                //always turn on the "indication", so controls are always visible
                indicationGoal = 1;
                if (!slider.isVisible()) { //when the component isn't yet initialized
                    indication = 1; //initialize it to fully indicated
                }
            }

            if (indication != indicationGoal) {
                if (animatingThread == null || !animatingThread.isAlive()) {
                    animatingThread = new Thread(animatingRunnable);
                    animatingThread.start();
                }
            }
        }
    }

    protected synchronized void calculateGeometry() {
        trackRect = calculateTrackRect();

        float[] pos = slider.getThumbPositions();

        if (thumbPositions.length != pos.length) {
            thumbPositions = new int[pos.length];
            thumbIndications = new float[pos.length];
        }
        if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            for (int a = 0; a < thumbPositions.length; a++) {
                if (!slider.isInverted()) {
                    thumbPositions[a] = trackRect.x + (int) (trackRect.width * pos[a]);
                } else {
                    thumbPositions[a] = trackRect.x + (int) (trackRect.width * (1 - pos[a]));
                }
                thumbIndications[a] = 0;
            }
        } else {
            for (int a = 0; a < thumbPositions.length; a++) {
                if (slider.isInverted()) {
                    thumbPositions[a] = trackRect.y + (int) (trackRect.height * pos[a]);
                } else {
                    thumbPositions[a] = trackRect.y + (int) (trackRect.height * (1 - pos[a]));
                }
                thumbIndications[a] = 0;
            }
        }
    }

    protected Rectangle calculateTrackRect() {
        Insets i = new Insets(5, 5, 5, 5);
        int w, h;
        if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            w = slider.getWidth() - i.left - i.right;
            h = Math.min(DEPTH, slider.getHeight() - i.top - i.bottom);
        } else {
            h = slider.getHeight() - i.top - i.bottom;
            w = Math.min(DEPTH, slider.getWidth() - i.left - i.right);
        }
        return new Rectangle(slider.getWidth() / 2 - w / 2, slider.getHeight() / 2 - h / 2, w, h);
    }

    private void nudge(int thumbIndex, int direction) {
        float pixelFraction;
        if (slider.getOrientation() == MultiThumbSlider.HORIZONTAL) {
            pixelFraction = 1.0f / trackRect.width;
        } else {
            pixelFraction = 1.0f / trackRect.height;
        }
        if (direction < 0) {
            pixelFraction *= -1;
        }
        if (slider.isInverted()) {
            pixelFraction *= -1;
        }
        if (slider.getOrientation() == MultiThumbSlider.VERTICAL) {
            pixelFraction *= -1;
        }

        //repeat a couple of times: it's possible we'll nudge two values
        //so they're exactly equal, which will make validate() fail.
        //in that case: move the value ANOTHER nudge to the left/right
        //to really make a change.  But make sure we still respect the [0,1] limits.
        State state = new State();
        while (state.positions[thumbIndex] >= 0 && state.positions[thumbIndex] <= 1) {
            state.positions[thumbIndex] += pixelFraction;
            if (validatePositions(state)) {
                state.install();
                return;
            }
        }
    }

    @Override
    public void installUI(JComponent slider) {
        slider.addMouseListener(this);
        slider.addMouseMotionListener(this);
        slider.addFocusListener(focusListener);
        slider.addKeyListener(keyListener);
        slider.addComponentListener(compListener);
        slider.addPropertyChangeListener(propertyListener);
    }

    @Override
    public void paint(Graphics g, JComponent slider2) {
        if (slider2 != slider) {
            throw new RuntimeException("only use this UI on the GradientSlider it was constructed with");
        }

        Graphics2D g2 = (Graphics2D) g;
        int w = slider.getWidth();
        int h = slider.getHeight();

        if (slider.isOpaque()) {
            g.setColor(slider.getBackground());
            g.fillRect(0, 0, w, h);
        }

        g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_OFF);
        paintTrack(g2);
        g2.setRenderingHint(KEY_ANTIALIASING, VALUE_ANTIALIAS_ON);
        paintFocus(g2);
        paintThumbs(g2);
    }

    protected abstract void paintTrack(Graphics2D g);

    protected abstract void paintFocus(Graphics2D g);

    protected abstract void paintThumbs(Graphics2D g);

    @Override
    public void uninstallUI(JComponent slider) {
        slider.removeMouseListener(this);
        slider.removeMouseMotionListener(this);
        slider.removeFocusListener(focusListener);
        slider.removeKeyListener(keyListener);
        slider.removeComponentListener(compListener);
        slider.removePropertyChangeListener(propertyListener);
        super.uninstallUI(slider);
    }
}