org.eclipse.swt.examples.accessibility.CTable.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.swt.examples.accessibility.CTable.java

Source

/*******************************************************************************
 * Copyright (c) 2000, 2016 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.swt.examples.accessibility;

import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.Accessible;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleControlAdapter;
import org.eclipse.swt.accessibility.AccessibleControlEvent;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.accessibility.AccessibleTableAdapter;
import org.eclipse.swt.accessibility.AccessibleTableEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Tracker;
import org.eclipse.swt.widgets.TypedListener;
import org.eclipse.swt.widgets.Widget;

/**
 * Instances of this class implement a selectable user interface
 * object that displays a list of images and strings and issues
 * notification when selected.
 * <p>
 * The item children that may be added to instances of this class
 * must be of type <code>TableItem</code>.
 * </p><p>
 * Style <code>VIRTUAL</code> is used to create a <code>Table</code> whose
 * <code>TableItem</code>s are to be populated by the client on an on-demand basis
 * instead of up-front.  This can provide significant performance improvements for
 * tables that are very large or for which <code>TableItem</code> population is
 * expensive (for example, retrieving values from an external source).
 * </p><p>
 * Here is an example of using a <code>Table</code> with style <code>VIRTUAL</code>:
 * <code><pre>
 *  final Table table = new Table (parent, SWT.VIRTUAL | SWT.BORDER);
 *  table.setItemCount (1000000);
 *  table.addListener (SWT.SetData, new Listener () {
 *      public void handleEvent (Event event) {
 *          TableItem item = (TableItem) event.item;
 *          int index = table.indexOf (item);
 *          item.setText ("Item " + index);
 *          System.out.println (item.getText ());
 *      }
 *  });
 * </pre></code>
 * </p><p>
 * Note that although this class is a subclass of <code>Composite</code>,
 * it does not normally make sense to add <code>Control</code> children to
 * it, or set a layout on it, unless implementing something like a cell
 * editor.
 * </p><p>
 * <dl>
 * <dt><b>Styles:</b></dt>
 * <dd>SINGLE, MULTI, CHECK, FULL_SELECTION, HIDE_SELECTION, VIRTUAL, NO_SCROLL</dd>
 * <dt><b>Events:</b></dt>
 * <dd>Selection, DefaultSelection, SetData, MeasureItem, EraseItem, PaintItem</dd>
 * </dl>
 * </p><p>
 * Note: Only one of the styles SINGLE, and MULTI may be specified.
 * </p><p>
 * IMPORTANT: This class is <em>not</em> intended to be subclassed.
 * </p>
 *
 * @see <a href="http://www.eclipse.org/swt/snippets/#table">Table, TableItem, TableColumn snippets</a>
 * @see <a href="http://www.eclipse.org/swt/examples.php">SWT Example: ControlExample</a>
 * @see <a href="http://www.eclipse.org/swt/">Sample code and further information</a>
 * @noextend This class is not intended to be subclassed by clients.
 */
public class CTable extends Composite {
    Canvas header;
    CTableColumn[] columns = new CTableColumn[0];
    CTableColumn[] orderedColumns;
    CTableItem[] items = new CTableItem[0];
    CTableItem[] selectedItems = new CTableItem[0];
    CTableItem focusItem, anchorItem, lastClickedItem;
    Event lastSelectionEvent;
    boolean linesVisible, ignoreKey, ignoreDispose, customHeightSet;
    int itemsCount = 0;
    int topIndex = 0, horizontalOffset = 0;
    int fontHeight = 0, imageHeight = 0, itemHeight = 0;
    int col0ImageWidth = 0;
    int headerImageHeight = 0;
    CTableColumn resizeColumn;
    int resizeColumnX = -1;
    int drawCount = 0;
    CTableColumn sortColumn;
    int sortDirection = SWT.NONE;

    /* column header tooltip */
    Listener toolTipListener;
    Shell toolTipShell;
    Label toolTipLabel;

    Rectangle arrowBounds, checkboxBounds, clientArea;

    static final int MARGIN_IMAGE = 3;
    static final int MARGIN_CELL = 1;
    static final int SIZE_HORIZONTALSCROLL = 5;
    static final int TOLLERANCE_COLUMNRESIZE = 2;
    static final int WIDTH_HEADER_SHADOW = 2;
    static final int WIDTH_CELL_HIGHLIGHT = 1;
    static final int[] toolTipEvents = new int[] { SWT.MouseExit, SWT.MouseHover, SWT.MouseMove, SWT.MouseDown };
    static final String ELLIPSIS = "..."; //$NON-NLS-1$
    static final String ID_UNCHECKED = "UNCHECKED"; //$NON-NLS-1$
    static final String ID_GRAYUNCHECKED = "GRAYUNCHECKED"; //$NON-NLS-1$
    static final String ID_CHECKMARK = "CHECKMARK"; //$NON-NLS-1$
    static final String ID_ARROWUP = "ARROWUP"; //$NON-NLS-1$
    static final String ID_ARROWDOWN = "ARROWDOWN"; //$NON-NLS-1$

    Display display;

    //TEMPORARY CODE
    boolean hasFocus;

    @Override
    public boolean isFocusControl() {
        return hasFocus;
    }

    /**
     * Constructs a new instance of this class given its parent
     * and a style value describing its behavior and appearance.
     * <p>
     * The style value is either one of the style constants defined in
     * class <code>SWT</code> which is applicable to instances of this
     * class, or must be built by <em>bitwise OR</em>'ing together
     * (that is, using the <code>int</code> "|" operator) two or more
     * of those <code>SWT</code> style constants. The class description
     * lists the style constants that are applicable to the class.
     * Style bits are also inherited from superclasses.
     * </p>
     *
     * @param parent a composite control which will be the parent of the new instance (cannot be null)
     * @param style the style of control to construct
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the parent is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the parent</li>
     *    <li>ERROR_INVALID_SUBCLASS - if this class is not an allowed subclass</li>
     * </ul>
     *
     * @see SWT#SINGLE
     * @see SWT#MULTI
     * @see SWT#CHECK
     * @see SWT#FULL_SELECTION
     * @see SWT#HIDE_SELECTION
     * @see SWT#VIRTUAL
     * @see SWT#NO_SCROLL
     * @see Widget#checkSubclass
     * @see Widget#getStyle
     */
    public CTable(Composite parent, int style) {
        super(parent, checkStyle(style));
        this.display = parent.getDisplay();
        setForeground(null); /* set foreground and background to chosen default colors */
        setBackground(null);
        GC gc = new GC(this);
        fontHeight = gc.getFontMetrics().getHeight();
        gc.dispose();
        itemHeight = fontHeight + (2 * getCellPadding());
        initImages(display);
        checkboxBounds = getUncheckedImage().getBounds();
        arrowBounds = getArrowDownImage().getBounds();
        clientArea = getClientArea();

        Listener listener = event -> handleEvents(event);
        addListener(SWT.Paint, listener);
        addListener(SWT.MouseDown, listener);
        addListener(SWT.MouseUp, listener);
        addListener(SWT.MouseDoubleClick, listener);
        addListener(SWT.Dispose, listener);
        addListener(SWT.Resize, listener);
        addListener(SWT.KeyDown, listener);
        addListener(SWT.FocusOut, listener);
        addListener(SWT.FocusIn, listener);
        addListener(SWT.Traverse, listener);

        initAccessibility();

        header = new Canvas(this, SWT.NO_REDRAW_RESIZE | SWT.NO_FOCUS);
        header.setVisible(false);
        header.setBounds(0, 0, 0, fontHeight + 2 * getHeaderPadding());
        header.addListener(SWT.Paint, listener);
        header.addListener(SWT.MouseDown, listener);
        header.addListener(SWT.MouseUp, listener);
        header.addListener(SWT.MouseHover, listener);
        header.addListener(SWT.MouseDoubleClick, listener);
        header.addListener(SWT.MouseMove, listener);
        header.addListener(SWT.MouseExit, listener);
        header.addListener(SWT.MenuDetect, listener);

        toolTipListener = event -> {
            switch (event.type) {
            case SWT.MouseHover:
            case SWT.MouseMove:
                if (headerUpdateToolTip(event.x))
                    break;
                // FALL THROUGH
            case SWT.MouseExit:
            case SWT.MouseDown:
                headerHideToolTip();
                break;
            }
        };

        ScrollBar hBar = getHorizontalBar();
        if (hBar != null) {
            hBar.setValues(0, 0, 1, 1, 1, 1);
            hBar.setVisible(false);
            hBar.addListener(SWT.Selection, listener);
        }
        ScrollBar vBar = getVerticalBar();
        if (vBar != null) {
            vBar.setValues(0, 0, 1, 1, 1, 1);
            vBar.setVisible(false);
            vBar.addListener(SWT.Selection, listener);
        }
    }

    /**
     * Adds the listener to the collection of listeners who will
     * be notified when the user changes the receiver's selection, by sending
     * it one of the messages defined in the <code>SelectionListener</code>
     * interface.
     * <p>
     * When <code>widgetSelected</code> is called, the item field of the event object is valid.
     * If the receiver has the <code>SWT.CHECK</code> style and the check selection changes,
     * the event object detail field contains the value <code>SWT.CHECK</code>.
     * <code>widgetDefaultSelected</code> is typically called when an item is double-clicked.
     * The item field of the event object is valid for default selection, but the detail field is not used.
     * </p>
     *
     * @param listener the listener which should be notified when the user changes the receiver's selection
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see SelectionListener
     * @see #removeSelectionListener
     * @see SelectionEvent
     */
    public void addSelectionListener(SelectionListener listener) {
        checkWidget();
        if (listener == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        TypedListener typedListener = new TypedListener(listener);
        addListener(SWT.Selection, typedListener);
        addListener(SWT.DefaultSelection, typedListener);
    }

    boolean checkData(CTableItem item, boolean redraw) {
        if (item.cached)
            return true;
        if ((getStyle() & SWT.VIRTUAL) != 0) {
            item.cached = true;
            Event event = new Event();
            event.item = item;
            event.index = indexOf(item);
            notifyListeners(SWT.SetData, event);
            if (isDisposed() || item.isDisposed())
                return false;
            if (redraw)
                redrawItem(item.index, false);
        }
        return true;
    }

    static int checkStyle(int style) {
        /*
        * Feature in Windows.  Even when WS_HSCROLL or
        * WS_VSCROLL is not specified, Windows creates
        * trees and tables with scroll bars.  The fix
        * is to set H_SCROLL and V_SCROLL.
        *
        * NOTE: This code appears on all platforms so that
        * applications have consistent scroll bar behavior.
        */
        if ((style & SWT.NO_SCROLL) == 0) {
            style |= SWT.H_SCROLL | SWT.V_SCROLL;
        }
        style |= SWT.NO_REDRAW_RESIZE | SWT.NO_BACKGROUND | SWT.DOUBLE_BUFFERED;
        //TEMPORARY CODE
        style |= SWT.FULL_SELECTION;
        return checkBits(style, SWT.SINGLE, SWT.MULTI, 0, 0, 0, 0);
    }

    static int checkBits(int style, int int0, int int1, int int2, int int3, int int4, int int5) {
        int mask = int0 | int1 | int2 | int3 | int4 | int5;
        if ((style & mask) == 0)
            style |= int0;
        if ((style & int0) != 0)
            style = (style & ~mask) | int0;
        if ((style & int1) != 0)
            style = (style & ~mask) | int1;
        if ((style & int2) != 0)
            style = (style & ~mask) | int2;
        if ((style & int3) != 0)
            style = (style & ~mask) | int3;
        if ((style & int4) != 0)
            style = (style & ~mask) | int4;
        if ((style & int5) != 0)
            style = (style & ~mask) | int5;
        return style;
    }

    /**
     * Clears the item at the given zero-relative index in the receiver.
     * The text, icon and other attributes of the item are set to the default
     * value.  If the table was created with the <code>SWT.VIRTUAL</code> style,
     * these attributes are requested again as needed.
     *
     * @param index the index of the item to clear
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see SWT#VIRTUAL
     * @see SWT#SetData
     *
     * @since 3.0
     */
    public void clear(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            SWT.error(SWT.ERROR_INVALID_RANGE);
        Rectangle bounds = items[index].getBounds(false);
        int oldRightX = bounds.x + bounds.width;
        items[index].clear();
        if (columns.length == 0)
            updateHorizontalBar(0, -oldRightX);
        redrawItem(index, false);
    }

    /**
     * Removes the items from the receiver which are between the given
     * zero-relative start and end indices (inclusive).  The text, icon
     * and other attributes of the items are set to their default values.
     * If the table was created with the <code>SWT.VIRTUAL</code> style,
     * these attributes are requested again as needed.
     *
     * @param start the start index of the item to clear
     * @param end the end index of the item to clear
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if either the start or end are not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see SWT#VIRTUAL
     * @see SWT#SetData
     *
     * @since 3.0
     */
    public void clear(int start, int end) {
        checkWidget();
        if (start > end)
            return;
        if (!(0 <= start && start <= end && end < itemsCount)) {
            SWT.error(SWT.ERROR_INVALID_RANGE);
        }
        for (int i = start; i <= end; i++) {
            items[i].clear();
        }
        updateHorizontalBar();
        redrawItems(start, end, false);
    }

    /**
     * Clears the items at the given zero-relative indices in the receiver.
     * The text, icon and other attributes of the items are set to their default
     * values.  If the table was created with the <code>SWT.VIRTUAL</code> style,
     * these attributes are requested again as needed.
     *
     * @param indices the array of indices of the items
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     *    <li>ERROR_NULL_ARGUMENT - if the indices array is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see SWT#VIRTUAL
     * @see SWT#SetData
     *
     * @since 3.0
     */
    public void clear(int[] indices) {
        checkWidget();
        if (indices == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (indices.length == 0)
            return;
        for (int i = 0; i < indices.length; i++) {
            if (!(0 <= indices[i] && indices[i] < itemsCount)) {
                SWT.error(SWT.ERROR_INVALID_RANGE);
            }
        }

        for (int index : indices) {
            items[index].clear();
        }
        updateHorizontalBar();
        for (int index : indices) {
            redrawItem(index, false);
        }
    }

    /**
     * Clears all the items in the receiver. The text, icon and other
     * attributes of the items are set to their default values. If the
     * table was created with the <code>SWT.VIRTUAL</code> style, these
     * attributes are requested again as needed.
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see SWT#VIRTUAL
     * @see SWT#SetData
     *
     * @since 3.0
     */
    public void clearAll() {
        checkWidget();
        clear(0, itemsCount - 1);
    }

    /*
     * Returns the ORDERED index of the column that the specified x falls within,
     * or -1 if the x lies to the right of the last column.
     */
    int computeColumnIntersect(int x, int startColumn) {
        CTableColumn[] orderedColumns = getOrderedColumns();
        if (orderedColumns.length - 1 < startColumn)
            return -1;
        int rightX = orderedColumns[startColumn].getX();
        for (int i = startColumn; i < orderedColumns.length; i++) {
            rightX += orderedColumns[i].width;
            if (x < rightX)
                return i;
        }
        return -1;
    }

    @Override
    public Point computeSize(int wHint, int hHint, boolean changed) {
        checkWidget();
        int width = 0, height = 0;
        if (wHint != SWT.DEFAULT) {
            width = wHint;
        } else {
            if (columns.length == 0) {
                for (int i = 0; i < itemsCount; i++) {
                    Rectangle itemBounds = items[i].getBounds(false);
                    width = Math.max(width, itemBounds.x + itemBounds.width);
                }
            } else {
                CTableColumn[] orderedColumns = getOrderedColumns();
                CTableColumn lastColumn = orderedColumns[orderedColumns.length - 1];
                width = lastColumn.getX() + lastColumn.width;
            }
        }
        if (hHint != SWT.DEFAULT) {
            height = hHint;
        } else {
            height = getHeaderHeight() + itemsCount * itemHeight;
        }
        Rectangle result = computeTrim(0, 0, width, height);
        return new Point(result.width, result.height);
    }

    void createItem(CTableColumn column, int index) {
        CTableColumn[] newColumns = new CTableColumn[columns.length + 1];
        System.arraycopy(columns, 0, newColumns, 0, index);
        newColumns[index] = column;
        System.arraycopy(columns, index, newColumns, index + 1, columns.length - index);
        columns = newColumns;

        if (orderedColumns != null) {
            int insertIndex = 0;
            if (index > 0) {
                insertIndex = columns[index - 1].getOrderIndex() + 1;
            }
            CTableColumn[] newOrderedColumns = new CTableColumn[orderedColumns.length + 1];
            System.arraycopy(orderedColumns, 0, newOrderedColumns, 0, insertIndex);
            newOrderedColumns[insertIndex] = column;
            System.arraycopy(orderedColumns, insertIndex, newOrderedColumns, insertIndex + 1,
                    orderedColumns.length - insertIndex);
            orderedColumns = newOrderedColumns;
        }

        /* allow all items to update their internal structures accordingly */
        for (int i = 0; i < itemsCount; i++) {
            items[i].addColumn(column);
        }

        /* existing items become hidden when going from 0 to 1 column (0 width) */
        if (columns.length == 1 && itemsCount > 0) {
            redrawFromItemDownwards(topIndex);
        } else {
            /* checkboxes become hidden when creating a column with index == orderedIndex == 0 (0 width) */
            if (itemsCount > 0 && (getStyle() & SWT.CHECK) != 0 && index == 0 && column.getOrderIndex() == 0) {
                redrawFromItemDownwards(topIndex);
            }
        }

        /* Columns were added, so notify the accessible. */
        int[] eventData = new int[5];
        eventData[0] = ACC.INSERT;
        eventData[1] = 0;
        eventData[2] = 0;
        eventData[3] = index;
        eventData[4] = 1;
        getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);
    }

    void createItem(CTableItem item) {
        int index = item.index;
        if (itemsCount == items.length) {
            int grow = drawCount <= 0 ? 4 : Math.max(4, items.length * 3 / 2);
            CTableItem[] newItems = new CTableItem[items.length + grow];
            System.arraycopy(items, 0, newItems, 0, items.length);
            items = newItems;
        }
        if (index != itemsCount) {
            /* new item is not at end of list, so shift other items right to create space for it */
            System.arraycopy(items, index, items, index + 1, itemsCount - index);
        }
        items[index] = item;
        itemsCount++;

        /* update the index for items bumped down by this new item */
        for (int i = index + 1; i < itemsCount; i++) {
            items[i].index = i;
        }

        /* Rows were added, so notify the accessible. */
        int[] eventData = new int[5];
        eventData[0] = ACC.INSERT;
        eventData[1] = index;
        eventData[2] = 1;
        eventData[3] = 0;
        eventData[4] = 0;
        getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);

        /* update scrollbars */
        updateVerticalBar();
        Rectangle bounds = item.getBounds(false);
        int rightX = bounds.x + bounds.width;
        updateHorizontalBar(rightX, rightX);
        /*
         * If new item is above viewport then adjust topIndex and the vertical
         * scrollbar so that the current viewport items will not change.
         */
        if (item.index < topIndex) {
            topIndex++;
            ScrollBar vBar = getVerticalBar();
            if (vBar != null)
                vBar.setSelection(topIndex);
            return;
        }
        /*
         * If this is the first item and the receiver has focus then its boundary
         * focus ring must be removed.
         */
        if (itemsCount == 1 && isFocusControl()) {
            focusItem = item;
            redraw();
            return;
        }
        if (item.isInViewport()) {
            redrawFromItemDownwards(index);
        }
    }

    /**
     * Deselects the item at the given zero-relative index in the receiver.
     * If the item at the index was already deselected, it remains
     * deselected. Indices that are out of range are ignored.
     *
     * @param index the index of the item to deselect
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void deselect(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            return;
        CTableItem item = items[index];
        int selectIndex = getSelectionIndex(item);
        if (selectIndex == -1)
            return;

        CTableItem[] newSelectedItems = new CTableItem[selectedItems.length - 1];
        System.arraycopy(selectedItems, 0, newSelectedItems, 0, selectIndex);
        System.arraycopy(selectedItems, selectIndex + 1, newSelectedItems, selectIndex,
                newSelectedItems.length - selectIndex);
        selectedItems = newSelectedItems;

        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            redrawItem(item.index, false);
        }
        getAccessible().selectionChanged();
    }

    /**
     * Deselects the items at the given zero-relative indices in the receiver.
     * If the item at the given zero-relative index in the receiver
     * is selected, it is deselected.  If the item at the index
     * was not selected, it remains deselected.  The range of the
     * indices is inclusive. Indices that are out of range are ignored.
     *
     * @param start the start index of the items to deselect
     * @param end the end index of the items to deselect
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void deselect(int start, int end) {
        checkWidget();
        if (start == 0 && end == itemsCount - 1) {
            deselectAll();
        } else {
            start = Math.max(start, 0);
            end = Math.min(end, itemsCount - 1);
            for (int i = start; i <= end; i++) {
                deselect(i);
            }
        }
    }

    /**
     * Deselects the items at the given zero-relative indices in the receiver.
     * If the item at the given zero-relative index in the receiver
     * is selected, it is deselected.  If the item at the index
     * was not selected, it remains deselected. Indices that are out
     * of range and duplicate indices are ignored.
     *
     * @param indices the array of indices for the items to deselect
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the set of indices is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void deselect(int[] indices) {
        checkWidget();
        if (indices == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (indices.length == 0)
            return;
        for (int index : indices) {
            deselect(index);
        }
    }

    /**
     * Deselects all selected items in the receiver.
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void deselectAll() {
        checkWidget();
        CTableItem[] oldSelection = selectedItems;
        selectedItems = new CTableItem[0];
        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            for (CTableItem element : oldSelection) {
                redrawItem(element.index, true);
            }
        }
        for (CTableItem element : oldSelection) {
            element.getAccessible(getAccessible(), 0).selectionChanged();
        }
        if (oldSelection.length > 0)
            getAccessible().selectionChanged();
    }

    void deselectItem(CTableItem item) {
        int index = getSelectionIndex(item);
        if (index == -1)
            return;
        CTableItem[] newSelectedItems = new CTableItem[selectedItems.length - 1];
        System.arraycopy(selectedItems, 0, newSelectedItems, 0, index);
        System.arraycopy(selectedItems, index + 1, newSelectedItems, index, newSelectedItems.length - index);
        selectedItems = newSelectedItems;
        item.getAccessible(getAccessible(), 0).selectionChanged();
    }

    void destroyItem(CTableColumn column) {
        headerHideToolTip();
        int index = column.getIndex();
        int orderedIndex = column.getOrderIndex();

        CTableColumn[] newColumns = new CTableColumn[columns.length - 1];
        System.arraycopy(columns, 0, newColumns, 0, index);
        System.arraycopy(columns, index + 1, newColumns, index, newColumns.length - index);
        columns = newColumns;

        if (orderedColumns != null) {
            if (columns.length < 2) {
                orderedColumns = null;
            } else {
                int removeIndex = column.getOrderIndex();
                CTableColumn[] newOrderedColumns = new CTableColumn[orderedColumns.length - 1];
                System.arraycopy(orderedColumns, 0, newOrderedColumns, 0, removeIndex);
                System.arraycopy(orderedColumns, removeIndex + 1, newOrderedColumns, removeIndex,
                        newOrderedColumns.length - removeIndex);
                orderedColumns = newOrderedColumns;
            }
        }

        /* ensure that column 0 always has left-alignment */
        if (index == 0 && columns.length > 0) {
            int style = columns[0].getStyle();
            style |= SWT.LEFT;
            style &= ~(SWT.CENTER | SWT.RIGHT);
            columns[0].setStyle(style);
        }

        /* allow all items to update their internal structures accordingly */
        for (int i = 0; i < itemsCount; i++) {
            items[i].removeColumn(column, index);
        }

        /* update horizontal scrollbar */
        int lastColumnIndex = columns.length - 1;
        if (lastColumnIndex < 0) { /* no more columns */
            updateHorizontalBar();
        } else {
            int newWidth = 0;
            for (CTableColumn column2 : columns) {
                newWidth += column2.width;
            }
            ScrollBar hBar = getHorizontalBar();
            if (hBar != null) {
                hBar.setMaximum(newWidth);
                hBar.setVisible(clientArea.width < newWidth);
            }
            int selection = hBar.getSelection();
            if (selection != horizontalOffset) {
                horizontalOffset = selection;
                redraw();
                if (header.isVisible() && drawCount <= 0)
                    header.redraw();
            }
        }
        CTableColumn[] orderedColumns = getOrderedColumns();
        for (int i = orderedIndex; i < orderedColumns.length; i++) {
            if (!orderedColumns[i].isDisposed()) {
                orderedColumns[i].notifyListeners(SWT.Move, new Event());
            }
        }

        int[] eventData = new int[5];
        eventData[0] = ACC.DELETE;
        eventData[1] = 0;
        eventData[2] = 0;
        eventData[3] = index;
        eventData[4] = 1;
        getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);

        if (sortColumn == column) {
            sortColumn = null;
        }
    }

    /*
     * Allows the Table to update internal structures it has that may contain the
     * item being destroyed.
     */
    void destroyItem(CTableItem item) {
        if (item == focusItem)
            reassignFocus();

        int index = item.index;
        Rectangle bounds = item.getBounds(false);
        int rightX = bounds.x + bounds.width;

        if (index != itemsCount - 1) {
            /* item is not at end of items list, so must shift items left to reclaim its slot */
            System.arraycopy(items, index + 1, items, index, itemsCount - index - 1);
            items[itemsCount - 1] = null;
        } else {
            items[index] = null; /* last item, so no array copy needed */
        }
        itemsCount--;

        if (drawCount <= 0 && items.length - itemsCount == 4) {
            /* shrink the items array */
            CTableItem[] newItems = new CTableItem[itemsCount];
            System.arraycopy(items, 0, newItems, 0, newItems.length);
            items = newItems;
        }

        /* update the index on affected items */
        for (int i = index; i < itemsCount; i++) {
            items[i].index = i;
        }
        item.index = -1;

        int oldTopIndex = topIndex;
        updateVerticalBar();
        updateHorizontalBar(0, -rightX);
        /*
         * If destroyed item is above viewport then adjust topIndex and the vertical
         * scrollbar so that the current viewport items will not change.
         */
        if (index < topIndex) {
            topIndex = oldTopIndex - 1;
            ScrollBar vBar = getVerticalBar();
            if (vBar != null)
                vBar.setSelection(topIndex);
        }

        /* selectedItems array */
        if (item.isSelected()) {
            int selectionIndex = getSelectionIndex(item);
            CTableItem[] newSelectedItems = new CTableItem[selectedItems.length - 1];
            System.arraycopy(selectedItems, 0, newSelectedItems, 0, selectionIndex);
            System.arraycopy(selectedItems, selectionIndex + 1, newSelectedItems, selectionIndex,
                    newSelectedItems.length - selectionIndex);
            selectedItems = newSelectedItems;
        }
        if (item == anchorItem)
            anchorItem = null;
        if (item == lastClickedItem)
            lastClickedItem = null;
        /*
         * If this was the last item and the receiver has focus then its boundary
         * focus ring must be redrawn.
         */
        if (itemsCount == 0 && isFocusControl()) {
            redraw();
        }

        int[] eventData = new int[5];
        eventData[0] = ACC.DELETE;
        eventData[1] = index;
        eventData[2] = 1;
        eventData[3] = 0;
        eventData[4] = 0;
        getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);
    }

    Image getArrowDownImage() {
        return (Image) display.getData(ID_ARROWDOWN);
    }

    Image getArrowUpImage() {
        return (Image) display.getData(ID_ARROWUP);
    }

    int getCellPadding() {
        return MARGIN_CELL + WIDTH_CELL_HIGHLIGHT;
    }

    Image getCheckmarkImage() {
        return (Image) display.getData(ID_CHECKMARK);
    }

    @Override
    public Control[] getChildren() {
        checkWidget();
        Control[] controls = super.getChildren();
        if (header == null)
            return controls;
        Control[] result = new Control[controls.length - 1];
        /* remove the Header from the returned set of children */
        int index = 0;
        for (Control control : controls) {
            if (control != header) {
                result[index++] = control;
            }
        }
        return result;
    }

    /**
     * Returns the column at the given, zero-relative index in the
     * receiver. Throws an exception if the index is out of range.
     * Columns are returned in the order that they were created.
     * If no <code>TableColumn</code>s were created by the programmer,
     * this method will throw <code>ERROR_INVALID_RANGE</code> despite
     * the fact that a single column of data may be visible in the table.
     * This occurs when the programmer uses the table like a list, adding
     * items but never creating a column.
     *
     * @param index the index of the column to return
     * @return the column at the given index
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#getColumnOrder()
     * @see CTable#setColumnOrder(int[])
     * @see CTableColumn#getMoveable()
     * @see CTableColumn#setMoveable(boolean)
     * @see SWT#Move
     */
    public CTableColumn getColumn(int index) {
        checkWidget();
        if (!(0 <= index && index < columns.length))
            SWT.error(SWT.ERROR_INVALID_RANGE);
        return columns[index];
    }

    /**
     * Returns the number of columns contained in the receiver.
     * If no <code>TableColumn</code>s were created by the programmer,
     * this value is zero, despite the fact that visually, one column
     * of items may be visible. This occurs when the programmer uses
     * the table like a list, adding items but never creating a column.
     *
     * @return the number of columns
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getColumnCount() {
        checkWidget();
        return columns.length;
    }

    /**
     * Returns an array of zero-relative integers that map
     * the creation order of the receiver's items to the
     * order in which they are currently being displayed.
     * <p>
     * Specifically, the indices of the returned array represent
     * the current visual order of the items, and the contents
     * of the array represent the creation order of the items.
     * </p><p>
     * Note: This is not the actual structure used by the receiver
     * to maintain its list of items, so modifying the array will
     * not affect the receiver.
     * </p>
     *
     * @return the current visual order of the receiver's items
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#setColumnOrder(int[])
     * @see CTableColumn#getMoveable()
     * @see CTableColumn#setMoveable(boolean)
     * @see SWT#Move
     *
     * @since 3.1
     */
    public int[] getColumnOrder() {
        checkWidget();
        int[] result = new int[columns.length];
        if (orderedColumns != null) {
            for (int i = 0; i < result.length; i++) {
                result[i] = orderedColumns[i].getIndex();
            }
        } else {
            for (int i = 0; i < columns.length; i++) {
                result[i] = i;
            }
        }
        return result;
    }

    /**
     * Returns an array of <code>TableColumn</code>s which are the
     * columns in the receiver.  Columns are returned in the order
     * that they were created.  If no <code>TableColumn</code>s were
     * created by the programmer, the array is empty, despite the fact
     * that visually, one column of items may be visible. This occurs
     * when the programmer uses the table like a list, adding items but
     * never creating a column.
     * <p>
     * Note: This is not the actual structure used by the receiver
     * to maintain its list of items, so modifying the array will
     * not affect the receiver.
     * </p>
     *
     * @return the items in the receiver
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#getColumnOrder()
     * @see CTable#setColumnOrder(int[])
     * @see CTableColumn#getMoveable()
     * @see CTableColumn#setMoveable(boolean)
     * @see SWT#Move
     */
    public CTableColumn[] getColumns() {
        checkWidget();
        CTableColumn[] result = new CTableColumn[columns.length];
        System.arraycopy(columns, 0, result, 0, columns.length);
        return result;
    }

    Image getGrayUncheckedImage() {
        return (Image) display.getData(ID_GRAYUNCHECKED);
    }

    /**
     * Returns the width in pixels of a grid line.
     *
     * @return the width of a grid line in pixels
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getGridLineWidth() {
        checkWidget();
        return 1;
    }

    /**
     * Returns the height of the receiver's header
     *
     * @return the height of the header or zero if the header is not visible
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @since 2.0
     */
    public int getHeaderHeight() {
        checkWidget();
        if (!header.getVisible())
            return 0;
        return header.getSize().y;
    }

    int getHeaderPadding() {
        return MARGIN_CELL + WIDTH_HEADER_SHADOW;
    }

    /**
     * Returns <code>true</code> if the receiver's header is visible,
     * and <code>false</code> otherwise.
     * <p>
     * If one of the receiver's ancestors is not visible or some
     * other condition makes the receiver not visible, this method
     * may still indicate that it is considered visible even though
     * it may not actually be showing.
     * </p>
     *
     * @return the receiver's header's visibility state
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public boolean getHeaderVisible() {
        checkWidget();
        return header.getVisible();
    }

    /**
     * Returns the item at the given, zero-relative index in the
     * receiver. Throws an exception if the index is out of range.
     *
     * @param index the index of the item to return
     * @return the item at the given index
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public CTableItem getItem(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            SWT.error(SWT.ERROR_INVALID_RANGE);
        return items[index];
    }

    /**
     * Returns the item at the given point in the receiver
     * or null if no such item exists. The point is in the
     * coordinate system of the receiver.
     * <p>
     * The item that is returned represents an item that could be selected by the user.
     * For example, if selection only occurs in items in the first column, then null is
     * returned if the point is outside of the item.
     * Note that the SWT.FULL_SELECTION style hint, which specifies the selection policy,
     * determines the extent of the selection.
     * </p>
     *
     * @param point the point used to locate the item
     * @return the item at the given point, or null if the point is not in a selectable item
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the point is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public CTableItem getItem(Point point) {
        checkWidget();
        if (point == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        int index = (point.y - getHeaderHeight()) / itemHeight + topIndex;
        if (!(0 <= index && index < itemsCount))
            return null; /* below the last item */
        CTableItem result = items[index];
        if (!result.getHitBounds().contains(point))
            return null; /* considers the x value */
        return result;
    }

    /**
     * Returns the number of items contained in the receiver.
     *
     * @return the number of items
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getItemCount() {
        checkWidget();
        return itemsCount;
    }

    /**
     * Returns the height of the area which would be used to
     * display <em>one</em> of the items in the receiver.
     *
     * @return the height of one item
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getItemHeight() {
        checkWidget();
        return itemHeight;
    }

    /**
     * Returns a (possibly empty) array of <code>TableItem</code>s which
     * are the items in the receiver.
     * <p>
     * Note: This is not the actual structure used by the receiver
     * to maintain its list of items, so modifying the array will
     * not affect the receiver.
     * </p>
     *
     * @return the items in the receiver
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public CTableItem[] getItems() {
        checkWidget();
        CTableItem[] result = new CTableItem[itemsCount];
        System.arraycopy(items, 0, result, 0, itemsCount);
        return result;
    }

    /*
     * Returns the current y-coordinate that the specified item should have.
     */
    int getItemY(CTableItem item) {
        return (item.index - topIndex) * itemHeight + getHeaderHeight();
    }

    /**
     * Returns <code>true</code> if the receiver's lines are visible,
     * and <code>false</code> otherwise. Note that some platforms draw
     * grid lines while others may draw alternating row colors.
     * <p>
     * If one of the receiver's ancestors is not visible or some
     * other condition makes the receiver not visible, this method
     * may still indicate that it is considered visible even though
     * it may not actually be showing.
     * </p>
     *
     * @return the visibility state of the lines
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public boolean getLinesVisible() {
        checkWidget();
        return linesVisible;
    }

    CTableColumn[] getOrderedColumns() {
        if (orderedColumns != null)
            return orderedColumns;
        return columns;
    }

    /**
     * Returns an array of <code>TableItem</code>s that are currently
     * selected in the receiver. The order of the items is unspecified.
     * An empty array indicates that no items are selected.
     * <p>
     * Note: This is not the actual structure used by the receiver
     * to maintain its selection, so modifying the array will
     * not affect the receiver.
     * </p>
     * @return an array representing the selection
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public CTableItem[] getSelection() {
        checkWidget();
        CTableItem[] result = new CTableItem[selectedItems.length];
        System.arraycopy(selectedItems, 0, result, 0, selectedItems.length);
        sortAscent(result);
        return result;
    }

    /**
     * Returns the number of selected items contained in the receiver.
     *
     * @return the number of selected items
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getSelectionCount() {
        checkWidget();
        return selectedItems.length;
    }

    /**
     * Returns the zero-relative index of the item which is currently
     * selected in the receiver, or -1 if no item is selected.
     *
     * @return the index of the selected item
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getSelectionIndex() {
        checkWidget();
        if (selectedItems.length == 0)
            return -1;
        return selectedItems[0].index;
    }

    /*
     * Returns the index of the argument in the receiver's array of currently-
     * selected items, or -1 if the item is not currently selected.
     */
    int getSelectionIndex(CTableItem item) {
        for (int i = 0; i < selectedItems.length; i++) {
            if (selectedItems[i] == item)
                return i;
        }
        return -1;
    }

    /**
     * Returns the zero-relative indices of the items which are currently
     * selected in the receiver. The order of the indices is unspecified.
     * The array is empty if no items are selected.
     * <p>
     * Note: This is not the actual structure used by the receiver
     * to maintain its selection, so modifying the array will
     * not affect the receiver.
     * </p>
     * @return the array of indices of the selected items
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int[] getSelectionIndices() {
        checkWidget();
        int[] result = new int[selectedItems.length];
        for (int i = 0; i < selectedItems.length; i++) {
            result[i] = selectedItems[i].index;
        }
        sortAscent(result);
        return result;
    }

    /**
     * Returns the column which shows the sort indicator for
     * the receiver. The value may be null if no column shows
     * the sort indicator.
     *
     * @return the sort indicator
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see #setSortColumn(CTableColumn)
     *
     * @since 3.2
     */
    public CTableColumn getSortColumn() {
        checkWidget();
        return sortColumn;
    }

    /**
     * Returns the direction of the sort indicator for the receiver.
     * The value will be one of <code>UP</code>, <code>DOWN</code>
     * or <code>NONE</code>.
     *
     * @return the sort direction
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see #setSortDirection(int)
     *
     * @since 3.2
     */
    public int getSortDirection() {
        checkWidget();
        return sortDirection;
    }

    /**
     * Returns the zero-relative index of the item which is currently
     * at the top of the receiver. This index can change when items are
     * scrolled or new items are added or removed.
     *
     * @return the index of the top item
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int getTopIndex() {
        checkWidget();
        return topIndex;
    }

    Image getUncheckedImage() {
        return (Image) display.getData(ID_UNCHECKED);
    }

    void handleEvents(Event event) {
        switch (event.type) {
        case SWT.Paint:
            if (event.widget == header) {
                headerOnPaint(event);
            } else {
                onPaint(event);
            }
            break;
        case SWT.MenuDetect: {
            notifyListeners(SWT.MenuDetect, event);
            break;
        }
        case SWT.MouseDown:
            if (event.widget == header) {
                headerOnMouseDown(event);
            } else {
                onMouseDown(event);
            }
            break;
        case SWT.MouseUp:
            if (event.widget == header) {
                headerOnMouseUp(event);
            } else {
                onMouseUp(event);
            }
            break;
        case SWT.MouseHover:
            headerOnMouseHover(event);
            break;
        case SWT.MouseMove:
            headerOnMouseMove(event);
            break;
        case SWT.MouseDoubleClick:
            if (event.widget == header) {
                headerOnMouseDoubleClick(event);
            } else {
                onMouseDoubleClick(event);
            }
            break;
        case SWT.MouseExit:
            headerOnMouseExit();
            break;
        case SWT.Dispose:
            onDispose(event);
            break;
        case SWT.KeyDown:
            onKeyDown(event);
            break;
        case SWT.Resize:
            onResize(event);
            break;
        case SWT.Selection:
            if (event.widget == getHorizontalBar()) {
                onScrollHorizontal(event);
            }
            if (event.widget == getVerticalBar()) {
                onScrollVertical(event);
            }
            break;
        case SWT.FocusOut:
            onFocusOut();
            break;
        case SWT.FocusIn:
            onFocusIn();
            break;
        case SWT.Traverse:
            switch (event.detail) {
            case SWT.TRAVERSE_ESCAPE:
            case SWT.TRAVERSE_RETURN:
            case SWT.TRAVERSE_TAB_NEXT:
            case SWT.TRAVERSE_TAB_PREVIOUS:
            case SWT.TRAVERSE_PAGE_NEXT:
            case SWT.TRAVERSE_PAGE_PREVIOUS:
                event.doit = true;
                break;
            }
            break;
        }
    }

    String headerGetToolTip(int x) {
        if (resizeColumn != null)
            return null;
        int orderedIndex = computeColumnIntersect(x, 0);
        if (orderedIndex == -1)
            return null;
        CTableColumn[] orderedColumns = getOrderedColumns();
        CTableColumn column = orderedColumns[orderedIndex];
        if (column.toolTipText == null)
            return null;

        /* no tooltip should appear if the hover is at a column resize opportunity */
        int columnX = column.getX();
        if (orderedIndex > 0 && orderedColumns[orderedIndex - 1].resizable) {
            /* left column bound is resizable */
            if (x - columnX <= TOLLERANCE_COLUMNRESIZE)
                return null;
        }
        if (column.resizable) {
            /* right column bound is resizable */
            int columnRightX = columnX + column.width;
            if (columnRightX - x <= TOLLERANCE_COLUMNRESIZE)
                return null;
        }
        return removeMnemonics(column.toolTipText);
    }

    void headerHideToolTip() {
        if (toolTipShell == null)
            return;
        for (int toolTipEvent : toolTipEvents) {
            header.removeListener(toolTipEvent, toolTipListener);
        }
        toolTipShell.dispose();
        toolTipShell = null;
        toolTipLabel = null;
    }

    void headerOnMouseDoubleClick(Event event) {
        if (!isFocusControl())
            setFocus();
        if (columns.length == 0)
            return;
        CTableColumn[] orderedColumns = getOrderedColumns();
        int x = -horizontalOffset;
        for (int i = 0; i < orderedColumns.length; i++) {
            CTableColumn column = orderedColumns[i];
            x += column.width;
            if (event.x < x) {
                /* found the clicked column */
                CTableColumn packColumn = null;
                if (x - event.x <= TOLLERANCE_COLUMNRESIZE) {
                    /* clicked on column bound for this column */
                    packColumn = column;
                } else {
                    if (i > 0 && event.x - column.getX() <= TOLLERANCE_COLUMNRESIZE) {
                        /* clicked on column bound that applies to previous column */
                        packColumn = orderedColumns[i - 1];
                    }
                }
                if (packColumn != null) {
                    packColumn.pack();
                    resizeColumn = null;
                    if (Math.abs(packColumn.getX() + packColumn.width - event.x) > TOLLERANCE_COLUMNRESIZE) {
                        /* column separator has relocated away from pointer location */
                        setCursor(null);
                    }
                    return;
                }
                /* did not click on column separator, so just fire column event */
                Event newEvent = new Event();
                newEvent.widget = column;
                column.notifyListeners(SWT.DefaultSelection, newEvent);
                return;
            }
        }
    }

    void headerOnMouseDown(Event event) {
        if (event.button != 1)
            return;
        CTableColumn[] orderedColumns = getOrderedColumns();
        int x = -horizontalOffset;
        for (CTableColumn column : orderedColumns) {
            x += column.width;
            /* if close to a resizable column separator line then begin column resize */
            if (column.resizable && Math.abs(x - event.x) <= TOLLERANCE_COLUMNRESIZE) {
                resizeColumn = column;
                resizeColumnX = x;
                return;
            }
            /*
             * If within column but not near separator line then start column drag
             * if column is moveable, or just fire column Selection otherwise.
             */
            if (event.x < x) {
                if (column.moveable) {
                    /* open tracker on the dragged column's header cell */
                    int columnX = column.getX();
                    int pointerOffset = event.x - columnX;
                    headerHideToolTip();
                    Tracker tracker = new Tracker(this, SWT.NONE);
                    tracker.setRectangles(
                            new Rectangle[] { new Rectangle(columnX, 0, column.width, getHeaderHeight()) });
                    if (!tracker.open())
                        return; /* cancelled */
                    /* determine which column was dragged onto */
                    Rectangle result = tracker.getRectangles()[0];
                    int pointerX = result.x + pointerOffset;
                    if (pointerX < 0)
                        return; /* dragged too far left */
                    x = -horizontalOffset;
                    for (int destIndex = 0; destIndex < orderedColumns.length; destIndex++) {
                        CTableColumn destColumn = orderedColumns[destIndex];
                        x += destColumn.width;
                        if (pointerX < x) {
                            int oldIndex = column.getOrderIndex();
                            if (destIndex == oldIndex) { /* dragged onto self */
                                Event newEvent = new Event();
                                newEvent.widget = column;
                                column.notifyListeners(SWT.Selection, newEvent);
                                return;
                            }
                            int leftmostIndex = Math.min(destIndex, oldIndex);
                            int[] oldOrder = getColumnOrder();
                            int[] newOrder = new int[oldOrder.length];
                            System.arraycopy(oldOrder, 0, newOrder, 0, leftmostIndex);
                            if (leftmostIndex == oldIndex) {
                                /* column moving to the right */
                                System.arraycopy(oldOrder, oldIndex + 1, newOrder, oldIndex, destIndex - oldIndex);
                            } else {
                                /* column moving to the left */
                                System.arraycopy(oldOrder, destIndex, newOrder, destIndex + 1,
                                        oldIndex - destIndex);
                            }
                            newOrder[destIndex] = oldOrder[oldIndex];
                            int rightmostIndex = Math.max(destIndex, oldIndex);
                            System.arraycopy(oldOrder, rightmostIndex + 1, newOrder, rightmostIndex + 1,
                                    newOrder.length - rightmostIndex - 1);
                            setColumnOrder(newOrder);
                            return;
                        }
                    }
                    return; /* dragged too far right */
                }
                /* column is not moveable */
                Event newEvent = new Event();
                newEvent.widget = column;
                column.notifyListeners(SWT.Selection, newEvent);
                return;
            }
        }
    }

    void headerOnMouseExit() {
        if (resizeColumn != null)
            return;
        setCursor(null); /* ensure that a column resize cursor does not escape */
    }

    void headerOnMouseHover(Event event) {
        headerShowToolTip(event.x);
    }

    void headerOnMouseMove(Event event) {
        if (resizeColumn == null) {
            /* not currently resizing a column */
            for (CTableColumn column : columns) {
                int x = column.getX() + column.width;
                if (Math.abs(x - event.x) <= TOLLERANCE_COLUMNRESIZE) {
                    if (column.resizable) {
                        setCursor(display.getSystemCursor(SWT.CURSOR_SIZEWE));
                    } else {
                        setCursor(null);
                    }
                    return;
                }
            }
            setCursor(null);
            return;
        }

        /* currently resizing a column */

        /* don't allow the resize x to move left of the column's x position */
        if (event.x <= resizeColumn.getX())
            return;

        /* redraw the resizing line at its new location */
        GC gc = new GC(this);
        gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
        int lineHeight = clientArea.height;
        redraw(resizeColumnX - 1, 0, 1, lineHeight, false);
        resizeColumnX = event.x;
        gc.drawLine(resizeColumnX - 1, 0, resizeColumnX - 1, lineHeight);
        gc.dispose();
    }

    void headerOnMouseUp(Event event) {
        if (resizeColumn == null)
            return; /* not resizing a column */

        /* remove the resize line */
        GC gc = new GC(this);
        redraw(resizeColumnX - 1, 0, 1, clientArea.height, false);
        gc.dispose();

        int newWidth = resizeColumnX - resizeColumn.getX();
        if (newWidth != resizeColumn.width) {
            setCursor(null);
            updateColumnWidth(resizeColumn, newWidth);
        }
        resizeColumnX = -1;
        resizeColumn = null;
    }

    void headerOnPaint(Event event) {
        CTableColumn[] orderedColumns = getOrderedColumns();
        int numColumns = orderedColumns.length;
        GC gc = event.gc;
        Rectangle clipping = gc.getClipping();
        int startColumn = -1, endColumn = -1;
        if (numColumns > 0) {
            startColumn = computeColumnIntersect(clipping.x, 0);
            if (startColumn != -1) { /* the clip x is within a column's bounds */
                endColumn = computeColumnIntersect(clipping.x + clipping.width, startColumn);
                if (endColumn == -1)
                    endColumn = numColumns - 1;
            }
        } else {
            startColumn = endColumn = 0;
        }

        /* paint the column header shadow that spans the full header width */
        Point headerSize = header.getSize();
        headerPaintHShadows(gc, 0, 0, headerSize.x, headerSize.y);

        /* if all damage is to the right of the last column then finished */
        if (startColumn == -1)
            return;

        /* paint each of the column headers */
        if (numColumns == 0)
            return; /* no headers to paint */
        for (int i = startColumn; i <= endColumn; i++) {
            headerPaintVShadows(gc, orderedColumns[i].getX(), 0, orderedColumns[i].width, headerSize.y);
            orderedColumns[i].paint(gc);
        }
    }

    void headerPaintHShadows(GC gc, int x, int y, int width, int height) {
        gc.setClipping(x, y, width, height);
        int endX = x + width;
        gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW));
        gc.drawLine(x, y, endX, y); /* highlight shadow */
        gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
        gc.drawLine(x, height - 2, endX, height - 2); /* lowlight shadow */
        gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW));
        gc.drawLine(x, height - 1, endX, height - 1); /* outer shadow */
    }

    void headerPaintVShadows(GC gc, int x, int y, int width, int height) {
        gc.setClipping(x, y, width, height);
        int endX = x + width;
        gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW));
        gc.drawLine(x, y, x, y + height - 1); /* highlight shadow */
        gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW));
        gc.drawLine(endX - 2, y + 1, endX - 2, height - 2); /* light inner shadow */
        gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_DARK_SHADOW));
        gc.drawLine(endX - 1, y, endX - 1, height - 1); /* dark outer shadow */
    }

    void headerShowToolTip(int x) {
        String tooltip = headerGetToolTip(x);
        if (tooltip == null || tooltip.length() == 0)
            return;

        if (toolTipShell == null) {
            toolTipShell = new Shell(getShell(), SWT.ON_TOP | SWT.TOOL);
            toolTipLabel = new Label(toolTipShell, SWT.CENTER);
            Display display = toolTipShell.getDisplay();
            toolTipLabel.setForeground(display.getSystemColor(SWT.COLOR_INFO_FOREGROUND));
            toolTipLabel.setBackground(display.getSystemColor(SWT.COLOR_INFO_BACKGROUND));
            for (int toolTipEvent : toolTipEvents) {
                header.addListener(toolTipEvent, toolTipListener);
            }
        }
        if (headerUpdateToolTip(x)) {
            toolTipShell.setVisible(true);
        } else {
            headerHideToolTip();
        }
    }

    boolean headerUpdateToolTip(int x) {
        String tooltip = headerGetToolTip(x);
        if (tooltip == null || tooltip.length() == 0)
            return false;
        if (tooltip.equals(toolTipLabel.getText()))
            return true;

        toolTipLabel.setText(tooltip);
        CTableColumn column = getOrderedColumns()[computeColumnIntersect(x, 0)];
        toolTipShell.setData(Integer.valueOf(column.getIndex()));
        Point labelSize = toolTipLabel.computeSize(SWT.DEFAULT, SWT.DEFAULT, true);
        labelSize.x += 2;
        labelSize.y += 2;
        toolTipLabel.setSize(labelSize);
        toolTipShell.pack();
        /*
         * On some platforms, there is a minimum size for a shell
         * which may be greater than the label size.
         * To avoid having the background of the tip shell showing
         * around the label, force the label to fill the entire client area.
         */
        Rectangle area = toolTipShell.getClientArea();
        toolTipLabel.setSize(area.width, area.height);

        /* Position the tooltip and ensure it's not located off the screen */
        Point cursorLocation = getDisplay().getCursorLocation();
        int cursorHeight = 21; /* assuming cursor is 21x21 */
        Point size = toolTipShell.getSize();
        Rectangle rect = getMonitor().getBounds();
        Point pt = new Point(cursorLocation.x, cursorLocation.y + cursorHeight + 2);
        pt.x = Math.max(pt.x, rect.x);
        if (pt.x + size.x > rect.x + rect.width)
            pt.x = rect.x + rect.width - size.x;
        if (pt.y + size.y > rect.y + rect.height)
            pt.y = cursorLocation.y - 2 - size.y;
        toolTipShell.setLocation(pt);
        return true;
    }

    /**
     * Searches the receiver's list starting at the first column
     * (index 0) until a column is found that is equal to the
     * argument, and returns the index of that column. If no column
     * is found, returns -1.
     *
     * @param column the search column
     * @return the index of the column
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the column is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int indexOf(CTableColumn column) {
        checkWidget();
        if (column == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (column.parent != this)
            return -1;
        return column.getIndex();
    }

    /**
     * Searches the receiver's list starting at the first item
     * (index 0) until an item is found that is equal to the
     * argument, and returns the index of that item. If no item
     * is found, returns -1.
     *
     * @param item the search item
     * @return the index of the item
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the item is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public int indexOf(CTableItem item) {
        checkWidget();
        if (item == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (item.parent != this)
            return -1;
        return item.index;
    }

    void initAccessibility() {
        // TODO: does this all work if CTable is virtual?
        final Accessible accessibleTable = getAccessible();
        accessibleTable.addAccessibleListener(new AccessibleAdapter() {
            @Override
            public void getName(AccessibleEvent e) {
                /* CTables take their name from the preceding Label, if any. */
                Control[] siblings = getParent().getChildren();
                for (int i = 0; i < siblings.length; i++) {
                    if (i != 0 && siblings[i] == CTable.this) {
                        Control control = siblings[i - 1];
                        if (control instanceof Label) {
                            e.result = ((Label) control).getText();
                        }
                    }
                }
            }

            @Override
            public void getHelp(AccessibleEvent e) {
                /* A CTable's toolTip text (if any) can be used as its help text. */
                e.result = getToolTipText();
            }
        });
        accessibleTable.addAccessibleControlListener(new AccessibleControlAdapter() {
            /* Child IDs are assigned as follows:
             * - column header ids (if any) are numbered from 0 to columnCount - 1
             * - cell ids are numbered in row-major order (starting from columnCount if there are columns)
             * Accessibles are returned in getChild.
             */
            @Override
            public void getChild(AccessibleControlEvent e) {
                int childID = e.childID;
                if (childID == ACC.CHILDID_CHILD_AT_INDEX)
                    childID = e.detail; // childID == index
                if (columns.length > 0 && 0 <= childID && childID < columns.length) { // header cell
                    CTableColumn column = columns[childID];
                    e.accessible = column.getAccessible(accessibleTable);
                } else { // item cell
                    int columnCount = columns.length > 0 ? columns.length : 1;
                    if (columns.length > 0)
                        childID -= columnCount;
                    if (0 <= childID && childID < itemsCount * columnCount) {
                        int rowIndex = childID / columnCount;
                        int columnIndex = childID - rowIndex * columnCount;
                        e.accessible = items[rowIndex].getAccessible(accessibleTable, columnIndex);
                    }
                }
            }

            @Override
            public void getChildAtPoint(AccessibleControlEvent e) {
                Point point = toControl(e.x, e.y);
                if (columns.length > 0 && point.y < getHeaderHeight()) { // header cell
                    int columnIndex = computeColumnIntersect(point.x, 0);
                    if (columnIndex != -1) {
                        CTableColumn column = columns[columnIndex];
                        e.accessible = column.getAccessible(accessibleTable);
                    }
                } else { // item cell
                    int columnIndex = columns.length > 0 ? computeColumnIntersect(point.x, 0) : 0;
                    if (columnIndex != -1) {
                        int rowIndex = (point.y - getHeaderHeight()) / itemHeight + topIndex;
                        if (0 <= rowIndex && rowIndex < itemsCount) {
                            if (items[rowIndex].getHitBounds().contains(point)) { /* considers the x value */
                                e.accessible = items[rowIndex].getAccessible(accessibleTable, columnIndex);
                            }
                        }
                    }
                }
            }

            @Override
            public void getChildCount(AccessibleControlEvent e) {
                e.detail = columns.length > 0 ? columns.length + itemsCount * columns.length : itemsCount;
            }

            @Override
            public void getChildren(AccessibleControlEvent e) {
                int childIdCount = columns.length > 0 ? columns.length + itemsCount * columns.length : itemsCount;
                Object[] children = new Object[childIdCount];
                for (int i = 0; i < childIdCount; i++) {
                    children[i] = Integer.valueOf(i);
                }
                e.children = children;
            }

            @Override
            public void getFocus(AccessibleControlEvent e) {
                e.childID = isFocusControl() ? ACC.CHILDID_SELF : ACC.CHILDID_NONE;
            }

            @Override
            public void getLocation(AccessibleControlEvent e) {
                Rectangle location = null;
                Point pt = null;
                int childID = e.childID;
                if (childID == ACC.CHILDID_SELF) { // table
                    location = getBounds();
                    pt = getParent().toDisplay(location.x, location.y);
                } else if (columns.length > 0 && 0 <= childID && childID < columns.length) { // header cell
                    CTableColumn column = columns[childID];
                    location = new Rectangle(column.getX(), 0, column.getWidth(), getHeaderHeight());
                    pt = toDisplay(location.x, location.y);
                } else { // item cell
                    int columnCount = columns.length > 0 ? columns.length : 1;
                    if (columns.length > 0)
                        childID -= columnCount;
                    if (0 <= childID && childID < itemsCount * columnCount) {
                        int rowIndex = childID / columnCount;
                        int columnIndex = childID - rowIndex * columnCount;
                        location = items[rowIndex].getBounds(columnIndex);
                        pt = toDisplay(location.x, location.y);
                    }
                }
                if (location != null && pt != null) {
                    e.x = pt.x;
                    e.y = pt.y;
                    e.width = location.width;
                    e.height = location.height;
                }
            }

            @Override
            public void getRole(AccessibleControlEvent e) {
                e.detail = e.childID == ACC.CHILDID_SELF ? ACC.ROLE_TABLE : ACC.ROLE_TABLECELL;
            }

            @Override
            public void getSelection(AccessibleControlEvent e) {
                int columnCount = columns.length > 0 ? columns.length : 1;
                int[] selectionIndices = getSelectionIndices();
                Object[] selectedChildren = new Object[selectionIndices.length * columnCount];
                for (int i = 0; i < selectionIndices.length; i++) {
                    int row = selectionIndices[i];
                    for (int col = 0; col < columnCount; col++) {
                        selectedChildren[i] = Integer.valueOf(row * columnCount + col);
                    }
                }
                e.children = selectedChildren;
            }

            @Override
            public void getState(AccessibleControlEvent e) {
                int state = ACC.STATE_NORMAL;
                int childID = e.childID;
                if (childID == ACC.CHILDID_SELF) { // table
                    state |= ACC.STATE_FOCUSABLE;
                    if (isFocusControl()) {
                        state |= ACC.STATE_FOCUSED;
                    }
                } else if (columns.length > 0 && 0 <= childID && childID < columns.length) { // header cell
                    /* CTable does not support header cell focus or selection. */
                    state |= ACC.STATE_SIZEABLE;
                } else { // item cell
                    int columnCount = columns.length > 0 ? columns.length : 1;
                    if (columns.length > 0)
                        childID -= columnCount;
                    if (0 <= childID && childID < itemsCount * columnCount) {
                        /* CTable does not support cell selection (only row selection). */
                        int rowIndex = childID / columnCount;
                        state |= ACC.STATE_SELECTABLE;
                        if (isFocusControl()) {
                            state |= ACC.STATE_FOCUSABLE;
                        }
                        if (items[rowIndex].isSelected()) {
                            state |= ACC.STATE_SELECTED;
                            if (isFocusControl()) {
                                state |= ACC.STATE_FOCUSED;
                            }
                        }
                    }
                }
                e.detail = state;
            }
        });
        accessibleTable.addAccessibleTableListener(new AccessibleTableAdapter() {
            @Override
            public void deselectColumn(AccessibleTableEvent e) {
                /* CTable does not support column selection. */
            }

            @Override
            public void deselectRow(AccessibleTableEvent e) {
                deselect(e.row);
                e.result = ACC.OK;
            }

            @Override
            public void getCaption(AccessibleTableEvent e) {
                // TODO: What is a caption? How does it differ from name? Should app supply?
                e.result = "This is the Custom Table's Test Caption";
            }

            @Override
            public void getCell(AccessibleTableEvent e) {
                int index = e.row;
                if (0 <= index && index < itemsCount) {
                    CTableItem row = items[index];
                    index = e.column;
                    if (columns.length == 0 || 0 <= index && index < columns.length) {
                        e.accessible = row.getAccessible(accessibleTable, index);
                    }
                }
            }

            @Override
            public void getColumnCount(AccessibleTableEvent e) {
                int columnCount = columns.length > 0 ? columns.length : 1;
                e.count = columnCount;
            }

            @Override
            public void getColumnDescription(AccessibleTableEvent e) {
                // TODO: What is a description? How does it differ from name? Should app supply?
                e.result = "This is the Custom Table's Test Description for column " + e.column;
            }

            //      public void getColumnHeader(AccessibleTableEvent e) {
            //         e.accessible = header.getAccessible();
            //      }
            @Override
            public void getColumnHeaderCells(AccessibleTableEvent e) {
                if (columns.length == 0) {
                    /* The CTable is being used as a list, and there are no headers. */
                    e.accessibles = null;
                } else {
                    Accessible[] accessibles = new Accessible[columns.length];
                    for (int i = 0; i < columns.length; i++) {
                        CTableColumn column = columns[i];
                        accessibles[i] = column.getAccessible(accessibleTable);
                    }
                    e.accessibles = accessibles;
                }
            }

            @Override
            public void getRowCount(AccessibleTableEvent e) {
                e.count = itemsCount;
            }

            @Override
            public void getRowDescription(AccessibleTableEvent e) {
                // TODO: What is a description? How does it differ from name? Should app supply?
                e.result = "This is the Custom Table's Test Description for row " + e.row;
            }

            @Override
            public void getRowHeader(AccessibleTableEvent e) {
                /* CTable does not support row headers. */
            }

            @Override
            public void getSelectedCellCount(AccessibleTableEvent e) {
                int columnCount = columns.length > 0 ? columns.length : 1;
                e.count = selectedItems.length * columnCount;
            }

            @Override
            public void getSelectedCells(AccessibleTableEvent e) {
                int columnCount = columns.length > 0 ? columns.length : 1;
                Accessible[] accessibles = new Accessible[selectedItems.length * columnCount];
                for (int r = 0; r < selectedItems.length; r++) {
                    CTableItem row = selectedItems[r];
                    for (int c = 0; c < columnCount; c++)
                        accessibles[r + c] = row.getAccessible(accessibleTable, c);
                }
                e.accessibles = accessibles;
            }

            @Override
            public void getSelectedColumnCount(AccessibleTableEvent e) {
                e.count = 0; /* CTable does not support column selection. */
            }

            @Override
            public void getSelectedColumns(AccessibleTableEvent e) {
                /* CTable does not support column selection. */
            }

            @Override
            public void getSelectedRowCount(AccessibleTableEvent e) {
                e.count = selectedItems.length;
            }

            @Override
            public void getSelectedRows(AccessibleTableEvent e) {
                int[] selectedIndices = new int[selectedItems.length];
                for (int i = 0; i < selectedItems.length; i++) {
                    selectedIndices[i] = selectedItems[i].index;
                }
                e.selected = selectedIndices;
            }

            @Override
            public void getSummary(AccessibleTableEvent e) {
                // TODO: What is a summary? How does it differ from name? Should app supply?
                e.result = "This is the Custom Table's Summary";
            }

            @Override
            public void isColumnSelected(AccessibleTableEvent e) {
                e.isSelected = false; /* CTable does not support column selection. */
            }

            @Override
            public void isRowSelected(AccessibleTableEvent e) {
                e.isSelected = isSelected(e.row);
            }

            @Override
            public void selectColumn(AccessibleTableEvent e) {
                /* CTable does not support column selection. */
            }

            @Override
            public void selectRow(AccessibleTableEvent e) {
                select(e.row);
                e.result = ACC.OK;
            }

            @Override
            public void setSelectedColumn(AccessibleTableEvent e) {
                /* CTable does not support column selection. */
            }

            @Override
            public void setSelectedRow(AccessibleTableEvent e) {
                setSelection(e.row);
                e.result = ACC.OK;
            }
        });
    }

    static void initImages(final Display display) {
        PaletteData arrowPalette = new PaletteData(new RGB(0, 0, 0), new RGB(255, 255, 255));
        if (display.getData(ID_ARROWDOWN) == null) {
            ImageData arrowDown = new ImageData(7, 4, 1, arrowPalette, 1,
                    new byte[] { 0x00, (byte) 0x83, (byte) 0xC7, (byte) 0xEF });
            arrowDown.transparentPixel = 0x1; /* use white for transparency */
            display.setData(ID_ARROWDOWN, new Image(display, arrowDown));
        }
        if (display.getData(ID_ARROWUP) == null) {
            ImageData arrowUp = new ImageData(7, 4, 1, arrowPalette, 1,
                    new byte[] { (byte) 0xEF, (byte) 0xC7, (byte) 0x83, 0x00 });
            arrowUp.transparentPixel = 0x1; /* use white for transparency */
            display.setData(ID_ARROWUP, new Image(display, arrowUp));
        }

        PaletteData checkMarkPalette = new PaletteData(new RGB(0, 0, 0), new RGB(252, 3, 251));
        byte[] checkbox = new byte[] { 0, 0, 127, -64, 127, -64, 127, -64, 127, -64, 127, -64, 127, -64, 127, -64,
                127, -64, 127, -64, 0, 0 };
        ImageData checkmark = new ImageData(7, 7, 1, checkMarkPalette, 1,
                new byte[] { -4, -8, 112, 34, 6, -114, -34 });
        checkmark.transparentPixel = 1;
        if (display.getData(ID_CHECKMARK) == null) {
            display.setData(ID_CHECKMARK, new Image(display, checkmark));
        }

        if (display.getData(ID_UNCHECKED) == null) {
            PaletteData uncheckedPalette = new PaletteData(new RGB(128, 128, 128), new RGB(255, 255, 255));
            ImageData unchecked = new ImageData(11, 11, 1, uncheckedPalette, 2, checkbox);
            display.setData(ID_UNCHECKED, new Image(display, unchecked));
        }

        if (display.getData(ID_GRAYUNCHECKED) == null) {
            PaletteData grayUncheckedPalette = new PaletteData(new RGB(128, 128, 128), new RGB(192, 192, 192));
            ImageData grayUnchecked = new ImageData(11, 11, 1, grayUncheckedPalette, 2, checkbox);
            display.setData(ID_GRAYUNCHECKED, new Image(display, grayUnchecked));
        }

        display.disposeExec(() -> {
            Image unchecked = (Image) display.getData(ID_UNCHECKED);
            if (unchecked != null)
                unchecked.dispose();
            Image grayUnchecked = (Image) display.getData(ID_GRAYUNCHECKED);
            if (grayUnchecked != null)
                grayUnchecked.dispose();
            Image checkmark1 = (Image) display.getData(ID_CHECKMARK);
            if (checkmark1 != null)
                checkmark1.dispose();
            Image arrowDown = (Image) display.getData(ID_ARROWDOWN);
            if (arrowDown != null)
                arrowDown.dispose();
            Image arrowUp = (Image) display.getData(ID_ARROWUP);
            if (arrowUp != null)
                arrowUp.dispose();

            display.setData(ID_UNCHECKED, null);
            display.setData(ID_GRAYUNCHECKED, null);
            display.setData(ID_CHECKMARK, null);
            display.setData(ID_ARROWDOWN, null);
            display.setData(ID_ARROWUP, null);
        });
    }

    /**
     * Returns <code>true</code> if the item is selected,
     * and <code>false</code> otherwise.  Indices out of
     * range are ignored.
     *
     * @param index the index of the item
     * @return the selection state of the item at the index
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public boolean isSelected(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            return false;
        return items[index].isSelected();
    }

    @Override
    public void notifyListeners(int eventType, Event event) {
        super.notifyListeners(eventType, event);
        if (eventType == SWT.Selection && event.detail != SWT.CHECK)
            getAccessible().selectionChanged();
    }

    void onArrowDown(int stateMask) {
        if ((stateMask & (SWT.SHIFT | SWT.CTRL)) == 0) {
            /* Down Arrow with no modifiers */
            int newFocusIndex = focusItem.index + 1;
            if (newFocusIndex == itemsCount)
                return; /* at bottom */
            selectItem(items[newFocusIndex], false);
            setFocusItem(items[newFocusIndex], true);
            redrawItem(newFocusIndex, true);
            showItem(items[newFocusIndex]);
            Event newEvent = new Event();
            newEvent.item = items[newFocusIndex];
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        if ((getStyle() & SWT.SINGLE) != 0) {
            if ((stateMask & SWT.CTRL) != 0) {
                /* CTRL+Down Arrow, CTRL+Shift+Down Arrow */
                int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
                if (itemsCount <= topIndex + visibleItemCount)
                    return; /* at bottom */
                update();
                topIndex++;
                ScrollBar vBar = getVerticalBar();
                if (vBar != null)
                    vBar.setSelection(topIndex);
                GC gc = new GC(this);
                gc.copyArea(0, 0, clientArea.width, clientArea.height, 0, -itemHeight);
                gc.dispose();
                return;
            }
            /* Shift+Down Arrow */
            int newFocusIndex = focusItem.index + 1;
            if (newFocusIndex == itemsCount)
                return; /* at bottom */
            selectItem(items[newFocusIndex], false);
            setFocusItem(items[newFocusIndex], true);
            redrawItem(newFocusIndex, true);
            showItem(items[newFocusIndex]);
            Event newEvent = new Event();
            newEvent.item = items[newFocusIndex];
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        /* SWT.MULTI */
        if ((stateMask & SWT.CTRL) != 0) {
            if ((stateMask & SWT.SHIFT) != 0) {
                /* CTRL+Shift+Down Arrow */
                int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
                if (itemsCount <= topIndex + visibleItemCount)
                    return; /* at bottom */
                update();
                topIndex++;
                ScrollBar vBar = getVerticalBar();
                if (vBar != null)
                    vBar.setSelection(topIndex);
                GC gc = new GC(this);
                gc.copyArea(0, 0, clientArea.width, clientArea.height, 0, -itemHeight);
                gc.dispose();
                return;
            }
            /* CTRL+Down Arrow */
            int focusIndex = focusItem.index;
            if (focusIndex == itemsCount - 1)
                return; /* at bottom */
            CTableItem newFocusItem = items[focusIndex + 1];
            setFocusItem(newFocusItem, true);
            redrawItem(newFocusItem.index, true);
            showItem(newFocusItem);
            return;
        }
        /* Shift+Down Arrow */
        int newFocusIndex = focusItem.index + 1;
        if (newFocusIndex == itemsCount)
            return; /* at bottom */
        if (anchorItem == null)
            anchorItem = focusItem;
        if (focusItem.index < anchorItem.index) {
            deselectItem(focusItem);
            redrawItem(focusItem.index, true);
        }
        selectItem(items[newFocusIndex], true);
        setFocusItem(items[newFocusIndex], true);
        redrawItem(newFocusIndex, true);
        showItem(items[newFocusIndex]);
        Event newEvent = new Event();
        newEvent.item = items[newFocusIndex];
        notifyListeners(SWT.Selection, newEvent);
    }

    void onArrowLeft(int stateMask) {
        if (horizontalOffset == 0)
            return;
        int newSelection = Math.max(0, horizontalOffset - SIZE_HORIZONTALSCROLL);
        update();
        GC gc = new GC(this);
        gc.copyArea(0, 0, clientArea.width, clientArea.height, horizontalOffset - newSelection, 0);
        gc.dispose();
        if (header.getVisible()) {
            header.update();
            Rectangle headerClientArea = header.getClientArea();
            gc = new GC(header);
            gc.copyArea(0, 0, headerClientArea.width, headerClientArea.height, horizontalOffset - newSelection, 0);
            gc.dispose();
        }
        horizontalOffset = newSelection;
        ScrollBar hBar = getHorizontalBar();
        if (hBar != null)
            hBar.setSelection(horizontalOffset);
    }

    void onArrowRight(int stateMask) {
        ScrollBar hBar = getHorizontalBar();
        if (hBar == null)
            return;
        int maximum = hBar.getMaximum();
        int clientWidth = clientArea.width;
        if ((horizontalOffset + clientArea.width) == maximum)
            return;
        if (maximum <= clientWidth)
            return;
        int newSelection = Math.min(horizontalOffset + SIZE_HORIZONTALSCROLL, maximum - clientWidth);
        update();
        GC gc = new GC(this);
        gc.copyArea(0, 0, clientArea.width, clientArea.height, horizontalOffset - newSelection, 0);
        gc.dispose();
        if (header.getVisible()) {
            Rectangle headerClientArea = header.getClientArea();
            header.update();
            gc = new GC(header);
            gc.copyArea(0, 0, headerClientArea.width, headerClientArea.height, horizontalOffset - newSelection, 0);
            gc.dispose();
        }
        horizontalOffset = newSelection;
        hBar.setSelection(horizontalOffset);
    }

    void onArrowUp(int stateMask) {
        if ((stateMask & (SWT.SHIFT | SWT.CTRL)) == 0) {
            /* Up Arrow with no modifiers */
            int newFocusIndex = focusItem.index - 1;
            if (newFocusIndex < 0)
                return; /* at top */
            CTableItem item = items[newFocusIndex];
            selectItem(item, false);
            setFocusItem(item, true);
            redrawItem(newFocusIndex, true);
            showItem(item);
            Event newEvent = new Event();
            newEvent.item = item;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        if ((getStyle() & SWT.SINGLE) != 0) {
            if ((stateMask & SWT.CTRL) != 0) {
                /* CTRL+Up Arrow, CTRL+Shift+Up Arrow */
                if (topIndex == 0)
                    return; /* at top */
                update();
                topIndex--;
                ScrollBar vBar = getVerticalBar();
                if (vBar != null)
                    vBar.setSelection(topIndex);
                GC gc = new GC(this);
                gc.copyArea(0, 0, clientArea.width, clientArea.height, 0, itemHeight);
                gc.dispose();
                return;
            }
            /* Shift+Up Arrow */
            int newFocusIndex = focusItem.index - 1;
            if (newFocusIndex < 0)
                return; /* at top */
            CTableItem item = items[newFocusIndex];
            selectItem(item, false);
            setFocusItem(item, true);
            redrawItem(newFocusIndex, true);
            showItem(item);
            Event newEvent = new Event();
            newEvent.item = item;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        /* SWT.MULTI */
        if ((stateMask & SWT.CTRL) != 0) {
            if ((stateMask & SWT.SHIFT) != 0) {
                /* CTRL+Shift+Up Arrow */
                if (topIndex == 0)
                    return; /* at top */
                update();
                topIndex--;
                ScrollBar vBar = getVerticalBar();
                if (vBar != null)
                    vBar.setSelection(topIndex);
                GC gc = new GC(this);
                gc.copyArea(0, 0, clientArea.width, clientArea.height, 0, itemHeight);
                gc.dispose();
                return;
            }
            /* CTRL+Up Arrow */
            int focusIndex = focusItem.index;
            if (focusIndex == 0)
                return; /* at top */
            CTableItem newFocusItem = items[focusIndex - 1];
            setFocusItem(newFocusItem, true);
            showItem(newFocusItem);
            redrawItem(newFocusItem.index, true);
            return;
        }
        /* Shift+Up Arrow */
        int newFocusIndex = focusItem.index - 1;
        if (newFocusIndex < 0)
            return; /* at top */
        if (anchorItem == null)
            anchorItem = focusItem;
        if (anchorItem.index < focusItem.index) {
            deselectItem(focusItem);
            redrawItem(focusItem.index, true);
        }
        CTableItem item = items[newFocusIndex];
        selectItem(item, true);
        setFocusItem(item, true);
        redrawItem(newFocusIndex, true);
        showItem(item);
        Event newEvent = new Event();
        newEvent.item = item;
        notifyListeners(SWT.Selection, newEvent);
    }

    void onCR() {
        if (focusItem == null)
            return;
        Event event = new Event();
        event.item = focusItem;
        notifyListeners(SWT.DefaultSelection, event);
    }

    void onDispose(Event event) {
        if (isDisposed())
            return;
        if (ignoreDispose)
            return;
        ignoreDispose = true;
        notifyListeners(SWT.Dispose, event);
        event.type = SWT.None;
        for (int i = 0; i < itemsCount; i++) {
            items[i].dispose(false);
        }
        for (CTableColumn column : columns) {
            column.dispose(false);
        }
        if (toolTipShell != null) {
            toolTipShell.dispose();
            toolTipShell = null;
            toolTipLabel = null;
        }
        toolTipListener = null;
        itemsCount = topIndex = horizontalOffset = 0;
        items = selectedItems = null;
        columns = orderedColumns = null;
        focusItem = anchorItem = lastClickedItem = null;
        lastSelectionEvent = null;
        header = null;
        resizeColumn = sortColumn = null;
    }

    void onEnd(int stateMask) {
        int lastAvailableIndex = itemsCount - 1;
        if ((stateMask & (SWT.CTRL | SWT.SHIFT)) == 0) {
            /* End with no modifiers */
            if (focusItem.index == lastAvailableIndex)
                return; /* at bottom */
            CTableItem item = items[lastAvailableIndex];
            selectItem(item, false);
            setFocusItem(item, true);
            redrawItem(lastAvailableIndex, true);
            showItem(item);
            Event newEvent = new Event();
            newEvent.item = item;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        if ((getStyle() & SWT.SINGLE) != 0) {
            if ((stateMask & SWT.CTRL) != 0) {
                /* CTRL+End, CTRL+Shift+End */
                int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
                setTopIndex(itemsCount - visibleItemCount);
                return;
            }
            /* Shift+End */
            if (focusItem.index == lastAvailableIndex)
                return; /* at bottom */
            CTableItem item = items[lastAvailableIndex];
            selectItem(item, false);
            setFocusItem(item, true);
            redrawItem(lastAvailableIndex, true);
            showItem(item);
            Event newEvent = new Event();
            newEvent.item = item;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        /* SWT.MULTI */
        if ((stateMask & SWT.CTRL) != 0) {
            if ((stateMask & SWT.SHIFT) != 0) {
                /* CTRL+Shift+End */
                showItem(items[lastAvailableIndex]);
                return;
            }
            /* CTRL+End */
            if (focusItem.index == lastAvailableIndex)
                return; /* at bottom */
            CTableItem item = items[lastAvailableIndex];
            setFocusItem(item, true);
            showItem(item);
            redrawItem(item.index, true);
            return;
        }
        /* Shift+End */
        if (anchorItem == null)
            anchorItem = focusItem;
        CTableItem selectedItem = items[lastAvailableIndex];
        if (selectedItem == focusItem && selectedItem.isSelected())
            return;
        int anchorIndex = anchorItem.index;
        int selectIndex = selectedItem.index;
        CTableItem[] newSelection = new CTableItem[selectIndex - anchorIndex + 1];
        int writeIndex = 0;
        for (int i = anchorIndex; i <= selectIndex; i++) {
            newSelection[writeIndex++] = items[i];
        }
        setSelection(newSelection, false);
        setFocusItem(selectedItem, true);
        redrawItems(anchorIndex, selectIndex, true);
        showItem(selectedItem);
        Event newEvent = new Event();
        newEvent.item = selectedItem;
        notifyListeners(SWT.Selection, newEvent);
    }

    void onFocusIn() {
        hasFocus = true;
        if (itemsCount == 0) {
            redraw();
            return;
        }
        if ((getStyle() & (SWT.HIDE_SELECTION | SWT.MULTI)) == (SWT.HIDE_SELECTION | SWT.MULTI)) {
            for (CTableItem selectedItem : selectedItems) {
                redrawItem(selectedItem.index, true);
            }
        }
        if (focusItem != null) {
            redrawItem(focusItem.index, true);
            return;
        }
        /* an initial focus item must be selected */
        CTableItem initialFocus;
        if (selectedItems.length > 0) {
            initialFocus = selectedItems[0];
        } else {
            initialFocus = items[topIndex];
        }
        setFocusItem(initialFocus, false);
        redrawItem(initialFocus.index, true);
        return;
    }

    void onFocusOut() {
        hasFocus = false;
        if (itemsCount == 0) {
            redraw();
            return;
        }
        if (focusItem != null) {
            redrawItem(focusItem.index, true);
        }
        if ((getStyle() & (SWT.HIDE_SELECTION | SWT.MULTI)) == (SWT.HIDE_SELECTION | SWT.MULTI)) {
            for (CTableItem selectedItem : selectedItems) {
                redrawItem(selectedItem.index, true);
            }
        }
    }

    void onHome(int stateMask) {
        if ((stateMask & (SWT.CTRL | SWT.SHIFT)) == 0) {
            /* Home with no modifiers */
            if (focusItem.index == 0)
                return; /* at top */
            CTableItem item = items[0];
            selectItem(item, false);
            setFocusItem(item, true);
            redrawItem(0, true);
            showItem(item);
            Event newEvent = new Event();
            newEvent.item = item;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        if ((getStyle() & SWT.SINGLE) != 0) {
            if ((stateMask & SWT.CTRL) != 0) {
                /* CTRL+Home, CTRL+Shift+Home */
                setTopIndex(0);
                return;
            }
            /* Shift+Home */
            if (focusItem.index == 0)
                return; /* at top */
            CTableItem item = items[0];
            selectItem(item, false);
            setFocusItem(item, true);
            redrawItem(0, true);
            showItem(item);
            Event newEvent = new Event();
            newEvent.item = item;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }
        /* SWT.MULTI */
        if ((stateMask & SWT.CTRL) != 0) {
            if ((stateMask & SWT.SHIFT) != 0) {
                /* CTRL+Shift+Home */
                setTopIndex(0);
                return;
            }
            /* CTRL+Home */
            if (focusItem.index == 0)
                return; /* at top */
            CTableItem item = items[0];
            setFocusItem(item, true);
            showItem(item);
            redrawItem(item.index, true);
            return;
        }
        /* Shift+Home */
        if (anchorItem == null)
            anchorItem = focusItem;
        CTableItem selectedItem = items[0];
        if (selectedItem == focusItem && selectedItem.isSelected())
            return;
        int anchorIndex = anchorItem.index;
        int selectIndex = selectedItem.index;
        CTableItem[] newSelection = new CTableItem[anchorIndex + 1];
        int writeIndex = 0;
        for (int i = anchorIndex; i >= 0; i--) {
            newSelection[writeIndex++] = items[i];
        }
        setSelection(newSelection, false);
        setFocusItem(selectedItem, true);
        redrawItems(anchorIndex, selectIndex, true);
        showItem(selectedItem);
        Event newEvent = new Event();
        newEvent.item = selectedItem;
        notifyListeners(SWT.Selection, newEvent);
    }

    void onKeyDown(Event event) {
        if (ignoreKey) {
            ignoreKey = false;
            return;
        }
        ignoreKey = true;
        notifyListeners(event.type, event);
        event.type = SWT.None;
        if (!event.doit)
            return;
        if (focusItem == null)
            return;
        if ((event.stateMask & SWT.SHIFT) == 0 && event.keyCode != SWT.SHIFT) {
            anchorItem = null;
        }
        switch (event.keyCode) {
        case SWT.ARROW_UP:
            onArrowUp(event.stateMask);
            return;
        case SWT.ARROW_DOWN:
            onArrowDown(event.stateMask);
            return;
        case SWT.ARROW_LEFT:
            onArrowLeft(event.stateMask);
            return;
        case SWT.ARROW_RIGHT:
            onArrowRight(event.stateMask);
            return;
        case SWT.PAGE_UP:
            onPageUp(event.stateMask);
            return;
        case SWT.PAGE_DOWN:
            onPageDown(event.stateMask);
            return;
        case SWT.HOME:
            onHome(event.stateMask);
            return;
        case SWT.END:
            onEnd(event.stateMask);
            return;
        }
        if (event.character == ' ') {
            onSpace();
            return;
        }
        if (event.character == SWT.CR) {
            onCR();
            return;
        }
        if ((event.stateMask & SWT.CTRL) != 0)
            return;

        int initialIndex = focusItem.index;
        char character = Character.toLowerCase(event.character);
        /* check available items from current focus item to bottom */
        for (int i = initialIndex + 1; i < itemsCount; i++) {
            CTableItem item = items[i];
            String text = item.getText(0, false);
            if (text.length() > 0) {
                if (Character.toLowerCase(text.charAt(0)) == character) {
                    selectItem(item, false);
                    setFocusItem(item, true);
                    redrawItem(i, true);
                    showItem(item);
                    Event newEvent = new Event();
                    newEvent.item = item;
                    notifyListeners(SWT.Selection, newEvent);
                    return;
                }
            }
        }
        /* check available items from top to current focus item */
        for (int i = 0; i < initialIndex; i++) {
            CTableItem item = items[i];
            String text = item.getText(0, false);
            if (text.length() > 0) {
                if (Character.toLowerCase(text.charAt(0)) == character) {
                    selectItem(item, false);
                    setFocusItem(item, true);
                    redrawItem(i, true);
                    showItem(item);
                    Event newEvent = new Event();
                    newEvent.item = item;
                    notifyListeners(SWT.Selection, newEvent);
                    return;
                }
            }
        }
    }

    void onMouseDoubleClick(Event event) {
        if (!isFocusControl())
            setFocus();
        int index = (event.y - getHeaderHeight()) / itemHeight + topIndex;
        if (!(0 <= index && index < itemsCount))
            return; /* not on an available item */
        CTableItem selectedItem = items[index];

        /*
         * If the two clicks of the double click did not occur over the same item then do not
         * consider this to be a default selection.
         */
        if (selectedItem != lastClickedItem)
            return;

        if (!selectedItem.getHitBounds().contains(event.x, event.y))
            return; /* considers x */

        Event newEvent = new Event();
        newEvent.item = selectedItem;
        notifyListeners(SWT.DefaultSelection, newEvent);
    }

    void onMouseDown(Event event) {
        if (!isFocusControl())
            forceFocus();
        int index = (event.y - getHeaderHeight()) / itemHeight + topIndex;
        if (!(0 <= index && index < itemsCount))
            return; /* not on an available item */
        CTableItem selectedItem = items[index];

        /* if click was in checkbox */
        if ((getStyle() & SWT.CHECK) != 0 && selectedItem.getCheckboxBounds().contains(event.x, event.y)) {
            if (event.button != 1)
                return;
            selectedItem.setChecked(!selectedItem.checked);
            Event newEvent = new Event();
            newEvent.item = selectedItem;
            newEvent.detail = SWT.CHECK;
            notifyListeners(SWT.Selection, newEvent);
            return;
        }

        if (!selectedItem.getHitBounds().contains(event.x, event.y))
            return;

        if ((event.stateMask & SWT.SHIFT) == 0 && event.keyCode != SWT.SHIFT)
            anchorItem = null;

        boolean sendSelection = true;
        /* Detect when this is the second click of a DefaultSelection and don't fire Selection */
        if (lastSelectionEvent != null && lastSelectionEvent.item == selectedItem) {
            if (event.time - lastSelectionEvent.time <= display.getDoubleClickTime()) {
                sendSelection = false;
            } else {
                lastSelectionEvent = event;
                event.item = selectedItem;
            }
        } else {
            lastSelectionEvent = event;
            event.item = selectedItem;
        }

        if ((getStyle() & SWT.SINGLE) != 0) {
            if (!selectedItem.isSelected()) {
                if (event.button == 1) {
                    selectItem(selectedItem, false);
                    setFocusItem(selectedItem, true);
                    redrawItem(selectedItem.index, true);
                    if (sendSelection) {
                        Event newEvent = new Event();
                        newEvent.item = selectedItem;
                        notifyListeners(SWT.Selection, newEvent);
                    }
                    return;
                }
                if ((event.stateMask & (SWT.CTRL | SWT.SHIFT)) == 0) {
                    selectItem(selectedItem, false);
                    setFocusItem(selectedItem, true);
                    redrawItem(selectedItem.index, true);
                    if (sendSelection) {
                        Event newEvent = new Event();
                        newEvent.item = selectedItem;
                        notifyListeners(SWT.Selection, newEvent);
                    }
                    return;
                }
            }
            /* item is selected */
            if (event.button == 1) {
                /* fire a selection event, though the selection did not change */
                if (sendSelection) {
                    Event newEvent = new Event();
                    newEvent.item = selectedItem;
                    notifyListeners(SWT.Selection, newEvent);
                }
                return;
            }
        }
        /* SWT.MULTI */
        if (!selectedItem.isSelected()) {
            if (event.button == 1) {
                if ((event.stateMask & (SWT.CTRL | SWT.SHIFT)) == SWT.SHIFT) {
                    if (anchorItem == null)
                        anchorItem = focusItem;
                    int anchorIndex = anchorItem.index;
                    int selectIndex = selectedItem.index;
                    CTableItem[] newSelection = new CTableItem[Math.abs(anchorIndex - selectIndex) + 1];
                    int step = anchorIndex < selectIndex ? 1 : -1;
                    int writeIndex = 0;
                    for (int i = anchorIndex; i != selectIndex; i += step) {
                        newSelection[writeIndex++] = items[i];
                    }
                    newSelection[writeIndex] = items[selectIndex];
                    setSelection(newSelection, false);
                    setFocusItem(selectedItem, true);
                    redrawItems(Math.min(anchorIndex, selectIndex), Math.max(anchorIndex, selectIndex), true);
                    if (sendSelection) {
                        Event newEvent = new Event();
                        newEvent.item = selectedItem;
                        notifyListeners(SWT.Selection, newEvent);
                    }
                    return;
                }
                selectItem(selectedItem, (event.stateMask & SWT.CTRL) != 0);
                setFocusItem(selectedItem, true);
                redrawItem(selectedItem.index, true);
                if (sendSelection) {
                    Event newEvent = new Event();
                    newEvent.item = selectedItem;
                    notifyListeners(SWT.Selection, newEvent);
                }
                return;
            }
            /* button 3 */
            if ((event.stateMask & (SWT.CTRL | SWT.SHIFT)) == 0) {
                selectItem(selectedItem, false);
                setFocusItem(selectedItem, true);
                redrawItem(selectedItem.index, true);
                if (sendSelection) {
                    Event newEvent = new Event();
                    newEvent.item = selectedItem;
                    notifyListeners(SWT.Selection, newEvent);
                }
                return;
            }
        }
        /* item is selected */
        if (event.button != 1)
            return;
        if ((event.stateMask & SWT.CTRL) != 0) {
            removeSelectedItem(getSelectionIndex(selectedItem));
            setFocusItem(selectedItem, true);
            redrawItem(selectedItem.index, true);
            if (sendSelection) {
                Event newEvent = new Event();
                newEvent.item = selectedItem;
                notifyListeners(SWT.Selection, newEvent);
            }
            return;
        }
        if ((event.stateMask & SWT.SHIFT) != 0) {
            if (anchorItem == null)
                anchorItem = focusItem;
            int anchorIndex = anchorItem.index;
            int selectIndex = selectedItem.index;
            CTableItem[] newSelection = new CTableItem[Math.abs(anchorIndex - selectIndex) + 1];
            int step = anchorIndex < selectIndex ? 1 : -1;
            int writeIndex = 0;
            for (int i = anchorIndex; i != selectIndex; i += step) {
                newSelection[writeIndex++] = items[i];
            }
            newSelection[writeIndex] = items[selectIndex];
            setSelection(newSelection, false);
            setFocusItem(selectedItem, true);
            redrawItems(Math.min(anchorIndex, selectIndex), Math.max(anchorIndex, selectIndex), true);
            if (sendSelection) {
                Event newEvent = new Event();
                newEvent.item = selectedItem;
                notifyListeners(SWT.Selection, newEvent);
            }
            return;
        }
        selectItem(selectedItem, false);
        setFocusItem(selectedItem, true);
        redrawItem(selectedItem.index, true);
        if (sendSelection) {
            Event newEvent = new Event();
            newEvent.item = selectedItem;
            notifyListeners(SWT.Selection, newEvent);
        }
    }

    void onMouseUp(Event event) {
        int index = (event.y - getHeaderHeight()) / itemHeight + topIndex;
        if (!(0 <= index && index < itemsCount))
            return; /* not on an available item */
        lastClickedItem = items[index];
    }

    void onPageDown(int stateMask) {
        int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
        if ((stateMask & (SWT.CTRL | SWT.SHIFT)) == 0) {
            /* PageDown with no modifiers */
            int newFocusIndex = focusItem.index + visibleItemCount - 1;
            newFocusIndex = Math.min(newFocusIndex, itemsCount - 1);
            if (newFocusIndex == focusItem.index)
                return;
            CTableItem item = items[newFocusIndex];
            selectItem(item, false);
            setFocusItem(item, true);
            showItem(item);
            redrawItem(item.index, true);
            return;
        }
        if ((stateMask & (SWT.CTRL | SWT.SHIFT)) == (SWT.CTRL | SWT.SHIFT)) {
            /* CTRL+Shift+PageDown */
            int newTopIndex = topIndex + visibleItemCount;
            newTopIndex = Math.min(newTopIndex, itemsCount - visibleItemCount);
            if (newTopIndex == topIndex)
                return;
            setTopIndex(newTopIndex);
            return;
        }
        if ((getStyle() & SWT.SINGLE) != 0) {
            if ((stateMask & SWT.SHIFT) != 0) {
                /* Shift+PageDown */
                int newFocusIndex = focusItem.index + visibleItemCount - 1;
                newFocusIndex = Math.min(newFocusIndex, itemsCount - 1);
                if (newFocusIndex == focusItem.index)
                    return;
                CTableItem item = items[newFocusIndex];
                selectItem(item, false);
                setFocusItem(item, true);
                showItem(item);
                redrawItem(item.index, true);
                return;
            }
            /* CTRL+PageDown */
            int newTopIndex = topIndex + visibleItemCount;
            newTopIndex = Math.min(newTopIndex, itemsCount - visibleItemCount);
            if (newTopIndex == topIndex)
                return;
            setTopIndex(newTopIndex);
            return;
        }
        /* SWT.MULTI */
        if ((stateMask & SWT.CTRL) != 0) {
            /* CTRL+PageDown */
            int bottomIndex = Math.min(topIndex + visibleItemCount - 1, itemsCount - 1);
            if (focusItem.index != bottomIndex) {
                /* move focus to bottom item in viewport */
                setFocusItem(items[bottomIndex], true);
                redrawItem(bottomIndex, true);
            } else {
                /* at bottom of viewport, so set focus to bottom item one page down */
                int newFocusIndex = Math.min(itemsCount - 1, bottomIndex + visibleItemCount);
                if (newFocusIndex == focusItem.index)
                    return;
                setFocusItem(items[newFocusIndex], true);
                showItem(items[newFocusIndex]);
                redrawItem(newFocusIndex, true);
            }
            return;
        }
        /* Shift+PageDown */
        if (anchorItem == null)
            anchorItem = focusItem;
        int anchorIndex = anchorItem.index;
        int bottomIndex = Math.min(topIndex + visibleItemCount - 1, itemsCount - 1);
        int selectIndex;
        if (focusItem.index != bottomIndex) {
            /* select from focus to bottom item in viewport */
            selectIndex = bottomIndex;
        } else {
            /* already at bottom of viewport, so select to bottom of one page down */
            selectIndex = Math.min(itemsCount - 1, bottomIndex + visibleItemCount);
            if (selectIndex == focusItem.index && focusItem.isSelected())
                return;
        }
        CTableItem selectedItem = items[selectIndex];
        CTableItem[] newSelection = new CTableItem[Math.abs(anchorIndex - selectIndex) + 1];
        int step = anchorIndex < selectIndex ? 1 : -1;
        int writeIndex = 0;
        for (int i = anchorIndex; i != selectIndex; i += step) {
            newSelection[writeIndex++] = items[i];
        }
        newSelection[writeIndex] = items[selectIndex];
        setSelection(newSelection, false);
        setFocusItem(selectedItem, true);
        showItem(selectedItem);
        Event newEvent = new Event();
        newEvent.item = selectedItem;
        notifyListeners(SWT.Selection, newEvent);
    }

    void onPageUp(int stateMask) {
        int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
        if ((stateMask & (SWT.CTRL | SWT.SHIFT)) == 0) {
            /* PageUp with no modifiers */
            int newFocusIndex = Math.max(0, focusItem.index - visibleItemCount + 1);
            if (newFocusIndex == focusItem.index)
                return;
            CTableItem item = items[newFocusIndex];
            selectItem(item, false);
            setFocusItem(item, true);
            showItem(item);
            redrawItem(item.index, true);
            return;
        }
        if ((stateMask & (SWT.CTRL | SWT.SHIFT)) == (SWT.CTRL | SWT.SHIFT)) {
            /* CTRL+Shift+PageUp */
            int newTopIndex = Math.max(0, topIndex - visibleItemCount);
            if (newTopIndex == topIndex)
                return;
            setTopIndex(newTopIndex);
            return;
        }
        if ((getStyle() & SWT.SINGLE) != 0) {
            if ((stateMask & SWT.SHIFT) != 0) {
                /* Shift+PageUp */
                int newFocusIndex = Math.max(0, focusItem.index - visibleItemCount + 1);
                if (newFocusIndex == focusItem.index)
                    return;
                CTableItem item = items[newFocusIndex];
                selectItem(item, false);
                setFocusItem(item, true);
                showItem(item);
                redrawItem(item.index, true);
                return;
            }
            /* CTRL+PageUp */
            int newTopIndex = Math.max(0, topIndex - visibleItemCount);
            if (newTopIndex == topIndex)
                return;
            setTopIndex(newTopIndex);
            return;
        }
        /* SWT.MULTI */
        if ((stateMask & SWT.CTRL) != 0) {
            /* CTRL+PageUp */
            if (focusItem.index != topIndex) {
                /* move focus to top item in viewport */
                setFocusItem(items[topIndex], true);
                redrawItem(topIndex, true);
            } else {
                /* at top of viewport, so set focus to top item one page up */
                int newFocusIndex = Math.max(0, focusItem.index - visibleItemCount);
                if (newFocusIndex == focusItem.index)
                    return;
                setFocusItem(items[newFocusIndex], true);
                showItem(items[newFocusIndex]);
                redrawItem(newFocusIndex, true);
            }
            return;
        }
        /* Shift+PageUp */
        if (anchorItem == null)
            anchorItem = focusItem;
        int anchorIndex = anchorItem.index;
        int selectIndex;
        if (focusItem.index != topIndex) {
            /* select from focus to top item in viewport */
            selectIndex = topIndex;
        } else {
            /* already at top of viewport, so select to top of one page up */
            selectIndex = Math.max(0, topIndex - visibleItemCount);
            if (selectIndex == focusItem.index && focusItem.isSelected())
                return;
        }
        CTableItem selectedItem = items[selectIndex];
        CTableItem[] newSelection = new CTableItem[Math.abs(anchorIndex - selectIndex) + 1];
        int step = anchorIndex < selectIndex ? 1 : -1;
        int writeIndex = 0;
        for (int i = anchorIndex; i != selectIndex; i += step) {
            newSelection[writeIndex++] = items[i];
        }
        newSelection[writeIndex] = items[selectIndex];
        setSelection(newSelection, false);
        setFocusItem(selectedItem, true);
        showItem(selectedItem);
        Event newEvent = new Event();
        newEvent.item = selectedItem;
        notifyListeners(SWT.Selection, newEvent);
    }

    void onPaint(Event event) {
        CTableColumn[] orderedColumns = getOrderedColumns();
        GC gc = event.gc;
        Rectangle clipping = gc.getClipping();
        int headerHeight = getHeaderHeight();
        int numColumns = orderedColumns.length;
        int startColumn = -1, endColumn = -1;
        if (numColumns > 0) {
            startColumn = computeColumnIntersect(clipping.x, 0);
            if (startColumn != -1) { /* the clip x is within a column's bounds */
                endColumn = computeColumnIntersect(clipping.x + clipping.width, startColumn);
                if (endColumn == -1)
                    endColumn = numColumns - 1;
            }
        } else {
            startColumn = endColumn = 0;
        }

        /* Determine the items to be painted */
        int startIndex = (clipping.y - headerHeight) / itemHeight + topIndex;
        int endIndex = -1;
        if (startIndex < itemsCount) {
            endIndex = startIndex + (int) Math.ceil((float) clipping.height / itemHeight);
        }
        startIndex = Math.max(0, startIndex);
        endIndex = Math.min(endIndex, itemsCount - 1);

        /* fill background not handled by items */
        gc.setBackground(getBackground());
        gc.setClipping(clipping);
        int bottomY = endIndex >= 0 ? getItemY(items[endIndex]) + itemHeight : 0;
        int fillHeight = Math.max(0, clientArea.height - bottomY);
        if (fillHeight > 0) { /* space below bottom item */
            gc.fillRectangle(0, bottomY, clientArea.width, fillHeight);
            //drawBackground (gc, 0, bottomY, clientArea.width, fillHeight);
        }
        if (columns.length > 0) {
            CTableColumn column = orderedColumns[orderedColumns.length - 1]; /* last column */
            int rightX = column.getX() + column.width;
            if (rightX < clientArea.width) {
                gc.fillRectangle(rightX, 0, clientArea.width - rightX, clientArea.height - fillHeight);
                //drawBackground (gc, rightX, 0, clientArea.width - rightX, clientArea.height - fillHeight);
            }
        }

        /* paint the items */
        boolean noFocusDraw = false;
        int[] lineDash = gc.getLineDash();
        int lineWidth = gc.getLineWidth();
        for (int i = startIndex; i <= Math.min(endIndex, itemsCount - 1); i++) {
            CTableItem item = items[i];
            if (!item.isDisposed()) { /* ensure that item was not disposed in a callback */
                if (startColumn == -1) {
                    /* indicates that region to paint is to the right of the last column */
                    noFocusDraw = item.paint(gc, null, true) || noFocusDraw;
                } else {
                    if (numColumns == 0) {
                        noFocusDraw = item.paint(gc, null, false) || noFocusDraw;
                    } else {
                        for (int j = startColumn; j <= Math.min(endColumn, columns.length - 1); j++) {
                            if (!item.isDisposed()) { /* ensure that item was not disposed in a callback */
                                noFocusDraw = item.paint(gc, orderedColumns[j], false) || noFocusDraw;
                            }
                            if (isDisposed() || gc.isDisposed())
                                return; /* ensure that receiver was not disposed in a callback */
                        }
                    }
                }
            }
            if (isDisposed() || gc.isDisposed())
                return; /* ensure that receiver was not disposed in a callback */
        }

        /* repaint grid lines */
        gc.setClipping(clipping);
        gc.setLineWidth(lineWidth);
        if (linesVisible) {
            gc.setForeground(display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW));
            gc.setLineDash(lineDash);
            if (numColumns > 0 && startColumn != -1) {
                /* vertical column lines */
                for (int i = startColumn; i <= endColumn; i++) {
                    int x = orderedColumns[i].getX() + orderedColumns[i].width - 1;
                    gc.drawLine(x, clipping.y, x, clipping.y + clipping.height);
                }
            }
            /* horizontal item lines */
            bottomY = clipping.y + clipping.height;
            int rightX = clipping.x + clipping.width;
            int y = (clipping.y - headerHeight) / itemHeight * itemHeight + headerHeight;
            while (y <= bottomY) {
                gc.drawLine(clipping.x, y, rightX, y);
                y += itemHeight;
            }
        }

        /* paint focus rectangle */
        if (!noFocusDraw && isFocusControl()) {
            if (focusItem != null) {
                Rectangle focusBounds = focusItem.getFocusBounds();
                if (focusBounds.width > 0) {
                    gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
                    gc.setClipping(focusBounds);
                    if (focusItem.isSelected()) {
                        gc.setLineDash(new int[] { 2, 2 });
                    } else {
                        gc.setLineDash(new int[] { 1, 1 });
                    }
                    gc.drawFocus(focusBounds.x, focusBounds.y, focusBounds.width, focusBounds.height);
                }
            } else {
                /* no items, so draw focus border around Table */
                int y = headerHeight + 1;
                int width = Math.max(0, clientArea.width - 2);
                int height = Math.max(0, clientArea.height - headerHeight - 2);
                gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
                gc.setClipping(1, y, width, height);
                gc.setLineDash(new int[] { 1, 1 });
                gc.drawFocus(1, y, width, height);
            }
        }
    }

    void onResize(Event event) {
        clientArea = getClientArea();
        /* vertical scrollbar */
        ScrollBar vBar = getVerticalBar();
        if (vBar != null) {
            int clientHeight = (clientArea.height - getHeaderHeight()) / itemHeight;
            int thumb = Math.min(clientHeight, itemsCount);
            vBar.setThumb(thumb);
            vBar.setPageIncrement(thumb);
            int index = vBar.getSelection();
            if (index != topIndex) {
                topIndex = index;
                redraw();
            }
            boolean visible = clientHeight < itemsCount;
            if (visible != vBar.getVisible()) {
                vBar.setVisible(visible);
                clientArea = getClientArea();
            }
        }

        /* horizontal scrollbar */
        ScrollBar hBar = getHorizontalBar();
        if (hBar != null) {
            int hBarMaximum = hBar.getMaximum();
            int thumb = Math.min(clientArea.width, hBarMaximum);
            hBar.setThumb(thumb);
            hBar.setPageIncrement(thumb);
            horizontalOffset = hBar.getSelection();
            boolean visible = clientArea.width < hBarMaximum;
            if (visible != hBar.getVisible()) {
                hBar.setVisible(visible);
                clientArea = getClientArea();
            }
        }

        /* header */
        int headerHeight = Math.max(fontHeight, headerImageHeight) + 2 * getHeaderPadding();
        header.setSize(clientArea.width, headerHeight);

        /* if this is the focus control but there are no items then the boundary focus ring must be repainted */
        if (itemsCount == 0 && isFocusControl())
            redraw();
    }

    void onScrollHorizontal(Event event) {
        ScrollBar hBar = getHorizontalBar();
        if (hBar == null)
            return;
        int newSelection = hBar.getSelection();
        update();
        if (itemsCount > 0) {
            GC gc = new GC(this);
            gc.copyArea(0, 0, clientArea.width, clientArea.height, horizontalOffset - newSelection, 0);
            gc.dispose();
        } else {
            redraw(); /* ensure that static focus rectangle updates properly */
        }

        if (drawCount <= 0 && header.isVisible()) {
            header.update();
            Rectangle headerClientArea = header.getClientArea();
            GC gc = new GC(header);
            gc.copyArea(0, 0, headerClientArea.width, headerClientArea.height, horizontalOffset - newSelection, 0);
            gc.dispose();
        }
        horizontalOffset = newSelection;
    }

    void onScrollVertical(Event event) {
        ScrollBar vBar = getVerticalBar();
        if (vBar == null)
            return;
        int newSelection = vBar.getSelection();
        update();
        GC gc = new GC(this);
        gc.copyArea(0, 0, clientArea.width, clientArea.height, 0, (topIndex - newSelection) * itemHeight);
        gc.dispose();
        topIndex = newSelection;
    }

    void onSpace() {
        if (focusItem == null)
            return;
        if (!focusItem.isSelected()) {
            selectItem(focusItem, (getStyle() & SWT.MULTI) != 0);
            redrawItem(focusItem.index, true);
        }
        if ((getStyle() & SWT.CHECK) != 0) {
            focusItem.setChecked(!focusItem.checked);
        }
        showItem(focusItem);
        Event event = new Event();
        event.item = focusItem;
        notifyListeners(SWT.Selection, event);
        if ((getStyle() & SWT.CHECK) == 0)
            return;

        /* SWT.CHECK */
        event = new Event();
        event.item = focusItem;
        event.detail = SWT.CHECK;
        notifyListeners(SWT.Selection, event);
    }

    /*
     * The current focus item is about to become unavailable, so reassign focus.
     */
    void reassignFocus() {
        if (focusItem == null)
            return;

        /*
         * reassign to the previous root-level item if there is one, or the next
         * root-level item otherwise
         */
        int index = focusItem.index;
        if (index != 0) {
            index--;
        } else {
            index++;
        }
        if (index < itemsCount) {
            CTableItem item = items[index];
            setFocusItem(item, false);
            showItem(item);
        } else {
            setFocusItem(null, false); /* no items left */
        }
    }

    @Override
    public void redraw() {
        checkWidget();
        if (drawCount <= 0)
            super.redraw();
    }

    @Override
    public void redraw(int x, int y, int width, int height, boolean all) {
        checkWidget();
        if (drawCount <= 0)
            super.redraw(x, y, width, height, all);
    }

    /*
     * Redraws from the specified index down to the last available item inclusive.  Note
     * that the redraw bounds do not extend beyond the current last item, so clients
     * that reduce the number of available items should use #redrawItems(int,int) instead
     * to ensure that redrawing extends down to the previous bottom item boundary.
     */
    void redrawFromItemDownwards(int index) {
        redrawItems(index, itemsCount - 1, false);
    }

    /*
     * Redraws the table item at the specified index.  It is valid for this index to reside
     * beyond the last available item.
     */
    void redrawItem(int itemIndex, boolean focusBoundsOnly) {
        if (itemIndex < itemsCount && !items[itemIndex].isInViewport())
            return;
        redrawItems(itemIndex, itemIndex, focusBoundsOnly);
    }

    /*
     * Redraws the table between the start and end item indices inclusive.  It is valid
     * for the end index value to extend beyond the last available item.
     */
    void redrawItems(int startIndex, int endIndex, boolean focusBoundsOnly) {
        if (drawCount > 0)
            return;

        int startY = (startIndex - topIndex) * itemHeight + getHeaderHeight();
        int height = (endIndex - startIndex + 1) * itemHeight;
        if (focusBoundsOnly) {
            boolean custom = isListening(SWT.EraseItem) || isListening(SWT.PaintItem);
            if (!custom && columns.length > 0) {
                CTableColumn lastColumn;
                if ((getStyle() & SWT.FULL_SELECTION) != 0) {
                    CTableColumn[] orderedColumns = getOrderedColumns();
                    lastColumn = orderedColumns[orderedColumns.length - 1];
                } else {
                    lastColumn = columns[0];
                }
                int rightX = lastColumn.getX() + lastColumn.getWidth();
                if (rightX <= 0)
                    return; /* focus column(s) not visible */
            }
            endIndex = Math.min(endIndex, itemsCount - 1);
            for (int i = startIndex; i <= endIndex; i++) {
                CTableItem item = items[i];
                if (item.isInViewport()) {
                    /* if custom painting is being done then repaint the full item */
                    if (custom) {
                        redraw(0, getItemY(item), clientArea.width, itemHeight, false);
                    } else {
                        /* repaint the item's focus bounds */
                        Rectangle bounds = item.getFocusBounds();
                        redraw(bounds.x, startY, bounds.width, height, false);
                    }
                }
            }
        } else {
            redraw(0, startY, clientArea.width, height, false);
        }
    }

    /**
     * Removes the item from the receiver at the given
     * zero-relative index.
     *
     * @param index the index for the item
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void remove(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            SWT.error(SWT.ERROR_INVALID_RANGE);
        items[index].dispose();
        int[] eventData = new int[5];
        eventData[0] = ACC.DELETE;
        eventData[1] = index;
        eventData[2] = 1;
        eventData[3] = 0;
        eventData[4] = 0;
        getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);
    }

    /**
     * Removes the items from the receiver which are
     * between the given zero-relative start and end
     * indices (inclusive).
     *
     * @param start the start of the range
     * @param end the end of the range
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if either the start or end are not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void remove(int start, int end) {
        checkWidget();
        if (start > end)
            return;
        if (!(0 <= start && start <= end && end < itemsCount)) {
            SWT.error(SWT.ERROR_INVALID_RANGE);
        }
        if (start == 0 && end == itemsCount - 1) {
            removeAll();
        } else {
            for (int i = end; i >= start; i--) {
                items[i].dispose();
            }

            int[] eventData = new int[5];
            eventData[0] = ACC.DELETE;
            eventData[1] = start;
            eventData[2] = end - start + 1;
            eventData[3] = 0;
            eventData[4] = 0;
            getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);

        }
    }

    /**
     * Removes the items from the receiver's list at the given
     * zero-relative indices.
     *
     * @param indices the array of indices of the items
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_RANGE - if the index is not between 0 and the number of elements in the list minus 1 (inclusive)</li>
     *    <li>ERROR_NULL_ARGUMENT - if the indices array is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void remove(int[] indices) {
        checkWidget();
        if (indices == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (indices.length == 0)
            return;
        int[] newIndices = new int[indices.length];
        System.arraycopy(indices, 0, newIndices, 0, indices.length);
        sortDescent(newIndices);
        int start = newIndices[newIndices.length - 1], end = newIndices[0];
        if (!(0 <= start && start <= end && end < itemsCount)) {
            SWT.error(SWT.ERROR_INVALID_RANGE);
        }
        int lastRemovedIndex = -1;
        int[] eventData = new int[5];
        for (int newIndice : newIndices) {
            if (newIndice != lastRemovedIndex) {
                items[newIndice].dispose();
                eventData[0] = ACC.DELETE;
                eventData[1] = newIndice;
                eventData[2] = 1;
                eventData[3] = 0;
                eventData[4] = 0;
                getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);
                lastRemovedIndex = newIndice;
            }
        }
    }

    /**
     * Removes all of the items from the receiver.
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void removeAll() {
        checkWidget();
        if (itemsCount == 0)
            return;
        setRedraw(false);

        setFocusItem(null, false);
        for (int i = 0; i < itemsCount; i++) {
            items[i].dispose(false);
        }
        items = new CTableItem[0];
        selectedItems = new CTableItem[0];
        int oldCount = itemsCount;
        itemsCount = topIndex = 0;
        anchorItem = lastClickedItem = null;
        lastSelectionEvent = null;

        int[] eventData = new int[5];
        eventData[0] = ACC.DELETE;
        eventData[1] = 0;
        eventData[2] = oldCount;
        eventData[3] = 0;
        eventData[4] = 0;
        getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);

        ScrollBar vBar = getVerticalBar();
        if (vBar != null) {
            vBar.setMaximum(1);
            vBar.setVisible(false);
        }
        if (columns.length == 0) {
            horizontalOffset = 0;
            ScrollBar hBar = getHorizontalBar();
            if (hBar != null) {
                hBar.setMaximum(1);
                hBar.setVisible(false);
            }
        }

        setRedraw(true);
    }

    String removeMnemonics(String string) {
        /* removes single ampersands and preserves double-ampersands */
        char[] chars = new char[string.length()];
        string.getChars(0, chars.length, chars, 0);
        int i = 0, j = 0;
        for (; i < chars.length; i++, j++) {
            if (chars[i] == '&') {
                if (++i == chars.length)
                    break;
                if (chars[i] == '&') {
                    chars[j++] = chars[i - 1];
                }
            }
            chars[j] = chars[i];
        }
        if (i == j)
            return string;
        return new String(chars, 0, j);
    }

    void removeSelectedItem(int index) {
        CTableItem[] newSelectedItems = new CTableItem[selectedItems.length - 1];
        System.arraycopy(selectedItems, 0, newSelectedItems, 0, index);
        System.arraycopy(selectedItems, index + 1, newSelectedItems, index, newSelectedItems.length - index);
        selectedItems = newSelectedItems;
    }

    /**
     * Removes the listener from the collection of listeners who will
     * be notified when the user changes the receiver's selection.
     *
     * @param listener the listener which should no longer be notified
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the listener is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see SelectionListener
     * @see #addSelectionListener(SelectionListener)
     */
    public void removeSelectionListener(SelectionListener listener) {
        checkWidget();
        if (listener == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        removeListener(SWT.Selection, listener);
        removeListener(SWT.DefaultSelection, listener);
    }

    /**
     * Selects the item at the given zero-relative index in the receiver.
     * If the item at the index was already selected, it remains
     * selected. Indices that are out of range are ignored.
     *
     * @param index the index of the item to select
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void select(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            return;
        selectItem(items[index], (getStyle() & SWT.MULTI) != 0);
        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            redrawItem(index, false);
        }
        getAccessible().selectionChanged();
    }

    /**
     * Selects the items in the range specified by the given zero-relative
     * indices in the receiver. The range of indices is inclusive.
     * The current selection is not cleared before the new items are selected.
     * <p>
     * If an item in the given range is not selected, it is selected.
     * If an item in the given range was already selected, it remains selected.
     * Indices that are out of range are ignored and no items will be selected
     * if start is greater than end.
     * If the receiver is single-select and there is more than one item in the
     * given range, then all indices are ignored.
     * </p>
     *
     * @param start the start of the range
     * @param end the end of the range
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#setSelection(int,int)
     */
    public void select(int start, int end) {
        checkWidget();
        if (end < 0 || start > end || ((getStyle() & SWT.SINGLE) != 0 && start != end))
            return;
        if (itemsCount == 0 || start >= itemsCount)
            return;
        start = Math.max(start, 0);
        end = Math.min(end, itemsCount - 1);
        for (int i = start; i <= end; i++) {
            selectItem(items[i], (getStyle() & SWT.MULTI) != 0);
        }
        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            redrawItems(start, end, false);
        }
        getAccessible().selectionChanged();
    }

    /**
     * Selects the items at the given zero-relative indices in the receiver.
     * The current selection is not cleared before the new items are selected.
     * <p>
     * If the item at a given index is not selected, it is selected.
     * If the item at a given index was already selected, it remains selected.
     * Indices that are out of range and duplicate indices are ignored.
     * If the receiver is single-select and multiple indices are specified,
     * then all indices are ignored.
     * </p>
     *
     * @param indices the array of indices for the items to select
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the array of indices is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#setSelection(int[])
     */
    public void select(int[] indices) {
        checkWidget();
        if (indices == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (indices.length == 0 || ((getStyle() & SWT.SINGLE) != 0 && indices.length > 1))
            return;
        for (int index : indices) {
            if (0 <= index && index < itemsCount) {
                selectItem(items[index], (getStyle() & SWT.MULTI) != 0);
            }
        }
        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            for (int index : indices) {
                if (0 <= index && index < itemsCount) {
                    redrawItem(index, false);
                }
            }
        }
        getAccessible().selectionChanged();
    }

    /**
     * Selects all of the items in the receiver.
     * <p>
     * If the receiver is single-select, do nothing.
     * </p>
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void selectAll() {
        checkWidget();
        if ((getStyle() & SWT.SINGLE) != 0)
            return;
        selectedItems = new CTableItem[itemsCount];
        System.arraycopy(items, 0, selectedItems, 0, itemsCount);
        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            redraw();
        }
        for (CTableItem selectedItem : selectedItems) {
            selectedItem.getAccessible(getAccessible(), 0).selectionChanged();
        }
        getAccessible().selectionChanged();
    }

    void selectItem(CTableItem item, boolean addToSelection) {
        CTableItem[] oldSelectedItems = selectedItems;
        if (!addToSelection || (getStyle() & SWT.SINGLE) != 0) {
            selectedItems = new CTableItem[] { item };
            if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
                for (CTableItem oldSelectedItem : oldSelectedItems) {
                    if (oldSelectedItem != item) {
                        redrawItem(oldSelectedItem.index, true);
                    }
                }
            }
            for (CTableItem oldSelectedItem : oldSelectedItems) {
                oldSelectedItem.getAccessible(getAccessible(), 0).selectionChanged();
            }
        } else {
            if (item.isSelected())
                return;
            selectedItems = new CTableItem[selectedItems.length + 1];
            System.arraycopy(oldSelectedItems, 0, selectedItems, 0, oldSelectedItems.length);
            selectedItems[selectedItems.length - 1] = item;
        }

        item.getAccessible(getAccessible(), 0).selectionChanged();
        getAccessible().selectionChanged();
    }

    @Override
    public void setBackground(Color color) {
        checkWidget();
        if (color == null)
            color = display.getSystemColor(SWT.COLOR_LIST_BACKGROUND);
        super.setBackground(color);
    }

    @Override
    public void setForeground(Color color) {
        checkWidget();
        if (color == null)
            color = display.getSystemColor(SWT.COLOR_LIST_FOREGROUND);
        super.setForeground(color);
    }

    /**
     * Sets the order that the items in the receiver should
     * be displayed in to the given argument which is described
     * in terms of the zero-relative ordering of when the items
     * were added.
     *
     * @param order the new order to display the items
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the item order is null</li>
     *    <li>ERROR_INVALID_ARGUMENT - if the item order is not the same length as the number of items</li>
     * </ul>
     *
     * @see CTable#getColumnOrder()
     * @see CTableColumn#getMoveable()
     * @see CTableColumn#setMoveable(boolean)
     * @see SWT#Move
     *
     * @since 3.1
     */
    public void setColumnOrder(int[] order) {
        checkWidget();
        if (order == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (columns.length == 0) {
            if (order.length != 0)
                SWT.error(SWT.ERROR_INVALID_ARGUMENT);
            return;
        }
        if (order.length != columns.length)
            SWT.error(SWT.ERROR_INVALID_ARGUMENT);
        boolean reorder = false;
        boolean[] seen = new boolean[columns.length];
        int[] oldOrder = getColumnOrder();
        for (int i = 0; i < order.length; i++) {
            int index = order[i];
            if (index < 0 || index >= columns.length)
                SWT.error(SWT.ERROR_INVALID_RANGE);
            if (seen[index])
                SWT.error(SWT.ERROR_INVALID_ARGUMENT);
            seen[index] = true;
            if (index != oldOrder[i])
                reorder = true;
        }
        if (!reorder)
            return;

        headerHideToolTip();
        int[] oldX = new int[columns.length];
        for (int i = 0; i < columns.length; i++) {
            oldX[i] = columns[i].getX();
        }
        orderedColumns = new CTableColumn[order.length];
        for (int i = 0; i < order.length; i++) {
            orderedColumns[i] = columns[order[i]];
        }
        for (CTableColumn orderedColumn : orderedColumns) {
            CTableColumn column = orderedColumn;
            if (!column.isDisposed() && column.getX() != oldX[column.getIndex()]) {
                column.notifyListeners(SWT.Move, new Event());
            }
        }

        redraw();
        if (drawCount <= 0 && header.isVisible())
            header.redraw();
    }

    void setFocusItem(CTableItem item, boolean redrawOldFocus) {
        if (item == focusItem)
            return;
        CTableItem oldFocusItem = focusItem;
        if (oldFocusItem != null)
            oldFocusItem.getAccessible(getAccessible(), 0).setFocus(ACC.CHILDID_SELF);
        focusItem = item;
        if (redrawOldFocus && oldFocusItem != null) {
            redrawItem(oldFocusItem.index, true);
        }
        if (focusItem != null)
            focusItem.getAccessible(getAccessible(), 0).setFocus(ACC.CHILDID_SELF);
    }

    @Override
    public void setFont(Font value) {
        checkWidget();
        Font oldFont = getFont();
        super.setFont(value);
        Font font = getFont();
        if (font.equals(oldFont))
            return;

        GC gc = new GC(this);

        /* recompute the receiver's cached font height and item height values */
        fontHeight = gc.getFontMetrics().getHeight();
        setItemHeight(Math.max(fontHeight, imageHeight) + 2 * getCellPadding());
        Point headerSize = header.getSize();
        int newHeaderHeight = Math.max(fontHeight, headerImageHeight) + 2 * getHeaderPadding();
        if (headerSize.y != newHeaderHeight) {
            header.setSize(headerSize.x, newHeaderHeight);
        }
        header.setFont(font);

        /*
         * Notify all columns and items of the font change so that elements that
         * use the receiver's font can recompute their cached string widths.
         */
        for (CTableColumn column : columns) {
            column.updateFont(gc);
        }
        for (int i = 0; i < itemsCount; i++) {
            items[i].updateFont(gc);
        }

        gc.dispose();

        if (drawCount <= 0 && header.isVisible())
            header.redraw();

        /* update scrollbars */
        if (columns.length == 0)
            updateHorizontalBar();
        ScrollBar vBar = getVerticalBar();
        if (vBar != null) {
            int thumb = (clientArea.height - getHeaderHeight()) / itemHeight;
            vBar.setThumb(thumb);
            vBar.setPageIncrement(thumb);
            topIndex = vBar.getSelection();
            vBar.setVisible(thumb < vBar.getMaximum());
        }
        redraw();
    }

    void setHeaderImageHeight(int value) {
        headerImageHeight = value;
        Point headerSize = header.getSize();
        int newHeaderHeight = Math.max(fontHeight, headerImageHeight) + 2 * getHeaderPadding();
        if (headerSize.y != newHeaderHeight) {
            header.setSize(headerSize.x, newHeaderHeight);
        }
    }

    /**
     * Marks the receiver's header as visible if the argument is <code>true</code>,
     * and marks it invisible otherwise.
     * <p>
     * If one of the receiver's ancestors is not visible or some
     * other condition makes the receiver not visible, marking
     * it visible may not actually cause it to be displayed.
     * </p>
     *
     * @param show the new visibility state
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void setHeaderVisible(boolean value) {
        checkWidget();
        if (header.getVisible() == value)
            return; /* no change */
        headerHideToolTip();
        header.setVisible(value);
        updateVerticalBar();
        redraw();
    }

    void setImageHeight(int value) {
        imageHeight = value;
        setItemHeight(Math.max(fontHeight, imageHeight) + 2 * getCellPadding());
    }

    /**
     * Sets the number of items contained in the receiver.
     *
     * @param count the number of items
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @since 3.0
     */
    public void setItemCount(int count) {
        checkWidget();
        count = Math.max(0, count);
        if (count == itemsCount)
            return;
        int oldCount = itemsCount;
        int redrawStart, redrawEnd;

        /* if the new item count is less than the current count then remove all excess items from the end */
        if (count < itemsCount) {
            redrawStart = count;
            redrawEnd = itemsCount - 1;
            for (int i = count; i < itemsCount; i++) {
                items[i].dispose(false);
            }

            int newSelectedCount = 0;
            for (int i = 0; i < selectedItems.length; i++) {
                if (!selectedItems[i].isDisposed())
                    newSelectedCount++;
            }
            if (newSelectedCount != selectedItems.length) {
                /* one or more selected items have been disposed */
                CTableItem[] newSelectedItems = new CTableItem[newSelectedCount];
                int pos = 0;
                for (CTableItem selectedItem : selectedItems) {
                    CTableItem item = selectedItem;
                    if (!item.isDisposed()) {
                        newSelectedItems[pos++] = item;
                    }
                }
                selectedItems = newSelectedItems;
            }

            if (anchorItem != null && anchorItem.isDisposed())
                anchorItem = null;
            if (lastClickedItem != null && lastClickedItem.isDisposed())
                lastClickedItem = null;
            if (focusItem != null && focusItem.isDisposed()) {
                CTableItem newFocusItem = count > 0 ? items[count - 1] : null;
                setFocusItem(newFocusItem, false);
            }
            int[] eventData = new int[5];
            eventData[0] = ACC.DELETE;
            eventData[1] = redrawStart;
            eventData[2] = redrawEnd - redrawStart;
            eventData[3] = 0;
            eventData[4] = 0;
            getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);

            itemsCount = count;
            if (columns.length == 0)
                updateHorizontalBar();
        } else {
            redrawStart = itemsCount;
            redrawEnd = count - 1;
            CTableItem[] newItems = new CTableItem[count];
            System.arraycopy(items, 0, newItems, 0, itemsCount);
            items = newItems;
            for (int i = itemsCount; i < count; i++) {
                items[i] = new CTableItem(this, SWT.NONE, i, false);
                itemsCount++;
            }

            int[] eventData = new int[5];
            eventData[0] = ACC.INSERT;
            eventData[1] = redrawStart;
            eventData[2] = count;
            eventData[3] = 0;
            eventData[4] = 0;
            getAccessible().sendEvent(ACC.EVENT_TABLE_CHANGED, eventData);
            if (oldCount == 0)
                focusItem = items[0];
        }

        updateVerticalBar();
        /*
         * If this is the focus control and the item count is going from 0->!0 or !0->0 then the
         * receiver must be redrawn to ensure that its boundary focus ring is updated.
         */
        if ((oldCount == 0 || itemsCount == 0) && isFocusControl()) {
            redraw();
            return;
        }
        redrawItems(redrawStart, redrawEnd, false);
    }

    boolean setItemHeight(int value) {
        boolean update = !customHeightSet || itemHeight < value;
        if (update)
            itemHeight = value;
        return update;
    }

    /**
     * Marks the receiver's lines as visible if the argument is <code>true</code>,
     * and marks it invisible otherwise. Note that some platforms draw grid lines
     * while others may draw alternating row colors.
     * <p>
     * If one of the receiver's ancestors is not visible or some
     * other condition makes the receiver not visible, marking
     * it visible may not actually cause it to be displayed.
     * </p>
     *
     * @param show the new visibility state
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void setLinesVisible(boolean value) {
        checkWidget();
        if (linesVisible == value)
            return; /* no change */
        linesVisible = value;
        redraw();
    }

    @Override
    public void setMenu(Menu menu) {
        super.setMenu(menu);
        header.setMenu(menu);
    }

    @Override
    public void setRedraw(boolean value) {
        checkWidget();
        if (value) {
            if (--drawCount == 0) {
                if (items.length - itemsCount > 3) {
                    CTableItem[] newItems = new CTableItem[itemsCount];
                    System.arraycopy(items, 0, newItems, 0, itemsCount);
                    items = newItems;
                }
                updateVerticalBar();
                updateHorizontalBar();
            }
        } else {
            drawCount++;
        }
        super.setRedraw(value);
        header.setRedraw(value);
    }

    /**
     * Sets the receiver's selection to the given item.
     * The current selection is cleared before the new item is selected.
     * <p>
     * If the item is not in the receiver, then it is ignored.
     * </p>
     *
     * @param item the item to select
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the item is null</li>
     *    <li>ERROR_INVALID_ARGUMENT - if the item has been disposed</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @since 3.2
     */
    public void setSelection(CTableItem item) {
        checkWidget();
        if (item == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        setSelection(new CTableItem[] { item }, true);
    }

    /**
     * Sets the receiver's selection to be the given array of items.
     * The current selection is cleared before the new items are selected.
     * <p>
     * Items that are not in the receiver are ignored.
     * If the receiver is single-select and multiple items are specified,
     * then all items are ignored.
     * </p>
     *
     * @param items the array of items
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the array of items is null</li>
     *    <li>ERROR_INVALID_ARGUMENT - if one of the items has been disposed</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#deselectAll()
     * @see CTable#select(int[])
     * @see CTable#setSelection(int[])
     */
    public void setSelection(CTableItem[] items) {
        checkWidget();
        if (items == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        setSelection(items, true);
    }

    void setSelection(CTableItem[] items, boolean updateViewport) {
        if (items.length == 0 || ((getStyle() & SWT.SINGLE) != 0 && items.length > 1)) {
            deselectAll();
            return;
        }
        CTableItem[] oldSelection = selectedItems;

        /* remove null and duplicate items */
        int index = 0;
        selectedItems = new CTableItem[items.length]; /* assume all valid items */
        for (CTableItem item2 : items) {
            CTableItem item = item2;
            if (item != null && item.parent == this && !item.isSelected()) {
                selectedItems[index++] = item;
            }
        }
        if (index != items.length) {
            /* an invalid item was provided so resize the array accordingly */
            CTableItem[] temp = new CTableItem[index];
            System.arraycopy(selectedItems, 0, temp, 0, index);
            selectedItems = temp;
        }
        if (selectedItems.length == 0) { /* no valid items */
            deselectAll();
            return;
        }

        boolean tableSelectionChanged = false;
        if (isFocusControl() || (getStyle() & SWT.HIDE_SELECTION) == 0) {
            for (int i = 0; i < oldSelection.length; i++) {
                if (!oldSelection[i].isSelected()) {
                    redrawItem(oldSelection[i].index, true);
                    oldSelection[i].getAccessible(getAccessible(), 0).selectionChanged();
                    tableSelectionChanged = true;
                }
            }
            for (CTableItem selectedItem : selectedItems) {
                redrawItem(selectedItem.index, true);
                selectedItem.getAccessible(getAccessible(), 0).selectionChanged();
                tableSelectionChanged = true;
            }
        }
        if (updateViewport) {
            showItem(selectedItems[0]);
            setFocusItem(selectedItems[0], true);
        }

        if (tableSelectionChanged)
            getAccessible().selectionChanged();
    }

    /**
     * Sets the column used by the sort indicator for the receiver. A null
     * value will clear the sort indicator.  The current sort column is cleared
     * before the new column is set.
     *
     * @param column the column used by the sort indicator or <code>null</code>
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_INVALID_ARGUMENT - if the column is disposed</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @since 3.2
     */
    public void setSortColumn(CTableColumn column) {
        checkWidget();
        if (column != null && column.isDisposed())
            SWT.error(SWT.ERROR_INVALID_ARGUMENT);
        if (column == sortColumn)
            return;
        if (sortColumn != null && !sortColumn.isDisposed()) {
            sortColumn.setSortDirection(SWT.NONE);
        }
        sortColumn = column;
        if (sortColumn != null) {
            sortColumn.setSortDirection(sortDirection);
        }
    }

    /**
     * Sets the direction of the sort indicator for the receiver. The value
     * can be one of <code>UP</code>, <code>DOWN</code> or <code>NONE</code>.
     *
     * @param direction the direction of the sort indicator
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @since 3.2
     */
    public void setSortDirection(int direction) {
        checkWidget();
        if (direction != SWT.UP && direction != SWT.DOWN && direction != SWT.NONE)
            return;
        sortDirection = direction;
        if (sortColumn == null || sortColumn.isDisposed())
            return;
        sortColumn.setSortDirection(sortDirection);
    }

    /**
     * Selects the item at the given zero-relative index in the receiver.
     * The current selection is first cleared, then the new item is selected.
     *
     * @param index the index of the item to select
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#deselectAll()
     * @see CTable#select(int)
     */
    public void setSelection(int index) {
        checkWidget();
        deselectAll();
        if (!(0 <= index && index < itemsCount))
            return;
        selectItem(items[index], false);
        setFocusItem(items[index], true);
        redrawItem(index, true);
        showSelection();
        getAccessible().selectionChanged();
    }

    /**
     * Selects the items in the range specified by the given zero-relative
     * indices in the receiver. The range of indices is inclusive.
     * The current selection is cleared before the new items are selected.
     * <p>
     * Indices that are out of range are ignored and no items will be selected
     * if start is greater than end.
     * If the receiver is single-select and there is more than one item in the
     * given range, then all indices are ignored.
     * </p>
     *
     * @param start the start index of the items to select
     * @param end the end index of the items to select
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#deselectAll()
     * @see CTable#select(int,int)
     */
    public void setSelection(int start, int end) {
        checkWidget();
        deselectAll();
        if (end < 0 || start > end || ((getStyle() & SWT.SINGLE) != 0 && start != end))
            return;
        if (itemsCount == 0 || start >= itemsCount)
            return;
        start = Math.max(0, start);
        end = Math.min(end, itemsCount - 1);
        select(start, end);
        setFocusItem(items[start], true);
        showSelection();
    }

    /**
     * Selects the items at the given zero-relative indices in the receiver.
     * The current selection is cleared before the new items are selected.
     * <p>
     * Indices that are out of range and duplicate indices are ignored.
     * If the receiver is single-select and multiple indices are specified,
     * then all indices are ignored.
     * </p>
     *
     * @param indices the indices of the items to select
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the array of indices is null</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#deselectAll()
     * @see CTable#select(int[])
     */
    public void setSelection(int[] indices) {
        checkWidget();
        if (indices == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        deselectAll();
        int length = indices.length;
        if (length == 0 || ((getStyle() & SWT.SINGLE) != 0 && length > 1))
            return;
        select(indices);
        int focusIndex = -1;
        for (int i = 0; i < indices.length && focusIndex == -1; i++) {
            if (0 <= indices[i] && indices[i] < itemsCount) {
                focusIndex = indices[i];
            }
        }
        if (focusIndex != -1)
            setFocusItem(items[focusIndex], true);
        showSelection();
    }

    /**
     * Sets the zero-relative index of the item which is currently
     * at the top of the receiver. This index can change when items
     * are scrolled or new items are added and removed.
     *
     * @param index the index of the top item
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     */
    public void setTopIndex(int index) {
        checkWidget();
        if (!(0 <= index && index < itemsCount))
            return;
        int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
        if (itemsCount <= visibleItemCount)
            return;
        index = Math.min(index, itemsCount - visibleItemCount);
        if (index == topIndex)
            return;

        update();
        int change = topIndex - index;
        topIndex = index;
        ScrollBar vBar = getVerticalBar();
        if (vBar != null)
            vBar.setSelection(topIndex);
        if (drawCount <= 0) {
            GC gc = new GC(this);
            gc.copyArea(0, 0, clientArea.width, clientArea.height, 0, change * itemHeight);
            gc.dispose();
        }
    }

    /**
     * Shows the column.  If the column is already showing in the receiver,
     * this method simply returns.  Otherwise, the columns are scrolled until
     * the column is visible.
     *
     * @param column the column to be shown
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the column is null</li>
     *    <li>ERROR_INVALID_ARGUMENT - if the column has been disposed</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @since 3.0
     */
    public void showColumn(CTableColumn column) {
        checkWidget();
        if (column == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (column.isDisposed())
            SWT.error(SWT.ERROR_INVALID_ARGUMENT);
        if (column.parent != this)
            return;

        int x = column.getX();
        int rightX = x + column.width;
        if (0 <= x && rightX <= clientArea.width)
            return; /* column is fully visible */

        headerHideToolTip();
        int absX = 0; /* the X of the column irrespective of the horizontal scroll */
        CTableColumn[] orderedColumns = getOrderedColumns();
        for (int i = 0; i < column.getOrderIndex(); i++) {
            absX += orderedColumns[i].width;
        }
        if (x < clientArea.x) { /* column is to left of viewport */
            horizontalOffset = absX;
        } else {
            horizontalOffset = absX + column.width - clientArea.width;
        }
        ScrollBar hBar = getHorizontalBar();
        if (hBar != null)
            hBar.setSelection(horizontalOffset);
        redraw();
        if (drawCount <= 0 && header.isVisible())
            header.redraw();
    }

    /**
     * Shows the item.  If the item is already showing in the receiver,
     * this method simply returns.  Otherwise, the items are scrolled until
     * the item is visible.
     *
     * @param item the item to be shown
     *
     * @exception IllegalArgumentException <ul>
     *    <li>ERROR_NULL_ARGUMENT - if the item is null</li>
     *    <li>ERROR_INVALID_ARGUMENT - if the item has been disposed</li>
     * </ul>
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#showSelection()
     */
    public void showItem(CTableItem item) {
        checkWidget();
        if (item == null)
            SWT.error(SWT.ERROR_NULL_ARGUMENT);
        if (item.isDisposed())
            SWT.error(SWT.ERROR_INVALID_ARGUMENT);
        if (item.parent != this)
            return;

        int index = item.index;
        int visibleItemCount = (clientArea.height - getHeaderHeight()) / itemHeight;
        /* nothing to do if item is already in viewport */
        if (topIndex <= index && index < topIndex + visibleItemCount)
            return;

        if (index <= topIndex) {
            /* item is above current viewport, so show on top */
            setTopIndex(item.index);
        } else {
            /* item is below current viewport, so show on bottom */
            visibleItemCount = Math.max(visibleItemCount, 1); /* item to show should be top item */
            setTopIndex(Math.min(index - visibleItemCount + 1, itemsCount - 1));
        }
    }

    /**
     * Shows the selection.  If the selection is already showing in the receiver,
     * this method simply returns.  Otherwise, the items are scrolled until
     * the selection is visible.
     *
     * @exception SWTException <ul>
     *    <li>ERROR_WIDGET_DISPOSED - if the receiver has been disposed</li>
     *    <li>ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver</li>
     * </ul>
     *
     * @see CTable#showItem(CTableItem)
     */
    public void showSelection() {
        checkWidget();
        if (selectedItems.length == 0)
            return;
        showItem(selectedItems[0]);
    }

    void sortDescent(int[] items) {
        /* Shell Sort from K&R, pg 108 */
        int length = items.length;
        for (int gap = length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < length; i++) {
                for (int j = i - gap; j >= 0; j -= gap) {
                    if (items[j] <= items[j + gap]) {
                        int swap = items[j];
                        items[j] = items[j + gap];
                        items[j + gap] = swap;
                    }
                }
            }
        }
    }

    void sortAscent(int[] items) {
        /* Shell Sort from K&R, pg 108 */
        int length = items.length;
        for (int gap = length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < length; i++) {
                for (int j = i - gap; j >= 0; j -= gap) {
                    if (items[j] >= items[j + gap]) {
                        int swap = items[j];
                        items[j] = items[j + gap];
                        items[j + gap] = swap;
                    }
                }
            }
        }
    }

    void sortAscent(CTableItem[] items) {
        /* Shell Sort from K&R, pg 108 */
        int length = items.length;
        for (int gap = length / 2; gap > 0; gap /= 2) {
            for (int i = gap; i < length; i++) {
                for (int j = i - gap; j >= 0; j -= gap) {
                    if (items[j].index >= items[j + gap].index) {
                        CTableItem swap = items[j];
                        items[j] = items[j + gap];
                        items[j + gap] = swap;
                    }
                }
            }
        }
    }

    void updateColumnWidth(CTableColumn column, int width) {
        headerHideToolTip();
        int oldWidth = column.width;
        int columnX = column.getX();
        int x = columnX + oldWidth - 1; /* -1 ensures that grid line is included */

        update();
        GC gc = new GC(this);
        gc.copyArea(x, 0, clientArea.width - x, clientArea.height, columnX + width - 1,
                0); /* dest x -1 offsets x's -1 above */
        if (width > oldWidth) {
            /* column width grew */
            int change = width - oldWidth + 1; /* +1 offsets x's -1 above */
            /* -1/+1 below ensure that right bound of selection redraws correctly in column */
            redraw(x - 1, 0, change + 1, clientArea.height, false);
        } else {
            int change = oldWidth - width + 1; /* +1 offsets x's -1 above */
            redraw(clientArea.width - change, 0, change, clientArea.height, false);
        }
        /* the focus box must be repainted because its stipple may become shifted as a result of its new width */
        if (focusItem != null)
            redrawItem(focusItem.index, true);

        GC headerGC = new GC(header);
        if (drawCount <= 0 && header.getVisible()) {
            Rectangle headerBounds = header.getClientArea();
            header.update();
            x -= 1; /* -1 ensures that full header column separator is included */
            headerGC.copyArea(x, 0, headerBounds.width - x, headerBounds.height, columnX + width - 2,
                    0); /* dest x -2 offsets x's -1s above */
            if (width > oldWidth) {
                /* column width grew */
                int change = width - oldWidth + 2; /* +2 offsets x's -1s above */
                header.redraw(x, 0, change, headerBounds.height, false);
            } else {
                int change = oldWidth - width + 2; /* +2 offsets x's -1s above */
                header.redraw(headerBounds.width - change, 0, change, headerBounds.height, false);
            }
        }

        column.width = width;

        /*
         * Notify column and all items of column width change so that display labels
         * can be recomputed if needed.
         */
        column.updateWidth(headerGC);
        headerGC.dispose();
        for (int i = 0; i < itemsCount; i++) {
            items[i].updateColumnWidth(column, gc);
        }
        gc.dispose();

        int maximum = 0;
        for (CTableColumn column2 : columns) {
            maximum += column2.width;
        }
        ScrollBar hBar = getHorizontalBar();
        if (hBar != null) {
            hBar.setMaximum(Math.max(1, maximum)); /* setting a value of 0 here is ignored */
            if (hBar.getThumb() != clientArea.width) {
                hBar.setThumb(clientArea.width);
                hBar.setPageIncrement(clientArea.width);
            }
            int oldHorizontalOffset = horizontalOffset; /* hBar.setVisible() can modify horizontalOffset */
            hBar.setVisible(clientArea.width < maximum);
            int selection = hBar.getSelection();
            if (selection != oldHorizontalOffset) {
                horizontalOffset = selection;
                redraw();
                if (drawCount <= 0 && header.getVisible())
                    header.redraw();
            }
        }

        column.notifyListeners(SWT.Resize, new Event());
        CTableColumn[] orderedColumns = getOrderedColumns();
        for (int i = column.getOrderIndex() + 1; i < orderedColumns.length; i++) {
            if (!orderedColumns[i].isDisposed()) {
                orderedColumns[i].notifyListeners(SWT.Move, new Event());
            }
        }

        if (itemsCount == 0)
            redraw(); /* ensure that static focus rectangle updates properly */
    }

    /*
     * This is a naive implementation that computes the value from scratch.
     */
    void updateHorizontalBar() {
        if (drawCount > 0)
            return;
        ScrollBar hBar = getHorizontalBar();
        if (hBar == null)
            return;

        int maxX = 0;
        if (columns.length > 0) {
            for (CTableColumn column : columns) {
                maxX += column.width;
            }
        } else {
            for (int i = 0; i < itemsCount; i++) {
                Rectangle itemBounds = items[i].getCellBounds(0);
                maxX = Math.max(maxX, itemBounds.x + itemBounds.width + horizontalOffset);
            }
        }

        int clientWidth = clientArea.width;
        if (maxX != hBar.getMaximum()) {
            hBar.setMaximum(Math.max(1, maxX)); /* setting a value of 0 here is ignored */
        }
        int thumb = Math.min(clientWidth, maxX);
        if (thumb != hBar.getThumb()) {
            hBar.setThumb(thumb);
            hBar.setPageIncrement(thumb);
        }
        hBar.setVisible(clientWidth < maxX);

        /* reclaim any space now left on the right */
        if (maxX < horizontalOffset + thumb) {
            horizontalOffset = maxX - thumb;
            hBar.setSelection(horizontalOffset);
            redraw();
        } else {
            int selection = hBar.getSelection();
            if (selection != horizontalOffset) {
                horizontalOffset = selection;
                redraw();
            }
        }
    }

    /*
     * Update the horizontal bar, if needed, in response to an item change (eg.- created,
     * disposed, expanded, etc.).  newRightX is the new rightmost X value of the item,
     * and rightXchange is the change that led to the item's rightmost X value becoming
     * newRightX (so oldRightX + rightXchange = newRightX)
     */
    void updateHorizontalBar(int newRightX, int rightXchange) {
        if (drawCount > 0)
            return;
        ScrollBar hBar = getHorizontalBar();
        if (hBar == null)
            return;

        newRightX += horizontalOffset;
        int barMaximum = hBar.getMaximum();
        if (newRightX > barMaximum) { /* item has extended beyond previous maximum */
            hBar.setMaximum(newRightX);
            int clientAreaWidth = clientArea.width;
            int thumb = Math.min(newRightX, clientAreaWidth);
            hBar.setThumb(thumb);
            hBar.setPageIncrement(thumb);
            hBar.setVisible(clientAreaWidth <= newRightX);
            return;
        }

        int previousRightX = newRightX - rightXchange;
        if (previousRightX != barMaximum) {
            /* this was not the rightmost item, so just check for client width change */
            int clientAreaWidth = clientArea.width;
            int thumb = Math.min(barMaximum, clientAreaWidth);
            hBar.setThumb(thumb);
            hBar.setPageIncrement(thumb);
            hBar.setVisible(clientAreaWidth <= barMaximum);
            return;
        }
        updateHorizontalBar(); /* must search for the new rightmost item */
    }

    void updateVerticalBar() {
        if (drawCount > 0)
            return;
        ScrollBar vBar = getVerticalBar();
        if (vBar == null)
            return;

        int pageSize = (clientArea.height - getHeaderHeight()) / itemHeight;
        int maximum = Math.max(1, itemsCount); /* setting a value of 0 here is ignored */
        if (maximum != vBar.getMaximum()) {
            vBar.setMaximum(maximum);
        }
        int thumb = Math.min(pageSize, maximum);
        if (thumb != vBar.getThumb()) {
            vBar.setThumb(thumb);
            vBar.setPageIncrement(thumb);
        }
        vBar.setVisible(pageSize < maximum);

        /* reclaim any space now left on the bottom */
        if (maximum < topIndex + thumb) {
            topIndex = maximum - thumb;
            vBar.setSelection(topIndex);
            redraw();
        } else {
            int selection = vBar.getSelection();
            if (selection != topIndex) {
                topIndex = selection;
                redraw();
            }
        }
    }
}