/******************************************************************************* * Copyright (c) 2009 xored software, Inc. * * 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: * xored software, Inc. - initial API and Implementation (Alex Panchenko) *******************************************************************************/ package org.eclipse.dltk.tcl.activestatedebugger.preferences; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import org.eclipse.jface.viewers.CheckStateChangedEvent; import org.eclipse.jface.viewers.CheckboxTreeViewer; import org.eclipse.jface.viewers.ICheckStateListener; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.ITreeViewerListener; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TreeExpansionEvent; import org.eclipse.swt.custom.BusyIndicator; public class TreeSelectionControl implements ITreeViewerListener, ICheckStateListener { static final boolean DEBUG = false; /** * @param viewer */ public TreeSelectionControl(CheckboxTreeViewer viewer) { this.fViewer = viewer; this.treeContentProvider = (ITreeContentProvider) viewer .getContentProvider(); } private final CheckboxTreeViewer fViewer; private final ITreeContentProvider treeContentProvider; private static class SelectionState { public static SelectionState WHITE = new SelectionState(); public static SelectionState CHECKED = new SelectionState(); @Override public String toString() { if (this == WHITE) return "WHITE"; //$NON-NLS-1$ else if (this == CHECKED) return "CHECKED"; //$NON-NLS-1$ else return super.toString(); } } protected String getLabelOf(Object element) { return ((ILabelProvider) fViewer.getLabelProvider()).getText(element); } private Map<Object, SelectionState> checkedStateStore = new HashMap<Object, SelectionState>(); private Set<Object> whiteChecked = new HashSet<Object>(); private Set<Object> expandedTreeNodes = new HashSet<Object>(); protected void treeItemChecked(Object treeElement, boolean state) { // recursively adjust all child tree elements appropriately setTreeChecked(treeElement, state); Object parent = treeContentProvider.getParent(treeElement); if (parent != null) { // 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); } if (DEBUG) { dump("onChecked"); //$NON-NLS-1$ } } void dump(String mode) { final ArrayList<Object> includes = new ArrayList<Object>(); final ArrayList<Object> excludes = new ArrayList<Object>(); ICollector collector = new ICollector() { public void include(Object object) { includes.add(object); } public void exclude(Object arg0) { excludes.add(arg0); } }; collectCheckedItems(collector); System.out.println("===" + mode + " ==="); //$NON-NLS-1$ //$NON-NLS-2$ System.out.println("[expandedTreeNodes]"); //$NON-NLS-1$ for (Iterator<?> i = expandedTreeNodes.iterator(); i.hasNext();) { Object item = i.next(); System.out.println(" " + getLabelOf(item)); //$NON-NLS-1$ } System.out.println("[white]"); //$NON-NLS-1$ for (Iterator<?> i = whiteChecked.iterator(); i.hasNext();) { Object item = i.next(); System.out.println(" " + getLabelOf(item)); //$NON-NLS-1$ } System.out.println("[state]"); //$NON-NLS-1$ for (Iterator<?> i = checkedStateStore.entrySet().iterator(); i .hasNext();) { Map.Entry<?, ?> entry = (Entry<?, ?>) i.next(); System.out.println(" " + getLabelOf(entry.getKey()) //$NON-NLS-1$ + "=" + entry.getValue()); //$NON-NLS-1$ } System.out.println("[INCLUDES]"); //$NON-NLS-1$ for (Iterator<?> i = includes.iterator(); i.hasNext();) { Object item = i.next(); System.out.println(" " + getLabelOf(item)); //$NON-NLS-1$ } System.out.println("[EXCLUDES]"); //$NON-NLS-1$ for (Iterator<?> i = excludes.iterator(); i.hasNext();) { Object item = i.next(); System.out.println(" " + getLabelOf(item)); //$NON-NLS-1$ } System.out.println("==="); //$NON-NLS-1$ } /** * Returns a flat list of all of the leaf elements which are checked. Filter * then based on the supplied ElementFilter. If monitor is cancelled then * return null * * @param collector * - the filter for the data */ public void collectCheckedItems(ICollector collector) { // Iterate through the children of the root as the root is not in the // store Object[] children = treeContentProvider.getElements(root); for (int i = 0; i < children.length; ++i) { final Object child = children[i]; collectAllSelectedItems(child, whiteChecked.contains(child), collector, false); } } public interface ICollector { public void include(Object element); public void exclude(Object element); } /** * Add all of the selected children of nextEntry to result recursively. This * does not set any values in the checked state. * * @param The * treeElement being queried * @param addAll * a boolean to indicate if the checked state store needs to be * queried * @param collector * IElementFilter - the filter being used on the data */ private void collectAllSelectedItems(Object treeElement, boolean addAll, ICollector collector, boolean wasInclude) { boolean nextWasInclude = wasInclude; if (addAll) { if (!wasInclude) { collector.include(treeElement); nextWasInclude = true; } } else { // Add what we have stored SelectionState state = checkedStateStore.get(treeElement); if (state != null) { if (state == SelectionState.CHECKED) { if (!wasInclude) { collector.include(treeElement); nextWasInclude = true; } } } else { collector.exclude(treeElement); return; } } if (!expandedTreeNodes.contains(treeElement)) { return; } Object[] treeChildren = treeContentProvider.getChildren(treeElement); for (int i = 0; i < treeChildren.length; i++) { Object child = treeChildren[i]; if (addAll) { collectAllSelectedItems(child, true, collector, nextWasInclude); } else { if (checkedStateStore.containsKey(child)) { // Only continue for those with checked state collectAllSelectedItems(child, whiteChecked.contains(child), collector, nextWasInclude); } else { collector.exclude(child); } } } } /** * */ public void install() { fViewer.addCheckStateListener(this); fViewer.addTreeListener(this); } private Object root; public void setInput(Object input) { this.root = input; fViewer.setInput(input); expandedTreeNodes.clear(); expandedTreeNodes.add(input); } /** * This method must be called just before this window becomes visible. */ public void aboutToOpen() { // determineWhiteCheckedDescendents(root); checkNewTreeElements(treeContentProvider.getElements(root)); // select the first Object in the list Object[] elements = treeContentProvider.getElements(root); Object primary = elements.length > 0 ? elements[0] : null; if (primary != null) { fViewer.setSelection(new StructuredSelection(primary)); } } /** * Recursively add appropriate tree elements to the collection of known * white-checked tree elements. * * @param treeElement */ protected void determineWhiteCheckedDescendents(Object treeElement) { // always go through all children first since their white-checked // statuses will be needed to determine the white-checked status for // this tree Object Object[] children = root == treeElement ? treeContentProvider .getElements(treeElement) : treeContentProvider .getChildren(treeElement); for (int i = 0; i < children.length; ++i) { determineWhiteCheckedDescendents(children[i]); } // now determine the white-checked status for this tree Object if (determineShouldBeWhiteChecked(treeElement)) { setWhiteChecked(treeElement, true); } } /** * Returns a boolean indicating whether the passed tree item should be * white-checked. * * @return boolean * @param treeElement */ protected boolean determineShouldBeWhiteChecked(Object treeElement) { return areAllChildrenWhiteChecked(treeElement) && isChecked(treeElement); } /** * Return a boolean indicating whether all children of the passed tree * Object are currently white-checked * * @return boolean * @param treeElement */ protected boolean areAllChildrenWhiteChecked(Object treeElement) { Object[] children = treeContentProvider.getChildren(treeElement); for (int i = 0; i < children.length; ++i) { if (!whiteChecked.contains(children[i])) { return false; } } return true; } /** * Return a boolean indicating whether all list elements associated with the * passed tree Object are currently checked * * @return boolean * @param treeElement */ protected boolean isChecked(Object treeElement) { return checkedStateStore.get(treeElement) == SelectionState.CHECKED; } /** * Set the checked state of the passed tree Object appropriately, and do so * recursively to all of its child tree elements as well */ protected void setTreeChecked(Object treeElement, boolean state) { if (state) { setListForWhiteSelection(treeElement); } else { checkedStateStore.remove(treeElement); } setWhiteChecked(treeElement, state); fViewer.setChecked(treeElement, state); fViewer.setGrayed(treeElement, false); if (expandedTreeNodes.contains(treeElement)) { Object[] children = treeContentProvider.getChildren(treeElement); for (int i = 0; i < children.length; ++i) { Object child = children[i]; setTreeChecked(child, state); } } } /** * Adjust the collection of references to white-checked tree elements * appropriately. * * @param treeElement * @param isWhiteChecked */ protected void setWhiteChecked(Object treeElement, boolean isWhiteChecked) { if (isWhiteChecked) { if (!whiteChecked.contains(treeElement)) { whiteChecked.add(treeElement); } } else { whiteChecked.remove(treeElement); } } /** * The treeElement has been white selected. Get the list for the Object and * set it in the checked state store. * * @param treeElement * the Object being updated */ private void setListForWhiteSelection(Object treeElement) { checkedStateStore.put(treeElement, SelectionState.CHECKED); } /** * Logically gray-check all ancestors of treeItem by ensuring that they * appear in the checked table */ protected void grayCheckHierarchy(Object treeElement) { // expand the Object first to make sure we have populated for it expandTreeElement(treeElement); // if this tree Object 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, SelectionState.WHITE); Object parent = treeContentProvider.getParent(treeElement); if (parent != null) { grayCheckHierarchy(parent); } } /** * Expand an Object in a tree viewer */ private void expandTreeElement(final Object item) { BusyIndicator.showWhile(fViewer.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(item)) { checkNewTreeElements(treeContentProvider .getChildren(item)); } else { expandedTreeNodes.add(item); if (whiteChecked.contains(item)) { // If this is the first expansion and this is a // white checked node then check the children Object[] children = treeContentProvider .getChildren(item); for (int i = 0; i < children.length; ++i) { if (!whiteChecked.contains(children[i])) { Object child = children[i]; setWhiteChecked(child, true); fViewer.setChecked(child, true); checkedStateStore.put(child, SelectionState.WHITE); } } // Now be sure to select the list of items too setListForWhiteSelection(item); } } } }); } /** * Iterate through the passed elements which are being realized for the * first time and check each one in the tree viewer as appropriate */ protected void checkNewTreeElements(Object[] elements) { for (int i = 0; i < elements.length; ++i) { Object currentElement = elements[i]; boolean checked = checkedStateStore.containsKey(currentElement); fViewer.setChecked(currentElement, checked); fViewer.setGrayed(currentElement, checked && !whiteChecked.contains(currentElement)); } } /** * Logically un-gray-check all ancestors of treeItem if appropriate. */ protected void ungrayCheckHierarchy(Object treeElement) { if (!determineShouldBeAtLeastGrayChecked(treeElement)) { checkedStateStore.remove(treeElement); } Object parent = treeContentProvider.getParent(treeElement); if (parent != null) { ungrayCheckHierarchy(parent); } } /** * Returns a boolean indicating whether the passed tree Object 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. * To determine whether a tree item should be white-checked use method * #determineShouldBeWhiteChecked(Object). * * @param treeElement * @return boolean * @see #determineShouldBeWhiteChecked(Object) */ protected boolean determineShouldBeAtLeastGrayChecked(Object treeElement) { // if any list items associated with treeElement are checked then it // retains its gray-checked status regardless of its children SelectionState checked = checkedStateStore.get(treeElement); if (checked == SelectionState.CHECKED) { 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; } /** * Set the checked state of self and all ancestors appropriately. Do not * white check anyone - this is only done down a hierarchy. */ private void grayUpdateHierarchy(Object treeElement) { boolean shouldBeAtLeastGray = determineShouldBeAtLeastGrayChecked(treeElement); fViewer.setGrayChecked(treeElement, shouldBeAtLeastGray); if (whiteChecked.contains(treeElement)) { whiteChecked.remove(treeElement); } // proceed up the tree Object hierarchy Object parent = treeContentProvider.getParent(treeElement); if (parent != null) { grayUpdateHierarchy(parent); } } /** * Handle the collapsing of an Object in a tree viewer */ public void treeCollapsed(TreeExpansionEvent event) { // We don't need to do anything with this } /** * Handle the expansion of an Object in a tree viewer */ public void treeExpanded(TreeExpansionEvent event) { expandTreeElement(event.getElement()); } /* * @see ICheckStateListener#checkStateChanged(CheckStateChangedEvent) */ public void checkStateChanged(CheckStateChangedEvent event) { treeItemChecked(event.getElement(), event.getChecked()); } /** * Update the selections of the tree elements in items to reflect the new * selections provided. * * @param includes * @param excludes */ public void setInitialState(Collection<?> includes, Collection<?> excludes) { // We are replacing all selected items with the given selected items, // so reinitialize everything. whiteChecked.clear(); checkedStateStore.clear(); expandedTreeNodes.clear(); final Set<Object> processedNodes = new HashSet<Object>(); for (Iterator<?> i = includes.iterator(); i.hasNext();) { final Object key = i.next(); checkedStateStore.put(key, SelectionState.CHECKED); collectHierarchy(key, processedNodes, true); } final Set<Object> allExcludes = new HashSet<Object>(); for (Iterator<?> i = excludes.iterator(); i.hasNext();) { collectHierarchy(i.next(), allExcludes, false); } for (Iterator<?> i = includes.iterator(); i.hasNext();) { final Object key = i.next(); if (!allExcludes.contains(key) && includes.contains(key)) { whiteChecked.add(key); } expandHierarchy(key, true, includes, excludes, allExcludes); } for (Iterator<?> i = excludes.iterator(); i.hasNext();) { expandHierarchy(i.next(), false, includes, excludes, allExcludes); } if (DEBUG) { dump("onLoad"); //$NON-NLS-1$ } } private void collectHierarchy(Object item, Set<Object> processedNodes, boolean include) { if (processedNodes.add(item)) { final Object parent = treeContentProvider.getParent(item); if (parent != null) { if (include) { checkedStateStore.put(parent, SelectionState.WHITE); } collectHierarchy(parent, processedNodes, include); } } } private void expandHierarchy(Object item, boolean include, Collection<?> includes, Collection<?> excludes, Set<Object> allExcludes) { if (DEBUG) { System.out.println(" expandHierarchy(" + getLabelOf(item) + ')'); //$NON-NLS-1$ } final Object parent = treeContentProvider.getParent(item); if (parent != null) { if (expandedTreeNodes.add(parent)) { if (DEBUG) { System.out.println(" expand " + getLabelOf(parent)); //$NON-NLS-1$ } final Object[] children = treeContentProvider .getChildren(parent); boolean anyExclude = false; for (int i = 0; i < children.length; ++i) { final Object child = children[i]; if (allExcludes.contains(child)) { anyExclude = true; break; } } for (int i = 0; i < children.length; ++i) { final Object child = children[i]; if (!excludes.contains(child)) { checkedStateStore.put(child, SelectionState.CHECKED); } if (!allExcludes.contains(child)) { if (anyExclude) { whiteChecked.add(child); } } } expandHierarchy(parent, include, includes, excludes, allExcludes); } } } public void resetState() { whiteChecked.clear(); checkedStateStore.clear(); } }