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/26 00:12:17 UTC

svn commit: r758461 [26/47] - in /incubator/pivot/branches: ./ 1.1/ 1.1/charts-test/ 1.1/charts-test/src/ 1.1/charts-test/src/pivot/ 1.1/charts-test/src/pivot/charts/ 1.1/charts-test/src/pivot/charts/test/ 1.1/charts/ 1.1/charts/lib/ 1.1/charts/src/ 1....

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeView.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeView.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeView.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeView.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,1748 @@
+/*
+ * 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;
+
+import java.util.Comparator;
+
+import pivot.collections.ArrayList;
+import pivot.collections.List;
+import pivot.collections.ListListener;
+import pivot.collections.Sequence;
+import pivot.util.ListenerList;
+import pivot.wtk.content.TreeViewNodeRenderer;
+
+/**
+ * Class that displays a hierarchical data structure, allowing a user to select
+ * one or more paths.
+ *
+ * @author tvolkert
+ */
+public class TreeView extends Component {
+    /**
+     * Enumeration defining supported selection modes. <tt>TreeView</tt>
+     * defaults to single select mode.
+     */
+    public enum SelectMode {
+        /**
+         * Selection is disabled.
+         */
+        NONE,
+
+        /**
+         * A single path may be selected at a time.
+         */
+        SINGLE,
+
+        /**
+         * Multiple paths may be concurrently selected.
+         */
+        MULTI;
+
+        public static SelectMode decode(String value) {
+            return valueOf(value.toUpperCase());
+        }
+    }
+
+    /**
+     * Enumeration defining node check states. Note that <tt>TreeView</tt> does
+     * not involve itself in the propagation of checkmarks (either up or down
+     * the tree). Developers who wish to propagate checkmarks may do so by
+     * registering a {@link TreeViewNodeStateListener} and setting the desired
+     * checkmark states manually.
+     */
+    public enum NodeCheckState {
+        /**
+         * The node is checked.
+         */
+        CHECKED,
+
+        /**
+         * The node is unchecked. If <tt>showMixedCheckmarkState</tt> is true,
+         * this implies that all of the node's descendants are unchecked as
+         * well.
+         */
+        UNCHECKED,
+
+        /**
+         * The node's check state is mixed, meaning that it is not checked,
+         * but at least one of its descendants is checked. This state will only
+         * be reported if <tt>showMixedCheckmarkState</tt> is true. Otherwise,
+         * the node will be reported as {@link #UNCHECKED}.
+         */
+        MIXED;
+
+        public static NodeCheckState decode(String value) {
+            return valueOf(value.toUpperCase());
+        }
+    }
+
+    /**
+     * Tree view node renderer interface.
+     *
+     * @author tvolkert
+     */
+    public interface NodeRenderer extends Renderer {
+        public void render(Object node, TreeView treeView, boolean expanded,
+            boolean selected, NodeCheckState checkState, boolean highlighted,
+            boolean disabled);
+    }
+
+    /**
+     * Tree view node editor interface.
+     *
+     * @author tvolkert
+     */
+    public interface NodeEditor extends Editor {
+        /**
+         * Notifies the editor that editing should begin.
+         *
+         * @param treeView
+         * The tree view containing the node to be edited.
+         *
+         * @param path
+         * The path to the node to edit.
+         */
+        public void edit(TreeView treeView, Sequence<Integer> path);
+    }
+
+    /**
+     * Tree view skin interface. Tree view skins must implement this.
+     *
+     * @author tvolkert
+     */
+    public interface Skin {
+        /**
+         * Gets the path to the node found at the specified y-coordinate
+         * (relative to the tree view).
+         *
+         * @param y
+         * The y-coordinate in pixels.
+         *
+         * @return
+         * The path to the node, or <tt>null</tt> if there is no node being
+         * painted at the specified y-coordinate.
+         */
+        public Sequence<Integer> getNodeAt(int y);
+
+        /**
+         * Gets the bounds of the node at the specified path relative to the
+         * tree view. Note that all nodes are left aligned with the tree; to
+         * get the pixel value of a node's indent, use
+         * {@link #getNodeIndent(int)}.
+         *
+         * @param path
+         * The path to the node.
+         *
+         * @return
+         * The bounds, or <tt>null</tt> if the node is not currently visible.
+         */
+        public Bounds getNodeBounds(Sequence<Integer> path);
+
+        /**
+         * Gets the pixel indent of nodes at the specified depth. Depth is
+         * measured in generations away from the tree view's "root" node, which
+         * is represented by the {@link #getTreeData() tree data}.
+         *
+         * @param depth
+         * The depth, where the first child of the root has depth 1, the child
+         * of that branch has depth 2, etc.
+         *
+         * @return
+         * The indent in pixels.
+         */
+        public int getNodeIndent(int depth);
+    }
+
+    /**
+     * Tree view listener list.
+     *
+     * @author tvolkert
+     */
+    private static class TreeViewListenerList extends ListenerList<TreeViewListener>
+        implements TreeViewListener {
+
+        public void treeDataChanged(TreeView treeView, List<?> previousTreeData) {
+            for (TreeViewListener listener : this) {
+                listener.treeDataChanged(treeView, previousTreeData);
+            }
+        }
+
+        public void nodeRendererChanged(TreeView treeView,
+            NodeRenderer previousNodeRenderer) {
+            for (TreeViewListener listener : this) {
+                listener.nodeRendererChanged(treeView, previousNodeRenderer);
+            }
+        }
+
+        public void nodeEditorChanged(TreeView treeView,
+            TreeView.NodeEditor previousNodeEditor) {
+            for (TreeViewListener listener : this) {
+                listener.nodeEditorChanged(treeView, previousNodeEditor);
+            }
+        }
+        public void selectModeChanged(TreeView treeView, SelectMode previousSelectMode) {
+            for (TreeViewListener listener : this) {
+                listener.selectModeChanged(treeView, previousSelectMode);
+            }
+        }
+
+        public void checkmarksEnabledChanged(TreeView treeView) {
+            for (TreeViewListener listener : this) {
+                listener.checkmarksEnabledChanged(treeView);
+            }
+        }
+
+        public void showMixedCheckmarkStateChanged(TreeView treeView) {
+            for (TreeViewListener listener : this) {
+                listener.showMixedCheckmarkStateChanged(treeView);
+            }
+        }
+    }
+
+    /**
+     * Tree view branch listener list.
+     *
+     * @author tvolkert
+     */
+    private static class TreeViewBranchListenerList extends ListenerList<TreeViewBranchListener>
+        implements TreeViewBranchListener {
+        public void branchExpanded(TreeView treeView, Sequence<Integer> path) {
+            for (TreeViewBranchListener listener : this) {
+                listener.branchExpanded(treeView, path);
+            }
+        }
+
+        public void branchCollapsed(TreeView treeView, Sequence<Integer> path) {
+            for (TreeViewBranchListener listener : this) {
+                listener.branchCollapsed(treeView, path);
+            }
+        }
+    }
+
+    /**
+     * Tree view node listener list.
+     *
+     * @author tvolkert
+     */
+    private static class TreeViewNodeListenerList extends ListenerList<TreeViewNodeListener>
+        implements TreeViewNodeListener {
+        public void nodeInserted(TreeView treeView, Sequence<Integer> path, int index) {
+            for (TreeViewNodeListener listener : this) {
+                listener.nodeInserted(treeView, path, index);
+            }
+        }
+
+        public void nodesRemoved(TreeView treeView, Sequence<Integer> path, int index,
+            int count) {
+            for (TreeViewNodeListener listener : this) {
+                listener.nodesRemoved(treeView, path, index, count);
+            }
+        }
+
+        public void nodeUpdated(TreeView treeView, Sequence<Integer> path, int index) {
+            for (TreeViewNodeListener listener : this) {
+                listener.nodeUpdated(treeView, path, index);
+            }
+        }
+
+        public void nodesSorted(TreeView treeView, Sequence<Integer> path) {
+            for (TreeViewNodeListener listener : this) {
+                listener.nodesSorted(treeView, path);
+            }
+        }
+    }
+
+    /**
+     * Tree view node state listener list.
+     *
+     * @author tvolkert
+     */
+    private static class TreeViewNodeStateListenerList
+        extends ListenerList<TreeViewNodeStateListener>
+        implements TreeViewNodeStateListener {
+        public void nodeDisabledChanged(TreeView treeView, Sequence<Integer> path) {
+            for (TreeViewNodeStateListener listener : this) {
+                listener.nodeDisabledChanged(treeView, path);
+            }
+        }
+
+        public void nodeCheckStateChanged(TreeView treeView, Sequence<Integer> path,
+            TreeView.NodeCheckState previousCheckState) {
+            for (TreeViewNodeStateListener listener : this) {
+                listener.nodeCheckStateChanged(treeView, path, previousCheckState);
+            }
+        }
+    }
+
+    /**
+     * Tree view selection listener list.
+     *
+     * @author tvolkert
+     */
+    private static class TreeViewSelectionListenerList
+        extends ListenerList<TreeViewSelectionListener>
+        implements TreeViewSelectionListener {
+        public void selectedPathAdded(TreeView treeView, Sequence<Integer> path) {
+            for (TreeViewSelectionListener listener : this) {
+                listener.selectedPathAdded(treeView, path);
+            }
+        }
+
+        public void selectedPathRemoved(TreeView treeView, Sequence<Integer> path) {
+            for (TreeViewSelectionListener listener : this) {
+                listener.selectedPathRemoved(treeView, path);
+            }
+        }
+
+        public void selectedPathsChanged(TreeView treeView,
+            Sequence<Sequence<Integer>> previousSelectedPaths) {
+            for (TreeViewSelectionListener listener : this) {
+                listener.selectedPathsChanged(treeView, previousSelectedPaths);
+            }
+        }
+    }
+
+    /**
+     * A comparator that sorts paths by the order in which they would visually
+     * appear in a fully expanded tree, otherwise known as their "row order".
+     *
+     * @author tvolkert
+     */
+    public static final class PathComparator implements Comparator<Sequence<Integer>> {
+        public int compare(Sequence<Integer> path1, Sequence<Integer> path2) {
+            int path1Length = path1.getLength();
+            int path2Length = path2.getLength();
+
+            for (int i = 0, n = Math.min(path1Length, path2Length); i < n; i++) {
+                int pathElement1 = path1.get(i);
+                int pathElement2 = path2.get(i);
+
+                if (pathElement1 != pathElement2) {
+                    return pathElement1 - pathElement2;
+                }
+            }
+
+            return path1Length - path2Length;
+        }
+    }
+
+    /**
+     * Notifies the tree of nested <tt>ListListener</tt> events that occur on
+     * the tree data.
+     *
+     * @author tvolkert
+     */
+    private class BranchHandler extends ArrayList<BranchHandler> implements ListListener<Object> {
+        private static final long serialVersionUID = -6132480635507615071L;
+
+        // Reference to its parent allows for the construction of its path
+        private BranchHandler parent;
+
+        // The backing data structure
+        private List<?> branchData;
+
+        /**
+         * Creates a new <tt>BranchHandler</tt> tied to the specified parent
+         * and listening to events from the specified branch data.
+         */
+        @SuppressWarnings("unchecked")
+        public BranchHandler(BranchHandler parent, List<?> branchData) {
+            super(branchData.getLength());
+
+            this.parent = parent;
+            this.branchData = branchData;
+
+            ((List<Object>)branchData).getListListeners().add(this);
+
+            // Create placeholder child entries, to be loaded lazily
+            for (int i = 0, n = branchData.getLength(); i < n; i++) {
+                add(null);
+            }
+        }
+
+        /**
+         * Gets the branch data that this handler is monitoring.
+         */
+        public List<?> getBranchData() {
+            return branchData;
+        }
+
+        /**
+         * Unregisters this branch handler's interest in ListListener events.
+         * This must be done to release references from the tree data to our
+         * internal BranchHandler data structures. Failure to do so would mean
+         * that our BranchHandler objects would remain in scope as long as the
+         * tree data remained in scope, even if  we were no longer using the
+         * BranchHandler objects.
+         */
+        @SuppressWarnings("unchecked")
+        public void release() {
+            ((List<Object>)branchData).getListListeners().remove(this);
+
+            // Recursively have all child branches unregister interest
+            for (int i = 0, n = getLength(); i < n; i++) {
+                BranchHandler branchHandler = get(i);
+
+                if (branchHandler != null) {
+                    branchHandler.release();
+                }
+            }
+        }
+
+        /**
+         * Gets the path that leads from the root of the tree data to this
+         * branch. Note: <tt>rootBranchHandler.getPath()</tt> will return and
+         * empty sequence.
+         */
+        @SuppressWarnings("unchecked")
+        private Sequence<Integer> getPath() {
+            Sequence<Integer> path = new ArrayList<Integer>();
+
+            BranchHandler handler = this;
+
+            while (handler.parent != null) {
+                int index = ((List<Object>)handler.parent.branchData).indexOf(handler.branchData);
+                path.insert(index, 0);
+
+                handler = handler.parent;
+            }
+
+            return path;
+        }
+
+        public void itemInserted(List<Object> list, int index) {
+            Sequence<Integer> path = getPath();
+
+            // Insert child handler placeholder (lazily loaded)
+            insert(null, index);
+
+            // Update our data structures
+            incrementPaths(expandedPaths, path, index);
+            incrementPaths(selectedPaths, path, index);
+            incrementPaths(disabledPaths, path, index);
+            incrementPaths(checkedPaths, path, index);
+
+            // Notify listeners
+            treeViewNodeListeners.nodeInserted(TreeView.this, path, index);
+        }
+
+        public void itemsRemoved(List<Object> list, int index, Sequence<Object> items) {
+            Sequence<Integer> path = getPath();
+
+            // Remove child handlers
+            int count = (items == null) ? getLength() : items.getLength();
+            Sequence<BranchHandler> removed = remove(index, count);
+
+            // Release each child handler that was removed
+            for (int i = 0, n = removed.getLength(); i < n; i++) {
+                BranchHandler handler = removed.get(i);
+
+                if (handler != null) {
+                    handler.release();
+                }
+            }
+
+            // Update our data structures
+            clearAndDecrementPaths(expandedPaths, path, index, count);
+            clearAndDecrementPaths(selectedPaths, path, index, count);
+            clearAndDecrementPaths(disabledPaths, path, index, count);
+            clearAndDecrementPaths(checkedPaths, path, index, count);
+
+            // Notify listeners
+            treeViewNodeListeners.nodesRemoved(TreeView.this, getPath(), index,
+                (items == null) ? -1 : items.getLength());
+        }
+
+        public void itemUpdated(List<Object> list, int index, Object previousItem) {
+            Sequence<Integer> path = getPath();
+
+            if (list.get(index) != previousItem) {
+                // Release child handler
+                BranchHandler handler = update(index, null);
+
+                if (handler != null) {
+                    handler.release();
+                }
+
+                // Update our data structures
+                clearPaths(expandedPaths, path, index);
+                clearPaths(selectedPaths, path, index);
+                clearPaths(disabledPaths, path, index);
+                clearPaths(checkedPaths, path, index);
+            }
+
+            // Notify listeners
+            treeViewNodeListeners.nodeUpdated(TreeView.this, path, index);
+        }
+
+        public void comparatorChanged(List<Object> list,
+            Comparator<Object> previousComparator) {
+            if (list.getComparator() != null) {
+                Sequence<Integer> path = getPath();
+
+                // Release all child handlers. This is safe because of the
+                // calls to clearPaths(). Failure to do this would result in
+                // the indices of our child handlers not matching those of the
+                // backing data structures, which would yield very hard to find
+                // bugs
+                for (int i = 0, n = getLength(); i < n; i++) {
+                    BranchHandler handler = update(i, null);
+
+                    if (handler != null) {
+                        handler.release();
+                    }
+                }
+
+                // Update our data structures
+                clearPaths(expandedPaths, path);
+                clearPaths(selectedPaths, path);
+                clearPaths(disabledPaths, path);
+                clearPaths(checkedPaths, path);
+
+                // Notify listeners
+                treeViewNodeListeners.nodesSorted(TreeView.this, path);
+            }
+        }
+
+        /**
+         * Updates the paths within the specified sequence in response to a tree
+         * data path insertion.  For instance, if <tt>paths</tt> is
+         * <tt>[[3, 0], [5, 0]]</tt>, <tt>basePath</tt> is <tt>[]</tt>, and
+         * <tt>index</tt> is <tt>4</tt>, then <tt>paths</tt> will be updated to
+         * <tt>[[3, 0], [6, 0]]</tt>. No events are fired.
+         *
+         * @param paths
+         * Sequence of paths guaranteed to be sorted by "row order".
+         *
+         * @param basePath
+         * The path to the parent of the inserted item.
+         *
+         * @param index
+         * The index of the inserted item within its parent.
+         */
+        private void incrementPaths(Sequence<Sequence<Integer>> paths,
+            Sequence<Integer> basePath, int index) {
+            // Calculate the child's path
+            Sequence<Integer> childPath = new ArrayList<Integer>(basePath);
+            childPath.add(index);
+
+            // Find the child path's place in our sorted paths sequence
+            int i = Sequence.Search.binarySearch(paths, childPath, PATH_COMPARATOR);
+            if (i < 0) {
+                i = -(i + 1);
+            }
+
+            // Update all affected paths by incrementing the appropriate path element
+            for (int depth = basePath.getLength(), n = paths.getLength(); i < n; i++) {
+                Sequence<Integer> affectedPath = paths.get(i);
+
+                if (!Sequence.Tree.isDescendant(basePath, affectedPath)) {
+                    // All paths from here forward are guaranteed to be unaffected
+                    break;
+                }
+
+                affectedPath.update(depth, affectedPath.get(depth) + 1);
+            }
+        }
+
+        /**
+         * Updates the paths within the specified sequence in response to items
+         * having been removed from the base path. For instance, if
+         * <tt>paths</tt> is <tt>[[3, 0], [3, 1], [6, 0]]</tt>,
+         * <tt>basePath</tt> is <tt>[]</tt>, <tt>index</tt> is <tt>3</tt>, and
+         * <tt>count</tt> is <tt>2</tt>, then <tt>paths</tt> will be updated to
+         * <tt>[[4, 0]]</tt>. No events are fired.
+         *
+         * @param paths
+         * Sequence of paths guaranteed to be sorted by "row order".
+         *
+         * @param basePath
+         * The path to the parent of the removed items.
+         *
+         * @param index
+         * The index of the first removed item within the base.
+         *
+         * @param count
+         * The number of items removed.
+         */
+        private void clearAndDecrementPaths(Sequence<Sequence<Integer>> paths,
+            Sequence<Integer> basePath, int index, int count) {
+            int depth = basePath.getLength();
+
+            // Find the index of the first path to clear (inclusive)
+            Sequence<Integer> testPath = new ArrayList<Integer>(basePath);
+            testPath.add(index);
+
+            int start = Sequence.Search.binarySearch(paths, testPath, PATH_COMPARATOR);
+            if (start < 0) {
+                start = -(start + 1);
+            }
+
+            // Find the index of the last path to clear (exclusive)
+            testPath.update(depth, index + count);
+
+            int end = Sequence.Search.binarySearch(paths, testPath, PATH_COMPARATOR);
+            if (end < 0) {
+                end = -(end + 1);
+            }
+
+            // Clear affected paths
+            if (end > start) {
+                paths.remove(start, end - start);
+            }
+
+            // Decrement paths as necessary
+            for (int i = start, n = paths.getLength(); i < n; i++) {
+                Sequence<Integer> affectedPath = paths.get(i);
+
+                if (!Sequence.Tree.isDescendant(basePath, affectedPath)) {
+                    // All paths from here forward are guaranteed to be unaffected
+                    break;
+                }
+
+                affectedPath.update(depth, affectedPath.get(depth) - count);
+            }
+        }
+
+        /**
+         * Removes affected paths from within the specified sequence in response
+         * to an item having been updated in the base path.  For instance, if
+         * <tt>paths</tt> is <tt>[[3], [3, 0], [3, 1], [5, 0]]</tt>,
+         * <tt>basePath</tt> is <tt>[3]</tt>, and <tt>index</tt> is <tt>0</tt>,
+         * then <tt>paths</tt> will be updated to
+         * <tt>[[3], [3, 1], [5, 0]]</tt>. No events are fired.
+         *
+         * @param paths
+         * Sequence of paths guaranteed to be sorted by "row order".
+         *
+         * @param basePath
+         * The path to the parent of the updated item.
+         *
+         * @param index
+         * The index of the updated item within its parent.
+         */
+        private void clearPaths(Sequence<Sequence<Integer>> paths,
+            Sequence<Integer> basePath, int index) {
+            // Calculate the child's path
+            Sequence<Integer> childPath = new ArrayList<Integer>(basePath);
+            childPath.add(index);
+
+            // Find the child path's place in our sorted paths sequence
+            int clearIndex = Sequence.Search.binarySearch(paths, childPath, PATH_COMPARATOR);
+            if (clearIndex < 0) {
+                clearIndex = -(clearIndex + 1);
+            }
+
+            // Remove the child and all descendants from the paths list
+            for (int i = clearIndex, n = paths.getLength(); i < n; i++) {
+                Sequence<Integer> affectedPath = paths.get(clearIndex);
+
+                if (!Sequence.Tree.isDescendant(childPath, affectedPath)) {
+                    break;
+                }
+
+                paths.remove(clearIndex, 1);
+            }
+        }
+
+        /**
+         * Removes affected paths from within the specified sequence in response
+         * to a base path having been sorted.  For instance, if <tt>paths</tt>
+         * is <tt>[[3], [3, 0], [3, 1], [5, 0]]</tt> and <tt>basePath</tt> is
+         * <tt>[3]</tt>, then <tt>paths</tt> will be updated to
+         * <tt>[[3], [5, 0]]</tt>. No events are fired.
+         *
+         * @param paths
+         * Sequence of paths guaranteed to be sorted by "row order".
+         *
+         * @param basePath
+         * The path whose children were sorted.
+         */
+        private void clearPaths(Sequence<Sequence<Integer>> paths, Sequence<Integer> basePath) {
+            // Find first descendant in paths list, if it exists
+            int index = Sequence.Search.binarySearch(paths, basePath, PATH_COMPARATOR);
+            index = (index < 0 ? -(index + 1) : index + 1);
+
+            // Remove all descendants from the paths list
+            for (int i = index, n = paths.getLength(); i < n; i++) {
+                Sequence<Integer> affectedPath = paths.get(index);
+
+                if (!Sequence.Tree.isDescendant(basePath, affectedPath)) {
+                    break;
+                }
+
+                paths.remove(index, 1);
+            }
+        }
+    }
+
+    // Core data model
+    private List<?> treeData = null;
+
+    // Ancillary data models
+    private ArrayList<Sequence<Integer>> expandedPaths =
+        new ArrayList<Sequence<Integer>>(PATH_COMPARATOR);
+    private ArrayList<Sequence<Integer>> selectedPaths =
+        new ArrayList<Sequence<Integer>>(PATH_COMPARATOR);
+    private ArrayList<Sequence<Integer>> disabledPaths =
+        new ArrayList<Sequence<Integer>>(PATH_COMPARATOR);
+    private ArrayList<Sequence<Integer>> checkedPaths =
+        new ArrayList<Sequence<Integer>>(PATH_COMPARATOR);
+
+    // Properties
+    private SelectMode selectMode = SelectMode.SINGLE;
+    private boolean checkmarksEnabled = false;
+    private boolean showMixedCheckmarkState = false;
+
+    // Handlers
+    private BranchHandler rootBranchHandler;
+
+    // Renderer & editor
+    private NodeRenderer nodeRenderer = DEFAULT_NODE_RENDERER;
+    private NodeEditor nodeEditor = null;
+
+    // Listener lists
+    private TreeViewListenerList treeViewListeners = new TreeViewListenerList();
+    private TreeViewBranchListenerList treeViewBranchListeners =
+        new TreeViewBranchListenerList();
+    private TreeViewNodeListenerList treeViewNodeListeners =
+        new TreeViewNodeListenerList();
+    private TreeViewNodeStateListenerList treeViewNodeStateListeners =
+        new TreeViewNodeStateListenerList();
+    private TreeViewSelectionListenerList treeViewSelectionListeners =
+        new TreeViewSelectionListenerList();
+
+    private static final NodeRenderer DEFAULT_NODE_RENDERER = new TreeViewNodeRenderer();
+
+    private static final Comparator<Sequence<Integer>> PATH_COMPARATOR =
+        new PathComparator();
+
+    /**
+     * Creates a new <tt>TreeView</tt> with empty tree data.
+     */
+    public TreeView() {
+        this(new ArrayList<Object>());
+    }
+
+    /**
+     * Creates a new <tt>TreeView</tt> with the specified tree data.
+     *
+     * @param treeData
+     * Default data set to be used with the tree. This list represents the root
+     * set of items displayed by the tree and will never itself be painted.
+     * Sub-items that also implement the <tt>List</tt> interface are considered
+     * branches; other items are considered leaves.
+     *
+     * @see #setTreeData(List)
+     */
+    public TreeView(List<?> treeData) {
+        setTreeData(treeData);
+        installSkin(TreeView.class);
+    }
+
+    /**
+     * Sets the skin, replacing any previous skin. This ensures that the skin
+     * being set implements the {@link TreeView.Skin} interface.
+     *
+     * @param skin
+     * The new skin.
+     */
+    @Override
+    protected void setSkin(pivot.wtk.Skin skin) {
+        if (!(skin instanceof TreeView.Skin)) {
+            throw new IllegalArgumentException("Skin class must implement "
+                + TreeView.Skin.class.getName());
+        }
+
+        super.setSkin(skin);
+    }
+
+    /**
+     * Returns the tree view's data model. This list represents the root
+     * set of items displayed by the tree and will never itself be painted.
+     * Sub-items that also implement the <tt>List</tt> interface are considered
+     * branches; other items are considered leaves.
+     * <p>
+     * For instance, a tree view that displays a single root branch would be
+     * backed by list with one child (also a list).
+     *
+     * @return
+     * The tree view's data model.
+     */
+    public List<?> getTreeData() {
+        return treeData;
+    }
+
+    /**
+     * Sets the tree data. Note that it is the responsibility of the
+     * caller to ensure that the current tree node renderer is capable of
+     * displaying the contents of the tree structure. By default, an instance
+     * of {@link TreeViewNodeRenderer} is used.
+     * <p>
+     * When the tree data is changed, the state of all nodes (expansion,
+     * selection, disabled, and checked) will be cleared since the nodes
+     * themselves are being replaced. Note that corresponding events will
+     * <b>not</b> be fired, since these actions are implied by the
+     * {@link TreeViewListener#treeDataChanged(TreeView,List) treeDataChanged}
+     * event.
+     *
+     * @param treeData
+     * The data to be presented by the tree.
+     */
+    public void setTreeData(List<?> treeData) {
+        if (treeData == null) {
+            throw new IllegalArgumentException("treeData is null.");
+        }
+
+        List<?> previousTreeData = this.treeData;
+
+        if (previousTreeData != treeData) {
+            if (previousTreeData != null) {
+                // Reset our data models
+                expandedPaths.clear();
+                selectedPaths.clear();
+                disabledPaths.clear();
+                checkedPaths.clear();
+
+                // Release our existing branch handlers
+                rootBranchHandler.release();
+            }
+
+            // Update our root branch handler
+            rootBranchHandler = new BranchHandler(null, treeData);
+
+            // Update the tree data
+            this.treeData = treeData;
+
+            // Notify listeners
+            treeViewListeners.treeDataChanged(this, previousTreeData);
+        }
+    }
+
+    /**
+     * Gets the tree view's node renderer, which is responsible for the
+     * appearance of the node data. As such, note that there is an implied
+     * coordination between the node renderer and the data model. The default
+     * node renderer used is an instance of <tt>TreeViewNodeRenderer</tt>.
+     *
+     * @return
+     * The current node renderer.
+     *
+     * @see TreeViewNodeRenderer
+     */
+    public NodeRenderer getNodeRenderer() {
+        return nodeRenderer;
+    }
+
+    /**
+     * Sets the tree view's node renderer, which is responsible for the
+     * appearance of the node data.
+     *
+     * @param nodeRenderer
+     * The new node renderer.
+     */
+    public void setNodeRenderer(NodeRenderer nodeRenderer) {
+        if (nodeRenderer == null) {
+            throw new IllegalArgumentException("nodeRenderer is null.");
+        }
+
+        NodeRenderer previousNodeRenderer = this.nodeRenderer;
+
+        if (previousNodeRenderer != nodeRenderer) {
+            this.nodeRenderer = nodeRenderer;
+
+            treeViewListeners.nodeRendererChanged(this, previousNodeRenderer);
+        }
+    }
+
+    /**
+     * Returns the editor used to edit nodes in this tree.
+     *
+     * @return
+     * The node editor, or <tt>null</tt> if no editor is installed.
+     */
+    public NodeEditor getNodeEditor() {
+        return nodeEditor;
+    }
+
+    /**
+     * Sets the editor used to edit nodes in this tree.
+     *
+     * @param nodeEditor
+     * The node editor for the tree.
+     */
+    public void setNodeEditor(NodeEditor nodeEditor) {
+        NodeEditor previousNodeEditor = this.nodeEditor;
+
+        if (previousNodeEditor != nodeEditor) {
+            this.nodeEditor = nodeEditor;
+            treeViewListeners.nodeEditorChanged(this, previousNodeEditor);
+        }
+    }
+
+    /**
+     * Returns the current selection mode.
+     *
+     * @return
+     * The current selection mode.
+     */
+    public SelectMode getSelectMode() {
+        return selectMode;
+    }
+
+    /**
+     * Sets the selection mode. Clears the selection if the mode has changed.
+     * Note that if the selection is cleared, selection listeners will not
+     * be notified, as the clearing of the selection is implied by the
+     * {@link TreeViewListener#selectModeChanged(TreeView,TreeView.SelectMode)
+     * selectModeChanged} event.
+     *
+     * @param selectMode
+     * The new selection mode.
+     *
+     * @see
+     * TreeViewListener
+     *
+     * @see
+     * TreeViewSelectionListener
+     */
+    public void setSelectMode(SelectMode selectMode) {
+        if (selectMode == null) {
+            throw new IllegalArgumentException("selectMode is null");
+        }
+
+        SelectMode previousSelectMode = this.selectMode;
+
+        if (selectMode != previousSelectMode) {
+            // Clear any current selection
+            selectedPaths.clear();
+
+            // Update the selection mode
+            this.selectMode = selectMode;
+
+            // Fire select mode change event
+            treeViewListeners.selectModeChanged(this, previousSelectMode);
+        }
+    }
+
+    /**
+     * Sets the selection mode.
+     *
+     * @param selectMode
+     * The new selection mode.
+     *
+     * @see
+     * #setSelectMode(SelectMode)
+     */
+    public final void setSelectMode(String selectMode) {
+        if (selectMode == null) {
+            throw new IllegalArgumentException("selectMode is null.");
+        }
+
+        setSelectMode(SelectMode.decode(selectMode));
+    }
+
+    /**
+     *
+     */
+    public Sequence<Sequence<Integer>> getSelectedPaths() {
+        int count = selectedPaths.getLength();
+
+        Sequence<Sequence<Integer>> selectedPaths = new ArrayList<Sequence<Integer>>(count);
+
+        // Deep copy the selected paths into a new list
+        for (int i = 0; i < count; i++) {
+            selectedPaths.add(new ArrayList<Integer>(this.selectedPaths.get(i)));
+        }
+
+        return selectedPaths;
+    }
+
+    /**
+     *
+     *
+     * @throws IllegalStateException
+     * If selection has been disabled (select mode <tt>NONE</tt>).
+     */
+    public void setSelectedPaths(Sequence<Sequence<Integer>> selectedPaths) {
+        if (selectedPaths == null) {
+            throw new IllegalArgumentException("selectedPaths is null.");
+        }
+
+        if (selectMode == SelectMode.NONE) {
+            throw new IllegalStateException("Selection is not enabled.");
+        }
+
+        if (selectMode == SelectMode.SINGLE
+            && selectedPaths.getLength() > 1) {
+            throw new IllegalArgumentException("Selection length is greater than 1.");
+        }
+
+        Sequence<Sequence<Integer>> previousSelectedPaths = this.selectedPaths;
+
+        if (selectedPaths != previousSelectedPaths) {
+            this.selectedPaths = new ArrayList<Sequence<Integer>>(PATH_COMPARATOR);
+
+            for (int i = 0, n = selectedPaths.getLength(); i < n; i++) {
+                Sequence<Integer> path = selectedPaths.get(i);
+
+                // Monitor the path's parent
+                monitorBranch(new ArrayList<Integer>(path, 0, path.getLength() - 1));
+
+                // Update the selection
+                this.selectedPaths.add(new ArrayList<Integer>(path));
+            }
+
+            // Notify listeners
+            treeViewSelectionListeners.selectedPathsChanged(this, previousSelectedPaths);
+        }
+    }
+
+    /**
+     * Returns the first selected path, as it would appear in a fully expanded
+     * tree.
+     *
+     * @return
+     * The first selected path, or <tt>null</tt> if nothing is selected.
+     */
+    public Sequence<Integer> getFirstSelectedPath() {
+        Sequence<Integer> selectedPath = null;
+
+        if (selectedPaths.getLength() > 0) {
+            selectedPath = new ArrayList<Integer>(selectedPaths.get(0));
+        }
+
+        return selectedPath;
+    }
+
+    /**
+     * Returns the last selected path, as it would appear in a fully expanded
+     * tree.
+     *
+     * @return
+     * The last selected path, or <tt>null</tt> if nothing is selected.
+     */
+    public Sequence<Integer> getLastSelectedPath() {
+        Sequence<Integer> selectedPath = null;
+
+        if (selectedPaths.getLength() > 0) {
+            selectedPath = new ArrayList<Integer>
+                (selectedPaths.get(selectedPaths.getLength() - 1));
+        }
+
+        return selectedPath;
+    }
+
+    /**
+     *
+     *
+     * @return
+     * The selected path, or <tt>null</tt> if nothing is selected.
+     *
+     * @throws IllegalStateException
+     * If the tree view is not in single-select mode.
+     */
+    public Sequence<Integer> getSelectedPath() {
+        if (selectMode != SelectMode.SINGLE) {
+            throw new IllegalStateException("Tree view is not in single-select mode.");
+        }
+
+        Sequence<Integer> selectedPath = null;
+
+        if (selectedPaths.getLength() > 0) {
+            selectedPath = new ArrayList<Integer>(selectedPaths.get(0));
+        }
+
+        return selectedPath;
+    }
+
+    /**
+     *
+     */
+    public void setSelectedPath(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        Sequence<Sequence<Integer>> selectedPaths = new ArrayList<Sequence<Integer>>(1);
+        selectedPaths.add(new ArrayList<Integer>(path));
+
+        setSelectedPaths(selectedPaths);
+    }
+
+    /**
+     *
+     *
+     * @return
+     * The selected object, or <tt>null</tt> if nothing is selected. Note that
+     * technically, the selected path could be backed by a <tt>null</tt> data
+     * value. If the caller wishes to distinguish between these cases, they can
+     * use <tt>getSelectedPath()</tt> instead.
+     */
+    public Object getSelectedNode() {
+        Sequence<Integer> path = getSelectedPath();
+        Object node = null;
+
+        if (path != null) {
+            node = Sequence.Tree.get(treeData, path);
+        }
+
+        return node;
+    }
+
+    /**
+     *
+     *
+     * @throws IllegalStateException
+     * If multi-select is not enabled.
+     */
+    public void addSelectedPath(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        if (selectMode != SelectMode.MULTI) {
+            throw new IllegalStateException("Tree view is not in multi-select mode.");
+        }
+
+        if (selectedPaths.indexOf(path) < 0) {
+            // Monitor the path's parent
+            monitorBranch(new ArrayList<Integer>(path, 0, path.getLength() - 1));
+
+            // Update the selection
+            selectedPaths.add(new ArrayList<Integer>(path));
+
+            // Notify listeners
+            treeViewSelectionListeners.selectedPathAdded(this, path);
+        }
+    }
+
+    /**
+     *
+     *
+     * @throws IllegalStateException
+     * If multi-select is not enabled.
+     */
+    public void removeSelectedPath(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        if (selectMode != SelectMode.MULTI) {
+            throw new IllegalStateException("Tree view is not in multi-select mode.");
+        }
+
+        int index = selectedPaths.indexOf(path);
+
+        if (index >= 0) {
+            // Update the selection
+            selectedPaths.remove(index, 1);
+
+            // Notify listeners
+            treeViewSelectionListeners.selectedPathRemoved(this, path);
+        }
+    }
+
+    /**
+     *
+     */
+    public void clearSelection() {
+        if (selectedPaths.getLength() > 0) {
+            Sequence<Sequence<Integer>> previousSelectedPaths = selectedPaths;
+
+            // Update the selection
+            selectedPaths = new ArrayList<Sequence<Integer>>(PATH_COMPARATOR);
+
+            // Notify listeners
+            treeViewSelectionListeners.selectedPathsChanged(this, previousSelectedPaths);
+        }
+    }
+
+    /**
+     *
+     */
+    public boolean isNodeSelected(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        return (selectedPaths.indexOf(path) >= 0);
+    }
+
+    /**
+     * Returns the disabled state of a given node.
+     *
+     * @param path
+     * The path to the node whose disabled state is to be tested
+     *
+     * @return
+     * <tt>true</tt> if the node is disabled; <tt>false</tt>,
+     * otherwise
+     */
+    public boolean isNodeDisabled(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        return (disabledPaths.indexOf(path) >= 0);
+    }
+
+    /**
+     * Sets the disabled state of a node. A disabled node is not interactive to
+     * the user. Note, however, that disabled nodes may still be expanded,
+     * selected, and checked <i>programatically</i>. Disabling a node does
+     * <b>not</b> disable its children.
+     *
+     * @param path
+     * The path to the node.
+     *
+     * @param disabled
+     * <tt>true</tt> to disable the node; <tt>false</tt> to enable it.
+     */
+    public void setNodeDisabled(Sequence<Integer> path, boolean disabled) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        int index = disabledPaths.indexOf(path);
+
+        if ((index < 0 && disabled)
+            || (index >= 0 && !disabled)) {
+            if (disabled) {
+                // Monitor the path's parent
+                monitorBranch(new ArrayList<Integer>(path, 0, path.getLength() - 1));
+
+                // Update the disabled paths
+                disabledPaths.add(new ArrayList<Integer>(path));
+            } else {
+                // Update the disabled paths
+                disabledPaths.remove(index, 1);
+            }
+
+            // Notify listeners
+            treeViewNodeStateListeners.nodeDisabledChanged(this, path);
+        }
+    }
+
+    /**
+     *
+     */
+    public Sequence<Sequence<Integer>> getDisabledPaths() {
+        int count = disabledPaths.getLength();
+
+        Sequence<Sequence<Integer>> disabledPaths = new ArrayList<Sequence<Integer>>(count);
+
+        // Deep copy the disabled paths into a new list
+        for (int i = 0; i < count; i++) {
+            disabledPaths.add(new ArrayList<Integer>(this.disabledPaths.get(i)));
+        }
+
+        return disabledPaths;
+    }
+
+    /**
+     *
+     */
+    public boolean getCheckmarksEnabled() {
+        return checkmarksEnabled;
+    }
+
+    /**
+     * Enables or disables checkmarks. If checkmarks are being disabled, all
+     * checked nodes will be automatically unchecked. Note that the
+     * corresponding event will <b>not</b> be fired, since the clearing of
+     * existing checkmarks is implied by the
+     * {@link TreeViewListener#checkmarksEnabledChanged(TreeView)
+     * checkmarksEnabledChanged} event.
+     *
+     * @param checkmarksEnabled
+     * <tt>true</tt> to enable checkmarks; <tt>false</tt> to disable them.
+     */
+    public void setCheckmarksEnabled(boolean checkmarksEnabled) {
+        if (this.checkmarksEnabled != checkmarksEnabled) {
+            // Clear any current check state
+            checkedPaths.clear();
+
+            // Update the checkmark mode
+            this.checkmarksEnabled = checkmarksEnabled;
+
+            // Fire checkmarks enabled change event
+            treeViewListeners.checkmarksEnabledChanged(this);
+        }
+    }
+
+    /**
+     * Tells whether or not the mixed check state will be reported by this
+     * tree view. This state is a derived state meaning "the node is not
+     * checked, but one or more of its descendants are." When this state is
+     * configured to not be shown, such nodes will simply be reported as
+     * unchecked.
+     *
+     * @return
+     * <tt>true</tt> if the tree view will report so-called mixed nodes as
+     * mixed; <tt>false</tt> if it will report them as unchecked.
+     *
+     * @see
+     * NodeCheckState#MIXED
+     */
+    public boolean getShowMixedCheckmarkState() {
+        return showMixedCheckmarkState;
+    }
+
+    /**
+     * Sets whether or not the "mixed" check state will be reported by this
+     * tree view. This state is a derived state meaning "the node is not
+     * checked, but one or more of its descendants are." When this state is
+     * configured to not be shown, such nodes will simply be reported as
+     * unchecked.
+     * <p>
+     * Changing this flag may result in some nodes changing their reported
+     * check state. Note that the corresponding <tt>nodeCheckStateChanged</tt>
+     * events will <b>not</b> be fired, since the possibility of such a change
+     * in check state is implied by the
+     * {@link TreeViewListener#showMixedCheckmarkStateChanged(TreeView)
+     * showMixedCheckmarkStateChanged} event.
+     *
+     * @param showMixedCheckmarkState
+     * <tt>true</tt> to show the derived mixed state; <tt>false</tt> to report
+     * so-called "mixed" nodes as unchecked.
+     *
+     * @see
+     * NodeCheckState#MIXED
+     */
+    public void setShowMixedCheckmarkState(boolean showMixedCheckmarkState) {
+        if (this.showMixedCheckmarkState != showMixedCheckmarkState) {
+            // Update the flag
+            this.showMixedCheckmarkState = showMixedCheckmarkState;
+
+            // Notify listeners
+            treeViewListeners.showMixedCheckmarkStateChanged(this);
+        }
+    }
+
+    /**
+     * Tells whether or not the node at the specified path is checked. If
+     * checkmarks are not enabled, this is guaranteed to be <tt>false</tt>. So
+     * called mixed nodes will always be reported as unchecked in this method.
+     *
+     * @param path
+     * The path to the node.
+     *
+     * @return
+     * <tt>true</tt> if the node is explicitly checked; <tt>false</tt> otherwise.
+     *
+     * @see
+     * #getCheckmarksEnabled()
+     */
+    public boolean isNodeChecked(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        return (checkedPaths.indexOf(path) >= 0);
+    }
+
+    /**
+     * Returns the checkmark state of the node at the specified path. If
+     * checkmarks are not enabled, this is guaranteed to be <tt>UNCHECKED</tt>.
+     * <p>
+     * Note that the <tt>MIXED</tt> check state (meaning "the node is not
+     * checked, but one or more of its descendants are") is only reported when
+     * the tree view is configured as such. Otherwise, such nodes will be
+     * reported as <tt>UNCHECKED</tt>.
+     *
+     * @param path
+     * The path to the node.
+     *
+     * @return
+     * The checkmark state of the specified node.
+     *
+     * @see
+     * #getCheckmarksEnabled()
+     *
+     * @see
+     * #setShowMixedCheckmarkState(boolean)
+     */
+    public NodeCheckState getNodeCheckState(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        NodeCheckState checkState = NodeCheckState.UNCHECKED;
+
+        if (checkmarksEnabled) {
+            int index = Sequence.Search.binarySearch(checkedPaths, path, PATH_COMPARATOR);
+
+            if (index >= 0) {
+                checkState = NodeCheckState.CHECKED;
+            } else if (showMixedCheckmarkState) {
+                // Translate to the insertion index
+                index = -(index + 1);
+
+                if (index < checkedPaths.getLength()) {
+                    Sequence<Integer> nextCheckedPath = checkedPaths.get(index);
+
+                    if (Sequence.Tree.isDescendant(path, nextCheckedPath)) {
+                        checkState = NodeCheckState.MIXED;
+                    }
+                }
+            }
+        }
+
+        return checkState;
+    }
+
+    /**
+     * Sets the check state of the node at the specified path. If the node
+     * already has the specified check state, nothing happens.
+     * <p>
+     * Note that it is impossible to set the check state of a node to
+     * <tt>MIXED</tt>. This is because the mixed check state is a derived state
+     * meaning "the node is not checked, but one or more of its descendants
+     * are."
+     *
+     * @param path
+     * The path to the node.
+     *
+     * @param checked
+     * <tt>true</tt> to check the node; <tt>false</tt> to uncheck it.
+     *
+     * @throws IllegalStateException
+     * If checkmarks are not enabled (see {@link #getCheckmarksEnabled()}).
+     *
+     * @see
+     * NodeCheckState#MIXED
+     */
+    public void setNodeChecked(Sequence<Integer> path, boolean checked) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        if (!checkmarksEnabled) {
+            throw new IllegalStateException("Checkmarks are not enabled.");
+        }
+
+        int index = checkedPaths.indexOf(path);
+
+        if ((index < 0 && checked)
+            || (index >= 0 && !checked)) {
+            NodeCheckState previousCheckState = getNodeCheckState(path);
+
+            Sequence<NodeCheckState> ancestorCheckStates = null;
+
+            if (showMixedCheckmarkState) {
+                // Record the check states of our ancestors before we change
+                // anything so we know which events to fire after we're done
+                ancestorCheckStates = new ArrayList<NodeCheckState>(path.getLength() - 1);
+
+                Sequence<Integer> ancestorPath = new ArrayList<Integer>
+                    (path, 0, path.getLength() - 1);
+
+                for (int i = ancestorPath.getLength() - 1; i >= 0; i--) {
+                    ancestorCheckStates.insert(getNodeCheckState(ancestorPath), 0);
+
+                    ancestorPath.remove(i, 1);
+                }
+            }
+
+            if (checked) {
+                // Monitor the path's parent
+                monitorBranch(new ArrayList<Integer>(path, 0, path.getLength() - 1));
+
+                // Update the checked paths
+                checkedPaths.add(new ArrayList<Integer>(path));
+            } else {
+                // Update the checked paths
+                checkedPaths.remove(index, 1);
+            }
+
+            // Notify listeners
+            treeViewNodeStateListeners.nodeCheckStateChanged(this, path, previousCheckState);
+
+            if (showMixedCheckmarkState) {
+                // Notify listeners of any changes to our ancestors' check states
+                Sequence<Integer> ancestorPath = new ArrayList<Integer>
+                    (path, 0, path.getLength() - 1);
+
+                for (int i = ancestorPath.getLength() - 1; i >= 0; i--) {
+                    NodeCheckState ancestorPreviousCheckState = ancestorCheckStates.get(i);
+                    NodeCheckState ancestorCheckState = getNodeCheckState(ancestorPath);
+
+                    if (ancestorCheckState != ancestorPreviousCheckState) {
+                        treeViewNodeStateListeners.nodeCheckStateChanged
+                            (this, ancestorPath, ancestorPreviousCheckState);
+                    }
+
+                    ancestorPath.remove(i, 1);
+                }
+            }
+        }
+    }
+
+    /**
+     * Gets the sequence of node paths that are checked. If checkmarks are not
+     * enabled (see {@link #getCheckmarksEnabled()}), this is guaranteed to
+     * return an empty sequence.
+     * <p>
+     * Note that if the tree view is configured to show mixed checkmark states
+     * (see {@link #getShowMixedCheckmarkState()}), this will still only return
+     * the nodes that are fully checked.
+     *
+     * @return
+     * The paths to the checked nodes in the tree, guaranteed to be
+     * non-<tt>null</tt>.
+     */
+    public Sequence<Sequence<Integer>> getCheckedPaths() {
+        int count = checkedPaths.getLength();
+
+        Sequence<Sequence<Integer>> checkedPaths = new ArrayList<Sequence<Integer>>(count);
+
+        // Deep copy the checked paths into a new list
+        for (int i = 0; i < count; i++) {
+            checkedPaths.add(new ArrayList<Integer>(this.checkedPaths.get(i)));
+        }
+
+        return checkedPaths;
+    }
+
+    /**
+     * Sets the expansion state of the specified branch. If the branch already
+     * has the specified expansion state, nothing happens.
+     *
+     * @param path
+     * The path to the branch node.
+     *
+     * @param expanded
+     * <tt>true</tt> to expand the branch; <tt>false</tt> to collapse it.
+     */
+    public void setBranchExpanded(Sequence<Integer> path, boolean expanded) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        int index = expandedPaths.indexOf(path);
+
+        if (expanded && index < 0) {
+            // Monitor the branch
+            monitorBranch(path);
+
+            // Update the expanded paths
+            expandedPaths.add(new ArrayList<Integer>(path));
+
+            // Notify listeners
+            treeViewBranchListeners.branchExpanded(this, path);
+        } else if (!expanded && index >= 0) {
+            // Update the expanded paths
+            expandedPaths.remove(index, 1);
+
+            // Notify listeners
+            treeViewBranchListeners.branchCollapsed(this, path);
+        }
+    }
+
+    /**
+     * Tells whether or not the specified branch is expanded.
+     *
+     * @param path
+     * The path to the branch node.
+     *
+     * @return
+     * <tt>true</tt> if the branch is expanded; <tt>false</tt> otherwise.
+     */
+    public boolean isBranchExpanded(Sequence<Integer> path) {
+        if (path == null) {
+            throw new IllegalArgumentException("path is null.");
+        }
+
+        return (expandedPaths.indexOf(path) >= 0);
+    }
+
+    /**
+     * Expands the branch at the specified path. If the branch is already
+     * expanded, nothing happens.
+     *
+     * @param path
+     * The path to the branch node.
+     */
+    public final void expandBranch(Sequence<Integer> path) {
+        setBranchExpanded(path, true);
+    }
+
+    /**
+     * Collapses the branch at the specified path. If the branch is already
+     * collapsed, nothing happens.
+     *
+     * @param path
+     * The path to the branch node.
+     */
+    public final void collapseBranch(Sequence<Integer> path) {
+        setBranchExpanded(path, false);
+    }
+
+    /**
+     * Ensures that this tree view is listening for list events on every branch
+     * node along the specified path.
+     *
+     * @param path
+     * A path leading to a nested branch node.
+     *
+     * @throws IndexOutOfBoundsException
+     * If a path element is out of bounds.
+     *
+     * @throws IllegalArgumentException
+     * If the path contains any leaf nodes.
+     */
+    @SuppressWarnings("unchecked")
+    private void monitorBranch(Sequence<Integer> path) {
+        BranchHandler parent = rootBranchHandler;
+
+        for (int i = 0, n = path.getLength(); i < n; i++) {
+            int index = path.get(i);
+            if (index < 0
+                || index >= parent.getLength()) {
+                throw new IndexOutOfBoundsException
+                    ("Branch path out of bounds: " + path);
+            }
+
+            BranchHandler child = parent.get(index);
+
+            if (child == null) {
+                List<?> parentBranchData = parent.getBranchData();
+                Object childData = parentBranchData.get(index);
+
+                if (!(childData instanceof List)) {
+                    throw new IllegalArgumentException
+                        ("Unexpected leaf in branch path: " + path);
+                }
+
+                child = new BranchHandler(parent, (List<?>)childData);
+                parent.update(index, child);
+            }
+
+            parent = child;
+        }
+    }
+
+    /**
+     * Gets the path to the node found at the specified y-coordinate
+     * (relative to the tree view).
+     *
+     * @param y
+     * The y-coordinate in pixels.
+     *
+     * @return
+     * The path to the node, or <tt>null</tt> if there is no node being
+     * painted at the specified y-coordinate.
+     */
+    public Sequence<Integer> getNodeAt(int y) {
+        TreeView.Skin treeViewSkin = (TreeView.Skin)getSkin();
+        return treeViewSkin.getNodeAt(y);
+    }
+
+    /**
+     * Gets the bounds of the node at the specified path relative to the
+     * tree view. Note that all nodes are left aligned with the tree; to
+     * get the pixel value of a node's indent, use
+     * {@link #getNodeIndent(int)}.
+     *
+     * @param path
+     * The path to the node.
+     *
+     * @return
+     * The bounds, or <tt>null</tt> if the node is not currently visible.
+     */
+    public Bounds getNodeBounds(Sequence<Integer> path) {
+        TreeView.Skin treeViewSkin = (TreeView.Skin)getSkin();
+        return treeViewSkin.getNodeBounds(path);
+    }
+
+    /**
+     * Gets the pixel indent of nodes at the specified depth. Depth is measured
+     * in generations away from the tree view's "root" node, which is
+     * represented by the {@link #getTreeData() tree data}.
+     *
+     * @param depth
+     * The depth, where the first child of the root has depth 1, the child of
+     * that branch has depth 2, etc.
+     *
+     * @return
+     * The indent in pixels.
+     */
+    public int getNodeIndent(int depth) {
+        TreeView.Skin treeViewSkin = (TreeView.Skin)getSkin();
+        return treeViewSkin.getNodeIndent(depth);
+    }
+
+    /**
+     * Gets the <tt>TreeViewListener</tt>s. Developers interested in these
+     * events can register for notification on these events by adding
+     * themselves to the listener list.
+     *
+     * @return
+     * The tree view listeners.
+     */
+    public ListenerList<TreeViewListener> getTreeViewListeners() {
+        return treeViewListeners;
+    }
+
+    /**
+     * Gets the <tt>TreeViewBranchListener</tt>s. Developers interested in
+     * these events can register for notification on these events by adding
+     * themselves to the listener list.
+     *
+     * @return
+     * The tree view branch listeners.
+     */
+    public ListenerList<TreeViewBranchListener> getTreeViewBranchListeners() {
+        return treeViewBranchListeners;
+    }
+
+    /**
+     * Gets the <tt>TreeViewNodeListener</tt>s. Developers interested in these
+     * events can register for notification on these events by adding
+     * themselves to the listener list.
+     *
+     * @return
+     * The tree view node listeners.
+     */
+    public ListenerList<TreeViewNodeListener> getTreeViewNodeListeners() {
+        return treeViewNodeListeners;
+    }
+
+    /**
+     * Gets the <tt>TreeViewNodeStateListener</tt>s. Developers interested in
+     * these events can register for notification on these events by adding
+     * themselves to the listener list.
+     *
+     * @return
+     * The tree view node state listeners.
+     */
+    public ListenerList<TreeViewNodeStateListener> getTreeViewNodeStateListeners() {
+        return treeViewNodeStateListeners;
+    }
+
+    /**
+     * Gets the <tt>TreeViewSelectionListener</tt>s. Developers interested in
+     * these events can register for notification on these events by adding
+     * themselves to the listener list.
+     *
+     * @return
+     * The tree view selection listeners.
+     */
+    public ListenerList<TreeViewSelectionListener> getTreeViewSelectionListeners() {
+        return treeViewSelectionListeners;
+    }
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewBranchListener.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewBranchListener.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewBranchListener.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewBranchListener.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+import pivot.collections.Sequence;
+
+/**
+ * Tree view branch listener interface.
+ *
+ * @author gbrown
+ */
+public interface TreeViewBranchListener {
+    /**
+     * Called when a tree node is expanded. This event can be used to perform
+     * lazy loading of tree node data.
+     *
+     * @param treeView
+     * The source of the event.
+     *
+     * @param path
+     * The path of the node that was shown.
+     */
+    public void branchExpanded(TreeView treeView, Sequence<Integer> path);
+
+    /**
+     * Called when a tree node is collapsed.
+     *
+     * @param treeView
+     * The source of the event.
+     *
+     * @param path
+     * The path of the node that was collapsed.
+     */
+    public void branchCollapsed(TreeView treeView, Sequence<Integer> path);
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewListener.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewListener.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewListener.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewListener.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,72 @@
+/*
+ * 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;
+
+import pivot.collections.List;
+
+/**
+ * Tree view listener interface.
+ *
+ * @author gbrown
+ * @author tvolkert
+ */
+public interface TreeViewListener {
+    /**
+     * Called when a tree view's data has changed.
+     *
+     * @param treeView
+     * @param previousTreeData
+     */
+    public void treeDataChanged(TreeView treeView, List<?> previousTreeData);
+
+    /**
+     * Called when a tree view's node renderer has changed.
+     *
+     * @param treeView
+     * @param previousNodeRenderer
+     */
+    public void nodeRendererChanged(TreeView treeView, TreeView.NodeRenderer previousNodeRenderer);
+
+    /**
+     * Called when a tree view's node editor has changed.
+     *
+     * @param treeView
+     * @param previousNodeEditor
+     */
+    public void nodeEditorChanged(TreeView treeView, TreeView.NodeEditor previousNodeEditor);
+
+    /**
+     * Called when a tree view's select mode has changed.
+     *
+     * @param treeView
+     * @param previousSelectMode
+     */
+    public void selectModeChanged(TreeView treeView, TreeView.SelectMode previousSelectMode);
+
+    /**
+     * Called when a tree view's checkmarks enabled flag has changed.
+     *
+     * @param treeView
+     */
+    public void checkmarksEnabledChanged(TreeView treeView);
+
+    /**
+     * Called when a tree view's "show mixed checkmark state" flag has changed.
+     *
+     * @param treeView
+     */
+    public void showMixedCheckmarkStateChanged(TreeView treeView);
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeListener.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeListener.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeListener.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeListener.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+import pivot.collections.Sequence;
+
+/**
+ * Tree view node listener interface.
+ *
+ * @author gbrown
+ */
+public interface TreeViewNodeListener {
+    /**
+     * Called when a node has been inserted into the tree view.
+     *
+     * @param treeView
+     * @param path
+     * @param index
+     */
+    public void nodeInserted(TreeView treeView, Sequence<Integer> path, int index);
+
+    /**
+     * Called when nodes have been removed from the tree view.
+     *
+     * @param treeView
+     * @param path
+     * @param index
+     * @param count
+     * The number of nodes that were removed, or <tt>-1</tt> if all nodes
+     * were removed.
+     */
+    public void nodesRemoved(TreeView treeView, Sequence<Integer> path, int index, int count);
+
+    /**
+     * Called when a node in the tree view has been updated.
+     *
+     * @param treeView
+     * @param path
+     * @param index
+     */
+    public void nodeUpdated(TreeView treeView, Sequence<Integer> path, int index);
+
+    /**
+     * Called when the nodes in a branch have been sorted.
+     *
+     * @param treeView
+     * @param path
+     */
+    public void nodesSorted(TreeView treeView, Sequence<Integer> path);
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeStateListener.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeStateListener.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeStateListener.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewNodeStateListener.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import pivot.collections.Sequence;
+
+/**
+ * Tree view node state listener interface.
+ *
+ * @author gbrown
+ * @author tvolkert
+ */
+public interface TreeViewNodeStateListener {
+    /**
+     * Called when a node's disabled state has changed.
+     *
+     * @param treeView
+     * @param path
+     */
+    public void nodeDisabledChanged(TreeView treeView, Sequence<Integer> path);
+
+    /**
+     * Called when a node's checked state has changed.
+     *
+     * @param treeView
+     * @param path
+     * @param previousCheckState
+     */
+    public void nodeCheckStateChanged(TreeView treeView, Sequence<Integer> path,
+        TreeView.NodeCheckState previousCheckState);
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewSelectionListener.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewSelectionListener.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewSelectionListener.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/TreeViewSelectionListener.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,50 @@
+/*
+ * 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;
+
+import pivot.collections.Sequence;
+
+/**
+ * Tree view selection listener.
+ *
+ * @author gbrown
+ */
+public interface TreeViewSelectionListener {
+    /**
+     * Called when a selected path has been added to a tree view.
+     *
+     * @param treeView
+     * @param path
+     */
+    public void selectedPathAdded(TreeView treeView, Sequence<Integer> path);
+
+    /**
+     * Called when a selected path has been removed from a tree view.
+     *
+     * @param treeView
+     * @param path
+     */
+    public void selectedPathRemoved(TreeView treeView, Sequence<Integer> path);
+
+    /**
+     * Called when a tree view's selection state has been reset.
+     *
+     * @param treeView
+     * @param previousSelectedPaths
+     */
+    public void selectedPathsChanged(TreeView treeView,
+        Sequence<Sequence<Integer>> previousSelectedPaths);
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/VerticalAlignment.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/VerticalAlignment.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/VerticalAlignment.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/VerticalAlignment.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+/**
+ * Enumeration representing vertical alignment values.
+ *
+ * @author gbrown
+ */
+public enum VerticalAlignment {
+    TOP,
+    BOTTOM,
+    CENTER,
+    JUSTIFY;
+
+    public static VerticalAlignment decode(String value) {
+        return valueOf(value.toUpperCase());
+    }
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Viewport.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Viewport.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Viewport.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Viewport.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,148 @@
+/*
+ * 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;
+
+import pivot.collections.Sequence;
+import pivot.util.ListenerList;
+
+/**
+ * Abstract base class for viewport components. Viewports provide a windowed
+ * view on a component (called the "view") that is too large to fit within a
+ * given area. They are generally scrollable.
+ *
+ * @author tvolkert
+ */
+public abstract class Viewport extends Container {
+    /**
+     * Viewport skin interface. Viewport skins must implement this.
+     *
+     * @author tvolkert
+     */
+    public interface Skin {
+        public Bounds getViewportBounds();
+    }
+
+    private static class ViewportListenerList extends ListenerList<ViewportListener>
+        implements ViewportListener {
+
+        public void scrollTopChanged(Viewport viewport, int previousScrollTop) {
+            for (ViewportListener listener : this) {
+                listener.scrollTopChanged(viewport, previousScrollTop);
+            }
+        }
+
+        public void scrollLeftChanged(Viewport viewport, int previousScrollLeft) {
+            for (ViewportListener listener : this) {
+                listener.scrollLeftChanged(viewport, previousScrollLeft);
+            }
+        }
+
+        public void viewChanged(Viewport viewport, Component previousView) {
+            for (ViewportListener listener : this) {
+                listener.viewChanged(viewport, previousView);
+            }
+        }
+    }
+
+    private int scrollTop = 0;
+    private int scrollLeft = 0;
+    private Component view;
+    private ViewportListenerList viewportListeners = new ViewportListenerList();
+
+    @Override
+    protected void setSkin(pivot.wtk.Skin skin) {
+        if (!(skin instanceof Viewport.Skin)) {
+            throw new IllegalArgumentException("Skin class must implement "
+                + Viewport.Skin.class.getName());
+        }
+
+        super.setSkin(skin);
+    }
+
+    public int getScrollTop() {
+        return scrollTop;
+    }
+
+    public void setScrollTop(int scrollTop) {
+        int previousScrollTop = this.scrollTop;
+
+        if (scrollTop != previousScrollTop) {
+            this.scrollTop = scrollTop;
+            viewportListeners.scrollTopChanged(this, previousScrollTop);
+        }
+    }
+
+    public int getScrollLeft() {
+        return scrollLeft;
+    }
+
+    public void setScrollLeft(int scrollLeft) {
+        int previousScrollLeft = this.scrollLeft;
+
+        if (scrollLeft != previousScrollLeft) {
+            this.scrollLeft = scrollLeft;
+            viewportListeners.scrollLeftChanged(this, previousScrollLeft);
+        }
+    }
+
+    public Component getView() {
+        return view;
+    }
+
+    public void setView(Component view) {
+       Component previousView = this.view;
+
+        if (view != previousView) {
+            // Remove any previous view component
+            this.view = null;
+
+            if (previousView != null) {
+                remove(previousView);
+            }
+
+            // Set the new view component
+            if (view != null) {
+                insert(view, 0);
+            }
+
+            this.view = view;
+
+            viewportListeners.viewChanged(this, previousView);
+        }
+    }
+
+    public Bounds getViewportBounds() {
+        Viewport.Skin viewportSkin = (Viewport.Skin)getSkin();
+        return viewportSkin.getViewportBounds();
+    }
+
+    @Override
+    public Sequence<Component> remove(int index, int count) {
+        for (int i = index, n = index + count; i < n; i++) {
+            Component component = get(i);
+            if (component == view) {
+                throw new UnsupportedOperationException();
+            }
+        }
+
+        // Call the base method to remove the components
+        return super.remove(index, count);
+    }
+
+    public ListenerList<ViewportListener> getViewportListeners() {
+        return viewportListeners;
+    }
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/ViewportListener.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/ViewportListener.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/ViewportListener.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/ViewportListener.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,47 @@
+/*
+ * 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;
+
+/**
+ * Viewport listener interface.
+ *
+ * @author tvolkert
+ */
+public interface ViewportListener {
+    /**
+     * Called when a viewport's scroll top has changed.
+     *
+     * @param scrollPane
+     * @param previousScrollTop
+     */
+    public void scrollTopChanged(Viewport scrollPane, int previousScrollTop);
+
+    /**
+     * Called when a viewport's scroll left has changed.
+     *
+     * @param scrollPane
+     * @param previousScrollLeft
+     */
+    public void scrollLeftChanged(Viewport scrollPane, int previousScrollLeft);
+
+    /**
+     * Called when a viewport's view component has changed.
+     *
+     * @param scrollPane
+     * @param previousView
+     */
+    public void viewChanged(Viewport scrollPane, Component previousView);
+}

Added: incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Visual.java
URL: http://svn.apache.org/viewvc/incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Visual.java?rev=758461&view=auto
==============================================================================
--- incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Visual.java (added)
+++ incubator/pivot/branches/1.1/wtk/src/pivot/wtk/Visual.java Wed Mar 25 23:08:38 2009
@@ -0,0 +1,44 @@
+/*
+ * 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;
+
+import java.awt.Graphics2D;
+
+/**
+ * Interface representing a "visual". A visual is an object that can be drawn
+ * to the screen or other output device.
+ *
+ * @author gbrown
+ */
+public interface Visual {
+    /**
+     * Returns the visual's width.
+     */
+    public int getWidth();
+
+    /**
+     * Returns the visual's height.
+     */
+    public int getHeight();
+
+    /**
+     * Paints the visual.
+     *
+     * @param graphics
+     * The graphics context in which to paint the visual.
+     */
+    public void paint(Graphics2D graphics);
+}