/*
 * Copyright 2011 Vaadin Ltd.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.vaadin.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.client.ApplicationConnection;
import com.vaadin.client.BrowserInfo;
import com.vaadin.client.ComponentConnector;
import com.vaadin.client.ConnectorMap;
import com.vaadin.client.MouseEventDetailsBuilder;
import com.vaadin.client.UIDL;
import com.vaadin.client.Util;
import com.vaadin.client.ui.Action;
import com.vaadin.client.ui.ActionOwner;
import com.vaadin.client.ui.FocusElementPanel;
import com.vaadin.client.ui.Icon;
import com.vaadin.client.ui.SubPartAware;
import com.vaadin.client.ui.TreeAction;
import com.vaadin.client.ui.VLazyExecutor;
import com.vaadin.client.ui.dd.DDUtil;
import com.vaadin.client.ui.dd.VAbstractDropHandler;
import com.vaadin.client.ui.dd.VAcceptCallback;
import com.vaadin.client.ui.dd.VDragAndDropManager;
import com.vaadin.client.ui.dd.VDragEvent;
import com.vaadin.client.ui.dd.VDropHandler;
import com.vaadin.client.ui.dd.VHasDropHandler;
import com.vaadin.client.ui.dd.VTransferable;
import com.vaadin.shared.MouseEventDetails;
import com.vaadin.shared.MouseEventDetails.MouseButton;
import com.vaadin.shared.ui.MultiSelectMode;
import com.vaadin.shared.ui.dd.VerticalDropLocation;
import com.vaadin.shared.ui.tree.TreeConstants;

/**
 * 
 */
public class VTree extends FocusElementPanel implements VHasDropHandler,
        FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler,
        SubPartAware, ActionOwner {

    public static final String CLASSNAME = "v-tree";

    /**
     * @deprecated from 7.0, use {@link MultiSelectMode#DEFAULT} instead.
     */
    @Deprecated
    public static final MultiSelectMode MULTISELECT_MODE_DEFAULT = MultiSelectMode.DEFAULT;

    /**
     * @deprecated from 7.0, use {@link MultiSelectMode#SIMPLE} instead.
     */
    @Deprecated
    public static final MultiSelectMode MULTISELECT_MODE_SIMPLE = MultiSelectMode.SIMPLE;

    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;
    MultiSelectMode multiSelectMode = MultiSelectMode.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() {

                @Override
                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() {
            @Override
            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() {
                                @Override
                                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);
                }

                @Override
                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() {
            @Override
            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();
            }

            executeEventCommand(new ScheduledCommand() {

                @Override
                public void execute() {

                    if (multiSelectMode == MultiSelectMode.SIMPLE
                            || !isMultiselect) {
                        toggleSelection();
                        lastSelection = TreeNode.this;
                    } else if (multiSelectMode == MultiSelectMode.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;
                        }
                    }
                }
            });

            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;
            }

            final boolean inCaption = isCaptionElement(target);
            if (inCaption
                    && client.hasEventListeners(VTree.this,
                            TreeConstants.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
            }
        }

        /**
         * Checks if the given element is the caption or the icon.
         * 
         * @param target
         *            The element to check
         * @return true if the element is the caption or the icon
         */
        public boolean isCaptionElement(com.google.gwt.dom.client.Element target) {
            return (target == nodeCaptionSpan || (icon != null && target == icon
                    .getElement()));
        }

        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);

            executeEventCommand(new ScheduledCommand() {

                @Override
                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() == MouseButton.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);
                }
            });
        }

        /*
         * Must wait for Safari to focus before sending click and value change
         * events (see #6373, #6374)
         */
        private void executeEventCommand(ScheduledCommand command) {
            if (BrowserInfo.get().isWebkit() && !treeHasFocus) {
                Scheduler.get().scheduleDeferred(command);
            } else {
                command.execute();
            }
        }

        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.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;
        }

        @Override
        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;
        }

        @Override
        public ApplicationConnection getClient() {
            return client;
        }

        @Override
        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);

        }

    }

    @Override
    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() {
                    @Override
                    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)
     */
    @Override
    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)
     */
    @Override
    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)
     */
    @Override
    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)
     */
    @Override
    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.client.ui.SubPartAware#getSubPartElement(java
     * .lang.String)
     */
    @Override
    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.client.ui.SubPartAware#getSubPartName(com.google
     * .gwt.user.client.Element)
     */
    @Override
    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;
    }

    @Override
    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;
    }

    @Override
    public ApplicationConnection getClient() {
        return client;
    }

    @Override
    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();
    }

}
