001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.gui.component.ui;
020
021import icy.gui.component.RangeSlider;
022
023import java.awt.Graphics;
024import java.awt.Graphics2D;
025import java.awt.Insets;
026import java.awt.Rectangle;
027import java.awt.Shape;
028import java.awt.event.MouseEvent;
029import java.awt.geom.AffineTransform;
030import java.awt.geom.GeneralPath;
031import java.awt.geom.Point2D;
032import java.awt.image.BufferedImage;
033import java.beans.PropertyChangeEvent;
034import java.beans.PropertyChangeListener;
035import java.util.Map;
036
037import javax.swing.ButtonModel;
038import javax.swing.DefaultButtonModel;
039import javax.swing.Icon;
040import javax.swing.JComponent;
041import javax.swing.JSlider;
042import javax.swing.SwingConstants;
043import javax.swing.event.ChangeEvent;
044import javax.swing.event.ChangeListener;
045
046import org.pushingpixels.lafwidget.LafWidgetUtilities;
047import org.pushingpixels.substance.api.ColorSchemeAssociationKind;
048import org.pushingpixels.substance.api.ComponentState;
049import org.pushingpixels.substance.api.SubstanceColorScheme;
050import org.pushingpixels.substance.api.painter.border.SubstanceBorderPainter;
051import org.pushingpixels.substance.api.painter.fill.ClassicFillPainter;
052import org.pushingpixels.substance.api.painter.fill.SubstanceFillPainter;
053import org.pushingpixels.substance.internal.animation.StateTransitionTracker;
054import org.pushingpixels.substance.internal.animation.TransitionAwareUI;
055import org.pushingpixels.substance.internal.painter.BackgroundPaintingUtils;
056import org.pushingpixels.substance.internal.ui.SubstanceSliderUI;
057import org.pushingpixels.substance.internal.utils.HashMapKey;
058import org.pushingpixels.substance.internal.utils.RolloverControlListener;
059import org.pushingpixels.substance.internal.utils.SubstanceColorSchemeUtilities;
060import org.pushingpixels.substance.internal.utils.SubstanceCoreUtilities;
061import org.pushingpixels.substance.internal.utils.SubstanceOutlineUtilities;
062import org.pushingpixels.substance.internal.utils.SubstanceSizeUtils;
063
064/**
065 * UI delegate for the RangeSlider component with Substance AndFeel.
066 * RangeSliderUI paints two thumbs, one for the lower value and one for the upper value.
067 * 
068 * @author Stephane Dallongeville
069 */
070public class RangeSliderUI extends SubstanceSliderUI
071{
072    /** Location and size of thumb for upper value. */
073    Rectangle upperThumbRect;
074    /** Indicator that determines whether upper thumb is selected. */
075    boolean upperThumbSelected;
076
077    /** Indicator that determines whether lower thumb is being dragged. */
078    transient boolean lowerDragging;
079    /** Indicator that determines whether upper thumb is being dragged. */
080    transient boolean upperDragging;
081
082    /**
083     * Surrogate button model for tracking the general slider transitions.
084     */
085    ButtonModel sliderModel;
086    /**
087     * Surrogate button model for tracking the upper thumb transitions.
088     */
089    ButtonModel upperThumbModel;
090    /**
091     * General slider transition tracker.
092     */
093    protected StateTransitionTracker sliderStateTransitionTracker;
094    /**
095     * Upper thumb transition tracker.
096     */
097    protected StateTransitionTracker upperThumbStateTransitionTracker;
098
099    /**
100     * Listener for general slider transition animations.
101     */
102    protected RolloverControlListener sliderRolloverListener;
103    /**
104     * Listener for upper thumb transition animations.
105     */
106    protected RolloverControlListener upperThumbRolloverListener;
107
108    /**
109     * Listener on property change events.
110     */
111    protected PropertyChangeListener sliderPropertyChangeListener;
112
113    /**
114     * Needed to return correct transition tracker on thumb paint.
115     */
116    private boolean paintingLowerThumb;
117    private boolean paintingUpperThumb;
118
119    /**
120     * Constructs a RangeSliderUI for the specified slider component.
121     * 
122     * @param rangeSlider
123     *        RangeSlider
124     */
125    public RangeSliderUI(RangeSlider rangeSlider)
126    {
127        super(rangeSlider);
128
129        sliderModel = new DefaultButtonModel();
130        sliderModel.setArmed(false);
131        sliderModel.setSelected(false);
132        sliderModel.setPressed(false);
133        sliderModel.setRollover(false);
134        sliderModel.setEnabled(rangeSlider.isEnabled());
135
136        upperThumbModel = new DefaultButtonModel();
137        upperThumbModel.setArmed(false);
138        upperThumbModel.setSelected(false);
139        upperThumbModel.setPressed(false);
140        upperThumbModel.setRollover(false);
141        upperThumbModel.setEnabled(rangeSlider.isEnabled());
142
143        sliderStateTransitionTracker = new StateTransitionTracker(rangeSlider, sliderModel);
144        upperThumbStateTransitionTracker = new StateTransitionTracker(rangeSlider, upperThumbModel);
145
146        paintingLowerThumb = false;
147        paintingUpperThumb = false;
148    }
149
150    /**
151     * Installs this UI delegate on the specified component.
152     */
153    @Override
154    public void installUI(JComponent c)
155    {
156        upperThumbRect = new Rectangle();
157        super.installUI(c);
158    }
159
160    @Override
161    protected void installListeners(JSlider slider)
162    {
163        super.installListeners(slider);
164
165        sliderRolloverListener = new RolloverControlListener(new TransitionAwareUI()
166        {
167            @Override
168            public boolean isInside(MouseEvent me)
169            {
170                final double x = me.getX();
171                final double y = me.getY();
172                return isInsideLowerThumbInternal(x, y) || isInsideUpperThumbInternal(x, y);
173            }
174
175            @Override
176            public StateTransitionTracker getTransitionTracker()
177            {
178                return sliderStateTransitionTracker;
179            }
180        }, sliderModel);
181
182        slider.addMouseListener(sliderRolloverListener);
183        slider.addMouseMotionListener(sliderRolloverListener);
184
185        upperThumbRolloverListener = new RolloverControlListener(new TransitionAwareUI()
186        {
187            @Override
188            public boolean isInside(MouseEvent me)
189            {
190                return isInsideUpperThumb(me.getX(), me.getY());
191            }
192
193            @Override
194            public StateTransitionTracker getTransitionTracker()
195            {
196                return upperThumbStateTransitionTracker;
197            }
198        }, upperThumbModel);
199
200        slider.addMouseListener(upperThumbRolloverListener);
201        slider.addMouseMotionListener(upperThumbRolloverListener);
202
203        sliderPropertyChangeListener = new PropertyChangeListener()
204        {
205            @Override
206            public void propertyChange(PropertyChangeEvent evt)
207            {
208                if ("enabled".equals(evt.getPropertyName()))
209                {
210                    final boolean enabled = RangeSliderUI.this.slider.isEnabled();
211
212                    sliderModel.setEnabled(enabled);
213                    upperThumbModel.setEnabled(enabled);
214                }
215            }
216        };
217        slider.addPropertyChangeListener(sliderPropertyChangeListener);
218
219        sliderStateTransitionTracker.registerModelListeners();
220        sliderStateTransitionTracker.registerFocusListeners();
221        upperThumbStateTransitionTracker.registerModelListeners();
222        upperThumbStateTransitionTracker.registerFocusListeners();
223    }
224
225    @Override
226    protected void uninstallListeners(JSlider slider)
227    {
228        super.uninstallListeners(slider);
229
230        slider.removeMouseListener(sliderRolloverListener);
231        slider.removeMouseMotionListener(sliderRolloverListener);
232        sliderRolloverListener = null;
233        slider.removeMouseListener(upperThumbRolloverListener);
234        slider.removeMouseMotionListener(upperThumbRolloverListener);
235        upperThumbRolloverListener = null;
236
237        slider.removePropertyChangeListener(sliderPropertyChangeListener);
238        sliderPropertyChangeListener = null;
239
240        sliderStateTransitionTracker.unregisterModelListeners();
241        sliderStateTransitionTracker.unregisterFocusListeners();
242        upperThumbStateTransitionTracker.unregisterModelListeners();
243        upperThumbStateTransitionTracker.unregisterFocusListeners();
244    }
245
246    /**
247     * Creates a listener to handle track events in the specified slider.
248     */
249    @Override
250    protected TrackListener createTrackListener(JSlider slider)
251    {
252        return new RangeTrackListener();
253    }
254
255    /**
256     * Creates a listener to handle change events in the specified slider.
257     */
258    @Override
259    protected ChangeListener createChangeListener(JSlider slider)
260    {
261        return new ChangeHandler();
262    }
263
264    /**
265     * Updates the dimensions for both thumbs.
266     */
267    @Override
268    protected void calculateThumbSize()
269    {
270        // Call superclass method for lower thumb size.
271        super.calculateThumbSize();
272
273        // Set upper thumb size.
274        upperThumbRect.setSize(thumbRect.width, thumbRect.height);
275    }
276
277    /**
278     * Updates the locations for both thumbs.
279     */
280    @Override
281    protected void calculateThumbLocation()
282    {
283        // Call superclass method for lower thumb location.
284        super.calculateThumbLocation();
285
286        // Adjust upper value to snap to ticks if necessary.
287        if (slider.getSnapToTicks())
288        {
289            int upperValue = slider.getValue() + slider.getExtent();
290            int snappedValue = upperValue;
291            int majorTickSpacing = slider.getMajorTickSpacing();
292            int minorTickSpacing = slider.getMinorTickSpacing();
293            int tickSpacing = 0;
294
295            if (minorTickSpacing > 0)
296            {
297                tickSpacing = minorTickSpacing;
298            }
299            else if (majorTickSpacing > 0)
300            {
301                tickSpacing = majorTickSpacing;
302            }
303
304            if (tickSpacing != 0)
305            {
306                // If it's not on a tick, change the value
307                if ((upperValue - slider.getMinimum()) % tickSpacing != 0)
308                {
309                    float temp = (float) (upperValue - slider.getMinimum()) / (float) tickSpacing;
310                    int whichTick = Math.round(temp);
311                    snappedValue = slider.getMinimum() + (whichTick * tickSpacing);
312                }
313
314                if (snappedValue != upperValue)
315                {
316                    slider.setExtent(snappedValue - slider.getValue());
317                }
318            }
319        }
320
321        Rectangle trackRect = this.getPaintTrackRect();
322
323        if (slider.getOrientation() == SwingConstants.HORIZONTAL)
324        {
325            int valuePosition = xPositionForValue(slider.getValue() + slider.getExtent());
326
327            double centerY = trackRect.y + trackRect.height / 2.0;
328            upperThumbRect.y = (int) (centerY - upperThumbRect.height / 2.0) + 1;
329            upperThumbRect.x = valuePosition - upperThumbRect.width / 2;
330        }
331        else
332        {
333            int valuePosition = yPositionForValue(slider.getValue() + slider.getExtent());
334
335            double centerX = trackRect.x + trackRect.width / 2.0;
336            upperThumbRect.x = (int) (centerX - upperThumbRect.width / 2.0) + 1;
337            upperThumbRect.y = valuePosition - (upperThumbRect.height / 2);
338        }
339    }
340
341    @Override
342    public boolean isInside(MouseEvent me)
343    {
344        return isInsideLowerThumb(me.getX(), me.getY());
345    }
346
347    public boolean isInsideLowerThumbInternal(double x, double y)
348    {
349        final Rectangle thumbB = this.thumbRect;
350        return thumbB != null && thumbB.contains(x, y);
351    }
352
353    public boolean isInsideLowerThumb(double x, double y)
354    {
355        // inside lower ?
356        if (isInsideLowerThumbInternal(x, y))
357        {
358            // also inside upper ?
359            if (isInsideUpperThumbInternal(x, y))
360            {
361                final double dl = Point2D.distance(thumbRect.getCenterX(), thumbRect.getCenterY(), x, y);
362                final double du = Point2D.distance(upperThumbRect.getCenterX(), upperThumbRect.getCenterY(), x, y);
363
364                return (dl < du);
365            }
366
367            return true;
368        }
369
370        return false;
371    }
372
373    public boolean isInsideUpperThumbInternal(double x, double y)
374    {
375        final Rectangle upperThumbR = upperThumbRect;
376        return (upperThumbR != null) && upperThumbR.contains(x, y);
377    }
378
379    public boolean isInsideUpperThumb(double x, double y)
380    {
381        // inside lower ?
382        if (isInsideUpperThumbInternal(x, y))
383        {
384            // also inside upper ?
385            if (isInsideLowerThumbInternal(x, y))
386            {
387                // find closest one
388                final double dl = Point2D.distance(thumbRect.getCenterX(), thumbRect.getCenterY(), x, y);
389                final double du = Point2D.distance(upperThumbRect.getCenterX(), upperThumbRect.getCenterY(), x, y);
390
391                return (du <= dl);
392            }
393
394            return true;
395        }
396
397        return false;
398    }
399
400    @Override
401    public StateTransitionTracker getTransitionTracker()
402    {
403        if (paintingLowerThumb)
404            return super.getTransitionTracker();
405        if (paintingUpperThumb)
406            return upperThumbStateTransitionTracker;
407
408        return sliderStateTransitionTracker;
409    }
410
411    /**
412     * Returns the rectangle of track for painting.
413     * 
414     * @return The rectangle of track for painting.
415     */
416    private Rectangle getPaintTrackRect()
417    {
418        int trackLeft = 0;
419        int trackRight;
420        int trackTop = 0;
421        int trackBottom;
422        int trackWidth = this.getTrackWidth();
423
424        if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
425        {
426            trackTop = 3 + this.insetCache.top + 2 * this.focusInsets.top;
427            trackBottom = trackTop + trackWidth - 1;
428            trackRight = this.trackRect.width;
429            return new Rectangle(this.trackRect.x + trackLeft, trackTop, trackRight - trackLeft, trackBottom - trackTop);
430        }
431
432        if (this.slider.getPaintLabels() || this.slider.getPaintTicks())
433        {
434            if (this.slider.getComponentOrientation().isLeftToRight())
435            {
436                trackLeft = trackRect.x + this.insetCache.left + this.focusInsets.left;
437                trackRight = trackLeft + trackWidth - 1;
438            }
439            else
440            {
441                trackRight = trackRect.x + trackRect.width - this.insetCache.right - this.focusInsets.right;
442                trackLeft = trackRight - trackWidth - 1;
443            }
444        }
445        else
446        {
447            // horizontally center the track
448            if (this.slider.getComponentOrientation().isLeftToRight())
449            {
450                trackLeft = (this.insetCache.left + this.focusInsets.left + this.slider.getWidth()
451                        - this.insetCache.right - this.focusInsets.right)
452                        / 2 - trackWidth / 2;
453                trackRight = trackLeft + trackWidth - 1;
454            }
455            else
456            {
457                trackRight = (this.insetCache.left + this.focusInsets.left + this.slider.getWidth()
458                        - this.insetCache.right - this.focusInsets.right)
459                        / 2 + trackWidth / 2;
460                trackLeft = trackRight - trackWidth - 1;
461            }
462        }
463
464        trackBottom = this.trackRect.height - 1;
465        return new Rectangle(trackLeft, this.trackRect.y + trackTop, trackRight - trackLeft, trackBottom - trackTop);
466    }
467
468    @Override
469    public void paint(Graphics g, final JComponent c)
470    {
471        Graphics2D graphics = (Graphics2D) g.create();
472
473        ComponentState currState = ComponentState.getState(sliderModel, slider);
474        float alpha = SubstanceColorSchemeUtilities.getAlpha(slider, currState);
475
476        BackgroundPaintingUtils.updateIfOpaque(graphics, c);
477
478        recalculateIfInsetsChanged();
479        recalculateIfOrientationChanged();
480        final Rectangle clip = graphics.getClipBounds();
481
482        if (!clip.intersects(trackRect) && slider.getPaintTrack())
483            calculateGeometry();
484
485        graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.slider, alpha, g));
486        if (slider.getPaintTrack() && clip.intersects(trackRect))
487        {
488            paintTrack(graphics);
489        }
490        if (slider.getPaintTicks() && clip.intersects(tickRect))
491        {
492            paintTicks(graphics);
493        }
494        // don't paint focus as component is not focusable
495        // paintFocus(graphics);
496        if (clip.intersects(thumbRect))
497        {
498            paintLowerThumb(graphics);
499        }
500        if (clip.intersects(upperThumbRect))
501        {
502            paintUpperThumb(graphics);
503        }
504        graphics.setComposite(LafWidgetUtilities.getAlphaComposite(this.slider, 1.0f, g));
505        if (slider.getPaintLabels() && clip.intersects(labelRect))
506        {
507            paintLabels(graphics);
508        }
509
510        graphics.dispose();
511    }
512
513    public void paintLowerThumb(Graphics g)
514    {
515        paintingLowerThumb = true;
516
517        // default implementation
518        paintThumb(g);
519
520        paintingLowerThumb = false;
521    }
522
523    public void paintUpperThumb(Graphics g)
524    {
525        paintingUpperThumb = true;
526
527        final Graphics2D graphics = (Graphics2D) g.create();
528        final Rectangle knobBounds = upperThumbRect;
529
530        graphics.translate(knobBounds.x, knobBounds.y);
531
532        final Icon icon = getIcon();
533
534        if (slider.getOrientation() == SwingConstants.HORIZONTAL)
535        {
536            if (icon != null)
537                icon.paintIcon(this.slider, graphics, -1, 0);
538        }
539        else
540        {
541            if (slider.getComponentOrientation().isLeftToRight())
542            {
543                if (icon != null)
544                    icon.paintIcon(this.slider, graphics, 0, -1);
545            }
546            else
547            {
548                if (icon != null)
549                    icon.paintIcon(this.slider, graphics, 0, 1);
550            }
551        }
552
553        graphics.dispose();
554
555        paintingUpperThumb = false;
556    }
557
558    @Override
559    public void paintTrack(Graphics g)
560    {
561        Graphics2D graphics = (Graphics2D) g.create();
562
563        boolean drawInverted = drawInverted();
564
565        Rectangle paintRect = getPaintTrackRect();
566
567        // Width and height of the painting rectangle.
568        int width = paintRect.width;
569        int height = paintRect.height;
570
571        if (this.slider.getOrientation() == SwingConstants.VERTICAL)
572        {
573            // apply rotation / translate transformation on vertical
574            // slider tracks
575            int temp = width;
576            // noinspection SuspiciousNameCombination
577            width = height;
578            height = temp;
579            AffineTransform at = graphics.getTransform();
580            at.translate(paintRect.x, width + paintRect.y);
581            at.rotate(-Math.PI / 2);
582            graphics.setTransform(at);
583        }
584        else
585        {
586            graphics.translate(paintRect.x, paintRect.y);
587        }
588
589        StateTransitionTracker.ModelStateInfo modelStateInfo = sliderStateTransitionTracker.getModelStateInfo();
590
591        SubstanceColorScheme trackSchemeUnselected = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
592                slider.isEnabled() ? ComponentState.ENABLED : ComponentState.DISABLED_UNSELECTED);
593        SubstanceColorScheme trackBorderSchemeUnselected = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
594                ColorSchemeAssociationKind.BORDER, this.slider.isEnabled() ? ComponentState.ENABLED
595                        : ComponentState.DISABLED_UNSELECTED);
596        paintSliderTrack(graphics, drawInverted, trackSchemeUnselected, trackBorderSchemeUnselected, width, height);
597
598        Map<ComponentState, StateTransitionTracker.StateContributionInfo> activeStates = modelStateInfo
599                .getStateContributionMap();
600        for (Map.Entry<ComponentState, StateTransitionTracker.StateContributionInfo> activeEntry : activeStates
601                .entrySet())
602        {
603            ComponentState activeState = activeEntry.getKey();
604            if (!activeState.isActive())
605                continue;
606
607            float contribution = activeEntry.getValue().getContribution();
608            if (contribution == 0.0f)
609                continue;
610
611            graphics.setComposite(LafWidgetUtilities.getAlphaComposite(slider, contribution, g));
612
613            SubstanceColorScheme activeFillScheme = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
614                    activeState);
615            SubstanceColorScheme activeBorderScheme = SubstanceColorSchemeUtilities.getColorScheme(this.slider,
616                    ColorSchemeAssociationKind.BORDER, activeState);
617            paintSliderTrackSelected(graphics, paintRect, activeFillScheme, activeBorderScheme, width, height);
618        }
619
620        graphics.dispose();
621    }
622
623    /**
624     * Paints the slider track.
625     * 
626     * @param graphics
627     *        Graphics.
628     * @param drawInverted
629     *        Indicates whether the value-range shown for the slider is
630     *        reversed.
631     * @param fillColorScheme
632     *        Fill color scheme.
633     * @param borderScheme
634     *        Border color scheme.
635     * @param width
636     *        Track width.
637     * @param height
638     *        Track height.
639     */
640    private void paintSliderTrack(Graphics2D graphics, boolean drawInverted, SubstanceColorScheme fillColorScheme,
641            SubstanceColorScheme borderScheme, int width, int height)
642    {
643        Graphics2D g2d = (Graphics2D) graphics.create();
644
645        SubstanceFillPainter fillPainter = ClassicFillPainter.INSTANCE;
646        SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(this.slider);
647
648        int componentFontSize = SubstanceSizeUtils.getComponentFontSize(this.slider);
649        int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth(componentFontSize) / 2.0);
650        float radius = SubstanceSizeUtils.getClassicButtonCornerRadius(componentFontSize) / 2.0f;
651        int borderThickness = (int) SubstanceSizeUtils.getBorderStrokeWidth(componentFontSize);
652
653        HashMapKey key = SubstanceCoreUtilities.getHashKey(width, height, radius, borderDelta, borderThickness,
654                fillColorScheme.getDisplayName(), borderScheme.getDisplayName());
655
656        BufferedImage trackImage = trackCache.get(key);
657        if (trackImage == null)
658        {
659            trackImage = SubstanceCoreUtilities.getBlankImage(width + 1, height + 1);
660            Graphics2D cacheGraphics = trackImage.createGraphics();
661
662            Shape contour = SubstanceOutlineUtilities.getBaseOutline(width + 1, height + 1, radius, null, borderDelta);
663
664            fillPainter.paintContourBackground(cacheGraphics, slider, width, height, contour, false, fillColorScheme,
665                    false);
666
667            GeneralPath contourInner = SubstanceOutlineUtilities.getBaseOutline(width + 1, height + 1, radius
668                    - borderThickness, null, borderThickness + borderDelta);
669            borderPainter
670                    .paintBorder(cacheGraphics, slider, width + 1, height + 1, contour, contourInner, borderScheme);
671
672            trackCache.put(key, trackImage);
673            cacheGraphics.dispose();
674        }
675
676        g2d.drawImage(trackImage, 0, 0, null);
677
678        g2d.dispose();
679    }
680
681    /**
682     * Paints the selected part of the slider track.
683     * 
684     * @param graphics
685     *        Graphics.
686     * @param drawInverted
687     *        Indicates whether the value-range shown for the slider is
688     *        reversed.
689     * @param paintRect
690     *        Selected portion.
691     * @param fillScheme
692     *        Fill color scheme.
693     * @param borderScheme
694     *        Border color scheme.
695     * @param width
696     *        Track width.
697     * @param height
698     *        Track height.
699     */
700    private void paintSliderTrackSelected(Graphics2D graphics, Rectangle paintRect, SubstanceColorScheme fillScheme,
701            SubstanceColorScheme borderScheme, int width, int height)
702    {
703        Graphics2D g2d = (Graphics2D) graphics.create();
704        Insets insets = this.slider.getInsets();
705        insets.top /= 2;
706        insets.left /= 2;
707        insets.bottom /= 2;
708        insets.right /= 2;
709
710        SubstanceFillPainter fillPainter = SubstanceCoreUtilities.getFillPainter(this.slider);
711        SubstanceBorderPainter borderPainter = SubstanceCoreUtilities.getBorderPainter(this.slider);
712        float radius = SubstanceSizeUtils.getClassicButtonCornerRadius(SubstanceSizeUtils.getComponentFontSize(slider)) / 2.0f;
713        int borderDelta = (int) Math.floor(SubstanceSizeUtils.getBorderStrokeWidth(SubstanceSizeUtils
714                .getComponentFontSize(slider)) / 2.0);
715
716        // fill selected portion
717        if (this.slider.isEnabled())
718        {
719            if (this.slider.getOrientation() == SwingConstants.HORIZONTAL)
720            {
721                int ltPos = thumbRect.x + (this.thumbRect.width / 2) - paintRect.x;
722                int utPos = upperThumbRect.x + (this.upperThumbRect.width / 2) - paintRect.x;
723
724                int fillMinX;
725                int fillMaxX;
726
727                if (ltPos < utPos)
728                {
729                    fillMinX = ltPos;
730                    fillMaxX = utPos;
731                }
732                else
733                {
734                    fillMinX = utPos;
735                    fillMaxX = ltPos;
736                }
737
738                int fillWidth = fillMaxX - fillMinX;
739                int fillHeight = height + 1;
740                if ((fillWidth > 0) && (fillHeight > 0))
741                {
742                    Shape contour = SubstanceOutlineUtilities.getBaseOutline(fillWidth, fillHeight, radius, null,
743                            borderDelta);
744                    g2d.translate(fillMinX, 0);
745                    fillPainter.paintContourBackground(g2d, this.slider, fillWidth, fillHeight, contour, false,
746                            fillScheme, false);
747                    borderPainter.paintBorder(g2d, this.slider, fillWidth, fillHeight, contour, null, borderScheme);
748                }
749            }
750            else
751            {
752                int ltPos = thumbRect.y + (this.thumbRect.height / 2) - paintRect.y;
753                int utPos = upperThumbRect.y + (this.upperThumbRect.height / 2) - paintRect.y;
754                int fillMin;
755                int fillMax;
756
757                if (ltPos < utPos)
758                {
759                    fillMin = ltPos;
760                    fillMax = utPos;
761                }
762                else
763                {
764                    fillMin = utPos;
765                    fillMax = ltPos;
766                }
767
768                // if (this.drawInverted())
769                // {
770                // fillMin = 0;
771                // fillMax = middleOfThumb;
772                // // fix for issue 368 - inverted vertical sliders
773                // g2d.translate(width + 2 - middleOfThumb, 0);
774                // }
775                // else
776                // {
777                // fillMin = middleOfThumb;
778                // fillMax = width + 1;
779                // }
780
781                int fillWidth = fillMax - fillMin;
782                int fillHeight = height + 1;
783                if ((fillWidth > 0) && (fillHeight > 0))
784                {
785                    Shape contour = SubstanceOutlineUtilities.getBaseOutline(fillWidth, fillHeight, radius, null,
786                            borderDelta);
787                    g2d.translate(paintRect.height - fillMax, 0);
788                    fillPainter.paintContourBackground(g2d, this.slider, fillWidth, fillHeight, contour, false,
789                            fillScheme, false);
790                    borderPainter.paintBorder(g2d, this.slider, fillWidth, fillHeight, contour, null, borderScheme);
791                }
792            }
793        }
794        g2d.dispose();
795    }
796
797    /**
798     * Sets the location of the upper thumb, and repaints the slider. This is
799     * called when the upper thumb is dragged to repaint the slider. The
800     * <code>setThumbLocation()</code> method performs the same task for the
801     * lower thumb.
802     */
803    void setUpperThumbLocation(int x, int y)
804    {
805        upperThumbRect.setLocation(x, y);
806        slider.repaint();
807    }
808
809    /**
810     * Moves the selected thumb in the specified direction by a block increment.
811     * This method is called when the user presses the Page Up or Down keys.
812     */
813    @Override
814    public void scrollByBlock(int direction)
815    {
816        synchronized (slider)
817        {
818            int blockIncrement = (slider.getMaximum() - slider.getMinimum()) / 10;
819            if (blockIncrement <= 0 && slider.getMaximum() > slider.getMinimum())
820            {
821                blockIncrement = 1;
822            }
823            int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
824
825            if (upperThumbSelected)
826            {
827                int oldValue = ((RangeSlider) slider).getUpperValue();
828                ((RangeSlider) slider).setUpperValue(oldValue + delta);
829            }
830            else
831            {
832                int oldValue = slider.getValue();
833                slider.setValue(oldValue + delta);
834            }
835        }
836    }
837
838    /**
839     * Moves the selected thumb in the specified direction by a unit increment.
840     * This method is called when the user presses one of the arrow keys.
841     */
842    @Override
843    public void scrollByUnit(int direction)
844    {
845        synchronized (slider)
846        {
847            int delta = 1 * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
848
849            if (upperThumbSelected)
850            {
851                int oldValue = ((RangeSlider) slider).getUpperValue();
852                ((RangeSlider) slider).setUpperValue(oldValue + delta);
853            }
854            else
855            {
856                int oldValue = slider.getValue();
857                slider.setValue(oldValue + delta);
858            }
859        }
860    }
861
862    /**
863     * Listener to handle model change events. This calculates the thumb
864     * locations and repaints the slider if the value change is not caused by
865     * dragging a thumb.
866     */
867    public class ChangeHandler implements ChangeListener
868    {
869        @Override
870        public void stateChanged(ChangeEvent arg0)
871        {
872            if (!lowerDragging && !upperDragging)
873            {
874                calculateThumbLocation();
875                slider.repaint();
876            }
877        }
878    }
879
880    /**
881     * Listener to handle mouse movements in the slider track.
882     */
883    public class RangeTrackListener extends TrackListener
884    {
885        @Override
886        public void mousePressed(MouseEvent e)
887        {
888            if (!slider.isEnabled())
889                return;
890
891            currentMouseX = e.getX();
892            currentMouseY = e.getY();
893
894            if (slider.isRequestFocusEnabled())
895                slider.requestFocus();
896
897            // Determine which thumb is pressed. If the upper thumb is
898            // selected (last one dragged), then check its position first;
899            // otherwise check the position of the lower thumb first.
900            boolean lowerPressed = false;
901            boolean upperPressed = false;
902            if (isInsideLowerThumb(currentMouseX, currentMouseY))
903                lowerPressed = true;
904            else if (isInsideUpperThumb(currentMouseX, currentMouseY))
905                upperPressed = true;
906
907            // Handle lower thumb pressed.
908            if (lowerPressed)
909            {
910                switch (slider.getOrientation())
911                {
912                    case SwingConstants.VERTICAL:
913                        offset = currentMouseY - thumbRect.y;
914                        break;
915                    case SwingConstants.HORIZONTAL:
916                        offset = currentMouseX - thumbRect.x;
917                        break;
918                }
919                upperThumbSelected = false;
920                lowerDragging = true;
921                return;
922            }
923            lowerDragging = false;
924
925            // Handle upper thumb pressed.
926            if (upperPressed)
927            {
928                switch (slider.getOrientation())
929                {
930                    case SwingConstants.VERTICAL:
931                        offset = currentMouseY - upperThumbRect.y;
932                        break;
933                    case SwingConstants.HORIZONTAL:
934                        offset = currentMouseX - upperThumbRect.x;
935                        break;
936                }
937                upperThumbSelected = true;
938                upperDragging = true;
939                return;
940            }
941            upperDragging = false;
942        }
943
944        @Override
945        public void mouseReleased(MouseEvent e)
946        {
947            lowerDragging = false;
948            upperDragging = false;
949            slider.setValueIsAdjusting(false);
950            super.mouseReleased(e);
951        }
952
953        @Override
954        public void mouseDragged(MouseEvent e)
955        {
956            if (!slider.isEnabled())
957            {
958                return;
959            }
960
961            currentMouseX = e.getX();
962            currentMouseY = e.getY();
963
964            if (lowerDragging)
965            {
966                slider.setValueIsAdjusting(true);
967                moveLowerThumb();
968
969            }
970            else if (upperDragging)
971            {
972                slider.setValueIsAdjusting(true);
973                moveUpperThumb();
974            }
975        }
976
977        @Override
978        public boolean shouldScroll(int direction)
979        {
980            return false;
981        }
982
983        /**
984         * Moves the location of the lower thumb, and sets its corresponding
985         * value in the slider.
986         */
987        private void moveLowerThumb()
988        {
989            int thumbMiddle = 0;
990
991            switch (slider.getOrientation())
992            {
993                case SwingConstants.VERTICAL:
994                    int halfThumbHeight = thumbRect.height / 2;
995                    int thumbTop = currentMouseY - offset;
996                    int trackTop = trackRect.y;
997                    int trackBottom = trackRect.y + (trackRect.height - 1);
998                    int vMax = yPositionForValue(slider.getValue() + slider.getExtent());
999
1000                    // Apply bounds to thumb position.
1001                    if (drawInverted())
1002                    {
1003                        trackBottom = vMax;
1004                    }
1005                    else
1006                    {
1007                        trackTop = vMax;
1008                    }
1009                    thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
1010                    thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);
1011
1012                    setThumbLocation(thumbRect.x, thumbTop);
1013
1014                    // Update slider value.
1015                    thumbMiddle = thumbTop + halfThumbHeight;
1016                    slider.setValue(valueForYPosition(thumbMiddle));
1017                    break;
1018
1019                case SwingConstants.HORIZONTAL:
1020                    int halfThumbWidth = thumbRect.width / 2;
1021                    int thumbLeft = currentMouseX - offset;
1022                    int trackLeft = trackRect.x;
1023                    int trackRight = trackRect.x + (trackRect.width - 1);
1024                    int hMax = xPositionForValue(slider.getValue() + slider.getExtent());
1025
1026                    // Apply bounds to thumb position.
1027                    if (drawInverted())
1028                    {
1029                        trackLeft = hMax;
1030                    }
1031                    else
1032                    {
1033                        trackRight = hMax;
1034                    }
1035                    thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
1036                    thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);
1037
1038                    setThumbLocation(thumbLeft, thumbRect.y);
1039
1040                    // Update slider value.
1041                    thumbMiddle = thumbLeft + halfThumbWidth;
1042                    slider.setValue(valueForXPosition(thumbMiddle));
1043                    break;
1044
1045                default:
1046                    return;
1047            }
1048        }
1049
1050        /**
1051         * Moves the location of the upper thumb, and sets its corresponding
1052         * value in the slider.
1053         */
1054        private void moveUpperThumb()
1055        {
1056            int thumbMiddle = 0;
1057
1058            switch (slider.getOrientation())
1059            {
1060                case SwingConstants.VERTICAL:
1061                    int halfThumbHeight = thumbRect.height / 2;
1062                    int thumbTop = currentMouseY - offset;
1063                    int trackTop = trackRect.y;
1064                    int trackBottom = trackRect.y + (trackRect.height - 1);
1065                    int vMin = yPositionForValue(slider.getValue());
1066
1067                    // Apply bounds to thumb position.
1068                    if (drawInverted())
1069                    {
1070                        trackTop = vMin;
1071                    }
1072                    else
1073                    {
1074                        trackBottom = vMin;
1075                    }
1076                    thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
1077                    thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);
1078
1079                    setUpperThumbLocation(thumbRect.x, thumbTop);
1080
1081                    // Update slider extent.
1082                    thumbMiddle = thumbTop + halfThumbHeight;
1083                    slider.setExtent(valueForYPosition(thumbMiddle) - slider.getValue());
1084                    break;
1085
1086                case SwingConstants.HORIZONTAL:
1087                    int halfThumbWidth = thumbRect.width / 2;
1088                    int thumbLeft = currentMouseX - offset;
1089                    int trackLeft = trackRect.x;
1090                    int trackRight = trackRect.x + (trackRect.width - 1);
1091                    int hMin = xPositionForValue(slider.getValue());
1092
1093                    // Apply bounds to thumb position.
1094                    if (drawInverted())
1095                    {
1096                        trackRight = hMin;
1097                    }
1098                    else
1099                    {
1100                        trackLeft = hMin;
1101                    }
1102                    thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
1103                    thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);
1104
1105                    setUpperThumbLocation(thumbLeft, thumbRect.y);
1106
1107                    // Update slider extent.
1108                    thumbMiddle = thumbLeft + halfThumbWidth;
1109                    slider.setExtent(valueForXPosition(thumbMiddle) - slider.getValue());
1110                    break;
1111
1112                default:
1113                    return;
1114            }
1115        }
1116    }
1117}