/*******************************************************************************
 * Copyright (c) 2000, 2004 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials 
 * are made available under the terms of the Common Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/cpl-v10.html
 * 
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.ui.internal.ide.misc;

import java.util.*;
import java.util.List;

import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.util.ListenerList;
import org.eclipse.jface.util.SafeRunnable;
import org.eclipse.jface.viewers.*;
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.*;

/**
 *	Workbench-level composite that combines a CheckboxTreeViewer and CheckboxListViewer.
 *	All viewer selection-driven interactions are handled within this object
 */
public class CheckboxTreeAndListGroup implements ICheckStateListener, ISelectionChangedListener, ITreeViewerListener {
	private	Object			root;
	private	Object			currentTreeSelection;
	private	List			expandedTreeNodes = new ArrayList();
	private	Map				checkedStateStore = new HashMap(9);
	private List			whiteCheckedTreeItems = new ArrayList();
	private	ListenerList	listeners = new ListenerList();
	
	private	ITreeContentProvider		treeContentProvider;
	private	IStructuredContentProvider	listContentProvider;
	private ILabelProvider				treeLabelProvider;
	private ILabelProvider				listLabelProvider;
	
	// widgets
	private	CheckboxTreeViewer	treeViewer;
	private	CheckboxTableViewer	listViewer;
/**
 *  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 hardcode one of the
 *	sizing dimensions, specify the other dimension's value as -1)
 * @param parent
 * @param rootObject
 * @param treeContentProvider
 * @param treeLabelProvider
 * @param listContentProvider
 * @param listLabelProvider
 * @param style
 * @param width
 * @param height
 */
public CheckboxTreeAndListGroup(
	Composite parent,Object rootObject,
	ITreeContentProvider treeContentProvider,ILabelProvider treeLabelProvider,
	IStructuredContentProvider listContentProvider,ILabelProvider listLabelProvider,
	int style,int width,int height) {

	root = rootObject;
	this.treeContentProvider = treeContentProvider;
	this.listContentProvider = listContentProvider;
	this.treeLabelProvider = treeLabelProvider;
	this.listLabelProvider = listLabelProvider;
	createContents(parent, width, height, style);
}
/**
 * This method must be called just before this window becomes visible.
 */
public void aboutToOpen() {
	determineWhiteCheckedDescendents(root);
	checkNewTreeElements(treeContentProvider.getElements(root));
	currentTreeSelection = null;

	//select the first element in the list
	Object[] elements = treeContentProvider.getElements(root);
	Object primary = elements.length > 0 ? elements[0] : null;
	if (primary != null) {
		treeViewer.setSelection(new StructuredSelection(primary));
	}
	treeViewer.getControl().setFocus();
}
/**
 *	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);
}
/**
 *	Add the receiver and all of it's ancestors to the checkedStateStore if they
 * are not already there.
 */
private void addToHierarchyToCheckedStore(Object treeElement) {

	// if this tree element is already gray then its ancestors all are as well
	if (!checkedStateStore.containsKey(treeElement))
		checkedStateStore.put(treeElement, new ArrayList());

	Object parent = treeContentProvider.getParent(treeElement);
	if (parent != null)
		addToHierarchyToCheckedStore(parent);
}
/**
 *	Return a boolean indicating whether all children of the passed tree element
 *	are currently white-checked
 *
 *	@return boolean
 *	@param treeElement java.lang.Object
 */
protected boolean areAllChildrenWhiteChecked(Object treeElement) {
	Object[] children = treeContentProvider.getChildren(treeElement);
	for (int i = 0; i < children.length; ++i) {
		if (!whiteCheckedTreeItems.contains(children[i]))
			return false;
	}
	
	return true;
}
/**
 *	Return a boolean indicating whether all list elements associated with
 *	the passed tree element are currently checked
 *
 *	@return boolean
 *	@param treeElement java.lang.Object
 */
protected boolean areAllElementsChecked(Object treeElement) {
	List checkedElements = (List)checkedStateStore.get(treeElement);
	if (checkedElements == null)	// ie.- tree item not even gray-checked
		return false;

	return getListItemsSize(treeElement) == checkedElements.size();
}
/**
 *	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);
		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 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 width int
 *	@param height int
 */
protected void createContents(Composite parent, int width, int height, int style) {
	// group pane
	Composite composite = new Composite(parent,style);
	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));
	composite.setFont(parent.getFont());
			
	createTreeViewer(composite, width/2, height);
	createListViewer(composite, width/2, height);
	
	initialize();
}
/**
 *	Create this group's list viewer.
 */
protected void createListViewer(Composite parent, int width, int height) {
	listViewer = CheckboxTableViewer.newCheckList(parent, SWT.BORDER);
	GridData data = new GridData(GridData.FILL_BOTH);
	data.widthHint = width;
	data.heightHint = height;
	listViewer.getTable().setLayoutData(data);
	listViewer.getTable().setFont(parent.getFont());
	listViewer.setContentProvider(listContentProvider);
	listViewer.setLabelProvider(listLabelProvider);
	listViewer.addCheckStateListener(this);
}
/**
 *	Create this group's tree viewer.
 */
protected void createTreeViewer(Composite parent, int width, int height) {
	Tree tree = new Tree(parent, SWT.CHECK | SWT.BORDER);
	GridData data = new GridData(GridData.FILL_BOTH);
	data.widthHint = width;
	data.heightHint = height;
	tree.setLayoutData(data);
	tree.setFont(parent.getFont());
	
	treeViewer = new CheckboxTreeViewer(tree);
	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.
 * To determine whether a tree item should be white-checked use method
 * #determineShouldBeWhiteChecked(Object).
 *
 * @param treeElement java.lang.Object
 * @return boolean
 * @see #determineShouldBeWhiteChecked(java.lang.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
	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
	Object[] children = treeContentProvider.getChildren(treeElement);
	for (int i = 0 ; i < children.length; ++i) {
		if (checkedStateStore.containsKey(children[i]))
			return true;
	}

	return false;
}
/**
 * Returns a boolean indicating whether the passed tree item should be
 * white-checked.
 *
 * @return boolean
 * @param treeElement java.lang.Object
 */
protected boolean determineShouldBeWhiteChecked(Object treeElement) {
	return areAllChildrenWhiteChecked(treeElement) && areAllElementsChecked(treeElement);
}
/**
 *	Recursively add appropriate tree elements to the collection of
 *	known white-checked tree elements.
 *
 *	@param treeElement java.lang.Object
 */
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 element
	Object[] children = treeContentProvider.getElements(treeElement);
	for (int i = 0; i < children.length; ++i)
		determineWhiteCheckedDescendents(children[i]);

	// now determine the white-checked status for this tree element
	if (determineShouldBeWhiteChecked(treeElement))
		setWhiteChecked(treeElement,true);
}
/**
 * Cause the tree viewer to expand all its items
 */
public void expandAll() {
	treeViewer.expandAll();
}
/**
 *	Answer a flat collection of all of the checked elements in the
 *	list portion of self
 *
 *	@return java.util.Vector
 */
public Iterator getAllCheckedListItems() {
	List result = new ArrayList();
	Iterator listCollectionsEnum = checkedStateStore.values().iterator();
	
	while (listCollectionsEnum.hasNext()) {
		Iterator currentCollection = ((List)listCollectionsEnum.next()).iterator();
		while (currentCollection.hasNext())
			result.add(currentCollection.next());
	}
	
	return result.iterator();
}
/**
 *	Answer a collection of all of the checked elements in the tree portion
 *	of self
 *
 *	@return java.util.Vector
 */
public Set getAllCheckedTreeItems() {
	return checkedStateStore.keySet();
}
/**
 *	Answer the number of elements that have been checked by the
 *	user.
 *
 *	@return int
 */
public int getCheckedElementCount() {
	return checkedStateStore.size();
}
/**
 *	Return a count of the number of list items associated with a
 *	given tree item.
 *
 *	@return int
 *	@param treeElement java.lang.Object
 */
protected int getListItemsSize(Object treeElement) {
	Object[] elements = listContentProvider.getElements(treeElement);
	return elements.length;
}
/**
 * Get the table the list viewer uses.
 * @return org.eclipse.swt.widgets.Table
 */
public Table getListTable() {
	return this.listViewer.getTable();
}
/**
 *	Logically gray-check all ancestors of treeItem by ensuring that they
 *	appear in the checked table
 */
protected void grayCheckHierarchy(Object 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());
	if (determineShouldBeWhiteChecked(treeElement)) {
		setWhiteChecked(treeElement, true);
	}
	Object parent = treeContentProvider.getParent(treeElement);
	if (parent != null)
		grayCheckHierarchy(parent);
}
/**
 *	Set the initial checked state of the passed list element to true.
 */
public void initialCheckListItem(Object element) {
	Object parent = treeContentProvider.getParent(element);
	currentTreeSelection = parent;
	//As this is not done from the UI then set the box for updating from the selection to false 
	listItemChecked(element, true, false);
	updateHierarchy(parent);
}
/**
 *	Set the initial checked state of the passed element to true,
 *	as well as to all of its children and associated list elements
 */
public void initialCheckTreeItem(Object element) {
	treeItemChecked(element, true);
}
/**
 *	Initialize this group's viewers after they have been laid out.
 */
protected void initialize() {
	treeViewer.setInput(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.
 */
protected void listItemChecked(
	Object listElement,
	boolean state,
	boolean updatingFromSelection) {
	List checkedListItems = (List) checkedStateStore.get(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);
		}
	}

	if (updatingFromSelection)
		updateHierarchy(currentTreeSelection);
}
/**
 *	Notify all checked state listeners that the passed element has had
 *	its checked state changed to the passed state
 */
protected void notifyCheckStateChangeListeners(final CheckStateChangedEvent event) {
	Object[] array = listeners.getListeners();
	for (int i = 0; i < array.length; i ++) {
		final ICheckStateListener l = (ICheckStateListener)array[i];
		Platform.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
 */
protected void populateListViewer(final Object treeElement) {
	listViewer.setInput(treeElement);
	List listItemsToCheck = (List) checkedStateStore.get(treeElement);

	if (listItemsToCheck != null) {
		Iterator listItemsEnum = listItemsToCheck.iterator();
		while (listItemsEnum.hasNext())
			listViewer.setChecked(listItemsEnum.next(), true);
	}
}
/**
 *	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;
	}

	// ie.- 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 the selection
 * boolean. Be sure to update the displayed files as well.
 */
public void setAllSelections(final boolean selection) {

	//Potentially long operation - show a busy cursor
	BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new Runnable() {
		public void run() {
			setTreeChecked(root, selection);
			listViewer.setAllChecked(selection);
		}
	});
}
/**
 *	Set the list viewer's providers to those passed
 *
 *	@param contentProvider ITreeContentProvider
 *	@param labelProvider ILabelProvider
 */
public void setListProviders(IStructuredContentProvider contentProvider, ILabelProvider labelProvider) {
	listViewer.setContentProvider(contentProvider);
	listViewer.setLabelProvider(labelProvider);
}
/**
 *	Set the sorter that is to be applied to self's list viewer
 */
public void setListSorter(ViewerSorter sorter) {
	listViewer.setSorter(sorter);
}
/**
 * Set the root of the widget to be new Root. Regenerate all of the tables and lists from this
 * value.
 * @param newRoot 
 */
public void setRoot(Object newRoot) {
	this.root = newRoot;
	initialize();
}
/**
 *	Set the checked state of the passed tree element appropriately, and
 *	do so recursively to all of its child tree elements as well
 */
protected void setTreeChecked(Object treeElement,boolean state) {

	if (treeElement.equals(currentTreeSelection)) {
	    listViewer.setAllChecked(state);
	}
	
	if (state) {
		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);
	} else
		checkedStateStore.remove(treeElement);

	setWhiteChecked(treeElement,state);
	treeViewer.setChecked(treeElement,state);
	treeViewer.setGrayed(treeElement,false);
			
	// now logically check/uncheck all children as well
	Object[] children = treeContentProvider.getChildren(treeElement);
	for (int i = 0; i < children.length; ++i) {
		setTreeChecked(children[i],state);
	}
}
/**
 *	Set the tree viewer's providers to those passed
 *
 *	@param contentProvider ITreeContentProvider
 *	@param labelProvider ILabelProvider
 */
public void setTreeProviders(ITreeContentProvider contentProvider, ILabelProvider labelProvider) {
	treeViewer.setContentProvider(contentProvider);
	treeViewer.setLabelProvider(labelProvider);
}
/**
 *	Set the sorter that is to be applied to self's tree viewer
 */
public void setTreeSorter(ViewerSorter sorter) {
	treeViewer.setSorter(sorter);
}
/**
 *	Adjust the collection of references to white-checked tree elements appropriately.
 *
 *	@param treeElement java.lang.Object
 *	@param isWhiteChecked boolean
 */
protected 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
 */
public void treeCollapsed(TreeExpansionEvent event) {
	// We don't need to do anything with this
}
/**
 *	Handle the expansionsion of an element in a tree viewer
 */
public void treeExpanded(TreeExpansionEvent event) {

	Object item = event.getElement();
	
	// 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)) {
		expandedTreeNodes.add(item);
		checkNewTreeElements(treeContentProvider.getChildren(item));
	}
}
/**
 *  Callback that's invoked when the checked status of an item in the tree
 *  is changed by the user.
 */
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)
		return;

	// now update upwards in the tree hierarchy 
	if (state)
		grayCheckHierarchy(parent);
	else
		ungrayCheckHierarchy(parent);
		
	updateHierarchy(treeElement);
}
/**
 *	Logically un-gray-check all ancestors of treeItem iff appropriate.
 */
protected void ungrayCheckHierarchy(Object treeElement) {
	if (!determineShouldBeAtLeastGrayChecked(treeElement))
		checkedStateStore.remove(treeElement);

	Object parent = treeContentProvider.getParent(treeElement);
	if (parent != null)
		ungrayCheckHierarchy(parent);
}
/**
 *	Set the checked state of self and all ancestors appropriately
 */
protected void updateHierarchy(Object treeElement) {

	boolean whiteChecked = determineShouldBeWhiteChecked(treeElement);
	boolean shouldBeAtLeastGray = determineShouldBeAtLeastGrayChecked(treeElement);

	treeViewer.setChecked(treeElement, shouldBeAtLeastGray);
	setWhiteChecked(treeElement, whiteChecked);
	if (!whiteChecked)
		treeViewer.setGrayed(treeElement, shouldBeAtLeastGray);

	// proceed up the tree element hierarchy
	Object parent = treeContentProvider.getParent(treeElement);
	if (parent != null) {
		updateHierarchy(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).
 */
public void updateSelections(final Map items) {

	//Potentially long operation - show a busy cursor
	BusyIndicator.showWhile(treeViewer.getControl().getDisplay(), new Runnable() {
		public void run() {
			Iterator keyIterator = items.keySet().iterator();

			//Update the store before the hierarchy to prevent updating parents before all of the children are done
			while (keyIterator.hasNext()) {
				Object key = keyIterator.next();
				//Replace the items in the checked state store with those from the supplied items
				List selections = (List) items.get(key);
				if (selections.size() == 0)
					//If it is empty remove it from the list
					checkedStateStore.remove(key);
				else {
					checkedStateStore.put(key, selections);
					// proceed up the tree element hierarchy
					Object parent = treeContentProvider.getParent(key);
					if (parent != null) {
						addToHierarchyToCheckedStore(parent);
					}
				}
			}

			//Now update hierarchies
			keyIterator = items.keySet().iterator();

			while (keyIterator.hasNext()) {
				Object key = keyIterator.next();
				updateHierarchy(key);
				if (currentTreeSelection != null && currentTreeSelection.equals(key)) {
					listViewer.setAllChecked(false);
					listViewer.setCheckedElements(((List) items.get(key)).toArray());
				}
			}
		}
	});

}
}
