You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by tv...@apache.org on 2009/03/16 17:16:58 UTC

svn commit: r754926 [33/38] - in /incubator/pivot/tags/v1.0: ./ charts-test/ charts-test/src/ charts-test/src/pivot/ charts-test/src/pivot/charts/ charts-test/src/pivot/charts/test/ charts/ charts/lib/ charts/src/ charts/src/pivot/ charts/src/pivot/cha...

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraRollupSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraRollupSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraRollupSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraRollupSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,659 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.RoundRectangle2D;
+
+import pivot.collections.Sequence;
+import pivot.util.Vote;
+import pivot.wtk.Button;
+import pivot.wtk.ButtonPressListener;
+import pivot.wtk.Component;
+import pivot.wtk.ComponentMouseButtonListener;
+import pivot.wtk.Container;
+import pivot.wtk.Cursor;
+import pivot.wtk.Dimensions;
+import pivot.wtk.Mouse;
+import pivot.wtk.PushButton;
+import pivot.wtk.Rollup;
+import pivot.wtk.RollupListener;
+import pivot.wtk.Theme;
+import pivot.wtk.effects.Transition;
+import pivot.wtk.effects.TransitionListener;
+import pivot.wtk.effects.easing.Easing;
+import pivot.wtk.effects.easing.Quadratic;
+import pivot.wtk.media.Image;
+import pivot.wtk.skin.ButtonSkin;
+import pivot.wtk.skin.ContainerSkin;
+
+/**
+ * Rollup skin.
+ * <p>
+ * TODO Optimize this class by performing preferred size calculation in one
+ * pass.
+ *
+ * @author tvolkert
+ */
+public class TerraRollupSkin extends ContainerSkin
+    implements RollupListener, ButtonPressListener {
+    private class ExpansionTransition extends Transition {
+        private int height1;
+        private int height2;
+        private boolean reverse;
+
+        private int originalPreferredHeight;
+        private int height;
+
+        private Easing easing = new Quadratic();
+
+        public ExpansionTransition(int height1, int height2, boolean reverse, int duration, int rate) {
+            super(duration, rate, false);
+
+            this.height1 = height1;
+            this.height2 = height2;
+            this.reverse = reverse;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+
+        @Override
+        public void start(TransitionListener transitionListener) {
+            Rollup rollup = (Rollup)getComponent();
+            originalPreferredHeight = rollup.isPreferredHeightSet() ?
+                rollup.getPreferredHeight() : -1;
+
+            super.start(transitionListener);
+        }
+
+        @Override
+        public void stop() {
+            Rollup rollup = (Rollup)getComponent();
+            rollup.setPreferredHeight(originalPreferredHeight);
+
+            super.stop();
+        }
+
+        @Override
+        protected void update() {
+            float percentComplete = getPercentComplete();
+
+            if (percentComplete < 1f) {
+                int elapsedTime = getElapsedTime();
+                int duration = getDuration();
+
+                height = (int)(height1 + (height2 - height1) * percentComplete);
+                if (reverse) {
+                    height = (int)easing.easeIn(elapsedTime, height1, height2 - height1, duration);
+                } else {
+                    height = (int)easing.easeOut(elapsedTime, height1, height2 - height1, duration);
+                }
+
+                Rollup rollup = (Rollup)getComponent();
+                rollup.setPreferredHeight(height);
+            }
+        }
+    }
+
+    protected class RollupButton extends PushButton {
+        public RollupButton() {
+            super(null);
+            setSkin(new RollupButtonSkin());
+        }
+    }
+
+    protected class RollupButtonSkin extends ButtonSkin {
+        @Override
+        public boolean isFocusable() {
+            return false;
+        }
+
+        public int getPreferredWidth(int height) {
+            RollupButton rollupButton = (RollupButton)getComponent();
+            Button.DataRenderer dataRenderer = rollupButton.getDataRenderer();
+            dataRenderer.render(rollupButton.getButtonData(), rollupButton, false);
+            return dataRenderer.getPreferredWidth(height);
+        }
+
+        public int getPreferredHeight(int width) {
+            RollupButton rollupButton = (RollupButton)getComponent();
+            Button.DataRenderer dataRenderer = rollupButton.getDataRenderer();
+            dataRenderer.render(rollupButton.getButtonData(), rollupButton, false);
+            return dataRenderer.getPreferredHeight(width);
+        }
+
+        public Dimensions getPreferredSize() {
+            RollupButton rollupButton = (RollupButton)getComponent();
+            Button.DataRenderer dataRenderer = rollupButton.getDataRenderer();
+            dataRenderer.render(rollupButton.getButtonData(), rollupButton, false);
+            Dimensions contentSize = dataRenderer.getPreferredSize();
+            return new Dimensions(contentSize.width, contentSize.height);
+        }
+
+        public void paint(Graphics2D graphics) {
+            RollupButton rollupButton = (RollupButton)getComponent();
+
+            // Paint the content
+            Button.DataRenderer dataRenderer = rollupButton.getDataRenderer();
+            dataRenderer.render(rollupButton.getButtonData(), rollupButton, false);
+
+            Dimensions contentSize = dataRenderer.getPreferredSize();
+            dataRenderer.setSize(contentSize.width, contentSize.height);
+            dataRenderer.paint(graphics);
+        }
+
+        @Override
+        public void mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
+            PushButton pushButton = (PushButton)getComponent();
+            pushButton.press();
+        }
+    }
+
+    protected abstract class ButtonImage extends Image {
+        public int getWidth() {
+            return 7;
+        }
+
+        public int getHeight() {
+            return 7;
+        }
+    }
+
+    protected class ExpandImage extends ButtonImage {
+        public void paint(Graphics2D graphics) {
+            graphics.setStroke(new BasicStroke(0));
+            graphics.setPaint(buttonColor);
+
+            GeneralPath shape = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+            shape.moveTo(0, 0);
+            shape.lineTo(6, 3);
+            shape.lineTo(0, 6);
+            shape.closePath();
+
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            graphics.draw(shape);
+            graphics.fill(shape);
+        }
+    }
+
+    protected class CollapseImage extends ButtonImage {
+        public void paint(Graphics2D graphics) {
+            graphics.setStroke(new BasicStroke(0));
+            graphics.setPaint(buttonColor);
+
+            GeneralPath shape = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+            shape.moveTo(0, 0);
+            shape.lineTo(3, 6);
+            shape.lineTo(6, 0);
+            shape.closePath();
+
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            graphics.draw(shape);
+            graphics.fill(shape);
+        }
+    }
+
+    protected class BulletImage extends ButtonImage {
+        public void paint(Graphics2D graphics) {
+            graphics.setStroke(new BasicStroke(0));
+            graphics.setPaint(buttonColor);
+
+            RoundRectangle2D.Double shape = new RoundRectangle2D.Double(1, 1, 4, 4, 2, 2);
+
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            graphics.draw(shape);
+            graphics.fill(shape);
+        }
+    }
+
+    private class ToggleComponentMouseHandler
+        implements ComponentMouseButtonListener {
+        public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        public void mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
+            Rollup rollup = (Rollup)getComponent();
+            rollup.setExpanded(!rollup.isExpanded());
+        }
+    }
+
+    private RollupButton rollupButton = null;
+    private Component toggleComponent = null;
+    private ToggleComponentMouseHandler toggleComponentMouseHandler =
+        new ToggleComponentMouseHandler();
+    private ExpandImage expandImage = new ExpandImage();
+    private CollapseImage collapseImage = new CollapseImage();
+    private BulletImage bulletImage = new BulletImage();
+
+    private ExpansionTransition expandTransition = null;
+    private ExpansionTransition collapseTransition = null;
+
+    private Color buttonColor;
+    private int spacing;
+    private int buffer;
+    private boolean justify;
+    private boolean firstChildToggles;
+
+    private static final int EXPANSION_DURATION = 250;
+    private static final int EXPANSION_RATE = 30;
+
+    public TerraRollupSkin() {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        buttonColor = theme.getColor(9);
+        spacing = 4;
+        buffer = 4;
+        justify = false;
+        firstChildToggles = true;
+    }
+
+    @Override
+    public void install(Component component) {
+        super.install(component);
+
+        Rollup rollup = (Rollup)component;
+        rollup.getRollupListeners().add(this);
+
+        updateToggleComponent();
+
+        rollupButton = new RollupButton();
+        updateRollupButton();
+        rollup.add(rollupButton);
+        rollupButton.getButtonPressListeners().add(this);
+    }
+
+    @Override
+    public void uninstall() {
+        Rollup rollup = (Rollup)getComponent();
+        rollup.getRollupListeners().remove(this);
+
+        rollupButton.getButtonPressListeners().remove(this);
+        rollup.remove(rollupButton);
+        rollupButton = null;
+
+        if (toggleComponent != null) {
+            toggleComponent.getComponentMouseButtonListeners().remove(toggleComponentMouseHandler);
+            toggleComponent = null;
+        }
+
+        super.uninstall();
+    }
+
+    @Override
+    public int getPreferredWidth(int height) {
+        Rollup rollup = (Rollup)getComponent();
+
+        int preferredWidth = 0;
+
+        // Preferred width is the max of our childrens' preferred widths, plus
+        // the button width, buffer, and padding. If we're collapsed, we only
+        // look at the first child.
+        for (int i = 0, n = rollup.getLength(); i < n; i++) {
+            Component component = rollup.get(i);
+
+            if (component == rollupButton) {
+                // Ignore "private" component
+                continue;
+            }
+
+            if (component.isDisplayable()) {
+                int componentPreferredWidth = component.getPreferredWidth(-1);
+                preferredWidth = Math.max(preferredWidth, componentPreferredWidth);
+            }
+
+            if (!rollup.isExpanded()) {
+                // If we're collapsed, we only look at the first child.
+                break;
+            }
+        }
+
+        preferredWidth += rollupButton.getPreferredWidth(-1) + buffer;
+
+        return preferredWidth;
+    }
+
+    @Override
+    public int getPreferredHeight(int width) {
+        Rollup rollup = (Rollup)getComponent();
+        return getPreferredHeight(width, rollup.isExpanded());
+    }
+
+    private int getPreferredHeight(int width, boolean expanded) {
+        Rollup rollup = (Rollup)getComponent();
+
+        // Preferred height is the sum of our childrens' preferred heights,
+        // plus spacing and padding.
+        Dimensions rollupButtonPreferredSize = rollupButton.getPreferredSize();
+
+        if (justify
+            && width != -1) {
+            width = Math.max(width - rollupButtonPreferredSize.width - buffer, 0);
+        } else {
+            width = -1;
+        }
+
+        int preferredHeight = 0;
+
+        int displayableComponentCount = 0;
+        for (int i = 0, n = rollup.getLength(); i < n; i++) {
+            Component component = rollup.get(i);
+
+            if (component == rollupButton) {
+                // Ignore "private" component
+                continue;
+            }
+
+            if (component.isDisplayable()) {
+                preferredHeight += component.getPreferredHeight(width);
+                displayableComponentCount++;
+            }
+
+            if (!expanded) {
+                // If we're collapsed, we only look at the first child.
+                break;
+            }
+        }
+
+        if (displayableComponentCount > 0) {
+            preferredHeight += (displayableComponentCount - 1) * spacing;
+        }
+
+        preferredHeight = Math.max(preferredHeight,
+            rollupButtonPreferredSize.height);
+
+        return preferredHeight;
+    }
+
+    public void layout() {
+        Rollup rollup = (Rollup)getComponent();
+        Dimensions rollupButtonSize = rollupButton.getPreferredSize();
+        rollupButton.setSize(rollupButtonSize);
+
+        int x = rollupButtonSize.width + buffer;
+        int y = 0;
+        int justifiedWidth = Math.max(getWidth() - rollupButtonSize.width - buffer, 0);
+
+        Component firstComponent = null;
+
+        for (int i = 0, n = rollup.getLength(); i < n; i++) {
+            Component component = rollup.get(i);
+
+            if (component == rollupButton) {
+                // Ignore "private" component
+                continue;
+            }
+
+            if (firstComponent == null) {
+                firstComponent = component;
+            }
+
+            if ((component == firstComponent
+                || rollup.isExpanded())
+                && component.isDisplayable()) {
+                // We lay this child out and make sure it's painted.
+                component.setVisible(true);
+
+                int componentWidth, componentHeight;
+                if (justify) {
+                    componentWidth = justifiedWidth;
+                    componentHeight = component.getPreferredHeight(componentWidth);
+                } else {
+                    Dimensions componentPreferredSize = component.getPreferredSize();
+                    componentWidth = componentPreferredSize.width;
+                    componentHeight = componentPreferredSize.height;
+                }
+
+                component.setLocation(x, y);
+                component.setSize(componentWidth, componentHeight);
+
+                y += componentHeight + spacing;
+            } else {
+                // We make sure this child doesn't get painted.  There's also
+                // no need to lay the child out.
+                component.setVisible(false);
+            }
+        }
+
+        int rollupButtonY = (firstComponent == null) ?
+            0 : (firstComponent.getHeight() - rollupButtonSize.height) / 2 + 1;
+
+        rollupButton.setLocation(0, rollupButtonY);
+    }
+
+    public Color getButtonColor() {
+        return buttonColor;
+    }
+
+    public void setButtonColor(Color buttonColor) {
+        this.buttonColor = buttonColor;
+        repaintComponent();
+    }
+
+    public final void setButtonColor(String buttonColor) {
+        if (buttonColor == null) {
+            throw new IllegalArgumentException("buttonColor is null.");
+        }
+
+        setButtonColor(decodeColor(buttonColor));
+    }
+
+    public int getSpacing() {
+        return spacing;
+    }
+
+    public void setSpacing(int spacing) {
+        this.spacing = spacing;
+        invalidateComponent();
+    }
+
+    public int getBuffer() {
+        return buffer;
+    }
+
+    public void setBuffer(int buffer) {
+        this.buffer = buffer;
+        invalidateComponent();
+    }
+
+    public boolean getJustify() {
+        return justify;
+    }
+
+    public void setJustify(boolean justify) {
+        this.justify = justify;
+        invalidateComponent();
+    }
+
+    public boolean getFirstChildToggles() {
+        return firstChildToggles;
+    }
+
+    public void setFirstChildToggles(boolean firstChildToggles) {
+        this.firstChildToggles = firstChildToggles;
+        updateToggleComponent();
+    }
+
+    private void updateRollupButton() {
+        Rollup rollup = (Rollup)getComponent();
+
+        Image buttonData = null;
+        Cursor cursor = Cursor.HAND;
+
+        // Make sure to account for rollupButton
+        if (rollup.getLength() == 2) {
+            buttonData = bulletImage;
+            cursor = Cursor.DEFAULT;
+        } else if (rollup.isExpanded()) {
+            buttonData = collapseImage;
+        } else {
+            buttonData = expandImage;
+        }
+
+        rollupButton.setButtonData(buttonData);
+        rollupButton.setCursor(cursor);
+    }
+
+    private void updateToggleComponent() {
+        Rollup rollup = (Rollup)getComponent();
+        Component previousToggleComponent = toggleComponent;
+
+        toggleComponent = null;
+        if (firstChildToggles) {
+            for (int i = 0, n = rollup.getLength(); i < n; i++) {
+                Component child = rollup.get(i);
+                if (child != rollupButton) {
+                    toggleComponent = child;
+                    break;
+                }
+            }
+        }
+
+        if (toggleComponent != null
+            && rollup.getLength() > 2) {
+            // TODO Record original cursor
+            toggleComponent.setCursor(Cursor.HAND);
+        }
+
+        if (toggleComponent != previousToggleComponent) {
+            if (previousToggleComponent != null) {
+                // TODO Restore original cursor
+                previousToggleComponent.setCursor(Cursor.DEFAULT);
+
+                previousToggleComponent.getComponentMouseButtonListeners().remove(toggleComponentMouseHandler);
+            }
+
+            if (toggleComponent != null) {
+                toggleComponent.getComponentMouseButtonListeners().add(toggleComponentMouseHandler);
+            }
+        }
+    }
+
+    // Container events
+    @Override
+    public void componentInserted(Container container, int index) {
+        super.componentInserted(container, index);
+
+        updateRollupButton();
+        updateToggleComponent();
+    }
+
+    @Override
+    public void componentsRemoved(Container container, int index, Sequence<Component> components) {
+        super.componentsRemoved(container, index, components);
+
+        updateRollupButton();
+        updateToggleComponent();
+    }
+
+    // RollupListener methods
+
+    public Vote previewExpandedChange(final Rollup rollup) {
+        Vote vote = Vote.APPROVE;
+
+        if (rollup.getDisplay() != null) {
+            if (rollup.isExpanded()) {
+                // Start a collapse transition, return false, and set the
+                // expanded state when the transition is complete
+                if (collapseTransition == null) {
+                    int duration = EXPANSION_DURATION;
+                    int height1 = getHeight();
+
+                    if (expandTransition != null) {
+                        // Stop the expand transition
+                        expandTransition.stop();
+
+                        // Record its progress so we can reverse it at the right point
+                        duration = expandTransition.getElapsedTime();
+                        height1 = expandTransition.getHeight();
+
+                        expandTransition = null;
+                    }
+
+                    if (duration > 0) {
+                        int height2 = getPreferredHeight(-1, false);
+
+                        collapseTransition = new ExpansionTransition(height1, height2,
+                            true, duration, EXPANSION_RATE);
+                        collapseTransition.start(new TransitionListener() {
+                            public void transitionCompleted(Transition transition) {
+                                rollup.setExpanded(false);
+                                collapseTransition = null;
+                            }
+                        });
+
+                        vote = Vote.DEFER;
+                    }
+                } else {
+                    vote = collapseTransition.isRunning() ? Vote.DEFER : Vote.APPROVE;
+                }
+            }
+        }
+
+        return vote;
+    }
+
+    public void expandedChangeVetoed(Rollup rollup, Vote reason) {
+        if (reason == Vote.DENY
+            && collapseTransition != null) {
+            collapseTransition.stop();
+            collapseTransition = null;
+        }
+    }
+
+    public void expandedChanged(Rollup rollup) {
+        updateRollupButton();
+        invalidateComponent();
+
+        if (rollup.getDisplay() != null) {
+            if (rollup.isExpanded()) {
+                // Start an expansion transition
+                int height1 = getHeight();
+                int height2 = getPreferredHeight(-1, true);
+
+                expandTransition = new ExpansionTransition(height1, height2,
+                    false, EXPANSION_DURATION, EXPANSION_RATE);
+                expandTransition.start(new TransitionListener() {
+                    public void transitionCompleted(Transition transition) {
+                        expandTransition = null;
+                    }
+                });
+            }
+        }
+    }
+
+    // ButtonPressListener methods
+
+    public void buttonPressed(Button button) {
+        Rollup rollup = (Rollup)getComponent();
+        rollup.setExpanded(!rollup.isExpanded());
+    }
+}

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollBarSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollBarSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollBarSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollBarSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,1194 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.geom.GeneralPath;
+
+import pivot.wtk.ApplicationContext;
+import pivot.wtk.Component;
+import pivot.wtk.ComponentMouseListener;
+import pivot.wtk.ComponentMouseButtonListener;
+import pivot.wtk.Dimensions;
+import pivot.wtk.Display;
+import pivot.wtk.Mouse;
+import pivot.wtk.Orientation;
+import pivot.wtk.Point;
+import pivot.wtk.ScrollBar;
+import pivot.wtk.ScrollBarListener;
+import pivot.wtk.ScrollBarValueListener;
+import pivot.wtk.Theme;
+import pivot.wtk.media.Image;
+import pivot.wtk.skin.ComponentSkin;
+import pivot.wtk.skin.ContainerSkin;
+
+/**
+ * Scroll bar skin.
+ *
+ * @author tvolkert
+ */
+public class TerraScrollBarSkin extends ContainerSkin
+    implements ScrollBarListener, ScrollBarValueListener {
+
+    /**
+     * The types of scroll that are supported.
+     *
+     * @author tvolkert
+     */
+    protected enum IncrementType {
+        UNIT,
+        BLOCK;
+    }
+
+    /**
+     * Encapsulates the code needed to perform timer-controlled scrolling. This
+     * class is used by <tt>TerraScrollBarSkin</tt> (automatic block increment
+     * scrolling) and <tt>ScrollButtonSkin</tt> (automatic unit increment
+     * scrolling).
+     *
+     * @author tvolkert
+     */
+    protected class AutomaticScroller {
+        public int direction;
+        public IncrementType incrementType;
+        public int stopValue;
+
+        private int timeoutID = -1;
+        private int intervalID = -1;
+
+        /**
+         * Starts scrolling the specified scroll bar with no stop value.
+         *
+         * @param direction
+         * <tt>1</tt> to adjust the scroll bar's value larger; <tt>-1</tt> to
+         * adjust it smaller
+         *
+         * @param incrementType
+         * Determines whether we'll use the scroll bar's unit increment or the
+         * block increment when scrolling
+         *
+         * @exception IllegalStateException
+         * If automatic scrolling of any scroll bar is already in progress.
+         * Only one scroll bar may be automatically scrolled at one time
+         */
+        public void start(int direction, IncrementType incrementType) {
+            start(direction, incrementType, -1);
+        }
+
+        /**
+         * Starts scrolling the specified scroll bar, stopping the scroll when
+         * the specified value has been reached.
+         *
+         * @param direction
+         * <tt>1</tt> to adjust the scroll bar's value larger; <tt>-1</tt> to
+         * adjust it smaller
+         *
+         * @param incrementType
+         * Determines whether we'll use the scroll bar's unit increment or the
+         * block increment when scrolling
+         *
+         * @param stopValue
+         * The value which, once reached, will stop the automatic scrolling.
+         * Use <tt>-1</tt> to specify no stop value
+         *
+         * @exception IllegalStateException
+         * If automatic scrolling of any scroll bar is already in progress.
+         * Only one scroll bar may be automatically scrolled at one time
+         */
+        public void start(int direction, IncrementType incrementType, int stopValue) {
+            if (timeoutID != -1
+                || intervalID != -1) {
+                throw new IllegalStateException("Already running");
+            }
+
+            this.direction = direction;
+            this.incrementType = incrementType;
+            this.stopValue = stopValue;
+
+            // Wait a timeout period, then begin repidly scrolling
+            timeoutID = ApplicationContext.setTimeout(new Runnable() {
+                public void run() {
+                    intervalID = ApplicationContext.setInterval(new Runnable() {
+                        public void run() {
+                            scroll();
+                        }
+                    }, 30);
+
+                    timeoutID = -1;
+                }
+            }, 400);
+
+            // We initially scroll once to register that we've started
+            scroll();
+        }
+
+        /**
+         * Stops any automatic scrolling in progress.
+         */
+        public void stop() {
+            if (timeoutID != -1) {
+                ApplicationContext.clearTimeout(timeoutID);
+                timeoutID = -1;
+            }
+
+            if (intervalID != -1) {
+                ApplicationContext.clearInterval(intervalID);
+                intervalID = -1;
+            }
+        }
+
+        private void scroll() {
+            ScrollBar scrollBar = (ScrollBar)TerraScrollBarSkin.this.getComponent();
+
+            int rangeStart = scrollBar.getRangeStart();
+            int rangeEnd = scrollBar.getRangeEnd();
+            int extent = scrollBar.getExtent();
+            int value = scrollBar.getValue();
+
+            int adjustment;
+
+            if (incrementType == IncrementType.UNIT) {
+                adjustment = direction * scrollBar.getUnitIncrement();
+            } else {
+                adjustment = direction * scrollBar.getBlockIncrement();
+            }
+
+            if (adjustment < 0) {
+                int newValue = Math.max(value + adjustment, rangeStart);
+                scrollBar.setValue(newValue);
+
+                if (stopValue != -1
+                    && newValue < stopValue) {
+                    // We've reached the explicit stop value
+                    stop();
+                }
+
+                if (newValue == rangeStart) {
+                    // We implicit stop at the minimum scroll bar value
+                    stop();
+                }
+            } else {
+                int newValue = Math.min(value + adjustment, rangeEnd - extent);
+                scrollBar.setValue(newValue);
+
+                if (stopValue != -1
+                    && newValue > stopValue) {
+                    // We've reached the explicit stop value
+                    stop();
+                }
+
+                if (newValue == rangeEnd - extent) {
+                    // We implicitly stop at the maximum scroll bar value
+                    stop();
+                }
+            }
+        }
+    }
+
+    /**
+     * Scroll bar scroll button component.
+     *
+     * @author tvolkert
+     */
+    protected class ScrollButton extends Component {
+        private int direction;
+        private ScrollButtonImage buttonImage;
+
+        public ScrollButton(int direction, ScrollButtonImage buttonImage) {
+            this.direction = direction;
+            this.buttonImage = buttonImage;
+            setSkin(new ScrollButtonSkin());
+        }
+
+        public int getDirection() {
+            return direction;
+        }
+
+        public ScrollButtonImage getButtonImage() {
+            return buttonImage;
+        }
+    }
+
+    /**
+     * Scroll bar scroll button component skin.
+     *
+     * @author tvolkert
+     */
+    protected class ScrollButtonSkin extends ComponentSkin {
+        private boolean highlighted = false;
+        private boolean pressed = false;
+
+        @Override
+        public boolean isFocusable() {
+            return false;
+        }
+
+        public int getPreferredWidth(int height) {
+            return 15;
+        }
+
+        public int getPreferredHeight(int width) {
+            return 15;
+        }
+
+        public void layout() {
+            // No-op
+        }
+
+        public void paint(Graphics2D graphics) {
+            // Apply scroll bar styles to the button
+            ScrollButton scrollButton = (ScrollButton)getComponent();
+            ScrollBar scrollBar = (ScrollBar)TerraScrollBarSkin.this.getComponent();
+
+            int width = getWidth();
+            int height = getHeight();
+
+            Color backgroundColor;
+            if (scrollButton.isEnabled()) {
+                if (pressed) {
+                    backgroundColor = scrollButtonPressedBackgroundColor;
+                } else if (highlighted) {
+                    backgroundColor = scrollButtonHighlightedBackgroundColor;
+                } else {
+                    backgroundColor = scrollButtonBackgroundColor;
+                }
+            } else {
+                backgroundColor = scrollButtonDisabledBackgroundColor;
+            }
+
+            // Paint the background
+            graphics.setPaint(backgroundColor);
+            graphics.fillRect(1, 1, width - 2, height - 2);
+
+            // Paint the border
+            graphics.setPaint(borderColor);
+            graphics.setStroke(new BasicStroke());
+            graphics.drawRect(0, 0, width - 1, height - 1);
+
+            // Size the image to be proportional to our size
+            int buttonImageWidth, buttonImageHeight;
+            if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+                buttonImageWidth = (int)((float)width / 3.0f);
+                buttonImageHeight = (int)Math.floor((float)height / 2.0f);
+            } else {
+                buttonImageWidth = (int)Math.floor((float)width / 2.0f);
+                buttonImageHeight = (int)((float)height / 3.0f);
+            }
+            ScrollButtonImage buttonImage = scrollButton.getButtonImage();
+            buttonImage.setSize(buttonImageWidth, buttonImageHeight);
+
+            // Paint the image
+            Graphics2D imageGraphics = (Graphics2D)graphics.create();
+            int buttonImageX = (width - buttonImageWidth) / 2;
+            int buttonImageY = (height - buttonImageHeight) / 2;
+            imageGraphics.translate(buttonImageX, buttonImageY);
+            imageGraphics.clipRect(0, 0, buttonImageWidth, buttonImageHeight);
+            buttonImage.paint(imageGraphics);
+            imageGraphics.dispose();
+        }
+
+        @Override
+        public void enabledChanged(Component component) {
+            super.enabledChanged(component);
+
+            automaticScroller.stop();
+
+            pressed = false;
+            highlighted = false;
+            repaintComponent();
+        }
+
+        @Override
+        public void mouseOver(Component component) {
+            super.mouseOver(component);
+
+            highlighted = true;
+            repaintComponent();
+        }
+
+        @Override
+        public void mouseOut(Component component) {
+            super.mouseOut(component);
+
+            automaticScroller.stop();
+
+            pressed = false;
+            highlighted = false;
+            repaintComponent();
+        }
+
+        @Override
+        public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+            boolean consumed = super.mouseDown(component, button, x, y);
+
+            if (button == Mouse.Button.LEFT) {
+                ScrollButton scrollButton = (ScrollButton)getComponent();
+
+                // Start the automatic scroller. It'll be stopped when we
+                // mouse up or mouse out
+                automaticScroller.start(scrollButton.getDirection(),
+                    IncrementType.UNIT);
+
+                pressed = true;
+                repaintComponent();
+
+                consumed = true;
+            }
+
+            return consumed;
+        }
+
+        @Override
+        public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+            boolean consumed = super.mouseUp(component, button, x, y);
+
+            if (button == Mouse.Button.LEFT) {
+                automaticScroller.stop();
+
+                pressed = false;
+                repaintComponent();
+            }
+
+            return consumed;
+        }
+    }
+
+    protected abstract class ScrollButtonImage extends Image {
+        private int width = 0;
+        private int height = 0;
+
+        public int getWidth() {
+            return width;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+
+        public void setSize(int width, int height) {
+            this.width = width;
+            this.height = height;
+        }
+    }
+
+    protected class ScrollUpImage extends ScrollButtonImage {
+        public void paint(Graphics2D graphics) {
+            ScrollBar scrollBar = (ScrollBar)getComponent();
+
+            int width = getWidth();
+            int height = getHeight();
+
+            graphics.setPaint(scrollButtonImageColor);
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            GeneralPath arrow = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+
+            if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+                arrow.moveTo((float)width + 0.5f, 0);
+                arrow.lineTo(0, (float)height / 2.0f);
+                arrow.lineTo((float)width + 0.5f, height);
+            } else {
+                arrow.moveTo(0, (float)height + 0.5f);
+                arrow.lineTo((float)width / 2.0f, 0);
+                arrow.lineTo(width, (float)height + 0.5f);
+            }
+
+            arrow.closePath();
+            // TODO Use Graphics#fillPolygon() as optimization?
+            graphics.fill(arrow);
+        }
+    }
+
+    protected class ScrollDownImage extends ScrollButtonImage {
+        public void paint(Graphics2D graphics) {
+            ScrollBar scrollBar = (ScrollBar)getComponent();
+
+            int width = getWidth();
+            int height = getHeight();
+
+            graphics.setPaint(scrollButtonImageColor);
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            GeneralPath arrow = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
+
+            if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+                arrow.moveTo(0, 0);
+                arrow.lineTo((float)width + 0.5f, (float)height / 2.0f);
+                arrow.lineTo(0, height);
+            } else {
+                arrow.moveTo(0, 0);
+                arrow.lineTo((float)width / 2.0f, (float)height + 0.5f);
+                arrow.lineTo(width, 0);
+            }
+
+            arrow.closePath();
+            // TODO Use Graphics#fillPolygon() as optimization?
+            graphics.fill(arrow);
+        }
+    }
+
+    /**
+     * Scroll bar scroll handle component.
+     *
+     * @author tvolkert
+     */
+    protected class ScrollHandle extends Component {
+        public ScrollHandle() {
+            setSkin(new ScrollHandleSkin());
+        }
+    }
+
+    /**
+     * Scroll bar scroll handle component skin.
+     *
+     * @author tvolkert
+     */
+    protected class ScrollHandleSkin extends ComponentSkin {
+        private class DisplayMouseHandler
+            implements ComponentMouseButtonListener {
+            public boolean mouseDown(Component component, Mouse.Button button,
+                int x, int y) {
+                return false;
+            }
+
+            public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+                if (button == Mouse.Button.LEFT) {
+                    assert (component instanceof Display);
+                    component.getComponentMouseButtonListeners().remove(this);
+
+                    highlighted = false;
+                    repaintComponent();
+                }
+
+                return false;
+            }
+
+            public void mouseClick(Component component, Mouse.Button button,
+                int x, int y, int count) {
+                // No-op
+            }
+        }
+
+        private DisplayMouseHandler displayMouseHandler = new DisplayMouseHandler();
+
+        private boolean highlighted = false;
+
+        @Override
+        public boolean isFocusable() {
+            return false;
+        }
+
+        public int getPreferredWidth(int height) {
+            throw new UnsupportedOperationException();
+        }
+
+        public int getPreferredHeight(int width) {
+            throw new UnsupportedOperationException();
+        }
+
+        public Dimensions getPreferredSize() {
+            throw new UnsupportedOperationException();
+        }
+
+        public void layout() {
+            // No-op
+        }
+
+        public void paint(Graphics2D graphics) {
+            ScrollBar scrollBar = (ScrollBar)TerraScrollBarSkin.this.getComponent();
+
+            int width = getWidth();
+            int height = getHeight();
+
+            // Paint the background
+            Color backgroundColor = highlighted ?
+                scrollButtonHighlightedBackgroundColor :
+                scrollButtonBackgroundColor;
+            graphics.setPaint(backgroundColor);
+            graphics.fillRect(1, 1, width - 2, height - 2);
+
+            // Paint the border
+            graphics.setPaint(borderColor);
+            graphics.setStroke(new BasicStroke());
+            graphics.drawRect(0, 0, width - 1, height - 1);
+
+            // Paint the hash marks
+            if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+                int middle = width / 2;
+                graphics.setPaint(TerraTheme.darken(backgroundColor));
+                graphics.drawLine(middle - 3, 4, middle - 3, height - 5);
+                graphics.drawLine(middle, 4, middle, height - 5);
+                graphics.drawLine(middle + 3, 4, middle + 3, height - 5);
+                graphics.setPaint(Color.WHITE);
+                graphics.drawLine(middle - 2, 4, middle - 2, height - 5);
+                graphics.drawLine(middle + 1, 4, middle + 1, height - 5);
+                graphics.drawLine(middle + 4, 4, middle + 4, height - 5);
+            } else {
+                int middle = height / 2;
+                graphics.setPaint(TerraTheme.darken(backgroundColor));
+                graphics.drawLine(4, middle - 3, width - 5, middle - 3);
+                graphics.drawLine(4, middle, width - 5, middle);
+                graphics.drawLine(4, middle + 3, width - 5, middle + 3);
+                graphics.setPaint(Color.WHITE);
+                graphics.drawLine(4, middle - 2, width - 5, middle - 2);
+                graphics.drawLine(4, middle + 1, width - 5, middle + 1);
+                graphics.drawLine(4, middle + 4, width - 5, middle + 4);
+            }
+        }
+
+        @Override
+        public void mouseOver(Component component) {
+            super.mouseOver(component);
+
+            if (highlighted) {
+                // If the handle is already highlighted when the mouse enters
+                // it, it means that the handle is "grabbed", meaning that we
+                // have registered our display mouse handler.  Unregister it
+                // here so as to not register multiple times as we move our
+                // mouse in and out of the handle
+                Display display = component.getDisplay();
+                display.getComponentMouseButtonListeners().remove(displayMouseHandler);
+            } else {
+                // The handle is highlighted as long as the mouse is over it or
+                // we're dragging it
+                highlighted = true;
+                repaintComponent();
+            }
+        }
+
+        @Override
+        public void mouseOut(Component component) {
+            super.mouseOut(component);
+
+            if (Mouse.isPressed(Mouse.Button.LEFT)) {
+                // The user is currently dragging the handle.  We don't
+                // un-highlight it until the user releases the left mouse
+                // button.  NOTE the code that actually sets the scroll bar's
+                // value during the drag operation is handled by ScrollBarSkin
+                // since it needs access to scroll bar layout information
+                Display display = component.getDisplay();
+                display.getComponentMouseButtonListeners().add(displayMouseHandler);
+            } else {
+                // If we're not dragging the handle, then we un-highlight it
+                // as soon as the mouse exits
+                highlighted = false;
+                repaintComponent();
+            }
+        }
+    }
+
+    private class DisplayMouseHandler
+        implements ComponentMouseListener, ComponentMouseButtonListener {
+        public boolean mouseMove(Component component, int x, int y) {
+            ScrollBar scrollBar = (ScrollBar)getComponent();
+
+            int pixelValue;
+
+            if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+                pixelValue = x - dragOffset.x - scrollUpButton.getWidth() + 1;
+            } else {
+                pixelValue = y - dragOffset.y - scrollUpButton.getHeight() + 1;
+            }
+
+            int realValue = (int)((float)pixelValue / getValueScale());
+
+            int rangeEnd = scrollBar.getRangeEnd();
+            int extent = scrollBar.getExtent();
+
+            scrollBar.setValue(Math.min(Math.max(realValue, 0), rangeEnd - extent));
+
+            return false;
+        }
+
+        public void mouseOver(Component component) {
+        }
+
+        public void mouseOut(Component component) {
+        }
+
+        public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+            if (button == Mouse.Button.LEFT) {
+                assert (component instanceof Display);
+                component.getComponentMouseListeners().remove(this);
+                component.getComponentMouseButtonListeners().remove(this);
+            }
+
+            return false;
+        }
+
+        public void mouseClick(Component component, Mouse.Button button, int x, int y,
+            int count) {
+        }
+    }
+
+    private static final int DEFAULT_THICKNESS = 15;
+    private static final int DEFAULT_LENGTH = 100;
+
+    private AutomaticScroller automaticScroller = new AutomaticScroller();
+
+    private DisplayMouseHandler displayMouseHandler = new DisplayMouseHandler();
+    private Point dragOffset = null;
+
+    private ScrollButton scrollUpButton = new ScrollButton(-1, new ScrollUpImage());
+    private ScrollButton scrollDownButton = new ScrollButton(1, new ScrollDownImage());
+    private ScrollHandle scrollHandle = new ScrollHandle();
+
+    private int minimumHandleLength;
+    private Color borderColor;
+    private Color scrollButtonImageColor;
+    private Color scrollButtonBackgroundColor;
+    private Color scrollButtonDisabledBackgroundColor;
+    private Color scrollButtonPressedBackgroundColor;
+    private Color scrollButtonHighlightedBackgroundColor;
+
+    public TerraScrollBarSkin() {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        minimumHandleLength = 31;
+        borderColor = theme.getColor(7);
+        scrollButtonImageColor = theme.getColor(1);
+        scrollButtonBackgroundColor = theme.getColor(10);
+        scrollButtonDisabledBackgroundColor = theme.getColor(10);
+        scrollButtonPressedBackgroundColor = theme.getColor(9);
+        scrollButtonHighlightedBackgroundColor = theme.getColor(11);
+
+        setBackgroundColor(theme.getColor(9));
+    }
+
+    @Override
+    public void install(Component component) {
+        super.install(component);
+
+        ScrollBar scrollBar = (ScrollBar)component;
+        scrollBar.getScrollBarListeners().add(this);
+        scrollBar.getScrollBarValueListeners().add(this);
+
+        scrollBar.add(scrollUpButton);
+        scrollBar.add(scrollDownButton);
+        scrollBar.add(scrollHandle);
+
+        enabledChanged(scrollBar);
+    }
+
+    @Override
+    public void uninstall() {
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+        scrollBar.getScrollBarListeners().remove(this);
+        scrollBar.getScrollBarValueListeners().remove(this);
+
+        scrollBar.remove(scrollUpButton);
+        scrollBar.remove(scrollDownButton);
+        scrollBar.remove(scrollHandle);
+
+        super.uninstall();
+    }
+
+    public int getPreferredWidth(int height) {
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        int preferredWidth = 0;
+
+        if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+           preferredWidth = DEFAULT_LENGTH;
+        } else {
+           preferredWidth = DEFAULT_THICKNESS;
+        }
+
+        return preferredWidth;
+    }
+
+    public int getPreferredHeight(int width) {
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        int preferredHeight = 0;
+
+        if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+            preferredHeight = DEFAULT_THICKNESS;
+        } else {
+            preferredHeight = DEFAULT_LENGTH;
+        }
+
+        return preferredHeight;
+    }
+
+    public Dimensions getPreferredSize() {
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        int preferredWidth = 0;
+        int preferredHeight = 0;
+
+        if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+           preferredWidth = DEFAULT_LENGTH;
+           preferredHeight = DEFAULT_THICKNESS;
+        } else {
+           preferredWidth = DEFAULT_THICKNESS;
+           preferredHeight = DEFAULT_LENGTH;
+        }
+
+        return new Dimensions(preferredWidth, preferredHeight);
+    }
+
+    public void layout() {
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        int width = getWidth();
+        int height = getHeight();
+
+        int rangeStart = scrollBar.getRangeStart();
+        int rangeEnd = scrollBar.getRangeEnd();
+        int extent = scrollBar.getExtent();
+        int value = scrollBar.getValue();
+
+        int maxLegalRealValue = rangeEnd - extent;
+        int numLegalRealValues = maxLegalRealValue - rangeStart + 1;
+        float extentPercentage = (float)extent / (float)(rangeEnd - rangeStart);
+
+        if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+            scrollUpButton.setSize(scrollUpButton.getPreferredWidth(-1), height);
+            scrollUpButton.setLocation(0, 0);
+
+            scrollDownButton.setSize(scrollDownButton.getPreferredWidth(-1), height);
+            scrollDownButton.setLocation(width - scrollDownButton.getWidth(), 0);
+
+            if (scrollBar.isEnabled()) {
+                // Calculate the handle width first, as it dictates how much
+                // room is left to represent the range of legal values. Note
+                // that the handle may overlap each scroll button by 1px so
+                // that its borders merge into the borders of the scroll buttons
+                int availableWidth = width - scrollUpButton.getWidth() -
+                    scrollDownButton.getWidth() + 2;
+                int handleWidth = Math.max(minimumHandleLength,
+                    Math.round(extentPercentage * (float)availableWidth));
+
+                // Calculate the position of the handle by calculating the
+                // scale that maps logical value to pixel value
+                int numLegalPixelValues = availableWidth - handleWidth + 1;
+                float valueScale = (float)numLegalPixelValues / (float)numLegalRealValues;
+                int handleX = (int)((float)value * valueScale) +
+                    scrollUpButton.getWidth() - 1;
+
+                if (handleWidth > availableWidth) {
+                    // If we can't fit the handle, we hide it
+                    scrollHandle.setVisible(false);
+                } else {
+                    scrollHandle.setVisible(true);
+
+                    scrollHandle.setSize(handleWidth, height);
+                    scrollHandle.setLocation(handleX, 0);
+                }
+            } else {
+                scrollHandle.setVisible(false);
+            }
+        } else {
+            scrollUpButton.setSize(width, scrollUpButton.getPreferredHeight(-1));
+            scrollUpButton.setLocation(0, 0);
+
+            scrollDownButton.setSize(width, scrollDownButton.getPreferredHeight(-1));
+            scrollDownButton.setLocation(0, height - scrollDownButton.getHeight());
+
+            if (scrollBar.isEnabled()) {
+                // Calculate the handle height first, as it dictates how much
+                // room is left to represent the range of legal values. Note
+                // that the handle may overlap each scroll button by 1px so
+                // that its borders merge into the borders of the scroll buttons
+                int availableHeight = height - scrollUpButton.getHeight() -
+                    scrollDownButton.getHeight() + 2;
+                int handleHeight = Math.max(minimumHandleLength,
+                    Math.round(extentPercentage * (float)availableHeight));
+
+                // Calculate the position of the handle by calculating the
+                // scale maps logical value to pixel value
+                int numLegalPixelValues = availableHeight - handleHeight + 1;
+                float valueScale = (float)numLegalPixelValues / (float)numLegalRealValues;
+                int handleY = (int)((float)value * valueScale) +
+                    scrollUpButton.getHeight() - 1;
+
+                if (handleHeight > availableHeight) {
+                    // If we can't fit the handle, we hide it
+                    scrollHandle.setVisible(false);
+                } else {
+                    scrollHandle.setVisible(true);
+
+                    scrollHandle.setSize(width, handleHeight);
+                    scrollHandle.setLocation(0, handleY);
+                }
+            } else {
+                scrollHandle.setVisible(false);
+            }
+        }
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        super.paint(graphics);
+
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        int width = getWidth();
+        int height = getHeight();
+
+        graphics.setStroke(new BasicStroke());
+        graphics.setPaint(borderColor);
+
+        // Paint the scroll bar border lines
+        if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+            int scrollUpButtonWidth = scrollUpButton.getWidth();
+            int scrollDownButtonWidth = scrollDownButton.getWidth();
+
+            graphics.drawLine(scrollUpButtonWidth, 0,
+                width - scrollDownButtonWidth - 1, 0);
+            graphics.drawLine(scrollUpButtonWidth, height - 1,
+                width - scrollDownButtonWidth - 1, height - 1);
+        } else {
+            int scrollUpButtonHeight = scrollUpButton.getHeight();
+            int scrollDownButtonHeight = scrollDownButton.getHeight();
+
+            graphics.drawLine(0, scrollUpButtonHeight, 0,
+                height - scrollDownButtonHeight - 1);
+            graphics.drawLine(width - 1, scrollUpButtonHeight, width - 1,
+                height - scrollDownButtonHeight - 1);
+        }
+    }
+
+    public int getMinimumHandleLength() {
+        return minimumHandleLength;
+    }
+
+    public void setMinimumHandleLength(int minimumHandleLength) {
+        if (minimumHandleLength != this.minimumHandleLength) {
+            this.minimumHandleLength = minimumHandleLength;
+            repaintComponent();
+        }
+    }
+
+    public Color getBorderColor() {
+        return borderColor;
+    }
+
+    public void setBorderColor(Color borderColor) {
+        this.borderColor = borderColor;
+        repaintComponent();
+    }
+
+    public final void setBorderColor(String borderColor) {
+        if (borderColor == null) {
+            throw new IllegalArgumentException("borderColor is null");
+        }
+
+        setBorderColor(decodeColor(borderColor));
+    }
+
+    public Color getScrollButtonImageColor() {
+        return scrollButtonImageColor;
+    }
+
+    public void setScrollButtonImageColor(Color scrollButtonImageColor) {
+        this.scrollButtonImageColor = scrollButtonImageColor;
+        repaintComponent();
+    }
+
+    public final void setScrollButtonImageColor(String scrollButtonImageColor) {
+        if (scrollButtonImageColor == null) {
+            throw new IllegalArgumentException("scrollButtonImageColor is null");
+        }
+
+        setScrollButtonImageColor(decodeColor(scrollButtonImageColor));
+    }
+
+    public Color getScrollButtonBackgroundColor() {
+        return scrollButtonBackgroundColor;
+    }
+
+    public void setScrollButtonBackgroundColor(Color scrollButtonBackgroundColor) {
+        this.scrollButtonBackgroundColor = scrollButtonBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setScrollButtonBackgroundColor(String scrollButtonBackgroundColor) {
+        if (scrollButtonBackgroundColor == null) {
+            throw new IllegalArgumentException("scrollButtonBackgroundColor is null");
+        }
+
+        setScrollButtonBackgroundColor(decodeColor(scrollButtonBackgroundColor));
+    }
+
+    public Color getScrollButtonDisabledBackgroundColor() {
+        return scrollButtonDisabledBackgroundColor;
+    }
+
+    public void setScrollButtonDisabledBackgroundColor(Color scrollButtonDisabledBackgroundColor) {
+        this.scrollButtonDisabledBackgroundColor = scrollButtonDisabledBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setScrollButtonDisabledBackgroundColor(String scrollButtonDisabledBackgroundColor) {
+        if (scrollButtonDisabledBackgroundColor == null) {
+            throw new IllegalArgumentException("scrollButtonDisabledBackgroundColor is null");
+        }
+
+        setScrollButtonDisabledBackgroundColor(decodeColor(scrollButtonDisabledBackgroundColor));
+    }
+
+    public Color getScrollButtonPressedBackgroundColor() {
+        return scrollButtonPressedBackgroundColor;
+    }
+
+    public void setScrollButtonPressedBackgroundColor(Color scrollButtonPressedBackgroundColor) {
+        this.scrollButtonPressedBackgroundColor = scrollButtonPressedBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setScrollButtonPressedBackgroundColor(String scrollButtonPressedBackgroundColor) {
+        if (scrollButtonPressedBackgroundColor == null) {
+            throw new IllegalArgumentException("scrollButtonPressedBackgroundColor is null");
+        }
+
+        setScrollButtonPressedBackgroundColor(decodeColor(scrollButtonPressedBackgroundColor));
+    }
+
+    public Color getScrollButtonHighlightedBackgroundColor() {
+        return scrollButtonHighlightedBackgroundColor;
+    }
+
+    public void setScrollButtonHighlightedBackgroundColor(Color scrollButtonHighlightedBackgroundColor) {
+        this.scrollButtonHighlightedBackgroundColor = scrollButtonHighlightedBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setScrollButtonHighlightedBackgroundColor(String scrollButtonHighlightedBackgroundColor) {
+        if (scrollButtonHighlightedBackgroundColor == null) {
+            throw new IllegalArgumentException("scrollButtonHighlightedBackgroundColor is null");
+        }
+
+        setScrollButtonHighlightedBackgroundColor(decodeColor(scrollButtonHighlightedBackgroundColor));
+    }
+
+    @Override
+    public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+        boolean consumed = super.mouseDown(component, button, x, y);
+
+        if (scrollHandle.isVisible()
+            && button == Mouse.Button.LEFT) {
+            ScrollBar scrollBar = (ScrollBar)getComponent();
+
+            Component mouseDownComponent = scrollBar.getComponentAt(x, y);
+
+            if (mouseDownComponent == scrollHandle) {
+                // Begin dragging the scroll handle. Register our display
+                // mouse handler to do the actual work
+                Display display = scrollBar.getDisplay();
+
+                dragOffset = scrollBar.mapPointToAncestor(display, x, y);
+                dragOffset.translate(-scrollHandle.getX(), -scrollHandle.getY());
+
+                display.getComponentMouseListeners().add(displayMouseHandler);
+                display.getComponentMouseButtonListeners().add(displayMouseHandler);
+            } else if (mouseDownComponent == null) {
+                // Begin automatic block scrolling. Calculate the direction of
+                // the scroll by checking to see if the user pressed the mouse
+                // in the area "before" the handle or "after" it.
+
+                int direction;
+                int realStopValue;
+
+                if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+                    direction = x < scrollHandle.getX() ? -1 : 1;
+
+                    int pixelStopValue = x - scrollUpButton.getWidth() + 1;
+
+                    if (direction == 1) {
+                        // If we're scrolling down, account for the width of the
+                        // handle in our pixel stop value so that we stop as soon
+                        // as the *bottom* of the handle reaches our click point
+                        pixelStopValue -= scrollHandle.getWidth();
+                    }
+
+                    realStopValue = (int)((float)pixelStopValue / getValueScale());
+                } else {
+                    direction = y < scrollHandle.getY() ? -1 : 1;
+
+                    int pixelStopValue = y - scrollUpButton.getHeight() + 1;
+
+                    if (direction == 1) {
+                        // If we're scrolling down, account for the height of the
+                        // handle in our pixel stop value so that we stop as soon
+                        // as the *bottom* of the handle reaches our click point
+                        pixelStopValue -= scrollHandle.getHeight();
+                    }
+
+                    realStopValue = (int)((float)pixelStopValue / getValueScale());
+                }
+
+                // Start the automatic scroller; we'll stop it upon mouse out or
+                // mouse up
+                automaticScroller.start(direction,
+                    IncrementType.BLOCK, realStopValue);
+            }
+
+            consumed = true;
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public void mouseOut(Component component) {
+        super.mouseOut(component);
+
+        automaticScroller.stop();
+    }
+
+    @Override
+    public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+        boolean consumed = super.mouseUp(component, button, x, y);
+
+        if (button == Mouse.Button.LEFT) {
+            automaticScroller.stop();
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public boolean mouseWheel(Component component, Mouse.ScrollType scrollType, int scrollAmount,
+        int wheelRotation, int x, int y) {
+        boolean consumed = false;
+
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        int previousValue = scrollBar.getValue();
+        int newValue = previousValue + (scrollAmount * wheelRotation *
+            scrollBar.getUnitIncrement());
+
+        if (wheelRotation > 0) {
+            int maxValue = scrollBar.getRangeEnd() - scrollBar.getExtent();
+            newValue = Math.min(newValue, maxValue);
+
+            if (previousValue < maxValue) {
+                consumed = true;
+            }
+        } else {
+            newValue = Math.max(newValue, 0);
+
+            if (previousValue > 0) {
+                consumed = true;
+            }
+        }
+
+        scrollBar.setValue(newValue);
+
+        return consumed;
+    }
+
+    /**
+     * Gets the scale factor that allows us to translate pixel values to scroll
+     * bar values and vice versa. This assumes that the range of pixels spans
+     * from the last pixel of <tt>scrollUpButton</tt> to the first pixel of
+     * <tt>scrollDownButton</tt> and excludes the pixels taken up by
+     * <tt>scrollHandle</tt>.
+     * <p>
+     * To map from scroll bar values (<i>real values</i>) to pixel values, you
+     * multiply by the value scale. To map from pixel values back to real
+     * values, you divide by the value scale.
+     *
+     * @return
+     * <tt>&lt;number of legal pixel values&gt; / &lt;number of legal real values&gt;</tt>
+     */
+    private float getValueScale() {
+        ScrollBar scrollBar = (ScrollBar)getComponent();
+
+        float valueScale;
+
+        int rangeStart = scrollBar.getRangeStart();
+        int rangeEnd = scrollBar.getRangeEnd();
+        int extent = scrollBar.getExtent();
+        int maxLegalRealValue = rangeEnd - extent;
+
+        int numLegalRealValues = maxLegalRealValue - rangeStart + 1;
+        int numLegalPixelValues;
+
+        if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+            int availableWidth = getWidth() - scrollUpButton.getWidth() -
+                scrollDownButton.getWidth() + 2;
+            numLegalPixelValues = availableWidth - scrollHandle.getWidth() + 1;
+        } else {
+            int availableHeight = getHeight() - scrollUpButton.getHeight() -
+                scrollDownButton.getHeight() + 2;
+            numLegalPixelValues = availableHeight - scrollHandle.getHeight() + 1;
+        }
+
+        valueScale = (float)numLegalPixelValues / (float)numLegalRealValues;
+
+        return valueScale;
+    }
+
+    @Override
+    public void enabledChanged(Component component) {
+        boolean enabled = component.isEnabled();
+
+        scrollUpButton.setEnabled(enabled);
+        scrollDownButton.setEnabled(enabled);
+
+        invalidateComponent();
+    }
+
+    // ScrollBarListener methods
+
+    public void orientationChanged(ScrollBar scrollBar, Orientation previousOrientation) {
+        invalidateComponent();
+    }
+
+    public void scopeChanged(ScrollBar scrollBar, int previousRangeStart,
+        int previousRangeEnd, int previousExtent) {
+        invalidateComponent();
+    }
+
+    public void unitIncrementChanged(ScrollBar scrollBar, int previousUnitIncrement) {
+        // No-op
+    }
+
+    public void blockIncrementChanged(ScrollBar scrollBar, int previousBlockIncrement) {
+        // No-op
+    }
+
+    // ScrollBarValueListener methods
+
+    public void valueChanged(ScrollBar scrollBar, int previousValue) {
+        if (scrollHandle.isVisible()) {
+           int value = scrollBar.getValue();
+
+           if (scrollBar.getOrientation() == Orientation.HORIZONTAL) {
+              int handleX = (int)((float)value * getValueScale()) +
+                 scrollUpButton.getWidth() - 1;
+
+              scrollHandle.setLocation(handleX, 0);
+           } else {
+              int handleY = (int)((float)value * getValueScale()) +
+                 scrollUpButton.getHeight() - 1;
+
+              scrollHandle.setLocation(0, handleY);
+           }
+        }
+    }
+}

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneCornerSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneCornerSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneCornerSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneCornerSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.geom.Rectangle2D;
+
+import pivot.wtk.Dimensions;
+import pivot.wtk.skin.ComponentSkin;
+
+/**
+ * Scroll pane corner skin.
+ *
+ * @author tvolkert
+ */
+public class TerraScrollPaneCornerSkin extends ComponentSkin {
+    private Color backgroundColor = new Color(0xF0, 0xEC, 0xE7);
+    private Color color = new Color(0x81, 0x76, 0x67);
+
+    @Override
+    public boolean isFocusable() {
+        return false;
+    }
+
+    public int getPreferredWidth(int height) {
+        // ScrollPane corners have no implicit preferred size.
+        return 0;
+    }
+
+    public int getPreferredHeight(int width) {
+        // ScrollPane corners have no implicit preferred size.
+        return 0;
+    }
+
+    public Dimensions getPreferredSize() {
+        // ScrollPane corners have no implicit preferred size.
+        return new Dimensions(0, 0);
+    }
+
+    public void layout() {
+        // No-op
+    }
+
+    public void paint(Graphics2D graphics) {
+        int width = getWidth();
+        int height = getHeight();
+
+        graphics.setPaint(backgroundColor);
+        graphics.fill(new Rectangle2D.Double(0, 0, width, height));
+    }
+
+    public Color getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    public void setBackgroundColor(Color backgroundColor) {
+        this.backgroundColor = backgroundColor;
+        repaintComponent();
+    }
+
+    public final void setBackgroundColor(String backgroundColor) {
+        if (backgroundColor == null) {
+            throw new IllegalArgumentException("backgroundColor is null.");
+        }
+
+        setBackgroundColor(decodeColor(backgroundColor));
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public void setColor(Color color) {
+        this.color = color;
+        repaintComponent();
+    }
+
+    public final void setColor(String color) {
+        if (color == null) {
+            throw new IllegalArgumentException("color is null.");
+        }
+
+        setColor(decodeColor(color));
+    }
+}

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraScrollPaneSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import pivot.wtk.Theme;
+import pivot.wtk.skin.ScrollPaneSkin;
+
+/**
+ * Scroll pane skin that applies terra-specific colors.
+ */
+public class TerraScrollPaneSkin extends ScrollPaneSkin {
+    public final void setBackgroundColor(int backgroundColor) {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        setBackgroundColor(theme.getColor(backgroundColor));
+    }
+}

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSeparatorSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSeparatorSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSeparatorSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSeparatorSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import pivot.wtk.Theme;
+import pivot.wtk.skin.SeparatorSkin;
+
+/**
+ * Terra sheet skin.
+ *
+ * @author gbrown
+ */
+public class TerraSeparatorSkin extends SeparatorSkin {
+    public TerraSeparatorSkin() {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        setColor(theme.getColor(7));
+        setHeadingColor(theme.getColor(13));
+    }
+
+    public void setColor(int color) {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        setColor(theme.getColor(color));
+    }
+
+    public void setHeadingColor(int headingColor) {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        setHeadingColor(theme.getColor(headingColor));
+    }
+}

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSheetSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSheetSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSheetSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraSheetSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+
+import pivot.collections.Dictionary;
+import pivot.util.Vote;
+import pivot.wtk.ApplicationContext;
+import pivot.wtk.Component;
+import pivot.wtk.ComponentMouseButtonListener;
+import pivot.wtk.Dimensions;
+import pivot.wtk.Insets;
+import pivot.wtk.Keyboard;
+import pivot.wtk.Mouse;
+import pivot.wtk.Sheet;
+import pivot.wtk.SheetStateListener;
+import pivot.wtk.Theme;
+import pivot.wtk.Window;
+import pivot.wtk.effects.DropShadowDecorator;
+import pivot.wtk.effects.Transition;
+import pivot.wtk.effects.TransitionListener;
+import pivot.wtk.skin.WindowSkin;
+
+/**
+ * Sheet skin class.
+ * <p>
+ * TODO Wire up the "resizable" flag. It current exists but does nothing.
+ *
+ * @author gbrown
+ * @author tvolkert
+ */
+public class TerraSheetSkin extends WindowSkin implements SheetStateListener {
+    private Color borderColor;
+    private Insets padding;
+    private boolean resizable;
+
+    // Derived colors
+    private Color bevelColor;
+
+    private SlideTransition openTransition = null;
+    private SlideTransition closeTransition = null;
+
+    private ComponentMouseButtonListener ownerMouseButtonListener =
+        new ComponentMouseButtonListener() {
+        public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+            Window owner = (Window)component;
+            Component ownerContent = owner.getContent();
+
+            if (ownerContent != null
+                && !ownerContent.isEnabled()
+                && owner.getComponentAt(x, y) == ownerContent) {
+                ApplicationContext.beep();
+            }
+
+            return false;
+        }
+
+        public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        public void mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
+            // No-op
+        }
+    };
+
+    private DropShadowDecorator dropShadowDecorator = null;
+
+    private static final int SLIDE_DURATION = 250;
+    private static final int SLIDE_RATE = 30;
+
+    public TerraSheetSkin() {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+
+        Color backgroundColor = theme.getColor(11);
+        backgroundColor = new Color(backgroundColor.getRed(), backgroundColor.getGreen(),
+            backgroundColor.getBlue(), 0xF0);
+        setBackgroundColor(backgroundColor);
+
+        borderColor = theme.getColor(7);
+        padding = new Insets(8);
+        resizable = false;
+
+        // Set the derived colors
+        bevelColor = TerraTheme.darken(backgroundColor);
+    }
+
+    @Override
+    public void install(Component component) {
+        super.install(component);
+
+        Sheet sheet = (Sheet)component;
+        sheet.getSheetStateListeners().add(this);
+
+        // Attach the drop-shadow decorator
+        dropShadowDecorator = new DropShadowDecorator(3, 3, 3);
+        sheet.getDecorators().add(dropShadowDecorator);
+    }
+
+    @Override
+    public void uninstall() {
+        Sheet sheet = (Sheet)getComponent();
+        sheet.getSheetStateListeners().remove(this);
+
+        // Detach the drop shadow decorator
+        sheet.getDecorators().remove(dropShadowDecorator);
+        dropShadowDecorator = null;
+
+        super.uninstall();
+    }
+
+    @Override
+    public int getPreferredWidth(int height) {
+        int preferredWidth = 0;
+
+        Sheet sheet = (Sheet)getComponent();
+        Component content = sheet.getContent();
+
+        if (content != null
+            && content.isDisplayable()) {
+            if (height != -1) {
+                height = Math.max(height - (padding.top + padding.bottom + 2), 0);
+            }
+
+            preferredWidth = content.getPreferredWidth(height);
+        }
+
+        preferredWidth += (padding.left + padding.right + 2);
+
+        return preferredWidth;
+    }
+
+    @Override
+    public int getPreferredHeight(int width) {
+        int preferredHeight = 0;
+
+        Sheet sheet = (Sheet)getComponent();
+        Component content = sheet.getContent();
+
+        if (content != null
+            && content.isDisplayable()) {
+            if (width != -1) {
+                width = Math.max(width - (padding.left + padding.right + 2), 0);
+            }
+
+            preferredHeight = content.getPreferredHeight(width);
+        }
+
+        preferredHeight += (padding.top + padding.bottom + 2);
+
+        return preferredHeight;
+    }
+
+    @Override
+    public Dimensions getPreferredSize() {
+        int preferredWidth = 0;
+        int preferredHeight = 0;
+
+        Sheet sheet = (Sheet)getComponent();
+        Component content = sheet.getContent();
+
+        if (content != null
+            && content.isDisplayable()) {
+            Dimensions preferredContentSize = content.getPreferredSize();
+            preferredWidth = preferredContentSize.width;
+            preferredHeight = preferredContentSize.height;
+        }
+
+        preferredWidth += (padding.left + padding.right + 2);
+        preferredHeight += (padding.top + padding.bottom + 2);
+
+        Dimensions preferredSize = new Dimensions(preferredWidth, preferredHeight);
+
+        return preferredSize;
+    }
+
+    public void layout() {
+        int width = getWidth();
+        int height = getHeight();
+
+        Sheet sheet = (Sheet)getComponent();
+        Component content = sheet.getContent();
+
+        if (content != null) {
+            if (content.isDisplayable()) {
+                content.setVisible(true);
+
+                content.setLocation(padding.left + 1, padding.top + 1);
+
+                int contentWidth = Math.max(width - (padding.left + padding.right + 2), 0);
+                int contentHeight = Math.max(height - (padding.top + padding.bottom + 2), 0);
+
+                content.setSize(contentWidth, contentHeight);
+            } else {
+                content.setVisible(false);
+            }
+        }
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        super.paint(graphics);
+
+        int width = getWidth();
+        int height = getHeight();
+
+        graphics.setPaint(borderColor);
+        graphics.drawRect(0, 0, width - 1, height - 1);
+
+        graphics.setPaint(bevelColor);
+        graphics.drawLine(1, height - 2, width - 2, height - 2);
+    }
+
+    @Override
+    public boolean keyPressed(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
+        boolean consumed = false;
+
+        Sheet sheet = (Sheet)getComponent();
+
+        if (keyCode == Keyboard.KeyCode.ENTER) {
+            sheet.close(true);
+            consumed = true;
+        } else if (keyCode == Keyboard.KeyCode.ESCAPE) {
+            sheet.close(false);
+            consumed = true;
+        } else {
+            consumed = super.keyPressed(component, keyCode, keyLocation);
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public void setBackgroundColor(Color backgroundColor) {
+        super.setBackgroundColor(backgroundColor);
+        bevelColor = TerraTheme.darken(backgroundColor);
+    }
+
+    public Color getBorderColor() {
+        return borderColor;
+    }
+
+    public void setBorderColor(Color borderColor) {
+        if (borderColor == null) {
+            throw new IllegalArgumentException("borderColor is null.");
+        }
+
+        this.borderColor = borderColor;
+        repaintComponent();
+    }
+
+    public final void setBorderColor(String borderColor) {
+        if (borderColor == null) {
+            throw new IllegalArgumentException("borderColor is null.");
+        }
+
+        setBorderColor(decodeColor(borderColor));
+    }
+
+    public Insets getPadding() {
+        return padding;
+    }
+
+    public void setPadding(Insets padding) {
+        if (padding == null) {
+            throw new IllegalArgumentException("padding is null.");
+        }
+
+        this.padding = padding;
+        invalidateComponent();
+    }
+
+    public final void setPadding(Dictionary<String, ?> padding) {
+        if (padding == null) {
+            throw new IllegalArgumentException("padding is null.");
+        }
+
+        setPadding(new Insets(padding));
+    }
+
+    public final void setPadding(int padding) {
+        setPadding(new Insets(padding));
+    }
+
+    public final void setPadding(Number padding) {
+        if (padding == null) {
+            throw new IllegalArgumentException("padding is null.");
+        }
+
+        setPadding(padding.intValue());
+    }
+
+    public boolean isResizable() {
+        return resizable;
+    }
+
+    public void setResizable(boolean resizable) {
+        this.resizable = resizable;
+        invalidateComponent();
+    }
+
+    @Override
+    public void windowOpened(final Window window) {
+        super.windowOpened(window);
+
+        Window owner = window.getOwner();
+        owner.getComponentMouseButtonListeners().add(ownerMouseButtonListener);
+
+        ApplicationContext.queueCallback(new Runnable() {
+            public void run() {
+                openTransition = new SlideTransition(window, 0, 0,
+                    -window.getHeight(), 0, false, SLIDE_DURATION, SLIDE_RATE);
+                openTransition.start(new TransitionListener() {
+                    public void transitionCompleted(Transition transition) {
+                        openTransition = null;
+                    }
+                });
+            }
+        });
+    }
+
+    public Vote previewSheetClose(final Sheet sheet, final boolean result) {
+        // Start a close transition, return false, and close the window
+        // when the transition is complete
+        Vote vote = Vote.APPROVE;
+
+        if (closeTransition == null) {
+            int duration = SLIDE_DURATION;
+            int beginX = 0;
+            int beginY = 0;
+
+            if (openTransition != null) {
+                // Stop the open transition
+                openTransition.stop();
+
+                // Record its progress so we can reverse it at the right point
+                duration = openTransition.getElapsedTime();
+                beginX = openTransition.getX();
+                beginY = openTransition.getY();
+
+                openTransition = null;
+            }
+
+            if (duration > 0) {
+                closeTransition = new SlideTransition(sheet, beginX, 0,
+                    beginY, -sheet.getHeight(), true, duration, SLIDE_RATE);
+                closeTransition.start(new TransitionListener() {
+                    public void transitionCompleted(Transition transition) {
+                        sheet.close(result);
+                        closeTransition = null;
+                    }
+                });
+
+                vote = Vote.DEFER;
+            }
+        } else {
+            vote = (closeTransition.isRunning()) ? Vote.DEFER : Vote.APPROVE;
+        }
+
+        return vote;
+    }
+
+    public void sheetCloseVetoed(Sheet sheet, Vote reason) {
+        if (reason == Vote.DENY
+            && closeTransition != null) {
+            closeTransition.stop();
+        }
+    }
+
+    public void sheetClosed(Sheet sheet) {
+        Window owner = sheet.getOwner();
+        owner.getComponentMouseButtonListeners().remove(ownerMouseButtonListener);
+
+        closeTransition = null;
+    }
+}