From 57e9ad50450a4ed3c9661d123f38cc6c4c4fb7ed Mon Sep 17 00:00:00 2001 From: John Alhroos Date: Thu, 6 May 2010 13:32:45 +0000 Subject: [PATCH] - Added Ctrl+Shift multiple selection to Tree - Moved MultiSelectMode enum to AbstractSelect - Added test application for Tree ctrl+Shift selection - Added keyboard navigation sample to the Sampler feature set svn changeset:13070/svn branch:6.4 --- .../vaadin/terminal/gwt/client/ui/VTree.java | 454 +++++++++++++++++- src/com/vaadin/ui/AbstractSelect.java | 15 + src/com/vaadin/ui/Table.java | 16 - src/com/vaadin/ui/Tree.java | 71 ++- .../components/tree/CtrlShiftMultiselect.java | 117 +++++ .../table/TestMultipleSelection.java | 2 +- 6 files changed, 656 insertions(+), 19 deletions(-) create mode 100644 tests/src/com/vaadin/tests/components/tree/CtrlShiftMultiselect.java diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTree.java b/src/com/vaadin/terminal/gwt/client/ui/VTree.java index 42421b80de..f4d968df5d 100644 --- a/src/com/vaadin/terminal/gwt/client/ui/VTree.java +++ b/src/com/vaadin/terminal/gwt/client/ui/VTree.java @@ -4,9 +4,12 @@ package com.vaadin.terminal.gwt.client.ui; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; import java.util.Set; import com.google.gwt.dom.client.NativeEvent; @@ -43,12 +46,17 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { public static final String ITEM_CLICK_EVENT_ID = "itemClick"; + public static final int MULTISELECT_MODE_DEFAULT = 0; + public static final int MULTISELECT_MODE_SIMPLE = 1; + private Set selectedIds = new HashSet(); private ApplicationConnection client; private String paintableId; private boolean selectable; private boolean isMultiselect; private String currentMouseOverKey; + private TreeNode lastSelection; + private int multiSelectMode = MULTISELECT_MODE_DEFAULT; private final HashMap keyToNode = new HashMap(); @@ -152,6 +160,10 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { selectable = !"none".equals(selectMode); isMultiselect = "multi".equals(selectMode); + if (isMultiselect) { + multiSelectMode = uidl.getIntAttribute("multiselectmode"); + } + selectedIds = uidl.getStringArrayVariableAsSet("selected"); rendering = false; @@ -314,6 +326,10 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { treeNode.setSelected(false); } + sendSelectionToServer(); + } + + private void sendSelectionToServer() { client.updateVariable(paintableId, "selected", selectedIds .toArray(new String[selectedIds.size()]), immediate); } @@ -430,6 +446,47 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { } } + /** + * Handles mouse selection + * + * @param ctrl + * Was the ctrl-key pressed + * @param shift + * Was the shift-key pressed + * @return Returns true if event was handled, else false + */ + private boolean handleClickSelection(boolean ctrl, boolean shift) { + + if (multiSelectMode == MULTISELECT_MODE_SIMPLE || !isMultiselect) { + toggleSelection(); + lastSelection = this; + } else if (multiSelectMode == MULTISELECT_MODE_DEFAULT) { + // Handle ctrl+click + if (isMultiselect && ctrl && !shift) { + toggleSelection(); + lastSelection = this; + + // Handle shift+click + } else if (isMultiselect && !ctrl && shift) { + deselectAll(); + selectNodeRange(lastSelection.key, key); + sendSelectionToServer(); + + // Handle ctrl+shift click + } else if (isMultiselect && ctrl && shift) { + selectNodeRange(lastSelection.key, key); + + // Handle click + } else { + deselectAll(); + toggleSelection(); + lastSelection = this; + } + } + + return true; + } + @Override public void onBrowserEvent(Event event) { super.onBrowserEvent(event); @@ -453,13 +510,20 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { toggleState(); } else if (!readonly && inCaption) { // caption click = selection change && possible click event - toggleSelection(); + if (handleClickSelection(event.getCtrlKey() + || event.getMetaKey(), event.getShiftKey())) { + event.preventDefault(); + } } DOM.eventCancelBubble(event, true); } else if (type == Event.ONCONTEXTMENU) { showContextMenu(event); } + if (type == Event.ONMOUSEDOWN) { + event.preventDefault(); + } + if (dragMode != 0 || dropHandler != null) { if (type == Event.ONMOUSEDOWN) { if (nodeCaptionDiv.isOrHasChild(event.getTarget())) { @@ -674,6 +738,24 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { return childrenLoaded; } + /** + * Returns the children of the node + * + * @return A set of tree nodes + */ + public List getChildren() { + List nodes = new LinkedList(); + + if (!isLeaf() && isChildrenLoaded()) { + Iterator iter = childNodeContainer.iterator(); + while (iter.hasNext()) { + TreeNode node = (TreeNode) iter.next(); + nodes.add(node); + } + } + return nodes; + } + public Action[] getActions() { if (actionKeys == null) { return new Action[] {}; @@ -713,6 +795,30 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { return VTree.this.isSelected(this); } + /** + * Travels up the hierarchy looking for this node + * + * @param child + * The child which grandparent this is or is not + * @return True if this is a grandparent of the child node + */ + public boolean isGrandParentOf(TreeNode child) { + TreeNode currentNode = child; + boolean isGrandParent = false; + while (currentNode != null) { + currentNode = currentNode.getParentNode(); + if (currentNode == this) { + isGrandParent = true; + break; + } + } + return isGrandParent; + } + + public boolean isSibling(TreeNode node) { + return node.getParentNode() == getParentNode(); + } + public void showContextMenu(Event event) { if (!readonly && !disabled) { if (actionKeys != null) { @@ -753,6 +859,11 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { super.onDetach(); client.getContextMenu().ensureHidden(this); } + + @Override + public String toString() { + return nodeCaptionSpan.getInnerText(); + } } public VDropHandler getDropHandler() { @@ -762,4 +873,345 @@ public class VTree extends FlowPanel implements Paintable, VHasDropHandler { public TreeNode getNodeByKey(String key) { return keyToNode.get(key); } + + /** + * Deselects all items in the tree + */ + public void deselectAll() { + for (String key : selectedIds) { + TreeNode node = keyToNode.get(key); + if (node != null) { + node.setSelected(false); + } + } + selectedIds.clear(); + } + + /** + * Selects a range of nodes + * + * @param startNodeKey + * The start node key + * @param endNodeKey + * The end node key + */ + private void selectNodeRange(String startNodeKey, String endNodeKey){ + + TreeNode startNode = keyToNode.get(startNodeKey); + TreeNode endNode = keyToNode.get(endNodeKey); + + // The nodes have the same parent + if(startNode.getParent() == endNode.getParent()){ + doSiblingSelection(startNode, endNode); + + // The start node is a grandparent of the end node + } else if (startNode.isGrandParentOf(endNode)) { + doRelationSelection(startNode, endNode); + + // The end node is a grandparent of the start node + } else if (endNode.isGrandParentOf(startNode)) { + doRelationSelection(endNode, startNode); + + } else { + doNoRelationSelection(startNode, endNode); + } + } + + /** + * Selects all the open children to a node + * + * @param node + * The parent node + */ + private void selectAllChildren(TreeNode node, boolean includeRootNode) { + if (includeRootNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + for (TreeNode child : node.getChildren()) { + if (!child.isLeaf() && child.getState()) { + selectAllChildren(child, true); + } else { + child.setSelected(true); + selectedIds.add(child.key); + } + } + } + + /** + * Selects all children until a stop child is reached + * + * @param root + * The root not to start from + * @param stopNode + * The node to finish with + * @param includeRootNode + * Should the root node be selected + * @param includeStopNode + * Should the stop node be selected + * + * @return Returns false if the stop child was found, else true if all + * children was selected + */ + private boolean selectAllChildrenUntil(TreeNode root, TreeNode stopNode, + boolean includeRootNode, boolean includeStopNode) { + if (includeRootNode) { + root.setSelected(true); + selectedIds.add(root.key); + } + if (root.getState() && root != stopNode) { + for (TreeNode child : root.getChildren()) { + if (!child.isLeaf() && child.getState() && child != stopNode) { + if (!selectAllChildrenUntil(child, stopNode, true, + includeStopNode)) { + return false; + } + } else if (child == stopNode) { + if (includeStopNode) { + child.setSelected(true); + selectedIds.add(child.key); + } + return false; + } else if (child.isLeaf()) { + child.setSelected(true); + selectedIds.add(child.key); + } + } + } + + return true; + } + + /** + * Select a range between two nodes which have no relation to each other + * + * @param startNode + * The start node to start the selection from + * @param endNode + * The end node to end the selection to + */ + private void doNoRelationSelection(TreeNode startNode, TreeNode endNode) { + + TreeNode commonParent = getCommonGrandParent(startNode, endNode); + TreeNode startBranch = null, endBranch = null; + + // Find the children of the common parent + List children; + if(commonParent != null){ + children = commonParent.getChildren(); + }else{ + children = new LinkedList(); + for (Widget w : getChildren()) { + children.add((TreeNode) w); + } + } + + // Find the start and end branches + for (TreeNode node : children) { + if (nodeIsInBranch(startNode, node)) { + startBranch = node; + } + if (nodeIsInBranch(endNode, node)) { + endBranch = node; + } + } + + // Swap nodes if necessary + if (children.indexOf(startBranch) > children.indexOf(endBranch)) { + TreeNode temp = startBranch; + startBranch = endBranch; + endBranch = temp; + + temp = startNode; + startNode = endNode; + endNode = temp; + } + + // Select all children under the start node + selectAllChildren(startNode, true); + TreeNode startParent = startNode.getParentNode(); + TreeNode currentNode = startNode; + while (startParent != null && startParent != commonParent) { + List startChildren = startParent.getChildren(); + for (int i = startChildren.indexOf(currentNode) + 1; i < startChildren + .size(); i++) { + selectAllChildren(startChildren.get(i), true); + } + + currentNode = startParent; + startParent = startParent.getParentNode(); + } + + // Select nodes until the end node is reached + for (int i = children.indexOf(startBranch) + 1; i <= children + .indexOf(endBranch); i++) { + selectAllChildrenUntil(children.get(i), endNode, true, true); + } + + // Ensure end node was selected + endNode.setSelected(true); + selectedIds.add(endNode.key); + } + + /** + * Examines the children of the branch node and returns true if a node is in + * that branch + * + * @param node + * The node to search for + * @param branch + * The branch to search in + * @return True if found, false if not found + */ + private boolean nodeIsInBranch(TreeNode node, TreeNode branch) { + if (node == branch) { + return true; + } + for (TreeNode child : branch.getChildren()) { + if (child == node) { + return true; + } + if (!child.isLeaf() && child.getState()) { + if (nodeIsInBranch(node, child)) { + return true; + } + } + } + return false; + } + + /** + * Selects a range of items which are in direct relation with each other.
+ * NOTE: The start node MUST be before the end node! + * + * @param startNode + * + * @param endNode + */ + private void doRelationSelection(TreeNode startNode, TreeNode endNode) { + TreeNode currentNode = endNode; + while (currentNode != startNode) { + currentNode.setSelected(true); + selectedIds.add(currentNode.key); + + // Traverse children above the selection + List subChildren = currentNode.getParentNode() + .getChildren(); + if (subChildren.size() > 1) { + selectNodeRange(subChildren.iterator().next().key, + currentNode.key); + } else if (subChildren.size() == 1) { + TreeNode n = subChildren.get(0); + n.setSelected(true); + selectedIds.add(n.key); + } + + currentNode = currentNode.getParentNode(); + } + startNode.setSelected(true); + selectedIds.add(startNode.key); + } + + /** + * Selects a range of items which have the same parent. + * + * @param startNode + * The start node + * @param endNode + * The end node + */ + private void doSiblingSelection(TreeNode startNode, TreeNode endNode) { + TreeNode parent = startNode.getParentNode(); + + List children = new LinkedList(); + if (parent == null) { + // Topmost parent + for (Widget w : getChildren()) { + TreeNode node = (TreeNode) w; + children.add(node); + } + } else { + children = parent.getChildren(); + } + + // Swap start and end point if needed + if (children.indexOf(startNode) > children.indexOf(endNode)) { + TreeNode temp = startNode; + startNode = endNode; + endNode = temp; + } + + Iterator childIter = children.iterator(); + boolean startFound = false; + while (childIter.hasNext()) { + TreeNode node = childIter.next(); + if (node == startNode) { + startFound = true; + } + + if (startFound && node != endNode && node.getState()) { + selectAllChildren(node, true); + } else if (startFound && node != endNode) { + node.setSelected(true); + selectedIds.add(node.key); + } + + if (node == endNode) { + node.setSelected(true); + selectedIds.add(node.key); + break; + } + } + } + + /** + * Returns the first common parent of two nodes + * + * @param node1 + * The first node + * @param node2 + * The second node + * @return The common parent or null + */ + public TreeNode getCommonGrandParent(TreeNode node1, TreeNode node2) { + // If either one does not have a grand parent then return null + if (node1.getParentNode() == null || node2.getParentNode() == null) { + return null; + } + + // If the nodes are parents of each other then return null + if (node1.isGrandParentOf(node2) || node2.isGrandParentOf(node1)) { + return null; + } + + // Get parents of node1 + List parents1 = new ArrayList(); + TreeNode parent1 = node1.getParentNode(); + while (parent1 != null) { + parents1.add(parent1); + parent1 = parent1.getParentNode(); + } + + // Get parents of node2 + List parents2 = new ArrayList(); + TreeNode parent2 = node2.getParentNode(); + while (parent2 != null) { + parents2.add(parent2); + parent2 = parent2.getParentNode(); + } + + // Search the parents for the first common parent + for (int i = 0; i < parents1.size(); i++) { + parent1 = parents1.get(i); + for (int j = 0; j < parents2.size(); j++) { + parent2 = parents2.get(j); + if (parent1 == parent2) { + return parent1; + } + } + } + + return null; + } } diff --git a/src/com/vaadin/ui/AbstractSelect.java b/src/com/vaadin/ui/AbstractSelect.java index 9667de32d6..a8451e7ef6 100644 --- a/src/com/vaadin/ui/AbstractSelect.java +++ b/src/com/vaadin/ui/AbstractSelect.java @@ -126,6 +126,21 @@ public abstract class AbstractSelect extends AbstractField implements } + /** + * Multi select modes that controls how multi select behaves. + */ + public enum MultiSelectMode { + /** + * The default behavior of the multi select mode + */ + DEFAULT, + + /** + * The previous more simple behavior of the multselect + */ + SIMPLE + } + /** * Is the select in multiselect mode? */ diff --git a/src/com/vaadin/ui/Table.java b/src/com/vaadin/ui/Table.java index 9322447bf9..4fde62d4bd 100644 --- a/src/com/vaadin/ui/Table.java +++ b/src/com/vaadin/ui/Table.java @@ -94,22 +94,6 @@ public class Table extends AbstractSelect implements Action.Container, MULTIROW } - /** - * Multi select modes that controls how multi select behaves. - */ - public enum MultiSelectMode { - /** - * Simple left clicks only selects one item, CTRL+left click selects - * multiple items and SHIFT-left click selects a range of items. - */ - DEFAULT, - /** - * Uses the old method of selection. CTRL- and SHIFT-clicks are disabled and - * clicking on the items selects/deselects them. - */ - SIMPLE - } - private static final int CELL_KEY = 0; private static final int CELL_HEADER = 1; diff --git a/src/com/vaadin/ui/Tree.java b/src/com/vaadin/ui/Tree.java index 2b38d5a6eb..11b049d803 100644 --- a/src/com/vaadin/ui/Tree.java +++ b/src/com/vaadin/ui/Tree.java @@ -119,7 +119,9 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } private TreeDragMode dragMode = TreeDragMode.NONE; - + + private MultiSelectMode multiSelectMode = MultiSelectMode.DEFAULT; + /* Tree constructors */ /** @@ -338,6 +340,29 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, requestRepaint(); } } + + /** + * Sets the behavior of the multiselect mode + * + * @param mode + * The mode to set + */ + public void setMultiselectMode(MultiSelectMode mode) { + if (multiSelectMode != mode && mode != null) { + multiSelectMode = mode; + requestRepaint(); + } + } + + /** + * Returns the mode the multiselect is in. The mode controls how + * multiselection can be done. + * + * @return The mode + */ + public MultiSelectMode getMultiselectMode() { + return multiSelectMode; + } /* Component API */ @@ -396,6 +421,15 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } } + // AbstractSelect cannot handle multiselection so we handle + // it ourself + if (variables.containsKey("selected") && isMultiSelect() + && multiSelectMode == MultiSelectMode.DEFAULT) { + handleSelectedItems(variables); + variables = new HashMap(variables); + variables.remove("selected"); + } + // Selections are handled by the select component super.changeVariables(source, variables); @@ -418,6 +452,37 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, } } + /** + * Handles the selection + * + * @param variables + * The variables sent to the server from the client + */ + private void handleSelectedItems(Map variables) { + final String[] ka = (String[]) variables.get("selected"); + + // Converts the key-array to id-set + final LinkedList s = new LinkedList(); + for (int i = 0; i < ka.length; i++) { + final Object id = itemIdMapper.get(ka[i]); + if (!isNullSelectionAllowed() + && (id == null || id == getNullSelectionItemId())) { + // skip empty selection if nullselection is not allowed + requestRepaint(); + } else if (id != null && containsId(id)) { + s.add(id); + } + } + + if (!isNullSelectionAllowed() && s.size() < 1) { + // empty selection not allowed, keep old value + requestRepaint(); + return; + } + + setValue(s, true); + } + /** * Paints any needed component-specific things to the given UIDL stream. * @@ -442,6 +507,10 @@ public class Tree extends AbstractSelect implements Container.Hierarchical, if (isSelectable()) { target.addAttribute("selectmode", (isMultiSelect() ? "multi" : "single")); + if (isMultiSelect()) { + target.addAttribute("multiselectmode", multiSelectMode + .ordinal()); + } } else { target.addAttribute("selectmode", "none"); } diff --git a/tests/src/com/vaadin/tests/components/tree/CtrlShiftMultiselect.java b/tests/src/com/vaadin/tests/components/tree/CtrlShiftMultiselect.java new file mode 100644 index 0000000000..1a329b17f5 --- /dev/null +++ b/tests/src/com/vaadin/tests/components/tree/CtrlShiftMultiselect.java @@ -0,0 +1,117 @@ +package com.vaadin.tests.components.tree; + +import java.util.Set; + +import com.vaadin.data.Item; +import com.vaadin.data.Property; +import com.vaadin.data.Property.ValueChangeEvent; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.tests.components.TestBase; +import com.vaadin.ui.Label; +import com.vaadin.ui.Tree; + +public class CtrlShiftMultiselect extends TestBase { + + private final Tree tree = new Tree(); + private final Label valueLbl = new Label("No selection"); + + @Override + protected void setup() { + + getLayout().setSpacing(true); + + tree.setContainerDataSource(createContainer()); + tree.setItemCaptionPropertyId("name"); + tree.setWidth("300px"); + tree.setImmediate(true); + tree.setSelectable(true); + tree.setMultiSelect(true); + tree.expandItemsRecursively("Item 1"); + tree.expandItemsRecursively("Item 4"); + + tree.addListener(new Property.ValueChangeListener() { + public void valueChange(ValueChangeEvent event) { + if (tree.getValue() instanceof Set) { + Set itemIds = (Set) tree.getValue(); + if (itemIds.size() == 0) { + valueLbl.setValue("No selection"); + } else { + valueLbl.setValue(itemIds); + } + } else { + valueLbl.setValue(tree.getValue()); + } + } + }); + + addComponent(tree); + + valueLbl.setWidth("300px"); + valueLbl.setHeight("600px"); + addComponent(valueLbl); + + } + + @Override + protected String getDescription() { + return "Add ctlr+shift multi selection in Tree"; + } + + @Override + protected Integer getTicketNumber() { + return 4259; + } + + private HierarchicalContainer createContainer() { + HierarchicalContainer cont = new HierarchicalContainer(); + cont.addContainerProperty("name", String.class, ""); + + for (int i = 0; i < 20; i++) { + Item item = cont.addItem("Item " + i); + item.getItemProperty("name").setValue("Item " + i); + cont.setChildrenAllowed("Item " + i, false); + + if (i == 1 || i == 4) { + cont.setChildrenAllowed("Item " + i, true); + } + + // Add three items to item 1 + if (i > 1 && i < 4) { + cont.setParent("Item " + i, "Item 1"); + } + + // Add 5 items to item 4 + if (i > 4 && i < 10) { + cont.setChildrenAllowed("Item " + i, true); + + if (i == 7) { + item = cont.addItem("Item 71"); + item.getItemProperty("name").setValue("Item 71"); + cont.setParent("Item 71", "Item " + i); + cont.setChildrenAllowed("Item 71", false); + + item = cont.addItem("Item 72"); + item.getItemProperty("name").setValue("Item 72"); + cont.setParent("Item 72", "Item " + i); + cont.setChildrenAllowed("Item 72", true); + + item = cont.addItem("Item 73"); + item.getItemProperty("name").setValue("Item 73"); + cont.setParent("Item 73", "Item 72"); + cont.setChildrenAllowed("Item 73", true); + + item = cont.addItem("Item 74"); + item.getItemProperty("name").setValue("Item 74"); + cont.setParent("Item 74", "Item " + i); + cont.setChildrenAllowed("Item 74", true); + } + + cont.setParent("Item " + i, "Item " + (i - 1)); + + } + } + + return cont; + } + +} diff --git a/tests/src/com/vaadin/tests/server/component/table/TestMultipleSelection.java b/tests/src/com/vaadin/tests/server/component/table/TestMultipleSelection.java index 42d60bdfef..c87ebe9a91 100644 --- a/tests/src/com/vaadin/tests/server/component/table/TestMultipleSelection.java +++ b/tests/src/com/vaadin/tests/server/component/table/TestMultipleSelection.java @@ -8,7 +8,7 @@ import junit.framework.TestCase; import com.vaadin.data.Container; import com.vaadin.data.util.IndexedContainer; import com.vaadin.ui.Table; -import com.vaadin.ui.Table.MultiSelectMode; +import com.vaadin.ui.AbstractSelect.MultiSelectMode; public class TestMultipleSelection extends TestCase { -- 2.39.5