/*******************************************************************************
* Copyright (c) 2012-2017 Codenvy, S.A.
* 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:
* Codenvy, S.A. - initial API and implementation
*******************************************************************************/
package org.eclipse.che.ide.ui.tree;
import org.eclipse.che.ide.collections.ListHelper;
import org.eclipse.che.ide.util.input.SignalEvent;
import java.util.ArrayList;
import java.util.List;
//import com.google.collide.client.util.logging.Log;
/*
* TODO : Since we have breadcrumbs and will soon have tabs, we don't
* need the notion of an active node that is separate from selected nodes.
*/
/**
* Selection model for selected selected nodes in a {@link Tree}.
* <p/>
* A Tree allows for multiple selected nodes with modifiers for ctrl and shift
* click driven selects.
*/
public class SelectionModel<D> {
private List<D> selectedNodes;
private final NodeDataAdapter<D> dataAdapter;
private final Tree.Css css;
private final boolean multilevelSelection;
public SelectionModel(NodeDataAdapter<D> dataAdapter, Tree.Css css) {
this(dataAdapter, css, false);
}
public SelectionModel(final NodeDataAdapter<D> dataAdapter, final Tree.Css css, final boolean multilevelSelection) {
this.dataAdapter = dataAdapter;
this.css = css;
this.selectedNodes = new ArrayList<>();
this.multilevelSelection = multilevelSelection;
}
/**
* Adds the node to the selection if it isn't already there in response to a
* context selection.
*/
public boolean contextSelect(D nodeData) {
if (selectedNodes.isEmpty()) {
// There are no selected nodes. So we should select.
insertAndSelectNode(nodeData, 0, true);
return true;
}
if (!hasSameParent(selectedNodes.get(0), nodeData) || selectedNodes.size() == 1) {
return selectSingleNode(nodeData);
}
if (!selectedNodes.contains(nodeData)) {
int insertionIndex = getInsertionIndex(nodeData);
insertAndSelectNode(nodeData, insertionIndex, true);
return true;
}
return false;
}
/**
* Returns the list of selected nodes. Not a copy. So don't play fast and
* loose mutating the list outside of this API!
*/
public List<D> getSelectedNodes() {
return selectedNodes;
}
public void removeNode(D nodeData) {
selectedNodes.remove(nodeData);
}
/**
* Restores visual selection for all selected nodes tracked by the
* SelectionModel.
*/
public List<List<String>> computeSelectedPaths() {
List<List<String>> selectedPaths = new ArrayList<>();
for (int i = 0, n = selectedNodes.size(); i < n; i++) {
D nodeData = selectedNodes.get(i);
selectedPaths.add(dataAdapter.getNodePath(nodeData));
}
return selectedPaths;
}
/**
* Adds the specified node to the list of selected nodes. The list of selected
* nodes is guaranteed to be sorted in the same direction that the nodes
* appear in the tree. We only allow nodes to be in the selected list that are
* peers in the tree (we do not allow selects to span multiple depths in the
* tree.
* <p/>
* Behavior: If no modifier key is depressed, the list of selected nodes will
* be set to contain just the node passed to this method.
* <p/>
* Shift select: If shift is depressed, then we attempt to do a continuous
* range select. If there exists one or more nodes in the selected nodes list,
* we test if the node falls within the list. If it does not fall within, we
* connect the contiguous range of nodes from the specified node to the
* nearest selected node. If the node falls within the list, we do a
* continuous range selection to the LAST node that was selected, not the
* closest.
* <p/>
* CTRL select: If CTRL is depressed then we simply search for the insertion
* point of the specified node in the already sorted select list. If the node
* is already present, then we remove it and unselect the node. If it was not
* present, then we insert the node at the appropriate spot in the array and
* select it.
*
* @param nodeData
* the node to select
* @param event
* the DOM event that was associated with the select trigger.
* This is needed to detect modifier keys. If {@code null} then we
* assume that we are appending to the selection and behave just like a
* CTRL-click.
* @return whether or not the select region changed at all.
*/
public boolean selectNode(D nodeData, SignalEvent event) {
if (selectedNodes.isEmpty()) {
// There are no selected nodes. So we should select.
insertAndSelectNode(nodeData, 0, true);
return true;
}
// Ensure that the node we are selecting is a child of the same
// directory of the other nodes.
if (!hasSameParent(selectedNodes.get(0), nodeData)) {
if (!this.multilevelSelection || event.getShiftKey()) {
return selectSingleNode(nodeData);
}
}
// So we are guaranteed to have a node that is a peer of the current set of
// nodes. Now we must examine modifier keys.
if (event == null || event.getCommandKey()) {
ctrlSelect(nodeData);
return true;
} else {
if (event.getShiftKey()) {
return shiftSelect(nodeData);
}
}
// Neither a shift nor a ctrl select. So replace the contents of the
// selected list with this node.
return selectSingleNode(nodeData);
}
/**
* Clears the the current selection and selects a single node.
*
* @return returns whether or not we actually changed the selection.
*/
public boolean selectSingleNode(D nodeData) {
// This is the case where we have a single node selected, and it is the same
// one we are clicking. We do nothing in this case.
if ((selectedNodes.size() == 1) && (selectedNodes.get(0).equals(nodeData))) {
return false;
}
clearSelections();
insertAndSelectNode(nodeData, 0, true);
return true;
}
public void clearSelections() {
visuallySelect(selectedNodes, false);
selectedNodes.clear();
}
/**
* Collects all nodes in a continuous range from the start node to the end
* node (assuming that the nodes are peers) obeying the inclusion boolean
* params for the boundaries. These nodes are not allowed to be in the
* selected list.
*/
private List<D> collectRangeToSelect(
D startNode, D endNode, boolean includeStart, boolean includeEnd) {
D parentNode = dataAdapter.getParent(startNode);
// Do some debug compile sanity checking.
assert (parentNode != null) : "Null parent node when doing range select!";
assert (parentNode.equals(
dataAdapter.getParent(endNode))) : "Different parent nodes when doing range highlight!";
assert (dataAdapter.compare(startNode, endNode) <= 0) :
"Nodes are in reverse order for range select! " + dataAdapter.getNodeName(startNode) + " - "
+ dataAdapter.getNodeName(endNode);
List<D> range = new ArrayList<>();
// Do a linear scan until we find the startNode.
List<D> children = dataAdapter.getChildren(parentNode);
int i = 0;
boolean adding = false;
for (int n = children.size(); i < n; i++) {
D child = children.get(i);
if (child.equals(startNode)) {
adding = true;
if (includeStart) {
range.add(child);
}
continue;
}
if (adding) {
if (child.equals(endNode)) {
if (!includeEnd) {
break;
}
range.add(child);
break;
}
range.add(child);
}
}
// Sanity check
if (i == children.size()) {
// Log.error(getClass(), "Failed to find the start when doing a range selection. Start:",
// startNode, " End:", endNode);
}
return range;
}
/**
* If CTRL is depressed then we simply search for the insertion point of the
* specified node in the already sorted select list. If the node is already
* present, then we remove it and unselect the node. If it was not present,
* then we insert the node at the appropriate spot in the array and select it.
*/
private void ctrlSelect(D nodeData) {
// Find the relevant spot in the list of selected nodes.
int insertionIndex = getInsertionIndex(nodeData);
// Either select or not select depending on whether or not it was
// already present in the list.
insertAndSelectNode(
nodeData, insertionIndex, !nodeData.equals(selectedNodes.get(insertionIndex)));
}
private int getInsertionIndex(D nodeData) {
int insertionIndex = 0;
while (insertionIndex < selectedNodes.size()
&& dataAdapter.compare(nodeData, selectedNodes.get(insertionIndex)) > 0) {
insertionIndex++;
}
return insertionIndex;
}
private boolean hasSameParent(D a, D b) {
D parent1 = dataAdapter.getParent(a);
D parent2 = dataAdapter.getParent(b);
return parent1 == parent2 || (parent1 != null && parent1.equals(parent2));
}
private void insertAndSelectNode(D nodeData, int insertionIndex, boolean selectingNewNode) {
// Visually represent it.
visuallySelect(nodeData, selectingNewNode);
// Update the model.
if (selectingNewNode) {
// The node was not in the list. Add it.
ListHelper.splice(selectedNodes, insertionIndex, 0, nodeData);
} else {
// The node was already in the list. Take it out.
ListHelper.splice(selectedNodes, insertionIndex, 1);
}
}
/**
* If shift is depressed, then we attempt to do a continuous range select. If
* there exists one or more nodes in the selected nodes list, we test if the
* node falls within the list. If it does not fall within, we connect the
* contiguous range of nodes from the specified node to the nearest selected
* node. If the node falls within the list, we do a continuous range selection
* to the LAST node that was selected, not the closest.
*/
private boolean shiftSelect(D nodeData) {
// We are guaranteed to have at least one node in the list.
D firstNode = selectedNodes.get(0);
D lastNode = selectedNodes.get(selectedNodes.size() - 1);
int comparisonToFirst = dataAdapter.compare(nodeData, firstNode);
int comparisonToLast = dataAdapter.compare(nodeData, lastNode);
// If it is to the left.
if (comparisonToFirst < 0) {
List<D> range = collectRangeToSelect(nodeData, firstNode, true, false);
visuallySelect(range, true);
selectedNodes.addAll(range);
return true;
}
// If it is to the right.
if (comparisonToLast > 0) {
List<D> range = collectRangeToSelect(lastNode, nodeData, false, true);
visuallySelect(range, true);
selectedNodes.addAll(range);
return true;
}
// If it is somewhere in between, or on the boundary.
if (comparisonToFirst >= 0 && comparisonToLast <= 0) {
// Clear the set of selected nodes.
clearSelections();
selectedNodes = collectRangeToSelect(nodeData, lastNode, true, true);
visuallySelect(selectedNodes, true);
return true;
}
assert false : "SelectionModel#shiftSelect(D): This should be unreachable!";
return false;
}
private void visuallySelect(D nodeData, boolean isSelected) {
TreeNodeElement<D> renderedNode = dataAdapter.getRenderedTreeNode(nodeData);
if (renderedNode != null) {
renderedNode.setSelected(isSelected, active, css);
}
}
private void visuallySelect(List<D> nodeDatas, boolean isSelected) {
for (D nodeData : nodeDatas) {
visuallySelect(nodeData, isSelected);
}
}
/**
* Indicates whether tree has focus or not.
* Is used for changing highlighting of selected nodes.
*/
private boolean active = false;
/**
* Sets new tree state
*
* @param active active or not
*/
public void setTreeActive(boolean active) {
this.active = active;
}
/**
* Sets new tree state and updates the selection.
*
* @param active active or not
*/
public void updateSelection(boolean active) {
this.active = active;
for (int i = 0, n = selectedNodes.size(); i < n; i++) {
D nodeData = selectedNodes.get(i);
TreeNodeElement<D> renderedNode = dataAdapter.getRenderedTreeNode(nodeData);
if (renderedNode != null) {
renderedNode.setSelected(true, active, css);
}
}
}
}