You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by gb...@apache.org on 2011/01/10 00:19:29 UTC

svn commit: r1057053 [8/12] - in /pivot/branches/3.x: ./ core/ core/src/ core/src/org/ core/src/org/apache/ core/src/org/apache/pivot/ core/src/org/apache/pivot/beans/ core/src/org/apache/pivot/bxml/ core/src/org/apache/pivot/csv/ core/src/org/apache/p...

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Font.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Font.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Font.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Font.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import java.util.Map;
+
+import org.apache.pivot.beans.BeanAdapter;
+import org.apache.pivot.io.SerializationException;
+import org.apache.pivot.json.JSONSerializer;
+
+/**
+ * Class representing a font.
+ */
+public class Font {
+    /**
+     * Class representing font metric information.
+     */
+    public static class Metrics {
+        public final float ascent;
+        public final float descent;
+        public final float leading;
+
+        public Metrics(float ascent, float descent, float leading) {
+            this.ascent = ascent;
+            this.descent = descent;
+            this.leading = leading;
+        }
+    }
+
+    public final String name;
+    public final int size;
+    public final boolean bold;
+    public final boolean italic;
+
+    private Object nativeFont = null;
+
+    public static final String NAME_KEY = "name";
+    public static final String SIZE_KEY = "size";
+    public static final String BOLD_KEY = "bold";
+    public static final String ITALIC_KEY = "italic";
+
+    public Font(String name, int size) {
+        this(name, size, false, false);
+    }
+
+    public Font(String name, int size, boolean bold, boolean italic) {
+        this.name = name;
+        this.size = size;
+        this.bold = bold;
+        this.italic = italic;
+    }
+
+    protected Object getNativeFont() {
+        if (nativeFont == null) {
+            nativeFont = Platform.getPlatform().getNativeFont(this);
+        }
+
+        return nativeFont;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        boolean equals = false;
+
+        if (object instanceof Font) {
+            Font font = (Font)object;
+            equals = (name.equals(font.name)
+                && size == font.size
+                && bold == font.bold
+                && italic == font.italic);
+        }
+
+        return equals;
+    }
+
+    @Override
+    public int hashCode() {
+        return name.hashCode() ^ size
+            + (bold ? 0x10 : 0x00)
+            + (italic ? 0x01 : 0x00);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getName() + " [" + name + " " + size
+            + (bold ? " bold" : "")
+            + (italic ? " italic" : "")
+            + "]";
+    }
+
+    public static Font decode(String value) {
+        if (value == null) {
+            throw new IllegalArgumentException();
+        }
+
+        Map<String, ?> map;
+        try {
+            map = JSONSerializer.parseMap(value);
+        } catch (SerializationException exception) {
+            throw new IllegalArgumentException(exception);
+        }
+
+        Font font = Platform.getPlatform().getDefaultFont();
+
+        String name = font.name;
+        if (map.containsKey(NAME_KEY)) {
+            name = BeanAdapter.get(map, NAME_KEY);
+        }
+
+        int size = font.size;
+        if (map.containsKey(SIZE_KEY)) {
+            Object sizeValue = BeanAdapter.get(map, SIZE_KEY);
+
+            if (sizeValue instanceof String) {
+                size = decodeSize(font, (String)sizeValue);
+            } else {
+                size = ((Number)sizeValue).intValue();
+            }
+        }
+
+        boolean bold = font.bold;
+        if (map.containsKey(BOLD_KEY)) {
+            bold = BeanAdapter.get(map, BOLD_KEY);
+        }
+
+        boolean italic = font.italic;
+        if (map.containsKey(ITALIC_KEY)) {
+            italic = BeanAdapter.get(map, ITALIC_KEY);
+        }
+
+        return new Font(name, size, bold, italic);
+    }
+
+    private static int decodeSize(Font font, String value) {
+        int size;
+
+        if (value.endsWith("%")) {
+            value = value.substring(0, value.length() - 1);
+            float percentage = Float.parseFloat(value) / 100f;
+            size = Math.round(font.size * percentage);
+        } else {
+            throw new IllegalArgumentException(value + " is not a valid font size.");
+        }
+
+        return size;
+    }
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Graphics.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Graphics.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Graphics.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Graphics.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import org.apache.pivot.scene.media.Raster;
+
+/**
+ * Interface representing a drawing surface.
+ */
+public abstract class Graphics {
+    /**
+     * Enumeration representing a compositing operation.
+     */
+    public enum CompositeOperation {
+        SOURCE_ATOP,
+        SOURCE_IN,
+        SOURCE_OUT,
+        SOURCE_OVER,
+        DESTINATION_ATOP,
+        DESTINATION_IN,
+        DESTINATION_OUT,
+        DESTINATION_OVER,
+        CLEAR,
+        XOR
+    }
+
+    // Clipping
+    public void clip(Bounds bounds) {
+        clip(bounds.x, bounds.y, bounds.width, bounds.height);
+    }
+
+    public abstract void clip(int x, int y, int width, int height);
+    public abstract Bounds getClipBounds();
+
+    // Compositing
+    public abstract float getAlpha();
+    public abstract void setAlpha(float alpha);
+
+    public abstract CompositeOperation getCompositeOperation();
+    public abstract void setCompositeOperation(CompositeOperation compositeOperation);
+
+    // Anti-aliasing
+    public abstract boolean isAntiAliased();
+    public abstract void setAntiAliased(boolean antiAliased);
+
+    // Primitive drawing/filling
+    public abstract Stroke getStroke();
+    public abstract void setStroke(Stroke stroke);
+
+    public abstract Paint getPaint();
+    public abstract void setPaint(Paint paint);
+
+    public abstract void drawLine(float x1, float y1, float x2, float y2);
+
+    public abstract void drawRectangle(float x, float y, float width, float height, float cornerRadius);
+    public abstract void drawArc(float x, float y, float width, float height, float start, float extent);
+    public abstract void drawEllipse(float x, float y, float width, float height);
+    public abstract void drawPath(PathGeometry pathGeometry);
+
+    public abstract void fillRectangle(float x, float y, float width, float height, float cornerRadius);
+    public abstract void fillArc(float x, float y, float width, float height, float start, float extent);
+    public abstract void fillEllipse(float x, float y, float width, float height);
+    public abstract void fillPath(PathGeometry pathGeometry);
+
+    // Raster drawing
+    public void drawRaster(Raster raster, int x, int y) {
+        drawRaster(raster, x, y, raster.getWidth(), raster.getHeight());
+    }
+
+    public abstract void drawRaster(Raster raster, int x, int y, int width, int height);
+
+    // Blitting
+    public abstract void copyArea(int x, int y, int width, int height, int dx, int dy);
+
+    // Text
+    public abstract Font getFont();
+    public abstract void setFont(Font font);
+
+    public void drawText(CharSequence text, float x, float y) {
+        drawText(text, 0, text.length(), x, y);
+    }
+
+    public abstract void drawText(CharSequence text, int start, int length, float x, float y);
+
+    // Transformations
+    public void translate(float dx, float dy) {
+        // TODO
+    }
+
+    public void rotate(float theta) {
+        // TODO
+    }
+
+    public void rotate(float theta, float x, float y) {
+        // TODO
+    }
+
+    public void scale(float sx, float sy) {
+        // TODO
+    }
+
+    public void transform(Transform transform) {
+        transform(transform.m11, transform.m12, transform.m21, transform.m22, transform.dx, transform.dy);
+    }
+
+    public abstract void transform(float m11, float m12, float m21, float m22, float dx, float dy);
+    public abstract Transform getCurrentTransform();
+
+    // Creation/disposal
+    public abstract Graphics create();
+    public abstract void dispose();
+
+    public abstract Object getNativeGraphics();
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Group.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Group.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Group.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Group.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,1234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.pivot.bxml.DefaultProperty;
+import org.apache.pivot.scene.effect.Decorator;
+import org.apache.pivot.util.ListenerList;
+import org.apache.pivot.util.ObservableList;
+import org.apache.pivot.util.ObservableListAdapter;
+
+/**
+ * Container for a group of nodes. A group is primarily responsible for
+ * laying out its child nodes.
+ */
+@DefaultProperty("nodes")
+public class Group extends Node {
+    private class NodeList extends ObservableListAdapter<Node> {
+        public NodeList() {
+            super(new ArrayList<Node>());
+        }
+
+        @Override
+        public void add(int index, Node node) {
+            addNode(node);
+            invalidate();
+
+            super.add(node);
+        }
+
+        @Override
+        public boolean addAll(int index, Collection<? extends Node> nodes) {
+            for (Node node : nodes) {
+                addNode(node);
+            }
+
+            invalidate();
+
+            return super.addAll(index, nodes);
+        }
+
+        private void addNode(Node node) {
+            if (node == null) {
+                throw new IllegalArgumentException();
+            }
+
+            if (node.getGroup() != null) {
+                throw new IllegalArgumentException();
+            }
+
+            node.setGroup(Group.this);
+            repaint(node.getDecoratedBounds());
+        }
+
+        @Override
+        public Node remove(int index) {
+            removeNode(get(index));
+            invalidate();
+
+            return super.remove(index);
+        }
+
+        @Override
+        protected void removeRange(int fromIndex, int toIndex) {
+            for (int i = fromIndex; i < toIndex; i++) {
+                removeNode(get(i));
+            }
+
+            invalidate();
+
+            super.removeRange(fromIndex, toIndex);
+        }
+
+        private void removeNode(Node node) {
+            node.setGroup(null);
+            repaint(node.getDecoratedBounds());
+        }
+
+        @Override
+        public Node set(int index, Node node) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public List<Node> setAll(int index, Collection<? extends Node> nodes) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    private static class GroupListenerList extends ListenerList<GroupListener>
+        implements GroupListener {
+        @Override
+        public void preferredSizeChanged(Group group, int previousPreferredWidth,
+            int previousPreferredHeight) {
+            for (GroupListener listener : listeners()) {
+                listener.preferredSizeChanged(group, previousPreferredWidth, previousPreferredHeight);
+            }
+        }
+
+        @Override
+        public void widthLimitsChanged(Group group, int previousMinimumWidth,
+            int previousMaximumWidth) {
+            for (GroupListener listener : listeners()) {
+                listener.widthLimitsChanged(group, previousMinimumWidth, previousMaximumWidth);
+            }
+        }
+
+        @Override
+        public void heightLimitsChanged(Group group, int previousMinimumHeight,
+            int previousMaximumHeight) {
+            for (GroupListener listener : listeners()) {
+                listener.heightLimitsChanged(group, previousMinimumHeight, previousMaximumHeight);
+            }
+        }
+
+        @Override
+        public void layoutChanged(Group group, Layout previousLayout) {
+            for (GroupListener listener : listeners()) {
+                listener.layoutChanged(group, previousLayout);
+            }
+        }
+
+        @Override
+        public void focusTraversalPolicyChanged(Group group,
+            FocusTraversalPolicy previousFocusTraversalPolicy) {
+            for (GroupListener listener : listeners()) {
+                listener.focusTraversalPolicyChanged(group, previousFocusTraversalPolicy);
+            }
+        }
+    }
+
+    private static class GroupMouseListenerList extends ListenerList<GroupMouseListener>
+        implements GroupMouseListener {
+        @Override
+        public boolean mouseMoved(Group group, int x, int y, boolean captured) {
+            for (GroupMouseListener listener : listeners()) {
+                listener.mouseMoved(group, x, y, captured);
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean mousePressed(Group group, Mouse.Button button, int x, int y) {
+            for (GroupMouseListener listener : listeners()) {
+                listener.mousePressed(group, button, x, y);
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean mouseReleased(Group group, Mouse.Button button, int x, int y) {
+            for (GroupMouseListener listener : listeners()) {
+                listener.mouseReleased(group, button, x, y);
+            }
+
+            return false;
+        }
+
+        @Override
+        public boolean mouseWheelScrolled(Group group, Mouse.ScrollType scrollType,
+            int scrollAmount, int wheelRotation, int x, int y) {
+            for (GroupMouseListener listener : listeners()) {
+                listener.mouseWheelScrolled(group, scrollType, scrollAmount, wheelRotation, x, y);
+            }
+
+            return false;
+        }
+    }
+
+    private ObservableList<Node> nodes = new NodeList();
+
+    private Layout layout = null;
+    private FocusTraversalPolicy focusTraversalPolicy = null;
+
+    // Preferred width and height values explicitly set by the user
+    private int preferredWidth = -1;
+    private int preferredHeight = -1;
+
+    // Bounds on preferred size
+    private int minimumWidth = 0;
+    private int maximumWidth = Integer.MAX_VALUE;
+    private int minimumHeight = 0;
+    private int maximumHeight = Integer.MAX_VALUE;
+
+    // Calculated preferred size value
+    private Dimensions preferredSize = null;
+
+    // Calculated baseline for current size
+    private int baseline = -1;
+
+    private Node mouseOverNode = null;
+    private boolean mouseDown = false;
+    private Node mouseDownNode = null;
+    private long mouseDownTime = 0;
+    private int mouseClickCount = 0;
+    private boolean mouseClickConsumed = false;
+
+    // Listener lists
+    private GroupListenerList groupListeners = new GroupListenerList();
+    private GroupMouseListenerList groupMouseListeners = new GroupMouseListenerList();
+
+    public Group() {
+        this(null);
+    }
+
+    public Group(Layout layout) {
+        setLayout(layout);
+    }
+
+    /**
+     * Returns the list of this group's child nodes.
+     */
+    public ObservableList<Node> getNodes() {
+        return nodes;
+    }
+
+    @Override
+    protected void setGroup(Group group) {
+        // If this group is being removed from the node hierarchy
+        // and contains the focused node, clear the focus
+        if (group == null
+            && containsFocus()) {
+            clearFocus();
+        }
+
+        super.setGroup(group);
+    }
+
+    public Node getNodeAt(int x, int y) {
+        Node node = null;
+
+        int i = nodes.size() - 1;
+        while (i >= 0) {
+            node = nodes.get(i);
+            if (node.isVisible()) {
+                Bounds bounds = node.getBounds();
+                if (bounds.contains(x, y)) {
+                    break;
+                }
+            }
+
+            i--;
+        }
+
+        if (i < 0) {
+            node = null;
+        }
+
+        return node;
+    }
+
+    public Node getDescendantAt(int x, int y) {
+        Node descendant = getNodeAt(x, y);
+
+        if (descendant instanceof Group) {
+            Group group = (Group)descendant;
+            descendant = group.getDescendantAt(x - group.getX(), y - group.getY());
+        }
+
+        if (descendant == null) {
+            descendant = this;
+        }
+
+        return descendant;
+    }
+
+    /**
+     * Tests if this group is an ancestor of a given node. A group
+     * is considered to be its own ancestor.
+     *
+     * @param node
+     * The node to test.
+     *
+     * @return
+     * <tt>true</tt> if this group is an ancestor of <tt>node</tt>;
+     * <tt>false</tt> otherwise.
+     */
+    public boolean isAncestor(Node node) {
+        boolean ancestor = false;
+
+        while (node != null) {
+           if (node == this) {
+              ancestor = true;
+              break;
+           }
+
+           node = node.getGroup();
+        }
+
+        return ancestor;
+    }
+
+    @Override
+    public void setVisible(boolean visible) {
+        if (!visible
+            && containsFocus()) {
+            clearFocus();
+        }
+
+        super.setVisible(visible);
+    }
+
+    /**
+     * Returns the layout the group uses to arrange its children.
+     */
+    public Layout getLayout() {
+        return layout;
+    }
+
+    /**
+     * Sets the layout the group uses to arrange its children.
+     *
+     * @param layout
+     * The layout the group will use to arrange its children, or <tt>null</tt>
+     * for no layout.
+     */
+    public void setLayout(Layout layout) {
+        Layout previousLayout = this.layout;
+        if (previousLayout != layout) {
+            this.layout = layout;
+            invalidate();
+
+            groupListeners.layoutChanged(this, previousLayout);
+        }
+    }
+
+    @Override
+    public Extents getExtents() {
+        int minimumX = 0;
+        int maximumX = 0;
+        int minimumY = 0;
+        int maximumY = 0;
+
+        for (int i = 0, n = nodes.size(); i < n; i++) {
+            Node node = nodes.get(i);
+
+            if (node.isVisible()) {
+                Extents extents = node.getExtents();
+
+                minimumX = Math.min(extents.minimumX, minimumX);
+                maximumX = Math.max(extents.maximumX, maximumX);
+                minimumY = Math.min(extents.minimumY, minimumY);
+                maximumY = Math.max(extents.maximumY, maximumY);
+            }
+        }
+
+        return new Extents(minimumX, maximumX, minimumY, maximumY);
+    }
+
+    @Override
+    public boolean contains(int x, int y) {
+        // TODO Find node at x, y and call contains() on it
+
+        // TODO What should we return here? Does a group always contain the point
+        // if it is within the extents? IMPORTANT - we may need to cache extents
+        // in this class, since we may need to use it here.
+
+        // TODO How does contains() affect mouse notifications?
+
+        return false;
+    }
+
+    @Override
+    public boolean isFocusable() {
+        return false;
+    }
+
+    /**
+     * Returns the group's unconstrained preferred width.
+     */
+    public int getPreferredWidth() {
+        return getPreferredWidth(-1);
+    }
+
+    /**
+     * Returnsgrouponent's constrained preferred width.
+     *
+     * @param height
+     * The height value by which the preferred width should be constrained, or
+     * <tt>-1</tt> for no constraint.
+     *
+     * @return
+     * The constrained preferred width.
+     */
+    @Override
+    public int getPreferredWidth(int height) {
+        int preferredWidth;
+
+        if (this.preferredWidth == -1) {
+            if (height == -1) {
+                preferredWidth = getPreferredSize().width;
+            } else {
+                if (preferredSize != null
+                    && preferredSize.height == height) {
+                    preferredWidth = preferredSize.width;
+                } else {
+                    preferredWidth = (layout == null) ? 0 : layout.getPreferredWidth(this, height);
+
+                    Limits widthLimits = getWidthLimits();
+                    preferredWidth = widthLimits.constrain(preferredWidth);
+                }
+            }
+        } else {
+            preferredWidth = this.preferredWidth;
+        }
+
+        return preferredWidth;
+    }
+
+    /**
+     * Sets the group's preferred width.
+     *
+     * @param preferredWidth
+     * The preferred width value, or <tt>-1</tt> to use the default
+     * value determined by the skin.
+     */
+    public void setPreferredWidth(int preferredWidth) {
+        setPreferredSize(preferredWidth, preferredHeight);
+    }
+
+    /**
+     * Returns a flag indicating whether the preferred width was explicitly
+     * set by the caller or is the default value determined by the skin.
+     *
+     * @return
+     * <tt>true</tt> if the preferred width was explicitly set; <tt>false</tt>,
+     * otherwise.
+     */
+    public boolean isPreferredWidthSet() {
+        return (preferredWidth != -1);
+    }
+
+    /**
+     * Returns the group's unconstrained preferred height.
+     */
+    public int getPreferredHeight() {
+        return getPreferredHeight(-1);
+    }
+
+    /**
+     * Returns the group's constrained preferred height.
+     *
+     * @param width
+     * The width value by which the preferred height should be constrained, or
+     * <tt>-1</tt> for no constraint.
+     *
+     * @return
+     * The constrained preferred height.
+     */
+    @Override
+    public int getPreferredHeight(int width) {
+        int preferredHeight;
+
+        if (this.preferredHeight == -1) {
+            if (width == -1) {
+                preferredHeight = getPreferredSize().height;
+            } else {
+                if (preferredSize != null
+                    && preferredSize.width == width) {
+                    preferredHeight = preferredSize.height;
+                } else {
+                    preferredHeight = (layout == null) ? 0 : layout.getPreferredHeight(this, width);
+
+                    Limits heightLimits = getHeightLimits();
+                    preferredHeight = heightLimits.constrain(preferredHeight);
+                }
+            }
+        } else {
+            preferredHeight = this.preferredHeight;
+        }
+
+        return preferredHeight;
+    }
+
+    /**
+     * Sets the group's preferred height.
+     *
+     * @param preferredHeight
+     * The preferred height value, or <tt>-1</tt> to use the default
+     * value determined by the skin.
+     */
+    public void setPreferredHeight(int preferredHeight) {
+        setPreferredSize(preferredWidth, preferredHeight);
+    }
+
+    /**
+     * Returns a flag indicating whether the preferred height was explicitly
+     * set by the caller or is the default value determined by the skin.
+     *
+     * @return
+     * <tt>true</tt> if the preferred height was explicitly set; <tt>false</tt>,
+     * otherwise.
+     */
+    public boolean isPreferredHeightSet() {
+        return (preferredHeight != -1);
+    }
+
+    /**
+     * Gets the group's unconstrained preferred size.
+     */
+    public Dimensions getPreferredSize() {
+        if (preferredSize == null) {
+            if (layout == null) {
+                preferredSize = new Dimensions(0, 0);
+            } else {
+                Dimensions preferredSize;
+
+                if (preferredWidth == -1
+                    && preferredHeight == -1) {
+                    preferredSize = new Dimensions(layout.getPreferredWidth(this, preferredHeight),
+                        layout.getPreferredHeight(this, preferredWidth));
+                } else if (preferredWidth == -1) {
+                    preferredSize = new Dimensions(layout.getPreferredWidth(this, preferredHeight),
+                        preferredHeight);
+                } else if (preferredHeight == -1) {
+                    preferredSize = new Dimensions(preferredWidth, layout.getPreferredHeight(this,
+                        preferredWidth));
+                } else {
+                    preferredSize = new Dimensions(preferredWidth, preferredHeight);
+                }
+
+                Limits widthLimits = getWidthLimits();
+                Limits heightLimits = getHeightLimits();
+
+                int preferredWidth = widthLimits.constrain(preferredSize.width);
+                int preferredHeight = heightLimits.constrain(preferredSize.height);
+
+                if (preferredSize.width > preferredWidth) {
+                    preferredHeight = heightLimits.constrain(layout.getPreferredHeight(this, preferredWidth));
+                }
+
+                if (preferredSize.height > preferredHeight) {
+                    preferredWidth = widthLimits.constrain(layout.getPreferredWidth(this, preferredHeight));
+                }
+
+                this.preferredSize = new Dimensions(preferredWidth, preferredHeight);
+            }
+        }
+
+        return preferredSize;
+    }
+
+    public final void setPreferredSize(Dimensions preferredSize) {
+        if (preferredSize == null) {
+            throw new IllegalArgumentException("preferredSize is null.");
+        }
+
+        setPreferredSize(preferredSize.width, preferredSize.height);
+    }
+
+    /**
+     * Sets the group's preferred size.
+     *
+     * @param preferredWidth
+     * The preferred width value, or <tt>-1</tt> to use the default
+     * value determined by the skin.
+     *
+     * @param preferredHeight
+     * The preferred height value, or <tt>-1</tt> to use the default
+     * value determined by the skin.
+     */
+    public void setPreferredSize(int preferredWidth, int preferredHeight) {
+        if (preferredWidth < -1) {
+            throw new IllegalArgumentException(preferredWidth
+                + " is not a valid value for preferredWidth.");
+        }
+
+        if (preferredHeight < -1) {
+            throw new IllegalArgumentException(preferredHeight
+                + " is not a valid value for preferredHeight.");
+        }
+
+        int previousPreferredWidth = this.preferredWidth;
+        int previousPreferredHeight = this.preferredHeight;
+
+        if (previousPreferredWidth != preferredWidth
+            || previousPreferredHeight != preferredHeight) {
+            this.preferredWidth = preferredWidth;
+            this.preferredHeight = preferredHeight;
+
+            invalidate();
+
+            groupListeners.preferredSizeChanged(this, previousPreferredWidth,
+                previousPreferredHeight);
+        }
+    }
+
+    /**
+     * Returns a flag indicating whether the preferred size was explicitly
+     * set by the caller or is the default value determined by the skin.
+     *
+     * @return
+     * <tt>true</tt> if the preferred size was explicitly set; <tt>false</tt>,
+     * otherwise.
+     */
+    public boolean isPreferredSizeSet() {
+        return isPreferredWidthSet()
+            && isPreferredHeightSet();
+    }
+
+    /**
+     * Returns the minimum width of this group.
+     */
+    public int getMinimumWidth() {
+        return minimumWidth;
+    }
+
+    /**
+     * Sets the minimum width of this group.
+     *
+     * @param minimumWidth
+     */
+    public void setMinimumWidth(int minimumWidth) {
+        setWidthLimits(minimumWidth, getMaximumWidth());
+    }
+
+    /**
+     * Returns the maximum width of this group.
+     */
+    public int getMaximumWidth() {
+        return maximumWidth;
+    }
+
+    /**
+     * Sets the maximum width of this group.
+     *
+     * @param maximumWidth
+     */
+    public void setMaximumWidth(int maximumWidth) {
+        setWidthLimits(getMinimumWidth(), maximumWidth);
+    }
+
+    /**
+     * Returns the width limits for this group.
+     */
+    public Limits getWidthLimits() {
+        return new Limits(minimumWidth, maximumWidth);
+    }
+
+    /**
+     * Sets the width limits for this group.
+     *
+     * @param minimumWidth
+     * @param maximumWidth
+     */
+    public void setWidthLimits(int minimumWidth, int maximumWidth) {
+        int previousMinimumWidth = this.minimumWidth;
+        int previousMaximumWidth = this.maximumWidth;
+
+        if (previousMinimumWidth != minimumWidth
+            || previousMaximumWidth != maximumWidth) {
+            if (minimumWidth < 0) {
+                throw new IllegalArgumentException("minimumWidth is negative.");
+            }
+
+            if (minimumWidth > maximumWidth) {
+                throw new IllegalArgumentException("minimumWidth is greater than maximumWidth.");
+            }
+
+            this.minimumWidth = minimumWidth;
+            this.maximumWidth = maximumWidth;
+
+            invalidate();
+
+            groupListeners.widthLimitsChanged(this, previousMinimumWidth, previousMaximumWidth);
+        }
+    }
+
+    /**
+     * Sets the width limits for this group.
+     *
+     * @param widthLimits
+     */
+    public final void setWidthLimits(Limits widthLimits) {
+        if (widthLimits == null) {
+            throw new IllegalArgumentException("widthLimits is null.");
+        }
+
+        setWidthLimits(widthLimits.minimum, widthLimits.maximum);
+    }
+
+    /**
+     * Returns the minimum height of this group.
+     */
+    public int getMinimumHeight() {
+        return minimumHeight;
+    }
+
+    /**
+     * Sets the minimum height of this group.
+     *
+     * @param minimumHeight
+     */
+    public void setMinimumHeight(int minimumHeight) {
+        setHeightLimits(minimumHeight, getMaximumHeight());
+    }
+
+    /**
+     * Returns the maximum height of this group.
+     */
+    public int getMaximumHeight() {
+        return maximumHeight;
+    }
+
+    /**
+     * Sets the maximum height of this group.
+     *
+     * @param maximumHeight
+     */
+    public void setMaximumHeight(int maximumHeight) {
+        setHeightLimits(getMinimumHeight(), maximumHeight);
+    }
+
+    /**
+     * Returns the height limits for this group.
+     */
+    public Limits getHeightLimits() {
+        return new Limits(minimumHeight, maximumHeight);
+    }
+
+    /**
+     * Sets the height limits for this group.
+     *
+     * @param minimumHeight
+     * @param maximumHeight
+     */
+    public void setHeightLimits(int minimumHeight, int maximumHeight) {
+        int previousMinimumHeight = this.minimumHeight;
+        int previousMaximumHeight = this.maximumHeight;
+
+        if (previousMinimumHeight != minimumHeight
+            || previousMaximumHeight != maximumHeight) {
+            if (minimumHeight < 0) {
+                throw new IllegalArgumentException("minimumHeight is negative.");
+            }
+
+            if (minimumHeight > maximumHeight) {
+                throw new IllegalArgumentException("minimumHeight is greater than maximumHeight.");
+            }
+
+            this.minimumHeight = minimumHeight;
+            this.maximumHeight = maximumHeight;
+
+            invalidate();
+
+            groupListeners.heightLimitsChanged(this, previousMinimumHeight, previousMaximumHeight);
+        }
+    }
+
+    /**
+     * Sets the height limits for this group.
+     *
+     * @param heightLimits
+     */
+    public final void setHeightLimits(Limits heightLimits) {
+        if (heightLimits == null) {
+            throw new IllegalArgumentException("heightLimits is null.");
+        }
+
+        setHeightLimits(heightLimits.minimum, heightLimits.maximum);
+    }
+
+    @Override
+    public int getBaseline(int width, int height) {
+        return (layout == null) ? -1 : layout.getBaseline(this, width, height);
+    }
+
+    /**
+     * Returns the group's baseline.
+     *
+     * @return
+     * The baseline relative to the origin of this group, or <tt>-1</tt> if
+     * this group does not have a baseline.
+     */
+    public int getBaseline() {
+        if (baseline == -1) {
+            baseline = getBaseline(getWidth(), getHeight());
+        }
+
+        return baseline;
+    }
+
+    // ----------------------------------------
+
+    @Override
+    public void layout() {
+        if (layout != null) {
+            layout.layout(this);
+        }
+
+        for (int i = 0, n = nodes.size(); i < n; i++) {
+            Node node = nodes.get(i);
+            node.validate();
+        }
+    }
+
+    @Override
+    public void invalidate() {
+        // Clear the preferred size and baseline
+        preferredSize = null;
+        baseline = -1;
+
+        super.invalidate();
+    }
+
+    @Override
+    public void paint(Graphics graphics) {
+        Bounds clipBounds = graphics.getClipBounds();
+
+        for (int i = 0, n = nodes.size(); i < n; i++) {
+            Node node = nodes.get(i);
+
+            // Only paint nodes that are visible and intersect the
+            // current clip rectangle
+            if (node.isVisible()
+                && node.getDecoratedBounds().intersects(clipBounds)) {
+                paintNode(graphics, node);
+            }
+        }
+    }
+
+    private void paintNode(Graphics graphics, Node node) {
+        // TODO Transform graphics before passing to node, etc.
+
+        Bounds nodeBounds = node.getBounds();
+
+        // Create a copy of the current graphics context and
+        // translate to the node's coordinate system
+        Graphics decoratedGraphics = graphics.create();
+        decoratedGraphics.translate(nodeBounds.x, nodeBounds.y);
+
+        // Prepare the decorators
+        List<Decorator> decorators = node.getDecorators();
+        int n = decorators.size();
+
+        for (int j = n - 1; j >= 0; j--) {
+            Decorator decorator = decorators.get(j);
+            decoratedGraphics = decorator.prepare(node, decoratedGraphics);
+        }
+
+        // Paint the node
+        Graphics nodeGraphics = decoratedGraphics.create();
+        if (node.getClip()) {
+            nodeGraphics.clip(0, 0, nodeBounds.width, nodeBounds.height);
+        }
+
+        node.paint(nodeGraphics);
+        nodeGraphics.dispose();
+
+        // Update the decorators
+        for (int j = 0; j < n; j++) {
+            Decorator decorator = decorators.get(j);
+            decorator.update();
+        }
+    }
+
+    /**
+     * Requests that focus be given to this group. If this group is not
+     * focusable, this requests that focus be set to the first focusable
+     * descendant in this group.
+     *
+     * @return
+     * The node that got the focus, or <tt>null</tt> if the focus request
+     * was denied
+     */
+    @Override
+    public boolean requestFocus() {
+        boolean focused = false;
+
+        if (isFocusable()) {
+            focused = super.requestFocus();
+        } else {
+            if (focusTraversalPolicy != null) {
+                Node first = focusTraversalPolicy.getNextNode(this, null, FocusTraversalDirection.FORWARD);
+
+                Node node = first;
+                while (node != null
+                    && !node.requestFocus()) {
+                    node = focusTraversalPolicy.getNextNode(this, node, FocusTraversalDirection.FORWARD);
+
+                    // Ensure that we don't get into an infinite loop
+                    if (node == first) {
+                        break;
+                    }
+                }
+
+                focused = (node != null);
+            }
+        }
+
+        return focused;
+    }
+
+    /**
+     * Transfers focus to the next focusable node.
+     *
+     * @param node
+     * The node from which focus will be transferred.
+     *
+     * @param direction
+     * The direction in which to transfer focus.
+     */
+    public Node transferFocus(Node node, FocusTraversalDirection direction) {
+        if (focusTraversalPolicy == null) {
+            // The group has no traversal policy; move up a level
+            node = transferFocus(direction);
+        } else {
+            do {
+                node = focusTraversalPolicy.getNextNode(this, node, direction);
+
+                if (node != null) {
+                    if (node.isFocusable()) {
+                        node.requestFocus();
+                    } else {
+                        if (node instanceof Group) {
+                            Group group = (Group)node;
+                            node = group.transferFocus(null, direction);
+                        }
+                    }
+                }
+            } while (node != null
+                && !node.isFocused());
+
+            if (node == null) {
+                // We are at the end of the traversal
+                node = transferFocus(direction);
+            }
+        }
+
+        return node;
+    }
+
+    /**
+     * Returns this group's focus traversal policy.
+     */
+    public FocusTraversalPolicy getFocusTraversalPolicy() {
+        return this.focusTraversalPolicy;
+    }
+
+    /**
+     * Sets this group's focus traversal policy.
+     *
+     * @param focusTraversalPolicy
+     * The focus traversal policy to use with this group.
+     */
+    public void setFocusTraversalPolicy(FocusTraversalPolicy focusTraversalPolicy) {
+        FocusTraversalPolicy previousFocusTraversalPolicy = this.focusTraversalPolicy;
+
+        if (previousFocusTraversalPolicy != focusTraversalPolicy) {
+            this.focusTraversalPolicy = focusTraversalPolicy;
+            groupListeners.focusTraversalPolicyChanged(this, previousFocusTraversalPolicy);
+        }
+    }
+
+    /**
+     * Tests whether this group is an ancestor of the currently focused
+     * node.
+     *
+     * @return
+     * <tt>true</tt> if a node is focused and this group is an
+     * ancestor of the node; <tt>false</tt>, otherwise.
+     */
+    public boolean containsFocus() {
+        Node focusedNode = getFocusedNode();
+        return (focusedNode != null
+            && isAncestor(focusedNode));
+    }
+
+    protected void descendantAdded(Node descendant) {
+        Group group = getGroup();
+
+        if (group != null) {
+            group.descendantAdded(descendant);
+        }
+    }
+
+    protected void descendantRemoved(Node descendant) {
+        Group group = getGroup();
+
+        if (group != null) {
+            group.descendantRemoved(descendant);
+        }
+    }
+
+    protected void descendantGainedFocus(Node descendant) {
+        Group group = getGroup();
+
+        if (group != null) {
+            group.descendantGainedFocus(descendant);
+        }
+    }
+
+    protected void descendantLostFocus(Node descendant) {
+        Group group = getGroup();
+
+        if (group != null) {
+            group.descendantLostFocus(descendant);
+        }
+    }
+
+    @Override
+    protected boolean mouseMoved(int x, int y, boolean captured) {
+        boolean consumed = false;
+
+        // Clear the mouse over node if its mouse-over state has
+        // changed (e.g. if its enabled or visible properties have
+        // changed)
+        if (mouseOverNode != null
+            && !mouseOverNode.isMouseOver()) {
+            mouseOverNode = null;
+        }
+
+        if (isEnabled()) {
+            // Synthesize mouse over/out events
+            Node node = getNodeAt(x, y);
+
+            if (mouseOverNode != node) {
+                if (mouseOverNode != null) {
+                    mouseOverNode.mouseExited();
+                }
+
+                mouseOverNode = null;
+            }
+
+            // Notify group listeners
+            consumed = groupMouseListeners.mouseMoved(this, x, y, captured);
+
+            if (!consumed) {
+                if (mouseOverNode != node) {
+                    mouseOverNode = node;
+
+                    if (mouseOverNode!= null) {
+                        mouseOverNode.mouseEntered();
+                    }
+                }
+
+                // Propagate event to subnodes
+                if (node != null) {
+                    // TODO Transform coordinates before passing to node
+                    consumed = node.mouseMoved(x - node.getX(), y - node.getY(), captured);
+                }
+
+                // Notify the base class
+                if (!consumed) {
+                    consumed = super.mouseMoved(x, y, captured);
+                }
+            }
+        }
+
+        return consumed;
+    }
+
+    @Override
+    protected void mouseExited() {
+        // Ensure that mouse out is called on descendant nodes
+        if (mouseOverNode != null
+            && mouseOverNode.isMouseOver()) {
+            mouseOverNode.mouseExited();
+        }
+
+        mouseOverNode = null;
+
+        super.mouseExited();
+    }
+
+    @Override
+    protected boolean mousePressed(Mouse.Button button, int x, int y) {
+        boolean consumed = false;
+
+        mouseDown = true;
+
+        if (isEnabled()) {
+            // Notify group listeners
+            consumed = groupMouseListeners.mousePressed(this, button, x, y);
+
+            if (!consumed) {
+                // Synthesize mouse click event
+                Node node = getNodeAt(x, y);
+
+                long currentTime = System.currentTimeMillis();
+                int multiClickInterval = Mouse.getMultiClickInterval();
+                if (mouseDownNode == node
+                    && currentTime - mouseDownTime < multiClickInterval) {
+                    mouseClickCount++;
+                } else {
+                    mouseDownTime = System.currentTimeMillis();
+                    mouseClickCount = 1;
+                }
+
+                mouseDownNode = node;
+
+                // Propagate event to subnodes
+                if (node != null) {
+                    // Ensure that mouse over is called
+                    if (!node.isMouseOver()) {
+                        node.mouseEntered();
+                    }
+
+                    // TODO Transform coordinates before passing to node
+                    consumed = node.mousePressed(button, x - node.getX(), y - node.getY());
+                }
+
+                // Notify the base class
+                if (!consumed) {
+                    consumed = super.mousePressed(button, x, y);
+                }
+            }
+        }
+
+        return consumed;
+    }
+
+    @Override
+    protected boolean mouseReleased(Mouse.Button button, int x, int y) {
+        boolean consumed = false;
+
+        if (isEnabled()) {
+            // Notify group listeners
+            consumed = groupMouseListeners.mouseReleased(this, button, x, y);
+
+            if (!consumed) {
+                // Propagate event to subnodes
+                Node node = getNodeAt(x, y);
+
+                if (node != null) {
+                    // Ensure that mouse over is called
+                    if (!node.isMouseOver()) {
+                        node.mouseEntered();
+                    }
+
+                    // TODO Transform coordinates before passing to node
+                    consumed = node.mouseReleased(button, x - node.getX(), y - node.getY());
+                }
+
+                // Notify the base class
+                if (!consumed) {
+                    consumed = super.mouseReleased(button, x, y);
+                }
+
+                // Synthesize mouse click event
+                if (mouseDown
+                    && node != null
+                    && node == mouseDownNode
+                    && node.isEnabled()
+                    && node.isVisible()) {
+
+                    // TODO Transform coordinates before passing to node (consolidate with
+                    // above?)
+                    mouseClickConsumed = node.mouseClicked(button, x - node.getX(),
+                        y - node.getY(), mouseClickCount);
+                }
+            }
+        }
+
+        mouseDown = false;
+
+        return consumed;
+    }
+
+    @Override
+    protected boolean mouseClicked(Mouse.Button button, int x, int y, int count) {
+        if (isEnabled()) {
+            if (!mouseClickConsumed) {
+                // Allow the event to propagate
+                mouseClickConsumed = super.mouseClicked(button, x, y, count);
+            }
+        }
+
+        return mouseClickConsumed;
+    }
+
+    @Override
+    protected boolean mouseWheelScrolled(Mouse.ScrollType scrollType, int scrollAmount,
+        int wheelRotation, int x, int y) {
+        boolean consumed = false;
+
+        if (isEnabled()) {
+            // Notify group listeners
+            consumed = groupMouseListeners.mouseWheelScrolled(this, scrollType, scrollAmount,
+                wheelRotation, x, y);
+
+            if (!consumed) {
+                // Propagate event to subnodes
+                Node node = getNodeAt(x, y);
+
+                if (node != null) {
+                    // Ensure that mouse over is called
+                    if (!node.isMouseOver()) {
+                        node.mouseEntered();
+                    }
+
+                    // TODO Transform coordinates before passing to node
+                    consumed = node.mouseWheelScrolled(scrollType, scrollAmount, wheelRotation,
+                        x - node.getX(), y - node.getY());
+                }
+
+                // Notify the base class
+                if (!consumed) {
+                    consumed = super.mouseWheelScrolled(scrollType, scrollAmount,
+                        wheelRotation, x, y);
+                }
+            }
+        }
+
+        return consumed;
+    }
+
+    public ListenerList<GroupListener> getGroupListeners() {
+        return groupListeners;
+    }
+
+    public ListenerList<GroupMouseListener> getGroupMouseListeners() {
+        return groupMouseListeners;
+    }
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupListener.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupListener.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupListener.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupListener.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+/**
+ * Group listener interface.
+ */
+public interface GroupListener {
+    /**
+     * Group listener adapter.
+     */
+    public static class Adapter implements GroupListener {
+        @Override
+        public void preferredSizeChanged(Group group, int previousPreferredWidth,
+            int previousPreferredHeight) {
+        }
+
+        @Override
+        public void widthLimitsChanged(Group group, int previousMinimumWidth,
+            int previousMaximumWidth) {
+        }
+
+        @Override
+        public void heightLimitsChanged(Group group, int previousMinimumHeight,
+            int previousMaximumHeight) {
+        }
+
+
+        @Override
+        public void layoutChanged(Group group, Layout previousLayout) {
+        }
+
+        @Override
+        public void focusTraversalPolicyChanged(Group group,
+            FocusTraversalPolicy previousFocusTraversalPolicy) {
+        }
+    }
+
+    /**
+     * Called when a group's preferred size has changed.
+     *
+     * @param group
+     * @param previousPreferredWidth
+     * @param previousPreferredHeight
+     */
+    public void preferredSizeChanged(Group group, int previousPreferredWidth,
+        int previousPreferredHeight);
+
+    /**
+     * Called when a group's preferred width limits have changed.
+     *
+     * @param group
+     * @param previousMinimumWidth
+     * @param previousMaximumWidth
+     */
+    public void widthLimitsChanged(Group group, int previousMinimumWidth,
+        int previousMaximumWidth);
+
+    /**
+     * Called when a group's preferred height limits have changed.
+     *
+     * @param group
+     * @param previousMinimumHeight
+     * @param previousMaximumHeight
+     */
+    public void heightLimitsChanged(Group group, int previousMinimumHeight,
+        int previousMaximumHeight);
+
+    /**
+     * Called when a group's layout has changed.
+     *
+     * @param group
+     * @param previousLayout
+     */
+    public void layoutChanged(Group group, Layout previousLayout);
+
+    /**
+     * Called when a group's focus traversal policy has changed.
+     *
+     * @param group
+     * @param previousFocusTraversalPolicy
+     */
+    public void focusTraversalPolicyChanged(Group group,
+        FocusTraversalPolicy previousFocusTraversalPolicy);
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupMouseListener.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupMouseListener.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupMouseListener.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/GroupMouseListener.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+/**
+ * Group mouse listener interface. Group mouse events are "tunneling" events;
+ * consuming a group mouse event prevents it from propagating down the node
+ * hierarchy.
+ */
+public interface GroupMouseListener {
+    /**
+     * Group mouse listener adapter.
+     */
+    public static class Adapter implements GroupMouseListener {
+        @Override
+        public boolean mouseMoved(Group group, int x, int y, boolean captured) {
+            return false;
+        }
+
+        @Override
+        public boolean mousePressed(Group group, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        @Override
+        public boolean mouseReleased(Group group, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        @Override
+        public boolean mouseWheelScrolled(Group group, Mouse.ScrollType scrollType,
+            int scrollAmount, int wheelRotation, int x, int y) {
+            return false;
+        }
+    }
+
+    /**
+     * Called when the mouse is moved while over a group.
+     *
+     * @param group
+     * @param x
+     * @param y
+     * @param captured
+     *
+     * @return
+     * <tt>true</tt> to consume the event; <tt>false</tt> to allow it to
+     * propagate.
+     */
+    public boolean mouseMoved(Group group, int x, int y, boolean captured);
+
+    /**
+     * Called when the mouse is pressed over a group.
+     *
+     * @param group
+     * @param button
+     * @param x
+     * @param y
+     *
+     * @return
+     * <tt>true</tt> to consume the event; <tt>false</tt> to allow it to
+     * propagate.
+     */
+    public boolean mousePressed(Group group, Mouse.Button button, int x, int y);
+
+    /**
+     * Called when the mouse is released over a group.
+     *
+     * @param group
+     * @param button
+     * @param x
+     * @param y
+     *
+     * @return
+     * <tt>true</tt> to consume the event; <tt>false</tt> to allow it to
+     * propagate.
+     */
+    public boolean mouseReleased(Group group, Mouse.Button button, int x, int y);
+
+    /**
+     * Called when the mouse wheel is scrolled over a group.
+     *
+     * @param group
+     * @param scrollType
+     * @param scrollAmount
+     * @param wheelRotation
+     * @param x
+     * @param y
+     *
+     * @return
+     * <tt>true</tt> to consume the event; <tt>false</tt> to allow it to
+     * propagate.
+     */
+    public boolean mouseWheelScrolled(Group group, Mouse.ScrollType scrollType,
+        int scrollAmount, int wheelRotation, int x, int y);
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/HorizontalAlignment.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/HorizontalAlignment.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/HorizontalAlignment.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/HorizontalAlignment.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+/**
+ * Enumeration representing horizontal alignment values.
+ */
+public enum HorizontalAlignment {
+    /**
+     * Align to the right.
+     */
+    RIGHT,
+
+    /**
+     * Align to the left.
+     */
+    LEFT,
+
+    /**
+     * Align to center.
+     */
+    CENTER
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Insets.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Insets.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Insets.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Insets.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import org.apache.pivot.beans.BeanAdapter;
+import org.apache.pivot.io.SerializationException;
+import org.apache.pivot.json.JSONSerializer;
+
+/**
+ * Class representing the insets of an object.
+ */
+public final class Insets implements Serializable {
+    private static final long serialVersionUID = 0;
+
+    public final int top;
+    public final int left;
+    public final int bottom;
+    public final int right;
+
+    public static final String TOP_KEY = "top";
+    public static final String LEFT_KEY = "left";
+    public static final String BOTTOM_KEY = "bottom";
+    public static final String RIGHT_KEY = "right";
+
+    public static final Insets NONE = new Insets(0);
+
+    public Insets(int inset) {
+        this.top = inset;
+        this.left = inset;
+        this.bottom = inset;
+        this.right = inset;
+    }
+
+    public Insets(int top, int left, int bottom, int right) {
+        this.top = top;
+        this.left = left;
+        this.bottom = bottom;
+        this.right = right;
+    }
+
+    public Insets(Insets insets) {
+        if (insets == null) {
+            throw new IllegalArgumentException("insets is null.");
+        }
+
+        this.top = insets.top;
+        this.left = insets.left;
+        this.bottom = insets.bottom;
+        this.right = insets.right;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        boolean equals = false;
+
+        if (object instanceof Insets) {
+            Insets insets = (Insets) object;
+            equals = (top == insets.top
+                && left == insets.left
+                && bottom == insets.bottom
+                && right == insets.right);
+        }
+
+        return equals;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + top;
+        result = prime * result + left;
+        result = prime * result + bottom;
+        result = prime * result + right;
+
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getName() + " [" + top + ", " + left + ", " + bottom + ", " + right + "]";
+    }
+
+    public static Insets decode(String value) {
+        if (value == null) {
+            throw new IllegalArgumentException();
+        }
+
+        Insets insets;
+        if (value.startsWith("{")) {
+            Map<String, ?> map;
+            try {
+                map = JSONSerializer.parseMap(value);
+            } catch (SerializationException exception) {
+                throw new IllegalArgumentException(exception);
+            }
+
+            int top = BeanAdapter.getInt(map, TOP_KEY);
+            int left = BeanAdapter.getInt(map, LEFT_KEY);
+            int bottom = BeanAdapter.getInt(map, BOTTOM_KEY);
+            int right = BeanAdapter.getInt(map, RIGHT_KEY);
+
+            insets = new Insets(top, left, bottom, right);
+        } else {
+            insets = new Insets(Integer.parseInt(value));
+        }
+
+        return insets;
+    }
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Keyboard.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Keyboard.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Keyboard.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Keyboard.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,339 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import java.awt.Toolkit;
+import java.awt.event.KeyEvent;
+import java.lang.reflect.Field;
+import java.util.Locale;
+
+/**
+ * Class representing the system keyboard.
+ */
+public final class Keyboard {
+    /**
+     * Enumeration representing keyboard modifiers.
+     */
+    public enum Modifier {
+        SHIFT,
+        CTRL,
+        ALT,
+        META;
+
+        public int getMask() {
+            return 1 << ordinal();
+        }
+    }
+
+    /**
+     * Enumeration representing key locations.
+     */
+    public enum KeyLocation {
+        STANDARD,
+        LEFT,
+        RIGHT,
+        KEYPAD
+    }
+
+    /**
+     * Represents a keystroke, a combination of a keycode and modifier flags.
+     */
+    public static final class KeyStroke {
+        private int keyCode = KeyCode.UNDEFINED;
+        private int modifiers = 0x00;
+
+        public static final String COMMAND_ABBREVIATION = "CMD";
+
+        public KeyStroke(int keyCode, int modifiers) {
+            this.keyCode = keyCode;
+            this.modifiers = modifiers;
+        }
+
+        public int getKeyCode() {
+            return keyCode;
+        }
+
+        public int getModifiers() {
+            return modifiers;
+        }
+
+        @Override
+        public boolean equals(Object object) {
+            boolean equals = false;
+
+            if (object instanceof KeyStroke) {
+                KeyStroke keyStroke = (KeyStroke)object;
+                equals = (this.keyCode == keyStroke.keyCode
+                    && this.modifiers == keyStroke.modifiers);
+            }
+
+            return equals;
+        }
+
+        @Override
+        public int hashCode() {
+            // NOTE Key codes are currently defined as 16-bit values, so
+            // shifting by 4 bits to append the modifiers should be OK.
+            // However, if Oracle changes the key code values in the future,
+            // this may no longer be safe.
+            int hashCode = keyCode << 4 | modifiers;
+            return hashCode;
+        }
+
+        @Override
+        public String toString() {
+            int awtModifiers = 0x00;
+
+            if (((modifiers & Modifier.META.getMask()) > 0)) {
+                awtModifiers |= KeyEvent.META_DOWN_MASK;
+            }
+
+            if (((modifiers & Modifier.CTRL.getMask()) > 0)) {
+                awtModifiers |= KeyEvent.CTRL_DOWN_MASK;
+            }
+
+            if (((modifiers & Modifier.ALT.getMask()) > 0)) {
+                awtModifiers |= KeyEvent.ALT_DOWN_MASK;
+            }
+
+            if (((modifiers & Modifier.SHIFT.getMask()) > 0)) {
+                awtModifiers |= KeyEvent.SHIFT_DOWN_MASK;
+            }
+
+            return KeyEvent.getModifiersExText(awtModifiers) + getKeyStrokeModifierSeparator()
+                + KeyEvent.getKeyText(keyCode);
+        }
+
+        public static KeyStroke decode(String value) {
+            if (value == null) {
+                throw new IllegalArgumentException("value is null.");
+            }
+
+            int keyCode = KeyCode.UNDEFINED;
+            int modifiers = 0x00;
+
+            String[] keys = value.split("-");
+            for (int i = 0, n = keys.length; i < n; i++) {
+                if (i < n - 1) {
+                    // Modifier
+                    String modifierAbbreviation = keys[i].toUpperCase(Locale.ENGLISH);
+
+                    Modifier modifier;
+                    if (modifierAbbreviation.equals(COMMAND_ABBREVIATION)) {
+                        modifier = getCommandModifier();
+                    } else {
+                        modifier = Modifier.valueOf(modifierAbbreviation);
+                    }
+
+                    modifiers |= modifier.getMask();
+                } else {
+                    // Keycode
+                    try {
+                        Field keyCodeField = KeyCode.class.getField(keys[i].toUpperCase(Locale.ENGLISH));
+                        keyCode = (Integer)keyCodeField.get(null);
+                    } catch(Exception exception) {
+                        throw new IllegalArgumentException(exception);
+                    }
+                }
+            }
+
+            return new KeyStroke(keyCode, modifiers);
+        }
+    }
+
+    /**
+     * Contains a set of key code constants that are common to all locales.
+     */
+    public static final class KeyCode {
+        public static final int A = KeyEvent.VK_A;
+        public static final int B = KeyEvent.VK_B;
+        public static final int C = KeyEvent.VK_C;
+        public static final int D = KeyEvent.VK_D;
+        public static final int E = KeyEvent.VK_E;
+        public static final int F = KeyEvent.VK_F;
+        public static final int G = KeyEvent.VK_G;
+        public static final int H = KeyEvent.VK_H;
+        public static final int I = KeyEvent.VK_I;
+        public static final int J = KeyEvent.VK_J;
+        public static final int K = KeyEvent.VK_K;
+        public static final int L = KeyEvent.VK_L;
+        public static final int M = KeyEvent.VK_M;
+        public static final int N = KeyEvent.VK_N;
+        public static final int O = KeyEvent.VK_O;
+        public static final int P = KeyEvent.VK_P;
+        public static final int Q = KeyEvent.VK_Q;
+        public static final int R = KeyEvent.VK_R;
+        public static final int S = KeyEvent.VK_S;
+        public static final int T = KeyEvent.VK_T;
+        public static final int U = KeyEvent.VK_U;
+        public static final int V = KeyEvent.VK_V;
+        public static final int W = KeyEvent.VK_W;
+        public static final int X = KeyEvent.VK_X;
+        public static final int Y = KeyEvent.VK_Y;
+        public static final int Z = KeyEvent.VK_Z;
+
+        public static final int N0 = KeyEvent.VK_0;
+        public static final int N1 = KeyEvent.VK_1;
+        public static final int N2 = KeyEvent.VK_2;
+        public static final int N3 = KeyEvent.VK_3;
+        public static final int N4 = KeyEvent.VK_4;
+        public static final int N5 = KeyEvent.VK_5;
+        public static final int N6 = KeyEvent.VK_6;
+        public static final int N7 = KeyEvent.VK_7;
+        public static final int N8 = KeyEvent.VK_8;
+        public static final int N9 = KeyEvent.VK_9;
+
+        public static final int TAB = KeyEvent.VK_TAB;
+        public static final int SPACE = KeyEvent.VK_SPACE;
+        public static final int ENTER = KeyEvent.VK_ENTER;
+        public static final int ESCAPE = KeyEvent.VK_ESCAPE;
+        public static final int BACKSPACE = KeyEvent.VK_BACK_SPACE;
+        public static final int DELETE = KeyEvent.VK_DELETE;
+
+        public static final int UP = KeyEvent.VK_UP;
+        public static final int DOWN = KeyEvent.VK_DOWN;
+        public static final int LEFT = KeyEvent.VK_LEFT;
+        public static final int RIGHT = KeyEvent.VK_RIGHT;
+
+        public static final int PAGE_UP = KeyEvent.VK_PAGE_UP;
+        public static final int PAGE_DOWN = KeyEvent.VK_PAGE_DOWN;
+
+        public static final int HOME = KeyEvent.VK_HOME;
+        public static final int END = KeyEvent.VK_END;
+
+        public static final int KEYPAD_0 = KeyEvent.VK_NUMPAD0;
+        public static final int KEYPAD_1 = KeyEvent.VK_NUMPAD1;
+        public static final int KEYPAD_2 = KeyEvent.VK_NUMPAD2;
+        public static final int KEYPAD_3 = KeyEvent.VK_NUMPAD3;
+        public static final int KEYPAD_4 = KeyEvent.VK_NUMPAD4;
+        public static final int KEYPAD_5 = KeyEvent.VK_NUMPAD5;
+        public static final int KEYPAD_6 = KeyEvent.VK_NUMPAD6;
+        public static final int KEYPAD_7 = KeyEvent.VK_NUMPAD7;
+        public static final int KEYPAD_8 = KeyEvent.VK_NUMPAD8;
+        public static final int KEYPAD_9 = KeyEvent.VK_NUMPAD9;
+        public static final int KEYPAD_UP = KeyEvent.VK_KP_UP;
+        public static final int KEYPAD_DOWN = KeyEvent.VK_KP_DOWN;
+        public static final int KEYPAD_LEFT = KeyEvent.VK_KP_LEFT;
+        public static final int KEYPAD_RIGHT = KeyEvent.VK_KP_RIGHT;
+
+        public static final int PLUS = KeyEvent.VK_PLUS;
+        public static final int MINUS = KeyEvent.VK_MINUS;
+        public static final int EQUALS = KeyEvent.VK_EQUALS;
+
+        public static final int ADD = KeyEvent.VK_ADD;
+        public static final int SUBTRACT = KeyEvent.VK_SUBTRACT;
+        public static final int MULTIPLY = KeyEvent.VK_MULTIPLY;
+        public static final int DIVIDE = KeyEvent.VK_DIVIDE;
+
+        public static final int F1 = KeyEvent.VK_F1;
+        public static final int F2 = KeyEvent.VK_F2;
+        public static final int F3 = KeyEvent.VK_F3;
+        public static final int F4 = KeyEvent.VK_F4;
+        public static final int F5 = KeyEvent.VK_F5;
+        public static final int F6 = KeyEvent.VK_F6;
+        public static final int F7 = KeyEvent.VK_F7;
+        public static final int F8 = KeyEvent.VK_F8;
+        public static final int F9 = KeyEvent.VK_F9;
+        public static final int F10 = KeyEvent.VK_F10;
+        public static final int F11 = KeyEvent.VK_F11;
+        public static final int F12 = KeyEvent.VK_F12;
+
+        public static final int UNDEFINED = KeyEvent.VK_UNDEFINED;
+    }
+
+    private static int modifiers = 0;
+
+    private static final int DEFAULT_CARET_BLINK_RATE = 600;
+
+    private static final Modifier COMMAND_MODIFIER;
+    private static final Modifier WORD_NAVIGATION_MODIFIER;
+    private static final String KEYSTROKE_MODIFIER_SEPARATOR;
+
+    static {
+        String osName = System.getProperty("os.name").toLowerCase(Locale.ENGLISH);
+
+        if (osName.startsWith("mac os x")) {
+            COMMAND_MODIFIER = Modifier.META;
+            WORD_NAVIGATION_MODIFIER = Modifier.ALT;
+            KEYSTROKE_MODIFIER_SEPARATOR = "";
+        } else {
+            COMMAND_MODIFIER = Modifier.CTRL;
+            WORD_NAVIGATION_MODIFIER = Modifier.CTRL;
+            KEYSTROKE_MODIFIER_SEPARATOR = "-";
+        }
+    }
+
+    /**
+     * Returns a bitfield representing the keyboard modifiers that are
+     * currently pressed.
+     */
+    public static int getModifiers() {
+        return modifiers;
+    }
+
+    protected static void setModifiers(int modifiers) {
+        Keyboard.modifiers = modifiers;
+    }
+
+    /**
+     * Tests the pressed state of a modifier.
+     *
+     * @param modifier
+     *
+     * @return
+     * <tt>true</tt> if the modifier is pressed; <tt>false</tt>, otherwise.
+     */
+    public static boolean isPressed(Modifier modifier) {
+        return (modifiers & modifier.getMask()) > 0;
+    }
+
+    /**
+     * Returns the system caret blink rate.
+     */
+    public static int getCaretBlinkRate() {
+        Toolkit toolkit = Toolkit.getDefaultToolkit();
+        Integer cursorBlinkRate = (Integer)toolkit.getDesktopProperty("awt.cursorBlinkRate");
+
+        if (cursorBlinkRate == null) {
+            cursorBlinkRate = DEFAULT_CARET_BLINK_RATE;
+        }
+
+        return cursorBlinkRate;
+    }
+
+    /**
+     * Returns the system command modifier key.
+     */
+    public static Modifier getCommandModifier() {
+        return COMMAND_MODIFIER;
+    }
+
+    /**
+     * Returns the word navigation modifier key.
+     */
+    public static Modifier getWordNavigationModifier() {
+        return WORD_NAVIGATION_MODIFIER;
+    }
+
+    /**
+     * Returns the keystroke modifier separator text.
+     */
+    public static String getKeyStrokeModifierSeparator() {
+        return KEYSTROKE_MODIFIER_SEPARATOR;
+    }
+}
+

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Layout.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Layout.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Layout.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Layout.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+/**
+ * Interface that encapsulates layout behavior for a group.
+ */
+public interface Layout {
+    /**
+     * Calculates the preferred width for a group.
+     *
+     * @param group
+     * @param height
+     */
+    public int getPreferredWidth(Group group, int height);
+
+    /**
+     * Calculates the preferred height for a group.
+     *
+     * @param group
+     * @param width
+     */
+    public int getPreferredHeight(Group group, int width);
+
+    /**
+     * Calculates the baseline for a group.
+     *
+     * @param group
+     * @param width
+     * @param height
+     * @return
+     */
+    public int getBaseline(Group group, int width, int height);
+
+    /**
+     * Lays out the group.
+     *
+     * @param group
+     */
+    public void layout(Group group);
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/Limits.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/Limits.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/Limits.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/Limits.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,137 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import java.io.Serializable;
+import java.util.Map;
+
+import org.apache.pivot.beans.BeanAdapter;
+import org.apache.pivot.io.SerializationException;
+import org.apache.pivot.json.JSONSerializer;
+
+/**
+ * Class representing minimum and maximum values.
+ */
+public final class Limits implements Serializable {
+    private static final long serialVersionUID = 0;
+
+    public final int minimum;
+    public final int maximum;
+
+    public static final String MINIMUM_KEY = "minimum";
+    public static final String MAXIMUM_KEY = "maximum";
+
+    public Limits(int minimum, int maximum) {
+        if (minimum > maximum) {
+            throw new IllegalArgumentException("minimum is greater than maximum.");
+        }
+
+        this.minimum = minimum;
+        this.maximum = maximum;
+    }
+
+    public Limits(Limits limits) {
+        if (limits == null) {
+            throw new IllegalArgumentException("limits is null.");
+        }
+
+        minimum = limits.minimum;
+        maximum = limits.maximum;
+    }
+
+    /**
+     * Limits the specified value to the minimum and maximum values of
+     * this object.
+     *
+     * @param value
+     * The value to limit.
+     *
+     * @return
+     * The bounded value.
+     */
+    public int constrain(int value) {
+        if (value < minimum) {
+            value = minimum;
+        } else if (value > maximum) {
+            value = maximum;
+        }
+
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        boolean equals = false;
+
+        if (object instanceof Limits) {
+            Limits limits = (Limits)object;
+            equals = (minimum == limits.minimum
+                && maximum == limits.maximum);
+        }
+
+        return equals;
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * minimum + maximum;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buf = new StringBuilder();
+
+        buf.append(getClass().getName());
+        buf.append(" [");
+
+        if (minimum == Integer.MIN_VALUE) {
+            buf.append("MIN");
+        } else {
+            buf.append(minimum);
+        }
+
+        buf.append("-");
+
+        if (maximum == Integer.MAX_VALUE) {
+            buf.append("MAX");
+        } else {
+            buf.append(maximum);
+        }
+
+        buf.append("]");
+
+        return buf.toString();
+    }
+
+    public static Limits decode(String value) {
+        if (value == null) {
+            throw new IllegalArgumentException();
+        }
+
+        Map<String, ?> map;
+        try {
+            map = JSONSerializer.parseMap(value);
+        } catch (SerializationException exception) {
+            throw new IllegalArgumentException(exception);
+        }
+
+        int minimum = BeanAdapter.getInt(map, MINIMUM_KEY);
+        int maximum = BeanAdapter.getInt(map, MAXIMUM_KEY);
+
+        return new Limits(minimum, maximum);
+    }
+}

Added: pivot/branches/3.x/scene/src/org/apache/pivot/scene/LinearGradientPaint.java
URL: http://svn.apache.org/viewvc/pivot/branches/3.x/scene/src/org/apache/pivot/scene/LinearGradientPaint.java?rev=1057053&view=auto
==============================================================================
--- pivot/branches/3.x/scene/src/org/apache/pivot/scene/LinearGradientPaint.java (added)
+++ pivot/branches/3.x/scene/src/org/apache/pivot/scene/LinearGradientPaint.java Sun Jan  9 23:19:19 2011
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.pivot.scene;
+
+import java.util.List;
+
+/**
+ * Class representing a linear gradient paint.
+ */
+public final class LinearGradientPaint extends MultiStopGradientPaint {
+    public final Point start;
+    public final Point end;
+
+    public LinearGradientPaint(int startX, int startY, int endX, int endY, List<Stop> stops) {
+        this(new Point(startX, startY), new Point(endX, endY), stops);
+    }
+
+    public LinearGradientPaint(Point start, Point end, List<Stop> stops) {
+        super(stops);
+
+        this.start = start;
+        this.end = end;
+    }
+
+    @Override
+    protected Object getNativePaint() {
+        if (nativePaint == null) {
+            nativePaint = Platform.getPlatform().getNativePaint(this);
+        }
+
+        return nativePaint;
+    }
+
+    @Override
+    public boolean equals(Object object) {
+        boolean equals = false;
+
+        if (object instanceof LinearGradientPaint) {
+            LinearGradientPaint linearGradientPaint = (LinearGradientPaint)object;
+            equals = (start.equals(linearGradientPaint.start)
+                && end.equals(linearGradientPaint.end));
+        }
+
+        return equals;
+    }
+
+    @Override
+    public int hashCode() {
+        return 31 * start.hashCode() + end.hashCode();
+    }
+}