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