blob: 7d92b6d47dd8e67801d14fe814e433ce3e2ee114 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* 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.google.gwt.user.client.ui;
import com.google.gwt.core.client.GWT;
import com.google.gwt.i18n.client.LocaleInfo;
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.ui.AbstractImagePrototype.ImagePrototypeElement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* A standard hierarchical tree widget. The tree contains a hierarchy of
* {@link com.google.gwt.user.client.ui.TreeItem TreeItems} that the user can
* open, close, and select.
* <p>
* <img class='gallery' src='Tree.png'/>
* </p>
* <h3>CSS Style Rules</h3>
* <ul class='css'>
* <li>.gwt-Tree { the tree itself }</li>
* <li>.gwt-Tree .gwt-TreeItem { a tree item }</li>
* <li>.gwt-Tree .gwt-TreeItem-selected { a selected tree item }</li>
* </ul>
* <p>
* <h3>Example</h3>
* {@example com.google.gwt.examples.TreeExample}
* </p>
*/
public class Tree extends Widget implements HasWidgets, SourcesTreeEvents,
HasFocus, HasAnimation {
/**
* Provides images to support the the deprecated case where a url prefix is
* passed in through {@link Tree#setImageBase(String)}. This class is used in
* such a way that it will be completely removed by the compiler if the
* deprecated methods, {@link Tree#setImageBase(String)} and
* {@link Tree#getImageBase()}, are not called.
*/
private static class ImagesFromImageBase implements TreeImages {
/**
* A convenience image prototype that implements
* {@link AbstractImagePrototype#applyTo(Image)} for a specified image name.
*/
private class Prototype extends AbstractImagePrototype {
private final String imageUrl;
Prototype(String url) {
imageUrl = url;
}
@Override
public void applyTo(Image image) {
image.setUrl(baseUrl + imageUrl);
}
@Override
public Image createImage() {
// NOTE: This class is only used internally and, therefore only needs
// to support applyTo(Image).
throw new UnsupportedOperationException("createImage is unsupported.");
}
@Override
public String getHTML() {
// NOTE: This class is only used internally and, therefore only needs
// to support applyTo(Image).
throw new UnsupportedOperationException("getHTML is unsupported.");
}
}
private final String baseUrl;
ImagesFromImageBase(String baseUrl) {
this.baseUrl = baseUrl;
}
public AbstractImagePrototype treeClosed() {
return new Prototype("tree_closed.gif");
}
public AbstractImagePrototype treeLeaf() {
return new Prototype("tree_white.gif");
}
public AbstractImagePrototype treeOpen() {
return new Prototype("tree_open.gif");
}
String getBaseUrl() {
return baseUrl;
}
}
private static final int OTHER_KEY_DOWN = 63233;
private static final int OTHER_KEY_LEFT = 63234;
private static final int OTHER_KEY_RIGHT = 63235;
private static final int OTHER_KEY_UP = 63232;
static native boolean shouldTreeDelegateFocusToElement(Element elem) /*-{
var name = elem.nodeName;
return ((name == "SELECT") ||
(name == "INPUT") ||
(name == "TEXTAREA") ||
(name == "OPTION") ||
(name == "BUTTON") ||
(name == "LABEL"));
}-*/;
private static boolean isArrowKey(int code) {
switch (code) {
case OTHER_KEY_DOWN:
case OTHER_KEY_RIGHT:
case OTHER_KEY_UP:
case OTHER_KEY_LEFT:
case KeyboardListener.KEY_DOWN:
case KeyboardListener.KEY_RIGHT:
case KeyboardListener.KEY_UP:
case KeyboardListener.KEY_LEFT:
return true;
default:
return false;
}
}
/**
* Normalized key codes. Also switches KEY_RIGHT and KEY_LEFT in RTL
* languages.
*/
private static int standardizeKeycode(int code) {
switch (code) {
case OTHER_KEY_DOWN:
code = KeyboardListener.KEY_DOWN;
break;
case OTHER_KEY_RIGHT:
code = KeyboardListener.KEY_RIGHT;
break;
case OTHER_KEY_UP:
code = KeyboardListener.KEY_UP;
break;
case OTHER_KEY_LEFT:
code = KeyboardListener.KEY_LEFT;
break;
}
if (LocaleInfo.getCurrentLocale().isRTL()) {
if (code == KeyboardListener.KEY_RIGHT) {
code = KeyboardListener.KEY_LEFT;
} else if (code == KeyboardListener.KEY_LEFT) {
code = KeyboardListener.KEY_RIGHT;
}
}
return code;
}
/**
* Map of TreeItem.widget -> TreeItem.
*/
private final Map<Widget, TreeItem> childWidgets = new HashMap<Widget, TreeItem>();
private TreeItem curSelection;
private Element focusable;
private FocusListenerCollection focusListeners;
private TreeImages images;
private String indentValue;
private boolean isAnimationEnabled = false;
private KeyboardListenerCollection keyboardListeners;
private boolean lastWasKeyDown;
private TreeListenerCollection listeners;
private MouseListenerCollection mouseListeners = null;
private TreeItem root;
private boolean useLeafImages;
/**
* Constructs an empty tree.
*/
public Tree() {
if (LocaleInfo.getCurrentLocale().isRTL()) {
init(GWT.<TreeImagesRTL> create(TreeImagesRTL.class), false);
} else {
init(GWT.<TreeImages> create(TreeImages.class), false);
}
}
/**
* Constructs a tree that uses the specified image bundle for images.
*
* @param images a bundle that provides tree specific images
*/
public Tree(TreeImages images) {
init(images, true);
}
/**
* Constructs a tree that uses the specified image bundle for images. If this
* tree does not use leaf images, the width of the TreeImage's leaf image will
* control the leaf indent.
*
* @param images a bundle that provides tree specific images
* @param useLeafImages use leaf images from bundle
*/
public Tree(TreeImages images, boolean useLeafImages) {
init(images, useLeafImages);
}
/**
* Adds the widget as a root tree item.
*
* @see com.google.gwt.user.client.ui.HasWidgets#add(com.google.gwt.user.client.ui.Widget)
* @param widget widget to add.
*/
public void add(Widget widget) {
addItem(widget);
}
public void addFocusListener(FocusListener listener) {
if (focusListeners == null) {
focusListeners = new FocusListenerCollection();
}
focusListeners.add(listener);
}
/**
* Adds a simple tree item containing the specified text.
*
* @param itemText the text of the item to be added
* @return the item that was added
*/
public TreeItem addItem(String itemText) {
TreeItem ret = new TreeItem(itemText);
addItem(ret);
return ret;
}
/**
* Adds an item to the root level of this tree.
*
* @param item the item to be added
*/
public void addItem(TreeItem item) {
root.addItem(item);
}
/**
* Adds a new tree item containing the specified widget.
*
* @param widget the widget to be added
*/
public TreeItem addItem(Widget widget) {
return root.addItem(widget);
}
public void addKeyboardListener(KeyboardListener listener) {
if (keyboardListeners == null) {
keyboardListeners = new KeyboardListenerCollection();
}
keyboardListeners.add(listener);
}
public void addMouseListener(MouseListener listener) {
if (mouseListeners == null) {
mouseListeners = new MouseListenerCollection();
}
mouseListeners.add(listener);
}
public void addTreeListener(TreeListener listener) {
if (listeners == null) {
listeners = new TreeListenerCollection();
}
listeners.add(listener);
}
/**
* Clears all tree items from the current tree.
*/
public void clear() {
int size = root.getChildCount();
for (int i = size - 1; i >= 0; i--) {
root.getChild(i).remove();
}
}
/**
* Ensures that the currently-selected item is visible, opening its parents
* and scrolling the tree as necessary.
*/
public void ensureSelectedItemVisible() {
if (curSelection == null) {
return;
}
TreeItem parent = curSelection.getParentItem();
while (parent != null) {
parent.setState(true);
parent = parent.getParentItem();
}
}
/**
* Gets this tree's default image package.
*
* @return the tree's image package
* @see #setImageBase
* @deprecated Use {@link #Tree(TreeImages)} as it provides a more efficent
* and manageable way to supply a set of images to be used within
* a tree.
*/
@Deprecated
public String getImageBase() {
return (images instanceof ImagesFromImageBase)
? ((ImagesFromImageBase) images).getBaseUrl() : GWT.getModuleBaseURL();
}
/**
* Gets the top-level tree item at the specified index.
*
* @param index the index to be retrieved
* @return the item at that index
*/
public TreeItem getItem(int index) {
return root.getChild(index);
}
/**
* Gets the number of items contained at the root of this tree.
*
* @return this tree's item count
*/
public int getItemCount() {
return root.getChildCount();
}
/**
* Gets the currently selected item.
*
* @return the selected item
*/
public TreeItem getSelectedItem() {
return curSelection;
}
public int getTabIndex() {
return FocusPanel.impl.getTabIndex(focusable);
}
public boolean isAnimationEnabled() {
return isAnimationEnabled;
}
public Iterator<Widget> iterator() {
final Widget[] widgets = new Widget[childWidgets.size()];
childWidgets.keySet().toArray(widgets);
return WidgetIterators.createWidgetIterator(this, widgets);
}
@Override
@SuppressWarnings("fallthrough")
public void onBrowserEvent(Event event) {
int eventType = DOM.eventGetType(event);
switch (eventType) {
case Event.ONKEYDOWN: {
// If nothing's selected, select the first item.
if (curSelection == null) {
if (root.getChildCount() > 0) {
onSelection(root.getChild(0), true, true);
}
super.onBrowserEvent(event);
return;
}
}
// Intentional fallthrough.
case Event.ONKEYPRESS:
case Event.ONKEYUP:
// Issue 1890: Do not block history navigation via alt+left/right
if (DOM.eventGetAltKey(event) || DOM.eventGetMetaKey(event)) {
super.onBrowserEvent(event);
return;
}
break;
}
switch (eventType) {
case Event.ONCLICK: {
Element e = DOM.eventGetTarget(event);
if (shouldTreeDelegateFocusToElement(e)) {
// The click event should have given focus to this element already.
// Avoid moving focus back up to the tree (so that focusable widgets
// attached to TreeItems can receive keyboard events).
} else if (curSelection != null
&& curSelection.getContentElem().isOrHasChild(e)) {
setFocus(true);
}
break;
}
case Event.ONMOUSEDOWN: {
if (mouseListeners != null) {
mouseListeners.fireMouseEvent(this, event);
}
// Currently, the way we're using image bundles causes extraneous events
// to be sunk on individual items' open/close images. This leads to an
// extra event reaching the Tree, which we will ignore here.
if (DOM.eventGetCurrentTarget(event) == getElement()) {
elementClicked(DOM.eventGetTarget(event));
}
break;
}
case Event.ONMOUSEUP: {
if (mouseListeners != null) {
mouseListeners.fireMouseEvent(this, event);
}
break;
}
case Event.ONMOUSEMOVE: {
if (mouseListeners != null) {
mouseListeners.fireMouseEvent(this, event);
}
break;
}
case Event.ONMOUSEOVER: {
if (mouseListeners != null) {
mouseListeners.fireMouseEvent(this, event);
}
break;
}
case Event.ONMOUSEOUT: {
if (mouseListeners != null) {
mouseListeners.fireMouseEvent(this, event);
}
break;
}
case Event.ONFOCUS:
if (focusListeners != null) {
focusListeners.fireFocusEvent(this, event);
}
break;
case Event.ONBLUR: {
if (focusListeners != null) {
focusListeners.fireFocusEvent(this, event);
}
break;
}
case Event.ONKEYDOWN: {
keyboardNavigation(event);
lastWasKeyDown = true;
break;
}
case Event.ONKEYPRESS: {
if (!lastWasKeyDown) {
keyboardNavigation(event);
}
lastWasKeyDown = false;
break;
}
case Event.ONKEYUP: {
if (DOM.eventGetKeyCode(event) == KeyboardListener.KEY_TAB) {
ArrayList<Element> chain = new ArrayList<Element>();
collectElementChain(chain, getElement(), DOM.eventGetTarget(event));
TreeItem item = findItemByChain(chain, 0, root);
if (item != getSelectedItem()) {
setSelectedItem(item, true);
}
}
lastWasKeyDown = false;
break;
}
}
switch (eventType) {
case Event.ONKEYDOWN:
case Event.ONKEYUP:
case Event.ONKEYPRESS: {
if (keyboardListeners != null) {
keyboardListeners.fireKeyboardEvent(this, event);
}
if (isArrowKey(DOM.eventGetKeyCode(event))) {
DOM.eventCancelBubble(event, true);
DOM.eventPreventDefault(event);
}
break;
}
}
// We must call SynthesizedWidget's implementation for all other events.
super.onBrowserEvent(event);
}
public boolean remove(Widget w) {
// Validate.
TreeItem item = childWidgets.get(w);
if (item == null) {
return false;
}
// Delegate to TreeItem.setWidget, which performs correct removal.
item.setWidget(null);
return true;
}
public void removeFocusListener(FocusListener listener) {
if (focusListeners != null) {
focusListeners.remove(listener);
}
}
/**
* Removes an item from the root level of this tree.
*
* @param item the item to be removed
*/
public void removeItem(TreeItem item) {
root.removeItem(item);
}
/**
* Removes all items from the root level of this tree.
*/
public void removeItems() {
while (getItemCount() > 0) {
removeItem(getItem(0));
}
}
public void removeKeyboardListener(KeyboardListener listener) {
if (keyboardListeners != null) {
keyboardListeners.remove(listener);
}
}
public void removeTreeListener(TreeListener listener) {
if (listeners != null) {
listeners.remove(listener);
}
}
public void setAccessKey(char key) {
FocusPanel.impl.setAccessKey(focusable, key);
}
public void setAnimationEnabled(boolean enable) {
isAnimationEnabled = enable;
}
public void setFocus(boolean focus) {
if (focus) {
FocusPanel.impl.focus(focusable);
} else {
FocusPanel.impl.blur(focusable);
}
}
/**
* Sets the base URL under which this tree will find its default images. These
* images must be named "tree_white.gif", "tree_open.gif", and
* "tree_closed.gif".
*
* @param baseUrl
* @deprecated Use {@link #Tree(TreeImages)} as it provides a more efficent
* and manageable way to supply a set of images to be used within
* a tree.
*/
@Deprecated
public void setImageBase(String baseUrl) {
images = new ImagesFromImageBase(baseUrl);
root.updateStateRecursive();
}
/**
* Selects a specified item.
*
* @param item the item to be selected, or <code>null</code> to deselect all
* items
*/
public void setSelectedItem(TreeItem item) {
setSelectedItem(item, true);
}
/**
* Selects a specified item.
*
* @param item the item to be selected, or <code>null</code> to deselect all
* items
* @param fireEvents <code>true</code> to allow selection events to be fired
*/
public void setSelectedItem(TreeItem item, boolean fireEvents) {
if (item == null) {
if (curSelection == null) {
return;
}
curSelection.setSelected(false);
curSelection = null;
return;
}
onSelection(item, fireEvents, true);
}
public void setTabIndex(int index) {
FocusPanel.impl.setTabIndex(focusable, index);
}
/**
* Iterator of tree items.
*/
public Iterator<TreeItem> treeItemIterator() {
List<TreeItem> accum = new ArrayList<TreeItem>();
root.addTreeItems(accum);
return accum.iterator();
}
@Override
protected void doAttachChildren() {
// Ensure that all child widgets are attached.
for (Iterator<Widget> it = iterator(); it.hasNext();) {
Widget child = it.next();
child.onAttach();
}
DOM.setEventListener(focusable, this);
}
@Override
protected void doDetachChildren() {
// Ensure that all child widgets are detached.
for (Iterator<Widget> it = iterator(); it.hasNext();) {
Widget child = it.next();
child.onDetach();
}
DOM.setEventListener(focusable, null);
}
/**
* Indicates if keyboard navigation is enabled for the Tree and for a given
* TreeItem. Subclasses of Tree can override this function to selectively
* enable or disable keyboard navigation.
*
* @param currentItem the currently selected TreeItem
* @return <code>true</code> if the Tree will response to arrow keys by
* changing the currently selected item
*/
protected boolean isKeyboardNavigationEnabled(TreeItem currentItem) {
return true;
}
/**
* <b>Affected Elements:</b>
* <ul>
* <li>-root = The root {@link TreeItem}.</li>
* </ul>
*
* @see UIObject#onEnsureDebugId(String)
*/
@Override
protected void onEnsureDebugId(String baseID) {
super.onEnsureDebugId(baseID);
root.ensureDebugId(baseID + "-root");
}
@Override
protected void onLoad() {
root.updateStateRecursive();
}
void adopt(Widget widget, TreeItem treeItem) {
assert (!childWidgets.containsKey(widget));
childWidgets.put(widget, treeItem);
widget.setParent(this);
}
void fireStateChanged(TreeItem item) {
if (listeners != null) {
listeners.fireItemStateChanged(item);
}
}
/*
* This method exists solely to support unit tests.
*/
Map<Widget, TreeItem> getChildWidgets() {
return childWidgets;
}
TreeImages getImages() {
return images;
}
void maybeUpdateSelection(TreeItem itemThatChangedState, boolean isItemOpening) {
/**
* If we just closed the item, let's check to see if this item is the parent
* of the currently selected item. If so, we should make this item the
* currently selected selected item.
*/
if (!isItemOpening) {
TreeItem tempItem = curSelection;
while (tempItem != null) {
if (tempItem == itemThatChangedState) {
setSelectedItem(itemThatChangedState);
return;
}
tempItem = tempItem.getParentItem();
}
}
}
void orphan(Widget widget) {
// Validation should already be done.
assert (widget.getParent() == this);
// Orphan.
widget.setParent(null);
// Logical detach.
childWidgets.remove(widget);
}
/**
* Called only from {@link TreeItem}: Shows the closed image on that tree
* item.
*
* @param treeItem the tree item
*/
void showClosedImage(TreeItem treeItem) {
showImage(treeItem, images.treeClosed());
}
/**
* Called only from {@link TreeItem}: Shows the leaf image on a tree item.
*
* @param treeItem the tree item
*/
void showLeafImage(TreeItem treeItem) {
if (useLeafImages) {
showImage(treeItem, images.treeLeaf());
} else {
DOM.setStyleAttribute(treeItem.getElement(), "paddingLeft", indentValue);
}
}
/**
* Called only from {@link TreeItem}: Shows the open image on a tree item.
*
* @param treeItem the tree item
*/
void showOpenImage(TreeItem treeItem) {
showImage(treeItem, images.treeOpen());
}
/**
* Collects parents going up the element tree, terminated at the tree root.
*/
private void collectElementChain(ArrayList<Element> chain, Element hRoot,
Element hElem) {
if ((hElem == null) || (hElem == hRoot)) {
return;
}
collectElementChain(chain, hRoot, DOM.getParent(hElem));
chain.add(hElem);
}
private boolean elementClicked(Element hElem) {
ArrayList<Element> chain = new ArrayList<Element>();
collectElementChain(chain, getElement(), hElem);
TreeItem item = findItemByChain(chain, 0, root);
if (item != null && item != root) {
if (item.getChildCount() > 0
&& DOM.isOrHasChild(item.getImageElement(), hElem)) {
item.setState(!item.getState(), true);
return true;
} else if (DOM.isOrHasChild(item.getElement(), hElem)) {
onSelection(item, true, !shouldTreeDelegateFocusToElement(hElem));
return true;
}
}
return false;
}
private TreeItem findDeepestOpenChild(TreeItem item) {
if (!item.getState()) {
return item;
}
return findDeepestOpenChild(item.getChild(item.getChildCount() - 1));
}
private TreeItem findItemByChain(ArrayList<Element> chain, int idx,
TreeItem root) {
if (idx == chain.size()) {
return root;
}
Element hCurElem = chain.get(idx);
for (int i = 0, n = root.getChildCount(); i < n; ++i) {
TreeItem child = root.getChild(i);
if (child.getElement() == hCurElem) {
TreeItem retItem = findItemByChain(chain, idx + 1, root.getChild(i));
if (retItem == null) {
return child;
}
return retItem;
}
}
return findItemByChain(chain, idx + 1, root);
}
/**
* Get the top parent above this {@link TreeItem} that is in closed state. In
* other words, get the parent that is guaranteed to be visible.
*
* @param item
* @return the closed parent, or null if all parents are opened
*/
private TreeItem getTopClosedParent(TreeItem item) {
TreeItem topClosedParent = null;
TreeItem parent = item.getParentItem();
while (parent != null && parent != root) {
if (!parent.getState()) {
topClosedParent = parent;
}
parent = parent.getParentItem();
}
return topClosedParent;
}
private void init(TreeImages images, boolean useLeafImages) {
setImages(images, useLeafImages);
setElement(DOM.createDiv());
DOM.setStyleAttribute(getElement(), "position", "relative");
// Fix rendering problem with relatively-positioned elements and their
// children by
// forcing the element that is positioned relatively to 'have layout'
DOM.setStyleAttribute(getElement(), "zoom", "1");
focusable = FocusPanel.impl.createFocusable();
DOM.setStyleAttribute(focusable, "fontSize", "0");
DOM.setStyleAttribute(focusable, "position", "absolute");
// Hide focus outline in Mozilla/Webkit/Opera
DOM.setStyleAttribute(focusable, "outline", "0px");
// Hide focus outline in IE 6/7
DOM.setElementAttribute(focusable, "hideFocus", "true");
DOM.setIntStyleAttribute(focusable, "zIndex", -1);
DOM.appendChild(getElement(), focusable);
sinkEvents(Event.MOUSEEVENTS | Event.ONCLICK | Event.KEYEVENTS);
DOM.sinkEvents(focusable, Event.FOCUSEVENTS);
// The 'root' item is invisible and serves only as a container
// for all top-level items.
root = new TreeItem() {
@Override
public void addItem(TreeItem item) {
// If this element already belongs to a tree or tree item, remove it.
if ((item.getParentItem() != null) || (item.getTree() != null)) {
item.remove();
}
DOM.appendChild(Tree.this.getElement(), item.getElement());
item.setTree(this.getTree());
// Explicitly set top-level items' parents to null.
item.setParentItem(null);
getChildren().add(item);
// Use no margin on top-most items.
if (LocaleInfo.getCurrentLocale().isRTL()) {
DOM.setIntStyleAttribute(item.getElement(), "marginRight", 0);
} else {
DOM.setIntStyleAttribute(item.getElement(), "marginLeft", 0);
}
}
@Override
public void removeItem(TreeItem item) {
if (!getChildren().contains(item)) {
return;
}
// Update Item state.
item.setTree(null);
item.setParentItem(null);
getChildren().remove(item);
DOM.removeChild(Tree.this.getElement(), item.getElement());
}
};
root.initChildren();
root.setTree(this);
setStyleName("gwt-Tree");
// Add a11y role "tree"
Accessibility.setRole(getElement(), Accessibility.ROLE_TREE);
Accessibility.setRole(focusable, Accessibility.ROLE_TREEITEM);
}
private void keyboardNavigation(Event event) {
// Handle keyboard events if keyboard navigation is enabled
if (isKeyboardNavigationEnabled(curSelection)) {
int code = DOM.eventGetKeyCode(event);
switch (standardizeKeycode(code)) {
case KeyboardListener.KEY_UP: {
moveSelectionUp(curSelection);
break;
}
case KeyboardListener.KEY_DOWN: {
moveSelectionDown(curSelection, true);
break;
}
case KeyboardListener.KEY_LEFT: {
maybeCollapseTreeItem();
break;
}
case KeyboardListener.KEY_RIGHT: {
maybeExpandTreeItem();
break;
}
default: {
return;
}
}
}
}
private void maybeCollapseTreeItem() {
TreeItem topClosedParent = getTopClosedParent(curSelection);
if (topClosedParent != null) {
// Select the first visible parent if curSelection is hidden
setSelectedItem(topClosedParent);
} else if (curSelection.getState()) {
curSelection.setState(false);
} else {
TreeItem parent = curSelection.getParentItem();
if (parent != null) {
setSelectedItem(parent);
}
}
}
private void maybeExpandTreeItem() {
TreeItem topClosedParent = getTopClosedParent(curSelection);
if (topClosedParent != null) {
// Select the first visible parent if curSelection is hidden
setSelectedItem(topClosedParent);
} else if (!curSelection.getState()) {
curSelection.setState(true);
} else if (curSelection.getChildCount() > 0) {
setSelectedItem(curSelection.getChild(0));
}
}
/**
* Move the tree focus to the specified selected item.
*/
private void moveFocus() {
HasFocus focusableWidget = curSelection.getFocusableWidget();
if (focusableWidget != null) {
focusableWidget.setFocus(true);
DOM.scrollIntoView(((Widget) focusableWidget).getElement());
} else {
// Get the location and size of the given item's content element relative
// to the tree.
Element selectedElem = curSelection.getContentElem();
int containerLeft = getAbsoluteLeft();
int containerTop = getAbsoluteTop();
int left = DOM.getAbsoluteLeft(selectedElem) - containerLeft;
int top = DOM.getAbsoluteTop(selectedElem) - containerTop;
int width = DOM.getElementPropertyInt(selectedElem, "offsetWidth");
int height = DOM.getElementPropertyInt(selectedElem, "offsetHeight");
// If the item is not visible, quite here
if (width == 0 || height == 0) {
DOM.setIntStyleAttribute(focusable, "left", 0);
DOM.setIntStyleAttribute(focusable, "top", 0);
return;
}
// Set the focusable element's position and size to exactly underlap the
// item's content element.
DOM.setStyleAttribute(focusable, "left", left + "px");
DOM.setStyleAttribute(focusable, "top", top + "px");
DOM.setStyleAttribute(focusable, "width", width + "px");
DOM.setStyleAttribute(focusable, "height", height + "px");
// Scroll it into view.
DOM.scrollIntoView(focusable);
// Update ARIA attributes to reflect the information from the
// newly-selected item.
updateAriaAttributes();
// Ensure Focus is set, as focus may have been previously delegated by
// tree.
setFocus(true);
}
}
/**
* Moves to the next item, going into children as if dig is enabled.
*/
private void moveSelectionDown(TreeItem sel, boolean dig) {
if (sel == root) {
return;
}
// Find a parent that is visible
TreeItem topClosedParent = getTopClosedParent(sel);
if (topClosedParent != null) {
moveSelectionDown(topClosedParent, false);
return;
}
TreeItem parent = sel.getParentItem();
if (parent == null) {
parent = root;
}
int idx = parent.getChildIndex(sel);
if (!dig || !sel.getState()) {
if (idx < parent.getChildCount() - 1) {
onSelection(parent.getChild(idx + 1), true, true);
} else {
moveSelectionDown(parent, false);
}
} else if (sel.getChildCount() > 0) {
onSelection(sel.getChild(0), true, true);
}
}
/**
* Moves the selected item up one.
*/
private void moveSelectionUp(TreeItem sel) {
// Find a parent that is visible
TreeItem topClosedParent = getTopClosedParent(sel);
if (topClosedParent != null) {
onSelection(topClosedParent, true, true);
return;
}
TreeItem parent = sel.getParentItem();
if (parent == null) {
parent = root;
}
int idx = parent.getChildIndex(sel);
if (idx > 0) {
TreeItem sibling = parent.getChild(idx - 1);
onSelection(findDeepestOpenChild(sibling), true, true);
} else {
onSelection(parent, true, true);
}
}
private void onSelection(TreeItem item, boolean fireEvents, boolean moveFocus) {
// 'root' isn't a real item, so don't let it be selected
// (some cases in the keyboard handler will try to do this)
if (item == root) {
return;
}
if (curSelection != null) {
curSelection.setSelected(false);
}
curSelection = item;
if (moveFocus && curSelection != null) {
moveFocus();
// Select the item and fire the selection event.
curSelection.setSelected(true);
if (fireEvents && (listeners != null)) {
listeners.fireItemSelected(curSelection);
}
}
}
private void setImages(TreeImages images, boolean useLeafImages) {
this.images = images;
this.useLeafImages = useLeafImages;
if (!useLeafImages) {
Image image = images.treeLeaf().createImage();
DOM.setStyleAttribute(image.getElement(), "visibility", "hidden");
RootPanel.get().add(image);
int size = image.getWidth() + TreeItem.IMAGE_PAD;
image.removeFromParent();
indentValue = (size) + "px";
}
}
private void showImage(TreeItem treeItem, AbstractImagePrototype proto) {
Element holder = treeItem.getImageHolderElement();
Element child = DOM.getFirstChild(holder);
if (child == null) {
// If no image element has been created yet, create one from the
// prototype.
DOM.appendChild(holder, proto.createElement().<Element> cast());
} else {
// Otherwise, simply apply the prototype to the existing element.
proto.applyTo(child.<ImagePrototypeElement> cast());
}
}
private void updateAriaAttributes() {
Element curSelectionContentElem = curSelection.getContentElem();
// Set the 'aria-level' state. To do this, we need to compute the level of
// the currently selected item.
// We initialize itemLevel to -1 because the level value is zero-based.
// Note that the root node is not a part of the TreeItem hierachy, and we
// do not consider the root node to have a designated level. The level of
// the root's children is level 0, its children's children is level 1, etc.
int curSelectionLevel = -1;
TreeItem tempItem = curSelection;
while (tempItem != null) {
tempItem = tempItem.getParentItem();
++curSelectionLevel;
}
Accessibility.setState(curSelectionContentElem, Accessibility.STATE_LEVEL,
String.valueOf(curSelectionLevel + 1));
// Set the 'aria-setsize' and 'aria-posinset' states. To do this, we need to
// compute the the number of siblings that the currently selected item has,
// and the item's position among its siblings.
TreeItem curSelectionParent = curSelection.getParentItem();
if (curSelectionParent == null) {
curSelectionParent = root;
}
Accessibility.setState(curSelectionContentElem,
Accessibility.STATE_SETSIZE,
String.valueOf(curSelectionParent.getChildCount()));
int curSelectionIndex = curSelectionParent.getChildIndex(curSelection);
Accessibility.setState(curSelectionContentElem,
Accessibility.STATE_POSINSET, String.valueOf(curSelectionIndex + 1));
// Set the 'aria-expanded' state. This depends on the state of the currently
// selected item.
// If the item has no children, we remove the 'aria-expanded' state.
if (curSelection.getChildCount() == 0) {
Accessibility.removeState(curSelectionContentElem,
Accessibility.STATE_EXPANDED);
} else {
if (curSelection.getState()) {
Accessibility.setState(curSelectionContentElem,
Accessibility.STATE_EXPANDED, "true");
} else {
Accessibility.setState(curSelectionContentElem,
Accessibility.STATE_EXPANDED, "false");
}
}
// Make sure that 'aria-selected' is true.
Accessibility.setState(curSelectionContentElem,
Accessibility.STATE_SELECTED, "true");
// Update the 'aria-activedescendant' state for the focusable element to
// match the id of the currently selected item
Accessibility.setState(focusable, Accessibility.STATE_ACTIVEDESCENDANT,
DOM.getElementAttribute(curSelectionContentElem, "id"));
}
}