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

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

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewHeaderSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewHeaderSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewHeaderSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewHeaderSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,797 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.geom.GeneralPath;
+import java.awt.geom.Line2D;
+
+import pivot.collections.Sequence;
+import pivot.wtk.Component;
+import pivot.wtk.ComponentMouseListener;
+import pivot.wtk.ComponentMouseButtonListener;
+import pivot.wtk.Cursor;
+import pivot.wtk.Dimensions;
+import pivot.wtk.Display;
+import pivot.wtk.Mouse;
+import pivot.wtk.Point;
+import pivot.wtk.Bounds;
+import pivot.wtk.SortDirection;
+import pivot.wtk.TableView;
+import pivot.wtk.TableViewColumnListener;
+import pivot.wtk.TableViewHeader;
+import pivot.wtk.TableViewHeaderListener;
+import pivot.wtk.Theme;
+import pivot.wtk.media.Image;
+import pivot.wtk.skin.ComponentSkin;
+
+/**
+ * Table view header skin.
+ *
+ * @author gbrown
+ */
+public class TerraTableViewHeaderSkin extends ComponentSkin
+    implements TableViewHeader.Skin, TableViewHeaderListener, TableViewColumnListener {
+    private class SortIndicatorImage extends Image {
+        private SortDirection sortDirection = null;
+
+        public SortIndicatorImage(SortDirection sortDirection) {
+            this.sortDirection = sortDirection;
+        }
+
+        public int getWidth() {
+            return 7;
+        }
+
+        public int getHeight() {
+            return 4;
+        }
+
+        public Dimensions getPreferredSize() {
+            return new Dimensions(getPreferredWidth(-1), getPreferredHeight(-1));
+        }
+
+        public void paint(Graphics2D graphics) {
+            GeneralPath shape = new GeneralPath();
+
+            switch (sortDirection) {
+                case ASCENDING: {
+                    shape.moveTo(0, 3);
+                    shape.lineTo(3, 0);
+                    shape.lineTo(6, 3);
+                    break;
+                }
+
+                case DESCENDING: {
+                    shape.moveTo(0, 0);
+                    shape.lineTo(3, 3);
+                    shape.lineTo(6, 0);
+                    break;
+                }
+            }
+
+            shape.closePath();
+
+            graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+                RenderingHints.VALUE_ANTIALIAS_ON);
+
+            graphics.setStroke(new BasicStroke(0));
+            graphics.setPaint(borderColor);
+
+            graphics.draw(shape);
+            graphics.fill(shape);
+        }
+    }
+
+    private class ResizeHandler
+        implements ComponentMouseListener, ComponentMouseButtonListener {
+        TableView.Column column = null;
+        int headerX = 0;
+        int offset = 0;
+
+        public static final int MINIMUM_COLUMN_WIDTH = 2;
+
+        public ResizeHandler(TableView.Column column, int headerX, int offset) {
+            assert (!column.isRelative()) : "Relative width columns cannot be resized.";
+
+            this.column = column;
+            this.headerX = headerX;
+            this.offset = offset;
+        }
+
+        public boolean mouseMove(Component component, int x, int y) {
+            int columnWidth = Math.max(x - headerX + offset, MINIMUM_COLUMN_WIDTH);
+            column.setWidth(columnWidth, false);
+
+            return false;
+        }
+
+        public void mouseOver(Component component) {
+        }
+
+        public void mouseOut(Component component) {
+        }
+
+        public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+            return false;
+        }
+
+        public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+            Mouse.setCursor(component.getCursor());
+
+            assert (component instanceof Display);
+            component.getComponentMouseListeners().remove(this);
+            component.getComponentMouseButtonListeners().remove(this);
+
+            resizing = false;
+            Mouse.setCursor(getComponent().getCursor());
+
+            return false;
+        }
+
+        public void mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
+        }
+    }
+
+    private Font font;
+    private Color color;
+    private Color disabledColor;
+    private Color backgroundColor;
+    private Color disabledBackgroundColor;
+    private Color borderColor;
+    private Color disabledBorderColor;
+    private boolean headersPressable;
+    private boolean columnsResizable;
+    private boolean includeTrailingVerticalGridLine;
+
+    // Derived colors
+    private Color bevelColor;
+    private Color pressedBevelColor;
+    private Color disabledBevelColor;
+
+    private int pressedHeaderIndex = -1;
+    private boolean resizing = false;
+
+    private static final int SORT_INDICATOR_PADDING = 2;
+    private static final int RESIZE_HANDLE_SIZE = 6;
+
+    protected SortIndicatorImage sortAscendingImage = new SortIndicatorImage(SortDirection.ASCENDING);
+    protected SortIndicatorImage sortDescendingImage = new SortIndicatorImage(SortDirection.DESCENDING);
+
+    public TerraTableViewHeaderSkin() {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+
+        font = theme.getFont();
+        color = theme.getColor(1);
+        disabledColor = theme.getColor(7);
+        backgroundColor = theme.getColor(10);
+        disabledBackgroundColor = theme.getColor(10);
+        borderColor = theme.getColor(7);
+        disabledBorderColor = theme.getColor(7);
+        headersPressable = true;
+        columnsResizable = true;
+        includeTrailingVerticalGridLine = false;
+
+        // Set the derived colors
+        bevelColor = TerraTheme.brighten(backgroundColor);
+        pressedBevelColor = TerraTheme.darken(backgroundColor);
+        disabledBevelColor = disabledBackgroundColor;
+    }
+
+    public void install(Component component) {
+        super.install(component);
+
+        TableViewHeader tableViewHeader = (TableViewHeader)component;
+        tableViewHeader.getTableViewHeaderListeners().add(this);
+
+        TableView tableView = tableViewHeader.getTableView();
+        if (tableView != null) {
+            tableView.getTableViewColumnListeners().add(this);
+        }
+    }
+
+    public void uninstall() {
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+        tableViewHeader.getTableViewHeaderListeners().remove(this);
+
+        TableView tableView = tableViewHeader.getTableView();
+        if (tableView != null) {
+            tableView.getTableViewColumnListeners().remove(this);
+        }
+
+        super.uninstall();
+    }
+
+    public int getPreferredWidth(int height) {
+        int preferredWidth = 0;
+
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+        TableView tableView = tableViewHeader.getTableView();
+
+        if (tableView != null) {
+            TableView.ColumnSequence columns = tableView.getColumns();
+
+            int n = columns.getLength();
+            int gridLineStop = includeTrailingVerticalGridLine ? n : n - 1;
+
+            for (int i = 0; i < n; i++) {
+                TableView.Column column = columns.get(i);
+
+                if (!column.isRelative()) {
+                    preferredWidth += column.getWidth();
+
+                    // Include space for vertical gridlines
+                    if (i < gridLineStop) {
+                        preferredWidth++;
+                    }
+                }
+            }
+        }
+
+        return preferredWidth;
+    }
+
+    public int getPreferredHeight(int width) {
+        int preferredHeight = 0;
+
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+        TableView tableView = tableViewHeader.getTableView();
+
+        if (tableView != null) {
+            TableView.ColumnSequence columns = tableView.getColumns();
+            TableViewHeader.DataRenderer dataRenderer = tableViewHeader.getDataRenderer();
+
+            for (int i = 0, n = columns.getLength(); i < n; i++) {
+                TableView.Column column = columns.get(i);
+                dataRenderer.render(column.getHeaderData(), tableViewHeader, false);
+                preferredHeight = Math.max(preferredHeight, dataRenderer.getPreferredHeight(-1));
+            }
+
+            // Include the bottom border
+            preferredHeight++;
+        }
+
+        return preferredHeight;
+    }
+
+    public Dimensions getPreferredSize() {
+        return new Dimensions(getPreferredWidth(-1), getPreferredHeight(-1));
+    }
+
+    public void layout() {
+        // No-op
+    }
+
+    public void paint(Graphics2D graphics) {
+        int width = getWidth();
+        int height = getHeight();
+
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+
+        Color backgroundColor = null;
+        Color bevelColor = null;
+        Color borderColor = null;
+
+        if (tableViewHeader.isEnabled()) {
+            backgroundColor = this.backgroundColor;
+            bevelColor = this.bevelColor;
+            borderColor = this.borderColor;
+        } else {
+            backgroundColor = disabledBackgroundColor;
+            bevelColor = disabledBevelColor;
+            borderColor = disabledBorderColor;
+        }
+
+        // Paint the background
+        graphics.setPaint(backgroundColor);
+        graphics.fillRect(0, 0, width, height);
+
+        // Draw all lines with a 1px solid stroke
+        graphics.setStroke(new BasicStroke());
+
+        // Paint the bevel
+        Line2D bevelLine = new Line2D.Double(0, 0, width, 0);
+        graphics.setPaint(bevelColor);
+        graphics.draw(bevelLine);
+
+        // Paint the border
+        Line2D borderLine = new Line2D.Double(0, height - 1, width, height - 1);
+        graphics.setPaint(borderColor);
+        graphics.draw(borderLine);
+
+        // Paint the content
+        TableView tableView = tableViewHeader.getTableView();
+
+        if (tableView != null) {
+            TableView.ColumnSequence columns = tableView.getColumns();
+            Sequence<Integer> columnWidths =
+                TerraTableViewSkin.getColumnWidths(columns, getWidth());
+
+            TableViewHeader.DataRenderer dataRenderer = tableViewHeader.getDataRenderer();
+
+            int cellX = 0;
+            for (int columnIndex = 0, columnCount = columns.getLength();
+                columnIndex < columnCount; columnIndex++) {
+                TableView.Column column = columns.get(columnIndex);
+                int columnWidth = columnWidths.get(columnIndex);
+
+                // Paint the pressed bevel
+                if (columnIndex == pressedHeaderIndex) {
+                    bevelLine = new Line2D.Double(cellX, 0, cellX + columnWidth, 0);
+                    graphics.setPaint(pressedBevelColor);
+                    graphics.draw(bevelLine);
+                }
+
+                // Paint the header data
+                Object headerData = column.getHeaderData();
+                dataRenderer.render(headerData, tableViewHeader, false);
+                dataRenderer.setSize(columnWidth, height - 1);
+
+                Graphics2D rendererGraphics = (Graphics2D)graphics.create(cellX, 0,
+                    columnWidth, height - 1);
+                dataRenderer.paint(rendererGraphics);
+                rendererGraphics.dispose();
+
+                // Draw the sort image
+                Image sortImage = null;
+                SortDirection sortDirection = column.getSortDirection();
+
+                if (sortDirection != null) {
+                    switch (sortDirection) {
+                        case ASCENDING: {
+                            sortImage = sortAscendingImage;
+                            break;
+                        }
+
+                        case DESCENDING: {
+                            sortImage = sortDescendingImage;
+                            break;
+                        }
+                    }
+                }
+
+                if (sortImage != null) {
+                    int sortImageMargin = sortImage.getWidth()
+                    + SORT_INDICATOR_PADDING * 2;
+
+                    if (columnWidth >= dataRenderer.getPreferredWidth(-1) + sortImageMargin) {
+                        Graphics2D sortImageGraphics = (Graphics2D)graphics.create();
+                        sortImageGraphics.translate(cellX + columnWidth - sortImageMargin,
+                            (height - sortImage.getHeight()) / 2);
+                        sortImage.paint(sortImageGraphics);
+                        sortImageGraphics.dispose();
+                    }
+                }
+
+                // Draw the divider
+                cellX += columnWidth;
+
+                Line2D dividerLine = new Line2D.Double(cellX, 0, cellX, height - 1);
+                graphics.setPaint(borderColor);
+                graphics.draw(dividerLine);
+
+                cellX++;
+            }
+        }
+    }
+
+    public int getHeaderAt(int x) {
+        if (x < 0) {
+            throw new IllegalArgumentException("x is negative");
+        }
+
+        int index = -1;
+
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+        TableView tableView = tableViewHeader.getTableView();
+
+        if (tableView != null) {
+            Sequence<Integer> columnWidths =
+                TerraTableViewSkin.getColumnWidths(tableView.getColumns(), getWidth());
+
+            int i = 0;
+            int n = columnWidths.getLength();
+            int columnX = 0;
+            while (i < n
+                && x > columnX) {
+                columnX += (columnWidths.get(i) + 1);
+                i++;
+            }
+
+            if (x <= columnX) {
+                index = i - 1;
+            }
+        }
+
+        return index;
+    }
+
+    public Bounds getHeaderBounds(int index) {
+        Bounds headerBounds = null;
+
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+        TableView tableView = tableViewHeader.getTableView();
+
+        if (tableView != null) {
+            Sequence<Integer> columnWidths =
+                TerraTableViewSkin.getColumnWidths(tableView.getColumns(), getWidth());
+
+            if (index < 0
+                || index >= columnWidths.getLength()) {
+                throw new IndexOutOfBoundsException();
+            }
+
+            int cellX = 0;
+            for (int i = 0; i < index; i++) {
+                cellX += (columnWidths.get(i) + 1);
+            }
+
+            headerBounds = new Bounds(cellX, 0, columnWidths.get(index), getHeight() - 1);
+        }
+
+        return headerBounds;
+    }
+
+    @Override
+    public boolean isFocusable() {
+        return false;
+    }
+
+    public Font getFont() {
+        return font;
+    }
+
+    public void setFont(Font font) {
+        if (font == null) {
+            throw new IllegalArgumentException("font is null.");
+        }
+
+        this.font = font;
+        invalidateComponent();
+    }
+
+    public final void setFont(String font) {
+        if (font == null) {
+            throw new IllegalArgumentException("font is null.");
+        }
+
+        setFont(Font.decode(font));
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public void setColor(Color color) {
+        if (color == null) {
+            throw new IllegalArgumentException("color is null.");
+        }
+
+        this.color = color;
+        repaintComponent();
+    }
+
+    public final void setColor(String color) {
+        if (color == null) {
+            throw new IllegalArgumentException("color is null.");
+        }
+
+        setColor(decodeColor(color));
+    }
+
+    public Color getDisabledColor() {
+        return disabledColor;
+    }
+
+    public void setDisabledColor(Color disabledColor) {
+        if (disabledColor == null) {
+            throw new IllegalArgumentException("disabledColor is null.");
+        }
+
+        this.disabledColor = disabledColor;
+        repaintComponent();
+    }
+
+    public final void setDisabledColor(String disabledColor) {
+        if (disabledColor == null) {
+            throw new IllegalArgumentException("disabledColor is null.");
+        }
+
+        setDisabledColor(decodeColor(disabledColor));
+    }
+
+    public Color getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    public void setBackgroundColor(Color backgroundColor) {
+        if (backgroundColor == null) {
+            throw new IllegalArgumentException("backgroundColor is null.");
+        }
+
+        this.backgroundColor = backgroundColor;
+        bevelColor = TerraTheme.brighten(backgroundColor);
+        pressedBevelColor = TerraTheme.darken(backgroundColor);
+        repaintComponent();
+    }
+
+    public final void setBackgroundColor(String backgroundColor) {
+        if (backgroundColor == null) {
+            throw new IllegalArgumentException("backgroundColor is null.");
+        }
+
+        setBackgroundColor(decodeColor(backgroundColor));
+    }
+
+    public Color getDisabledBackgroundColor() {
+        return disabledBackgroundColor;
+    }
+
+    public void setDisabledBackgroundColor(Color disabledBackgroundColor) {
+        if (disabledBackgroundColor == null) {
+            throw new IllegalArgumentException("disabledBackgroundColor is null.");
+        }
+
+        this.disabledBackgroundColor = disabledBackgroundColor;
+        disabledBevelColor = disabledBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setDisabledBackgroundColor(String disabledBackgroundColor) {
+        if (disabledBackgroundColor == null) {
+            throw new IllegalArgumentException("disabledBackgroundColor is null.");
+        }
+
+        setDisabledBackgroundColor(decodeColor(disabledBackgroundColor));
+    }
+
+    public Color getBorderColor() {
+        return borderColor;
+    }
+
+    public void setBorderColor(Color borderColor) {
+        if (borderColor == null) {
+            throw new IllegalArgumentException("borderColor is null.");
+        }
+
+        this.borderColor = borderColor;
+        repaintComponent();
+    }
+
+    public final void setBorderColor(String borderColor) {
+        if (borderColor == null) {
+            throw new IllegalArgumentException("borderColor is null.");
+        }
+
+        setBorderColor(decodeColor(borderColor));
+    }
+
+    public Color getDisabledBorderColor() {
+        return disabledBorderColor;
+    }
+
+    public void setDisabledBorderColor(Color disabledBorderColor) {
+        if (disabledBorderColor == null) {
+            throw new IllegalArgumentException("disabledBorderColor is null.");
+        }
+
+        this.disabledBorderColor = disabledBorderColor;
+        repaintComponent();
+    }
+
+    public final void setDisabledBorderColor(String disabledBorderColor) {
+        if (disabledBorderColor == null) {
+            throw new IllegalArgumentException("disabledBorderColor is null.");
+        }
+
+        setDisabledBorderColor(decodeColor(disabledBorderColor));
+    }
+
+    public boolean getHeadersPressable() {
+        return headersPressable;
+    }
+
+    public void setHeadersPressable(boolean headersPressable) {
+        this.headersPressable = headersPressable;
+
+        pressedHeaderIndex = -1;
+        repaintComponent();
+    }
+
+    public boolean getColumnsResizable() {
+        return columnsResizable;
+    }
+
+    public void setColumnsResizable(boolean columnsResizable) {
+        this.columnsResizable = columnsResizable;
+    }
+
+    public boolean getIncludeTrailingVerticalGridLine() {
+        return includeTrailingVerticalGridLine;
+    }
+
+    public void setIncludeTrailingVerticalGridLine(boolean includeTrailingVerticalGridLine) {
+        this.includeTrailingVerticalGridLine = includeTrailingVerticalGridLine;
+        invalidateComponent();
+    }
+
+    @Override
+    public void enabledChanged(Component component) {
+        super.enabledChanged(component);
+
+        pressedHeaderIndex = -1;
+        repaintComponent();
+    }
+
+    @Override
+    public boolean mouseMove(Component component, int x, int y) {
+        boolean consumed = super.mouseMove(component, x, y);
+
+        if (!resizing) {
+            TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+            TableView tableView = tableViewHeader.getTableView();
+
+            if (tableView != null) {
+                int headerIndex = getHeaderAt(x);
+
+                Cursor cursor = tableViewHeader.getCursor();
+                if (headerIndex != -1
+                    && columnsResizable) {
+                    Bounds headerBounds = getHeaderBounds(headerIndex);
+                    TableView.Column column = tableView.getColumns().get(headerIndex);
+
+                    if (!column.isRelative()
+                        && x > headerBounds.x + headerBounds.width - RESIZE_HANDLE_SIZE) {
+                        cursor = Cursor.RESIZE_EAST;
+                    }
+                }
+
+                Mouse.setCursor(cursor);
+            }
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public void mouseOut(Component component) {
+        super.mouseOut(component);
+
+        if (pressedHeaderIndex != -1) {
+            repaintComponent(getHeaderBounds(pressedHeaderIndex));
+            pressedHeaderIndex = -1;
+        }
+    }
+
+    @Override
+    public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+        boolean consumed = super.mouseDown(component, button, x, y);
+
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+        TableView tableView = tableViewHeader.getTableView();
+
+        if (tableView != null) {
+            int headerIndex = getHeaderAt(x);
+
+            if (headerIndex != -1) {
+                Bounds headerBounds = getHeaderBounds(headerIndex);
+                TableView.Column column = tableView.getColumns().get(headerIndex);
+
+                if (columnsResizable
+                    && !column.isRelative()
+                    && x > headerBounds.x + headerBounds.width - RESIZE_HANDLE_SIZE) {
+                    // Begin drag
+                    Display display = tableViewHeader.getDisplay();
+                    Point headerCoordinates = tableViewHeader.mapPointToAncestor(display,
+                        headerBounds.x, 0);
+                    ResizeHandler dragHandler = new ResizeHandler(column, headerCoordinates.x,
+                        headerBounds.x + headerBounds.width - x);
+
+                    display.getComponentMouseListeners().add(dragHandler);
+                    display.getComponentMouseButtonListeners().add(dragHandler);
+
+                    resizing = true;
+                    Mouse.setCursor(Cursor.RESIZE_EAST);
+                } else if (headersPressable) {
+                    pressedHeaderIndex = headerIndex;
+                    repaintComponent(getHeaderBounds(pressedHeaderIndex));
+                }
+            }
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+        boolean consumed = super.mouseUp(component, button, x, y);
+
+        if (pressedHeaderIndex != -1) {
+            repaintComponent(getHeaderBounds(pressedHeaderIndex));
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public void mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
+        TableViewHeader tableViewHeader = (TableViewHeader)getComponent();
+
+        if (pressedHeaderIndex != -1
+            && headersPressable) {
+            tableViewHeader.pressHeader(pressedHeaderIndex);
+        }
+
+        pressedHeaderIndex = -1;
+    }
+
+    // Table view header events
+    public void tableViewChanged(TableViewHeader tableViewHeader,
+        TableView previousTableView) {
+        if (previousTableView != null) {
+            previousTableView.getTableViewColumnListeners().remove(this);
+        }
+
+        TableView tableView = tableViewHeader.getTableView();
+        if (tableView != null) {
+            tableView.getTableViewColumnListeners().add(this);
+        }
+
+        invalidateComponent();
+    }
+
+    public void dataRendererChanged(TableViewHeader tableViewHeader,
+        TableViewHeader.DataRenderer previousDataRenderer) {
+        invalidateComponent();
+    }
+
+    // Table view column events
+    public void columnInserted(TableView tableView, int index) {
+        invalidateComponent();
+    }
+
+    public void columnsRemoved(TableView tableView, int index, Sequence<TableView.Column> columns) {
+        invalidateComponent();
+    }
+
+    public void columnNameChanged(TableView.Column column, String previousName) {
+        // No-op
+    }
+
+    public void columnHeaderDataChanged(TableView.Column column, Object previousHeaderData) {
+        invalidateComponent();
+    }
+
+    public void columnWidthChanged(TableView.Column column, int previousWidth, boolean previousRelative) {
+        invalidateComponent();
+    }
+
+    public void columnSortDirectionChanged(TableView.Column column, SortDirection previousSortDirection) {
+        repaintComponent();
+    }
+
+    public void columnFilterChanged(TableView.Column column, Object previousFilter) {
+        // No-op
+    }
+
+    public void columnCellRendererChanged(TableView.Column column, TableView.CellRenderer previousCellRenderer) {
+        // No-op
+    }
+}

Added: incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewSkin.java
URL: http://svn.apache.org/viewvc/incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewSkin.java?rev=754926&view=auto
==============================================================================
--- incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewSkin.java (added)
+++ incubator/pivot/tags/v1.0/wtk/src/pivot/wtk/skin/terra/TerraTableViewSkin.java Mon Mar 16 16:16:40 2009
@@ -0,0 +1,1000 @@
+/*
+ * Copyright (c) 2008 VMware, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package pivot.wtk.skin.terra;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+
+import pivot.collections.ArrayList;
+import pivot.collections.List;
+import pivot.collections.Sequence;
+import pivot.util.Vote;
+import pivot.wtk.Component;
+import pivot.wtk.Dimensions;
+import pivot.wtk.Keyboard;
+import pivot.wtk.Mouse;
+import pivot.wtk.Bounds;
+import pivot.wtk.SortDirection;
+import pivot.wtk.Span;
+import pivot.wtk.TableView;
+import pivot.wtk.TableViewListener;
+import pivot.wtk.TableViewColumnListener;
+import pivot.wtk.TableViewRowListener;
+import pivot.wtk.TableViewRowStateListener;
+import pivot.wtk.TableViewSelectionDetailListener;
+import pivot.wtk.Theme;
+import pivot.wtk.skin.ComponentSkin;
+
+/**
+ * Table view skin.
+ * <p>
+ * NOTE This skin assumes a fixed renderer height.
+ * <p>
+ * TODO Add disableMouseSelection style to support the case where selection
+ * should be enabled but the caller wants to implement the management of it;
+ * e.g. changing a message's flag state in an email client.
+ *
+ * @author gbrown
+ */
+public class TerraTableViewSkin extends ComponentSkin implements TableView.Skin,
+    TableViewListener, TableViewColumnListener, TableViewRowListener,
+    TableViewRowStateListener, TableViewSelectionDetailListener {
+    private Font font;
+    private Color color;
+    private Color disabledColor;
+    private Color backgroundColor;
+    private Color selectionColor;
+    private Color selectionBackgroundColor;
+    private Color inactiveSelectionColor;
+    private Color inactiveSelectionBackgroundColor;
+    private Color highlightBackgroundColor;
+    private Color alternateRowColor;
+    private Color gridColor;
+    private boolean showHorizontalGridLines;
+    private boolean showVerticalGridLines;
+    private boolean showHighlight;
+    private boolean includeTrailingVerticalGridLine;
+
+    private int highlightedIndex = -1;
+
+    public TerraTableViewSkin() {
+        TerraTheme theme = (TerraTheme)Theme.getTheme();
+        font = theme.getFont();
+        color = theme.getColor(1);
+        disabledColor = theme.getColor(7);
+        backgroundColor = theme.getColor(4);
+        selectionColor = theme.getColor(4);
+        selectionBackgroundColor = theme.getColor(19);
+        inactiveSelectionColor = theme.getColor(1);
+        inactiveSelectionBackgroundColor = theme.getColor(9);
+        highlightBackgroundColor = theme.getColor(10);
+        alternateRowColor = theme.getColor(11);
+        gridColor = theme.getColor(11);
+        showHorizontalGridLines = true;
+        showVerticalGridLines = true;
+        showHighlight = true;
+        includeTrailingVerticalGridLine = false;
+    }
+
+    public void install(Component component) {
+        super.install(component);
+
+        TableView tableView = (TableView)component;
+        tableView.getTableViewListeners().add(this);
+        tableView.getTableViewColumnListeners().add(this);
+        tableView.getTableViewRowListeners().add(this);
+        tableView.getTableViewRowStateListeners().add(this);
+        tableView.getTableViewSelectionDetailListeners().add(this);
+    }
+
+    public void uninstall() {
+        TableView tableView = (TableView)getComponent();
+        tableView.getTableViewListeners().remove(this);
+        tableView.getTableViewColumnListeners().remove(this);
+        tableView.getTableViewRowListeners().remove(this);
+        tableView.getTableViewRowStateListeners().remove(this);
+        tableView.getTableViewSelectionDetailListeners().remove(this);
+
+        super.uninstall();
+    }
+
+    public int getPreferredWidth(int height) {
+        int preferredWidth = 0;
+
+        TableView tableView = (TableView)getComponent();
+        TableView.ColumnSequence columns = tableView.getColumns();
+
+        int n = columns.getLength();
+        int gridLineStop = includeTrailingVerticalGridLine ? n : n - 1;
+
+        for (int i = 0; i < n; i++) {
+            TableView.Column column = columns.get(i);
+
+            if (!column.isRelative()) {
+                preferredWidth += column.getWidth();
+
+                // Include space for vertical gridlines; even if we are
+                // not painting them, the header does
+                if (i < gridLineStop) {
+                    preferredWidth++;
+                }
+            }
+        }
+
+        return preferredWidth;
+    }
+
+    public int getPreferredHeight(int width) {
+        int preferredHeight = 0;
+
+        TableView tableView = (TableView)getComponent();
+
+        int n = tableView.getTableData().getLength();
+        preferredHeight = getRowHeight() * n;
+
+        return preferredHeight;
+    }
+
+    public Dimensions getPreferredSize() {
+        return new Dimensions(getPreferredWidth(-1), getPreferredHeight(-1));
+    }
+
+    public void layout() {
+        // No-op
+    }
+
+    @SuppressWarnings("unchecked")
+    public void paint(Graphics2D graphics) {
+        TableView tableView = (TableView)getComponent();
+        List<Object> tableData = (List<Object>)tableView.getTableData();
+        TableView.ColumnSequence columns = tableView.getColumns();
+
+        int width = getWidth();
+        int height = getHeight();
+
+        int rowHeight = getRowHeight();
+        Sequence<Integer> columnWidths = getColumnWidths();
+
+        // Paint the background
+        graphics.setPaint(backgroundColor);
+        graphics.fillRect(0, 0, width, height);
+
+        // Paint the list contents
+        int rowStart = 0;
+        int rowEnd = tableData.getLength() - 1;
+
+        // Ensure that we only paint items that are visible
+        Rectangle clipBounds = graphics.getClipBounds();
+        if (clipBounds != null) {
+            rowStart = Math.max(rowStart, (int)Math.floor(clipBounds.y
+                / (double)rowHeight));
+            rowEnd = Math.min(rowEnd, (int)Math.ceil((clipBounds.y
+                + clipBounds.height) / (double)rowHeight) - 1);
+        }
+
+        int rowY = rowStart * rowHeight;
+
+        for (int rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+            Object rowData = tableData.get(rowIndex);
+            boolean rowHighlighted = (rowIndex == highlightedIndex
+                && Mouse.getButtons() == 0
+                && tableView.getSelectMode() != TableView.SelectMode.NONE);
+            boolean rowSelected = tableView.isIndexSelected(rowIndex);
+            boolean rowDisabled = tableView.isRowDisabled(rowIndex);
+
+            Color rowBackgroundColor = null;
+
+            if (rowSelected) {
+                rowBackgroundColor = (tableView.isFocused())
+                    ? this.selectionBackgroundColor : inactiveSelectionBackgroundColor;
+            } else {
+                if (rowHighlighted && showHighlight && !rowDisabled) {
+                    rowBackgroundColor = highlightBackgroundColor;
+                } else {
+                    if (alternateRowColor != null
+                        && rowIndex % 2 > 0) {
+                        rowBackgroundColor = alternateRowColor;
+                    }
+                }
+            }
+
+            if (rowBackgroundColor != null) {
+                graphics.setPaint(rowBackgroundColor);
+                graphics.fillRect(0, rowY, width, rowHeight);
+            }
+
+            // Paint the cells
+            int cellX = 0;
+
+            for (int columnIndex = 0, columnCount = columns.getLength();
+                columnIndex < columnCount; columnIndex++) {
+                TableView.Column column = columns.get(columnIndex);
+
+                TableView.CellRenderer cellRenderer = column.getCellRenderer();
+
+                int columnWidth = columnWidths.get(columnIndex);
+
+                Graphics2D rendererGraphics = (Graphics2D)graphics.create(cellX, rowY,
+                    columnWidth, rowHeight);
+
+                cellRenderer.render(rowData, tableView, column, rowSelected,
+                    rowHighlighted, rowDisabled);
+                cellRenderer.setSize(columnWidth, rowHeight - 1);
+                cellRenderer.paint(rendererGraphics);
+
+                rendererGraphics.dispose();
+
+                cellX += columnWidth + 1;
+            }
+
+            rowY += rowHeight;
+        }
+
+        // Set the grid stroke and color
+        graphics.setStroke(new BasicStroke());
+        graphics.setPaint(gridColor);
+
+        // Paint the vertical grid lines
+        if (showVerticalGridLines) {
+            int gridX = 0;
+
+            for (int columnIndex = 0, columnCount = columns.getLength();
+                columnIndex < columnCount; columnIndex++) {
+                gridX += columnWidths.get(columnIndex);
+
+                graphics.drawLine(gridX, 0, gridX, height);
+                gridX++;
+            }
+        }
+
+        // Paint the horizontal grid line
+        if (showHorizontalGridLines) {
+            for (int rowIndex = rowStart; rowIndex <= rowEnd; rowIndex++) {
+                if (rowIndex > 0) {
+                    int gridY = rowIndex * rowHeight;
+                    graphics.drawLine(0, gridY, width, gridY);
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the table row height, which is determined as the maximum
+     * preferred height of all cell renderers.
+     *
+     * @return
+     * The height of one table row.
+     */
+    public int getRowHeight() {
+        int rowHeight = 0;
+
+        TableView tableView = (TableView)getComponent();
+        TableView.ColumnSequence columns = tableView.getColumns();
+
+        for (int i = 0, n = columns.getLength(); i < n; i++) {
+            TableView.Column column = columns.get(i);
+            TableView.CellRenderer cellRenderer = column.getCellRenderer();
+
+            rowHeight = Math.max(rowHeight, cellRenderer.getPreferredHeight(-1));
+        }
+
+        rowHeight++;
+
+        return rowHeight;
+    }
+
+    /**
+     * Returns the column widths for this table.
+     *
+     * @return
+     * The widths of all columns based on the current overall width.
+     */
+    public Sequence<Integer> getColumnWidths() {
+        TableView tableView = (TableView)getComponent();
+
+        return getColumnWidths(tableView.getColumns(), getWidth());
+    }
+
+    /**
+     * Returns the column widths, determined by applying relative size values
+     * to the available width.
+     *
+     * TODO Cache these values and recalculate only when size changes.
+     *
+     * @param columns
+     * The columns whose widths are to be determined.
+     *
+     * @param width
+     * The total available width for the columns.
+     *
+     * @return
+     * The widths of all columns based on the current overall width.
+     */
+    public static Sequence<Integer> getColumnWidths(TableView.ColumnSequence columns, int width) {
+        int fixedWidth = 0;
+        int relativeWidth = 0;
+
+        int n = columns.getLength();
+
+        for (int i = 0; i < n; i++) {
+            TableView.Column column = columns.get(i);
+
+            if (column.isRelative()) {
+                relativeWidth += column.getWidth();
+            } else {
+                fixedWidth += column.getWidth();
+            }
+        }
+
+        fixedWidth += n - 1;
+        int variableWidth = Math.max(width - fixedWidth, 0);
+
+        ArrayList<Integer> columnWidths = new ArrayList<Integer>(columns.getLength());
+
+        for (int i = 0; i < n; i++) {
+            TableView.Column column = columns.get(i);
+
+            if (column.isRelative()) {
+                int columnWidth = (int)Math.round((double)(column.getWidth()
+                    * variableWidth) / (double)relativeWidth);
+                columnWidths.add(columnWidth);
+            } else {
+                columnWidths.add(column.getWidth());
+            }
+        }
+
+        return columnWidths;
+    }
+
+    // Table view skin methods
+    @SuppressWarnings("unchecked")
+    public int getRowAt(int y) {
+        if (y < 0) {
+            throw new IllegalArgumentException("y is negative");
+        }
+
+        TableView tableView = (TableView)getComponent();
+        List<Object> tableData = (List<Object>)tableView.getTableData();
+
+        int rowIndex = (y / getRowHeight());
+
+        if (rowIndex >= tableData.getLength()) {
+            rowIndex = -1;
+        }
+
+        return rowIndex;
+    }
+
+    public int getColumnAt(int x) {
+        if (x < 0) {
+            throw new IllegalArgumentException("x is negative");
+        }
+
+        Sequence<Integer> columnWidths = getColumnWidths();
+
+        int i = 0;
+        int n = columnWidths.getLength();
+        int columnX = 0;
+        while (i < n
+            && x > columnX) {
+            columnX += (columnWidths.get(i) + 1);
+            i++;
+        }
+
+        int columnIndex = -1;
+
+        if (x <= columnX) {
+            columnIndex = i - 1;
+        }
+
+        return columnIndex;
+    }
+
+    public Bounds getRowBounds(int rowIndex) {
+        int rowHeight = getRowHeight();
+        return new Bounds(0, rowIndex * rowHeight, getWidth(), rowHeight);
+    }
+
+    public Bounds getColumnBounds(int columnIndex) {
+        Sequence<Integer> columnWidths = getColumnWidths();
+        int columnCount = columnWidths.getLength();
+
+        if (columnIndex < 0
+            || columnIndex >= columnCount) {
+            throw new IndexOutOfBoundsException("Column index out of bounds: " +
+                columnIndex);
+        }
+
+        int columnX = 0;
+        for (int i = 0; i < columnIndex; i++) {
+            columnX += (columnWidths.get(i) + 1);
+        }
+
+        return new Bounds(columnX, 0, columnWidths.get(columnIndex), getHeight());
+    }
+
+    @SuppressWarnings("unchecked")
+    public Bounds getCellBounds(int rowIndex, int columnIndex) {
+        TableView tableView = (TableView)getComponent();
+        List<Object> tableData = (List<Object>)tableView.getTableData();
+        Sequence<Integer> columnWidths = getColumnWidths();
+
+        if (rowIndex < 0
+            || rowIndex >= tableData.getLength()) {
+            throw new IndexOutOfBoundsException();
+        }
+
+        if (columnIndex < 0
+            || columnIndex >= columnWidths.getLength()) {
+            throw new IndexOutOfBoundsException();
+        }
+
+
+        int rowHeight = getRowHeight();
+
+        int cellX = 0;
+        for (int i = 0; i < columnIndex; i++) {
+            cellX += (columnWidths.get(i) + 1);
+        }
+
+        return new Bounds(cellX, rowIndex * rowHeight,
+            columnWidths.get(columnIndex), rowHeight);
+    }
+
+    @Override
+    public boolean isFocusable() {
+        TableView tableView = (TableView)getComponent();
+        return (tableView.getSelectMode() != TableView.SelectMode.NONE);
+    }
+
+    public Font getFont() {
+        return font;
+    }
+
+    public void setFont(Font font) {
+        if (font == null) {
+            throw new IllegalArgumentException("font is null.");
+        }
+
+        this.font = font;
+        invalidateComponent();
+    }
+
+    public final void setFont(String font) {
+        if (font == null) {
+            throw new IllegalArgumentException("font is null.");
+        }
+
+        setFont(Font.decode(font));
+    }
+
+    public Color getColor() {
+        return color;
+    }
+
+    public void setColor(Color color) {
+        if (color == null) {
+            throw new IllegalArgumentException("color is null.");
+        }
+
+        this.color = color;
+        repaintComponent();
+    }
+
+    public final void setColor(String color) {
+        if (color == null) {
+            throw new IllegalArgumentException("color is null.");
+        }
+
+        setColor(decodeColor(color));
+    }
+
+    public Color getDisabledColor() {
+        return disabledColor;
+    }
+
+    public void setDisabledColor(Color disabledColor) {
+        if (disabledColor == null) {
+            throw new IllegalArgumentException("disabledColor is null.");
+        }
+
+        this.disabledColor = disabledColor;
+        repaintComponent();
+    }
+
+    public final void setDisabledColor(String disabledColor) {
+        if (disabledColor == null) {
+            throw new IllegalArgumentException("disabledColor is null.");
+        }
+
+        setDisabledColor(decodeColor(disabledColor));
+    }
+
+    public Color getBackgroundColor() {
+        return backgroundColor;
+    }
+
+    public void setBackgroundColor(Color backgroundColor) {
+        if (backgroundColor == null) {
+            throw new IllegalArgumentException("backgroundColor is null.");
+        }
+
+        this.backgroundColor = backgroundColor;
+        repaintComponent();
+    }
+
+    public final void setBackgroundColor(String backgroundColor) {
+        if (backgroundColor == null) {
+            throw new IllegalArgumentException("backgroundColor is null.");
+        }
+
+        setBackgroundColor(decodeColor(backgroundColor));
+    }
+
+
+    public Color getSelectionColor() {
+        return selectionColor;
+    }
+
+    public void setSelectionColor(Color selectionColor) {
+        if (selectionColor == null) {
+            throw new IllegalArgumentException("selectionColor is null.");
+        }
+
+        this.selectionColor = selectionColor;
+        repaintComponent();
+    }
+
+    public final void setSelectionColor(String selectionColor) {
+        if (selectionColor == null) {
+            throw new IllegalArgumentException("selectionColor is null.");
+        }
+
+        setSelectionColor(decodeColor(selectionColor));
+    }
+
+    public Color getSelectionBackgroundColor() {
+        return selectionBackgroundColor;
+    }
+
+    public void setSelectionBackgroundColor(Color selectionBackgroundColor) {
+        if (selectionBackgroundColor == null) {
+            throw new IllegalArgumentException("selectionBackgroundColor is null.");
+        }
+
+        this.selectionBackgroundColor = selectionBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setSelectionBackgroundColor(String selectionBackgroundColor) {
+        if (selectionBackgroundColor == null) {
+            throw new IllegalArgumentException("selectionBackgroundColor is null.");
+        }
+
+        setSelectionBackgroundColor(decodeColor(selectionBackgroundColor));
+    }
+
+    public Color getInactiveSelectionColor() {
+        return inactiveSelectionColor;
+    }
+
+    public void setInactiveSelectionColor(Color inactiveSelectionColor) {
+        if (inactiveSelectionColor == null) {
+            throw new IllegalArgumentException("inactiveSelectionColor is null.");
+        }
+
+        this.inactiveSelectionColor = inactiveSelectionColor;
+        repaintComponent();
+    }
+
+    public final void setInactiveSelectionColor(String inactiveSelectionColor) {
+        if (inactiveSelectionColor == null) {
+            throw new IllegalArgumentException("inactiveSelectionColor is null.");
+        }
+
+        setInactiveSelectionColor(decodeColor(inactiveSelectionColor));
+    }
+
+    public Color getInactiveSelectionBackgroundColor() {
+        return inactiveSelectionBackgroundColor;
+    }
+
+    public void setInactiveSelectionBackgroundColor(Color inactiveSelectionBackgroundColor) {
+        if (inactiveSelectionBackgroundColor == null) {
+            throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
+        }
+
+        this.inactiveSelectionBackgroundColor = inactiveSelectionBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setInactiveSelectionBackgroundColor(String inactiveSelectionBackgroundColor) {
+        if (inactiveSelectionBackgroundColor == null) {
+            throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
+        }
+
+        setInactiveSelectionBackgroundColor(decodeColor(inactiveSelectionBackgroundColor));
+    }
+
+    public Color getHighlightBackgroundColor() {
+        return highlightBackgroundColor;
+    }
+
+    public void setHighlightBackgroundColor(Color highlightBackgroundColor) {
+        if (highlightBackgroundColor == null) {
+            throw new IllegalArgumentException("highlightBackgroundColor is null.");
+        }
+
+        this.highlightBackgroundColor = highlightBackgroundColor;
+        repaintComponent();
+    }
+
+    public final void setHighlightBackgroundColor(String highlightBackgroundColor) {
+        if (highlightBackgroundColor == null) {
+            throw new IllegalArgumentException("highlightBackgroundColor is null.");
+        }
+
+        setHighlightBackgroundColor(decodeColor(highlightBackgroundColor));
+    }
+
+    public Color getAlternateRowColor() {
+        return alternateRowColor;
+    }
+
+    public void setAlternateRowColor(Color alternateRowColor) {
+        this.alternateRowColor = alternateRowColor;
+        repaintComponent();
+    }
+
+    public final void setAlternateRowColor(String alternateRowColor) {
+        if (alternateRowColor == null) {
+            throw new IllegalArgumentException("alternateRowColor is null.");
+        }
+
+        setAlternateRowColor(decodeColor(alternateRowColor));
+    }
+
+    public Color getGridColor() {
+        return gridColor;
+    }
+
+    public void setGridColor(Color gridColor) {
+        if (gridColor == null) {
+            throw new IllegalArgumentException("gridColor is null.");
+        }
+
+        this.gridColor = gridColor;
+        repaintComponent();
+    }
+
+    public final void setGridColor(String gridColor) {
+        if (gridColor == null) {
+            throw new IllegalArgumentException("gridColor is null.");
+        }
+
+        setGridColor(decodeColor(gridColor));
+    }
+
+    public boolean getShowHorizontalGridLines() {
+        return showHorizontalGridLines;
+    }
+
+    public void setShowHorizontalGridLines(boolean showHorizontalGridLines) {
+        this.showHorizontalGridLines = showHorizontalGridLines;
+        repaintComponent();
+    }
+
+    public boolean getShowVerticalGridLines() {
+        return showVerticalGridLines;
+    }
+
+    public void setShowVerticalGridLines(boolean showVerticalGridLines) {
+        this.showVerticalGridLines = showVerticalGridLines;
+        repaintComponent();
+    }
+
+    public boolean getShowHighlight() {
+        return showHighlight;
+    }
+
+    public void setShowHighlight(boolean showHighlight) {
+        this.showHighlight = showHighlight;
+        repaintComponent();
+    }
+
+    public boolean getIncludeTrailingVerticalGridLine() {
+        return includeTrailingVerticalGridLine;
+    }
+
+    public void setIncludeTrailingVerticalGridLine(boolean includeTrailingVerticalGridLine) {
+        this.includeTrailingVerticalGridLine = includeTrailingVerticalGridLine;
+        invalidateComponent();
+    }
+
+    @Override
+    public boolean mouseMove(Component component, int x, int y) {
+        boolean consumed = super.mouseMove(component, x, y);
+
+        int previousHighlightedIndex = this.highlightedIndex;
+        highlightedIndex = getRowAt(y);
+
+        if (previousHighlightedIndex != highlightedIndex) {
+            if (previousHighlightedIndex != -1) {
+                repaintComponent(getRowBounds(previousHighlightedIndex));
+            }
+
+            if (highlightedIndex != -1) {
+                repaintComponent(getRowBounds(highlightedIndex));
+            }
+        }
+
+        return consumed;
+    }
+
+    @Override
+    public void mouseOut(Component component) {
+        super.mouseOut(component);
+
+        if (highlightedIndex != -1) {
+            Bounds rowBounds = getRowBounds(highlightedIndex);
+            repaintComponent(rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height);
+        }
+
+        highlightedIndex = -1;
+    }
+
+    @Override
+    public void mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
+        TableView tableView = (TableView)getComponent();
+
+        if (isFocusable()) {
+            tableView.requestFocus();
+        }
+
+        int rowIndex = getRowAt(y);
+
+        if (rowIndex >= 0
+            && !tableView.isRowDisabled(rowIndex)) {
+            TableView.SelectMode selectMode = tableView.getSelectMode();
+
+            if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)
+                && selectMode == TableView.SelectMode.MULTI) {
+                // Select the range
+                int startIndex = tableView.getFirstSelectedIndex();
+                int endIndex = tableView.getLastSelectedIndex();
+                Span selectedRange = (rowIndex > startIndex) ?
+                    new Span(startIndex, rowIndex) : new Span(rowIndex, endIndex);
+
+                ArrayList<Span> selectedRanges = new ArrayList<Span>();
+                Sequence<Integer> disabledIndexes = tableView.getDisabledIndexes();
+                if (disabledIndexes.getLength() == 0) {
+                    selectedRanges.add(selectedRange);
+                } else {
+                    // TODO Split the range by the disabled indexes; for now,
+                    // just return
+                    return;
+                }
+
+                tableView.setSelectedRanges(selectedRanges);
+            } else if (Keyboard.isPressed(Keyboard.Modifier.CTRL)
+                && selectMode == TableView.SelectMode.MULTI) {
+                // Toggle the item's selection state
+                if (tableView.isIndexSelected(rowIndex)) {
+                    tableView.removeSelectedIndex(rowIndex);
+                } else {
+                    tableView.addSelectedIndex(rowIndex);
+                }
+            } else {
+                // Select the row
+                if ((selectMode == TableView.SelectMode.SINGLE
+                        && tableView.getSelectedIndex() != rowIndex)
+                    || selectMode == TableView.SelectMode.MULTI) {
+                    tableView.setSelectedIndex(rowIndex);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean mouseWheel(Component component, Mouse.ScrollType scrollType, int scrollAmount,
+        int wheelRotation, int x, int y) {
+        if (highlightedIndex != -1) {
+            Bounds rowBounds = getRowBounds(highlightedIndex);
+            repaintComponent(rowBounds.x, rowBounds.y, rowBounds.width, rowBounds.height);
+        }
+
+        highlightedIndex = -1;
+
+        return super.mouseWheel(component, scrollType, scrollAmount, wheelRotation, x, y);
+    }
+
+    @Override
+    public boolean keyPressed(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
+        boolean consumed = super.keyPressed(component, keyCode, keyLocation);
+
+        TableView tableView = (TableView)getComponent();
+
+        switch (keyCode) {
+            case Keyboard.KeyCode.UP: {
+                int index = tableView.getFirstSelectedIndex();
+
+                do {
+                    index--;
+                } while (index >= 0
+                    && tableView.isRowDisabled(index));
+
+                if (index >= 0) {
+                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)
+                        && tableView.getSelectMode() == TableView.SelectMode.MULTI) {
+                        tableView.addSelectedIndex(index);
+                    } else {
+                        tableView.setSelectedIndex(index);
+                    }
+
+                    tableView.scrollAreaToVisible(getRowBounds(index));
+                }
+
+                consumed = true;
+                break;
+            }
+
+            case Keyboard.KeyCode.DOWN: {
+                int index = tableView.getLastSelectedIndex();
+                int count = tableView.getTableData().getLength();
+
+                do {
+                    index++;
+                } while (index < count
+                    && tableView.isRowDisabled(index));
+
+                if (index < count) {
+                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)
+                        && tableView.getSelectMode() == TableView.SelectMode.MULTI) {
+                        tableView.addSelectedIndex(index);
+                    } else {
+                        tableView.setSelectedIndex(index);
+                    }
+
+                    tableView.scrollAreaToVisible(getRowBounds(index));
+                }
+
+                consumed = true;
+                break;
+            }
+        }
+
+        // Clear the highlight
+        if (highlightedIndex != -1) {
+            highlightedIndex = -1;
+            repaintComponent(getRowBounds(highlightedIndex));
+        }
+
+        return consumed;
+    }
+
+    // Component state events
+    @Override
+    public void enabledChanged(Component component) {
+        super.enabledChanged(component);
+
+        repaintComponent();
+    }
+
+    @Override
+    public void focusedChanged(Component component, boolean temporary) {
+        super.focusedChanged(component, temporary);
+
+        repaintComponent();
+    }
+
+    // Table view events
+    public void tableDataChanged(TableView tableView, List<?> previousTableData) {
+        invalidateComponent();
+    }
+
+    public void selectModeChanged(TableView tableView, TableView.SelectMode previousSelectMode) {
+        // No-op
+    }
+
+    // Table view column events
+    public void columnInserted(TableView tableView, int index) {
+        invalidateComponent();
+    }
+
+    public void columnsRemoved(TableView tableView, int index, Sequence<TableView.Column> columns) {
+        invalidateComponent();
+    }
+
+    public void columnNameChanged(TableView.Column column, String previousName) {
+        invalidateComponent();
+    }
+
+    public void columnHeaderDataChanged(TableView.Column column, Object previousHeaderData) {
+        // No-op
+    }
+
+    public void columnWidthChanged(TableView.Column column, int previousWidth, boolean previousRelative)  {
+        invalidateComponent();
+    }
+
+    public void columnSortDirectionChanged(TableView.Column column, SortDirection previousSortDirection) {
+        // No-op
+        // TODO Repaint; paint a "selection" color for the sorted column
+    }
+
+    public void columnFilterChanged(TableView.Column column, Object previousFilter) {
+        // No-op
+    }
+
+    public void columnCellRendererChanged(TableView.Column column, TableView.CellRenderer previousCellRenderer) {
+        invalidateComponent();
+    }
+
+    // Table view row events
+    public void rowInserted(TableView tableView, int index) {
+        invalidateComponent();
+    }
+
+    public void rowsRemoved(TableView tableView, int index, int count) {
+        invalidateComponent();
+    }
+
+    public void rowUpdated(TableView tableView, int index) {
+        repaintComponent(getRowBounds(index));
+    }
+
+    public void rowsSorted(TableView tableView) {
+        repaintComponent();
+    }
+
+    // Table view row state events
+
+    public Vote previewRowDisabledChange(TableView tableView, int index) {
+        return Vote.APPROVE;
+    }
+
+    public void rowDisabledChangeVetoed(TableView tableView, int index, Vote reason) {
+        // No-op
+    }
+
+    public void rowDisabledChanged(TableView tableView, int index) {
+        repaintComponent(getRowBounds(index));
+    }
+
+    // Table view selection detail events
+    public void selectedRangeAdded(TableView tableView, int rangeStart, int rangeEnd) {
+        // Repaint the area containing the added selection
+        int rowHeight = getRowHeight();
+        repaintComponent(0, rangeStart * rowHeight,
+            getWidth(), (rangeEnd - rangeStart + 1) * rowHeight);
+    }
+
+    public void selectedRangeRemoved(TableView tableView, int rangeStart, int rangeEnd) {
+        // Repaint the area containing the removed selection
+        int rowHeight = getRowHeight();
+        repaintComponent(0, rangeStart * rowHeight,
+            getWidth(), (rangeEnd - rangeStart + 1) * rowHeight);
+    }
+
+    public void selectionReset(TableView tableView, Sequence<Span> previousSelectedRanges) {
+        // TODO Repaint only the area that changed (intersection of previous
+        // and new selection)
+        repaintComponent();
+    }
+}