/******************************************************************************* * Copyright (c) 2014 Mentor Graphics 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: * Mentor Graphics - initial API and implementation *******************************************************************************/ package com.codesourcery.internal.installer.ui; import java.util.ArrayList; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.ICheckable; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.ISelectionProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.MouseAdapter; import org.eclipse.swt.events.MouseEvent; import org.eclipse.swt.events.MouseMoveListener; import org.eclipse.swt.events.MouseTrackAdapter; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.TraverseEvent; import org.eclipse.swt.events.TraverseListener; 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.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.graphics.TextLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.ScrollBar; import com.codesourcery.internal.installer.ui.DetailTreeItem.ItemRegion; /** * Tree control that supports check-boxes and detail text. */ public class DetailTree extends Canvas implements ISelectionProvider, ICheckable { /** Tree image types */ public enum ImageType { /** Collapsed item image */ COLLAPSED, /** Expanded item image */ EXPANDED, /** Checked item image */ CHECKED, /** Un-checked item image */ UNCHECKED, /** No check item image */ NOCHECK } /** No items constant */ private static final DetailTreeItem[] NO_ITEMS = new DetailTreeItem[0]; /** Text drawing flags */ private final static int TEXT_FLAGS = SWT.DRAW_TRANSPARENT | SWT.DRAW_MNEMONIC; /** Margin between image */ private final static int ITEM_IMAGE_MARGIN = 4; /** Vertical margin between icons */ private final static int ITEM_VERTICAL_MARGIN = 6; /** Description vertical margin */ private final static int DESCRIPTION_VERTICAL_MARGIN = 2; /** Tree images */ private Image[] images = new Image[ImageType.values().length]; /** Root items */ DetailTreeItem[] rootItems = NO_ITEMS; /** Text layout */ private TextLayout textLayout; /** Description foreground color */ private Color descriptionForeground; /** Hover background color */ private Color hoverBackground; /** Selected item or <code>null</code> */ private DetailTreeItem selectedItem; /** Hovered item or <code>null</code> */ private DetailTreeItem hoverItem; /** Description font */ private Font descriptionFont; /** Default computed size */ private Point defaultSize; /** Current scroll offset */ private Point scrollOffset = new Point(0, 0); /** Vertical scroll increment */ private int verticalScrollIncrement = 16; /** <code>true</code> if control has focus */ private boolean hasFocus; /** <code>true</code> if description text should be wrapped */ private boolean wrapDescription; /** Selection listeners */ private ListenerList selectionListeners = new ListenerList(); /** Check listeners */ private ListenerList checkListeners = new ListenerList(); /** <code>true</code> to draw horizontal separator between items */ private boolean drawSeparator = false; /** * Constructor * * @param parent Parent * @param style Style flags can include: * <ul> * <li>SWT.V_SCROLL - Enable vertical scrolling</li> * <li>SWT.H_SCROLL - Enable horizontal scrolling</li> * <li>SWT.NO_FOCUS<li> - Don't show focus</li> * <li>SWT.WRAP - Wrap description text</li> * <li>DOUBLE_BUFFERED - Double-buffer drawing to reduce flicker</li> * </ul> */ public DetailTree(Composite parent, int style) { super(parent, style | SWT.NO_BACKGROUND); wrapDescription = ((style & SWT.WRAP) == SWT.WRAP); // Create text layout textLayout = new TextLayout(getShell().getDisplay()); // Description foreground color descriptionForeground = new Color(getShell().getDisplay(), blendRGB(getDisplay().getSystemColor(SWT.COLOR_LIST_FOREGROUND).getRGB(), getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(), 40)); // Hover background color hoverBackground = new Color(getShell().getDisplay(), blendRGB(getDisplay().getSystemColor(SWT.COLOR_LIST_SELECTION).getRGB(), getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND).getRGB(), 45)); // Paint listener addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { onPaint(e.gc); } }); // Handle mouse buttons addMouseListener(new MouseAdapter() { @Override public void mouseDoubleClick(MouseEvent e) { DetailTreeItem item = hitTest(e.x, e.y, ItemRegion.TEXT); // If label double-clicked, toggle expansion if (item != null) { toggleExpand(item); } } @Override public void mouseDown(MouseEvent e) { onMouseDown(e.x, e.y); } }); // Handle mouse tracking addMouseTrackListener(new MouseTrackAdapter() { @Override public void mouseExit(MouseEvent e) { if (hoverItem != null) { DetailTreeItem previousHoverItem = hoverItem; hoverItem = null; redraw(previousHoverItem, ItemRegion.TEXT); } } }); // Handle mouse moves addMouseMoveListener(new MouseMoveListener() { @Override public void mouseMove(MouseEvent e) { DetailTreeItem item = hitTest(e.x, e.y, ItemRegion.TEXT); if (item != hoverItem) { DetailTreeItem previousItem = hoverItem; // Set new hover item unless it is selected hoverItem = (item == getSelectedItem()) ? null : item; // Updated previous hover item if any redraw(previousItem, ItemRegion.TEXT); // Update new hover item redraw(hoverItem, ItemRegion.TEXT); } } }); // Handle control resize addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { recalcScrollBars(); // Reset scroll offset to show more control when sized taller ScrollBar vScrollBar = getVerticalBar(); if (vScrollBar != null) { scrollOffset.y = vScrollBar.getSelection(); redraw(); } } }); // Initialize scroll bars recalcScrollBars(); // Handle horizontal scrolling final ScrollBar hScrollBar = getHorizontalBar(); if (hScrollBar != null) { hScrollBar.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { scrollOffset.x = hScrollBar.getSelection(); redraw(); } }); } // Handle vertical scrolling final ScrollBar vScrollBar = getVerticalBar(); if (vScrollBar != null) { vScrollBar.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { scrollOffset.y = vScrollBar.getSelection(); redraw(); } }); } // Listen to focus addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { if (isEnabled()) { if ((getStyle() & SWT.NO_FOCUS) != SWT.NO_FOCUS) { hasFocus = true; redraw(getSelectedItem(), ItemRegion.TEXT); } } } @Override public void focusLost(FocusEvent e) { if (isEnabled()) { if ((getStyle() & SWT.NO_FOCUS) != SWT.NO_FOCUS) { hasFocus = false; redraw(getSelectedItem(), ItemRegion.TEXT); } } } }); // Handle focus traversal addTraverseListener(new TraverseListener() { @Override public void keyTraversed(TraverseEvent e) { e.doit = true; } }); // Handle key pressed addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (isEnabled()) { // Arrow down if (e.keyCode == SWT.ARROW_DOWN) { select(true); } // Arrow up else if (e.keyCode == SWT.ARROW_UP) { select(false); } DetailTreeItem item = getSelectedItem(); if (item != null) { // Arrow left if (e.keyCode == SWT.ARROW_LEFT) { item.collapse(); } // Arrow right else if (e.keyCode == SWT.ARROW_RIGHT) { item.expand(); } // Space - select item else if (e.keyCode == SWT.SPACE) { if ((item.getStyle() & SWT.CHECK) == SWT.CHECK) { item.setChecked(!item.isChecked(), true); } } } } e.doit = true; } }); // Set font setFont(parent.getFont()); } @Override public void dispose() { if (descriptionForeground != null) { descriptionForeground.dispose(); descriptionForeground = null; } if (hoverBackground != null) { hoverBackground.dispose(); hoverBackground = null; } super.dispose(); } /** * Selects the next or previous item. The item is revealed if necessary. * * @param next <code>true</code> to select the next item, <code>false</code> * to select the previous item. */ private void select(final boolean next) { visitItems(new ItemVisitor() { DetailTreeItem lastItem = null; @Override public boolean visit(DetailTreeItem item) { if (!item.isRevealed()) return true; // Select next item if (next) { if (lastItem != null) { setSelectedItem(item); reveal(item); notifySelectionChanged(); return false; } else if (getSelectedItem() == item) { lastItem = item; } } // Select previous item else { if (getSelectedItem() == item) { if (lastItem != null) { setSelectedItem(lastItem); reveal(lastItem); } return false; } else { lastItem = item; } } return true; } }); } /** * Redraws a region of a single item. This is used instead of * <code>redraw()</code> when only an item changes to avoid flicker. * * @param item Item to redraw * @param region Region to redraw */ void redraw(DetailTreeItem item, ItemRegion region) { if (item != null) { Rectangle itemArea = item.getRegion(region); if (itemArea != null) { redraw(itemArea.x, itemArea.y, itemArea.width, itemArea.height, true); } } } /** * Expands all items. */ public void expandAll() { for (DetailTreeItem rootItem : getRootItems()) { rootItem.expandAll(); } } /** * Reveals an item, scrolling vertically if necessary. * * @param item Item to reveal */ public void reveal(DetailTreeItem item) { // Clear hover item hoverItem = null; // If item is not revealed if (!item.isRevealed()) { // Expand all parent items DetailTreeItem parent = item.getParent(); while (parent != null) { parent.expand(); parent = parent.getParent(); } } ScrollBar vScrollBar = getVerticalBar(); if (vScrollBar != null) { Rectangle clientArea = getClientArea(); int itemExtent = item.getArea().y + item.getArea().height; // Bottom of item is below visible area if (itemExtent > clientArea.height) { scrollOffset.y += itemExtent - clientArea.height; vScrollBar.setSelection(scrollOffset.y); redraw(); } // Top of item is above visible area else if (item.getArea().y < 0) { scrollOffset.y += item.getArea().y; vScrollBar.setSelection(scrollOffset.y); redraw(); } } } /** * Calculates the extends and visibility of horizontal and vertical * scroll bars. */ void recalcScrollBars() { ScrollBar hScrollBar = getHorizontalBar(); ScrollBar vScrollBar = getVerticalBar(); // If either horizontal or vertical scrolling is enabled if ((hScrollBar != null) || (vScrollBar != null)) { // Compute default size of control Point size = computeSize(SWT.DEFAULT, SWT.DEFAULT); // Get the visible client area Rectangle clientArea = getClientArea(); if (hScrollBar != null) { // Show horizontal scroll bar if content does not fit horizontally hScrollBar.setVisible(size.x > clientArea.width); // Set the maximum horizontal scroll to be the default width of // control minus what will show in the client area. hScrollBar.setMaximum(size.x); hScrollBar.setThumb(clientArea.width); hScrollBar.setPageIncrement(clientArea.width); } if (vScrollBar != null) { // Show vertical scroll bar if content does not fit vertically vScrollBar.setVisible(size.y > clientArea.height); // Set the maximum vertical scroll to be the default height of // control minus what will show in the client area. vScrollBar.setMaximum(size.y); vScrollBar.setIncrement(verticalScrollIncrement); vScrollBar.setThumb(clientArea.height); vScrollBar.setPageIncrement(clientArea.height); } } } /** * Returns the vertical scroll bar width. * * @return Width */ protected int getVerticalScrollBarWidth() { int width = 0; ScrollBar scrollBar = getVerticalBar(); if (scrollBar != null) { width = scrollBar.getSize().x; } return width; } /** * Returns the horizontal scroll bar height. * * @return Height */ protected int getHorizontalScrollBarHeight() { int height = 0; ScrollBar scrollBar = getHorizontalBar(); if (scrollBar != null) { height = scrollBar.getSize().y; } return height; } /** * Checks if coordinates correspond to an item. * * @param x Horizontal coordinate * @param y Vertical coordinate * @param region Item region to test for or <code>null</code> for the * entire item region * @return Item or <code>null</code> */ public DetailTreeItem hitTest(final int x, final int y, final ItemRegion region) { final DetailTreeItem[] hitItem = new DetailTreeItem[] { null }; visitItems(new ItemVisitor() { @Override public boolean visit(DetailTreeItem item) { // Entire item area if (region == null) { Rectangle itemArea = item.getArea(); if ((itemArea != null) && itemArea.contains(x, y)) { hitItem[0] = item; return false; } } // Item region else { ItemRegion itemRegion = item.hitTest(x, y); if (itemRegion == region) { hitItem[0] = item; return false; } } return true; } }); return hitItem[0]; } /** * Adds a new item. * * @param item Item */ void addItem(DetailTreeItem item) { DetailTreeItem[] newItems = new DetailTreeItem[rootItems.length + 1]; System.arraycopy(rootItems, 0, newItems, 0, rootItems.length); newItems[newItems.length - 1] = item; rootItems = newItems; recalcScrollBars(); redraw(); } /** * Sets the selected item. * * @param selectedItem Item or <code>null</code> to clear selection */ private void setSelectedItem(DetailTreeItem selectedItem) { DetailTreeItem previousItem = this.selectedItem; this.selectedItem = selectedItem; redraw(previousItem, ItemRegion.TEXT); redraw(selectedItem, ItemRegion.TEXT); } /** * Returns the selected item. * * @return Selected item or <code>null</code> */ private DetailTreeItem getSelectedItem() { return selectedItem; } /** * Sets all items checked or un-checked. * * @param checked <code>true</code> to set checked, <code>true</code> to * set un-checked */ public void setAllChecked(final boolean checked) { visitItems(new ItemVisitor() { @Override public boolean visit(DetailTreeItem item) { if ((item.getStyle() & SWT.CHECK) == SWT.CHECK) { item.setChecked(checked); } return true; } }); } /** * Removes all items from the tree. */ public void removeAll() { rootItems = NO_ITEMS; scrollOffset = new Point(0, 0); recalcScrollBars(); redraw(); } /** * Returns root items. * * @return Root times */ public DetailTreeItem[] getRootItems() { return rootItems; } /** * Returns all items. * * @return All items */ public DetailTreeItem[] getAllItems() { final ArrayList<DetailTreeItem> items = new ArrayList<DetailTreeItem>(); visitItems(new ItemVisitor() { @Override public boolean visit(DetailTreeItem item) { items.add(item); return true; } }); return items.toArray(new DetailTreeItem[items.size()]); } /** * Returns items that are checked. * * @return Checked items */ public DetailTreeItem[] getCheckedItems() { final ArrayList<DetailTreeItem> items = new ArrayList<DetailTreeItem>(); visitItems(new ItemVisitor() { @Override public boolean visit(DetailTreeItem item) { if (item.isChecked()) { items.add(item); } return true; } }); return items.toArray(new DetailTreeItem[items.size()]); } /** * Sets a tree image. * * @param type Image type * @param image Image or <code>null</code> */ public void setImage(ImageType type, Image image) { images[type.ordinal()] = image; } /** * Returns a tree image. * * @param type Image type * @return Image or <code>null</code> */ public Image getImage(ImageType type) { return images[type.ordinal()]; } /** * Sets the description font. * * @param descriptionFont Font */ public void setDescriptionFont(Font descriptionFont) { this.descriptionFont = descriptionFont; } /** * Returns the description font. * * @return Font */ public Font getDescriptionFont() { if (descriptionFont == null) return getFont(); else return descriptionFont; } /** * Sets if a horizontal separator will be drawn between root items. * * @param drawSeparator <code>true</code> to draw separator */ public void setDrawSeparator(boolean drawSeparator) { this.drawSeparator = drawSeparator; } /** * Returns if a horizontal separator will be drawn between root items. * * @return <code>true</code> if separator will be drawn */ public boolean getDrawSeparator() { return drawSeparator; } @Override public Point computeSize(int wHint, int hHint, boolean changed) { Point offset = new Point(0, 0); defaultSize = new Point(0, 0); // Compute the preferred size GC gc = new GC(this); for (DetailTreeItem item : getRootItems()) { offset = paintItem(gc, offset, item, false); } gc.dispose(); Point computedSize = super.computeSize(wHint, hHint, changed); if (wHint == SWT.DEFAULT) computedSize.x = defaultSize.x; if (hHint == SWT.DEFAULT) computedSize.y = defaultSize.y; return computedSize; } /** * Returns the offset of a value centered in another value. * * @param size Value to center in * @param value Value to center * @return Offset of value for centered alignment */ private int center(int size, int value) { if (size == value) { return 0; } else { return size / 2 - value / 2; } } /** * Paints an item or computes it size. * * @param gc Graphics context * @param offset Horizontal offset of item * @param item Item * @param draw <code>true</code> to draw item or <code>false</code> to only * compute the default size. * * @return Offset of next item */ private Point paintItem(GC gc, Point offset, DetailTreeItem item, boolean draw) { // Initialize item area origin (width and height computed later) Rectangle itemArea = new Rectangle(offset.x, offset.y, 0, 0); // Clear item hit regions if (draw) { item.clearRegions(); } // Expand/collapse image Image expandImage = item.isExpanded() ? getImage(ImageType.EXPANDED) : getImage(ImageType.COLLAPSED); ImageData expandImageData = expandImage.getImageData(); // Check-box image Image checkImage; if ((item.getStyle() & SWT.CHECK) == SWT.CHECK) { checkImage = item.isChecked() ? getImage(ImageType.CHECKED) : getImage(ImageType.UNCHECKED); } else { checkImage = getImage(ImageType.NOCHECK); } ImageData checkImageData = checkImage.getImageData(); // Set label font gc.setFont(getFont()); // Compute size of label text Point textSize = (item.getText() != null) ? gc.textExtent(item.getText(), TEXT_FLAGS) : new Point(0, 0); // The label height will be the height of the largest image or label text int labelHeight = Math.max(Math.max(textSize.y, expandImageData.height), checkImageData.height); // Region drawing offset int xOffset = offset.x; // If item has children, draw expand image if (item.hasChildren()) { Rectangle expandRegion = new Rectangle(xOffset, offset.y + center(labelHeight, expandImageData.height) + 2, expandImageData.width + ITEM_IMAGE_MARGIN, expandImageData.height); if (draw) { gc.drawImage(expandImage, expandRegion.x, expandRegion.y); item.setRegion(ItemRegion.EXPAND, expandRegion); } } xOffset += expandImageData.width + ITEM_IMAGE_MARGIN; // Draw check-box image Rectangle checkBoxRegion = new Rectangle(xOffset, offset.y + center(labelHeight, checkImageData.height) + 2, checkImageData.width + ITEM_IMAGE_MARGIN, checkImageData.height); if (draw) { gc.drawImage(checkImage, checkBoxRegion.x, checkBoxRegion.y); item.setRegion(ItemRegion.CHECKBOX, checkBoxRegion); } xOffset += checkImageData.width + ITEM_IMAGE_MARGIN; // Draw label int labelVerticalOffset = offset.y + center(labelHeight, textSize.y); if (draw) { // Selected if (item == getSelectedItem()) { gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT)); gc.setBackground(hasFocus ? gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION) : hoverBackground); } // Normal else { gc.setForeground((hoverItem == item) ? gc.getDevice().getSystemColor(SWT.COLOR_LIST_SELECTION_TEXT) : gc.getDevice().getSystemColor(SWT.COLOR_LIST_FOREGROUND)); gc.setBackground((hoverItem == item) ? hoverBackground : gc.getDevice().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); } gc.fillRoundRectangle(xOffset, labelVerticalOffset, textSize.x, textSize.y, 2, 2); gc.drawText(item.getText(), xOffset, labelVerticalOffset, TEXT_FLAGS); item.setRegion(ItemRegion.TEXT, new Rectangle(xOffset, labelVerticalOffset, textSize.x, textSize.y)); } // Update default width if (xOffset + textSize.x > defaultSize.x) { defaultSize.x = xOffset + textSize.x; } // Set description offset past text int descOffset = offset.y + textSize.y + DESCRIPTION_VERTICAL_MARGIN; // Update offset past text area offset.y += labelHeight; // Draw description if available if (item.getDescription() != null) { gc.setForeground(descriptionForeground); textLayout.setFont(getDescriptionFont()); if (wrapDescription) { Rectangle clientArea = getClientArea(); int width = clientArea.width - xOffset; if (width < 0) width = 1; textLayout.setWidth(width); } textLayout.setText(item.getDescription()); Rectangle descriptionBounds = textLayout.getBounds(); if (xOffset + descriptionBounds.width > defaultSize.x) { defaultSize.x = xOffset + descriptionBounds.width - getVerticalScrollBarWidth(); } if (draw) { textLayout.draw(gc, xOffset, descOffset); item.setRegion(ItemRegion.DESCRIPTION, new Rectangle(xOffset, descOffset, descriptionBounds.width, descriptionBounds.height)); } offset.y += descriptionBounds.height; } // Offset between items offset.y += ITEM_VERTICAL_MARGIN; // If item is expanded, draw children if (item.isExpanded()) { // Start children indention after check image int childIndent = checkImageData.width + ITEM_IMAGE_MARGIN; offset.x += childIndent; // Draw children for (DetailTreeItem child : item.getChildren()) { offset = paintItem(gc, offset, child, draw); } // Restore idention level offset.x -= childIndent; } // Otherwise, clear previously computed item regions for all children to // prevent it matching a hit test after an item is collapsed. else { for (DetailTreeItem child : item.getChildren()) { visitItems(child, clearRegionsVisitor); } } // Update default height defaultSize.y = offset.y; // Update item area itemArea.height = defaultSize.y - itemArea.y; itemArea.width = defaultSize.x - itemArea.x; item.setArea(itemArea); // Update the vertical scrolling increment to the height of the shortest item if ((verticalScrollIncrement == 0) || (itemArea.height < verticalScrollIncrement)) { verticalScrollIncrement = itemArea.height; } return offset; } /** * Handle mouse button down. * * @param x Horizontal coordinates * @param y Vertical coordinates */ private void onMouseDown(final int x, final int y) { DetailTreeItem item = hitTest(x, y, ItemRegion.CHECKBOX); if (item == null) { item = hitTest(x, y, ItemRegion.TEXT); } if (item != null) { if (item != getSelectedItem()) { setSelectedItem(item); notifySelectionChanged(); } } visitItems(new ItemVisitor() { @Override public boolean visit(DetailTreeItem item) { ItemRegion region = item.hitTest(x, y); if (region != null) { // Expand/collapse item if (region == ItemRegion.EXPAND) { toggleExpand(item); return false; } // Check/un-check item else if ((region == ItemRegion.CHECKBOX) || (region == ItemRegion.TEXT)) { if ((item.getStyle() & SWT.CHECK) == SWT.CHECK) { item.setChecked(!item.isChecked(), true); } return false; } } return true; } }); } /** * Toggles an item expansion. * * @param item Item */ private void toggleExpand(DetailTreeItem item) { if (item.isExpanded()) { item.collapse(); } else { item.expand(); } } /** * Visits all items. * * @param v Visitor */ public void visitItems(ItemVisitor v) { for (DetailTreeItem item : getRootItems()) { if (!visitItems(item, v)) { break; } } } /** * Visits an item and all of its child items. * * @param item Item * @param v Visitor * @return <code>true</code> to continue visiting */ public boolean visitItems(DetailTreeItem item, ItemVisitor v) { if (!v.visit(item)) { return false; } for (DetailTreeItem child : item.getChildren()) { if (!visitItems(child, v)) { return false; } } return true; } /** * Paints the control. * * @param gc Graphics context */ private void onPaint(GC gc) { Rectangle clientArea = getClientArea(); GC gcDevice = gc; // Double-buffer drawing Image bufferImage = null; if ((getStyle() & SWT.DOUBLE_BUFFERED) == SWT.DOUBLE_BUFFERED) { bufferImage = new Image(getDisplay(), clientArea.width, clientArea.height); gc = new GC(bufferImage); } // Erase background gc.setBackground(gc.getDevice().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); gc.fillRectangle(clientArea); // Initialize scroll offset Point offset = new Point(-scrollOffset.x, -scrollOffset.y); // Paint items for (DetailTreeItem item : getRootItems()) { offset = paintItem(gc, offset, item, true); // Draw separator if (getDrawSeparator()) { gc.setForeground(gc.getDevice().getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)); gc.drawLine(clientArea.x, offset.y, clientArea.x + clientArea.width, offset.y); } } if (bufferImage != null) { gcDevice.drawImage(bufferImage, 0, 0); bufferImage.dispose(); } } /** * Blends two RGB values using the provided ratio. * * @param c1 First RGB value * @param c2 Second RGB value * @param ratio Percentage of the first RGB to blend with * second RGB (0-100) * * @return The RGB value of the blended color */ public static RGB blendRGB(RGB c1, RGB c2, int ratio) { ratio = Math.max(0, Math.min(255, ratio)); int r = Math.max(0, Math.min(255, (ratio * c1.red + (100 - ratio) * c2.red) / 100)); int g = Math.max(0, Math.min(255, (ratio * c1.green + (100 - ratio) * c2.green) / 100)); int b = Math.max(0, Math.min(255, (ratio * c1.blue + (100 - ratio) * c2.blue) / 100)); return new RGB(r, g, b); } @Override public ISelection getSelection() { return new StructuredSelection(getSelectedItem()); } @Override public void setSelection(ISelection selection) { if (selection instanceof IStructuredSelection) { IStructuredSelection sel = (IStructuredSelection)selection; setSelectedItem(sel.isEmpty() ? null : (DetailTreeItem)sel.getFirstElement()); notifySelectionChanged(); } } /** * Adds a new listener to selection change events. * * @param listener Listener to add */ public void addSelectionChangedListener(ISelectionChangedListener listener) { selectionListeners.add(listener); } /** * Removes a listener from selection change events. * * @param listener Listener to remove */ public void removeSelectionChangedListener(ISelectionChangedListener listener) { selectionListeners.remove(listener); } @Override public void addCheckStateListener(ICheckStateListener listener) { checkListeners.add(listener); } @Override public void removeCheckStateListener(ICheckStateListener listener) { checkListeners.remove(listener); } @Override public boolean getChecked(Object element) { return ((DetailTreeItem)element).isChecked(); } @Override public boolean setChecked(Object element, boolean state) { DetailTreeItem item = (DetailTreeItem)element; item.setChecked(state); return item.isChecked() == state; } /** * Notifies listeners of a selection changed. */ void notifySelectionChanged() { SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection()); Object[] listeners = selectionListeners.getListeners(); for (Object listener : listeners) { try { ((ISelectionChangedListener)listener).selectionChanged(event); } catch (Exception e) { e.printStackTrace(); } } } /** * Notifies listeners of a checked state changed. * * @param item Item that changed */ void notifyCheckStateChanged(DetailTreeItem item) { CheckStateChangedEvent event = new CheckStateChangedEvent(this, item, item.isChecked()); Object[] listeners = checkListeners.getListeners(); for (Object listener : listeners) { try { ((ICheckStateListener)listener).checkStateChanged(event); } catch (Exception e) { e.printStackTrace(); } } } /** * Tree item visitor. */ public interface ItemVisitor { /** * Called to visit an item. * * @param item Item * @return <code>true</code> to continue visiting items. */ public boolean visit(DetailTreeItem item); } /** Visitor to clear item regions */ private final static ItemVisitor clearRegionsVisitor = new ItemVisitor() { @Override public boolean visit(DetailTreeItem item) { item.clearRegions(); return true; } }; }