blob: 278be8fafe7700c61beeade87029388049d246f3 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2016 ALL4TEC & CEA LIST.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* ALL4TEC & CEA LIST - initial API and implementation
******************************************************************************/
package org.polarsys.esf.core.common.ui.treeviewer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTreeViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.ITreeContentProvider;
/**
* Adds selection behaviour to CheckboxTreeSelectionHelper.
*
* Checking an element affects the following changes:
* <ol>
* <li>changes all descendants to the same check state</li>
* <li>if all siblings of the element are selected (and the element itself), sets parent element to checked state</li>
* <li>if some but not all siblings are in a checked state (including the element itself), parent is grayed</li>
* <li>if all siblings (and the element itself) are unchecked, parent is unchecked</li>
* <li>previous logic is applied to all ancestors of the element until the root is reached</li>
* </ol>
*
* If specified, there is a limitation applied on the number of final selected elements too. What is a final element is
* based on external strategy.
*
* Example usage: <br>
* <br>
* <code>
* CheckboxTreeSelectionHelper.attach(myCheckboxTreeViewer, myContentProvider);<br>
* myCheckboxTreeViewer.setInput(myInput);
* </code>
*
* @author $Author: jdumont $
* @version $Revision: 83 $
*/
public final class CheckboxTreeSelectionHelper {
/** Message when there is no content provider to set. */
private static final String MISSING_CONTENT_PROVIDER_ERROR = "Content provider is required"; //$NON-NLS-1$
/** The viewer. */
private CheckboxTreeViewer mViewer = null;
/** The content provider. */
private ITreeContentProvider mContentProvider = null;
/** Maximum number of final nodes selected. A final node is a node which is count as an item by the user. */
private int mMaximumFinalNodesSelected = 1;
/** Current number of final nodes selected. */
private int mCurrentFinalNodesSelected = 0;
/** Strategy for final node determination. */
private IFinalNodeStrategy mNodeStrategy = null;
/**
* Instantiates a new checkbox tree selection helper.
*
* @param pViewer The viewer
* @param pContentProvider The content provider
* @param pMaxFinalNodesSelected The maximum final nodes selected
* @param pFinalNodeStrategy Strategy for final node determination
*/
private CheckboxTreeSelectionHelper(final CheckboxTreeViewer pViewer,
final ITreeContentProvider pContentProvider,
final int pMaxFinalNodesSelected,
final IFinalNodeStrategy pFinalNodeStrategy) {
mViewer = pViewer;
mMaximumFinalNodesSelected = pMaxFinalNodesSelected;
mNodeStrategy = pFinalNodeStrategy;
// Check if content provider is not null
if (pContentProvider != null) {
mContentProvider = pContentProvider;
} else {
throw new IllegalArgumentException(MISSING_CONTENT_PROVIDER_ERROR);
}
init();
}
/**
* Initialise the listener and set the content provider if necessary.
*/
private void init() {
mViewer.addCheckStateListener(new CheckBoxStateListener());
// Update content provider viewer with this current provider
if (!mViewer.getContentProvider().equals(mContentProvider)) {
mViewer.setContentProvider(mContentProvider);
}
}
/**
* @return <code>true</code> if a final node can be added, <code>false</code> otherwise
*/
private boolean canAddFinalNode() {
return (mCurrentFinalNodesSelected < mMaximumFinalNodesSelected) || (mMaximumFinalNodesSelected == 0);
}
/**
* Check if the current node is final or not.
* What is final is just up to the user of this class. Final is more than just "no child". By example, if the user
* must choose a file, a folder without files is not a final node, even if it has no child.
*
* @param pNode The node to evaluate
* @return <code>true</code> if it is a final node, <code>false</code> otherwise
*/
private boolean isFinalNode(final Object pNode) {
boolean vResult = false;
if (mNodeStrategy != null) {
vResult = mNodeStrategy.isFinal(pNode);
}
return vResult;
}
/**
* Attach content provider and tree viewer with singleton instance of helper.
* Use the default behaviour for selection limitation : no limitation.
*
* @param pViewer The viewer
* @param pContentProvider The content provider
* @return The checkbox tree selection helper
*/
public static CheckboxTreeSelectionHelper attach(
final CheckboxTreeViewer pViewer,
final ITreeContentProvider pContentProvider) {
return new CheckboxTreeSelectionHelper(pViewer, pContentProvider, 0, null);
}
/**
* Attach content provider and tree viewer with singleton instance of helper.
*
* @param pViewer The viewer
* @param pContentProvider The content provider
* @param pMaxFinalNodesSelected The maximum final nodes selected
* @param pFinalNodeStrategy Strategy for final node determination
* @return The checkbox tree selection helper
*/
public static CheckboxTreeSelectionHelper attach(
final CheckboxTreeViewer pViewer,
final ITreeContentProvider pContentProvider,
final int pMaxFinalNodesSelected,
final IFinalNodeStrategy pFinalNodeStrategy) {
return new CheckboxTreeSelectionHelper(pViewer, pContentProvider, pMaxFinalNodesSelected, pFinalNodeStrategy);
}
/**
* Update tree.
*
* @param pNode Current node
* @param pCheckedElements All checked elements
* @param pChecked New state for current node
*/
private void updateTree(final Object pNode, final List<Object> pCheckedElements, final boolean pChecked) {
// Get descendants of current node
// NB : The list is built by depth, this order is important
// for the following loop
List<Object> vDescendantsList = getDescendants(pNode);
// Create set from checked tree elements list
Set<Object> vCheckedSet = new HashSet<Object>(pCheckedElements);
// Iterate on each descendant, by depth. This is not only direct descendants, but also indirect too.
// Moreover, the loop is stopped when the limit of allowed selected element is reached.
for (int i = 0; i < vDescendantsList.size() && canAddFinalNode(); i++) {
// Get the current descendant
Object vDescendant = vDescendantsList.get(i);
boolean vIsFinal = isFinalNode(vDescendant);
boolean vOriginalState = mViewer.getChecked(vDescendant);
// Erase gray state of descendant
mViewer.setGrayChecked(vDescendant, false);
// If it is an uncheck or if no more final node can be selected, there will only have uncheck action
boolean vCanCheck = pChecked && canAddFinalNode();
boolean vShouldHaveCheck = pChecked && !canAddFinalNode();
// There is still room left for selection
if (vCanCheck) {
mViewer.setChecked(vDescendant, pChecked);
// If it is a final node, count it
if (vIsFinal) {
mCurrentFinalNodesSelected++;
}
vCheckedSet.add(vDescendant);
} else {
// If it is an unchecked, reduce the number of final node already selected
if (vIsFinal && !pChecked && vOriginalState) {
mCurrentFinalNodesSelected--;
}
// No more room left, unselect each one, no matter if it is a final node or not
mViewer.setChecked(vDescendant, vCanCheck);
vCheckedSet.remove(vDescendant);
}
if (vShouldHaveCheck) {
// It is mandatory to keep a correct behavior.
// A final node should have been selected it there was still room free. Like it is not the case, the
// behavior coming from parent is bad and must be corrected
updateAncestors(vDescendant, vCheckedSet);
}
}
// Update ancestor's node
updateAncestors(pNode, vCheckedSet);
}
/**
* Update recursively ancestors.
*
* @param pChild An element for which ancestors will be updated
* @param pCheckedElements Current already checked elements
*/
private void updateAncestors(final Object pChild, final Set<Object> pCheckedElements) {
// Get parent's child
Object vParent = mContentProvider.getParent(pChild);
if (vParent != null) {
// Determine gray state of parent with child state
boolean vIsGrayed = mViewer.getChecked(pChild) && mViewer.getGrayed(pChild);
if (vIsGrayed) {
// if child is grayed then everything up should be grayed as well
mViewer.setGrayChecked(vParent, true);
} else {
// Get parent's children
Object[] vChildren = mContentProvider.getChildren(vParent);
// Filter by known checked elements
List<Object> vCloned = new ArrayList<Object>();
vCloned.addAll(Arrays.asList(vChildren));
vCloned.removeAll(pCheckedElements);
if (vCloned.isEmpty()) {
// Every child is checked
mViewer.setGrayed(vParent, false);
mViewer.setChecked(vParent, true);
// Add parent to checked elements set
pCheckedElements.add(vParent);
} else {
// Verify if parent is only checked
if (mViewer.getChecked(vParent) && !mViewer.getGrayed(vParent)) {
pCheckedElements.remove(vParent);
}
// Erase gray state of parent
mViewer.setGrayChecked(vParent, false);
// Some children selected but not all
if (vCloned.size() < vChildren.length) {
mViewer.setGrayChecked(vParent, true);
}
}
}
// Recursive call on parent
updateAncestors(vParent, pCheckedElements);
}
}
/**
* Gets the descendants.
*
* @param pNode The node
* @return The descendants
*/
private List<Object> getDescendants(final Object pNode) {
List<Object> vDescendantsList = new ArrayList<Object>();
getDescendantsHelper(vDescendantsList, pNode);
return vDescendantsList;
}
/**
* Help to construct the list of descendants for a given node.
* The list built must follow a specific order to ensure that when the
* descendant are browsed, it's made by depth first, and not on siblings children first.
*
* @param pDescendantsList The descendants list, which will be modified recursively and build by depth
* @param pNode The node for which descendants are searched recursively
*/
private void getDescendantsHelper(final List<Object> pDescendantsList, final Object pNode) {
// Get the children of the current node
Object[] vChildren = mContentProvider.getChildren(pNode);
// Loop on the children found
for (Object vChild : vChildren) {
// Add the current child to the descendant list
pDescendantsList.add(vChild);
// Make a recursive call to add its own children just after him
// NB : It's sibling elements will be added on the next loop iteration
getDescendantsHelper(pDescendantsList, vChild);
}
}
/**
* Gets the checked elements (excluding grayed out elements).
*
* @return The checked elements
*/
public List<Object> getCheckedElements() {
List<Object> vCheckedElements = new ArrayList<Object>(Arrays.asList(mViewer.getCheckedElements()));
vCheckedElements.removeAll(getGrayedElements());
return vCheckedElements;
}
/**
* Gets the grayed elements.
*
* @return The grayed elements
*/
public List<Object> getGrayedElements() {
return Arrays.asList(mViewer.getGrayedElements());
}
/**
* @return The maximum of final node allowed in selection
*/
public int getMaximumFinalNodesSelected() {
return mMaximumFinalNodesSelected;
}
/**
* @param pMaximumFinalNodesSelected The maximum of final node allowed in selection
*/
public void setMaximumFinalNodesSelected(final int pMaximumFinalNodesSelected) {
mMaximumFinalNodesSelected = pMaximumFinalNodesSelected;
}
/**
* @return The current number of final nodes selected
*/
public int getCurrentFinalNodesSelected() {
return mCurrentFinalNodesSelected;
}
/**
* @param pCurrentFinalNodesSelected The current number of final nodes selected
*/
public void setCurrentFinalNodesSelected(final int pCurrentFinalNodesSelected) {
mCurrentFinalNodesSelected = pCurrentFinalNodesSelected;
}
/**
* @return The final node strategy
*/
public IFinalNodeStrategy getNodeStrategy() {
return mNodeStrategy;
}
/**
* @param pNodeStrategy The final node strategy to set
*/
public void setNodeStrategy(final IFinalNodeStrategy pNodeStrategy) {
mNodeStrategy = pNodeStrategy;
}
/**
*
* Class used as listener for the selected state of each element in tree viewer.
*
* @author $Author: jdumont $
* @version $Revision: 83 $
*/
private class CheckBoxStateListener
implements ICheckStateListener {
/**
* Default constructor.
*/
CheckBoxStateListener() {
}
/**
* {@inheritDoc}
*
* Overridden to manually manage the grayed items.
*/
@Override
public void checkStateChanged(final CheckStateChangedEvent pEvent) {
// Get element
Object vNode = pEvent.getElement();
boolean vChecked = pEvent.getChecked();
boolean vFinalNode = isFinalNode(vNode);
if (vChecked) {
// If it is a check, check if there is still room left. Otherwise uncheck it
if (!canAddFinalNode()) {
mViewer.setChecked(vNode, false);
vChecked = false;
} else {
// If it is a final node keep track of selected final nodes number
if (vFinalNode) {
mCurrentFinalNodesSelected++;
}
}
} else if (vFinalNode) {
// It is an uncheck on a final node
mCurrentFinalNodesSelected--;
}
// Erase gray state of state node
if (mViewer.getGrayed(vNode)) {
mViewer.setGrayChecked(vNode, false);
}
// Get all checked elements
List<Object> vCheckedElements = getCheckedElements();
/*
* Update tree according to :
* - Current node
* - Current checked elements
* - New state of current node
*/
updateTree(vNode, vCheckedElements, vChecked);
}
}
}