/******************************************************************************* * Copyright (c) 2002, 2015 Innoopract Informationssysteme GmbH and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Innoopract Informationssysteme GmbH - initial API and implementation * EclipseSource - ongoing development ******************************************************************************/ package org.eclipse.swt.widgets; import static org.eclipse.rap.rwt.internal.textsize.TextSizeUtil.stringExtent; import static org.eclipse.swt.internal.widgets.MarkupUtil.isMarkupEnabledFor; import static org.eclipse.swt.internal.widgets.MarkupUtil.isToolTipMarkupEnabledFor; import static org.eclipse.swt.internal.widgets.MarkupValidator.isValidationDisabledFor; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.internal.lifecycle.WidgetLCA; import org.eclipse.rap.rwt.internal.textsize.TextSizeUtil; import org.eclipse.rap.rwt.internal.theme.Size; import org.eclipse.rap.rwt.internal.theme.ThemeAdapter; import org.eclipse.rap.rwt.template.Template; import org.eclipse.rap.rwt.theme.BoxDimensions; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.events.TreeListener; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.internal.SerializableCompatibility; import org.eclipse.swt.internal.widgets.ICellToolTipAdapter; import org.eclipse.swt.internal.widgets.ICellToolTipProvider; import org.eclipse.swt.internal.widgets.IControlAdapter; import org.eclipse.swt.internal.widgets.IItemHolderAdapter; import org.eclipse.swt.internal.widgets.ITreeAdapter; import org.eclipse.swt.internal.widgets.ItemHolder; import org.eclipse.swt.internal.widgets.MarkupValidator; import org.eclipse.swt.internal.widgets.WidgetTreeUtil; import org.eclipse.swt.internal.widgets.WidgetTreeVisitor; import org.eclipse.swt.internal.widgets.treekit.TreeLCA; import org.eclipse.swt.internal.widgets.treekit.TreeThemeAdapter; /** * Instances of this class provide a selectable user interface object that * displays a hierarchy of items and issues notification when an item in the * hierarchy is selected. * <p> * The item children that may be added to instances of this class must be of * type <code>TreeItem</code>. * </p> * <p> * Style <code>VIRTUAL</code> is used to create a <code>Tree</code> whose * <code>TreeItem</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 trees that are very large or for which <code>TreeItem</code> * population is expensive (for example, retrieving values from an external * source). * </p> * <p> * Here is an example of using a <code>Tree</code> with style * <code>VIRTUAL</code>: <code><pre> * final Tree tree = new Tree(parent, SWT.VIRTUAL | SWT.BORDER); * tree.setItemCount(20); * tree.addListener(SWT.SetData, new Listener() { * public void handleEvent(Event event) { * TreeItem item = (TreeItem)event.item; * TreeItem parentItem = item.getParentItem(); * String text = null; * if (parentItem == null) { * text = "node " + tree.indexOf(item); * } else { * text = parentItem.getText() + " - " + parentItem.indexOf(item); * } * item.setText(text); * System.out.println(text); * item.setItemCount(10); * } * }); * </pre></code> * </p> * <p> * Note that although this class is a subclass of <code>Composite</code>, it * does not make sense to add <code>Control</code> children to it, or set a * layout on it. * </p> * <p> * <dl> * <dt><b>Styles:</b></dt> * <dd>SINGLE, MULTI, CHECK, FULL_SELECTION, VIRTUAL, NO_SCROLL</dd> * <dt><b>Events:</b></dt> * <dd>Selection, DefaultSelection, Collapse, Expand, 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> * * @since 1.0 */ public class Tree extends Composite { private static final TreeItem[] EMPTY_SELECTION = new TreeItem[ 0 ]; // This values must be kept in sync with appearance of list items private static final int MIN_ITEM_HEIGHT = 16; private static final int GRID_WIDTH = 1; private static final Rectangle TEXT_MARGIN = new Rectangle( 3, 0, 8, 0 ); private int itemCount; private int customItemHeight; private TreeItem[] items; final ItemHolder<TreeColumn> columnHolder; private TreeItem[] selection; private boolean linesVisible; private int[] columnOrder; private int itemImageCount; private TreeColumn sortColumn; private int sortDirection; private boolean headerVisible; private final ITreeAdapter treeAdapter; private int scrollLeft; private int topItemIndex; private boolean hasVScrollBar; private boolean hasHScrollBar; private Point itemImageSize; LayoutCache layoutCache; boolean isFlatIndexValid; private int visibleItemsCount; private int preloadedItems; /** * 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#NO_SCROLL * @see Widget#checkSubclass * @see Widget#getStyle */ public Tree( Composite parent, int style ) { super( parent, checkStyle( style ) ); columnHolder = new ItemHolder<>( TreeColumn.class ); treeAdapter = new InternalTreeAdapter(); setTreeEmpty(); sortDirection = SWT.NONE; selection = EMPTY_SELECTION; customItemHeight = -1; layoutCache = new LayoutCache(); } TreeItem[] getCreatedItems() { TreeItem[] result; if( isVirtual() ) { int count = 0; for( int i = 0; i < itemCount; i++ ) { if( items[ i ] != null ) { count++; } } result = new TreeItem[ count ]; count = 0; for( int i = 0; i < itemCount; i++ ) { if( items[ i ] != null ) { result[ count ] = items[ i ]; count++; } } } else { result = new TreeItem[ itemCount ]; System.arraycopy( items, 0, result, 0, itemCount ); } return result; } private void setTreeEmpty() { items = new TreeItem[ 4 ]; // TODO: Not sure if we have to clear the image size???!!! // clearItemImageSize(); } @Override void initState() { removeState( /* CANVAS | */THEME_BACKGROUND ); } @Override @SuppressWarnings("unchecked") public <T> T getAdapter( Class<T> adapter ) { if( adapter == IItemHolderAdapter.class ) { return ( T )new CompositeItemHolder(); } if( adapter == ITreeAdapter.class || adapter == ICellToolTipAdapter.class ) { return ( T )treeAdapter; } if( adapter == WidgetLCA.class ) { return ( T )TreeLCA.INSTANCE; } return super.getAdapter( adapter ); } @Override public void setFont( Font font ) { super.setFont( font ); for( int i = 0; i < itemCount; i++ ) { if( items[ i ] != null ) { items[ i ].clearPreferredWidthBuffers( true ); } } clearCachedHeights(); updateScrollBars(); } // ///////////////////////// // Methods to manage items /** * Sets the number of root-level 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> */ public void setItemCount( int count ) { checkWidget(); int oldItemCount = itemCount; int newItemCount = Math.max( 0, count ); if( newItemCount != oldItemCount ) { int deleteIndex = oldItemCount - 1; while( deleteIndex >= newItemCount ) { TreeItem item = items[ deleteIndex ]; if( item != null && !item.isDisposed() ) { item.dispose(); } else { destroyItem( deleteIndex ); } deleteIndex--; } int length = Math.max( 4, ( newItemCount + 3 ) / 4 * 4 ); TreeItem[] newItems = new TreeItem[ length ]; System.arraycopy( items, 0, newItems, 0, Math.min( newItemCount, itemCount ) ); items = newItems; if( !isVirtual() ) { for( int i = itemCount; i < newItemCount; i++ ) { items[ i ] = new TreeItem( this, SWT.NONE, i ); } } itemCount = newItemCount; invalidateFlatIndex(); updateScrollBars(); redraw(); } } /** * Returns the number of items contained in the receiver that are direct item * children of the receiver. The number that is returned is the number of * roots in the tree. * * @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 itemCount; } /** * Returns a (possibly empty) array of items contained in the receiver that * are direct item children of the receiver. These are the roots of the tree. * <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 * @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 TreeItem[] getItems() { checkWidget(); TreeItem[] result = new TreeItem[ itemCount ]; if( isVirtual() ) { for( int i = 0; i < itemCount; i++ ) { result[ i ] = _getItem( i ); } } else { System.arraycopy( items, 0, result, 0, itemCount ); } return result; } private TreeItem _getItem( int index ) { if( isVirtual() && items[ index ] == null ) { items[ index ] = new TreeItem( this, null, SWT.NONE, index, false ); } return items[ index ]; } /** * 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 TreeItem getItem( int index ) { checkWidget(); if( index < 0 || index >= itemCount ) { SWT.error( SWT.ERROR_INVALID_RANGE ); } return _getItem( index ); } /** * 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 tool item is null</li> * <li>ERROR_INVALID_ARGUMENT - if the tool 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> */ public int indexOf( TreeItem item ) { checkWidget(); if( item == null ) { SWT.error( SWT.ERROR_NULL_ARGUMENT ); } if( item.isDisposed() ) { SWT.error( SWT.ERROR_INVALID_ARGUMENT ); } return item.parent == this ? item.index : -1; } /** * Returns the receiver's parent item, which must be a <code>TreeItem</code> * or null when the receiver is a root. * * @return the receiver's parent 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 TreeItem getParentItem() { checkWidget(); return null; } /** * 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(); for( int i = itemCount - 1; i >= 0; i-- ) { if( items[ i ] != null ) { items[ i ].dispose(); } else { itemCount--; } } setTreeEmpty(); selection = EMPTY_SELECTION; } /** * Shows the item. If the item is already showing in the receiver, this method * simply returns. Otherwise, the items are scrolled and expanded 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 Tree#showSelection() */ public void showItem( TreeItem item ) { checkWidget(); if( item == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } if( item.isDisposed() ) { error( SWT.ERROR_INVALID_ARGUMENT ); } if( item.getParent() == this ) { TreeItem parent = item.getParentItem(); while( parent != null ) { parent.setExpanded( true ); parent = parent.getParentItem(); } int flatIndex = item.getFlatIndex(); int topIndex = getTopItemIndex(); int visibleRows = getVisibleRowCount( false ); if( flatIndex < topIndex ) { // Show item as top item setTopItemIndex( flatIndex ); } else if( visibleRows > 0 && flatIndex >= topIndex + visibleRows ) { // Show item as last item setTopItemIndex( flatIndex - visibleRows + 1 ); } } } /** * Sets the item which is currently at the top of the receiver. * This item can change when items are expanded, collapsed, scrolled * or new items are added or removed. * * @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 Tree#getTopItem() * * @since 1.4 */ public void setTopItem( TreeItem item ) { checkWidget(); if( item == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } if( item.isDisposed() ) { error( SWT.ERROR_INVALID_ARGUMENT ); } if( item.getParent() == this ) { TreeItem parent = item.getParentItem(); while( parent != null ) { parent.setExpanded( true ); parent = parent.getParentItem(); } setTopItemIndex( item.getFlatIndex() ); } } /** * Returns the item which is currently at the top of the receiver. * This item can change when items are expanded, collapsed, scrolled * or new items are added or removed. * * @return the item at the top of 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> * * @since 1.4 */ public TreeItem getTopItem() { checkWidget(); TreeItem result = null; if( itemCount > 0 ) { List<TreeItem> visibleItems = collectVisibleItems( null ); result = visibleItems.get( getTopItemIndex() ); } return result; } private void setTopItemIndex( int index ) { if( index != topItemIndex ) { topItemIndex = index; adjustTopItemIndex(); updateAllItems(); } } int getTopItemIndex() { if( !isVisibleItemsCountValid() ) { adjustTopItemIndex(); } return topItemIndex; } /** * 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 1.3 */ public void showColumn( TreeColumn column ) { checkWidget(); if( column == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } if( column.isDisposed() ) { error( SWT.ERROR_INVALID_ARGUMENT ); } if( column.getParent() == this ) { int index = indexOf( column ); if( 0 <= index && index < getColumnCount() ) { int leftColumnsWidth = 0; int columnWidth = column.getWidth(); int clientWidth = getClientArea().width; int[] columnOrder = getColumnOrder(); boolean found = false; for( int i = 0; i < columnOrder.length; i++ ) { if( index != columnOrder[ i ] ) { int currentColumnWidth = getColumn( columnOrder[ i ] ).getWidth(); if( !found ) { if( isFixedColumn( columnOrder[ i ] ) ) { clientWidth -= currentColumnWidth; } else { leftColumnsWidth += currentColumnWidth; } } } else { found = true; } } if( getColumnLeftOffset( index ) > leftColumnsWidth ) { scrollLeft = leftColumnsWidth; } else if( scrollLeft < leftColumnsWidth + columnWidth - clientWidth ) { scrollLeft = leftColumnsWidth + columnWidth - clientWidth; } } } } /** * 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 Tree#showItem(TreeItem) */ public void showSelection() { checkWidget(); if( selection.length == 0 ) { return; } showItem( selection[ 0 ] ); } // /////////////////////////////////// // Methods to get/set/clear selection /** * Returns an array of <code>TreeItem</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 TreeItem[] getSelection() { checkWidget(); TreeItem[] result = new TreeItem[ selection.length ]; System.arraycopy( selection, 0, result, 0, selection.length ); 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 selection.length; } /** * 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 selection 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> */ public void setSelection( TreeItem selection ) { checkWidget(); if( selection == null ) { SWT.error( SWT.ERROR_NULL_ARGUMENT ); } setSelection( new TreeItem[]{ selection } ); } /** * 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 selection 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 Tree#deselectAll() */ public void setSelection( TreeItem[] selection ) { checkWidget(); if( selection == null ) { SWT.error( SWT.ERROR_NULL_ARGUMENT ); } int length = selection.length; if( ( style & SWT.SINGLE ) != 0 ) { if( length == 0 || length > 1 ) { deselectAll(); } else { TreeItem item = selection[ 0 ]; if( item != null ) { if( item.isDisposed() ) { SWT.error( SWT.ERROR_INVALID_ARGUMENT ); } this.selection = new TreeItem[]{ item }; } } } else { if( length == 0 ) { deselectAll(); } else { // Construct an array that contains all non-null items to be selected TreeItem[] validSelection = new TreeItem[ length ]; int validLength = 0; for( int i = 0; i < length; i++ ) { if( selection[ i ] != null ) { if( selection[ i ].isDisposed() ) { SWT.error( SWT.ERROR_INVALID_ARGUMENT ); } validSelection[ validLength ] = selection[ i ]; validLength++; } } if( validLength > 0 ) { // Copy the above created array to its 'final destination' this.selection = new TreeItem[ validLength ]; System.arraycopy( validSelection, 0, this.selection, 0, validLength ); } } } } /** * Selects an item in the receiver. If the item was already * selected, it remains selected. * * @param item the item to be selected * * @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 1.3 */ public void select( TreeItem item ) { checkWidget(); if( item == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } if( item.isDisposed() ) { error( SWT.ERROR_INVALID_ARGUMENT ); } if( ( style & SWT.SINGLE ) != 0 ) { setSelection( item ); } else { List<TreeItem> selItems = new ArrayList<>( Arrays.asList( selection ) ); if( !selItems.contains( item ) ) { selItems.add( item ); selection = new TreeItem[ selItems.size() ]; selItems.toArray( selection ); } } } /** * 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( ( style & SWT.MULTI ) != 0 ) { final java.util.List<TreeItem> allItems = new ArrayList<>(); WidgetTreeUtil.accept( this, new WidgetTreeVisitor() { @Override public boolean visit( Widget widget ) { if( widget instanceof TreeItem ) { allItems.add( ( TreeItem )widget ); } return true; } } ); selection = new TreeItem[ allItems.size() ]; allItems.toArray( selection ); } } /** * Deselects an item in the receiver. If the item was already * deselected, it remains deselected. * * @param item the item to be deselected * * @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 1.3 */ public void deselect( TreeItem item ) { checkWidget(); if( item == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } if( item.isDisposed() ) { error( SWT.ERROR_INVALID_ARGUMENT ); } List<TreeItem> selItems = new ArrayList<>( Arrays.asList( selection ) ); if( selItems.contains( item ) ) { selItems.remove( item ); selection = new TreeItem[ selItems.size() ]; selItems.toArray( selection ); } } /** * 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(); selection = EMPTY_SELECTION; } /** * Marks the receiver's lines 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 value 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; } /** * 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> * * @since 1.4 */ public int getGridLineWidth() { checkWidget(); return GRID_WIDTH; } /** * Returns <code>true</code> if the receiver's lines are 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 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; } /** * 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 * tree 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 * @param recursive <code>true</code> if all child items of the indexed item * should be cleared recursively, and <code>false</code> otherwise * @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 */ public void clear( int index, boolean recursive ) { checkWidget(); if( index < 0 || index >= itemCount ) { error( SWT.ERROR_INVALID_RANGE ); } TreeItem item = items[ index ]; if( item != null ) { item.clear(); if( recursive ) { item.clearAll( true, false ); } if( isVirtual() ) { redraw(); } } } /** * 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 TreeItem getItem( Point point ) { checkWidget(); if( point == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } TreeItem result = null; int index = ( point.y - getHeaderHeight() ) / getItemHeight() + getTopItemIndex(); List<TreeItem> visibleItems = collectVisibleItems( null ); if( 0 <= index && index < visibleItems.size() ) { result = visibleItems.get( index ); } return result; } /** * Returns the height of the area which would be used to display <em>one</em> * of the items in the tree. * * @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> * * @since 1.3 */ public int getItemHeight() { checkWidget(); int result = customItemHeight; if( result == -1 ) { if( !layoutCache.hasItemHeight() ) { layoutCache.itemHeight = computeItemHeight(); } result = layoutCache.itemHeight; } return result; } /** * Clears all the items in the receiver. The text, icon and other attributes * of the items are set to their default values. If the tree was created with * the <code>SWT.VIRTUAL</code> style, these attributes are requested again as * needed. * * @param recursive <code>true</code> if all child items should be cleared * recursively, and <code>false</code> otherwise * @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 */ public void clearAll( boolean recursive ) { checkWidget(); for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; if( item != null ) { item.clear(); if( recursive ) { item.clearAll( true, false ); } } } if( isVirtual() ) { redraw(); } } @Override public void changed( Control[] changed ) { clearItemsPreferredWidthBuffer(); super.changed( changed ); } private void clearItemsPreferredWidthBuffer() { for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; if( item != null ) { item.clearPreferredWidthBuffers( true ); } } } /** * Returns the number of columns contained in the receiver. If no * <code>TreeColumn</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 tree 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 columnHolder.size(); } void createColumn( TreeColumn column, int index ) { columnHolder.insert( column, index ); if( columnOrder == null ) { columnOrder = new int[]{ index }; } else { int length = columnOrder.length; for( int i = index; i < length; i++ ) { columnOrder[ i ]++; } int[] newColumnOrder = new int[ length + 1 ]; System.arraycopy( columnOrder, 0, newColumnOrder, 0, index ); System.arraycopy( columnOrder, index, newColumnOrder, index + 1, length - index ); columnOrder = newColumnOrder; columnOrder[ index ] = index; } for( int i = 0; i < itemCount; i++ ) { if( items[ i ] != null ) { items[ i ].shiftData( index ); } } updateScrollBars(); } final void destroyColumn( TreeColumn column ) { if( !isInDispose() ) { int index = indexOf( column ); // Remove data from TreeItems for( int i = 0; i < itemCount; i++ ) { if( items[ i ] != null ) { items[ i ].removeData( index ); } } // Reset sort column if necessary if( column == sortColumn ) { sortColumn = null; } // Remove from column holder columnHolder.remove( column ); // Remove from column order int length = columnOrder.length; int[] newColumnOrder = new int[ length - 1 ]; int count = 0; for( int i = 0; i < length; i++ ) { if( columnOrder[ i ] != index ) { int newOrder = columnOrder[ i ]; if( index < newOrder ) { newOrder--; } newColumnOrder[ count ] = newOrder; count++; } } columnOrder = newColumnOrder; updateScrollBars(); } } /** * 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> */ public int getHeaderHeight() { checkWidget(); if( !layoutCache.hasHeaderHeight() ) { layoutCache.headerHeight = computeHeaderHeight(); } return layoutCache.headerHeight; } /** * 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 value 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( headerVisible != value ) { headerVisible = value; layoutCache.invalidateHeaderHeight(); updateScrollBars(); } } /** * 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 headerVisible; } /** * 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( TreeColumn column ) { checkWidget(); if( column == null ) { SWT.error( SWT.ERROR_NULL_ARGUMENT ); } if( column.isDisposed() ) { error( SWT.ERROR_INVALID_ARGUMENT ); } return columnHolder.indexOf( column ); } /** * 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>TreeColumn</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 tree. This occurs when the programmer uses the * tree 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 Tree#getColumnOrder() * @see Tree#setColumnOrder(int[]) * @see TreeColumn#getMoveable() * @see TreeColumn#setMoveable(boolean) * @see SWT#Move */ public TreeColumn getColumn( int index ) { checkWidget(); if( !( 0 <= index && index < columnHolder.size() ) ) { error( SWT.ERROR_INVALID_RANGE ); } return columnHolder.getItem( index ); } /** * Returns an array of <code>TreeColumn</code>s which are the columns in the * receiver. Columns are returned in the order that they were created. If no * <code>TreeColumn</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 tree 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 Tree#getColumnOrder() * @see Tree#setColumnOrder(int[]) * @see TreeColumn#getMoveable() * @see TreeColumn#setMoveable(boolean) * @see SWT#Move */ public TreeColumn[] getColumns() { checkWidget(); return columnHolder.getItems(); } /** * 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 Tree#getColumnOrder() * @see TreeColumn#getMoveable() * @see TreeColumn#setMoveable(boolean) * @see SWT#Move */ public void setColumnOrder( int[] order ) { checkWidget(); if( order == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } int columnCount = getColumnCount(); if( order.length != columnCount ) { error( SWT.ERROR_INVALID_ARGUMENT ); } if( columnCount > 0 ) { int[] oldOrder = new int[ columnCount ]; System.arraycopy( columnOrder, 0, oldOrder, 0, columnOrder.length ); boolean reorder = false; boolean[] seen = new boolean[ columnCount ]; for( int i = 0; i < order.length; i++ ) { int index = order[ i ]; if( index < 0 || index >= columnCount ) { error( SWT.ERROR_INVALID_RANGE ); } if( seen[ index ] ) { error( SWT.ERROR_INVALID_ARGUMENT ); } seen[ index ] = true; if( index != oldOrder[ i ] ) { reorder = true; } } if( reorder ) { System.arraycopy( order, 0, columnOrder, 0, columnOrder.length ); for( int i = 0; i < seen.length; i++ ) { if( oldOrder[ i ] != columnOrder[ i ] ) { TreeColumn column = getColumn( columnOrder[ i ] ); column.notifyListeners( SWT.Move, new Event() ); } } } } } /** * 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> */ public void setSortColumn( TreeColumn column ) { checkWidget(); if( column != null && column.isDisposed() ) { 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> */ 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 ); } /** * 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(TreeColumn) */ public TreeColumn 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) */ public int getSortDirection() { checkWidget(); return sortDirection; } /** * 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 Tree#setColumnOrder(int[]) * @see TreeColumn#getMoveable() * @see TreeColumn#setMoveable(boolean) * @see SWT#Move */ public int[] getColumnOrder() { checkWidget(); int[] result; if( columnHolder.size() == 0 ) { result = new int[ 0 ]; } else { result = new int[ columnOrder.length ]; System.arraycopy( columnOrder, 0, result, 0, columnOrder.length ); } return result; } /////////////////////////////////////// // Listener registration/deregistration /** * Adds the listener to the collection of listeners who will be notified when * the receiver's selection changes, 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 <code>SWT.CHECK</code> style set 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 * @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 ); } /** * Removes the listener from the collection of listeners who will be notified * when the receiver's selection changes. * * @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 */ public void removeSelectionListener( SelectionListener listener ) { checkWidget(); if( listener == null ) { SWT.error( SWT.ERROR_NULL_ARGUMENT ); } removeListener( SWT.Selection, listener ); removeListener( SWT.DefaultSelection, listener ); } /** * Adds the listener to the collection of listeners who will be notified when * an item in the receiver is expanded or collapsed by sending it one of the * messages defined in the <code>TreeListener</code> interface. * * @param listener the listener which should 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 TreeListener * @see #removeTreeListener */ public void addTreeListener( TreeListener listener ) { checkWidget(); if( listener == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } TypedListener typedListener = new TypedListener( listener ); addListener( SWT.Expand, typedListener ); addListener( SWT.Collapse, typedListener ); } /** * Removes the listener from the collection of listeners who will be notified * when items in the receiver are expanded or collapsed. * * @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 TreeListener * @see #addTreeListener */ public void removeTreeListener( TreeListener listener ) { checkWidget(); if( listener == null ) { error( SWT.ERROR_NULL_ARGUMENT ); } removeListener( SWT.Expand, listener ); removeListener( SWT.Collapse, listener ); } @Override public void setData( String key, Object value ) { if( RWT.CUSTOM_VARIANT.equals( key ) ) { layoutCache.invalidateAll(); } else if( RWT.CUSTOM_ITEM_HEIGHT.equals( key ) ) { setCustomItemHeight( value ); } else if( RWT.PRELOADED_ITEMS.equals( key ) ) { setPreloadedItems( value ); } if( !RWT.MARKUP_ENABLED.equals( key ) || !isMarkupEnabledFor( this ) ) { super.setData( key, value ); } } ///////////////////////////////// // Methods to cleanup on dispose @Override void releaseChildren() { for( int i = items.length - 1; i >= 0; i-- ) { if( items[ i ] != null ) { items[ i ].dispose(); } } TreeColumn[] cols = columnHolder.getItems(); for( int c = 0; c < cols.length; c++ ) { cols[ c ].dispose(); columnHolder.remove( cols[ c ] ); } super.releaseChildren(); } void removeFromSelection( TreeItem item ) { int index = -1; for( int i = 0; index == -1 && i < selection.length; i++ ) { if( selection[ i ] == item ) { index = i; } } if( index != -1 ) { TreeItem[] newSelection = new TreeItem[ selection.length - 1 ]; System.arraycopy( selection, 0, newSelection, 0, index ); if( index < selection.length - 1 ) { int length = selection.length - index - 1; System.arraycopy( selection, index + 1, newSelection, index, length ); } selection = newSelection; } } ///////////////////// // Widget dimensions @Override public Point computeSize( int wHint, int hHint, boolean changed ) { checkWidget(); int width = 0; int height = 0; if( getColumnCount() > 0 ) { for( int i = 0; i < getColumnCount(); i++ ) { width += getColumn( i ).getWidth(); } } else { for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; if( item != null && item.isCached() ) { int itemWidth = getPreferredCellWidth( item, 0 ); width = Math.max( width, itemWidth ); if( item.getExpanded() ) { int innerWidth = getMaxInnerWidth( item.items, 0, 1, false ); width = Math.max( width, innerWidth ); } } } } height += getHeaderHeight(); height += itemCount * getItemHeight(); for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; if( item != null && !item.isInDispose() && item.getExpanded() ) { height += item.getInnerHeight(); } } if( width == 0 ) { width = DEFAULT_WIDTH; } if( height == 0 ) { height = DEFAULT_HEIGHT; } if( wHint != SWT.DEFAULT ) { width = wHint; } if( hHint != SWT.DEFAULT ) { height = hHint; } BoxDimensions border = getBorder(); width += border.left + border.right; height += border.top + border.bottom; if( ( style & SWT.V_SCROLL ) != 0 ) { width += getVerticalBar().getSize().x; } if( ( style & SWT.H_SCROLL ) != 0 ) { height += getHorizontalBar().getSize().y; } return new Point( width, height ); } private void setCustomItemHeight( Object value ) { if( value == null ) { customItemHeight = -1; } else { if( !( value instanceof Integer ) ) { error( SWT.ERROR_INVALID_ARGUMENT ); } int itemHeight = ( ( Integer )value ).intValue(); if( itemHeight < 0 ) { error( SWT.ERROR_INVALID_RANGE ); } customItemHeight = itemHeight; } } private void setPreloadedItems( Object value ) { if( value == null ) { preloadedItems = 0; } else { if( !( value instanceof Integer ) ) { error( SWT.ERROR_INVALID_ARGUMENT ); } preloadedItems = ( ( Integer )value ).intValue(); if( preloadedItems < 0 ) { error( SWT.ERROR_INVALID_RANGE ); } } } ///////////////////// // item layout helper int getMaxContentWidth( TreeColumn column ) { return getMaxInnerWidth( items, indexOf( column ), 1, true ); } private int getMaxInnerWidth( TreeItem[] items, int columnIndex, int level, boolean clearBuffer ) { int maxInnerWidth = 0; for( int i = 0; i < items.length; i++ ) { TreeItem item = items[ i ]; if( item != null && item.isCached() ) { int indention = columnIndex == 0 ? level * getIndentionWidth() : 0; // TODO [tb] : test if( clearBuffer ) { item.clearPreferredWidthBuffers( false ); } int itemWidth = getPreferredCellWidth( item, columnIndex ) + indention; maxInnerWidth = Math.max( maxInnerWidth, itemWidth ); if( item.getExpanded() ) { int innerWidth = getMaxInnerWidth( item.items, columnIndex, level + 1, clearBuffer ); maxInnerWidth = Math.max( maxInnerWidth, innerWidth ); } } } return maxInnerWidth; } int getCellLeft( int index ) { return getColumnCount() == 0 ? 0 : getColumn( index ).getLeft(); } private int getCellWidth( int index ) { return getColumnCount() == 0 && index == 0 ? getMaxInnerWidth( items, 0, 1, false ) : getColumn( index ).getWidth(); } int getImageOffset( int index ) { // Note: The left cell-padding is visually ignored for the tree-column int result = isTreeColumn( index ) ? 0 : getCellPadding().left; if( hasCheckBoxes( index ) ) { result += getCheckImageOuterSize().width; } return result; } private int getTextOffset( int index ) { int result = getImageOffset( index ); result += getItemImageOuterWidth( index ); if( isTreeColumn( index ) ) { result += TEXT_MARGIN.x; } return result; } int getTextWidth( int index ) { BoxDimensions padding = getCellPadding(); int result = getCellWidth( index ) - getTextOffset( index ) - padding.right; if( isTreeColumn( index ) ) { result -= ( TEXT_MARGIN.width - TEXT_MARGIN.x ); } return Math.max( 0, result ); } int getIndentionOffset( TreeItem item ) { return getIndentionWidth() * ( item.depth + 1); } int getVisualTextLeft( TreeItem item, int index ) { return getVisualCellLeft( item, index ) + getCellPadding().left + getItemImageOuterWidth( index ); } int getVisualCellLeft( TreeItem item, int index ) { int result = getCellLeft( index ) - getColumnLeftOffset( index ); if( isTreeColumn( index ) ) { result += getIndentionOffset( item ); } if( hasCheckBoxes( index ) ) { result += getCheckImageOuterSize().width; } return result; } int getVisualTextWidth( TreeItem item, int index ) { int result = 0; if( index == 0 && getColumnCount() == 0 ) { result = getStringExtent( item.getFont(), item.getText( 0 ) ).x + TEXT_MARGIN.width; } else if( index >= 0 && index < getColumnCount() ) { result = getTextWidth( index ) - getIndentionOffset( item ); result = Math.max( 0, result ); } return result; } int getVisualCellWidth( TreeItem item, int index ) { int result; if( getColumnCount() == 0 && index == 0 ) { BoxDimensions padding = getCellPadding(); int paddingWidth = padding.left + padding.right; int textWidth = getStringExtent( item.getFont(), item.getText( 0 ) ).x; result = paddingWidth + getItemImageOuterWidth( index ) + textWidth + TEXT_MARGIN.width; } else { result = getColumn( index ).getWidth(); if( isTreeColumn( index ) ) { result -= getIndentionOffset( item ); } if( hasCheckBoxes( index ) ) { result -= getCheckImageOuterSize().width; } result = Math.max( 0, result ); } return result; } int getPreferredCellWidth( TreeItem item, int index ) { int result = item.getPreferredWidthBuffer( index ); if( !item.hasPreferredWidthBuffer( index ) ) { BoxDimensions padding = getCellPadding(); result = getTextOffset( index ) ; result += getStringExtent( item.getFont(), item.getTextWithoutMaterialize( index ) ).x; result += padding.right; if( isTreeColumn( index ) ) { result += TEXT_MARGIN.width - TEXT_MARGIN.x; } item.setPreferredWidthBuffer( index, result ); } return result; } private Point getStringExtent( Font font, String text ) { return stringExtent( font, text, isMarkupEnabledFor( this ) ); } boolean isTreeColumn( int index ) { return index == 0 && getColumnCount() == 0 || getColumnCount() > 0 && getColumnOrder()[ 0 ] == index; } /** * Returns the scroll-offset of the column, which is the leftOffset unless it is a fixed column. */ final int getColumnLeftOffset( int columnIndex ) { int result = scrollLeft; if( columnIndex >= 0 ) { result = isFixedColumn( columnIndex ) ? 0 : scrollLeft; } return result; } private boolean isFixedColumn( int index ) { int[] columnOrder = getColumnOrder(); int visualIndex = -1; for( int i = 0; i < columnOrder.length && visualIndex == -1; i++ ) { if( index == columnOrder[ i ] ) { visualIndex = i; } } return visualIndex < getFixedColumns(); } private int getFixedColumns() { Object fixedColumns = getData( RWT.FIXED_COLUMNS ); if( fixedColumns instanceof Integer ) { if( !( getData( RWT.ROW_TEMPLATE ) instanceof Template ) ) { return ( ( Integer )fixedColumns ).intValue(); } } return -1; } private boolean hasCheckBoxes( int index ) { return ( style & SWT.CHECK ) != 0 && isTreeColumn( index ); } private boolean hasColumnImages( int columnIndex ) { int count = columnIndex == 0 ? itemImageCount : getColumn( columnIndex ).itemImageCount; return count > 0; } void updateColumnImageCount( int columnIndex, Image oldImage, Image newImage ) { int delta = 0; if( oldImage == null && newImage != null ) { delta = +1; } else if( oldImage != null && newImage == null ) { delta = -1; } if( delta != 0 ) { if( columnIndex == 0 ) { itemImageCount += delta; } else { TreeColumn column = getColumn( columnIndex ); column.itemImageCount += delta; } } } void updateItemImageSize( Image image ) { if( image != null && itemImageSize == null ) { Rectangle imageBounds = image.getBounds(); itemImageSize = new Point( imageBounds.width, imageBounds.height ); layoutCache.invalidateItemHeight(); } } Point getItemImageSize( int index ) { Point result; if( hasColumnImages( index ) ) { result = getItemImageSize(); if( getColumnCount() > 0 ) { int availWidth = getColumn( index ).getWidth(); availWidth -= getCellPadding().left; availWidth = Math.max( 0, availWidth ); result.x = Math.min( result.x, availWidth ); } } else { result = new Point( 0, 0 ); } return result; } private int getItemImageOuterWidth( int index ) { int result = 0; if( hasColumnImages( index ) ) { result += getItemImageSize( index ).x; result += getCellSpacing(); } return result; } private Point getItemImageSize() { Point result = new Point( 0, 0 ); if( itemImageSize != null ) { result.x = itemImageSize.x; result.y = itemImageSize.y; } return result; } private Size getCheckImageSize() { return getThemeAdapter().getCheckBoxImageSize( this ); } TreeThemeAdapter getThemeAdapter() { return ( TreeThemeAdapter )getAdapter( ThemeAdapter.class ); } private Size getCheckImageOuterSize() { Size result = getCheckImageSize(); BoxDimensions margin = getCheckBoxMargin(); int width = result.width + margin.left + margin.right; int height = result.height + margin.top + margin.bottom; return new Size( width, height ); } private BoxDimensions getCheckBoxMargin() { if( !layoutCache.hasCheckBoxMargin() ) { layoutCache.checkBoxMargin = getThemeAdapter().getCheckBoxMargin( this ); } return layoutCache.checkBoxMargin; } private int getIndentionWidth() { return getThemeAdapter().getIndentionWidth( this ); } private int computeHeaderHeight() { int result = 0; if( headerVisible ) { Font headerFont = getHeaderFont(); int textHeight = TextSizeUtil.getCharHeight( headerFont ); int imageHeight = 0; for( int i = 0; i < getColumnCount(); i++ ) { TreeColumn column = columnHolder.getItem( i ); if( column.getText().contains( "\n" ) ) { int columnTextHeight = TextSizeUtil.textExtent( headerFont, column.getText(), 0 ).y; textHeight = Math.max( textHeight, columnTextHeight ); } Image image = getColumn( i ).getImage(); int height = image == null ? 0 : image.getBounds().height; if( height > imageHeight ) { imageHeight = height; } } result = Math.max( textHeight, imageHeight ); TreeThemeAdapter themeAdapter = getThemeAdapter(); BoxDimensions headerPadding = themeAdapter.getHeaderPadding( this ); result += themeAdapter.getHeaderBorderBottomWidth( this ); result += headerPadding.top + headerPadding.bottom; } return result; } Font getHeaderFont() { IControlAdapter controlAdapter = getAdapter( IControlAdapter.class ); Font result = controlAdapter.getUserFont(); if( result == null ) { result = getThemeAdapter().getHeaderFont( this ); } return result; } private int computeItemHeight() { BoxDimensions padding = getCellPadding(); int paddingHeight = padding.top + padding.bottom; int textHeight = TextSizeUtil.getCharHeight( getFont() ); textHeight += TEXT_MARGIN.height + paddingHeight; int itemImageHeight = getItemImageSize().y + paddingHeight; int result = Math.max( itemImageHeight, textHeight ); if( hasCheckBoxes( 0 ) ) { result = Math.max( getCheckImageOuterSize().height, result ); } result += 1; // The space needed for horizontal gridline is always added, even if not visible result = Math.max( result, MIN_ITEM_HEIGHT ); return result; } /////////////////// // Helping methods @Override void notifyResize( Point oldSize ) { if( !oldSize.equals( getSize() ) && !TextSizeUtil.isTemporaryResize() ) { clearCachedHeights(); updateAllItems(); updateScrollBars(); adjustTopItemIndex(); } super.notifyResize( oldSize ); } void clearCachedHeights() { layoutCache.invalidateHeaderHeight(); layoutCache.invalidateItemHeight(); } private void adjustTopItemIndex() { int visibleItems = getVisibleItemsCount(); int visibleRows = getVisibleRowCount( false ); int correction = visibleRows == 0 ? 1 : 0; if( topItemIndex > visibleItems - visibleRows - correction ) { topItemIndex = Math.max( 0, visibleItems - visibleRows - correction ); } } final int getVisibleRowCount( boolean includePartlyVisible ) { int clientHeight = getClientArea().height - getHeaderHeight(); int result = 0; if( clientHeight >= 0 ) { int itemHeight = getItemHeight(); result = clientHeight / itemHeight; if( includePartlyVisible && clientHeight % itemHeight != 0 ) { result++; } } return result; } private int getVisibleItemsCount() { if( !isVisibleItemsCountValid() ) { visibleItemsCount = collectVisibleItems( null ).size(); } return visibleItemsCount; } private boolean isVisibleItemsCountValid() { return visibleItemsCount != -1; } private List<TreeItem> collectVisibleItems( TreeItem parentItem ) { List<TreeItem> result = new ArrayList<>(); TreeItem[] items = parentItem == null ? this.items : parentItem.items; int itemCount = parentItem == null ? this.itemCount : parentItem.itemCount; for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; result.add( item ); if( item != null && item.getExpanded() ) { result.addAll( collectVisibleItems( item ) ); } } return result; } void updateAllItems() { int flatIndex = 0; for( int index = 0; index < itemCount; index++ ) { flatIndex = updateAllItemsRecursively( null, index, flatIndex ); } isFlatIndexValid = true; visibleItemsCount = flatIndex; } private int updateAllItemsRecursively( TreeItem parent, int index, int flatIndex ) { int newFlatIndex = flatIndex; TreeItem item = parent == null ? items[ index ] : parent.items[ index ]; if( shouldResolveItem( flatIndex ) ) { if( item == null ) { item = parent == null ? _getItem( index ) : parent._getItem( index ); } checkData( item, index ); } if( item != null ) { item.setFlatIndex( newFlatIndex ); } newFlatIndex++; if( item != null && item.getExpanded() ) { for( int i = 0; i < item.itemCount; i++ ) { newFlatIndex = updateAllItemsRecursively( item, i, newFlatIndex ); } } return newFlatIndex; } private boolean shouldResolveItem( int flatIndex ) { int visibleRows = getVisibleRowCount( true ); int topIndex = getTopItemIndex(); int startIndex = topIndex - preloadedItems; int endIndex = topIndex + visibleRows + preloadedItems; return isVirtual() ? flatIndex >= startIndex && flatIndex < endIndex : false; } final boolean checkData( TreeItem item, int index ) { boolean result = true; if( isVirtual() && !item.isCached() ) { item.markCached(); Event event = new Event(); event.item = item; event.index = index; notifyListeners( SWT.SetData, event ); // widget could be disposed at this point if( isDisposed() || item.isDisposed() ) { result = false; } } return result; } void invalidateFlatIndex() { visibleItemsCount = -1; isFlatIndexValid = false; } private static int checkStyle( int style ) { int result = style; if( ( style & SWT.NO_SCROLL ) == 0 ) { result |= SWT.H_SCROLL | SWT.V_SCROLL; } return checkBits( result, SWT.SINGLE, SWT.MULTI, 0, 0, 0, 0 ); } BoxDimensions getCellPadding() { if( !layoutCache.hasCellPadding() ) { layoutCache.cellPadding = getThemeAdapter().getCellPadding( this ); } return layoutCache.cellPadding; } int getCellSpacing() { if( !layoutCache.hasCellSpacing() ) { layoutCache.cellSpacing = getThemeAdapter().getCellSpacing( this ); } return layoutCache.cellSpacing; } /////////////////////////////////////// // Helping methods - dynamic scrollbars boolean hasVScrollBar() { return hasVScrollBar; } boolean hasHScrollBar() { return hasHScrollBar; } @Override int getVScrollBarWidth() { int result = 0; if( hasVScrollBar() ) { result = getVerticalBar().getSize().x; } return result; } @Override int getHScrollBarHeight() { int result = 0; if( hasHScrollBar() ) { result = getHorizontalBar().getSize().y; } return result; } boolean needsVScrollBar() { int availableHeight = getClientArea().height; int height = getHeaderHeight(); height += itemCount * getItemHeight(); for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; if( item != null && item.getExpanded() ) { height += item.getInnerHeight(); } } return height > availableHeight; } boolean needsHScrollBar() { boolean result = false; int availableWidth = getClientArea().width; int columnCount = getColumnCount(); if( columnCount > 0 ) { int totalWidth = 0; for( int i = 0; i < columnCount; i++ ) { TreeColumn column = getColumn( i ); totalWidth += column.getWidth(); } result = totalWidth > availableWidth; } else { int maxWidth = 0; for( int i = 0; i < itemCount; i++ ) { TreeItem item = items[ i ]; if( item != null && !item.isInDispose() && item.isCached() ) { int itemWidth = getPreferredCellWidth( item, 0 ); maxWidth = Math.max( maxWidth, itemWidth ); if( item.getExpanded() ) { int innerWidth = getMaxInnerWidth( item.items, 0, 1, false ); maxWidth = Math.max( maxWidth, innerWidth ); } } } result = maxWidth > availableWidth; } return result; } void updateScrollBars() { if( ( style & SWT.NO_SCROLL ) == 0 ) { hasVScrollBar = false; hasHScrollBar = needsHScrollBar(); if( needsVScrollBar() ) { hasVScrollBar = true; hasHScrollBar = needsHScrollBar(); } getHorizontalBar().setVisible( hasHScrollBar ); getVerticalBar().setVisible( hasVScrollBar ); } } boolean isVirtual() { return ( style & SWT.VIRTUAL ) != 0; } void createItem( TreeItem item, int index ) { if( itemCount == items.length ) { /* * Grow the array faster when redraw is off or the table is not visible. * When the table is painted, the items array is resized to be smaller to * reduce memory usage. */ boolean small = /* drawCount == 0 && */isVisible(); int length = small ? items.length + 4 : Math.max( 4, items.length * 3 / 2 ); TreeItem[] newItems = new TreeItem[ length ]; System.arraycopy( items, 0, newItems, 0, items.length ); items = newItems; } System.arraycopy( items, index, items, index + 1, itemCount - index ); items[ index ] = item; itemCount++; adjustItemIndices( index ); } void destroyItem( int index ) { itemCount--; if( itemCount == 0 ) { setTreeEmpty(); } else { System.arraycopy( items, index + 1, items, index, itemCount - index ); items[ itemCount ] = null; } adjustItemIndices( index ); } private void adjustItemIndices( int start ) { for( int i = start; i < itemCount; i++ ) { if( items[ i ] != null ) { items[ i ].index = i; } } } /////////////////// // Skinning support @Override void reskinChildren( int flags ) { for( int i = 0; i < itemCount; i++ ) { if( items[ i ] != null ) { items[ i ].reskinChildren( flags ); } } TreeColumn[] columns = getColumns(); if( columns != null ) { for( int i = 0; i < columns.length; i++ ) { TreeColumn column = columns[ i ]; if( !column.isDisposed() ) { column.reskinChildren( flags ); } } } super.reskinChildren( flags ); } //////////////// // Inner classes private final class CompositeItemHolder implements IItemHolderAdapter<Item> { @Override public void add( Item item ) { if( item instanceof TreeColumn ) { columnHolder.add( ( TreeColumn )item ); } else { String msg = "Only TreeColumns may be added to CompositeItemHolder"; throw new IllegalArgumentException( msg ); } } @Override public void insert( Item item, int index ) { if( item instanceof TreeColumn ) { columnHolder.insert( ( TreeColumn )item, index ); } else { String msg = "Only TreeColumns may be inserted to CompositeItemHolder"; throw new IllegalArgumentException( msg ); } } @Override public void remove( Item item ) { if( item instanceof TreeColumn ) { columnHolder.remove( ( TreeColumn )item ); } else { String msg = "Only TreeColumns may be removed from CompositeItemHolder"; throw new IllegalArgumentException( msg ); } } @Override public Item[] getItems() { TreeItem[] items = getCreatedItems(); Item[] columns = columnHolder.getItems(); Item[] result = new Item[ columns.length + items.length ]; System.arraycopy( columns, 0, result, 0, columns.length ); System.arraycopy( items, 0, result, columns.length, items.length ); return result; } } private final class InternalTreeAdapter implements ITreeAdapter, ICellToolTipAdapter, SerializableCompatibility { private String toolTipText; private ICellToolTipProvider provider; @Override public void checkData() { updateAllItems(); } @Override public void setScrollLeft( int left ) { scrollLeft = left; } @Override public int getScrollLeft() { return scrollLeft; } @Override public boolean isCached( TreeItem item ) { return item.isCached(); } @Override public Point getItemImageSize( int index ) { return Tree.this.getItemImageSize( index ); } @Override public int getCellLeft( int index ) { return Tree.this.getCellLeft( index ); } @Override public int getCellWidth( int index ) { return Tree.this.getCellWidth( index ); } @Override public int getTextOffset( int index ) { return Tree.this.getTextOffset( index ); } @Override public int getTextMaxWidth( int index ) { return getTextWidth( index ); } @Override public int getCheckWidth() { return getCheckImageSize().width; } @Override public int getImageOffset( int index ) { return Tree.this.getImageOffset( index ); } @Override public int getIndentionWidth() { return Tree.this.getIndentionWidth(); } @Override public int getCheckLeft() { return getCheckBoxMargin().left; } @Override public Rectangle getTextMargin() { return TEXT_MARGIN; } @Override public int getTopItemIndex() { return Tree.this.getTopItemIndex(); } @Override public void setTopItemIndex( int index ) { Tree.this.setTopItemIndex( index ); } @Override public int getColumnLeft( TreeColumn column ) { int index = Tree.this.indexOf( column ); return getColumn( index ).getLeft(); } @Override public ICellToolTipProvider getCellToolTipProvider() { return provider; } @Override public void setCellToolTipProvider( ICellToolTipProvider provider ) { this.provider = provider; } @Override public String getCellToolTipText() { return toolTipText; } @Override public void setCellToolTipText( String toolTipText ) { if( toolTipText != null && isToolTipMarkupEnabledFor( Tree.this ) && !isValidationDisabledFor( Tree.this ) ) { MarkupValidator.getInstance().validate( toolTipText ); } this.toolTipText = toolTipText; } @Override public int getFixedColumns() { return Tree.this.getFixedColumns(); } @Override public boolean isFixedColumn( TreeColumn column ) { return Tree.this.isFixedColumn( Tree.this.indexOf( column ) ); } } static final class LayoutCache implements SerializableCompatibility { private static final int UNKNOWN = -1; int headerHeight = UNKNOWN; int itemHeight = UNKNOWN; int cellSpacing = UNKNOWN; BoxDimensions cellPadding; BoxDimensions checkBoxMargin; public boolean hasHeaderHeight() { return headerHeight != UNKNOWN; } public void invalidateHeaderHeight() { headerHeight = UNKNOWN; } public boolean hasItemHeight() { return itemHeight != UNKNOWN; } public void invalidateItemHeight() { itemHeight = UNKNOWN; } public boolean hasCellSpacing() { return cellSpacing != UNKNOWN; } public void invalidateCellSpacing() { cellSpacing = UNKNOWN; } public boolean hasCellPadding() { return cellPadding != null; } public void invalidateCellPadding() { cellPadding = null; } public boolean hasCheckBoxMargin() { return checkBoxMargin != null; } public void invalidateCheckBoxMargin() { checkBoxMargin = null; } public void invalidateAll() { invalidateHeaderHeight(); invalidateItemHeight(); invalidateCellSpacing(); invalidateCellPadding(); invalidateCheckBoxMargin(); } } }