com.nokia.sdt.utils.ui.ThumbnailGridViewer.java Source code

Java tutorial

Introduction

Here is the source code for com.nokia.sdt.utils.ui.ThumbnailGridViewer.java

Source

/*
* 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;
    }

}