/*
 * Copyright (c) 2005-2008 Substance Kirill Grouchnikov. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of Substance Kirill Grouchnikov nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.jvnet.substance;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.EnumSet;
import java.util.Set;

import javax.swing.*;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.UIResource;
import javax.swing.plaf.basic.BasicScrollBarUI;

import org.jvnet.lafwidget.animation.*;
import org.jvnet.lafwidget.layout.TransitionLayout;
import org.jvnet.substance.api.*;
import org.jvnet.substance.api.SubstanceConstants.ScrollPaneButtonPolicyKind;
import org.jvnet.substance.api.SubstanceConstants.Side;
import org.jvnet.substance.painter.border.SimplisticSoftBorderPainter;
import org.jvnet.substance.painter.border.SubstanceBorderPainter;
import org.jvnet.substance.painter.gradient.SimplisticGradientPainter;
import org.jvnet.substance.painter.gradient.SubstanceGradientPainter;
import org.jvnet.substance.painter.utils.SubstanceFillBackgroundDelegate;
import org.jvnet.substance.shaper.ClassicButtonShaper;
import org.jvnet.substance.shaper.SubstanceButtonShaper;
import org.jvnet.substance.utils.*;
import org.jvnet.substance.utils.icon.ArrowButtonTransitionAwareIcon;
import org.jvnet.substance.utils.scroll.SubstanceScrollButton;

/**
 * UI for scroll bars in <b>Substance </b> look and feel.
 * 
 * @author Kirill Grouchnikov
 */
public class SubstanceScrollBarUI extends BasicScrollBarUI implements Trackable {
	/**
	 * The second decrease button. Is shown under
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#ADJACENT},
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE} and
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE_BOTH}
	 * modes.
	 * 
	 * @since version 3.1
	 */
	protected JButton mySecondDecreaseButton;

	/**
	 * The second increase button. Is shown only under
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE_BOTH} mode.
	 * 
	 * @since version 3.1
	 */
	protected JButton mySecondIncreaseButton;

	/**
	 * Surrogate button model for tracking the thumb transitions.
	 */
	private ButtonModel thumbModel;

	/**
	 * Stores computed images for vertical thumbs.
	 */
	private static LazyResettableHashMap<BufferedImage> thumbVerticalMap = new LazyResettableHashMap<BufferedImage>(
			"SubstanceScrollBarUI.thumbVertical");

	/**
	 * Stores computed images for horizontal thumbs.
	 */
	private static LazyResettableHashMap<BufferedImage> thumbHorizontalMap = new LazyResettableHashMap<BufferedImage>(
			"SubstanceScrollBarUI.thumbHorizontal");

	/**
	 * Stores computed images for full vertical tracks under
	 * {@link DefaultControlBackgroundComposite}.
	 */
	private static LazyResettableHashMap<BufferedImage> trackFullVerticalMap = new LazyResettableHashMap<BufferedImage>(
			"SubstanceScrollBarUI.trackFullVertical");

	/**
	 * Stores computed images for full horizontal tracks under
	 * {@link DefaultControlBackgroundComposite}.
	 */
	private static LazyResettableHashMap<BufferedImage> trackFullHorizontalMap = new LazyResettableHashMap<BufferedImage>(
			"SubstanceScrollBarUI.trackFullHorizontal");

	/**
	 * Mouse listener on the associated scroll bar.
	 */
	private MouseListener substanceMouseListener;

	/**
	 * Listener for thumb fade animations.
	 */
	private RolloverControlListener substanceThumbRolloverListener;

	/**
	 * Listener for fade animations.
	 */
	protected FadeStateListener substanceFadeStateListener;

	/**
	 * Property change listener.
	 * 
	 */
	private PropertyChangeListener substancePropertyListener;

	/**
	 * Scroll bar width.
	 */
	protected int scrollBarWidth;

	/**
	 * Cache of images for horizontal tracks.
	 */
	private static LazyResettableHashMap<BufferedImage> trackHorizontalMap = new LazyResettableHashMap<BufferedImage>(
			"SubstanceScrollBarUI.trackHorizontal");

	/**
	 * Cache of images for vertical tracks.
	 */
	private static LazyResettableHashMap<BufferedImage> trackVerticalMap = new LazyResettableHashMap<BufferedImage>(
			"SubstanceScrollBarUI.trackVertical");

	/**
	 * Listener on adjustments made to the scrollbar model - this is for the
	 * overlay mode (see {@link SubstanceLookAndFeel#OVERLAY_PROPERTY} and
	 * repaiting both scrollbars with the viewport.
	 * 
	 * @since version 3.2
	 */
	protected AdjustmentListener substanceAdjustmentListener;

	/**
	 * Surrogate model to sync between rollover effects of scroll buttons and
	 * scroll track / scroll thumb.
	 * 
	 * @since version 3.2
	 */
	protected CompositeButtonModel compositeScrollTrackModel;

	/**
	 * Surrogate model to sync between rollover effects of scroll buttons and
	 * scroll track / scroll thumb.
	 * 
	 * @since version 3.2
	 */
	protected CompositeButtonModel compositeButtonsModel;

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.ComponentUI#createUI(javax.swing.JComponent)
	 */
	public static ComponentUI createUI(JComponent comp) {
		SubstanceCoreUtilities.testComponentCreationThreadingViolation(comp);
		return new SubstanceScrollBarUI(comp);
	}

	/**
	 * Simple constructor.
	 * 
	 * @param b
	 *            Associated component.
	 */
	protected SubstanceScrollBarUI(JComponent b) {
		super();
		this.thumbModel = new DefaultButtonModel();
		this.thumbModel.setArmed(false);
		this.thumbModel.setSelected(false);
		this.thumbModel.setPressed(false);
		this.thumbModel.setRollover(false);

		b.setOpaque(false);
	}

	/**
	 * Creates a decrease button.
	 * 
	 * @param orientation
	 *            Button orientation.
	 * @param isRegular
	 *            if <code>true</code>, the regular (upper / left) decrease
	 *            button is created, if <code>false</code>, the additional
	 *            (lower / right) decrease button is created for
	 *            {@link SubstanceConstants.ScrollPaneButtonPolicyKind#ADJACENT}
	 *            ,
	 *            {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE}
	 *            and
	 *            {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE_BOTH}
	 *            kinds.
	 * @return Decrease button.
	 */
	protected JButton createGeneralDecreaseButton(final int orientation,
			boolean isRegular) {
		JButton result = new SubstanceScrollButton(orientation);
		result.setFont(this.scrollbar.getFont());
		Icon icon = new ArrowButtonTransitionAwareIcon(result, orientation);
		// new TransitionAwareIcon(result,
		// new TransitionAwareIcon.Delegate() {
		// public Icon getColorSchemeIcon(SubstanceColorScheme scheme) {
		// return SubstanceImageCreator.getArrowIcon(
		// SubstanceSizeUtils
		// .getComponentFontSize(scrollbar),
		// orientation, scheme);
		// }
		// });
		result.setIcon(icon);
		result.setFont(scrollbar.getFont());

		result.setPreferredSize(new Dimension(this.scrollBarWidth,
				this.scrollBarWidth));

		Set<Side> openSides = EnumSet.noneOf(Side.class);
		Set<Side> straightSides = EnumSet.noneOf(Side.class);
		switch (orientation) {
		case NORTH:
			openSides.add(Side.BOTTOM);
			if (!isRegular)
				openSides.add(Side.TOP);
			if (isRegular)
				straightSides.add(Side.TOP);
			break;
		case EAST:
			openSides.add(Side.LEFT);
			if (!isRegular)
				openSides.add(Side.RIGHT);
			if (isRegular)
				straightSides.add(Side.RIGHT);
			break;
		case WEST:
			openSides.add(Side.RIGHT);
			if (!isRegular)
				openSides.add(Side.LEFT);
			if (isRegular)
				straightSides.add(Side.LEFT);
			break;
		}
		result.putClientProperty(
				SubstanceLookAndFeel.BUTTON_OPEN_SIDE_PROPERTY, openSides);
		result.putClientProperty(SubstanceLookAndFeel.BUTTON_SIDE_PROPERTY,
				straightSides);

		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#createDecreaseButton(int)
	 */
	@Override
	protected JButton createDecreaseButton(int orientation) {
		return this.createGeneralDecreaseButton(orientation, true);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#createIncreaseButton(int)
	 */
	@Override
	protected JButton createIncreaseButton(int orientation) {
		return this.createGeneralIncreaseButton(orientation, true);
	}

	/**
	 * Creates a increase button.
	 * 
	 * @param orientation
	 *            Button orientation.
	 * @param isRegular
	 *            if <code>true</code>, the regular (lower / right) increase
	 *            button is created, if <code>false</code>, the additional
	 *            (upper / left) increase button is created for
	 *            {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE_BOTH}
	 *            kind.
	 * @return Increase button.
	 */
	protected JButton createGeneralIncreaseButton(final int orientation,
			boolean isRegular) {
		JButton result = new SubstanceScrollButton(orientation);
		result.setFont(this.scrollbar.getFont());
		Icon icon = new ArrowButtonTransitionAwareIcon(result, orientation);
		// Icon icon = new TransitionAwareIcon(result,
		// new TransitionAwareIcon.Delegate() {
		// public Icon getColorSchemeIcon(SubstanceColorScheme scheme) {
		// return SubstanceImageCreator.getArrowIcon(
		// SubstanceSizeUtils
		// .getComponentFontSize(scrollbar),
		// orientation, scheme);
		// }
		// });
		result.setIcon(icon);
		result.setFont(scrollbar.getFont());
		// JButton result = new SubstanceScrollBarButton(icon, orientation);
		result.setPreferredSize(new Dimension(this.scrollBarWidth,
				this.scrollBarWidth));

		Set<Side> openSides = EnumSet.noneOf(Side.class);
		Set<Side> straightSides = EnumSet.noneOf(Side.class);
		switch (orientation) {
		case SOUTH:
			openSides.add(Side.TOP);
			if (!isRegular)
				openSides.add(Side.BOTTOM);
			if (isRegular)
				straightSides.add(Side.BOTTOM);
			break;
		case EAST:
			openSides.add(Side.LEFT);
			if (!isRegular)
				openSides.add(Side.RIGHT);
			if (isRegular)
				straightSides.add(Side.RIGHT);
			break;
		case WEST:
			openSides.add(Side.RIGHT);
			if (!isRegular)
				openSides.add(Side.LEFT);
			if (isRegular)
				straightSides.add(Side.LEFT);
			break;
		}
		result.putClientProperty(
				SubstanceLookAndFeel.BUTTON_OPEN_SIDE_PROPERTY, openSides);
		result.putClientProperty(SubstanceLookAndFeel.BUTTON_SIDE_PROPERTY,
				straightSides);
		return result;
	}

	/**
	 * Returns the image for a horizontal track.
	 * 
	 * @param trackBounds
	 *            Track bounds.
	 * @param leftActiveButton
	 *            The closest left button in the scroll bar. May be
	 *            <code>null</code>.
	 * @param rightActiveButton
	 *            The closest right button in the scroll bar. May be
	 *            <code>null</code> .
	 * @return Horizontal track image.
	 */
	private BufferedImage getTrackHorizontal(Rectangle trackBounds,
			SubstanceScrollButton leftActiveButton,
			SubstanceScrollButton rightActiveButton) {
		int width = Math.max(1, trackBounds.width);
		int height = Math.max(1, trackBounds.height);

		ComponentState compLeftState = this.getState(leftActiveButton);
		ComponentState compRightState = this.getState(rightActiveButton);

		Component tracked = SubstanceFadeUtilities.getTracked(
				FadeKind.ROLLOVER, this.scrollbar, this.decrButton,
				this.incrButton, this.mySecondDecreaseButton,
				this.mySecondIncreaseButton);
		if (tracked != null) {

			ComponentState state = (tracked == this.scrollbar) ? ComponentState
					.getState(this.thumbModel, null) : ComponentState.getState(
					((AbstractButton) tracked).getModel(), null);

			float cyclePos = state.getCyclePosition();
			FadeState highest = SubstanceFadeUtilities
					.getFadeStateWithHighestFadeCycle(FadeKind.ROLLOVER,
							this.scrollbar, this.decrButton, this.incrButton,
							this.mySecondDecreaseButton,
							this.mySecondIncreaseButton);
			if (highest != null) {
				cyclePos = highest.getFadePosition();
				if (!highest.isFadingIn())
					cyclePos = 1.0f - cyclePos;
			}

			SubstanceButtonShaper shaper = SubstanceCoreUtilities
					.getButtonShaper(this.scrollbar);
			HashMapKey key = SubstanceCoreUtilities
					.getHashKey(cyclePos, width, height,
							((leftActiveButton == null) ? "null"
									: ComponentState.getState(
											leftActiveButton.getModel(),
											leftActiveButton).name()),
							((leftActiveButton == null) ? "null"
									: SubstanceCoreUtilities
											.getPrevComponentState(
													leftActiveButton).name()),
							((rightActiveButton == null) ? "null"
									: ComponentState.getState(
											rightActiveButton.getModel(),
											rightActiveButton).name()),
							((rightActiveButton == null) ? "null"
									: SubstanceCoreUtilities
											.getPrevComponentState(
													rightActiveButton).name()),
							((compLeftState == null) ? "null" : compLeftState
									.name()),
							((compRightState == null) ? "null" : compRightState
									.name()), ((compLeftState == null) ? "null"
									: SubstanceColorSchemeUtilities
											.getColorScheme(leftActiveButton,
													compLeftState)
											.getDisplayName()),
							((compRightState == null) ? "null"
									: SubstanceColorSchemeUtilities
											.getColorScheme(rightActiveButton,
													compRightState)
											.getDisplayName()),
							SubstanceColorSchemeUtilities.getColorScheme(
									this.scrollbar, ComponentState.DEFAULT)
									.getDisplayName(), shaper.getDisplayName(),
							SubstanceSizeUtils
									.getBorderStrokeWidth(SubstanceSizeUtils
											.getComponentFontSize(scrollbar)));
			// System.out.println(key);
			if (trackFullHorizontalMap.containsKey(key)) {
				// System.out.println("Cache hit");
				return trackFullHorizontalMap.get(key);
			}
			// System.out.println("Cache miss");

			// System.out.println("New image for horizontal track");
			BufferedImage imageBack = getTrackBackHorizontal(this.scrollbar,
					leftActiveButton, rightActiveButton, width, height);
			Graphics2D backGraphics = imageBack.createGraphics();

			BufferedImage imageDefault = getTrackHorizontal(this.scrollbar,
					compLeftState, compRightState, width, height);

			backGraphics.drawImage(imageDefault, 0, 0, null);
			BufferedImage imageActive = getTrackHorizontal(this.scrollbar,
					compLeftState, compRightState, width, height);
			backGraphics.setComposite(AlphaComposite.SrcOver.derive(cyclePos));
			// System.out.println("Painting " + cyclePos);
			backGraphics.drawImage(imageActive, 0, 0, null);

			trackFullHorizontalMap.put(key, imageBack);
			return imageBack;
		}

		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(this.scrollbar);
		HashMapKey key = SubstanceCoreUtilities
				.getHashKey(width, height, ((compLeftState == null) ? "null"
						: ComponentState.getState(leftActiveButton.getModel(),
								leftActiveButton).name()),
						((compLeftState == null) ? "null"
								: SubstanceCoreUtilities.getPrevComponentState(
										leftActiveButton).name()),
						((compRightState == null) ? "null" : ComponentState
								.getState(rightActiveButton.getModel(),
										rightActiveButton).name()),
						((compRightState == null) ? "null"
								: SubstanceCoreUtilities.getPrevComponentState(
										rightActiveButton).name()),
						((compLeftState == null) ? "null" : compLeftState
								.name()), ((compRightState == null) ? "null"
								: compRightState.name()),
						((compLeftState == null) ? "null"
								: SubstanceColorSchemeUtilities.getColorScheme(
										leftActiveButton, compLeftState)
										.getDisplayName()),
						((compRightState == null) ? "null"
								: SubstanceColorSchemeUtilities.getColorScheme(
										rightActiveButton, compRightState)
										.getDisplayName()),
						SubstanceColorSchemeUtilities.getColorScheme(
								this.scrollbar, ComponentState.DEFAULT)
								.getDisplayName(), shaper.getDisplayName(),
						SubstanceSizeUtils
								.getBorderStrokeWidth(SubstanceSizeUtils
										.getComponentFontSize(scrollbar)));

		// System.out.println(key);
		if (trackFullHorizontalMap.containsKey(key)) {
			// System.out.println("Cache hit");
			return trackFullHorizontalMap.get(key);
		}
		// System.out.println("Cache miss");

		// System.out.println("New image for horizontal track");
		BufferedImage trackBack = getTrackBackHorizontal(this.scrollbar,
				leftActiveButton, rightActiveButton, width, height);
		Graphics2D backGraphics = trackBack.createGraphics();

		BufferedImage scrollTrackImage = getTrackHorizontal(this.scrollbar,
				compLeftState, compRightState, width, height);
		backGraphics.drawImage(scrollTrackImage, 0, 0, null);
		backGraphics.dispose();

		// System.out.println("Cache update");
		trackFullHorizontalMap.put(key, trackBack);
		return trackBack;
	}

	/**
	 * Returns the image for a horizontal track.
	 * 
	 * @param scrollBar
	 *            Scroll bar.
	 * @param trackBounds
	 *            Track bounds.
	 * @param compLeftState
	 *            The state of the left button in the scroll bar.
	 * @param compRightState
	 *            The state of the closest right button in the scroll bar.
	 * @param width
	 *            Scroll track width.
	 * @param height
	 *            Scroll track height.
	 * @param graphicsComposite
	 *            Composite to apply before painting the track.
	 * @return Horizontal track image.
	 */
	private static BufferedImage getTrackHorizontal(JScrollBar scrollBar,
			ComponentState compLeftState, ComponentState compRightState,
			int width, int height) {
		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(scrollBar);
		SubstanceColorScheme mainScheme = SubstanceColorSchemeUtilities
				.getColorScheme(scrollBar,
						scrollBar.isEnabled() ? ComponentState.DEFAULT
								: ComponentState.DISABLED_UNSELECTED);
		HashMapKey key = SubstanceCoreUtilities.getHashKey(mainScheme
				.getDisplayName(), width, height,
				((compLeftState == null) ? "null" : compLeftState.name()),
				((compRightState == null) ? "null" : compRightState.name()),
				shaper.getDisplayName());
		float radius = height / 2;
		if (shaper instanceof ClassicButtonShaper)
			radius = SubstanceSizeUtils
					.getClassicButtonCornerRadius(SubstanceSizeUtils
							.getComponentFontSize(scrollBar));

		int borderDelta = (int) Math.floor(SubstanceSizeUtils
				.getBorderStrokeWidth(SubstanceSizeUtils
						.getComponentFontSize(scrollBar)) / 2.0);
		Shape contour = SubstanceOutlineUtilities.getBaseOutline(width, height,
				radius, null, borderDelta);
		BufferedImage result = SubstanceScrollBarUI.trackHorizontalMap.get(key);
		if (result == null) {
			result = SubstanceCoreUtilities.getBlankImage(width, height);
			SimplisticGradientPainter.INSTANCE.paintContourBackground(result
					.createGraphics(), scrollBar, width, height, contour,
					false, mainScheme, mainScheme, 0, true, false);
			// result = new SimplisticGradientPainter().getContourBackground(
			// width, height, contour, false, mainScheme, mainScheme, 0,
			// true, false);
			SubstanceBorderPainter borderPainter = new SimplisticSoftBorderPainter();
			borderPainter.paintBorder(result.getGraphics(), scrollBar, width,
					height, contour, null, mainScheme, mainScheme, 0, false);

			SubstanceScrollBarUI.trackHorizontalMap.put(key, result);
		}
		return result;
	}

	/**
	 * Returns the image for a horizontal track.
	 * 
	 * @param scrollBar
	 *            Scroll bar.
	 * @param trackBounds
	 *            Track bounds.
	 * @param leftActiveButton
	 *            The closest left button in the scroll bar. May be
	 *            <code>null</code>.
	 * @param rightActiveButton
	 *            The closest right button in the scroll bar. May be
	 *            <code>null</code> .
	 * @param width
	 *            Scroll track width.
	 * @param height
	 *            Scroll track height.
	 * @param graphicsComposite
	 *            Composite to apply before painting the track.
	 * @return Horizontal track image.
	 */
	private static BufferedImage getTrackBackHorizontal(JScrollBar scrollBar,
			AbstractButton leftActiveButton, AbstractButton rightActiveButton,
			int width, int height) {
		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(scrollBar);
		int radius = height / 2;
		if (shaper instanceof ClassicButtonShaper)
			radius = 2;
		BufferedImage opaque = SubstanceImageCreator
				.getCompositeRoundedBackground(scrollBar, width, height,
						radius, leftActiveButton, rightActiveButton, false);
		return opaque;
	}

	/**
	 * Returns the image for a vertical track.
	 * 
	 * @param trackBounds
	 *            Track bounds.
	 * @param scrollBar
	 *            Scroll bar.
	 * @param topActiveButton
	 *            The closest top button in the scroll bar. May be
	 *            <code>null</code>.
	 * @param bottomActiveButton
	 *            The closest bottom button in the scroll bar. May be
	 *            <code>null</code>.
	 * @return Vertical track image.
	 */
	private BufferedImage getTrackVertical(Rectangle trackBounds,
			SubstanceScrollButton topActiveButton,
			SubstanceScrollButton bottomActiveButton) {

		int width = Math.max(1, trackBounds.width);
		int height = Math.max(1, trackBounds.height);

		ComponentState compTopState = this.getState(topActiveButton);
		ComponentState compBottomState = this.getState(bottomActiveButton);

		Component tracked = SubstanceFadeUtilities.getTracked(
				FadeKind.ROLLOVER, this.scrollbar, this.decrButton,
				this.incrButton, this.mySecondDecreaseButton,
				this.mySecondIncreaseButton);

		if (tracked != null) {
			ComponentState state = (tracked == this.scrollbar) ? ComponentState
					.getState(this.thumbModel, null) : ComponentState.getState(
					((AbstractButton) tracked).getModel(), null);

			float cyclePos = state.getCyclePosition();
			FadeState highest = SubstanceFadeUtilities
					.getFadeStateWithHighestFadeCycle(FadeKind.ROLLOVER,
							this.scrollbar, this.decrButton, this.incrButton,
							this.mySecondDecreaseButton,
							this.mySecondIncreaseButton);
			if (highest != null) {
				cyclePos = highest.getFadePosition();
				if (!highest.isFadingIn())
					cyclePos = 1.0f - cyclePos;
			}

			SubstanceButtonShaper shaper = SubstanceCoreUtilities
					.getButtonShaper(this.scrollbar);
			HashMapKey key = SubstanceCoreUtilities.getHashKey(cyclePos, width,
					height, ((topActiveButton == null) ? "null"
							: ComponentState
									.getState(topActiveButton.getModel(),
											topActiveButton).name()),
					((topActiveButton == null) ? "null"
							: SubstanceCoreUtilities.getPrevComponentState(
									topActiveButton).name()),
					((bottomActiveButton == null) ? "null" : ComponentState
							.getState(bottomActiveButton.getModel(),
									bottomActiveButton).name()),
					((bottomActiveButton == null) ? "null"
							: SubstanceCoreUtilities.getPrevComponentState(
									bottomActiveButton).name()),
					((compTopState == null) ? "null" : compTopState.name()),
					((compBottomState == null) ? "null" : compBottomState
							.name()), ((compBottomState == null) ? "null"
							: SubstanceColorSchemeUtilities.getColorScheme(
									bottomActiveButton, compBottomState)
									.getDisplayName()),
					((compTopState == null) ? "null"
							: SubstanceColorSchemeUtilities.getColorScheme(
									topActiveButton, compTopState)
									.getDisplayName()),
					SubstanceColorSchemeUtilities.getColorScheme(
							this.scrollbar, ComponentState.DEFAULT)
							.getDisplayName(), shaper.getDisplayName(),
					SubstanceSizeUtils.getBorderStrokeWidth(SubstanceSizeUtils
							.getComponentFontSize(scrollbar)));

			// System.out.println(key);
			if (trackFullVerticalMap.containsKey(key)) {
				// System.out.println("Cache hit");
				return trackFullVerticalMap.get(key);
			}
			// System.out.println("Cache miss");

			// System.out.println("New image for vertical track");
			BufferedImage imageBack = getTrackBackVertical(this.scrollbar,
					topActiveButton, bottomActiveButton, width, height);
			Graphics2D backGraphics = imageBack.createGraphics();

			BufferedImage imageDefault = getTrackVertical(this.scrollbar,
					compTopState, compBottomState, width, height);
			backGraphics.drawImage(imageDefault, 0, 0, null);
			BufferedImage imageActive = getTrackVertical(this.scrollbar,
					compTopState, compBottomState, width, height);
			backGraphics.setComposite(AlphaComposite.SrcOver.derive(cyclePos));
			// System.out.println("Painting " + cyclePos);
			backGraphics.drawImage(imageActive, 0, 0, null);

			// System.out.println("Cache update");
			trackFullVerticalMap.put(key, imageBack);
			return imageBack;
		}

		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(this.scrollbar);
		HashMapKey key = SubstanceCoreUtilities
				.getHashKey(
						width,
						height,
						((compTopState == null) ? "null" : ComponentState
								.getState(topActiveButton.getModel(),
										topActiveButton).name()),
						((compTopState == null) ? "null"
								: SubstanceCoreUtilities.getPrevComponentState(
										topActiveButton).name()),
						((compBottomState == null) ? "null" : ComponentState
								.getState(bottomActiveButton.getModel(),
										bottomActiveButton).name()),
						((compBottomState == null) ? "null"
								: SubstanceCoreUtilities.getPrevComponentState(
										bottomActiveButton).name()),
						((compTopState == null) ? "null" : compTopState.name()),
						((compBottomState == null) ? "null" : compBottomState
								.name()), ((compBottomState == null) ? "null"
								: SubstanceColorSchemeUtilities.getColorScheme(
										bottomActiveButton, compBottomState)
										.getDisplayName()),
						((compTopState == null) ? "null"
								: SubstanceColorSchemeUtilities.getColorScheme(
										topActiveButton, compTopState)
										.getDisplayName()),
						SubstanceColorSchemeUtilities.getColorScheme(
								this.scrollbar, ComponentState.DEFAULT)
								.getDisplayName(), shaper.getDisplayName(),
						SubstanceSizeUtils
								.getBorderStrokeWidth(SubstanceSizeUtils
										.getComponentFontSize(scrollbar)));
		if (trackFullVerticalMap.containsKey(key)) {
			// System.out.println("Cache hit");
			return trackFullVerticalMap.get(key);
		}

		// System.out.println("New image for vertical track");
		BufferedImage trackBack = getTrackBackVertical(this.scrollbar,
				topActiveButton, bottomActiveButton, width, height);
		Graphics2D backGraphics = trackBack.createGraphics();

		BufferedImage scrollTrackImage = getTrackVertical(this.scrollbar,
				compTopState, compBottomState, width, height);
		backGraphics.drawImage(scrollTrackImage, 0, 0, null);
		backGraphics.dispose();

		trackFullVerticalMap.put(key, trackBack);
		// System.out.println("Cache update");
		return trackBack;
	}

	/**
	 * Returns the image for a vertical track.
	 * 
	 * @param trackBounds
	 *            Track bounds.
	 * @param scrollBar
	 *            Scroll bar.
	 * @param compTopState
	 *            The state of the top button in the scroll bar.
	 * @param compBottomState
	 *            The state of the closest bottom button in the scroll bar.
	 * @param width
	 *            Scroll track width.
	 * @param height
	 *            Scroll track height.
	 * @param graphicsComposite
	 *            Composite to apply before painting the track.
	 * @return Vertical track image.
	 */
	private static BufferedImage getTrackVertical(JScrollBar scrollBar,
			ComponentState compTopState, ComponentState compBottomState,
			int width, int height) {
		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(scrollBar);
		SubstanceColorScheme mainScheme = SubstanceColorSchemeUtilities
				.getColorScheme(scrollBar,
						scrollBar.isEnabled() ? ComponentState.DEFAULT
								: ComponentState.DISABLED_UNSELECTED);
		HashMapKey key = SubstanceCoreUtilities.getHashKey(mainScheme
				.getDisplayName(), width, height,
				((compTopState == null) ? "null" : compTopState.name()),
				((compBottomState == null) ? "null" : compBottomState.name()),
				shaper.getDisplayName());
		BufferedImage result = SubstanceScrollBarUI.trackVerticalMap.get(key);
		if (result == null) {
			float radius = width / 2;
			if (shaper instanceof ClassicButtonShaper)
				radius = SubstanceSizeUtils
						.getClassicButtonCornerRadius(SubstanceSizeUtils
								.getComponentFontSize(scrollBar));

			int borderDelta = (int) Math.floor(SubstanceSizeUtils
					.getBorderStrokeWidth(SubstanceSizeUtils
							.getComponentFontSize(scrollBar)) / 2.0);
			Shape contour = SubstanceOutlineUtilities.getBaseOutline(height,
					width, radius, null, borderDelta);

			result = SubstanceCoreUtilities.getBlankImage(height, width);
			SimplisticGradientPainter.INSTANCE.paintContourBackground(result
					.createGraphics(), scrollBar, height, width, contour,
					false, mainScheme, mainScheme, 0, true, false);

			// result = new SimplisticGradientPainter().getContourBackground(
			// height, width, contour, false, mainScheme, mainScheme, 0,
			// true, false);
			SubstanceBorderPainter borderPainter = new SimplisticSoftBorderPainter();
			borderPainter.paintBorder(result.getGraphics(), scrollBar, height,
					width, contour, null, mainScheme, mainScheme, 0, false);
			result = SubstanceImageCreator.getRotated(result, 3);

			SubstanceScrollBarUI.trackVerticalMap.put(key, result);
		}
		return result;
	}

	/**
	 * Returns the image for a vertical track.
	 * 
	 * @param trackBounds
	 *            Track bounds.
	 * @param scrollBar
	 *            Scroll bar.
	 * @param topActiveButton
	 *            The closest top button in the scroll bar. May be
	 *            <code>null</code>.
	 * @param bottomActiveButton
	 *            The closest bottom button in the scroll bar. May be
	 *            <code>null</code>.
	 * @param width
	 *            Scroll track width.
	 * @param height
	 *            Scroll track height.
	 * @param graphicsComposite
	 *            Composite to apply before painting the track.
	 * @return Vertical track image.
	 */
	private static BufferedImage getTrackBackVertical(JScrollBar scrollBar,
			AbstractButton topActiveButton, AbstractButton bottomActiveButton,
			int width, int height) {
		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(scrollBar);
		int radius = width / 2;
		if (shaper instanceof ClassicButtonShaper)
			radius = 2;
		BufferedImage opaque = SubstanceImageCreator.getRotated(
				SubstanceImageCreator.getCompositeRoundedBackground(scrollBar,
						height, width, radius, topActiveButton,
						bottomActiveButton, true), 3);

		return opaque;
	}

	/**
	 * Retrieves image for vertical thumb.
	 * 
	 * @param thumbBounds
	 *            Thumb bounding rectangle.
	 * @return Image for vertical thumb.
	 */
	private BufferedImage getThumbVertical(Rectangle thumbBounds) {
		int width = Math.max(1, thumbBounds.width);
		int height = Math.max(1, thumbBounds.height);

		// System.out.println(ComponentState.getState(buttonModel, null)
		// .getColorSchemeKind().name());

		Component tracked = SubstanceFadeUtilities.getTracked(
				FadeKind.ROLLOVER, this.scrollbar, this.decrButton,
				this.incrButton, this.mySecondDecreaseButton,
				this.mySecondIncreaseButton);
		ComponentState state = ComponentState.getState(
				this.compositeScrollTrackModel, null);
		if (state.isKindActive(FadeKind.PRESS))
			tracked = null;

		if (tracked != null) {
			ComponentState trackedState = (tracked == this.scrollbar) ? ComponentState
					.getState(this.thumbModel, null)
					: ComponentState.getState(((AbstractButton) tracked)
							.getModel(), null);
			ComponentState prevState = SubstanceCoreUtilities
					.getPrevComponentState(this.scrollbar);
			// enabled scroll bar is always painted as active
			if (trackedState == ComponentState.DEFAULT)
				trackedState = ComponentState.ACTIVE;
			if (prevState == ComponentState.DEFAULT)
				prevState = ComponentState.ACTIVE;

			float cyclePos = trackedState.getCyclePosition();
			FadeState highest = SubstanceFadeUtilities
					.getFadeStateWithHighestFadeCycle(FadeKind.ROLLOVER,
							this.scrollbar, this.decrButton, this.incrButton,
							this.mySecondDecreaseButton,
							this.mySecondIncreaseButton);
			if (highest != null) {
				cyclePos = highest.getFadePosition();
				if (!highest.isFadingIn())
					cyclePos = 1.0f - cyclePos;
			}

			SubstanceColorScheme scheme2 = SubstanceColorSchemeUtilities
					.getColorScheme(this.scrollbar, trackedState);
			SubstanceColorScheme scheme1 = SubstanceColorSchemeUtilities
					.getColorScheme(this.scrollbar, prevState);
			SubstanceColorScheme borderScheme2 = SubstanceColorSchemeUtilities
					.getBorderColorScheme(this.scrollbar, trackedState);
			SubstanceColorScheme borderScheme1 = SubstanceColorSchemeUtilities
					.getBorderColorScheme(this.scrollbar, prevState);
			if (scheme1 == scheme2) {
				// special case for smooth rollover animations on skins
				// that have the same ROLLOVER and ACTIVE schemes
				cyclePos = 0.5f - Math.abs(0.5f - cyclePos);
			}
			// System.out.println(scheme1.getDisplayName() + "->"
			// + scheme2.getDisplayName() + ":" + cyclePos);
			return getThumbVertical(this.scrollbar, width, height, cyclePos,
					scheme1, scheme2, borderScheme1, borderScheme2);
		}

		ComponentState prevState = SubstanceCoreUtilities
				.getPrevComponentState(this.scrollbar);

		if (state == ComponentState.DEFAULT)
			state = ComponentState.ACTIVE;
		if (prevState == ComponentState.DEFAULT)
			prevState = ComponentState.ACTIVE;
		float cyclePos = state.getCyclePosition();

		SubstanceColorScheme colorScheme = SubstanceColorSchemeUtilities
				.getColorScheme(this.scrollbar, state);
		SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
				.getBorderColorScheme(this.scrollbar, state);
		SubstanceColorScheme colorScheme2 = colorScheme;
		SubstanceColorScheme borderScheme2 = borderScheme;
		FadeTracker fadeTracker = FadeTracker.getInstance();
		FadeState fadeState = fadeTracker.getFadeState(this.scrollbar,
				FadeKind.PRESS);
		if (fadeState != null) {
			colorScheme2 = SubstanceColorSchemeUtilities.getColorScheme(
					this.scrollbar, prevState);
			borderScheme2 = SubstanceColorSchemeUtilities.getBorderColorScheme(
					this.scrollbar, prevState);
			cyclePos = fadeState.getFadePosition();
			if (fadeState.isFadingIn()) {
				cyclePos = 1.0f - cyclePos;
			}
		} else {
			cyclePos = 0.0f;
		}
		// System.out.println(colorScheme.getDisplayName() + "->"
		// + colorScheme2.getDisplayName() + ":" + cyclePos);
		return getThumbVertical(this.scrollbar, width, height, cyclePos,
				colorScheme, colorScheme2, borderScheme, borderScheme2);
	}

	/**
	 * Retrieves image for vertical thumb.
	 * 
	 * @param scrollBar
	 *            Scroll bar.
	 * @param width
	 *            Thumb width.
	 * @param height
	 *            Thumb height.
	 * @param kind
	 *            Color scheme kind.
	 * @param cyclePos
	 *            Cycle position.
	 * @param scheme
	 *            The first color scheme.
	 * @param scheme2
	 *            The second color scheme.
	 * @param borderScheme
	 *            The first border color scheme.
	 * @param borderScheme2
	 *            The second border color scheme.
	 * @return Image for vertical thumb.
	 */
	private static BufferedImage getThumbVertical(JScrollBar scrollBar,
			int width, int height, float cyclePos, SubstanceColorScheme scheme,
			SubstanceColorScheme scheme2, SubstanceColorScheme borderScheme,
			SubstanceColorScheme borderScheme2) {
		SubstanceGradientPainter painter = SubstanceCoreUtilities
				.getGradientPainter(scrollBar);
		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(scrollBar);
		SubstanceBorderPainter borderPainter = SubstanceCoreUtilities
				.getBorderPainter(scrollBar);
		HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height,
				scheme.getDisplayName(), scheme2.getDisplayName(), borderScheme
						.getDisplayName(), borderScheme2.getDisplayName(),
				cyclePos, painter.getDisplayName(), shaper.getDisplayName(),
				borderPainter.getDisplayName());
		BufferedImage result = SubstanceScrollBarUI.thumbVerticalMap.get(key);
		if (result == null) {
			// System.out.println("Cache miss - computing");
			// System.out.println("New image for vertical thumb");
			float radius = width / 2;
			if (shaper instanceof ClassicButtonShaper)
				radius = SubstanceSizeUtils
						.getClassicButtonCornerRadius(SubstanceSizeUtils
								.getComponentFontSize(scrollBar));

			int borderDelta = (int) Math.floor(SubstanceSizeUtils
					.getBorderStrokeWidth(SubstanceSizeUtils
							.getComponentFontSize(scrollBar)) / 2.0);
			GeneralPath contour = SubstanceOutlineUtilities.getBaseOutline(
					height, width, radius, null, borderDelta);

			result = SubstanceCoreUtilities.getBlankImage(height, width);
			painter.paintContourBackground(result.createGraphics(), scrollBar,
					height, width, contour, false, scheme, scheme2, cyclePos,
					true, scheme != scheme2);

			int borderThickness = (int) SubstanceSizeUtils
					.getBorderStrokeWidth(SubstanceSizeUtils
							.getComponentFontSize(scrollBar));
			GeneralPath contourInner = SubstanceOutlineUtilities
					.getBaseOutline(height, width, radius, null,
							borderThickness + borderDelta);
			borderPainter.paintBorder(result.getGraphics(), scrollBar, height,
					width, contour, contourInner, borderScheme, borderScheme2,
					cyclePos, borderScheme != borderScheme2);
			result = SubstanceImageCreator.getRotated(result, 3);
			// System.out.println(key);
			SubstanceScrollBarUI.thumbVerticalMap.put(key, result);
		}

		return result;
	}

	/**
	 * Retrieves image for horizontal thumb.
	 * 
	 * @param thumbBounds
	 *            Thumb bounding rectangle.
	 * @return Image for horizontal thumb.
	 */
	private BufferedImage getThumbHorizontal(Rectangle thumbBounds) {
		int width = Math.max(1, thumbBounds.width);
		int height = Math.max(1, thumbBounds.height);

		Component tracked = SubstanceFadeUtilities.getTracked(
				FadeKind.ROLLOVER, this.scrollbar, this.decrButton,
				this.incrButton, this.mySecondDecreaseButton,
				this.mySecondIncreaseButton);
		ComponentState state = ComponentState.getState(
				this.compositeScrollTrackModel, null);
		if (state.isKindActive(FadeKind.PRESS))
			tracked = null;

		if (tracked != null) {
			ComponentState trackedState = (tracked == this.scrollbar) ? ComponentState
					.getState(this.thumbModel, null)
					: ComponentState.getState(((AbstractButton) tracked)
							.getModel(), null);
			ComponentState prevState = SubstanceCoreUtilities
					.getPrevComponentState(this.scrollbar);
			if (trackedState == ComponentState.DEFAULT)
				trackedState = ComponentState.ACTIVE;
			if (prevState == ComponentState.DEFAULT)
				prevState = ComponentState.ACTIVE;

			float cyclePos = trackedState.getCyclePosition();
			FadeState highest = SubstanceFadeUtilities
					.getFadeStateWithHighestFadeCycle(FadeKind.ROLLOVER,
							this.scrollbar, this.decrButton, this.incrButton,
							this.mySecondDecreaseButton,
							this.mySecondIncreaseButton);
			if (highest != null) {
				cyclePos = highest.getFadePosition();
				if (!highest.isFadingIn())
					cyclePos = 1.0f - cyclePos;
			}

			SubstanceColorScheme scheme2 = SubstanceColorSchemeUtilities
					.getColorScheme(this.scrollbar, trackedState);
			SubstanceColorScheme scheme1 = SubstanceColorSchemeUtilities
					.getColorScheme(this.scrollbar, prevState);
			SubstanceColorScheme borderScheme2 = SubstanceColorSchemeUtilities
					.getBorderColorScheme(this.scrollbar, trackedState);
			SubstanceColorScheme borderScheme1 = SubstanceColorSchemeUtilities
					.getBorderColorScheme(this.scrollbar, prevState);

			if (scheme1 == scheme2) {
				// special case for smooth rollover animations on skins
				// that have the same ROLLOVER and ACTIVE schemes
				cyclePos = 0.5f - Math.abs(0.5f - cyclePos);
			}
			return getThumbHorizontal(this.scrollbar, width, height, cyclePos,
					scheme1, scheme2, borderScheme1, borderScheme2);
		}

		ComponentState prevState = SubstanceCoreUtilities
				.getPrevComponentState(this.scrollbar);
		float cyclePos = state.getCyclePosition();

		if (state == ComponentState.DEFAULT)
			state = ComponentState.ACTIVE;
		if (prevState == ComponentState.DEFAULT)
			prevState = ComponentState.ACTIVE;

		SubstanceColorScheme colorScheme = SubstanceColorSchemeUtilities
				.getColorScheme(this.scrollbar, state);
		SubstanceColorScheme colorScheme2 = colorScheme;
		SubstanceColorScheme borderScheme = SubstanceColorSchemeUtilities
				.getBorderColorScheme(this.scrollbar, state);
		SubstanceColorScheme borderScheme2 = borderScheme;
		FadeTracker fadeTracker = FadeTracker.getInstance();
		FadeState fadeState = fadeTracker.getFadeState(this.scrollbar,
				FadeKind.PRESS);
		if (fadeState != null) {
			colorScheme2 = SubstanceColorSchemeUtilities.getColorScheme(
					this.scrollbar, prevState);
			borderScheme2 = SubstanceColorSchemeUtilities.getBorderColorScheme(
					this.scrollbar, prevState);
			cyclePos = fadeState.getFadePosition();
			if (fadeState.isFadingIn()) {
				cyclePos = 1.0f - cyclePos;
			}
		} else {
			cyclePos = 0.0f;
		}
		return getThumbHorizontal(this.scrollbar, width, height, cyclePos,
				colorScheme, colorScheme2, borderScheme, borderScheme2);
	}

	/**
	 * Retrieves image for horizontal thumb.
	 * 
	 * @param scrollBar
	 *            Scroll bar.
	 * @param width
	 *            Thumb width.
	 * @param height
	 *            Thumb height.
	 * @param kind
	 *            Color scheme kind.
	 * @param cyclePos
	 *            Cycle position.
	 * @param scheme
	 *            The first color scheme.
	 * @param scheme2
	 *            The second color scheme.
	 * @param borderScheme
	 *            The first border color scheme.
	 * @param borderScheme2
	 *            The second border color scheme.
	 * @return Image for horizontal thumb.
	 */
	private static BufferedImage getThumbHorizontal(JScrollBar scrollBar,
			int width, int height, float cyclePos, SubstanceColorScheme scheme,
			SubstanceColorScheme scheme2, SubstanceColorScheme borderScheme,
			SubstanceColorScheme borderScheme2) {
		SubstanceGradientPainter painter = SubstanceCoreUtilities
				.getGradientPainter(scrollBar);
		SubstanceButtonShaper shaper = SubstanceCoreUtilities
				.getButtonShaper(scrollBar);
		SubstanceBorderPainter borderPainter = SubstanceCoreUtilities
				.getBorderPainter(scrollBar);
		// GripPainter gripPainter = SubstanceCoreUtilities.getGripPainter(
		// scrollBar, null);
		// System.out.println(state.name());
		HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height,
				scheme.getDisplayName(), scheme2.getDisplayName(), cyclePos,
				painter.getDisplayName(), shaper.getDisplayName(),
				borderPainter.getDisplayName());
		// + ":"
		// + ((gripPainter != null) ? gripPainter.getDisplayName()
		// : "null");

		float radius = height / 2;
		if (shaper instanceof ClassicButtonShaper)
			radius = SubstanceSizeUtils
					.getClassicButtonCornerRadius(SubstanceSizeUtils
							.getComponentFontSize(scrollBar));
		int borderDelta = (int) Math.floor(SubstanceSizeUtils
				.getBorderStrokeWidth(SubstanceSizeUtils
						.getComponentFontSize(scrollBar)) / 2.0);
		GeneralPath contour = SubstanceOutlineUtilities.getBaseOutline(width,
				height, radius, null, borderDelta);
		BufferedImage opaque = SubstanceScrollBarUI.thumbHorizontalMap.get(key);
		if (opaque == null) {
			// System.out.println("New image for horizontal thumb");

			opaque = SubstanceCoreUtilities.getBlankImage(width, height);
			painter.paintContourBackground(opaque.createGraphics(), scrollBar,
					width, height, contour, false, scheme, scheme2, cyclePos,
					true, scheme != scheme2);

			int borderThickness = (int) SubstanceSizeUtils
					.getBorderStrokeWidth(SubstanceSizeUtils
							.getComponentFontSize(scrollBar));
			GeneralPath contourInner = SubstanceOutlineUtilities
					.getBaseOutline(width, height, radius, null,
							borderThickness + borderDelta);
			borderPainter.paintBorder(opaque.getGraphics(), scrollBar, width,
					height, contour, contourInner, borderScheme, borderScheme2,
					cyclePos, borderScheme != borderScheme2);
			SubstanceScrollBarUI.thumbHorizontalMap.put(key, opaque);
		}

		return opaque;
	}

	/**
	 * Returns the scroll button state.
	 * 
	 * @param scrollButton
	 *            Scroll button.
	 * @return Scroll button state.
	 */
	protected ComponentState getState(JButton scrollButton) {
		if (scrollButton == null)
			return null;

		ComponentState result = ComponentState.getState(scrollButton);
		if ((result == ComponentState.DEFAULT)
				&& SubstanceCoreUtilities.hasFlatAppearance(this.scrollbar,
						false)) {
			result = null;
		}
		if (SubstanceCoreUtilities.isButtonNeverPainted(scrollButton)) {
			result = null;
		}
		return result;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.swing.plaf.basic.BasicScrollBarUI#paintTrack(java.awt.Graphics,
	 * javax.swing.JComponent, java.awt.Rectangle)
	 */
	@Override
	protected void paintTrack(Graphics g, JComponent c, Rectangle trackBounds) {
		Graphics2D graphics = (Graphics2D) g.create();

		// System.out.println("Track");
		ScrollPaneButtonPolicyKind buttonPolicy = SubstanceCoreUtilities
				.getScrollPaneButtonsPolicyKind(this.scrollbar);
		SubstanceScrollButton compTopState = null;
		SubstanceScrollButton compBottomState = null;
		if (this.decrButton.isShowing() && this.incrButton.isShowing()
				&& this.mySecondDecreaseButton.isShowing()
				&& this.mySecondIncreaseButton.isShowing()) {
			switch (buttonPolicy) {
			case OPPOSITE:
				compTopState = (SubstanceScrollButton) this.decrButton;
				compBottomState = (SubstanceScrollButton) this.incrButton;
				break;
			case ADJACENT:
				compBottomState = (SubstanceScrollButton) this.mySecondDecreaseButton;
				break;
			case MULTIPLE:
				compTopState = (SubstanceScrollButton) this.decrButton;
				compBottomState = (SubstanceScrollButton) this.mySecondDecreaseButton;
				break;
			case MULTIPLE_BOTH:
				compTopState = (SubstanceScrollButton) this.mySecondIncreaseButton;
				compBottomState = (SubstanceScrollButton) this.mySecondDecreaseButton;
				break;
			}
		}

		if (this.scrollbar.getOrientation() == Adjustable.VERTICAL) {
			BufferedImage bi = this.getTrackVertical(trackBounds, compTopState,
					compBottomState);
			graphics.drawImage(bi, trackBounds.x, trackBounds.y, null);
		} else {
			BufferedImage bi = this.scrollbar.getComponentOrientation()
					.isLeftToRight() ? this.getTrackHorizontal(trackBounds,
					compTopState, compBottomState) : this.getTrackHorizontal(
					trackBounds, compBottomState, compTopState);
			graphics.drawImage(bi, trackBounds.x, trackBounds.y, null);
		}

		graphics.dispose();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.swing.plaf.basic.BasicScrollBarUI#paintThumb(java.awt.Graphics,
	 * javax.swing.JComponent, java.awt.Rectangle)
	 */
	@Override
	protected void paintThumb(Graphics g, JComponent c, Rectangle thumbBounds) {
		// System.out.println("Thumb");
		Graphics2D graphics = (Graphics2D) g.create();
		// ControlBackgroundComposite composite = SubstanceCoreUtilities
		// .getControlBackgroundComposite(this.scrollbar);

		// JScrollBar scrollBar = (JScrollBar) c;
		this.thumbModel.setSelected(this.thumbModel.isSelected()
				|| this.isDragging);
		this.thumbModel.setEnabled(c.isEnabled());
		boolean isVertical = (this.scrollbar.getOrientation() == Adjustable.VERTICAL);
		if (isVertical) {
			Rectangle adjustedBounds = new Rectangle(thumbBounds.x,
					thumbBounds.y, thumbBounds.width, thumbBounds.height);
			BufferedImage thumbImage = this.getThumbVertical(adjustedBounds);
			graphics.drawImage(thumbImage, adjustedBounds.x, adjustedBounds.y,
					null);
		} else {
			Rectangle adjustedBounds = new Rectangle(thumbBounds.x,
					thumbBounds.y, thumbBounds.width, thumbBounds.height);
			BufferedImage thumbImage = this.getThumbHorizontal(adjustedBounds);
			graphics.drawImage(thumbImage, adjustedBounds.x, adjustedBounds.y,
					null);
		}
		graphics.dispose();
	}

	@Override
	public void paint(Graphics g, JComponent c) {
		Graphics2D graphics = (Graphics2D) g.create();
		SubstanceFillBackgroundDelegate.GLOBAL_INSTANCE.update(graphics, c,
				false);
		float alpha = SubstanceColorSchemeUtilities.getAlpha(this.scrollbar,
				ComponentState.getState(this.thumbModel, this.scrollbar));
		graphics.setComposite(TransitionLayout.getAlphaComposite(c, alpha, g));
		super.paint(graphics, c);
		graphics.dispose();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#installDefaults()
	 */
	@Override
	protected void installDefaults() {
		super.installDefaults();
		this.scrollBarWidth = SubstanceSizeUtils
				.getScrollBarWidth(SubstanceSizeUtils
						.getComponentFontSize(this.scrollbar));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#installComponents()
	 */
	@Override
	protected void installComponents() {
		super.installComponents();
		switch (this.scrollbar.getOrientation()) {
		case JScrollBar.VERTICAL:
			this.mySecondDecreaseButton = this.createGeneralDecreaseButton(
					NORTH, false);
			this.mySecondIncreaseButton = this.createGeneralIncreaseButton(
					SOUTH, false);
			break;

		case JScrollBar.HORIZONTAL:
			if (this.scrollbar.getComponentOrientation().isLeftToRight()) {
				this.mySecondDecreaseButton = this.createGeneralDecreaseButton(
						WEST, false);
				this.mySecondIncreaseButton = this.createGeneralIncreaseButton(
						EAST, false);
			} else {
				this.mySecondDecreaseButton = this.createGeneralDecreaseButton(
						EAST, false);
				this.mySecondIncreaseButton = this.createGeneralIncreaseButton(
						WEST, false);
			}
			break;
		}
		this.scrollbar.add(this.mySecondDecreaseButton);
		this.scrollbar.add(this.mySecondIncreaseButton);

		this.compositeScrollTrackModel = new CompositeButtonModel(
				this.thumbModel, this.incrButton, this.decrButton,
				this.mySecondDecreaseButton, this.mySecondIncreaseButton);
		this.compositeButtonsModel = new CompositeButtonModel(
				new DefaultButtonModel(), this.incrButton, this.decrButton,
				this.mySecondDecreaseButton, this.mySecondIncreaseButton);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#uninstallComponents()
	 */
	@Override
	protected void uninstallComponents() {
		this.scrollbar.remove(this.mySecondDecreaseButton);
		this.scrollbar.remove(this.mySecondIncreaseButton);
		super.uninstallComponents();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#installListeners()
	 */
	@Override
	protected void installListeners() {
		super.installListeners();
		this.substanceMouseListener = new MouseAdapter() {
			@Override
			public void mouseEntered(MouseEvent e) {
				SubstanceScrollBarUI.this.scrollbar.repaint();
			}

			@Override
			public void mouseExited(MouseEvent e) {
				SubstanceScrollBarUI.this.scrollbar.repaint();
			}

			@Override
			public void mousePressed(MouseEvent e) {
				SubstanceScrollBarUI.this.scrollbar.repaint();
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				SubstanceScrollBarUI.this.scrollbar.repaint();
			}
		};

		this.incrButton.addMouseListener(this.substanceMouseListener);
		this.decrButton.addMouseListener(this.substanceMouseListener);
		this.mySecondDecreaseButton
				.addMouseListener(this.substanceMouseListener);
		this.mySecondIncreaseButton
				.addMouseListener(this.substanceMouseListener);

		this.substanceThumbRolloverListener = new RolloverControlListener(this,
				this.thumbModel);
		this.scrollbar.addMouseListener(this.substanceThumbRolloverListener);
		this.scrollbar
				.addMouseMotionListener(this.substanceThumbRolloverListener);

		this.substanceFadeStateListener = new FadeStateListener(this.scrollbar,
				this.thumbModel, SubstanceCoreUtilities.getFadeCallback(
						this.scrollbar, this.thumbModel, false, false,
						this.scrollbar));
		this.substanceFadeStateListener.registerListeners(false);

		this.substancePropertyListener = new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				if ("font".equals(evt.getPropertyName())) {
					SwingUtilities.invokeLater(new Runnable() {
						public void run() {
							scrollbar.updateUI();
						}
					});
				}
				if ("background".equals(evt.getPropertyName())) {
					// propagate application-specific background color to the
					// scroll buttons.
					Color newBackgr = (Color) evt.getNewValue();
					if (!(newBackgr instanceof UIResource)) {
						if (mySecondDecreaseButton != null) {
							if (mySecondDecreaseButton.getBackground() instanceof UIResource) {
								mySecondDecreaseButton.setBackground(newBackgr);
							}
						}
						if (mySecondIncreaseButton != null) {
							if (mySecondIncreaseButton.getBackground() instanceof UIResource) {
								mySecondIncreaseButton.setBackground(newBackgr);
							}
						}
						if (incrButton != null) {
							if (incrButton.getBackground() instanceof UIResource) {
								incrButton.setBackground(newBackgr);
							}
						}
						if (decrButton != null) {
							if (decrButton.getBackground() instanceof UIResource) {
								decrButton.setBackground(newBackgr);
							}
						}
					}
				}
			}
		};
		this.scrollbar
				.addPropertyChangeListener(this.substancePropertyListener);

		this.mySecondDecreaseButton.addMouseListener(this.buttonListener);
		this.mySecondIncreaseButton.addMouseListener(this.buttonListener);

		this.substanceAdjustmentListener = new AdjustmentListener() {
			public void adjustmentValueChanged(AdjustmentEvent e) {
				Component parent = SubstanceScrollBarUI.this.scrollbar
						.getParent();
				if (parent instanceof JScrollPane) {
					JScrollPane jsp = (JScrollPane) parent;
					JScrollBar hor = jsp.getHorizontalScrollBar();
					JScrollBar ver = jsp.getVerticalScrollBar();

					JScrollBar other = null;
					if (SubstanceScrollBarUI.this.scrollbar == hor) {
						other = ver;
					}
					if (SubstanceScrollBarUI.this.scrollbar == ver) {
						other = hor;
					}

					if ((other != null) && other.isVisible())
						other.repaint();
					SubstanceScrollBarUI.this.scrollbar.repaint();
				}
			}
		};
		this.scrollbar.addAdjustmentListener(this.substanceAdjustmentListener);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#uninstallListeners()
	 */
	@Override
	protected void uninstallListeners() {
		// fix for defect 109 - memory leak on changing theme
		this.incrButton.removeMouseListener(this.substanceMouseListener);
		this.decrButton.removeMouseListener(this.substanceMouseListener);
		this.mySecondDecreaseButton
				.removeMouseListener(this.substanceMouseListener);
		this.mySecondIncreaseButton
				.removeMouseListener(this.substanceMouseListener);
		this.substanceMouseListener = null;

		this.scrollbar.removeMouseListener(this.substanceThumbRolloverListener);
		this.scrollbar
				.removeMouseMotionListener(this.substanceThumbRolloverListener);
		this.substanceThumbRolloverListener = null;

		this.substanceFadeStateListener.unregisterListeners();
		this.substanceFadeStateListener = null;

		this.scrollbar
				.removePropertyChangeListener(this.substancePropertyListener);
		this.substancePropertyListener = null;

		this.mySecondDecreaseButton.removeMouseListener(this.buttonListener);
		this.mySecondIncreaseButton.removeMouseListener(this.buttonListener);

		this.scrollbar
				.removeAdjustmentListener(this.substanceAdjustmentListener);
		this.substanceAdjustmentListener = null;

		super.uninstallListeners();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jvnet.substance.Trackable#isInside(java.awt.event.MouseEvent)
	 */
	public boolean isInside(MouseEvent me) {
		// Rectangle thumbB = this.getThumbBounds();
		Rectangle trackB = this.getTrackBounds();
		if (trackB == null)
			return false;
		return trackB.contains(me.getX(), me.getY());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#scrollByBlock(int)
	 */
	@Override
	public void scrollByBlock(int direction) {
		// This method is called from SubstanceScrollPaneUI to implement wheel
		// scrolling.
		int oldValue = this.scrollbar.getValue();
		int blockIncrement = this.scrollbar.getBlockIncrement(direction);
		int delta = blockIncrement * ((direction > 0) ? +1 : -1);
		int newValue = oldValue + delta;

		// Check for overflow.
		if ((delta > 0) && (newValue < oldValue)) {
			newValue = this.scrollbar.getMaximum();
		} else if ((delta < 0) && (newValue > oldValue)) {
			newValue = this.scrollbar.getMinimum();
		}

		this.scrollbar.setValue(newValue);
	}

	/**
	 * Scrolls the associated scroll bar.
	 * 
	 * @param direction
	 *            Direction.
	 * @param units
	 *            Scroll units.
	 */
	public void scrollByUnits(int direction, int units) {
		// This method is called from SubstanceScrollPaneUI to implement wheel
		// scrolling.
		int delta;

		for (int i = 0; i < units; i++) {
			if (direction > 0) {
				delta = this.scrollbar.getUnitIncrement(direction);
			} else {
				delta = -this.scrollbar.getUnitIncrement(direction);
			}

			int oldValue = this.scrollbar.getValue();
			int newValue = oldValue + delta;

			// Check for overflow.
			if ((delta > 0) && (newValue < oldValue)) {
				newValue = this.scrollbar.getMaximum();
			} else if ((delta < 0) && (newValue > oldValue)) {
				newValue = this.scrollbar.getMinimum();
			}
			if (oldValue == newValue) {
				break;
			}
			this.scrollbar.setValue(newValue);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.swing.plaf.basic.BasicScrollBarUI#layoutVScrollbar(javax.swing.
	 * JScrollBar)
	 */
	@Override
	protected void layoutVScrollbar(JScrollBar sb) {
		ScrollPaneButtonPolicyKind buttonPolicy = SubstanceCoreUtilities
				.getScrollPaneButtonsPolicyKind(this.scrollbar);
		this.mySecondDecreaseButton.setBounds(0, 0, 0, 0);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);
		switch (buttonPolicy) {
		case OPPOSITE:
			super.layoutVScrollbar(sb);
			break;
		case NONE:
			this.layoutVScrollbarNone(sb);
			break;
		case ADJACENT:
			this.layoutVScrollbarAdjacent(sb);
			break;
		case MULTIPLE:
			this.layoutVScrollbarMultiple(sb);
			break;
		case MULTIPLE_BOTH:
			this.layoutVScrollbarMultipleBoth(sb);
			break;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.swing.plaf.basic.BasicScrollBarUI#layoutHScrollbar(javax.swing.
	 * JScrollBar)
	 */
	@Override
	protected void layoutHScrollbar(JScrollBar sb) {
		this.mySecondDecreaseButton.setBounds(0, 0, 0, 0);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);
		ScrollPaneButtonPolicyKind buttonPolicy = SubstanceCoreUtilities
				.getScrollPaneButtonsPolicyKind(this.scrollbar);
		switch (buttonPolicy) {
		case OPPOSITE:
			super.layoutHScrollbar(sb);
			break;
		case NONE:
			this.layoutHScrollbarNone(sb);
			break;
		case ADJACENT:
			this.layoutHScrollbarAdjacent(sb);
			break;
		case MULTIPLE:
			this.layoutHScrollbarMultiple(sb);
			break;
		case MULTIPLE_BOTH:
			this.layoutHScrollbarMultipleBoth(sb);
			break;
		}
	}

	/**
	 * Lays out the vertical scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#ADJACENT}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutVScrollbarAdjacent(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Width and left edge of the buttons and thumb.
		 */
		int itemW = sbSize.width - (sbInsets.left + sbInsets.right);
		int itemX = sbInsets.left;

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int incrButtonH = itemW;
		int incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);

		int decrButton2H = itemW;
		int decrButton2Y = incrButtonY - decrButton2H;

		/*
		 * The thumb must fit within the height left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsH = sbInsets.top + sbInsets.bottom;
		int sbButtonsH = decrButton2H + incrButtonH;
		float trackH = sbSize.height - (sbInsetsH + sbButtonsH);

		/*
		 * Compute the height and origin of the thumb. The case where the thumb
		 * is at the bottom edge is handled specially to avoid numerical
		 * problems in computing thumbY. Enforce the thumbs min/max dimensions.
		 * If the thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float extent = sb.getVisibleAmount();
		float range = sb.getMaximum() - min;
		float value = sb.getValue();

		int thumbH = (range <= 0) ? this.getMaximumThumbSize().height
				: (int) (trackH * (extent / range));
		thumbH = Math.max(thumbH, this.getMinimumThumbSize().height);
		thumbH = Math.min(thumbH, this.getMaximumThumbSize().height);

		int thumbY = decrButton2Y - thumbH;
		if (value < (sb.getMaximum() - sb.getVisibleAmount())) {
			float thumbRange = trackH - thumbH;
			thumbY = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the lower one (incrButton) down.
		 */
		int sbAvailButtonH = (sbSize.height - sbInsetsH);
		if (sbAvailButtonH < sbButtonsH) {
			incrButtonH = decrButton2H = sbAvailButtonH / 2;
			incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);
		}
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);
		this.decrButton.setBounds(0, 0, 0, 0);
		this.mySecondDecreaseButton.setBounds(itemX,
				incrButtonY - decrButton2H, itemW, decrButton2H);
		this.incrButton.setBounds(itemX, incrButtonY - 1, itemW,
				incrButtonH + 1);

		/*
		 * Update the trackRect field.
		 */
		int itrackY = 0;
		int itrackH = decrButton2Y - itrackY;
		this.trackRect.setBounds(itemX, itrackY, itemW, itrackH);

		/*
		 * If the thumb isn't going to fit, zero it's bounds. Otherwise make
		 * sure it fits between the buttons. Note that setting the thumbs bounds
		 * will cause a repaint.
		 */
		if (thumbH >= (int) trackH) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if ((thumbY + thumbH) > decrButton2Y) {
				thumbY = decrButton2Y - thumbH;
			}
			if (thumbY < 0) {
				thumbY = 0;
			}
			this.setThumbBounds(itemX, thumbY, itemW, thumbH);
		}
	}

	/**
	 * Lays out the vertical scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#ADJACENT}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutVScrollbarNone(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Width and left edge of the buttons and thumb.
		 */
		int itemW = sbSize.width - (sbInsets.left + sbInsets.right);
		int itemX = sbInsets.left;

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int incrButtonH = 0;
		int incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);

		int decrButton2H = 0;
		int decrButton2Y = incrButtonY - decrButton2H;

		/*
		 * The thumb must fit within the height left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsH = sbInsets.top + sbInsets.bottom;
		int sbButtonsH = decrButton2H + incrButtonH;
		float trackH = sbSize.height - (sbInsetsH + sbButtonsH);

		/*
		 * Compute the height and origin of the thumb. The case where the thumb
		 * is at the bottom edge is handled specially to avoid numerical
		 * problems in computing thumbY. Enforce the thumbs min/max dimensions.
		 * If the thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float extent = sb.getVisibleAmount();
		float range = sb.getMaximum() - min;
		float value = sb.getValue();

		int thumbH = (range <= 0) ? this.getMaximumThumbSize().height
				: (int) (trackH * (extent / range));
		thumbH = Math.max(thumbH, this.getMinimumThumbSize().height);
		thumbH = Math.min(thumbH, this.getMaximumThumbSize().height);

		int thumbY = decrButton2Y - thumbH;
		if (value < (sb.getMaximum() - sb.getVisibleAmount())) {
			float thumbRange = trackH - thumbH;
			thumbY = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the lower one (incrButton) down.
		 */
		int sbAvailButtonH = (sbSize.height - sbInsetsH);
		if (sbAvailButtonH < sbButtonsH) {
			incrButtonH = decrButton2H = 0;
			incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);
		}
		this.decrButton.setBounds(0, 0, 0, 0);
		this.mySecondDecreaseButton.setBounds(0, 0, 0, 0);
		this.incrButton.setBounds(0, 0, 0, 0);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);

		/*
		 * Update the trackRect field.
		 */
		int itrackY = 0;
		int itrackH = decrButton2Y - itrackY;
		this.trackRect.setBounds(itemX, itrackY, itemW, itrackH);

		/*
		 * If the thumb isn't going to fit, zero it's bounds. Otherwise make
		 * sure it fits between the buttons. Note that setting the thumbs bounds
		 * will cause a repaint.
		 */
		if (thumbH >= (int) trackH) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if ((thumbY + thumbH) > decrButton2Y) {
				thumbY = decrButton2Y - thumbH;
			}
			if (thumbY < 0) {
				thumbY = 0;
			}
			this.setThumbBounds(itemX, thumbY, itemW, thumbH);
		}
	}

	/**
	 * Lays out the vertical scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutVScrollbarMultiple(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Width and left edge of the buttons and thumb.
		 */
		int itemW = sbSize.width - (sbInsets.left + sbInsets.right);
		int itemX = sbInsets.left;

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int incrButtonH = itemW;
		int incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);

		int decrButton2H = itemW;
		int decrButton2Y = incrButtonY - decrButton2H;

		int decrButtonH = itemW;
		int decrButtonY = sbInsets.top;

		/*
		 * The thumb must fit within the height left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsH = sbInsets.top + sbInsets.bottom;
		int sbButtonsH = decrButton2H + incrButtonH + decrButtonH;
		float trackH = sbSize.height - (sbInsetsH + sbButtonsH);

		/*
		 * Compute the height and origin of the thumb. The case where the thumb
		 * is at the bottom edge is handled specially to avoid numerical
		 * problems in computing thumbY. Enforce the thumbs min/max dimensions.
		 * If the thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float extent = sb.getVisibleAmount();
		float range = sb.getMaximum() - min;
		float value = sb.getValue();

		int thumbH = (range <= 0) ? this.getMaximumThumbSize().height
				: (int) (trackH * (extent / range));
		thumbH = Math.max(thumbH, this.getMinimumThumbSize().height);
		thumbH = Math.min(thumbH, this.getMaximumThumbSize().height);

		int thumbY = decrButton2Y - thumbH;
		if (value < (sb.getMaximum() - sb.getVisibleAmount())) {
			float thumbRange = trackH - thumbH;
			thumbY = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
			thumbY += decrButtonY + decrButtonH;
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the lower one (incrButton) down.
		 */
		int sbAvailButtonH = (sbSize.height - sbInsetsH);
		if (sbAvailButtonH < sbButtonsH) {
			incrButtonH = decrButton2H = decrButtonH = sbAvailButtonH / 2;
			incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);
		}
		this.decrButton.setBounds(itemX, decrButtonY, itemW, decrButtonH);
		this.mySecondDecreaseButton.setBounds(itemX,
				incrButtonY - decrButton2H, itemW, decrButton2H);
		this.incrButton.setBounds(itemX, incrButtonY - 1, itemW,
				incrButtonH + 1);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);

		/*
		 * Update the trackRect field.
		 */
		int itrackY = decrButtonY + decrButtonH;
		int itrackH = decrButton2Y - itrackY;
		this.trackRect.setBounds(itemX, itrackY, itemW, itrackH);

		/*
		 * If the thumb isn't going to fit, zero it's bounds. Otherwise make
		 * sure it fits between the buttons. Note that setting the thumbs bounds
		 * will cause a repaint.
		 */
		if (thumbH >= (int) trackH) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if ((thumbY + thumbH) > decrButton2Y) {
				thumbY = decrButton2Y - thumbH;
			}
			if (thumbY < (decrButtonY + decrButtonH)) {
				thumbY = decrButtonY + decrButtonH + 1;
			}
			this.setThumbBounds(itemX, thumbY, itemW, thumbH);
		}
	}

	/**
	 * Lays out the vertical scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE_BOTH}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutVScrollbarMultipleBoth(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Width and left edge of the buttons and thumb.
		 */
		int itemW = sbSize.width - (sbInsets.left + sbInsets.right);
		int itemX = sbInsets.left;

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int incrButtonH = itemW;
		int incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);

		int decrButton2H = itemW;
		int decrButton2Y = incrButtonY - decrButton2H;

		int decrButtonH = itemW;
		int decrButtonY = sbInsets.top;

		int incrButton2H = itemW;
		int incrButton2Y = decrButtonY + decrButtonH;

		/*
		 * The thumb must fit within the height left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsH = sbInsets.top + sbInsets.bottom;
		int sbButtonsH = decrButton2H + incrButtonH + decrButtonH
				+ incrButton2H;
		float trackH = sbSize.height - (sbInsetsH + sbButtonsH);

		/*
		 * Compute the height and origin of the thumb. The case where the thumb
		 * is at the bottom edge is handled specially to avoid numerical
		 * problems in computing thumbY. Enforce the thumbs min/max dimensions.
		 * If the thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float extent = sb.getVisibleAmount();
		float range = sb.getMaximum() - min;
		float value = sb.getValue();

		int thumbH = (range <= 0) ? this.getMaximumThumbSize().height
				: (int) (trackH * (extent / range));
		thumbH = Math.max(thumbH, this.getMinimumThumbSize().height);
		thumbH = Math.min(thumbH, this.getMaximumThumbSize().height);

		int thumbY = decrButton2Y - thumbH;
		if (value < (sb.getMaximum() - sb.getVisibleAmount())) {
			float thumbRange = trackH - thumbH;
			thumbY = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
			thumbY += incrButton2Y + incrButton2H;
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the lower one (incrButton) down.
		 */
		int sbAvailButtonH = (sbSize.height - sbInsetsH);
		if (sbAvailButtonH < sbButtonsH) {
			incrButtonH = decrButton2H = decrButtonH = incrButton2H = sbAvailButtonH / 4;
			incrButtonY = sbSize.height - (sbInsets.bottom + incrButtonH);
		}
		this.decrButton.setBounds(itemX, decrButtonY, itemW, decrButtonH);
		this.mySecondDecreaseButton.setBounds(itemX,
				incrButtonY - decrButton2H, itemW, decrButton2H);
		this.incrButton.setBounds(itemX, incrButtonY - 1, itemW,
				incrButtonH + 1);
		this.mySecondIncreaseButton.setBounds(itemX, decrButtonY + decrButtonH
				- 1, itemW, incrButton2H + 1);

		/*
		 * Update the trackRect field.
		 */
		int itrackY = incrButton2Y + incrButton2H;
		int itrackH = decrButton2Y - itrackY;
		this.trackRect.setBounds(itemX, itrackY, itemW, itrackH);

		/*
		 * If the thumb isn't going to fit, zero it's bounds. Otherwise make
		 * sure it fits between the buttons. Note that setting the thumbs bounds
		 * will cause a repaint.
		 */
		if (thumbH >= (int) trackH) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if ((thumbY + thumbH) > decrButton2Y) {
				thumbY = decrButton2Y - thumbH;
			}
			if (thumbY < (incrButton2Y + incrButton2H)) {
				thumbY = incrButton2Y + incrButton2H + 1;
			}
			this.setThumbBounds(itemX, thumbY, itemW, thumbH);
		}
	}

	/**
	 * Lays out the horizontal scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#ADJACENT}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutHScrollbarAdjacent(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Height and top edge of the buttons and thumb.
		 */
		int itemH = sbSize.height - (sbInsets.top + sbInsets.bottom);
		int itemY = sbInsets.top;

		boolean ltr = sb.getComponentOrientation().isLeftToRight();

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int decrButton2W = itemH;
		int incrButtonW = itemH;
		int incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
				: sbInsets.left;
		int decrButton2X = ltr ? incrButtonX - decrButton2W : incrButtonX
				+ decrButton2W;

		/*
		 * The thumb must fit within the width left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsW = sbInsets.left + sbInsets.right;
		int sbButtonsW = decrButton2W + incrButtonW;
		float trackW = sbSize.width - (sbInsetsW + sbButtonsW);

		/*
		 * Compute the width and origin of the thumb. Enforce the thumbs min/max
		 * dimensions. The case where the thumb is at the right edge is handled
		 * specially to avoid numerical problems in computing thumbX. If the
		 * thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float max = sb.getMaximum();
		float extent = sb.getVisibleAmount();
		float range = max - min;
		float value = sb.getValue();

		int thumbW = (range <= 0) ? this.getMaximumThumbSize().width
				: (int) (trackW * (extent / range));
		thumbW = Math.max(thumbW, this.getMinimumThumbSize().width);
		thumbW = Math.min(thumbW, this.getMaximumThumbSize().width);

		int thumbX = ltr ? decrButton2X - thumbW : sbInsets.left;
		if (value < (max - sb.getVisibleAmount())) {
			float thumbRange = trackW - thumbW;
			if (ltr) {
				thumbX = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
			} else {
				thumbX = (int) (0.5f + (thumbRange * ((max - extent - value) / (range - extent))));
				thumbX += decrButton2X + decrButton2W;
			}
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the right one over.
		 */
		int sbAvailButtonW = (sbSize.width - sbInsetsW);
		if (sbAvailButtonW < sbButtonsW) {
			incrButtonW = decrButton2W = sbAvailButtonW / 2;
			incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
					: sbInsets.left;
		}

		this.mySecondDecreaseButton.setBounds(decrButton2X + (ltr ? 0 : -1),
				itemY, decrButton2W + 1, itemH);
		this.incrButton.setBounds(incrButtonX, itemY, incrButtonW, itemH);
		this.decrButton.setBounds(0, 0, 0, 0);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);

		/*
		 * Update the trackRect field.
		 */
		if (ltr) {
			int itrackX = sbInsets.left;
			int itrackW = decrButton2X - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		} else {
			int itrackX = decrButton2X + decrButton2W;
			int itrackW = sbSize.width - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		}

		/*
		 * Make sure the thumb fits between the buttons. Note that setting the
		 * thumbs bounds causes a repaint.
		 */
		if (thumbW >= (int) trackW) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if (ltr) {
				if (thumbX + thumbW > decrButton2X) {
					thumbX = decrButton2X - thumbW;
				}
				if (thumbX < 0) {
					thumbX = 1;
				}
			} else {
				if (thumbX + thumbW > (sbSize.width - sbInsets.left)) {
					thumbX = sbSize.width - sbInsets.left - thumbW;
				}
				if (thumbX < (decrButton2X + decrButton2W)) {
					thumbX = decrButton2X + decrButton2W + 1;
				}
			}
			this.setThumbBounds(thumbX, itemY, thumbW, itemH);
		}
	}

	/**
	 * Lays out the horizontal scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#NONE}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutHScrollbarNone(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Height and top edge of the buttons and thumb.
		 */
		int itemH = sbSize.height - (sbInsets.top + sbInsets.bottom);
		int itemY = sbInsets.top;

		boolean ltr = sb.getComponentOrientation().isLeftToRight();

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int decrButton2W = 0;
		int incrButtonW = 0;
		int incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
				: sbInsets.left;
		int decrButton2X = ltr ? incrButtonX - decrButton2W : incrButtonX
				+ decrButton2W;

		/*
		 * The thumb must fit within the width left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsW = sbInsets.left + sbInsets.right;
		int sbButtonsW = decrButton2W + incrButtonW;
		float trackW = sbSize.width - (sbInsetsW + sbButtonsW);

		/*
		 * Compute the width and origin of the thumb. Enforce the thumbs min/max
		 * dimensions. The case where the thumb is at the right edge is handled
		 * specially to avoid numerical problems in computing thumbX. If the
		 * thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float max = sb.getMaximum();
		float extent = sb.getVisibleAmount();
		float range = max - min;
		float value = sb.getValue();

		int thumbW = (range <= 0) ? this.getMaximumThumbSize().width
				: (int) (trackW * (extent / range));
		thumbW = Math.max(thumbW, this.getMinimumThumbSize().width);
		thumbW = Math.min(thumbW, this.getMaximumThumbSize().width);

		int thumbX = ltr ? decrButton2X - thumbW : sbInsets.left;
		if (value < (max - sb.getVisibleAmount())) {
			float thumbRange = trackW - thumbW;
			if (ltr) {
				thumbX = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
			} else {
				thumbX = (int) (0.5f + (thumbRange * ((max - extent - value) / (range - extent))));
				thumbX += decrButton2X + decrButton2W;
			}
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the right one over.
		 */
		int sbAvailButtonW = (sbSize.width - sbInsetsW);
		if (sbAvailButtonW < sbButtonsW) {
			incrButtonW = decrButton2W = 0;
			incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
					: sbInsets.left;
		}

		this.incrButton.setBounds(0, 0, 0, 0);
		this.decrButton.setBounds(0, 0, 0, 0);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);
		this.mySecondDecreaseButton.setBounds(0, 0, 0, 0);

		/*
		 * Update the trackRect field.
		 */
		if (ltr) {
			int itrackX = sbInsets.left;
			int itrackW = decrButton2X - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		} else {
			int itrackX = decrButton2X + decrButton2W;
			int itrackW = sbSize.width - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		}

		/*
		 * Make sure the thumb fits between the buttons. Note that setting the
		 * thumbs bounds causes a repaint.
		 */
		if (thumbW >= (int) trackW) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if (ltr) {
				if (thumbX + thumbW > decrButton2X) {
					thumbX = decrButton2X - thumbW;
				}
				if (thumbX < 0) {
					thumbX = 1;
				}
			} else {
				if (thumbX + thumbW > (sbSize.width - sbInsets.left)) {
					thumbX = sbSize.width - sbInsets.left - thumbW;
				}
				if (thumbX < (decrButton2X + decrButton2W)) {
					thumbX = decrButton2X + decrButton2W + 1;
				}
			}
			this.setThumbBounds(thumbX, itemY, thumbW, itemH);
		}
	}

	/**
	 * Lays out the horizontal scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutHScrollbarMultiple(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Height and top edge of the buttons and thumb.
		 */
		int itemH = sbSize.height - (sbInsets.top + sbInsets.bottom);
		int itemY = sbInsets.top;

		boolean ltr = sb.getComponentOrientation().isLeftToRight();

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int decrButton2W = itemH;
		int decrButtonW = itemH;
		int incrButtonW = itemH;
		int incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
				: sbInsets.left;
		int decrButton2X = ltr ? incrButtonX - decrButton2W : incrButtonX
				+ decrButton2W;
		int decrButtonX = ltr ? sbInsets.left : sbSize.width - sbInsets.right
				- decrButtonW;

		/*
		 * The thumb must fit within the width left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsW = sbInsets.left + sbInsets.right;
		int sbButtonsW = decrButton2W + incrButtonW + decrButtonW;
		float trackW = sbSize.width - (sbInsetsW + sbButtonsW);

		/*
		 * Compute the width and origin of the thumb. Enforce the thumbs min/max
		 * dimensions. The case where the thumb is at the right edge is handled
		 * specially to avoid numerical problems in computing thumbX. If the
		 * thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float max = sb.getMaximum();
		float extent = sb.getVisibleAmount();
		float range = max - min;
		float value = sb.getValue();

		int thumbW = (range <= 0) ? this.getMaximumThumbSize().width
				: (int) (trackW * (extent / range));
		thumbW = Math.max(thumbW, this.getMinimumThumbSize().width);
		thumbW = Math.min(thumbW, this.getMaximumThumbSize().width);

		int thumbX = ltr ? decrButton2X - thumbW : sbInsets.left;
		if (value < (max - sb.getVisibleAmount())) {
			float thumbRange = trackW - thumbW;
			if (ltr) {
				thumbX = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
				thumbX += decrButtonX + decrButtonW;
			} else {
				thumbX = (int) (0.5f + (thumbRange * ((max - extent - value) / (range - extent))));
				thumbX += decrButton2X + decrButton2W;
			}
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the right one over.
		 */
		int sbAvailButtonW = (sbSize.width - sbInsetsW);
		if (sbAvailButtonW < sbButtonsW) {
			incrButtonW = decrButton2W = decrButtonW = sbAvailButtonW / 2;
			incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
					: sbInsets.left;
		}

		this.mySecondDecreaseButton.setBounds(decrButton2X + (ltr ? 0 : -1),
				itemY, decrButton2W + 1, itemH);
		this.incrButton.setBounds(incrButtonX, itemY, incrButtonW, itemH);
		this.decrButton.setBounds(decrButtonX, itemY, decrButtonW, itemH);
		this.mySecondIncreaseButton.setBounds(0, 0, 0, 0);

		/*
		 * Update the trackRect field.
		 */
		if (ltr) {
			int itrackX = decrButtonX + decrButtonW;
			int itrackW = decrButton2X - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		} else {
			int itrackX = decrButton2X + decrButton2W;
			int itrackW = decrButtonX - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		}

		/*
		 * Make sure the thumb fits between the buttons. Note that setting the
		 * thumbs bounds causes a repaint.
		 */
		if (thumbW >= (int) trackW) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if (ltr) {
				if (thumbX + thumbW > decrButton2X) {
					thumbX = decrButton2X - thumbW;
				}
				if (thumbX < (decrButtonX + decrButtonW)) {
					thumbX = decrButtonX + decrButtonW + 1;
				}
			} else {
				if (thumbX + thumbW > decrButtonX) {
					thumbX = decrButtonX - thumbW;
				}
				if (thumbX < (decrButton2X + decrButton2W)) {
					thumbX = decrButton2X + decrButton2W + 1;
				}
			}
			this.setThumbBounds(thumbX, itemY, thumbW, itemH);
		}
	}

	/**
	 * Lays out the horizontal scroll bar when the button policy is
	 * {@link SubstanceConstants.ScrollPaneButtonPolicyKind#MULTIPLE}.
	 * 
	 * @param sb
	 *            Scroll bar.
	 */
	protected void layoutHScrollbarMultipleBoth(JScrollBar sb) {
		Dimension sbSize = sb.getSize();
		Insets sbInsets = sb.getInsets();

		/*
		 * Height and top edge of the buttons and thumb.
		 */
		int itemH = sbSize.height - (sbInsets.top + sbInsets.bottom);
		int itemY = sbInsets.top;

		boolean ltr = sb.getComponentOrientation().isLeftToRight();

		/*
		 * Nominal locations of the buttons, assuming their preferred size will
		 * fit.
		 */
		int decrButton2W = itemH;
		int incrButton2W = itemH;
		int decrButtonW = itemH;
		int incrButtonW = itemH;

		int incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
				: sbInsets.left;
		int decrButton2X = ltr ? incrButtonX - decrButton2W : incrButtonX
				+ decrButton2W;
		int decrButtonX = ltr ? sbInsets.left : sbSize.width - sbInsets.right
				- decrButtonW;
		int incrButton2X = ltr ? decrButtonX + decrButtonW : decrButtonX
				- incrButton2W;

		/*
		 * The thumb must fit within the width left over after we subtract the
		 * preferredSize of the buttons and the insets.
		 */
		int sbInsetsW = sbInsets.left + sbInsets.right;
		int sbButtonsW = decrButton2W + incrButtonW + decrButtonW
				+ incrButton2W;
		float trackW = sbSize.width - (sbInsetsW + sbButtonsW);

		/*
		 * Compute the width and origin of the thumb. Enforce the thumbs min/max
		 * dimensions. The case where the thumb is at the right edge is handled
		 * specially to avoid numerical problems in computing thumbX. If the
		 * thumb doesn't fit in the track (trackH) we'll hide it later.
		 */
		float min = sb.getMinimum();
		float max = sb.getMaximum();
		float extent = sb.getVisibleAmount();
		float range = max - min;
		float value = sb.getValue();

		int thumbW = (range <= 0) ? this.getMaximumThumbSize().width
				: (int) (trackW * (extent / range));
		thumbW = Math.max(thumbW, this.getMinimumThumbSize().width);
		thumbW = Math.min(thumbW, this.getMaximumThumbSize().width);

		int thumbX = ltr ? decrButton2X - thumbW : sbInsets.left;
		if (value < (max - sb.getVisibleAmount())) {
			float thumbRange = trackW - thumbW;
			if (ltr) {
				thumbX = (int) (0.5f + (thumbRange * ((value - min) / (range - extent))));
				thumbX += incrButton2X + incrButton2W;
			} else {
				thumbX = (int) (0.5f + (thumbRange * ((max - extent - value) / (range - extent))));
				thumbX += decrButton2X + decrButton2W;
			}
		}

		/*
		 * If the buttons don't fit, allocate half of the available space to
		 * each and move the right one over.
		 */
		int sbAvailButtonW = (sbSize.width - sbInsetsW);
		if (sbAvailButtonW < sbButtonsW) {
			incrButtonW = decrButton2W = decrButtonW = incrButton2W = sbAvailButtonW / 4;
			incrButtonX = ltr ? sbSize.width - (sbInsets.right + incrButtonW)
					: sbInsets.left;
		}

		this.mySecondDecreaseButton.setBounds(decrButton2X + (ltr ? 0 : -1),
				itemY, decrButton2W + 1, itemH);
		this.mySecondIncreaseButton.setBounds(incrButton2X + (ltr ? -1 : 0),
				itemY, incrButton2W + 1, itemH);
		this.incrButton.setBounds(incrButtonX, itemY, incrButtonW, itemH);
		this.decrButton.setBounds(decrButtonX, itemY, decrButtonW, itemH);

		/*
		 * Update the trackRect field.
		 */
		if (ltr) {
			int itrackX = incrButton2X + incrButton2W;
			int itrackW = decrButton2X - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		} else {
			int itrackX = decrButton2X + decrButton2W;
			int itrackW = incrButton2X - itrackX;
			this.trackRect.setBounds(itrackX, itemY, itrackW, itemH);
		}

		/*
		 * Make sure the thumb fits between the buttons. Note that setting the
		 * thumbs bounds causes a repaint.
		 */
		if (thumbW >= (int) trackW) {
			this.setThumbBounds(0, 0, 0, 0);
		} else {
			if (ltr) {
				if (thumbX + thumbW > decrButton2X) {
					thumbX = decrButton2X - thumbW;
				}
				if (thumbX < (incrButton2X + incrButton2W)) {
					thumbX = incrButton2X + incrButton2W + 1;
				}
			} else {
				if (thumbX + thumbW > incrButton2X) {
					thumbX = incrButton2X - thumbW;
				}
				if (thumbX < (decrButton2X + decrButton2W)) {
					thumbX = decrButton2X + decrButton2W + 1;
				}
			}
			this.setThumbBounds(thumbX, itemY, thumbW, itemH);
		}
	}

	/**
	 * Returns the memory usage string.
	 * 
	 * @return The memory usage string.
	 */
	public static String getMemoryUsage() {
		StringBuffer sb = new StringBuffer();
		sb.append("SubstanceScrollBarUI: \n");
		sb.append("\t" + thumbHorizontalMap.size() + " thumb horizontal, "
				+ thumbVerticalMap.size() + " thumb vertical");
		sb.append("\t" + trackHorizontalMap.size() + " track horizontal, "
				+ trackVerticalMap.size() + " track vertical");
		sb.append("\t" + trackFullHorizontalMap.size()
				+ " track full horizontal, " + trackFullVerticalMap.size()
				+ " track full vertical");
		return sb.toString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#createTrackListener()
	 */
	@Override
	protected TrackListener createTrackListener() {
		return new SubstanceTrackListener();
	}

	/**
	 * Track mouse drags. Had to take this one from BasicScrollBarUI since the
	 * setValueForm method is private.
	 */
	protected class SubstanceTrackListener extends TrackListener {
		/**
		 * Current scroll direction.
		 */
		private transient int direction = +1;

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$TrackListener#mouseReleased
		 * (java.awt.event.MouseEvent)
		 */
		@Override
		public void mouseReleased(MouseEvent e) {
			if (SubstanceScrollBarUI.this.isDragging) {
				SubstanceScrollBarUI.this.updateThumbState(e.getX(), e.getY());
			}
			if (SwingUtilities.isRightMouseButton(e)
					|| (!SubstanceScrollBarUI.this
							.getSupportsAbsolutePositioning() && SwingUtilities
							.isMiddleMouseButton(e)))
				return;
			if (!SubstanceScrollBarUI.this.scrollbar.isEnabled())
				return;

			Rectangle r = SubstanceScrollBarUI.this.getTrackBounds();
			SubstanceScrollBarUI.this.scrollbar.repaint(r.x, r.y, r.width,
					r.height);

			SubstanceScrollBarUI.this.trackHighlight = NO_HIGHLIGHT;
			SubstanceScrollBarUI.this.isDragging = false;
			this.offset = 0;
			SubstanceScrollBarUI.this.scrollTimer.stop();
			SubstanceScrollBarUI.this.scrollbar.setValueIsAdjusting(false);
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$TrackListener#mousePressed
		 * (java.awt.event.MouseEvent)
		 */
		@Override
		public void mousePressed(MouseEvent e) {
			// 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.
			if (SwingUtilities.isRightMouseButton(e)
					|| (!SubstanceScrollBarUI.this
							.getSupportsAbsolutePositioning() && SwingUtilities
							.isMiddleMouseButton(e)))
				return;
			if (!SubstanceScrollBarUI.this.scrollbar.isEnabled())
				return;

			if (!SubstanceScrollBarUI.this.scrollbar.hasFocus()
					&& SubstanceScrollBarUI.this.scrollbar
							.isRequestFocusEnabled()) {
				SubstanceScrollBarUI.this.scrollbar.requestFocus();
			}

			SubstanceScrollBarUI.this.scrollbar.setValueIsAdjusting(true);

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

			// Clicked in the Thumb area?
			if (SubstanceScrollBarUI.this.getThumbBounds().contains(
					this.currentMouseX, this.currentMouseY)) {
				switch (SubstanceScrollBarUI.this.scrollbar.getOrientation()) {
				case JScrollBar.VERTICAL:
					this.offset = this.currentMouseY
							- SubstanceScrollBarUI.this.getThumbBounds().y;
					break;
				case JScrollBar.HORIZONTAL:
					this.offset = this.currentMouseX
							- SubstanceScrollBarUI.this.getThumbBounds().x;
					break;
				}
				SubstanceScrollBarUI.this.isDragging = true;
				return;
			} else if (SubstanceScrollBarUI.this
					.getSupportsAbsolutePositioning()
					&& SwingUtilities.isMiddleMouseButton(e)) {
				switch (SubstanceScrollBarUI.this.scrollbar.getOrientation()) {
				case JScrollBar.VERTICAL:
					this.offset = SubstanceScrollBarUI.this.getThumbBounds().height / 2;
					break;
				case JScrollBar.HORIZONTAL:
					this.offset = SubstanceScrollBarUI.this.getThumbBounds().width / 2;
					break;
				}
				SubstanceScrollBarUI.this.isDragging = true;
				this.setValueFrom(e);
				return;
			}
			SubstanceScrollBarUI.this.isDragging = false;

			Dimension sbSize = SubstanceScrollBarUI.this.scrollbar.getSize();
			this.direction = +1;

			switch (SubstanceScrollBarUI.this.scrollbar.getOrientation()) {
			case JScrollBar.VERTICAL:
				if (SubstanceScrollBarUI.this.getThumbBounds().isEmpty()) {
					int scrollbarCenter = sbSize.height / 2;
					this.direction = (this.currentMouseY < scrollbarCenter) ? -1
							: +1;
				} else {
					int thumbY = SubstanceScrollBarUI.this.getThumbBounds().y;
					this.direction = (this.currentMouseY < thumbY) ? -1 : +1;
				}
				break;
			case JScrollBar.HORIZONTAL:
				if (SubstanceScrollBarUI.this.getThumbBounds().isEmpty()) {
					int scrollbarCenter = sbSize.width / 2;
					this.direction = (this.currentMouseX < scrollbarCenter) ? -1
							: +1;
				} else {
					int thumbX = SubstanceScrollBarUI.this.getThumbBounds().x;
					this.direction = (this.currentMouseX < thumbX) ? -1 : +1;
				}
				if (!SubstanceScrollBarUI.this.scrollbar
						.getComponentOrientation().isLeftToRight()) {
					this.direction = -this.direction;
				}
				break;
			}
			SubstanceScrollBarUI.this.scrollByBlock(this.direction);

			SubstanceScrollBarUI.this.scrollTimer.stop();
			SubstanceScrollBarUI.this.scrollListener
					.setDirection(this.direction);
			SubstanceScrollBarUI.this.scrollListener.setScrollByBlock(true);
			this.startScrollTimerIfNecessary();
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$TrackListener#mouseDragged
		 * (java.awt.event.MouseEvent)
		 */
		@Override
		public void mouseDragged(MouseEvent e) {
			// Set the models value to the position of the thumb's top of
			// Vertical scrollbar, or the left/right of Horizontal scrollbar in
			// LTR / RTL scrollbar relative to the origin of
			// the track.
			if (SwingUtilities.isRightMouseButton(e)
					|| (!SubstanceScrollBarUI.this
							.getSupportsAbsolutePositioning() && SwingUtilities
							.isMiddleMouseButton(e)))
				return;
			if (!SubstanceScrollBarUI.this.scrollbar.isEnabled()
					|| SubstanceScrollBarUI.this.getThumbBounds().isEmpty()) {
				return;
			}
			if (SubstanceScrollBarUI.this.isDragging) {
				this.setValueFrom(e);
			} else {
				this.currentMouseX = e.getX();
				this.currentMouseY = e.getY();
				SubstanceScrollBarUI.this.updateThumbState(this.currentMouseX,
						this.currentMouseY);
				this.startScrollTimerIfNecessary();
			}
		}

		/**
		 * Sets the scrollbar value based on the specified mouse event.
		 * 
		 * @param e
		 *            Mouse event.
		 */
		private void setValueFrom(MouseEvent e) {
			boolean active = SubstanceScrollBarUI.this.isThumbRollover();
			BoundedRangeModel model = SubstanceScrollBarUI.this.scrollbar
					.getModel();
			Rectangle thumbR = SubstanceScrollBarUI.this.getThumbBounds();
			int thumbMin = 0, thumbMax = 0, thumbPos;

			ScrollPaneButtonPolicyKind buttonPolicy = SubstanceCoreUtilities
					.getScrollPaneButtonsPolicyKind(SubstanceScrollBarUI.this.scrollbar);

			if (SubstanceScrollBarUI.this.scrollbar.getOrientation() == JScrollBar.VERTICAL) {
				switch (buttonPolicy) {
				case OPPOSITE:
					thumbMin = SubstanceScrollBarUI.this.decrButton.getY()
							+ SubstanceScrollBarUI.this.decrButton.getHeight();
					thumbMax = SubstanceScrollBarUI.this.incrButton.getY()
							- thumbR.height;
					break;
				case ADJACENT:
					thumbMin = 0;
					thumbMax = SubstanceScrollBarUI.this.mySecondDecreaseButton
							.getY()
							- thumbR.height;
					break;
				case NONE:
					thumbMin = 0;
					thumbMax = SubstanceScrollBarUI.this.scrollbar.getSize().height
							- SubstanceScrollBarUI.this.scrollbar.getInsets().bottom
							- thumbR.height;
					break;
				case MULTIPLE:
					thumbMin = SubstanceScrollBarUI.this.decrButton.getY()
							+ SubstanceScrollBarUI.this.decrButton.getHeight();
					thumbMax = SubstanceScrollBarUI.this.mySecondDecreaseButton
							.getY()
							- thumbR.height;
					break;
				case MULTIPLE_BOTH:
					thumbMin = SubstanceScrollBarUI.this.mySecondIncreaseButton
							.getY()
							+ SubstanceScrollBarUI.this.mySecondIncreaseButton
									.getHeight();
					thumbMax = SubstanceScrollBarUI.this.mySecondDecreaseButton
							.getY()
							- thumbR.height;
					break;
				}

				thumbPos = Math.min(thumbMax, Math.max(thumbMin,
						(e.getY() - this.offset)));
				SubstanceScrollBarUI.this.setThumbBounds(thumbR.x, thumbPos,
						thumbR.width, thumbR.height);
			} else {
				if (SubstanceScrollBarUI.this.scrollbar
						.getComponentOrientation().isLeftToRight()) {
					switch (buttonPolicy) {
					case OPPOSITE:
						thumbMin = SubstanceScrollBarUI.this.decrButton.getX()
								+ SubstanceScrollBarUI.this.decrButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.incrButton.getX()
								- thumbR.width;
						break;
					case ADJACENT:
						thumbMin = 0;
						thumbMax = SubstanceScrollBarUI.this.mySecondDecreaseButton
								.getX()
								- thumbR.width;
						break;
					case MULTIPLE:
						thumbMin = SubstanceScrollBarUI.this.decrButton.getX()
								+ SubstanceScrollBarUI.this.decrButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.mySecondDecreaseButton
								.getX()
								- thumbR.width;
						break;
					case MULTIPLE_BOTH:
						thumbMin = SubstanceScrollBarUI.this.mySecondIncreaseButton
								.getX()
								+ SubstanceScrollBarUI.this.mySecondIncreaseButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.mySecondDecreaseButton
								.getX()
								- thumbR.width;
						break;
					case NONE:
						thumbMin = 0;
						thumbMax = SubstanceScrollBarUI.this.scrollbar
								.getSize().width
								- SubstanceScrollBarUI.this.scrollbar
										.getInsets().right - thumbR.width;
						break;
					}
				} else {
					switch (buttonPolicy) {
					case OPPOSITE:
						thumbMin = SubstanceScrollBarUI.this.incrButton.getX()
								+ SubstanceScrollBarUI.this.incrButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.decrButton.getX()
								- thumbR.width;
						break;
					case ADJACENT:
						thumbMin = SubstanceScrollBarUI.this.mySecondDecreaseButton
								.getX()
								+ SubstanceScrollBarUI.this.mySecondDecreaseButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.scrollbar
								.getSize().width
								- SubstanceScrollBarUI.this.scrollbar
										.getInsets().right - thumbR.width;
						break;
					case MULTIPLE:
						thumbMin = SubstanceScrollBarUI.this.mySecondDecreaseButton
								.getX()
								+ SubstanceScrollBarUI.this.mySecondDecreaseButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.decrButton.getX()
								- thumbR.width;
						break;
					case MULTIPLE_BOTH:
						thumbMin = SubstanceScrollBarUI.this.mySecondDecreaseButton
								.getX()
								+ SubstanceScrollBarUI.this.mySecondDecreaseButton
										.getWidth();
						thumbMax = SubstanceScrollBarUI.this.mySecondIncreaseButton
								.getX()
								- thumbR.width;
						break;
					case NONE:
						thumbMin = 0;
						thumbMax = SubstanceScrollBarUI.this.scrollbar
								.getSize().width
								- SubstanceScrollBarUI.this.scrollbar
										.getInsets().right - thumbR.width;
						break;
					}
				}
				// System.out.println(thumbMin + " : " + thumbMax + " : "
				// + (e.getX() - offset));
				thumbPos = Math.min(thumbMax, Math.max(thumbMin,
						(e.getX() - this.offset)));
				SubstanceScrollBarUI.this.setThumbBounds(thumbPos, thumbR.y,
						thumbR.width, thumbR.height);
			}

			/*
			 * Set the scrollbars value. If the thumb has reached the end of the
			 * scrollbar, then just set the value to its maximum. Otherwise
			 * compute the value as accurately as possible.
			 */
			if (thumbPos == thumbMax) {
				if (SubstanceScrollBarUI.this.scrollbar.getOrientation() == JScrollBar.VERTICAL
						|| SubstanceScrollBarUI.this.scrollbar
								.getComponentOrientation().isLeftToRight()) {
					SubstanceScrollBarUI.this.scrollbar.setValue(model
							.getMaximum()
							- model.getExtent());
				} else {
					SubstanceScrollBarUI.this.scrollbar.setValue(model
							.getMinimum());
				}
			} else {
				float valueMax = model.getMaximum() - model.getExtent();
				float valueRange = valueMax - model.getMinimum();
				float thumbValue = thumbPos - thumbMin;
				float thumbRange = thumbMax - thumbMin;
				int value;
				if (SubstanceScrollBarUI.this.scrollbar.getOrientation() == JScrollBar.VERTICAL
						|| SubstanceScrollBarUI.this.scrollbar
								.getComponentOrientation().isLeftToRight()) {
					value = (int) (0.5 + ((thumbValue / thumbRange) * valueRange));
				} else {
					value = (int) (0.5 + (((thumbMax - thumbPos) / thumbRange) * valueRange));
				}

				SubstanceScrollBarUI.this.scrollbar.setValue(value
						+ model.getMinimum());
			}
			SubstanceScrollBarUI.this.setThumbRollover(active);
		}

		/**
		 * If necessary, starts the scroll timer.
		 */
		private void startScrollTimerIfNecessary() {
			if (SubstanceScrollBarUI.this.scrollTimer.isRunning()) {
				return;
			}
			switch (SubstanceScrollBarUI.this.scrollbar.getOrientation()) {
			case JScrollBar.VERTICAL:
				if (this.direction > 0) {
					if (SubstanceScrollBarUI.this.getThumbBounds().y
							+ SubstanceScrollBarUI.this.getThumbBounds().height < ((SubstanceTrackListener) SubstanceScrollBarUI.this.trackListener).currentMouseY) {
						SubstanceScrollBarUI.this.scrollTimer.start();
					}
				} else if (SubstanceScrollBarUI.this.getThumbBounds().y > ((SubstanceTrackListener) SubstanceScrollBarUI.this.trackListener).currentMouseY) {
					SubstanceScrollBarUI.this.scrollTimer.start();
				}
				break;
			case JScrollBar.HORIZONTAL:
				if (this.direction > 0) {
					if (SubstanceScrollBarUI.this.getThumbBounds().x
							+ SubstanceScrollBarUI.this.getThumbBounds().width < ((SubstanceTrackListener) SubstanceScrollBarUI.this.trackListener).currentMouseX) {
						SubstanceScrollBarUI.this.scrollTimer.start();
					}
				} else if (SubstanceScrollBarUI.this.getThumbBounds().x > ((SubstanceTrackListener) SubstanceScrollBarUI.this.trackListener).currentMouseX) {
					SubstanceScrollBarUI.this.scrollTimer.start();
				}
				break;
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$TrackListener#mouseMoved(
		 * java.awt.event.MouseEvent)
		 */
		@Override
		public void mouseMoved(MouseEvent e) {
			if (!SubstanceScrollBarUI.this.isDragging) {
				SubstanceScrollBarUI.this.updateThumbState(e.getX(), e.getY());
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$TrackListener#mouseExited
		 * (java.awt.event.MouseEvent)
		 */
		@Override
		public void mouseExited(MouseEvent e) {
			if (!SubstanceScrollBarUI.this.isDragging) {
				SubstanceScrollBarUI.this.setThumbRollover(false);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see javax.swing.plaf.basic.BasicScrollBarUI#createArrowButtonListener()
	 */
	@Override
	protected ArrowButtonListener createArrowButtonListener() {
		return new SubstanceArrowButtonListener();
	}

	/**
	 * Listener on arrow buttons. Need to override the super implementation for
	 * the {@link ScrollPaneButtonPolicyKind#MULTIPLE_BOTH} policy.
	 * 
	 * @author Kirill Grouchnikov
	 */
	protected class SubstanceArrowButtonListener extends ArrowButtonListener {
		/**
		 * Because we are handling both mousePressed and Actions we need to make
		 * sure we don't fire under both conditions. (keyfocus on scrollbars
		 * causes action without mousePress
		 */
		boolean handledEvent;

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$ArrowButtonListener#mousePressed
		 * (java.awt.event.MouseEvent)
		 */
		@Override
		public void mousePressed(MouseEvent e) {
			if (!SubstanceScrollBarUI.this.scrollbar.isEnabled()) {
				return;
			}
			// not an unmodified left mouse button
			// if(e.getModifiers() != InputEvent.BUTTON1_MASK) {return; }
			if (!SwingUtilities.isLeftMouseButton(e)) {
				return;
			}

			int direction = ((e.getSource() == SubstanceScrollBarUI.this.incrButton) || (e
					.getSource() == SubstanceScrollBarUI.this.mySecondIncreaseButton)) ? 1
					: -1;

			SubstanceScrollBarUI.this.scrollByUnit(direction);
			SubstanceScrollBarUI.this.scrollTimer.stop();
			SubstanceScrollBarUI.this.scrollListener.setDirection(direction);
			SubstanceScrollBarUI.this.scrollListener.setScrollByBlock(false);
			SubstanceScrollBarUI.this.scrollTimer.start();

			this.handledEvent = true;
			if (!SubstanceScrollBarUI.this.scrollbar.hasFocus()
					&& SubstanceScrollBarUI.this.scrollbar
							.isRequestFocusEnabled()) {
				SubstanceScrollBarUI.this.scrollbar.requestFocus();
			}
		}

		/*
		 * (non-Javadoc)
		 * 
		 * @see
		 * javax.swing.plaf.basic.BasicScrollBarUI$ArrowButtonListener#mouseReleased
		 * (java.awt.event.MouseEvent)
		 */
		@Override
		public void mouseReleased(MouseEvent e) {
			SubstanceScrollBarUI.this.scrollTimer.stop();
			this.handledEvent = false;
			SubstanceScrollBarUI.this.scrollbar.setValueIsAdjusting(false);
		}
	}

	/**
	 * Updates the thumb state based on the coordinates.
	 * 
	 * @param x
	 *            X coordinate.
	 * @param y
	 *            Y coordinate.
	 */
	private void updateThumbState(int x, int y) {
		Rectangle rect = this.getThumbBounds();

		this.setThumbRollover(rect.contains(x, y));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * javax.swing.plaf.basic.BasicScrollBarUI#getPreferredSize(javax.swing.
	 * JComponent)
	 */
	@Override
	public Dimension getPreferredSize(JComponent c) {
		if (scrollbar.getOrientation() == JScrollBar.VERTICAL) {
			return new Dimension(scrollBarWidth, Math.max(48,
					5 * scrollBarWidth));
		} else {
			return new Dimension(Math.max(48, 5 * scrollBarWidth),
					scrollBarWidth);
		}
	}
}