/******************************************************************************* * Copyright (c) 2000, 2011 IBM Corporation 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.ui.internal.editors.text; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.BusyIndicator; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Tree; import org.eclipse.core.runtime.ListenerList; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTableViewer; import org.eclipse.jface.viewers.CheckboxTreeViewer; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.ITreeViewerListener; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeExpansionEvent; import org.eclipse.jface.viewers.ViewerComparator; /* * XXX: This is an copy of the internal ResourceTreeAndListGroup class, see http://bugs.eclipse.org/147027 */ /** * Workbench-level composite that combines a CheckboxTreeViewer and * CheckboxListViewer. All viewer selection-driven interactions are handled * within this object */ class SelectResourcesBlock implements ICheckStateListener, ISelectionChangedListener, ITreeViewerListener { /** * The IElementFilter is a interface that defines * the API for filtering the current selection of * a ResourceTreeAndListGroup in order to find a * subset to update as the result of a type filtering. * This is meant as an internal class and is used exclusively * by the import dialog. */ interface IElementFilter { void filterElements(Collection elements) throws InterruptedException; void filterElements(Object[] elements) throws InterruptedException; } private Object root; private Object currentTreeSelection; private Collection expandedTreeNodes= new HashSet(); private Map checkedStateStore= new HashMap(9); private Collection whiteCheckedTreeItems= new HashSet(); private ListenerList listeners= new ListenerList(ListenerList.IDENTITY); private ITreeContentProvider treeContentProvider; private IStructuredContentProvider listContentProvider; private ILabelProvider treeLabelProvider; private ILabelProvider listLabelProvider; // widgets private CheckboxTreeViewer treeViewer; private CheckboxTableViewer listViewer; //height hint for viewers private static int PREFERRED_HEIGHT= 150; /** * Create an instance of this class. Use this constructor if you wish to specify the width * and/or height of the combined widget (to only hard code one of the sizing dimensions, specify * the other dimension's value as -1) * * @param parent the parent composite * @param rootObject the root object * @param treeContentProvider the tree content provider * @param treeLabelProvider the tree label provider * @param listContentProvider the list content provider * @param listLabelProvider the list label provider * @param style the style flags for the new Composite * @param useHeightHint If true then use the height hint to make this group big enough */ public SelectResourcesBlock(Composite parent, Object rootObject, ITreeContentProvider treeContentProvider, ILabelProvider treeLabelProvider, IStructuredContentProvider listContentProvider, ILabelProvider listLabelProvider, int style, boolean useHeightHint) { root= rootObject; this.treeContentProvider= treeContentProvider; this.listContentProvider= listContentProvider; this.treeLabelProvider= treeLabelProvider; this.listLabelProvider= listLabelProvider; createContents(parent, style, useHeightHint); } /** * Add the passed listener to self's collection of clients that listen for * changes to element checked states * * @param listener ICheckStateListener */ public void addCheckStateListener(ICheckStateListener listener) { listeners.add(listener); } /** * Iterates over the passed elements which are being realized for the * first time and check each one in the tree viewer as appropriate. * * @param elements the elements */ private void checkNewTreeElements(Object[] elements) { for (int i= 0; i < elements.length; ++i) { Object currentElement= elements[i]; boolean checked= checkedStateStore.containsKey(currentElement); treeViewer.setChecked(currentElement, checked); treeViewer.setGrayed(currentElement, checked && !whiteCheckedTreeItems.contains(currentElement)); } } /** * An item was checked in one of self's two views. Determine which view this * occurred in and delegate appropriately * * @param event the check state event * @see org.eclipse.jface.viewers.ICheckStateListener#checkStateChanged(org.eclipse.jface.viewers.CheckStateChangedEvent) */ public void checkStateChanged(final CheckStateChangedEvent event) { //Potentially long operation - show a busy cursor BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new Runnable() { public void run() { if (event.getCheckable().equals(treeViewer)) treeItemChecked(event.getElement(), event.getChecked()); else listItemChecked(event.getElement(), event.getChecked(), true); notifyCheckStateChangeListeners(event); } }); } /** * Lay out and initialize self's visual components. * * @param parent org.eclipse.swt.widgets.Composite * @param style the style flags for the new Composite * @param useHeightHint If true use the preferredHeight. */ private void createContents(Composite parent, int style, boolean useHeightHint) { // group pane Composite composite= new Composite(parent, style); composite.setFont(parent.getFont()); GridLayout layout= new GridLayout(); layout.numColumns= 2; layout.makeColumnsEqualWidth= true; layout.marginHeight= 0; layout.marginWidth= 0; composite.setLayout(layout); composite.setLayoutData(new GridData(GridData.FILL_BOTH)); createTreeViewer(composite, useHeightHint); createListViewer(composite, useHeightHint); initialize(); } /** * Creates this block's list viewer. * * @param parent the parent control * @param useHeightHint if <code>true</code> use the height hints */ private void createListViewer(Composite parent, boolean useHeightHint) { listViewer= CheckboxTableViewer.newCheckList(parent, SWT.BORDER); GridData data= new GridData(GridData.FILL_BOTH); if (useHeightHint) data.heightHint= PREFERRED_HEIGHT; listViewer.getTable().setLayoutData(data); listViewer.getTable().setFont(parent.getFont()); listViewer.setContentProvider(listContentProvider); listViewer.setLabelProvider(listLabelProvider); listViewer.addCheckStateListener(this); } /** * Create this block's tree viewer. * * @param parent the parent control * @param useHeightHint if <code>true</code> use the height hints */ private void createTreeViewer(Composite parent, boolean useHeightHint) { Tree tree= new Tree(parent, SWT.CHECK | SWT.BORDER); GridData data= new GridData(GridData.FILL_BOTH); if (useHeightHint) data.heightHint= PREFERRED_HEIGHT; tree.setLayoutData(data); tree.setFont(parent.getFont()); treeViewer= new CheckboxTreeViewer(tree); treeViewer.setUseHashlookup(true); treeViewer.setContentProvider(treeContentProvider); treeViewer.setLabelProvider(treeLabelProvider); treeViewer.addTreeListener(this); treeViewer.addCheckStateListener(this); treeViewer.addSelectionChangedListener(this); } /** * Returns a boolean indicating whether the passed tree element should be at * LEAST gray-checked. Note that this method does not consider whether it * should be white-checked, so a specified tree item which should be * white-checked will result in a <code>true</code> answer from this * method. * * @param treeElement java.lang.Object * @return boolean */ private boolean determineShouldBeAtLeastGrayChecked(Object treeElement) { // if any list items associated with treeElement are checked then it // retains its gray-checked status regardless of its children List checked= (List) checkedStateStore.get(treeElement); if (checked != null && (!checked.isEmpty())) return true; // if any children of treeElement are still gray-checked then // treeElement // must remain gray-checked as well. Only ask expanded nodes if (expandedTreeNodes.contains(treeElement)) { Object[] children= treeContentProvider.getChildren(treeElement); for (int i= 0; i < children.length; ++i) { if (checkedStateStore.containsKey(children[i])) return true; } } return false; } /** * Expands an element in a tree viewer. * * @param element the element to be expanded */ private void expandTreeElement(final Object element) { BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new Runnable() { public void run() { // First see if the children need to be given their checked // state at all. If they've // already been realized then this won't be necessary if (expandedTreeNodes.contains(element)) checkNewTreeElements(treeContentProvider.getChildren(element)); else { expandedTreeNodes.add(element); if (whiteCheckedTreeItems.contains(element)) { //If this is the first expansion and this is a white // checked node then check the children Object[] children= treeContentProvider.getChildren(element); for (int i= 0; i < children.length; ++i) { if (!whiteCheckedTreeItems.contains(children[i])) { Object child= children[i]; setWhiteChecked(child, true); treeViewer.setChecked(child, true); checkedStateStore.put(child, new ArrayList()); } } //Now be sure to select the list of items too setListForWhiteSelection(element); } } } }); } /** * Adds all of the selected children of <code>treeElement</code> to result recursively. This * does not set any values in the checked state. * * @param treeElement the tree element being queried * @param parentLabel the parent label * @param addAll a boolean to indicate if the checked state store needs to be queried * @param filter the filter being used on the data * @throws InterruptedException in case of interruption */ private void findAllSelectedListElements(Object treeElement, String parentLabel, boolean addAll, IElementFilter filter) throws InterruptedException { String fullLabel= null; if (addAll) filter.filterElements(listContentProvider.getElements(treeElement)); else { //Add what we have stored if (checkedStateStore.containsKey(treeElement)) filter.filterElements((Collection) checkedStateStore.get(treeElement)); } Object[] treeChildren= treeContentProvider.getChildren(treeElement); for (int i= 0; i < treeChildren.length; i++) { Object child= treeChildren[i]; if (addAll) findAllSelectedListElements(child, fullLabel, true, filter); else { //Only continue for those with checked state if (checkedStateStore.containsKey(child)) findAllSelectedListElements(child, fullLabel, whiteCheckedTreeItems.contains(child), filter); } } } /** * Find all of the white checked children of the treeElement and add them to * the collection. If the element itself is white select add it. If not then * add any selected list elements and recurse down to the children. * * @param treeElement java.lang.Object * @param result java.util.Collection */ private void findAllWhiteCheckedItems(Object treeElement, Collection result) { if (whiteCheckedTreeItems.contains(treeElement)) result.add(treeElement); else { Collection listChildren= (Collection) checkedStateStore.get(treeElement); //if it is not in the store then it and its children are not interesting if (listChildren == null) return; result.addAll(listChildren); Object[] children= treeContentProvider.getChildren(treeElement); for (int i= 0; i < children.length; ++i) { findAllWhiteCheckedItems(children[i], result); } } } /** * Returns a flat list of all of the leaf elements which are checked. Filter * then based on the supplied ElementFilter. If monitor is canceled then * return null * * @param filter - the filter for the data * @throws InterruptedException in case of interruption */ private void getAllCheckedListItems(IElementFilter filter) throws InterruptedException { //Iterate through the children of the root as the root is not in the store Object[] children= treeContentProvider.getChildren(root); for (int i= 0; i < children.length; ++i) { findAllSelectedListElements(children[i], null, whiteCheckedTreeItems.contains(children[i]), filter); } } /** * Returns a flat list of all of the leaf elements which are checked. * * @return all of the leaf elements which are checked. This API does not * return null in order to keep backwards compatibility. */ public List getAllCheckedListItems() { final ArrayList returnValue= new ArrayList(); IElementFilter passThroughFilter= new IElementFilter() { public void filterElements(Collection elements) throws InterruptedException { returnValue.addAll(elements); } public void filterElements(Object[] elements) throws InterruptedException { for (int i= 0; i < elements.length; i++) { returnValue.add(elements[i]); } } }; try { getAllCheckedListItems(passThroughFilter); } catch (InterruptedException exception) { return new ArrayList(); } return returnValue; } /** * Returns a list of all of the items that are white checked. Any folders * that are white checked are added and then any files from white checked * folders are added. * * @return the list of all of the items that are white checked */ public List getAllWhiteCheckedItems() { List result= new ArrayList(); //Iterate through the children of the root as the root is not in the // store Object[] children= treeContentProvider.getChildren(root); for (int i= 0; i < children.length; ++i) { findAllWhiteCheckedItems(children[i], result); } return result; } /** * Logically gray-check all ancestors of <code>treeElement</code> by ensuring that they * appear in the checked table. * * @param treeElement the tree element */ private void grayCheckHierarchy(Object treeElement) { //expand the element first to make sure we have populated for it expandTreeElement(treeElement); // if this tree element is already gray then its ancestors all are as // well if (checkedStateStore.containsKey(treeElement)) return; // no need to proceed upwards from here checkedStateStore.put(treeElement, new ArrayList()); Object parent= treeContentProvider.getParent(treeElement); if (parent != null) grayCheckHierarchy(parent); } /** * Set the checked state of self and all ancestors appropriately. Do not * white check anyone - this is only done down a hierarchy. * * @param treeElement the tree element */ private void grayUpdateHierarchy(Object treeElement) { boolean shouldBeAtLeastGray= determineShouldBeAtLeastGrayChecked(treeElement); treeViewer.setGrayChecked(treeElement, shouldBeAtLeastGray); if (whiteCheckedTreeItems.contains(treeElement)) whiteCheckedTreeItems.remove(treeElement); // proceed up the tree element hierarchy Object parent= treeContentProvider.getParent(treeElement); if (parent != null) { grayUpdateHierarchy(parent); } } public void selectAndReveal(Object treeElement) { treeViewer.reveal(treeElement); IStructuredSelection selection= new StructuredSelection(treeElement); treeViewer.setSelection(selection); } /** * Initialize this group's viewers after they have been laid out. */ private void initialize() { treeViewer.setInput(root); this.expandedTreeNodes= new ArrayList(); this.expandedTreeNodes.add(root); } /** * Callback that's invoked when the checked status of an item in the list is * changed by the user. Do not try and update the hierarchy if we are * building the initial list. * * @param listElement the list element * @param state the checked state * @param updatingFromSelection <code>true</code> if we are inside an * update caused by selection */ private void listItemChecked(Object listElement, boolean state, boolean updatingFromSelection) { List checkedListItems= (List) checkedStateStore.get(currentTreeSelection); //If it has not been expanded do so as the selection of list items will // affect gray state if (!expandedTreeNodes.contains(currentTreeSelection)) expandTreeElement(currentTreeSelection); if (state) { if (checkedListItems == null) { // since the associated tree item has gone from 0 -> 1 checked // list items, tree checking may need to be updated grayCheckHierarchy(currentTreeSelection); checkedListItems= (List) checkedStateStore.get(currentTreeSelection); } checkedListItems.add(listElement); } else { checkedListItems.remove(listElement); if (checkedListItems.isEmpty()) { // since the associated tree item has gone from 1 -> 0 checked // list items, tree checking may need to be updated ungrayCheckHierarchy(currentTreeSelection); } } //Update the list with the selections if there are any if (checkedListItems.size() > 0) checkedStateStore.put(currentTreeSelection, checkedListItems); if (updatingFromSelection) grayUpdateHierarchy(currentTreeSelection); } /** * Notify all checked state listeners with the given event. * * @param event the event */ private void notifyCheckStateChangeListeners(final CheckStateChangedEvent event) { Object[] array= listeners.getListeners(); for (int i= 0; i < array.length; i++) { final ICheckStateListener l= (ICheckStateListener) array[i]; SafeRunner.run(new SafeRunnable() { public void run() { l.checkStateChanged(event); } }); } } /** * Set the contents of the list viewer based upon the specified selected * tree element. This also includes checking the appropriate list items. * * @param treeElement java.lang.Object */ private void populateListViewer(final Object treeElement) { listViewer.setInput(treeElement); //If the element is white checked but not expanded we have not set up // all of the children yet if (!(expandedTreeNodes.contains(treeElement)) && whiteCheckedTreeItems.contains(treeElement)) { //Potentially long operation - show a busy cursor BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new Runnable() { public void run() { setListForWhiteSelection(treeElement); listViewer.setAllChecked(true); } }); } else { List listItemsToCheck= (List) checkedStateStore.get(treeElement); if (listItemsToCheck != null) { Iterator listItemsEnum= listItemsToCheck.iterator(); while (listItemsEnum.hasNext()) listViewer.setChecked(listItemsEnum.next(), true); } } } /** * Logically gray-check all ancestors of <code>item</code> by ensuring * that they appear in the checked table. Add any elements to the * <code>selectedNodes</code> so we can track that has been done. * * @param item the tree item * @param selectedNodes the set of selected nodes */ private void primeHierarchyForSelection(Object item, Set selectedNodes) { //Only prime it if we haven't visited yet if (selectedNodes.contains(item)) return; checkedStateStore.put(item, new ArrayList()); //mark as expanded as we are going to populate it after this expandedTreeNodes.add(item); selectedNodes.add(item); Object parent= treeContentProvider.getParent(item); if (parent != null) primeHierarchyForSelection(parent, selectedNodes); } /** * Remove the passed listener from self's collection of clients that listen * for changes to element checked states * * @param listener ICheckStateListener */ public void removeCheckStateListener(ICheckStateListener listener) { listeners.remove(listener); } /** * Handle the selection of an item in the tree viewer * * @param event SelectionChangedEvent */ public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection= (IStructuredSelection) event.getSelection(); Object selectedElement= selection.getFirstElement(); if (selectedElement == null) { currentTreeSelection= null; listViewer.setInput(currentTreeSelection); return; } // i.e.- if not an item deselection if (selectedElement != currentTreeSelection) populateListViewer(selectedElement); currentTreeSelection= selectedElement; } /** * Select or deselect all of the elements in the tree depending on the value * of <code>selection</code>. Be sure to update the displayed files as * well. * * @param selection <code>true</code> if selection should be set, * <code>false</code> if it should be removed */ public void setAllSelections(final boolean selection) { //If there is no root there is nothing to select if (root == null) return; //Potentially long operation - show a busy cursor BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new Runnable() { public void run() { setTreeChecked(root, selection); listViewer.setAllChecked(selection); } }); } /** * The treeElement has been white selected. Get the list for the element and * set it in the checked state store. * * @param treeElement the element being updated */ private void setListForWhiteSelection(Object treeElement) { Object[] listItems= listContentProvider.getElements(treeElement); List listItemsChecked= new ArrayList(); for (int i= 0; i < listItems.length; ++i) { listItemsChecked.add(listItems[i]); } checkedStateStore.put(treeElement, listItemsChecked); } /** * Set the <code>comparator</code> that is to be applied to self's list viewer * * @param comparator the comparator to be set */ public void setListComparator(ViewerComparator comparator) { listViewer.setComparator(comparator); } /** * Set the checked state of the passed <code>treeElement</code> * appropriately, and do so recursively to all of its child tree elements as * well. * * @param treeElement the root of the subtree * @param state <code>true</code> if checked, <code>false</code> otherwise */ private void setTreeChecked(Object treeElement, boolean state) { if (treeElement.equals(currentTreeSelection)) { listViewer.setAllChecked(state); } if (state) { setListForWhiteSelection(treeElement); } else checkedStateStore.remove(treeElement); setWhiteChecked(treeElement, state); treeViewer.setChecked(treeElement, state); treeViewer.setGrayed(treeElement, false); // now logically check/uncheck all children as well if it has been // expanded if (expandedTreeNodes.contains(treeElement)) { Object[] children= treeContentProvider.getChildren(treeElement); for (int i= 0; i < children.length; ++i) { setTreeChecked(children[i], state); } } } /** * Set the comparator that is to be applied to self's tree viewer. * * @param comparator the comparator to be set */ public void setTreeComparator(ViewerComparator comparator) { treeViewer.setComparator(comparator); } /** * Adjust the collection of references to white-checked tree elements * appropriately. * * @param treeElement the root of the subtree * @param isWhiteChecked <code>true</code> if white checked, * <code>false</code> otherwise */ private void setWhiteChecked(Object treeElement, boolean isWhiteChecked) { if (isWhiteChecked) { if (!whiteCheckedTreeItems.contains(treeElement)) whiteCheckedTreeItems.add(treeElement); } else whiteCheckedTreeItems.remove(treeElement); } /** * Handle the collapsing of an element in a tree viewer. * * @param event the collapse event */ public void treeCollapsed(TreeExpansionEvent event) { // We don't need to do anything with this } /** * Handle the expansion of an element in a tree viewer. * * @param event the expansion event */ public void treeExpanded(TreeExpansionEvent event) { expandTreeElement(event.getElement()); } /** * Callback that's invoked when the checked status of an item in the tree is * changed by the user. * * @param treeElement the tree element that has been checked/unchecked * @param state <code>true</code> if checked, <code>false</code> if * unchecked */ private void treeItemChecked(Object treeElement, boolean state) { // recursively adjust all child tree elements appropriately setTreeChecked(treeElement, state); Object parent= treeContentProvider.getParent(treeElement); if (parent == null) return; // now update upwards in the tree hierarchy if (state) grayCheckHierarchy(parent); else ungrayCheckHierarchy(parent); //Update the hierarchy but do not white select the parent grayUpdateHierarchy(parent); } /** * Logically un-gray-check all ancestors of <code>treeElement</code> if * appropriate. * * @param treeElement the root of the subtree */ private void ungrayCheckHierarchy(Object treeElement) { if (!determineShouldBeAtLeastGrayChecked(treeElement)) checkedStateStore.remove(treeElement); Object parent= treeContentProvider.getParent(treeElement); if (parent != null) ungrayCheckHierarchy(parent); } /** * Update the selections of the tree elements in items to reflect the new * selections provided. * * @param items Map with keys of Object (the tree element) and values of * List (the selected list elements). NOTE: This method does not * special case keys with no values (i.e., a tree element with an * empty list). If a tree element does not have any selected * items, do not include the element in the Map. */ public void updateSelections(Map items) { // We are replacing all selected items with the given selected items, // so reinitialize everything. this.listViewer.setAllChecked(false); this.treeViewer.setCheckedElements(new Object[0]); this.whiteCheckedTreeItems= new HashSet(); Set selectedNodes= new HashSet(); checkedStateStore= new HashMap(); //Update the store before the hierarchy to prevent updating parents // before all of the children are done Iterator keyIterator= items.keySet().iterator(); while (keyIterator.hasNext()) { Object key= keyIterator.next(); primeHierarchyForSelection(key, selectedNodes); checkedStateStore.put(key, items.get(key)); } // Update the checked tree items. Since each tree item has a selected // item, all the tree items will be gray checked. treeViewer.setCheckedElements(checkedStateStore.keySet().toArray()); treeViewer.setGrayedElements(checkedStateStore.keySet().toArray()); // Update the listView of the currently selected tree item. if (currentTreeSelection != null) { Object displayItems= items.get(currentTreeSelection); if (displayItems != null) listViewer.setCheckedElements(((List) displayItems).toArray()); } } }