Java tutorial
/* * Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies). * All rights reserved. * This component and the accompanying materials are made available * under the terms of the License "Eclipse Public License v1.0" * which accompanies this distribution, and is available * at the URL "http://www.eclipse.org/legal/epl-v10.html". * * Initial Contributors: * Nokia Corporation - initial contribution. * * Contributors: * * Description: * */ package com.nokia.sdt.utils.ui; import com.nokia.cpp.internal.api.utils.core.Pair; import com.nokia.sdt.utils.*; import com.nokia.sdt.utils.drawing.*; import org.eclipse.jface.viewers.*; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.ScrolledComposite; import org.eclipse.swt.events.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.widgets.*; import java.util.*; import java.util.List; /** * Displays thumbnails with caption for single or multiple selection */ public class ThumbnailGridViewer extends StructuredViewer { private List<Object> elements; // contains items or null private List<ThumbnailItem> items; private Map<Object, ThumbnailItem> itemMap; private Set<Integer> selectedIndices; //private Composite scroller; Composite composite; private int cursorIndex; private int style; Control keyHandlingWidget; private final static int DEFAULT_COLS = 4; ThumbnailItemLayout layout; private GC tempGC; private SwtFont itemFont; private int revealIndex; public static final int DEFAULT_HEIGHT = 90; public static final int DEFAULT_WIDTH = 90; private Point scrollOrigin; /** * Create a thumbnail grid viewer. Available style bits * are<p> * <li>SWT.MULTI: allow multiple selection * <p> * Remaining bits are passed to the ScrolledComposite * @param parent * @param style * @see ScrolledComposite#ScrolledComposite(Composite, int) */ public ThumbnailGridViewer(Composite parent, final int style) { this.style = style & SWT.MULTI; composite = new Composite(parent, (style & ~SWT.MULTI) /*| SWT.H_SCROLL*/ | SWT.V_SCROLL); composite.setBackground(parent.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); composite.setEnabled(true); //scroller.setExpandHorizontal(true); //scroller.setExpandVertical(true); //scroller.setMinSize(DEFAULT_WIDTH, DEFAULT_HEIGHT); tempGC = new GC(parent); itemFont = new SwtFont(tempGC.getFont()); //composite = new ThumbnailGridComposite(scroller, SWT.NONE); layout = new ThumbnailItemLayout(DEFAULT_COLS, isMultiSelect() ? 2 : 0); composite.setLayout(layout); //composite.setBackground(composite.getParent().getBackground()); //scroller.setContent(composite); //scroller.setLayout(new FillLayout()); elements = new ArrayList<Object>(); items = new ArrayList(); itemMap = new HashMap<Object, ThumbnailItem>(); selectedIndices = new HashSet<Integer>(); scrollOrigin = new Point(0, 0); /* scroller.addControlListener(new ControlListener() { public void controlMoved(ControlEvent e) { updateScrollSize(); } public void controlResized(ControlEvent e) { updateScrollSize(); } });*/ composite.addControlListener(new ControlListener() { public void controlMoved(ControlEvent e) { updateScrollSize(); } public void controlResized(ControlEvent e) { updateScrollSize(); } }); Listener scrollListener = new Listener() { public void handleEvent(Event event) { updateScrollPosition(); } }; composite.getVerticalBar().addListener(SWT.Selection, scrollListener); //composite.getHorizontalBar().addListener(SWT.Selection, scrollListener); cursorIndex = -1; revealIndex = -1; keyHandlingWidget = composite; keyHandlingWidget.addKeyListener(new KeyAdapter() { // this has to be done to become part of the tab order and // get the focus from it for traversal events public void keyPressed(KeyEvent e) { boolean additive = isMultiSelect() && (e.stateMask & SWT.CTRL) != 0; boolean range = isMultiSelect() && (e.stateMask & SWT.SHIFT) != 0; int index = cursorIndex; if (e.keyCode == ' ') { if (!selectedIndices.contains(index)) selectIndex(index, true); else if (isMultiSelect()) unselectIndex(index); } else if (e.keyCode == 'a' && additive /* e.g., CTRL and multi-select */) { selectAll(); } else if (e.keyCode == SWT.PAGE_DOWN || e.keyCode == SWT.PAGE_UP) { int direction = e.keyCode == SWT.PAGE_DOWN ? 1 : -1; reselectTraversal(additive, range, cursorIndex + direction * layout.columns * layout.visibleRows); } else if (e.keyCode == SWT.HOME) { int target = (e.stateMask & SWT.CTRL) != 0 ? 0 : layout.adjustEdgeIndex(cursorIndex); reselectTraversal(false, range, target); } else if (e.keyCode == SWT.END) { int target = (e.stateMask & SWT.CTRL) != 0 ? elements.size() : layout.adjustEdgeIndex(cursorIndex) + layout.columns; reselectTraversal(false, range, target - 1); } else { e.doit = false; } } }); keyHandlingWidget.addTraverseListener(new TraverseListener() { public void keyTraversed(TraverseEvent e) { redrawCursor(); if (e.detail == SWT.TRAVERSE_ESCAPE || e.detail == SWT.TRAVERSE_RETURN) { e.doit = true; return; } if (cursorIndex < 0 || e.detail == SWT.TRAVERSE_TAB_NEXT) { if (cursorIndex < 0 && items.size() > 0) { if (isMultiSelect()) selectIndex(0, false); else cursorIndex = 0; } keyHandlingWidget.setFocus(); } else if (e.detail == SWT.TRAVERSE_ARROW_NEXT || e.detail == SWT.TRAVERSE_ARROW_PREVIOUS) { boolean additive = isMultiSelect() && (e.stateMask & SWT.CTRL) != 0; boolean range = isMultiSelect() && (e.stateMask & SWT.SHIFT) != 0; int direction = e.detail == SWT.TRAVERSE_ARROW_NEXT ? 1 : -1; int jump = e.keyCode == SWT.ARROW_LEFT || e.keyCode == SWT.ARROW_RIGHT ? 1 : layout.columns; reselectTraversal(additive, range, cursorIndex + jump * direction); } redrawCursor(); } }); keyHandlingWidget.addListener(SWT.FocusIn, new Listener() { public void handleEvent(Event event) { redrawItems(); } }); keyHandlingWidget.addListener(SWT.FocusOut, new Listener() { public void handleEvent(Event event) { redrawItems(); } }); } /** * Update the selection upon a traversal from the cursor to the given index, * either replacing or toggling the selection. * @param additive was Ctrl held? * @param range was Shift held? * @param items how many items to traverse? * @param limit */ protected void reselectTraversal(boolean additive, boolean range, int limit) { limit = Math.max(0, Math.min(limit, elements.size() - 1)); if (range) { selectRangeTo(limit, additive); } else if (!additive) { selectIndex(limit, false); } else { cursorIndex = limit; revealIndex(cursorIndex); } } /** * */ protected void updateScrollPosition() { scrollOrigin = new Point(0, //composite.getHorizontalBar().getSelection(), composite.getVerticalBar().getSelection()); layout.setScrollOrigin(scrollOrigin); ensureVisibleItemsValid(); refreshItems(); } /** * */ protected void redrawCursor() { if (cursorIndex >= 0) { ThumbnailItem item = items.get(cursorIndex); if (item != null) { item.redraw(); } } } /** * */ protected void updateScrollSize() { Rectangle bounds = composite.getClientArea(); int selection = scrollOrigin.y; int bottom = Math.max(0, layout.maxHeight * layout.rows); // clamp selection to visible area if (bottom > bounds.height && selection + bounds.height >= bottom) selection = bottom - bounds.height; composite.getVerticalBar().setValues(selection, 0, bottom, layout.visibleRows * layout.maxHeight, layout.maxHeight, Math.min(bounds.height, bottom)); // resync scroll position and items scrollTo(new Point(scrollOrigin.x, selection)); // ensure a revealed item is actually visible if (revealIndex >= 0) { revealIndex(revealIndex); } } /** * Ensure that visible items in the scrolled area are valid, * i.e., that their images have been loaded */ protected void ensureVisibleItemsValid() { composite.setLayoutDeferred(true); for (ThumbnailItem item : items) { ensureVisibleItemValid(item); } composite.setLayoutDeferred(false); } /** * @param item */ private void ensureVisibleItemValid(ThumbnailItem item) { if (item != null && isItemVisible(item) && item.imageDataRef == null) { Point size = item.getSize(); Point newSize = item.computeSize(SWT.DEFAULT, SWT.DEFAULT); if (!size.equals(newSize)) composite.layout(); } } /** * Just redraw items, e.g., for focus change. * */ protected void redrawItems() { composite.redraw(); for (ThumbnailItem item : items) { if (item != null) item.redraw(); } } /** * Relayout and redraw the entire composite. * */ protected void redraw() { composite.computeSize(SWT.DEFAULT, SWT.DEFAULT, true); composite.layout(); composite.redraw(); for (ThumbnailItem item : items) { if (item != null && !item.isDisposed()) item.redraw(); } updateScrollSize(); } /** * Add or remove the items in the range, following Shift-Click behavior. * @param item * @param additive */ protected void selectRangeTo(int index, boolean additive) { int otherIndex = cursorIndex; boolean enable = !selectedIndices.contains(index); int dir = index > otherIndex ? 1 : -1; int stepIndex = otherIndex; while (stepIndex != index) { if (enable) { if (!selectedIndices.contains(stepIndex)) selectedIndices.add(stepIndex); } else selectedIndices.remove(stepIndex); stepIndex += dir; } if (!selectedIndices.contains(index)) selectedIndices.add(index); cursorIndex = index; syncSelection(); revealIndex(index); } /** * Set selection to item at given index, cropping to legal bounds * @param index * @param additive true: add to selection, false: replace */ protected void selectIndex(int index, boolean additive) { if (elements.size() == 0) index = 0; else { while (index < 0) index += elements.size(); index %= elements.size(); } selectItem(index, additive, true); } /** * Select an item, optionally adding to the selection, * and make it the cursor. * @param item */ void unselectIndex(int index) { cursorIndex = index; if (cursorIndex >= 0) { selectedIndices.remove(cursorIndex); syncSelection(); revealIndex(cursorIndex); } } protected void selectAll() { selectedIndices.clear(); for (int idx = 0; idx < elements.size(); idx++) selectedIndices.add(idx); syncSelection(); } /* * (non-Javadoc) * @see org.eclipse.jface.viewers.Viewer#inputChanged(java.lang.Object, java.lang.Object) */ protected synchronized void inputChanged(Object input, Object oldInput) { super.inputChanged(input, oldInput); resetElements(input); setSelection(StructuredSelection.EMPTY); redraw(); } /** * Reset the element array from the input and reset items to match. */ private void resetElements(Object input) { Object[] elementObjects = getFilteredChildren(input); elements = Arrays.asList(elementObjects); //System.out.println("# elements = " + elements.size()); layout.setVirtualChildCount(elements.size()); refreshItems(); } /** * Refresh the items map to reflect the visible items */ private void refreshItems() { // get the range of items actually visible Pair<Integer, Integer> range = getVisibleItemRange(); itemMap.clear(); // delete any items not visible for (int index = 0; index < items.size(); index++) { ThumbnailItem item = items.get(index); if (item == null) continue; if ((index >= elements.size() || item.getElement() != elements.get(index)) || (index < range.first || index >= range.second)) { items.set(index, null); item.dispose(); } } // ensure the item list is the right size while (items.size() > elements.size()) items.remove(items.size() - 1); while (items.size() < elements.size()) items.add(null); // create items we need to see, and update mappings and selected state for (int index = range.first; index < range.second; index++) { ThumbnailItem item = items.get(index); if (item == null) { item = createItem(index); items.set(index, item); } itemMap.put(item.getElement(), item); item.setSelected(selectedIndices.contains(index)); } ensureVisibleItemsValid(); composite.layout(); } /** * @return */ private Pair<Integer, Integer> getVisibleItemRange() { Point scrollerSize = composite.getSize(); if (scrollerSize.x == 0 || scrollerSize.y == 0) { return new Pair<Integer, Integer>(0, 0); } Point origin = scrollOrigin; int start = layout.getIndexAtPosition(origin); Point corner = new Point(origin.x + scrollerSize.x, origin.y + scrollerSize.y); int end = layout.getIndexAtPosition(corner); return new Pair<Integer, Integer>(Math.max(0, start), Math.min(end, elements.size())); } /** * @param index * @return */ private ThumbnailItem createItem(int index) { final ThumbnailItem item = new ThumbnailItem(this, (ILabelProvider) getLabelProvider(), itemFont, elements.get(index), index); item.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { if (item.isDisposed()) return; // draw the selection rectangle if (cursorIndex != item.getIndex()) return; Display display = ((Widget) e.getSource()).getDisplay(); boolean focused = keyHandlingWidget.isFocusControl() || composite.isFocusControl(); ThumbnailItem cursorItem = items.get(cursorIndex); if (cursorItem == null || cursorItem.isDisposed()) return; Point size = cursorItem.getSize(); if (isMultiSelect() && focused) { e.gc.setForeground(cursorItem.isSelected() ? display.getSystemColor(SWT.COLOR_LIST_BACKGROUND) : display.getSystemColor(SWT.COLOR_LIST_FOREGROUND)); e.gc.setLineDash(new int[] { 1, 1 }); e.gc.drawRectangle(0, 0, size.x - 1, size.y - 1); } } }); return item; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.ContentViewer#handleDispose(org.eclipse.swt.events.DisposeEvent) */ @Override protected void handleDispose(DisposeEvent event) { super.handleDispose(event); elements.clear(); items.clear(); itemMap.clear(); tempGC.dispose(); tempGC = null; itemFont.dispose(); itemFont = null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#handleLabelProviderChanged(org.eclipse.jface.viewers.LabelProviderChangedEvent) */ @Override protected void handleLabelProviderChanged(LabelProviderChangedEvent event) { super.handleLabelProviderChanged(event); if (items.size() > 0 && !composite.isDisposed()) { redraw(); } } /** * Select an item, optionally adding to the selection, * and make it the cursor. * @param item */ void selectItem(int index, boolean additive, boolean reveal) { if (!additive) { unselectAll(); } cursorIndex = index; if (index >= 0) { selectedIndices.add(index); syncSelection(); if (reveal) revealIndex(index); } else { selectedIndices.clear(); syncSelection(); } } /** * Get the structured selection corresponding to the selected indices * @return */ private ISelection createStructuredSelection() { IStructuredSelection selection = new StructuredSelection(getSelectionFromWidget()); return selection; } public void selectFirst() { if (elements.size() > 0) { setSelection(new StructuredSelection(elements.get(0))); cursorIndex = 0; revealIndex(0); } } public void unselectAll() { selectedIndices.clear(); syncSelection(); cursorIndex = -1; } /** * Synchronize the items with the selected indices */ private void syncSelection() { setSelection(createStructuredSelection()); } /** * Get the top-level widget for the viewer. * @return */ public Control getViewer() { return composite; } public Composite getComposite() { return composite; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.Viewer#getControl() */ public Control getControl() { return composite; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#doFindInputItem(java.lang.Object) */ protected Widget doFindInputItem(Object element) { if (element.equals(getInput())) return getComposite(); return null; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#doFindItem(java.lang.Object) */ protected Widget doFindItem(Object element) { return itemMap.get(element); } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#doUpdateItem(org.eclipse.swt.widgets.Widget, java.lang.Object, boolean) */ protected void doUpdateItem(Widget item, Object element, boolean fullMap) { //if (item instanceof Item) { // ((Item) item).computeSize(SWT.DEFAULT, SWT.DEFAULT); //} } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#getSelectionFromWidget() */ protected List getSelectionFromWidget() { List<Object> selected = new ArrayList<Object>(); for (Integer index : selectedIndices) { int idx = index.intValue(); if (idx < 0 || idx >= elements.size()) { UtilsPlugin.log(new IllegalArgumentException(index.toString())); } else { selected.add(elements.get(idx)); } } return selected; } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#internalRefresh(java.lang.Object) */ protected void internalRefresh(Object element) { resetElements(element); } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#reveal(java.lang.Object) */ public void reveal(Object element) { int index = elements.indexOf(element); revealIndex(index); } private void revealIndex(int index) { revealIndex = index; if (index < 0) return; Point location = layout.getPositionForIndex(index); Point size = new Point(layout.maxWidth, layout.maxHeight); /*Item item = items.get(index); if (item == null) { refreshItems(); item = items.get(index); if (item == null) return; }*/ Point scrollerOrigin = scrollOrigin; Point scrollerSize = composite.getSize(); if (scrollerSize.x == 0 || scrollerSize.y == 0) { // try again later return; } // scroll only enough to reveal item; avoid big jumps Point extent = new Point(location.x + size.x, location.y + size.y); Point scrollerExtent = new Point(scrollerOrigin.x + scrollerSize.x, scrollerOrigin.y + scrollerSize.y); if (location.x < scrollerOrigin.x || location.y < scrollerOrigin.y) { // move up to the item scrollTo(location); } else if (extent.x > scrollerExtent.x || extent.y > scrollerExtent.y) { // move only far enough down to reveal the hidden parts scrollTo(new Point(scrollerOrigin.x + (extent.x - scrollerExtent.x), scrollerOrigin.y + (extent.y - scrollerExtent.y))); } //ensureVisibleItemsValid(); // revealed revealIndex = -1; } /** * @param point */ private void scrollTo(Point point) { scrollOrigin = new Point(0, point.y); composite.getVerticalBar().setSelection(point.y); layout.setScrollOrigin(scrollOrigin); refreshItems(); //composite.getHorizontalBar().setSelection(point.x); } private boolean isItemVisible(ThumbnailItem item) { if (item == null || item.isDisposed()) return false; Point size = item.getSize(); if (size.x == 0 || size.y == 0) return false; Point location = item.getLocation(); Point extent = new Point(location.x + size.x, location.y + size.y); Point scrollerOrigin = scrollOrigin; Point scrollerSize = composite.getSize(); Point scrollerExtent = new Point(scrollerOrigin.x + scrollerSize.x, scrollerOrigin.y + scrollerSize.y); if (extent.x < scrollerOrigin.x || extent.y < scrollerOrigin.y) { return false; } else if (location.x >= scrollerExtent.x || location.y >= scrollerExtent.y) { return false; } else { return true; } } /* (non-Javadoc) * @see org.eclipse.jface.viewers.StructuredViewer#setSelectionToWidget(java.util.List, boolean) */ protected void setSelectionToWidget(List l, boolean reveal) { for (ThumbnailItem item : items) { if (item != null) item.setSelected(false); } Set<Integer> newSelectedIndices = new HashSet<Integer>(); // Only reveal if none of the elements are visible. // This avoids oscillations when shift-selecting to // grow a selection. Object revealedElement = null; if (!l.isEmpty()) { for (Object element : l) { int index = elements.indexOf(element); if (index < 0) { UtilsPlugin.log(new IllegalArgumentException(element.toString())); continue; } if (reveal && !selectedIndices.contains(index)) { revealedElement = element; if (cursorIndex < 0) cursorIndex = index; } newSelectedIndices.add(index); /* Item item = items.get(index); if (item != null) item.setSelected(true); */ } } selectedIndices = newSelectedIndices; refreshItems(); // don't syncSelection() here -- this is called /because/ the selection changed if (revealedElement != null) { reveal(revealedElement); } } /** * @return */ boolean isMultiSelect() { return (style & SWT.MULTI) != 0; } }