/* * Copyright 2000-2016 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.v7.client.ui; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import com.google.gwt.aria.client.ExpandedValue; import com.google.gwt.aria.client.Id; import com.google.gwt.aria.client.Roles; import com.google.gwt.aria.client.SelectedValue; 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.Element; 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.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.WidgetUtil; 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.aria.AriaHelper; import com.vaadin.client.ui.aria.HandlesAriaCaption; 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.v7.client.ui.tree.TreeConnector; import com.vaadin.v7.shared.ui.tree.TreeConstants; /** * */ public class VTree extends FocusElementPanel implements VHasDropHandler, FocusHandler, BlurHandler, KeyPressHandler, KeyDownHandler, SubPartAware, ActionOwner, HandlesAriaCaption { private String lastNodeKey = ""; public static final String CLASSNAME = "v-tree"; /** * @deprecated As of 7.0, use {@link MultiSelectMode#DEFAULT} instead. */ @Deprecated public static final MultiSelectMode MULTISELECT_MODE_DEFAULT = MultiSelectMode.DEFAULT; /** * @deprecated As of 7.0, use {@link MultiSelectMode#SIMPLE} instead. */ @Deprecated public static final MultiSelectMode MULTISELECT_MODE_SIMPLE = MultiSelectMode.SIMPLE; private static final int CHARCODE_SPACE = 32; /** For internal use only. May be removed or replaced in the future. */ public final FlowPanel body = new FlowPanel(); /** For internal use only. May be removed or replaced in the future. */ public Set<String> selectedIds = new HashSet<String>(); /** For internal use only. May be removed or replaced in the future. */ public ApplicationConnection client; /** For internal use only. May be removed or replaced in the future. */ public String paintableId; /** For internal use only. May be removed or replaced in the future. */ public boolean selectable; /** For internal use only. May be removed or replaced in the future. */ public boolean isMultiselect; private String currentMouseOverKey; /** For internal use only. May be removed or replaced in the future. */ public TreeNode lastSelection; /** For internal use only. May be removed or replaced in the future. */ public TreeNode focusedNode; /** For internal use only. May be removed or replaced in the future. */ public 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>(); /** For internal use only. May be removed or replaced in the future. */ public boolean immediate; /** For internal use only. May be removed or replaced in the future. */ public boolean isNullSelectionAllowed = true; /** For internal use only. May be removed or replaced in the future. */ public boolean isHtmlContentAllowed = false; /** For internal use only. May be removed or replaced in the future. */ public boolean disabled = false; /** For internal use only. May be removed or replaced in the future. */ public boolean readonly; /** For internal use only. May be removed or replaced in the future. */ public boolean rendering; private VAbstractDropHandler dropHandler; /** For internal use only. May be removed or replaced in the future. */ public int dragMode; private boolean selectionHasChanged = false; /* * to fix #14388. The cause of defect #14388: event 'clickEvent' is sent to * server before updating of "selected" variable, but should be sent after * it */ private boolean clickEventPending = false; /** For internal use only. May be removed or replaced in the future. */ public String[] bodyActionKeys; /** For internal use only. May be removed or replaced in the future. */ public TreeConnector connector; public VLazyExecutor iconLoaded = new VLazyExecutor(50, new ScheduledCommand() { @Override public void execute() { doLayout(); } }); public VTree() { super(); setStyleName(CLASSNAME); Roles.getTreeRole().set(body.getElement()); 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()) { 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 = WidgetUtil.findWidget(elementOver, TreeNode.class); return treeNode == null ? null : treeNode.key; } /** For internal use only. May be removed or replaced in the future. */ public 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() { /* * we should send selection to server immediately in 2 cases: 1) * 'immediate' property of Tree is true 2) clickEventPending is * true */ client.updateVariable(paintableId, "selected", selectedIds.toArray(new String[selectedIds.size()]), clickEventPending || immediate); clickEventPending = false; 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; /** For internal use only. May be removed or replaced in the future. */ public String[] actionKeys = null; /** For internal use only. May be removed or replaced in the future. */ public boolean childrenLoaded; Element nodeCaptionDiv; protected Element nodeCaptionSpan; /** For internal use only. May be removed or replaced in the future. */ public 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 && icon != null && 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 (nodeCaptionSpan.isOrHasChild(target) || (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. clickEventPending = false; if ((details.getButton() == MouseButton.LEFT || details.getButton() == MouseButton.MIDDLE) && !details.isDoubleClick() && selectable) { // Probably a selection that will cause a value change // event to be sent clickEventPending = true; // 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) { clickEventPending = false; } } client.updateVariable(paintableId, "clickedKey", key, false); client.updateVariable(paintableId, "clickEvent", details.toString(), !clickEventPending); } }); } /* * 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() { String labelId = DOM.createUniqueId(); addStyleName(CLASSNAME); String treeItemId = DOM.createUniqueId(); getElement().setId(treeItemId); Roles.getTreeitemRole().set(getElement()); Roles.getTreeitemRole().setAriaSelectedState(getElement(), SelectedValue.FALSE); Roles.getTreeitemRole().setAriaLabelledbyProperty(getElement(), Id.of(labelId)); nodeCaptionDiv = DOM.createDiv(); DOM.setElementProperty(nodeCaptionDiv, "className", CLASSNAME + "-caption"); Element wrapper = DOM.createDiv(); wrapper.setId(labelId); wrapper.setAttribute("for", treeItemId); 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"); Roles.getGroupRole().set(childNodeContainer.getElement()); setWidget(childNodeContainer); } public boolean isLeaf() { String[] styleNames = getStyleName().split(" "); for (String styleName : styleNames) { if (styleName.equals(CLASSNAME + "-leaf")) { return true; } } return false; } /** For internal use only. May be removed or replaced in the future. */ public 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"); Roles.getTreeitemRole().setAriaExpandedState(getElement(), ExpandedValue.TRUE); childNodeContainer.setVisible(true); } else { removeStyleName(CLASSNAME + "-expanded"); Roles.getTreeitemRole().setAriaExpandedState(getElement(), ExpandedValue.FALSE); childNodeContainer.setVisible(false); if (notifyServer) { client.updateVariable(paintableId, "collapse", new String[] { key }, true); } } open = state; if (!rendering) { doLayout(); } } /** For internal use only. May be removed or replaced in the future. */ public boolean getState() { return open; } /** For internal use only. May be removed or replaced in the future. */ public void setText(String text) { DOM.setInnerText(nodeCaptionSpan, text); } /** For internal use only. May be removed or replaced in the future. */ public void setHtml(String html) { nodeCaptionSpan.setInnerHTML(html); } 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. * <p> * For internal use only. May be removed or replaced in the future. * * @param selected */ public 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() { WidgetUtil.scrollIntoViewVertically(nodeCaptionDiv); } public void setIcon(String iconUrl, String altText) { if (icon != null) { DOM.getFirstChild(nodeCaptionDiv) .removeChild(icon.getElement()); } icon = client.getIcon(iconUrl); if (icon != null) { DOM.insertBefore(DOM.getFirstChild(nodeCaptionDiv), icon.getElement(), nodeCaptionSpan); icon.setAlternateText(altText); } } 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); Roles.getTreeRole().removeAriaActivedescendantProperty( focusedNode.getElement()); } if (node != null) { node.setFocused(true); Roles.getTreeitemRole().setAriaSelectedState(node.getElement(), SelectedValue.TRUE); /* * FIXME: This code needs to be changed when the keyboard navigation * doesn't immediately trigger a selection change anymore. * * Right now this function is called before and after the Tree is * rebuilt when up/down arrow keys are pressed. This leads to the * problem, that the newly selected item is announced too often with * a screen reader. * * Behaviour is different when using the Tree with and without * screen reader. */ if (node.key.equals(lastNodeKey)) { Roles.getTreeRole().setAriaActivedescendantProperty( getFocusElement(), Id.of(node.getElement())); } else { lastNodeKey = node.key; } } 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); } } showTooltipForKeyboardNavigation(node); } 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); } } showTooltipForKeyboardNavigation(node); } 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()); } } showTooltipForKeyboardNavigation(focusedNode); 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)); } } showTooltipForKeyboardNavigation(focusedNode); 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(); showTooltipForKeyboardNavigation(node); 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(); showTooltipForKeyboardNavigation(node); return true; } return false; } private void showTooltipForKeyboardNavigation(TreeNode node) { if (connector != null) { getClient().getVTooltip().showAssistive( connector.getTooltipInfo(node.nodeCaptionSpan)); } } 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 com.google.gwt.user.client.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 DOM.asOld(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( com.google.gwt.user.client.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 = WidgetUtil.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(); } @Override public void bindAriaCaption( com.google.gwt.user.client.Element captionElement) { AriaHelper.bindCaption(body, captionElement); } /** * Tell LayoutManager that a layout is needed later for this VTree */ private void doLayout() { // This calls LayoutManager setNeedsMeasure and layoutNow Util.notifyParentOfSizeChange(this, false); } }