Index: trunk/src/scriptbuilder/gui/drawers/RangeSliderUI.java
===================================================================
--- trunk/src/scriptbuilder/gui/drawers/RangeSliderUI.java	(revision 25)
+++ trunk/src/scriptbuilder/gui/drawers/RangeSliderUI.java	(revision 25)
@@ -0,0 +1,616 @@
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Ellipse2D;
+
+import javax.swing.JComponent;
+import javax.swing.JSlider;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.plaf.basic.BasicSliderUI;
+/*
+The MIT License
+
+Copyright (c) 2010 Ernest Yu. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/**
+ * UI delegate for the RangeSlider component.  RangeSliderUI paints two thumbs,
+ * one for the lower value and one for the upper value.
+ */
+class RangeSliderUI extends BasicSliderUI {
+
+    /** Color of selected range. */
+    private Color rangeColor = Color.GREEN;
+    
+    /** Location and size of thumb for upper value. */
+    private Rectangle upperThumbRect;
+    /** Indicator that determines whether upper thumb is selected. */
+    private boolean upperThumbSelected;
+    
+    /** Indicator that determines whether lower thumb is being dragged. */
+    private transient boolean lowerDragging;
+    /** Indicator that determines whether upper thumb is being dragged. */
+    private transient boolean upperDragging;
+    
+    /**
+     * Constructs a RangeSliderUI for the specified slider component.
+     * @param b RangeSlider
+     */
+    public RangeSliderUI(RangeSlider b) {
+        super(b);
+    }
+    
+    /**
+     * Installs this UI delegate on the specified component. 
+     */
+    @Override
+    public void installUI(JComponent c) {
+        upperThumbRect = new Rectangle();
+        super.installUI(c);
+    }
+
+    /**
+     * Creates a listener to handle track events in the specified slider.
+     */
+    @Override
+    protected TrackListener createTrackListener(JSlider slider) {
+        return new RangeTrackListener();
+    }
+
+    /**
+     * Creates a listener to handle change events in the specified slider.
+     */
+    @Override
+    protected ChangeListener createChangeListener(JSlider slider) {
+        return new ChangeHandler();
+    }
+    
+    /**
+     * Updates the dimensions for both thumbs. 
+     */
+    @Override
+    protected void calculateThumbSize() {
+        // Call superclass method for lower thumb size.
+        super.calculateThumbSize();
+        
+        // Set upper thumb size.
+        upperThumbRect.setSize(thumbRect.width, thumbRect.height);
+    }
+    
+    /**
+     * Updates the locations for both thumbs.
+     */
+    @Override
+    protected void calculateThumbLocation() {
+        // Call superclass method for lower thumb location.
+        super.calculateThumbLocation();
+        
+        // Adjust upper value to snap to ticks if necessary.
+        if (slider.getSnapToTicks()) {
+            int upperValue = slider.getValue() + slider.getExtent();
+            int snappedValue = upperValue; 
+            int majorTickSpacing = slider.getMajorTickSpacing();
+            int minorTickSpacing = slider.getMinorTickSpacing();
+            int tickSpacing = 0;
+            
+            if (minorTickSpacing > 0) {
+                tickSpacing = minorTickSpacing;
+            } else if (majorTickSpacing > 0) {
+                tickSpacing = majorTickSpacing;
+            }
+
+            if (tickSpacing != 0) {
+                // If it's not on a tick, change the value
+                if ((upperValue - slider.getMinimum()) % tickSpacing != 0) {
+                    float temp = (float)(upperValue - slider.getMinimum()) / (float)tickSpacing;
+                    int whichTick = Math.round(temp);
+                    snappedValue = slider.getMinimum() + (whichTick * tickSpacing);
+                }
+
+                if (snappedValue != upperValue) { 
+                    slider.setExtent(snappedValue - slider.getValue());
+                }
+            }
+        }
+        
+        // Calculate upper thumb location.  The thumb is centered over its 
+        // value on the track.
+        if (slider.getOrientation() == JSlider.HORIZONTAL) {
+            int upperPosition = xPositionForValue(slider.getValue() + slider.getExtent());
+            upperThumbRect.x = upperPosition - (upperThumbRect.width / 2);
+            upperThumbRect.y = trackRect.y;
+            
+        } else {
+            int upperPosition = yPositionForValue(slider.getValue() + slider.getExtent());
+            upperThumbRect.x = trackRect.x;
+            upperThumbRect.y = upperPosition - (upperThumbRect.height / 2);
+        }
+    }
+    
+    /**
+     * Returns the size of a thumb.
+     */
+    @Override
+    protected Dimension getThumbSize() {
+        return new Dimension(12, 12);
+    }
+
+    /**
+     * Paints the slider.  The selected thumb is always painted on top of the
+     * other thumb.
+     */
+    @Override
+    public void paint(Graphics g, JComponent c) {
+        super.paint(g, c);
+        
+        Rectangle clipRect = g.getClipBounds();
+        if (upperThumbSelected) {
+            // Paint lower thumb first, then upper thumb.
+            if (clipRect.intersects(thumbRect)) {
+                paintLowerThumb(g);
+            }
+            if (clipRect.intersects(upperThumbRect)) {
+                paintUpperThumb(g);
+            }
+            
+        } else {
+            // Paint upper thumb first, then lower thumb.
+            if (clipRect.intersects(upperThumbRect)) {
+                paintUpperThumb(g);
+            }
+            if (clipRect.intersects(thumbRect)) {
+                paintLowerThumb(g);
+            }
+        }
+    }
+    
+    /**
+     * Paints the track.
+     */
+    @Override
+    public void paintTrack(Graphics g) {
+        // Draw track.
+        super.paintTrack(g);
+        
+        Rectangle trackBounds = trackRect;
+        
+        if (slider.getOrientation() == JSlider.HORIZONTAL) {
+            // Determine position of selected range by moving from the middle
+            // of one thumb to the other.
+            int lowerX = thumbRect.x + (thumbRect.width / 2);
+            int upperX = upperThumbRect.x + (upperThumbRect.width / 2);
+            
+            // Determine track position.
+            int cy = (trackBounds.height / 2) - 2;
+
+            // Save color and shift position.
+            Color oldColor = g.getColor();
+            g.translate(trackBounds.x, trackBounds.y + cy);
+            
+            // Draw selected range.
+            g.setColor(rangeColor);
+            for (int y = 0; y <= 3; y++) {
+                g.drawLine(lowerX - trackBounds.x, y, upperX - trackBounds.x, y);
+            }
+
+            // Restore position and color.
+            g.translate(-trackBounds.x, -(trackBounds.y + cy));
+            g.setColor(oldColor);
+            
+        } else {
+            // Determine position of selected range by moving from the middle
+            // of one thumb to the other.
+            int lowerY = thumbRect.x + (thumbRect.width / 2);
+            int upperY = upperThumbRect.x + (upperThumbRect.width / 2);
+            
+            // Determine track position.
+            int cx = (trackBounds.width / 2) - 2;
+
+            // Save color and shift position.
+            Color oldColor = g.getColor();
+            g.translate(trackBounds.x + cx, trackBounds.y);
+
+            // Draw selected range.
+            g.setColor(rangeColor);
+            for (int x = 0; x <= 3; x++) {
+                g.drawLine(x, lowerY - trackBounds.y, x, upperY - trackBounds.y);
+            }
+            
+            // Restore position and color.
+            g.translate(-(trackBounds.x + cx), -trackBounds.y);
+            g.setColor(oldColor);
+        }
+    }
+    
+    /**
+     * Overrides superclass method to do nothing.  Thumb painting is handled
+     * within the <code>paint()</code> method.
+     */
+    @Override
+    public void paintThumb(Graphics g) {
+        // Do nothing.
+    }
+
+    /**
+     * Paints the thumb for the lower value using the specified graphics object.
+     */
+    private void paintLowerThumb(Graphics g) {
+        Rectangle knobBounds = thumbRect;
+        int w = knobBounds.width;
+        int h = knobBounds.height;      
+        
+        // Create graphics copy.
+        Graphics2D g2d = (Graphics2D) g.create();
+
+        // Create default thumb shape.
+        Shape thumbShape = createThumbShape(w - 1, h - 1);
+
+        // Draw thumb.
+        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+            RenderingHints.VALUE_ANTIALIAS_ON);
+        g2d.translate(knobBounds.x, knobBounds.y);
+
+        g2d.setColor(Color.CYAN);
+        g2d.fill(thumbShape);
+
+        g2d.setColor(Color.BLUE);
+        g2d.draw(thumbShape);
+        
+        // Dispose graphics.
+        g2d.dispose();
+    }
+    
+    /**
+     * Paints the thumb for the upper value using the specified graphics object.
+     */
+    private void paintUpperThumb(Graphics g) {
+        Rectangle knobBounds = upperThumbRect;
+        int w = knobBounds.width;
+        int h = knobBounds.height;      
+        
+        // Create graphics copy.
+        Graphics2D g2d = (Graphics2D) g.create();
+
+        // Create default thumb shape.
+        Shape thumbShape = createThumbShape(w - 1, h - 1);
+
+        // Draw thumb.
+        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+            RenderingHints.VALUE_ANTIALIAS_ON);
+        g2d.translate(knobBounds.x, knobBounds.y);
+
+        g2d.setColor(Color.PINK);
+        g2d.fill(thumbShape);
+
+        g2d.setColor(Color.RED);
+        g2d.draw(thumbShape);
+
+        // Dispose graphics.
+        g2d.dispose();
+    }
+
+    /**
+     * Returns a Shape representing a thumb.
+     */
+    private Shape createThumbShape(int width, int height) {
+        // Use circular shape.
+        Ellipse2D shape = new Ellipse2D.Double(0, 0, width, height);
+        return shape;
+    }
+    
+    /** 
+     * Sets the location of the upper thumb, and repaints the slider.  This is
+     * called when the upper thumb is dragged to repaint the slider.  The
+     * <code>setThumbLocation()</code> method performs the same task for the
+     * lower thumb.
+     */
+    private void setUpperThumbLocation(int x, int y) {
+        Rectangle upperUnionRect = new Rectangle();
+        upperUnionRect.setBounds(upperThumbRect);
+
+        upperThumbRect.setLocation(x, y);
+
+        SwingUtilities.computeUnion(upperThumbRect.x, upperThumbRect.y, upperThumbRect.width, upperThumbRect.height, upperUnionRect);
+        slider.repaint(upperUnionRect.x, upperUnionRect.y, upperUnionRect.width, upperUnionRect.height);
+    }
+    
+    /**
+     * Moves the selected thumb in the specified direction by a block increment.
+     * This method is called when the user presses the Page Up or Down keys.
+     */
+    public void scrollByBlock(int direction) {
+        synchronized (slider) {
+            int blockIncrement = (slider.getMaximum() - slider.getMinimum()) / 10;
+            if (blockIncrement <= 0 && slider.getMaximum() > slider.getMinimum()) {
+                blockIncrement = 1;
+            }
+            int delta = blockIncrement * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
+            
+            if (upperThumbSelected) {
+                int oldValue = ((RangeSlider) slider).getUpperValue();
+                ((RangeSlider) slider).setUpperValue(oldValue + delta);
+            } else {
+                int oldValue = slider.getValue();
+                slider.setValue(oldValue + delta);
+            }
+        }
+    }
+    
+    /**
+     * Moves the selected thumb in the specified direction by a unit increment.
+     * This method is called when the user presses one of the arrow keys.
+     */
+    public void scrollByUnit(int direction) {
+        synchronized (slider) {
+            int delta = 1 * ((direction > 0) ? POSITIVE_SCROLL : NEGATIVE_SCROLL);
+            
+            if (upperThumbSelected) {
+                int oldValue = ((RangeSlider) slider).getUpperValue();
+                ((RangeSlider) slider).setUpperValue(oldValue + delta);
+            } else {
+                int oldValue = slider.getValue();
+                slider.setValue(oldValue + delta);
+            }
+        }       
+    }
+    
+    /**
+     * Listener to handle model change events.  This calculates the thumb 
+     * locations and repaints the slider if the value change is not caused by
+     * dragging a thumb.
+     */
+    public class ChangeHandler implements ChangeListener {
+        public void stateChanged(ChangeEvent arg0) {
+            if (!lowerDragging && !upperDragging) {
+                calculateThumbLocation();
+                slider.repaint();
+            }
+        }
+    }
+    
+    /**
+     * Listener to handle mouse movements in the slider track.
+     */
+    public class RangeTrackListener extends TrackListener {
+        
+        @Override
+        public void mousePressed(MouseEvent e) {
+            if (!slider.isEnabled()) {
+                return;
+            }
+
+            currentMouseX = e.getX();
+            currentMouseY = e.getY();
+
+            if (slider.isRequestFocusEnabled()) {
+                slider.requestFocus();
+            }
+            
+            // Determine which thumb is pressed.  If the upper thumb is 
+            // selected (last one dragged), then check its position first;
+            // otherwise check the position of the lower thumb first.
+            boolean lowerPressed = false;
+            boolean upperPressed = false;
+            if (upperThumbSelected || slider.getMinimum() == slider.getValue()) {
+                if (upperThumbRect.contains(currentMouseX, currentMouseY)) {
+                    upperPressed = true;
+                } else if (thumbRect.contains(currentMouseX, currentMouseY)) {
+                    lowerPressed = true;
+                }
+            } else {
+                if (thumbRect.contains(currentMouseX, currentMouseY)) {
+                    lowerPressed = true;
+                } else if (upperThumbRect.contains(currentMouseX, currentMouseY)) {
+                    upperPressed = true;
+                }
+            }
+
+            // Handle lower thumb pressed.
+            if (lowerPressed) {
+                switch (slider.getOrientation()) {
+                case JSlider.VERTICAL:
+                    offset = currentMouseY - thumbRect.y;
+                    break;
+                case JSlider.HORIZONTAL:
+                    offset = currentMouseX - thumbRect.x;
+                    break;
+                }
+                upperThumbSelected = false;
+                lowerDragging = true;
+                return;
+            }
+            lowerDragging = false;
+            
+            // Handle upper thumb pressed.
+            if (upperPressed) {
+                switch (slider.getOrientation()) {
+                case JSlider.VERTICAL:
+                    offset = currentMouseY - upperThumbRect.y;
+                    break;
+                case JSlider.HORIZONTAL:
+                    offset = currentMouseX - upperThumbRect.x;
+                    break;
+                }
+                upperThumbSelected = true;
+                upperDragging = true;
+                return;
+            }
+            upperDragging = false;
+        }
+        
+        @Override
+        public void mouseReleased(MouseEvent e) {
+            lowerDragging = false;
+            upperDragging = false;
+            slider.setValueIsAdjusting(false);
+            super.mouseReleased(e);
+        }
+        
+        @Override
+        public void mouseDragged(MouseEvent e) {
+            if (!slider.isEnabled()) {
+                return;
+            }
+
+            currentMouseX = e.getX();
+            currentMouseY = e.getY();
+
+            if (lowerDragging) {
+                slider.setValueIsAdjusting(true);
+                moveLowerThumb();
+                
+            } else if (upperDragging) {
+                slider.setValueIsAdjusting(true);
+                moveUpperThumb();
+            }
+        }
+        
+        @Override
+        public boolean shouldScroll(int direction) {
+            return false;
+        }
+
+        /**
+         * Moves the location of the lower thumb, and sets its corresponding 
+         * value in the slider.
+         */
+        private void moveLowerThumb() {
+            int thumbMiddle = 0;
+            
+            switch (slider.getOrientation()) {
+            case JSlider.VERTICAL:      
+                int halfThumbHeight = thumbRect.height / 2;
+                int thumbTop = currentMouseY - offset;
+                int trackTop = trackRect.y;
+                int trackBottom = trackRect.y + (trackRect.height - 1);
+                int vMax = yPositionForValue(slider.getValue() + slider.getExtent());
+
+                // Apply bounds to thumb position.
+                if (drawInverted()) {
+                    trackBottom = vMax;
+                } else {
+                    trackTop = vMax;
+                }
+                thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
+                thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);
+
+                setThumbLocation(thumbRect.x, thumbTop);
+
+                // Update slider value.
+                thumbMiddle = thumbTop + halfThumbHeight;
+                slider.setValue(valueForYPosition(thumbMiddle));
+                break;
+                
+            case JSlider.HORIZONTAL:
+                int halfThumbWidth = thumbRect.width / 2;
+                int thumbLeft = currentMouseX - offset;
+                int trackLeft = trackRect.x;
+                int trackRight = trackRect.x + (trackRect.width - 1);
+                int hMax = xPositionForValue(slider.getValue() + slider.getExtent());
+
+                // Apply bounds to thumb position.
+                if (drawInverted()) {
+                    trackLeft = hMax;
+                } else {
+                    trackRight = hMax;
+                }
+                thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
+                thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);
+
+                setThumbLocation(thumbLeft, thumbRect.y);
+
+                // Update slider value.
+                thumbMiddle = thumbLeft + halfThumbWidth;
+                slider.setValue(valueForXPosition(thumbMiddle));
+                break;
+                
+            default:
+                return;
+            }
+        }
+
+        /**
+         * Moves the location of the upper thumb, and sets its corresponding 
+         * value in the slider.
+         */
+        private void moveUpperThumb() {
+            int thumbMiddle = 0;
+            
+            switch (slider.getOrientation()) {
+            case JSlider.VERTICAL:      
+                int halfThumbHeight = thumbRect.height / 2;
+                int thumbTop = currentMouseY - offset;
+                int trackTop = trackRect.y;
+                int trackBottom = trackRect.y + (trackRect.height - 1);
+                int vMin = yPositionForValue(slider.getValue());
+
+                // Apply bounds to thumb position.
+                if (drawInverted()) {
+                    trackTop = vMin;
+                } else {
+                    trackBottom = vMin;
+                }
+                thumbTop = Math.max(thumbTop, trackTop - halfThumbHeight);
+                thumbTop = Math.min(thumbTop, trackBottom - halfThumbHeight);
+
+                setUpperThumbLocation(thumbRect.x, thumbTop);
+
+                // Update slider extent.
+                thumbMiddle = thumbTop + halfThumbHeight;
+                slider.setExtent(valueForYPosition(thumbMiddle) - slider.getValue());
+                break;
+                
+            case JSlider.HORIZONTAL:
+                int halfThumbWidth = thumbRect.width / 2;
+                int thumbLeft = currentMouseX - offset;
+                int trackLeft = trackRect.x;
+                int trackRight = trackRect.x + (trackRect.width - 1);
+                int hMin = xPositionForValue(slider.getValue());
+
+                // Apply bounds to thumb position.
+                if (drawInverted()) {
+                    trackRight = hMin;
+                } else {
+                    trackLeft = hMin;
+                }
+                thumbLeft = Math.max(thumbLeft, trackLeft - halfThumbWidth);
+                thumbLeft = Math.min(thumbLeft, trackRight - halfThumbWidth);
+
+                setUpperThumbLocation(thumbLeft, thumbRect.y);
+                
+                // Update slider extent.
+                thumbMiddle = thumbLeft + halfThumbWidth;
+                slider.setExtent(valueForXPosition(thumbMiddle) - slider.getValue());
+                break;
+                
+            default:
+                return;
+            }
+        }
+    }
+}
Index: trunk/src/scriptbuilder/gui/drawers/RangeSlider.java
===================================================================
--- trunk/src/scriptbuilder/gui/drawers/RangeSlider.java	(revision 25)
+++ trunk/src/scriptbuilder/gui/drawers/RangeSlider.java	(revision 25)
@@ -0,0 +1,120 @@
+
+import javax.swing.JSlider;
+/*
+The MIT License
+
+Copyright (c) 2010 Ernest Yu. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/**
+ * An extension of JSlider to select a range of values using two thumb controls.
+ * The thumb controls are used to select the lower and upper value of a range
+ * with predetermined minimum and maximum values.
+ * 
+ * <p>Note that RangeSlider makes use of the default BoundedRangeModel, which 
+ * supports an inner range defined by a value and an extent.  The upper value
+ * returned by RangeSlider is simply the lower value plus the extent.</p>
+ */
+public class RangeSlider extends JSlider {
+
+    /**
+     * Constructs a RangeSlider with default minimum and maximum values of 0
+     * and 100.
+     */
+    public RangeSlider() {
+        initSlider();
+    }
+
+    /**
+     * Constructs a RangeSlider with the specified default minimum and maximum 
+     * values.
+     */
+    public RangeSlider(int min, int max) {
+        super(min, max);
+        initSlider();
+    }
+
+    /**
+     * Initializes the slider by setting default properties.
+     */
+    private void initSlider() {
+        setOrientation(HORIZONTAL);
+    }
+
+    /**
+     * Overrides the superclass method to install the UI delegate to draw two
+     * thumbs.
+     */
+    @Override
+    public void updateUI() {
+        setUI(new RangeSliderUI(this));
+        // Update UI for slider labels.  This must be called after updating the
+        // UI of the slider.  Refer to JSlider.updateUI().
+        updateLabelUIs();
+    }
+
+    /**
+     * Returns the lower value in the range.
+     */
+    @Override
+    public int getValue() {
+        return super.getValue();
+    }
+
+    /**
+     * Sets the lower value in the range.
+     */
+    @Override
+    public void setValue(int value) {
+        int oldValue = getValue();
+        if (oldValue == value) {
+            return;
+        }
+
+        // Compute new value and extent to maintain upper value.
+        int oldExtent = getExtent();
+        int newValue = Math.min(Math.max(getMinimum(), value), oldValue + oldExtent);
+        int newExtent = oldExtent + oldValue - newValue;
+
+        // Set new value and extent, and fire a single change event.
+        getModel().setRangeProperties(newValue, newExtent, getMinimum(), 
+            getMaximum(), getValueIsAdjusting());
+    }
+
+    /**
+     * Returns the upper value in the range.
+     */
+    public int getUpperValue() {
+        return getValue() + getExtent();
+    }
+
+    /**
+     * Sets the upper value in the range.
+     */
+    public void setUpperValue(int value) {
+        // Compute new extent.
+        int lowerValue = getValue();
+        int newExtent = Math.min(Math.max(0, value - lowerValue), getMaximum() - lowerValue);
+        
+        // Set extent to set upper value.
+        setExtent(newExtent);
+    }
+}
Index: trunk/src/scriptbuilder/gui/drawers/RangeSliderDemo.java
===================================================================
--- trunk/src/scriptbuilder/gui/drawers/RangeSliderDemo.java	(revision 25)
+++ trunk/src/scriptbuilder/gui/drawers/RangeSliderDemo.java	(revision 25)
@@ -0,0 +1,127 @@
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridBagConstraints;
+import java.awt.GridBagLayout;
+import java.awt.Insets;
+
+import javax.swing.BorderFactory;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.UIManager;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+/*
+The MIT License
+
+Copyright (c) 2010 Ernest Yu. All rights reserved.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+*/
+
+/**
+ * Demo application panel to display a range slider.
+ */
+public class RangeSliderDemo extends JPanel {
+
+    private JLabel rangeSliderLabel1 = new JLabel();
+    private JLabel rangeSliderValue1 = new JLabel();
+    private JLabel rangeSliderLabel2 = new JLabel();
+    private JLabel rangeSliderValue2 = new JLabel();
+    private RangeSlider rangeSlider = new RangeSlider();
+
+    public RangeSliderDemo() {
+        setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
+        setLayout(new GridBagLayout());
+        
+        rangeSliderLabel1.setText("Lower value:");
+        rangeSliderLabel2.setText("Upper value:");
+        rangeSliderValue1.setHorizontalAlignment(JLabel.LEFT);
+        rangeSliderValue2.setHorizontalAlignment(JLabel.LEFT);
+        
+        rangeSlider.setPreferredSize(new Dimension(240, rangeSlider.getPreferredSize().height));
+        rangeSlider.setMinimum(0);
+        rangeSlider.setMaximum(10);
+        
+        // Add listener to update display.
+        rangeSlider.addChangeListener(new ChangeListener() {
+            public void stateChanged(ChangeEvent e) {
+                RangeSlider slider = (RangeSlider) e.getSource();
+                rangeSliderValue1.setText(String.valueOf(slider.getValue()));
+                rangeSliderValue2.setText(String.valueOf(slider.getUpperValue()));
+            }
+        });
+
+        add(rangeSliderLabel1, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
+            GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 3, 3), 0, 0));
+        add(rangeSliderValue1, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0,
+            GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 3, 0), 0, 0));
+        add(rangeSliderLabel2, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0,
+            GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 3, 3), 0, 0));
+        add(rangeSliderValue2, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0,
+            GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 6, 0), 0, 0));
+        add(rangeSlider      , new GridBagConstraints(0, 2, 2, 1, 0.0, 0.0,
+            GridBagConstraints.NORTHWEST, GridBagConstraints.NONE, new Insets(0, 0, 0, 0), 0, 0));
+    }
+    
+    public void display() {
+        // Initialize values.
+        rangeSlider.setValue(3);
+        rangeSlider.setUpperValue(7);
+        
+        // Initialize value display.
+        rangeSliderValue1.setText(String.valueOf(rangeSlider.getValue()));
+        rangeSliderValue2.setText(String.valueOf(rangeSlider.getUpperValue()));
+        
+        // Create window frame.
+        JFrame frame = new JFrame();
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        frame.setResizable(false);
+        frame.setTitle("Range Slider Demo");
+        
+        // Set window content and validate.
+        frame.getContentPane().setLayout(new BorderLayout());
+        frame.getContentPane().add(this, BorderLayout.CENTER);
+        frame.pack();
+        
+        // Set window location and display.
+        frame.setLocationRelativeTo(null);
+        frame.setVisible(true);
+    }
+    
+    /**
+     * Main application method.
+     * @param args String[]
+     */
+    public static void main(String[] args) {
+        try {
+            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+
+        SwingUtilities.invokeLater(new Runnable() {
+            public void run() {
+                new RangeSliderDemo().display();
+            }
+        });
+    }
+}
