aboutsummaryrefslogtreecommitdiffstats
path: root/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java')
-rw-r--r--src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java2102
1 files changed, 2102 insertions, 0 deletions
diff --git a/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java b/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java
new file mode 100644
index 0000000000..6f19cba957
--- /dev/null
+++ b/src/com/vaadin/terminal/gwt/client/ui/tree/VTree.java
@@ -0,0 +1,2102 @@
+/*
+@VaadinApache2LicenseForJavaFiles@
+ */
+
+package com.vaadin.terminal.gwt.client.ui.tree;
+
+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.core.client.JavaScriptObject;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Node;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
+import com.google.gwt.event.dom.client.ContextMenuEvent;
+import com.google.gwt.event.dom.client.ContextMenuHandler;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.Widget;
+import com.vaadin.terminal.gwt.client.ApplicationConnection;
+import com.vaadin.terminal.gwt.client.BrowserInfo;
+import com.vaadin.terminal.gwt.client.ComponentConnector;
+import com.vaadin.terminal.gwt.client.ConnectorMap;
+import com.vaadin.terminal.gwt.client.MouseEventDetails;
+import com.vaadin.terminal.gwt.client.MouseEventDetailsBuilder;
+import com.vaadin.terminal.gwt.client.UIDL;
+import com.vaadin.terminal.gwt.client.Util;
+import com.vaadin.terminal.gwt.client.VTooltip;
+import com.vaadin.terminal.gwt.client.ui.Action;
+import com.vaadin.terminal.gwt.client.ui.ActionOwner;
+import com.vaadin.terminal.gwt.client.ui.FocusElementPanel;
+import com.vaadin.terminal.gwt.client.ui.Icon;
+import com.vaadin.terminal.gwt.client.ui.SubPartAware;
+import com.vaadin.terminal.gwt.client.ui.TreeAction;
+import com.vaadin.terminal.gwt.client.ui.VLazyExecutor;
+import com.vaadin.terminal.gwt.client.ui.dd.DDUtil;
+import com.vaadin.terminal.gwt.client.ui.dd.VAbstractDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VAcceptCallback;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragAndDropManager;
+import com.vaadin.terminal.gwt.client.ui.dd.VDragEvent;
+import com.vaadin.terminal.gwt.client.ui.dd.VDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VHasDropHandler;
+import com.vaadin.terminal.gwt.client.ui.dd.VTransferable;
+import com.vaadin.terminal.gwt.client.ui.dd.VerticalDropLocation;
+
+/**
+ *
+ */
+public class VTree extends FocusElementPanel implements VHasDropHandler,
+ FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler,
+ SubPartAware, ActionOwner {
+
+ public static final String CLASSNAME = "v-tree";
+
+ public static final String ITEM_CLICK_EVENT_ID = "itemClick";
+
+ /**
+ * Click selects the current node, ctrl/shift toggles multi selection
+ */
+ public static final int MULTISELECT_MODE_DEFAULT = 0;
+
+ /**
+ * Click/touch on node toggles its selected status
+ */
+ public static final int MULTISELECT_MODE_SIMPLE = 1;
+
+ private static final int CHARCODE_SPACE = 32;
+
+ final FlowPanel body = new FlowPanel();
+
+ Set<String> selectedIds = new HashSet<String>();
+ ApplicationConnection client;
+ String paintableId;
+ boolean selectable;
+ boolean isMultiselect;
+ private String currentMouseOverKey;
+ TreeNode lastSelection;
+ TreeNode focusedNode;
+ int multiSelectMode = MULTISELECT_MODE_DEFAULT;
+
+ private final HashMap<String, TreeNode> keyToNode = new HashMap<String, TreeNode>();
+
+ /**
+ * This map contains captions and icon urls for actions like: * "33_c" ->
+ * "Edit" * "33_i" -> "http://dom.com/edit.png"
+ */
+ private final HashMap<String, String> actionMap = new HashMap<String, String>();
+
+ boolean immediate;
+
+ boolean isNullSelectionAllowed = true;
+
+ boolean disabled = false;
+
+ boolean readonly;
+
+ boolean rendering;
+
+ private VAbstractDropHandler dropHandler;
+
+ int dragMode;
+
+ private boolean selectionHasChanged = false;
+
+ String[] bodyActionKeys;
+
+ public VLazyExecutor iconLoaded = new VLazyExecutor(50,
+ new ScheduledCommand() {
+
+ public void execute() {
+ Util.notifyParentOfSizeChange(VTree.this, true);
+ }
+
+ });
+
+ public VTree() {
+ super();
+ setStyleName(CLASSNAME);
+ add(body);
+
+ addFocusHandler(this);
+ addBlurHandler(this);
+
+ /*
+ * Listen to context menu events on the empty space in the tree
+ */
+ sinkEvents(Event.ONCONTEXTMENU);
+ addDomHandler(new ContextMenuHandler() {
+ public void onContextMenu(ContextMenuEvent event) {
+ handleBodyContextMenu(event);
+ }
+ }, ContextMenuEvent.getType());
+
+ /*
+ * Firefox auto-repeat works correctly only if we use a key press
+ * handler, other browsers handle it correctly when using a key down
+ * handler
+ */
+ if (BrowserInfo.get().isGecko() || BrowserInfo.get().isOpera()) {
+ addKeyPressHandler(this);
+ } else {
+ addKeyDownHandler(this);
+ }
+
+ /*
+ * We need to use the sinkEvents method to catch the keyUp events so we
+ * can cache a single shift. KeyUpHandler cannot do this. At the same
+ * time we catch the mouse down and up events so we can apply the text
+ * selection patch in IE
+ */
+ sinkEvents(Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONKEYUP);
+
+ /*
+ * Re-set the tab index to make sure that the FocusElementPanel's
+ * (super) focus element gets the tab index and not the element
+ * containing the tree.
+ */
+ setTabIndex(0);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt.user
+ * .client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ if (event.getTypeInt() == Event.ONMOUSEDOWN) {
+ // Prevent default text selection in IE
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast()).setPropertyJSO(
+ "onselectstart", applyDisableTextSelectionIEHack());
+ }
+ } else if (event.getTypeInt() == Event.ONMOUSEUP) {
+ // Remove IE text selection hack
+ if (BrowserInfo.get().isIE()) {
+ ((Element) event.getEventTarget().cast()).setPropertyJSO(
+ "onselectstart", null);
+ }
+ } else if (event.getTypeInt() == Event.ONKEYUP) {
+ if (selectionHasChanged) {
+ if (event.getKeyCode() == getNavigationDownKey()
+ && !event.getShiftKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == getNavigationUpKey()
+ && !event.getShiftKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == KeyCodes.KEY_SHIFT) {
+ sendSelectionToServer();
+ event.preventDefault();
+ } else if (event.getKeyCode() == getNavigationSelectKey()) {
+ sendSelectionToServer();
+ event.preventDefault();
+ }
+ }
+ }
+ }
+
+ public String getActionCaption(String actionKey) {
+ return actionMap.get(actionKey + "_c");
+ }
+
+ public String getActionIcon(String actionKey) {
+ return actionMap.get(actionKey + "_i");
+ }
+
+ /**
+ * Returns the first root node of the tree or null if there are no root
+ * nodes.
+ *
+ * @return The first root {@link TreeNode}
+ */
+ protected TreeNode getFirstRootNode() {
+ if (body.getWidgetCount() == 0) {
+ return null;
+ }
+ return (TreeNode) body.getWidget(0);
+ }
+
+ /**
+ * Returns the last root node of the tree or null if there are no root
+ * nodes.
+ *
+ * @return The last root {@link TreeNode}
+ */
+ protected TreeNode getLastRootNode() {
+ if (body.getWidgetCount() == 0) {
+ return null;
+ }
+ return (TreeNode) body.getWidget(body.getWidgetCount() - 1);
+ }
+
+ /**
+ * Returns a list of all root nodes in the Tree in the order they appear in
+ * the tree.
+ *
+ * @return A list of all root {@link TreeNode}s.
+ */
+ protected List<TreeNode> getRootNodes() {
+ ArrayList<TreeNode> rootNodes = new ArrayList<TreeNode>();
+ for (int i = 0; i < body.getWidgetCount(); i++) {
+ rootNodes.add((TreeNode) body.getWidget(i));
+ }
+ return rootNodes;
+ }
+
+ private void updateTreeRelatedDragData(VDragEvent drag) {
+
+ currentMouseOverKey = findCurrentMouseOverKey(drag.getElementOver());
+
+ drag.getDropDetails().put("itemIdOver", currentMouseOverKey);
+ if (currentMouseOverKey != null) {
+ TreeNode treeNode = getNodeByKey(currentMouseOverKey);
+ VerticalDropLocation detail = treeNode.getDropDetail(drag
+ .getCurrentGwtEvent());
+ Boolean overTreeNode = null;
+ if (treeNode != null && !treeNode.isLeaf()
+ && detail == VerticalDropLocation.MIDDLE) {
+ overTreeNode = true;
+ }
+ drag.getDropDetails().put("itemIdOverIsNode", overTreeNode);
+ drag.getDropDetails().put("detail", detail);
+ } else {
+ drag.getDropDetails().put("itemIdOverIsNode", null);
+ drag.getDropDetails().put("detail", null);
+ }
+
+ }
+
+ private String findCurrentMouseOverKey(Element elementOver) {
+ TreeNode treeNode = Util.findWidget(elementOver, TreeNode.class);
+ return treeNode == null ? null : treeNode.key;
+ }
+
+ void updateDropHandler(UIDL childUidl) {
+ if (dropHandler == null) {
+ dropHandler = new VAbstractDropHandler() {
+
+ @Override
+ public void dragEnter(VDragEvent drag) {
+ }
+
+ @Override
+ protected void dragAccepted(final VDragEvent drag) {
+
+ }
+
+ @Override
+ public void dragOver(final VDragEvent currentDrag) {
+ final Object oldIdOver = currentDrag.getDropDetails().get(
+ "itemIdOver");
+ final VerticalDropLocation oldDetail = (VerticalDropLocation) currentDrag
+ .getDropDetails().get("detail");
+
+ updateTreeRelatedDragData(currentDrag);
+ final VerticalDropLocation detail = (VerticalDropLocation) currentDrag
+ .getDropDetails().get("detail");
+ boolean nodeHasChanged = (currentMouseOverKey != null && currentMouseOverKey != oldIdOver)
+ || (currentMouseOverKey == null && oldIdOver != null);
+ boolean detailHasChanded = (detail != null && detail != oldDetail)
+ || (detail == null && oldDetail != null);
+
+ if (nodeHasChanged || detailHasChanded) {
+ final String newKey = currentMouseOverKey;
+ TreeNode treeNode = keyToNode.get(oldIdOver);
+ if (treeNode != null) {
+ // clear old styles
+ treeNode.emphasis(null);
+ }
+ if (newKey != null) {
+ validate(new VAcceptCallback() {
+ public void accepted(VDragEvent event) {
+ VerticalDropLocation curDetail = (VerticalDropLocation) event
+ .getDropDetails().get("detail");
+ if (curDetail == detail
+ && newKey.equals(currentMouseOverKey)) {
+ getNodeByKey(newKey).emphasis(detail);
+ }
+ /*
+ * Else drag is already on a different
+ * node-detail pair, new criteria check is
+ * going on
+ */
+ }
+ }, currentDrag);
+
+ }
+ }
+
+ }
+
+ @Override
+ public void dragLeave(VDragEvent drag) {
+ cleanUp();
+ }
+
+ private void cleanUp() {
+ if (currentMouseOverKey != null) {
+ getNodeByKey(currentMouseOverKey).emphasis(null);
+ currentMouseOverKey = null;
+ }
+ }
+
+ @Override
+ public boolean drop(VDragEvent drag) {
+ cleanUp();
+ return super.drop(drag);
+ }
+
+ @Override
+ public ComponentConnector getConnector() {
+ return ConnectorMap.get(client).getConnector(VTree.this);
+ }
+
+ public ApplicationConnection getApplicationConnection() {
+ return client;
+ }
+
+ };
+ }
+ dropHandler.updateAcceptRules(childUidl);
+ }
+
+ public void setSelected(TreeNode treeNode, boolean selected) {
+ if (selected) {
+ if (!isMultiselect) {
+ while (selectedIds.size() > 0) {
+ final String id = selectedIds.iterator().next();
+ final TreeNode oldSelection = getNodeByKey(id);
+ if (oldSelection != null) {
+ // can be null if the node is not visible (parent
+ // collapsed)
+ oldSelection.setSelected(false);
+ }
+ selectedIds.remove(id);
+ }
+ }
+ treeNode.setSelected(true);
+ selectedIds.add(treeNode.key);
+ } else {
+ if (!isNullSelectionAllowed) {
+ if (!isMultiselect || selectedIds.size() == 1) {
+ return;
+ }
+ }
+ selectedIds.remove(treeNode.key);
+ treeNode.setSelected(false);
+ }
+
+ sendSelectionToServer();
+ }
+
+ /**
+ * Sends the selection to the server
+ */
+ private void sendSelectionToServer() {
+ Command command = new Command() {
+ public void execute() {
+ client.updateVariable(paintableId, "selected",
+ selectedIds.toArray(new String[selectedIds.size()]),
+ immediate);
+ selectionHasChanged = false;
+ }
+ };
+
+ /*
+ * Delaying the sending of the selection in webkit to ensure the
+ * selection is always sent when the tree has focus and after click
+ * events have been processed. This is due to the focusing
+ * implementation in FocusImplSafari which uses timeouts when focusing
+ * and blurring.
+ */
+ if (BrowserInfo.get().isWebkit()) {
+ Scheduler.get().scheduleDeferred(command);
+ } else {
+ command.execute();
+ }
+ }
+
+ /**
+ * Is a node selected in the tree
+ *
+ * @param treeNode
+ * The node to check
+ * @return
+ */
+ public boolean isSelected(TreeNode treeNode) {
+ return selectedIds.contains(treeNode.key);
+ }
+
+ public class TreeNode extends SimplePanel implements ActionOwner {
+
+ public static final String CLASSNAME = "v-tree-node";
+ public static final String CLASSNAME_FOCUSED = CLASSNAME + "-focused";
+
+ public String key;
+
+ String[] actionKeys = null;
+
+ boolean childrenLoaded;
+
+ Element nodeCaptionDiv;
+
+ protected Element nodeCaptionSpan;
+
+ FlowPanel childNodeContainer;
+
+ private boolean open;
+
+ private Icon icon;
+
+ private Event mouseDownEvent;
+
+ private int cachedHeight = -1;
+
+ private boolean focused = false;
+
+ public TreeNode() {
+ constructDom();
+ sinkEvents(Event.ONCLICK | Event.ONDBLCLICK | Event.MOUSEEVENTS
+ | Event.TOUCHEVENTS | Event.ONCONTEXTMENU);
+ }
+
+ public VerticalDropLocation getDropDetail(NativeEvent currentGwtEvent) {
+ if (cachedHeight < 0) {
+ /*
+ * Height is cached to avoid flickering (drop hints may change
+ * the reported offsetheight -> would change the drop detail)
+ */
+ cachedHeight = nodeCaptionDiv.getOffsetHeight();
+ }
+ VerticalDropLocation verticalDropLocation = DDUtil
+ .getVerticalDropLocation(nodeCaptionDiv, cachedHeight,
+ currentGwtEvent, 0.15);
+ return verticalDropLocation;
+ }
+
+ protected void emphasis(VerticalDropLocation detail) {
+ String base = "v-tree-node-drag-";
+ UIObject.setStyleName(getElement(), base + "top",
+ VerticalDropLocation.TOP == detail);
+ UIObject.setStyleName(getElement(), base + "bottom",
+ VerticalDropLocation.BOTTOM == detail);
+ UIObject.setStyleName(getElement(), base + "center",
+ VerticalDropLocation.MIDDLE == detail);
+ base = "v-tree-node-caption-drag-";
+ UIObject.setStyleName(nodeCaptionDiv, base + "top",
+ VerticalDropLocation.TOP == detail);
+ UIObject.setStyleName(nodeCaptionDiv, base + "bottom",
+ VerticalDropLocation.BOTTOM == detail);
+ UIObject.setStyleName(nodeCaptionDiv, base + "center",
+ VerticalDropLocation.MIDDLE == detail);
+
+ // also add classname to "folder node" into which the drag is
+ // targeted
+
+ TreeNode folder = null;
+ /* Possible parent of this TreeNode will be stored here */
+ TreeNode parentFolder = getParentNode();
+
+ // TODO fix my bugs
+ if (isLeaf()) {
+ folder = parentFolder;
+ // note, parent folder may be null if this is root node => no
+ // folder target exists
+ } else {
+ if (detail == VerticalDropLocation.TOP) {
+ folder = parentFolder;
+ } else {
+ folder = this;
+ }
+ // ensure we remove the dragfolder classname from the previous
+ // folder node
+ setDragFolderStyleName(this, false);
+ setDragFolderStyleName(parentFolder, false);
+ }
+ if (folder != null) {
+ setDragFolderStyleName(folder, detail != null);
+ }
+
+ }
+
+ private TreeNode getParentNode() {
+ Widget parent2 = getParent().getParent();
+ if (parent2 instanceof TreeNode) {
+ return (TreeNode) parent2;
+ }
+ return null;
+ }
+
+ private void setDragFolderStyleName(TreeNode folder, boolean add) {
+ if (folder != null) {
+ UIObject.setStyleName(folder.getElement(),
+ "v-tree-node-dragfolder", add);
+ UIObject.setStyleName(folder.nodeCaptionDiv,
+ "v-tree-node-caption-dragfolder", add);
+ }
+ }
+
+ /**
+ * 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(final boolean ctrl,
+ final boolean shift) {
+
+ // always when clicking an item, focus it
+ setFocusedNode(this, false);
+
+ if (!BrowserInfo.get().isOpera()) {
+ /*
+ * Ensure that the tree's focus element also gains focus
+ * (TreeNodes focus is faked using FocusElementPanel in browsers
+ * other than Opera).
+ */
+ focus();
+ }
+
+ ScheduledCommand command = new ScheduledCommand() {
+ public void execute() {
+
+ if (multiSelectMode == MULTISELECT_MODE_SIMPLE
+ || !isMultiselect) {
+ toggleSelection();
+ lastSelection = TreeNode.this;
+ } else if (multiSelectMode == MULTISELECT_MODE_DEFAULT) {
+ // Handle ctrl+click
+ if (isMultiselect && ctrl && !shift) {
+ toggleSelection();
+ lastSelection = TreeNode.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 {
+ // TODO should happen only if this alone not yet
+ // selected,
+ // now sending excess server calls
+ deselectAll();
+ toggleSelection();
+ lastSelection = TreeNode.this;
+ }
+ }
+ }
+ };
+
+ if (BrowserInfo.get().isWebkit() && !treeHasFocus) {
+ /*
+ * Safari may need to wait for focus. See FocusImplSafari.
+ */
+ // VConsole.log("Deferring click handling to let webkit gain focus...");
+ Scheduler.get().scheduleDeferred(command);
+ } else {
+ command.execute();
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.user.client.ui.Widget#onBrowserEvent(com.google.gwt
+ * .user.client.Event)
+ */
+ @Override
+ public void onBrowserEvent(Event event) {
+ super.onBrowserEvent(event);
+ final int type = DOM.eventGetType(event);
+ final Element target = DOM.eventGetTarget(event);
+
+ if (type == Event.ONLOAD && target == icon.getElement()) {
+ iconLoaded.trigger();
+ }
+
+ if (disabled) {
+ return;
+ }
+
+ if (target == nodeCaptionSpan) {
+ client.handleTooltipEvent(event, VTree.this, key);
+ }
+
+ final boolean inCaption = target == nodeCaptionSpan
+ || (icon != null && target == icon.getElement());
+ if (inCaption
+ && client
+ .hasEventListeners(VTree.this, ITEM_CLICK_EVENT_ID)
+
+ && (type == Event.ONDBLCLICK || type == Event.ONMOUSEUP)) {
+ fireClick(event);
+ }
+ if (type == Event.ONCLICK) {
+ if (getElement() == target) {
+ // state change
+ toggleState();
+ } else if (!readonly && inCaption) {
+ if (selectable) {
+ // caption click = selection change && possible click
+ // event
+ if (handleClickSelection(
+ event.getCtrlKey() || event.getMetaKey(),
+ event.getShiftKey())) {
+ event.preventDefault();
+ }
+ } else {
+ // Not selectable, only focus the node.
+ setFocusedNode(this);
+ }
+ }
+ event.stopPropagation();
+ } else if (type == Event.ONCONTEXTMENU) {
+ showContextMenu(event);
+ }
+
+ if (dragMode != 0 || dropHandler != null) {
+ if (type == Event.ONMOUSEDOWN || type == Event.ONTOUCHSTART) {
+ if (nodeCaptionDiv.isOrHasChild((Node) event
+ .getEventTarget().cast())) {
+ if (dragMode > 0
+ && (type == Event.ONTOUCHSTART || event
+ .getButton() == NativeEvent.BUTTON_LEFT)) {
+ mouseDownEvent = event; // save event for possible
+ // dd operation
+ if (type == Event.ONMOUSEDOWN) {
+ event.preventDefault(); // prevent text
+ // selection
+ } else {
+ /*
+ * FIXME We prevent touch start event to be used
+ * as a scroll start event. Note that we cannot
+ * easily distinguish whether the user wants to
+ * drag or scroll. The same issue is in table
+ * that has scrollable area and has drag and
+ * drop enable. Some kind of timer might be used
+ * to resolve the issue.
+ */
+ event.stopPropagation();
+ }
+ }
+ }
+ } else if (type == Event.ONMOUSEMOVE
+ || type == Event.ONMOUSEOUT
+ || type == Event.ONTOUCHMOVE) {
+
+ if (mouseDownEvent != null) {
+ // start actual drag on slight move when mouse is down
+ VTransferable t = new VTransferable();
+ t.setDragSource(ConnectorMap.get(client).getConnector(
+ VTree.this));
+ t.setData("itemId", key);
+ VDragEvent drag = VDragAndDropManager.get().startDrag(
+ t, mouseDownEvent, true);
+
+ drag.createDragImage(nodeCaptionDiv, true);
+ event.stopPropagation();
+
+ mouseDownEvent = null;
+ }
+ } else if (type == Event.ONMOUSEUP) {
+ mouseDownEvent = null;
+ }
+ if (type == Event.ONMOUSEOVER) {
+ mouseDownEvent = null;
+ currentMouseOverKey = key;
+ event.stopPropagation();
+ }
+
+ } else if (type == Event.ONMOUSEDOWN
+ && event.getButton() == NativeEvent.BUTTON_LEFT) {
+ event.preventDefault(); // text selection
+ }
+ }
+
+ private void fireClick(final Event evt) {
+ /*
+ * Ensure we have focus in tree before sending variables. Otherwise
+ * previously modified field may contain dirty variables.
+ */
+ if (!treeHasFocus) {
+ if (BrowserInfo.get().isOpera()) {
+ if (focusedNode == null) {
+ getNodeByKey(key).setFocused(true);
+ } else {
+ focusedNode.setFocused(true);
+ }
+ } else {
+ focus();
+ }
+ }
+ final MouseEventDetails details = MouseEventDetailsBuilder
+ .buildMouseEventDetails(evt);
+ ScheduledCommand command = new ScheduledCommand() {
+ public void execute() {
+ // Determine if we should send the event immediately to the
+ // server. We do not want to send the event if there is a
+ // selection event happening after this. In all other cases
+ // we want to send it immediately.
+ boolean sendClickEventNow = true;
+
+ if (details.getButton() == NativeEvent.BUTTON_LEFT
+ && immediate && selectable) {
+ // Probably a selection that will cause a value change
+ // event to be sent
+ sendClickEventNow = false;
+
+ // The exception is that user clicked on the
+ // currently selected row and null selection is not
+ // allowed == no selection event
+ if (isSelected() && selectedIds.size() == 1
+ && !isNullSelectionAllowed) {
+ sendClickEventNow = true;
+ }
+ }
+
+ client.updateVariable(paintableId, "clickedKey", key, false);
+ client.updateVariable(paintableId, "clickEvent",
+ details.toString(), sendClickEventNow);
+ }
+ };
+ if (treeHasFocus) {
+ command.execute();
+ } else {
+ /*
+ * Webkits need a deferring due to FocusImplSafari uses timeout
+ */
+ Scheduler.get().scheduleDeferred(command);
+ }
+ }
+
+ private void toggleSelection() {
+ if (selectable) {
+ VTree.this.setSelected(this, !isSelected());
+ }
+ }
+
+ private void toggleState() {
+ setState(!getState(), true);
+ }
+
+ protected void constructDom() {
+ addStyleName(CLASSNAME);
+
+ nodeCaptionDiv = DOM.createDiv();
+ DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME
+ + "-caption");
+ Element wrapper = DOM.createDiv();
+ nodeCaptionSpan = DOM.createSpan();
+ DOM.sinkEvents(nodeCaptionSpan, VTooltip.TOOLTIP_EVENTS);
+ DOM.appendChild(getElement(), nodeCaptionDiv);
+ DOM.appendChild(nodeCaptionDiv, wrapper);
+ DOM.appendChild(wrapper, nodeCaptionSpan);
+
+ if (BrowserInfo.get().isOpera()) {
+ /*
+ * Focus the caption div of the node to get keyboard navigation
+ * to work without scrolling up or down when focusing a node.
+ */
+ nodeCaptionDiv.setTabIndex(-1);
+ }
+
+ childNodeContainer = new FlowPanel();
+ childNodeContainer.setStyleName(CLASSNAME + "-children");
+ setWidget(childNodeContainer);
+ }
+
+ public boolean isLeaf() {
+ String[] styleNames = getStyleName().split(" ");
+ for (String styleName : styleNames) {
+ if (styleName.equals(CLASSNAME + "-leaf")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void setState(boolean state, boolean notifyServer) {
+ if (open == state) {
+ return;
+ }
+ if (state) {
+ if (!childrenLoaded && notifyServer) {
+ client.updateVariable(paintableId, "requestChildTree",
+ true, false);
+ }
+ if (notifyServer) {
+ client.updateVariable(paintableId, "expand",
+ new String[] { key }, true);
+ }
+ addStyleName(CLASSNAME + "-expanded");
+ childNodeContainer.setVisible(true);
+
+ } else {
+ removeStyleName(CLASSNAME + "-expanded");
+ childNodeContainer.setVisible(false);
+ if (notifyServer) {
+ client.updateVariable(paintableId, "collapse",
+ new String[] { key }, true);
+ }
+ }
+ open = state;
+
+ if (!rendering) {
+ Util.notifyParentOfSizeChange(VTree.this, false);
+ }
+ }
+
+ boolean getState() {
+ return open;
+ }
+
+ void setText(String text) {
+ DOM.setInnerText(nodeCaptionSpan, text);
+ }
+
+ public boolean isChildrenLoaded() {
+ return childrenLoaded;
+ }
+
+ /**
+ * Returns the children of the node
+ *
+ * @return A set of tree nodes
+ */
+ public List<TreeNode> getChildren() {
+ List<TreeNode> nodes = new LinkedList<TreeNode>();
+
+ if (!isLeaf() && isChildrenLoaded()) {
+ Iterator<Widget> 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[] {};
+ }
+ final Action[] actions = new Action[actionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = actionKeys[i];
+ final TreeAction a = new TreeAction(this, String.valueOf(key),
+ actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ /**
+ * Adds/removes Vaadin specific style name. This method ought to be
+ * called only from VTree.
+ *
+ * @param selected
+ */
+ protected void setSelected(boolean selected) {
+ // add style name to caption dom structure only, not to subtree
+ setStyleName(nodeCaptionDiv, "v-tree-node-selected", selected);
+ }
+
+ protected boolean isSelected() {
+ 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) {
+ int left = event.getClientX();
+ int top = event.getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.Widget#onDetach()
+ */
+ @Override
+ protected void onDetach() {
+ super.onDetach();
+ client.getContextMenu().ensureHidden(this);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see com.google.gwt.user.client.ui.UIObject#toString()
+ */
+ @Override
+ public String toString() {
+ return nodeCaptionSpan.getInnerText();
+ }
+
+ /**
+ * Is the node focused?
+ *
+ * @param focused
+ * True if focused, false if not
+ */
+ public void setFocused(boolean focused) {
+ if (!this.focused && focused) {
+ nodeCaptionDiv.addClassName(CLASSNAME_FOCUSED);
+
+ this.focused = focused;
+ if (BrowserInfo.get().isOpera()) {
+ nodeCaptionDiv.focus();
+ }
+ treeHasFocus = true;
+ } else if (this.focused && !focused) {
+ nodeCaptionDiv.removeClassName(CLASSNAME_FOCUSED);
+ this.focused = focused;
+ treeHasFocus = false;
+ }
+ }
+
+ /**
+ * Scrolls the caption into view
+ */
+ public void scrollIntoView() {
+ Util.scrollIntoViewVertically(nodeCaptionDiv);
+ }
+
+ public void setIcon(String iconUrl) {
+ if (iconUrl != null) {
+ // Add icon if not present
+ if (icon == null) {
+ icon = new Icon(client);
+ DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv),
+ icon.getElement(), nodeCaptionSpan);
+ }
+ icon.setUri(iconUrl);
+ } else {
+ // Remove icon if present
+ if (icon != null) {
+ DOM.removeChild(DOM.getFirstChild(nodeCaptionDiv),
+ icon.getElement());
+ icon = null;
+ }
+ }
+ }
+
+ public void setNodeStyleName(String styleName) {
+ addStyleName(TreeNode.CLASSNAME + "-" + styleName);
+ setStyleName(nodeCaptionDiv, TreeNode.CLASSNAME + "-caption-"
+ + styleName, true);
+ childNodeContainer.addStyleName(TreeNode.CLASSNAME + "-children-"
+ + styleName);
+
+ }
+
+ }
+
+ public VDropHandler getDropHandler() {
+ return dropHandler;
+ }
+
+ 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();
+ selectionHasChanged = true;
+ }
+
+ /**
+ * 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 a node and deselect all other nodes
+ *
+ * @param node
+ * The node to select
+ */
+ private void selectNode(TreeNode node, boolean deselectPrevious) {
+ if (deselectPrevious) {
+ deselectAll();
+ }
+
+ if (node != null) {
+ node.setSelected(true);
+ selectedIds.add(node.key);
+ lastSelection = node;
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * Deselects a node
+ *
+ * @param node
+ * The node to deselect
+ */
+ private void deselectNode(TreeNode node) {
+ node.setSelected(false);
+ selectedIds.remove(node.key);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * 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);
+ }
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * 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 {
+ child.setSelected(true);
+ selectedIds.add(child.key);
+ }
+ }
+ }
+ selectionHasChanged = true;
+
+ 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<TreeNode> children;
+ if (commonParent != null) {
+ children = commonParent.getChildren();
+ } else {
+ children = getRootNodes();
+ }
+
+ // 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<TreeNode> 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);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * 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.<br/>
+ * NOTE: The start node <b>MUST</b> 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<TreeNode> 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);
+ selectionHasChanged = true;
+ }
+
+ /**
+ * 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<TreeNode> children;
+ if (parent == null) {
+ // Topmost parent
+ children = getRootNodes();
+ } 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<TreeNode> 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;
+ }
+ }
+ selectionHasChanged = true;
+ }
+
+ /**
+ * 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<TreeNode> parents1 = new ArrayList<TreeNode>();
+ TreeNode parent1 = node1.getParentNode();
+ while (parent1 != null) {
+ parents1.add(parent1);
+ parent1 = parent1.getParentNode();
+ }
+
+ // Get parents of node2
+ List<TreeNode> parents2 = new ArrayList<TreeNode>();
+ 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;
+ }
+
+ /**
+ * Sets the node currently in focus
+ *
+ * @param node
+ * The node to focus or null to remove the focus completely
+ * @param scrollIntoView
+ * Scroll the node into view
+ */
+ public void setFocusedNode(TreeNode node, boolean scrollIntoView) {
+ // Unfocus previously focused node
+ if (focusedNode != null) {
+ focusedNode.setFocused(false);
+ }
+
+ if (node != null) {
+ node.setFocused(true);
+ }
+
+ focusedNode = node;
+
+ if (node != null && scrollIntoView) {
+ /*
+ * Delay scrolling the focused node into view if we are still
+ * rendering. #5396
+ */
+ if (!rendering) {
+ node.scrollIntoView();
+ } else {
+ Scheduler.get().scheduleDeferred(new Command() {
+ public void execute() {
+ focusedNode.scrollIntoView();
+ }
+ });
+ }
+ }
+ }
+
+ /**
+ * Focuses a node and scrolls it into view
+ *
+ * @param node
+ * The node to focus
+ */
+ public void setFocusedNode(TreeNode node) {
+ setFocusedNode(node, true);
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.FocusHandler#onFocus(com.google.gwt.event
+ * .dom.client.FocusEvent)
+ */
+ public void onFocus(FocusEvent event) {
+ treeHasFocus = true;
+ // If no node has focus, focus the first item in the tree
+ if (focusedNode == null && lastSelection == null && selectable) {
+ setFocusedNode(getFirstRootNode(), false);
+ } else if (focusedNode != null && selectable) {
+ setFocusedNode(focusedNode, false);
+ } else if (lastSelection != null && selectable) {
+ setFocusedNode(lastSelection, false);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.BlurHandler#onBlur(com.google.gwt.event
+ * .dom.client.BlurEvent)
+ */
+ public void onBlur(BlurEvent event) {
+ treeHasFocus = false;
+ if (focusedNode != null) {
+ focusedNode.setFocused(false);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyPressHandler#onKeyPress(com.google
+ * .gwt.event.dom.client.KeyPressEvent)
+ */
+ public void onKeyPress(KeyPressEvent event) {
+ NativeEvent nativeEvent = event.getNativeEvent();
+ int keyCode = nativeEvent.getKeyCode();
+ if (keyCode == 0 && nativeEvent.getCharCode() == ' ') {
+ // Provide a keyCode for space to be compatible with FireFox
+ // keypress event
+ keyCode = CHARCODE_SPACE;
+ }
+ if (handleKeyNavigation(keyCode,
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.google.gwt.event.dom.client.KeyDownHandler#onKeyDown(com.google.gwt
+ * .event.dom.client.KeyDownEvent)
+ */
+ public void onKeyDown(KeyDownEvent event) {
+ if (handleKeyNavigation(event.getNativeEvent().getKeyCode(),
+ event.isControlKeyDown() || event.isMetaKeyDown(),
+ event.isShiftKeyDown())) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ /**
+ * Handles the keyboard navigation
+ *
+ * @param keycode
+ * The keycode of the pressed key
+ * @param ctrl
+ * Was ctrl pressed
+ * @param shift
+ * Was shift pressed
+ * @return Returns true if the key was handled, else false
+ */
+ protected boolean handleKeyNavigation(int keycode, boolean ctrl,
+ boolean shift) {
+ // Navigate down
+ if (keycode == getNavigationDownKey()) {
+ TreeNode node = null;
+ // If node is open and has children then move in to the children
+ if (!focusedNode.isLeaf() && focusedNode.getState()
+ && focusedNode.getChildren().size() > 0) {
+ node = focusedNode.getChildren().get(0);
+ }
+
+ // Else move down to the next sibling
+ else {
+ node = getNextSibling(focusedNode);
+ if (node == null) {
+ // Else jump to the parent and try to select the next
+ // sibling there
+ TreeNode current = focusedNode;
+ while (node == null && current.getParentNode() != null) {
+ node = getNextSibling(current.getParentNode());
+ current = current.getParentNode();
+ }
+ }
+ }
+
+ if (node != null) {
+ setFocusedNode(node);
+ if (selectable) {
+ if (!ctrl && !shift) {
+ selectNode(node, true);
+ } else if (shift && isMultiselect) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, node.key);
+ } else if (shift) {
+ selectNode(node, true);
+ }
+ }
+ }
+ return true;
+ }
+
+ // Navigate up
+ if (keycode == getNavigationUpKey()) {
+ TreeNode prev = getPreviousSibling(focusedNode);
+ TreeNode node = null;
+ if (prev != null) {
+ node = getLastVisibleChildInTree(prev);
+ } else if (focusedNode.getParentNode() != null) {
+ node = focusedNode.getParentNode();
+ }
+ if (node != null) {
+ setFocusedNode(node);
+ if (selectable) {
+ if (!ctrl && !shift) {
+ selectNode(node, true);
+ } else if (shift && isMultiselect) {
+ deselectAll();
+ selectNodeRange(lastSelection.key, node.key);
+ } else if (shift) {
+ selectNode(node, true);
+ }
+ }
+ }
+ return true;
+ }
+
+ // Navigate left (close branch)
+ if (keycode == getNavigationLeftKey()) {
+ if (!focusedNode.isLeaf() && focusedNode.getState()) {
+ focusedNode.setState(false, true);
+ } else if (focusedNode.getParentNode() != null
+ && (focusedNode.isLeaf() || !focusedNode.getState())) {
+
+ if (ctrl || !selectable) {
+ setFocusedNode(focusedNode.getParentNode());
+ } else if (shift) {
+ doRelationSelection(focusedNode.getParentNode(),
+ focusedNode);
+ setFocusedNode(focusedNode.getParentNode());
+ } else {
+ focusAndSelectNode(focusedNode.getParentNode());
+ }
+ }
+ return true;
+ }
+
+ // Navigate right (open branch)
+ if (keycode == getNavigationRightKey()) {
+ if (!focusedNode.isLeaf() && !focusedNode.getState()) {
+ focusedNode.setState(true, true);
+ } else if (!focusedNode.isLeaf()) {
+ if (ctrl || !selectable) {
+ setFocusedNode(focusedNode.getChildren().get(0));
+ } else if (shift) {
+ setSelected(focusedNode, true);
+ setFocusedNode(focusedNode.getChildren().get(0));
+ setSelected(focusedNode, true);
+ } else {
+ focusAndSelectNode(focusedNode.getChildren().get(0));
+ }
+ }
+ return true;
+ }
+
+ // Selection
+ if (keycode == getNavigationSelectKey()) {
+ if (!focusedNode.isSelected()) {
+ selectNode(
+ focusedNode,
+ (!isMultiselect || multiSelectMode == MULTISELECT_MODE_SIMPLE)
+ && selectable);
+ } else {
+ deselectNode(focusedNode);
+ }
+ return true;
+ }
+
+ // Home selection
+ if (keycode == getNavigationStartKey()) {
+ TreeNode node = getFirstRootNode();
+ if (ctrl || !selectable) {
+ setFocusedNode(node);
+ } else if (shift) {
+ deselectAll();
+ selectNodeRange(focusedNode.key, node.key);
+ } else {
+ selectNode(node, true);
+ }
+ sendSelectionToServer();
+ return true;
+ }
+
+ // End selection
+ if (keycode == getNavigationEndKey()) {
+ TreeNode lastNode = getLastRootNode();
+ TreeNode node = getLastVisibleChildInTree(lastNode);
+ if (ctrl || !selectable) {
+ setFocusedNode(node);
+ } else if (shift) {
+ deselectAll();
+ selectNodeRange(focusedNode.key, node.key);
+ } else {
+ selectNode(node, true);
+ }
+ sendSelectionToServer();
+ return true;
+ }
+
+ return false;
+ }
+
+ private void focusAndSelectNode(TreeNode node) {
+ /*
+ * Keyboard navigation doesn't work reliably if the tree is in
+ * multiselect mode as well as isNullSelectionAllowed = false. It first
+ * tries to deselect the old focused node, which fails since there must
+ * be at least one selection. After this the newly focused node is
+ * selected and we've ended up with two selected nodes even though we
+ * only navigated with the arrow keys.
+ *
+ * Because of this, we first select the next node and later de-select
+ * the old one.
+ */
+ TreeNode oldFocusedNode = focusedNode;
+ setFocusedNode(node);
+ setSelected(focusedNode, true);
+ setSelected(oldFocusedNode, false);
+ }
+
+ /**
+ * Traverses the tree to the bottom most child
+ *
+ * @param root
+ * The root of the tree
+ * @return The bottom most child
+ */
+ private TreeNode getLastVisibleChildInTree(TreeNode root) {
+ if (root.isLeaf() || !root.getState() || root.getChildren().size() == 0) {
+ return root;
+ }
+ List<TreeNode> children = root.getChildren();
+ return getLastVisibleChildInTree(children.get(children.size() - 1));
+ }
+
+ /**
+ * Gets the next sibling in the tree
+ *
+ * @param node
+ * The node to get the sibling for
+ * @return The sibling node or null if the node is the last sibling
+ */
+ private TreeNode getNextSibling(TreeNode node) {
+ TreeNode parent = node.getParentNode();
+ List<TreeNode> children;
+ if (parent == null) {
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ int idx = children.indexOf(node);
+ if (idx < children.size() - 1) {
+ return children.get(idx + 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the previous sibling in the tree
+ *
+ * @param node
+ * The node to get the sibling for
+ * @return The sibling node or null if the node is the first sibling
+ */
+ private TreeNode getPreviousSibling(TreeNode node) {
+ TreeNode parent = node.getParentNode();
+ List<TreeNode> children;
+ if (parent == null) {
+ children = getRootNodes();
+ } else {
+ children = parent.getChildren();
+ }
+
+ int idx = children.indexOf(node);
+ if (idx > 0) {
+ return children.get(idx - 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Add this to the element mouse down event by using element.setPropertyJSO
+ * ("onselectstart",applyDisableTextSelectionIEHack()); Remove it then again
+ * when the mouse is depressed in the mouse up event.
+ *
+ * @return Returns the JSO preventing text selection
+ */
+ private native JavaScriptObject applyDisableTextSelectionIEHack()
+ /*-{
+ return function(){ return false; };
+ }-*/;
+
+ /**
+ * Get the key that moves the selection head upwards. By default it is the
+ * up arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationUpKey() {
+ return KeyCodes.KEY_UP;
+ }
+
+ /**
+ * Get the key that moves the selection head downwards. By default it is the
+ * down arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationDownKey() {
+ return KeyCodes.KEY_DOWN;
+ }
+
+ /**
+ * Get the key that scrolls to the left in the table. By default it is the
+ * left arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationLeftKey() {
+ return KeyCodes.KEY_LEFT;
+ }
+
+ /**
+ * Get the key that scroll to the right on the table. By default it is the
+ * right arrow key but by overriding this you can change the key to whatever
+ * you want.
+ *
+ * @return The keycode of the key
+ */
+ protected int getNavigationRightKey() {
+ return KeyCodes.KEY_RIGHT;
+ }
+
+ /**
+ * Get the key that selects an item in the table. By default it is the space
+ * bar key but by overriding this you can change the key to whatever you
+ * want.
+ *
+ * @return
+ */
+ protected int getNavigationSelectKey() {
+ return CHARCODE_SPACE;
+ }
+
+ /**
+ * Get the key the moves the selection one page up in the table. By default
+ * this is the Page Up key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageUpKey() {
+ return KeyCodes.KEY_PAGEUP;
+ }
+
+ /**
+ * Get the key the moves the selection one page down in the table. By
+ * default this is the Page Down key but by overriding this you can change
+ * the key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationPageDownKey() {
+ return KeyCodes.KEY_PAGEDOWN;
+ }
+
+ /**
+ * Get the key the moves the selection to the beginning of the table. By
+ * default this is the Home key but by overriding this you can change the
+ * key to whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationStartKey() {
+ return KeyCodes.KEY_HOME;
+ }
+
+ /**
+ * Get the key the moves the selection to the end of the table. By default
+ * this is the End key but by overriding this you can change the key to
+ * whatever you want.
+ *
+ * @return
+ */
+ protected int getNavigationEndKey() {
+ return KeyCodes.KEY_END;
+ }
+
+ private final String SUBPART_NODE_PREFIX = "n";
+ private final String EXPAND_IDENTIFIER = "expand";
+
+ /*
+ * In webkit, focus may have been requested for this component but not yet
+ * gained. Use this to trac if tree has gained the focus on webkit. See
+ * FocusImplSafari and #6373
+ */
+ private boolean treeHasFocus;
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartElement(java
+ * .lang.String)
+ */
+ public Element getSubPartElement(String subPart) {
+ if ("fe".equals(subPart)) {
+ if (BrowserInfo.get().isOpera() && focusedNode != null) {
+ return focusedNode.getElement();
+ }
+ return getFocusElement();
+ }
+
+ if (subPart.startsWith(SUBPART_NODE_PREFIX + "[")) {
+ boolean expandCollapse = false;
+
+ // Node
+ String[] nodes = subPart.split("/");
+ TreeNode treeNode = null;
+ try {
+ for (String node : nodes) {
+ if (node.startsWith(SUBPART_NODE_PREFIX)) {
+
+ // skip SUBPART_NODE_PREFIX"["
+ node = node.substring(SUBPART_NODE_PREFIX.length() + 1);
+ // skip "]"
+ node = node.substring(0, node.length() - 1);
+ int position = Integer.parseInt(node);
+ if (treeNode == null) {
+ treeNode = getRootNodes().get(position);
+ } else {
+ treeNode = treeNode.getChildren().get(position);
+ }
+ } else if (node.startsWith(EXPAND_IDENTIFIER)) {
+ expandCollapse = true;
+ }
+ }
+
+ if (expandCollapse) {
+ return treeNode.getElement();
+ } else {
+ return treeNode.nodeCaptionSpan;
+ }
+ } catch (Exception e) {
+ // Invalid locator string or node could not be found
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see
+ * com.vaadin.terminal.gwt.client.ui.SubPartAware#getSubPartName(com.google
+ * .gwt.user.client.Element)
+ */
+ public String getSubPartName(Element subElement) {
+ // Supported identifiers:
+ //
+ // n[index]/n[index]/n[index]{/expand}
+ //
+ // Ends with "/expand" if the target is expand/collapse indicator,
+ // otherwise ends with the node
+
+ boolean isExpandCollapse = false;
+
+ if (!getElement().isOrHasChild(subElement)) {
+ return null;
+ }
+
+ if (subElement == getFocusElement()) {
+ return "fe";
+ }
+
+ TreeNode treeNode = Util.findWidget(subElement, TreeNode.class);
+ if (treeNode == null) {
+ // Did not click on a node, let somebody else take care of the
+ // locator string
+ return null;
+ }
+
+ if (subElement == treeNode.getElement()) {
+ // Targets expand/collapse arrow
+ isExpandCollapse = true;
+ }
+
+ ArrayList<Integer> positions = new ArrayList<Integer>();
+ while (treeNode.getParentNode() != null) {
+ positions.add(0,
+ treeNode.getParentNode().getChildren().indexOf(treeNode));
+ treeNode = treeNode.getParentNode();
+ }
+ positions.add(0, getRootNodes().indexOf(treeNode));
+
+ String locator = "";
+ for (Integer i : positions) {
+ locator += SUBPART_NODE_PREFIX + "[" + i + "]/";
+ }
+
+ locator = locator.substring(0, locator.length() - 1);
+ if (isExpandCollapse) {
+ locator += "/" + EXPAND_IDENTIFIER;
+ }
+ return locator;
+ }
+
+ public Action[] getActions() {
+ if (bodyActionKeys == null) {
+ return new Action[] {};
+ }
+ final Action[] actions = new Action[bodyActionKeys.length];
+ for (int i = 0; i < actions.length; i++) {
+ final String actionKey = bodyActionKeys[i];
+ final TreeAction a = new TreeAction(this, null, actionKey);
+ a.setCaption(getActionCaption(actionKey));
+ a.setIconUrl(getActionIcon(actionKey));
+ actions[i] = a;
+ }
+ return actions;
+ }
+
+ public ApplicationConnection getClient() {
+ return client;
+ }
+
+ public String getPaintableId() {
+ return paintableId;
+ }
+
+ private void handleBodyContextMenu(ContextMenuEvent event) {
+ if (!readonly && !disabled) {
+ if (bodyActionKeys != null) {
+ int left = event.getNativeEvent().getClientX();
+ int top = event.getNativeEvent().getClientY();
+ top += Window.getScrollTop();
+ left += Window.getScrollLeft();
+ client.getContextMenu().showAt(this, left, top);
+ }
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ }
+
+ public void registerAction(String key, String caption, String iconUrl) {
+ actionMap.put(key + "_c", caption);
+ if (iconUrl != null) {
+ actionMap.put(key + "_i", iconUrl);
+ } else {
+ actionMap.remove(key + "_i");
+ }
+
+ }
+
+ public void registerNode(TreeNode treeNode) {
+ keyToNode.put(treeNode.key, treeNode);
+ }
+
+ public void clearNodeToKeyMap() {
+ keyToNode.clear();
+ }
+
+}