/* * 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.animation.client.Animation; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.i18n.client.LocaleInfo; import com.google.gwt.safehtml.client.HasSafeHtml; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Element; import java.util.ArrayList; import java.util.List; /** * An item that can be contained within a * {@link com.google.gwt.user.client.ui.Tree}. * * Each tree item is assigned a unique DOM id in order to support ARIA. See * {@link com.google.gwt.user.client.ui.Accessibility} for more information. * * <p> * <h3>Example</h3> * {@example com.google.gwt.examples.TreeExample} * </p> */ public class TreeItem extends UIObject implements IsTreeItem, HasTreeItems, HasHTML, HasSafeHtml { /* * For compatibility with UiBinder interface HasTreeItems should be declared * before HasHTML, so that children items and widgets are processed before * interpreting HTML. */ /** * The margin applied to child items. */ private static final double CHILD_MARGIN = 16.0; /** * Implementation class for {@link TreeItem}. */ public static class TreeItemImpl { public TreeItemImpl() { initializeClonableElements(); } void convertToFullNode(TreeItem item) { if (item.imageHolder == null) { // Extract the Elements from the object Element itemTable = DOM.clone(BASE_INTERNAL_ELEM, true); DOM.appendChild(item.getElement(), itemTable); Element tr = DOM.getFirstChild(DOM.getFirstChild(itemTable)); Element tdImg = DOM.getFirstChild(tr); Element tdContent = DOM.getNextSibling(tdImg); // Undoes padding from table element. DOM.setStyleAttribute(item.getElement(), "padding", "0px"); DOM.appendChild(tdContent, item.contentElem); item.imageHolder = tdImg; } } /** * Setup clonable elements. */ void initializeClonableElements() { if (GWT.isClient()) { // Create the base table element that will be cloned. BASE_INTERNAL_ELEM = DOM.createTable(); Element contentElem = DOM.createDiv(); Element tbody = DOM.createTBody(), tr = DOM.createTR(); Element tdImg = DOM.createTD(), tdContent = DOM.createTD(); DOM.appendChild(BASE_INTERNAL_ELEM, tbody); DOM.appendChild(tbody, tr); DOM.appendChild(tr, tdImg); DOM.appendChild(tr, tdContent); DOM.setStyleAttribute(tdImg, "verticalAlign", "middle"); DOM.setStyleAttribute(tdContent, "verticalAlign", "middle"); DOM.appendChild(tdContent, contentElem); DOM.setStyleAttribute(contentElem, "display", "inline"); setStyleName(contentElem, "gwt-TreeItem"); DOM.setStyleAttribute(BASE_INTERNAL_ELEM, "whiteSpace", "nowrap"); // Create the base element that will be cloned BASE_BARE_ELEM = DOM.createDiv(); // Simulates padding from table element. DOM.setStyleAttribute(BASE_BARE_ELEM, "padding", "3px"); DOM.appendChild(BASE_BARE_ELEM, contentElem); Accessibility.setRole(contentElem, Accessibility.ROLE_TREEITEM); } } } /** * IE specific implementation class for {@link TreeItem}. */ public static class TreeItemImplIE6 extends TreeItemImpl { @Override void convertToFullNode(TreeItem item) { super.convertToFullNode(item); DOM.setStyleAttribute(item.getElement(), "marginBottom", "0px"); } } /** * An {@link Animation} used to open the child elements. If a {@link TreeItem} * is in the process of opening, it will immediately be opened and the new * {@link TreeItem} will use this animation. */ private static class TreeItemAnimation extends Animation { /** * The {@link TreeItem} currently being affected. */ private TreeItem curItem = null; /** * Whether the item is being opened or closed. */ private boolean opening = true; /** * The target height of the child items. */ private int scrollHeight = 0; /** * Open the specified {@link TreeItem}. * * @param item the {@link TreeItem} to open * @param animate true to animate, false to open instantly */ public void setItemState(TreeItem item, boolean animate) { // Immediately complete previous open cancel(); // Open the new item if (animate) { curItem = item; opening = item.open; run(Math.min(ANIMATION_DURATION, ANIMATION_DURATION_PER_ITEM * curItem.getChildCount())); } else { UIObject.setVisible(item.childSpanElem, item.open); } } @Override protected void onComplete() { if (curItem != null) { if (opening) { UIObject.setVisible(curItem.childSpanElem, true); onUpdate(1.0); DOM.setStyleAttribute(curItem.childSpanElem, "height", "auto"); } else { UIObject.setVisible(curItem.childSpanElem, false); } DOM.setStyleAttribute(curItem.childSpanElem, "overflow", "visible"); DOM.setStyleAttribute(curItem.childSpanElem, "width", "auto"); curItem = null; } } @Override protected void onStart() { scrollHeight = 0; // If the TreeItem is already open, we can get its scrollHeight // immediately. if (!opening) { scrollHeight = curItem.childSpanElem.getScrollHeight(); } DOM.setStyleAttribute(curItem.childSpanElem, "overflow", "hidden"); // If the TreeItem is already open, onStart will set its height to its // natural height. If the TreeItem is currently closed, onStart will set // its height to 1px (see onUpdate below), and then we make the TreeItem // visible so we can get its correct scrollHeight. super.onStart(); // If the TreeItem is currently closed, we need to make it visible before // we can get its height. if (opening) { UIObject.setVisible(curItem.childSpanElem, true); scrollHeight = curItem.childSpanElem.getScrollHeight(); } } @Override protected void onUpdate(double progress) { int height = (int) (progress * scrollHeight); if (!opening) { height = scrollHeight - height; } // Issue 2338: If the height is 0px, IE7 will display all of the children // instead of hiding them completely. height = Math.max(height, 1); DOM.setStyleAttribute(curItem.childSpanElem, "height", height + "px"); // We need to set the width explicitly of the item might be cropped int scrollWidth = DOM.getElementPropertyInt(curItem.childSpanElem, "scrollWidth"); DOM.setStyleAttribute(curItem.childSpanElem, "width", scrollWidth + "px"); } } // By not overwriting the default tree padding and spacing, we traditionally // added 7 pixels between our image and content. // <2>|<1>image<1>|<2>|<1>content // So to preserve the current spacing we must add a 7 pixel pad when no image // is supplied. static final int IMAGE_PAD = 7; /** * The duration of the animation. */ private static final int ANIMATION_DURATION = 200; /** * The duration of the animation per child {@link TreeItem}. If the per item * duration times the number of child items is less than the duration above, * the smaller duration will be used. */ private static final int ANIMATION_DURATION_PER_ITEM = 75; /** * The static animation used to open {@link TreeItem TreeItems}. */ private static TreeItemAnimation itemAnimation = new TreeItemAnimation(); /** * The structured table to hold images. */ private static Element BASE_INTERNAL_ELEM; /** * The base tree item element that will be cloned. */ private static Element BASE_BARE_ELEM; private static TreeItemImpl impl = GWT.create(TreeItemImpl.class); private ArrayList<TreeItem> children; private Element contentElem, childSpanElem, imageHolder; /** * Indicates that this item is a root item in a tree. */ private boolean isRoot; private boolean open; private TreeItem parent; private boolean selected; private Object userObject; private Tree tree; private Widget widget; /** * Creates an empty tree item. */ public TreeItem() { this(false); } /** * Constructs a tree item with the given HTML. * * @param html the item's HTML */ public TreeItem(String html) { this(); setHTML(html); } /** * Constructs a tree item with the given HTML. * * @param html the item's HTML */ public TreeItem(SafeHtml html) { this(html.asString()); } /** * Constructs a tree item with the given <code>Widget</code>. * * @param widget the item's widget */ public TreeItem(Widget widget) { this(); setWidget(widget); } /** * Creates an empty tree item. * * @param isRoot true if this item is the root of a tree */ TreeItem(boolean isRoot) { this.isRoot = isRoot; Element elem = DOM.clone(BASE_BARE_ELEM, true); setElement(elem); contentElem = DOM.getFirstChild(elem); DOM.setElementAttribute(contentElem, "id", DOM.createUniqueId()); // The root item always has children. if (isRoot) { initChildren(); } } /** * Adds a child tree item containing the specified html. * * @param itemHtml the text to be added * @return the item that was added */ public TreeItem addItem(String itemHtml) { TreeItem ret = new TreeItem(itemHtml); addItem(ret); return ret; } /** * Adds a child tree item containing the specified html. * * @param itemHtml the item's HTML * @return the item that was added */ public TreeItem addItem(SafeHtml itemHtml) { TreeItem ret = new TreeItem(itemHtml); addItem(ret); return ret; } /** * Adds another item as a child to this one. * * @param item the item to be added */ public void addItem(TreeItem item) { // If this is the item's parent, removing the item will affect the child // count. maybeRemoveItemFromParent(item); insertItem(getChildCount(), item); } /** * Adds another item as a child to this one. * * @param isItem the wrapper of item to be added */ public void addItem(IsTreeItem isItem) { TreeItem item = isItem.asTreeItem(); addItem(item); } /** * Adds a child tree item containing the specified widget. * * @param widget the widget to be added * @return the item that was added */ public TreeItem addItem(Widget widget) { TreeItem ret = new TreeItem(widget); addItem(ret); return ret; } /** * Adds a child tree item containing the specified text. * * @param itemText the text of the item to be added * @return the item that was added */ public TreeItem addTextItem(String itemText) { TreeItem ret = new TreeItem(); ret.setText(itemText); addItem(ret); return ret; } public TreeItem asTreeItem() { return this; } /** * Gets the child at the specified index. * * @param index the index to be retrieved * @return the item at that index */ public TreeItem getChild(int index) { if ((index < 0) || (index >= getChildCount())) { return null; } return children.get(index); } /** * Gets the number of children contained in this item. * * @return this item's child count. */ public int getChildCount() { if (children == null) { return 0; } return children.size(); } /** * Gets the index of the specified child item. * * @param child the child item to be found * @return the child's index, or <code>-1</code> if none is found */ public int getChildIndex(TreeItem child) { if (children == null) { return -1; } return children.indexOf(child); } public String getHTML() { return DOM.getInnerHTML(contentElem); } /** * Gets this item's parent. * * @return the parent item */ public TreeItem getParentItem() { return parent; } /** * Gets whether this item's children are displayed. * * @return <code>true</code> if the item is open */ public boolean getState() { return open; } public String getText() { return DOM.getInnerText(contentElem); } /** * Gets the tree that contains this item. * * @return the containing tree */ public final Tree getTree() { return tree; } /** * Gets the user-defined object associated with this item. * * @return the item's user-defined object */ public Object getUserObject() { return userObject; } /** * Gets the <code>Widget</code> associated with this tree item. * * @return the widget */ public Widget getWidget() { return widget; } /** * Inserts a child tree item at the specified index containing the specified * text. * * @param beforeIndex the index where the item will be inserted * @param itemText the text to be added * @return the item that was added * @throws IndexOutOfBoundsException if the index is out of range */ public TreeItem insertItem(int beforeIndex, String itemText) throws IndexOutOfBoundsException { TreeItem ret = new TreeItem(itemText); insertItem(beforeIndex, ret); return ret; } /** * Inserts a child tree item at the specified index containing the specified * text. * * @param beforeIndex the index where the item will be inserted * @param itemHtml the item's HTML * @return the item that was added * @throws IndexOutOfBoundsException if the index is out of range */ public TreeItem insertItem(int beforeIndex, SafeHtml itemHtml) throws IndexOutOfBoundsException { TreeItem ret = new TreeItem(itemHtml); insertItem(beforeIndex, ret); return ret; } /** * Inserts an item as a child to this one. * * @param beforeIndex the index where the item will be inserted * @param item the item to be added * @throws IndexOutOfBoundsException if the index is out of range */ public void insertItem(int beforeIndex, TreeItem item) throws IndexOutOfBoundsException { // Detach item from existing parent. maybeRemoveItemFromParent(item); // Check the index after detaching in case this item was already the parent. int childCount = getChildCount(); if (beforeIndex < 0 || beforeIndex > childCount) { throw new IndexOutOfBoundsException(); } if (children == null) { initChildren(); } // Set the margin. // Use no margin on top-most items. double margin = isRoot ? 0.0 : CHILD_MARGIN; if (LocaleInfo.getCurrentLocale().isRTL()) { item.getElement().getStyle().setMarginRight(margin, Unit.PX); } else { item.getElement().getStyle().setMarginLeft(margin, Unit.PX); } // Physical attach. Element childContainer = isRoot ? tree.getElement() : childSpanElem; if (beforeIndex == childCount) { childContainer.appendChild(item.getElement()); } else { Element beforeElem = getChild(beforeIndex).getElement(); childContainer.insertBefore(item.getElement(), beforeElem); } // Logical attach. // Explicitly set top-level items' parents to null if this is root. item.setParentItem(isRoot ? null : this); children.add(beforeIndex, item); // Adopt. item.setTree(tree); if (!isRoot && children.size() == 1) { updateState(false, false); } } /** * Inserts a child tree item at the specified index containing the specified * widget. * * @param beforeIndex the index where the item will be inserted * @param widget the widget to be added * @return the item that was added * @throws IndexOutOfBoundsException if the index is out of range */ public TreeItem insertItem(int beforeIndex, Widget widget) throws IndexOutOfBoundsException { TreeItem ret = new TreeItem(widget); insertItem(beforeIndex, ret); return ret; } /** * Determines whether this item is currently selected. * * @return <code>true</code> if it is selected */ public boolean isSelected() { return selected; } /** * Removes this item from its tree. */ public void remove() { if (parent != null) { // If this item has a parent, remove self from it. parent.removeItem(this); } else if (tree != null) { // If the item has no parent, but is in the Tree, it must be a top-level // element. tree.removeItem(this); } } /** * Removes one of this item's children. * * @param item the item to be removed */ public void removeItem(TreeItem item) { // Validate. if (children == null || !children.contains(item)) { return; } // Orphan. Tree oldTree = tree; item.setTree(null); // Physical detach. if (isRoot) { oldTree.getElement().removeChild(item.getElement()); } else { childSpanElem.removeChild(item.getElement()); } // Logical detach. item.setParentItem(null); children.remove(item); if (!isRoot && children.size() == 0) { updateState(false, false); } } /** * Removes one of this item's children. * * @param isItem the wrapper of item to be removed */ public void removeItem(IsTreeItem isItem) { if (isItem != null) { TreeItem item = isItem.asTreeItem(); removeItem(item); } } /** * Removes all of this item's children. */ public void removeItems() { while (getChildCount() > 0) { removeItem(getChild(0)); } } public void setHTML(String html) { setWidget(null); DOM.setInnerHTML(contentElem, html); } public void setHTML(SafeHtml html) { setHTML(html.asString()); } /** * Selects or deselects this item. * * @param selected <code>true</code> to select the item, <code>false</code> to * deselect it */ public void setSelected(boolean selected) { if (this.selected == selected) { return; } this.selected = selected; setStyleName(getContentElem(), "gwt-TreeItem-selected", selected); } /** * Sets whether this item's children are displayed. * * @param open whether the item is open */ public void setState(boolean open) { setState(open, true); } /** * Sets whether this item's children are displayed. * * @param open whether the item is open * @param fireEvents <code>true</code> to allow open/close events to be */ public void setState(boolean open, boolean fireEvents) { if (open && getChildCount() == 0) { return; } // Only do the physical update if it changes if (this.open != open) { this.open = open; updateState(true, true); if (fireEvents && tree != null) { tree.fireStateChanged(this, open); } } } public void setText(String text) { setWidget(null); DOM.setInnerText(contentElem, text); } /** * Sets the user-defined object associated with this item. * * @param userObj the item's user-defined object */ public void setUserObject(Object userObj) { userObject = userObj; } /** * Sets the current widget. Any existing child widget will be removed. * * @param newWidget Widget to set */ public void setWidget(Widget newWidget) { // Detach new child from old parent. if (newWidget != null) { newWidget.removeFromParent(); } // Detach old child from tree. if (widget != null) { try { if (tree != null) { tree.orphan(widget); } } finally { // Physical detach old child. contentElem.removeChild(widget.getElement()); widget = null; } } // Clear out any existing content before adding a widget. DOM.setInnerHTML(contentElem, ""); // Logical detach old/attach new. widget = newWidget; if (newWidget != null) { // Physical attach new. DOM.appendChild(contentElem, newWidget.getElement()); // Attach child to tree. if (tree != null) { tree.adopt(widget, this); } // Set tabIndex on the widget to -1, so that it doesn't mess up the tab // order of the entire tree if (Tree.shouldTreeDelegateFocusToElement(widget.getElement())) { DOM.setElementAttribute(widget.getElement(), "tabIndex", "-1"); } } } /** * Returns a suggested {@link Focusable} instance to use when this tree item * is selected. The tree maintains focus if this method returns null. By * default, if the tree item contains a focusable widget, that widget is * returned. * * Note, the {@link Tree} will ignore this value if the user clicked on an * input element such as a button or text area when selecting this item. * * @return the focusable item */ protected Focusable getFocusable() { Focusable focus = getFocusableWidget(); if (focus == null) { Widget w = getWidget(); if (w instanceof Focusable) { focus = (Focusable) w; } } return focus; } /** * Returns the widget, if any, that should be focused on if this TreeItem is * selected. * * @return widget to be focused. * @deprecated use {@link #getFocusable()} instead */ @Deprecated protected HasFocus getFocusableWidget() { Widget w = getWidget(); if (w instanceof HasFocus) { return (HasFocus) w; } else { return null; } } /** * <b>Affected Elements:</b> * <ul> * <li>-content = The text or {@link Widget} next to the image.</li> * <li>-child# = The child at the specified index.</li> * </ul> * * @see UIObject#onEnsureDebugId(String) */ @Override protected void onEnsureDebugId(String baseID) { super.onEnsureDebugId(baseID); ensureDebugId(contentElem, baseID, "content"); if (imageHolder != null) { // The image itself may or may not exist. ensureDebugId(imageHolder, baseID, "image"); } if (children != null) { int childCount = 0; for (TreeItem child : children) { child.ensureDebugId(baseID + "-child" + childCount); childCount++; } } } void addTreeItems(List<TreeItem> accum) { int size = getChildCount(); for (int i = 0; i < size; i++) { TreeItem item = children.get(i); accum.add(item); item.addTreeItems(accum); } } ArrayList<TreeItem> getChildren() { return children; } Element getContentElem() { return contentElem; } Element getImageElement() { return DOM.getFirstChild(getImageHolderElement()); } Element getImageHolderElement() { if (!isFullNode()) { convertToFullNode(); } return imageHolder; } void initChildren() { convertToFullNode(); childSpanElem = DOM.createDiv(); DOM.appendChild(getElement(), childSpanElem); DOM.setStyleAttribute(childSpanElem, "whiteSpace", "nowrap"); children = new ArrayList<TreeItem>(); } boolean isFullNode() { return imageHolder != null; } /** * Remove a tree item from its parent if it has one. * * @param item the tree item to remove from its parent */ void maybeRemoveItemFromParent(TreeItem item) { if ((item.getParentItem() != null) || (item.getTree() != null)) { item.remove(); } } void setParentItem(TreeItem parent) { this.parent = parent; } void setTree(Tree newTree) { // Early out. if (tree == newTree) { return; } // Remove this item from existing tree. if (tree != null) { if (tree.getSelectedItem() == this) { tree.setSelectedItem(null); } if (widget != null) { tree.orphan(widget); } } tree = newTree; for (int i = 0, n = getChildCount(); i < n; ++i) { children.get(i).setTree(newTree); } updateState(false, true); if (newTree != null) { if (widget != null) { // Add my widget to the new tree. newTree.adopt(widget, this); } } } void updateState(boolean animate, boolean updateTreeSelection) { // If the tree hasn't been set, there is no visual state to update. // If the tree is not attached, then update will be called on attach. if (tree == null || tree.isAttached() == false) { return; } if (getChildCount() == 0) { if (childSpanElem != null) { UIObject.setVisible(childSpanElem, false); } tree.showLeafImage(this); return; } // We must use 'display' rather than 'visibility' here, // or the children will always take up space. if (animate && (tree != null) && (tree.isAttached())) { itemAnimation.setItemState(this, tree.isAnimationEnabled()); } else { itemAnimation.setItemState(this, false); } // Change the status image if (open) { tree.showOpenImage(this); } else { tree.showClosedImage(this); } // We may need to update the tree's selection in response to a tree state // change. For example, if the tree's currently selected item is a // descendant of an item whose branch was just collapsed, then the item // itself should become the newly-selected item. if (updateTreeSelection) { tree.maybeUpdateSelection(this, this.open); } } void updateStateRecursive() { updateStateRecursiveHelper(); tree.maybeUpdateSelection(this, this.open); } private void convertToFullNode() { impl.convertToFullNode(this); } private void updateStateRecursiveHelper() { updateState(false, false); for (int i = 0, n = getChildCount(); i < n; ++i) { children.get(i).updateStateRecursiveHelper(); } } }