| /* |
| * Copyright 2010 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.cellview.client; |
| |
| import com.google.gwt.cell.client.Cell; |
| import com.google.gwt.cell.client.Cell.Context; |
| import com.google.gwt.core.client.GWT; |
| import com.google.gwt.core.client.Scheduler; |
| import com.google.gwt.dom.client.AnchorElement; |
| import com.google.gwt.dom.client.Document; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.dom.client.NativeEvent; |
| import com.google.gwt.dom.client.Style.Display; |
| import com.google.gwt.dom.client.Style.Overflow; |
| import com.google.gwt.event.logical.shared.CloseEvent; |
| import com.google.gwt.event.logical.shared.OpenEvent; |
| import com.google.gwt.event.shared.EventHandler; |
| import com.google.gwt.event.shared.GwtEvent; |
| import com.google.gwt.event.shared.GwtEvent.Type; |
| import com.google.gwt.event.shared.HandlerManager; |
| import com.google.gwt.event.shared.HandlerRegistration; |
| import com.google.gwt.i18n.client.LocaleInfo; |
| import com.google.gwt.safehtml.client.SafeHtmlTemplates; |
| import com.google.gwt.safehtml.shared.SafeHtml; |
| import com.google.gwt.safehtml.shared.SafeHtmlBuilder; |
| import com.google.gwt.safehtml.shared.SafeHtmlUtils; |
| import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy; |
| import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState; |
| import com.google.gwt.user.client.ui.UIObject; |
| import com.google.gwt.user.client.ui.impl.FocusImpl; |
| import com.google.gwt.view.client.CellPreviewEvent; |
| import com.google.gwt.view.client.CellPreviewEvent.Handler; |
| import com.google.gwt.view.client.HasData; |
| import com.google.gwt.view.client.ProvidesKey; |
| import com.google.gwt.view.client.Range; |
| import com.google.gwt.view.client.RangeChangeEvent; |
| import com.google.gwt.view.client.RowCountChangeEvent; |
| import com.google.gwt.view.client.SelectionModel; |
| import com.google.gwt.view.client.TreeViewModel; |
| import com.google.gwt.view.client.TreeViewModel.NodeInfo; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * A view of a tree node. |
| * |
| * @param <T> the type that this view contains |
| */ |
| // TODO(jlabanca): Convert this to be the type of the child and create lazily. |
| class CellTreeNodeView<T> extends UIObject { |
| |
| interface Template extends SafeHtmlTemplates { |
| @Template("<div onclick=\"\" style=\"position:relative;padding-{0}:{1}px;" |
| + "\" class=\"{2}\">{3}<div class=\"{4}\">{5}</div></div>") |
| SafeHtml innerDiv(String paddingDirection, int imageWidth, String classes, |
| SafeHtml image, String itemValueStyle, SafeHtml cellContents); |
| |
| @Template("<div><div style=\"padding-{0}:{1}px;\" class=\"{2}\">{3}</div></div>") |
| SafeHtml outerDiv(String paddingDirection, int paddingAmount, |
| String classes, SafeHtml content); |
| } |
| |
| private static final Template template = GWT.create(Template.class); |
| |
| /** |
| * The {@link com.google.gwt.view.client.HasData} used to show children. This |
| * class is intentionally static because we might move it to a new |
| * {@link CellTreeNodeView}, and we don't want non-static references to the |
| * old {@link CellTreeNodeView}. |
| * |
| * @param <C> the child item type |
| */ |
| private static class NodeCellList<C> implements HasData<C> { |
| |
| /** |
| * The view used by the NodeCellList. |
| */ |
| private class View implements HasDataPresenter.View<C> { |
| |
| private final Element childContainer; |
| |
| public View(Element childContainer) { |
| this.childContainer = childContainer; |
| } |
| |
| public <H extends EventHandler> HandlerRegistration addHandler(H handler, |
| Type<H> type) { |
| return handlerManger.addHandler(type, handler); |
| } |
| |
| public void render(SafeHtmlBuilder sb, List<C> values, int start, |
| SelectionModel<? super C> selectionModel) { |
| // Cache the style names that will be used for each child. |
| CellTree.Style style = nodeView.tree.getStyle(); |
| String itemValueStyle = style.cellTreeItemValue(); |
| String selectedStyle = " " + style.cellTreeSelectedItem(); |
| String itemStyle = style.cellTreeItem(); |
| String itemImageValueStyle = " " + style.cellTreeItemImageValue(); |
| String openStyle = " " + style.cellTreeOpenItem(); |
| String topStyle = " " + style.cellTreeTopItem(); |
| String topImageValueStyle = " " + style.cellTreeTopItemImageValue(); |
| boolean isRootNode = nodeView.isRootNode(); |
| SafeHtml openImage = nodeView.tree.getOpenImageHtml(isRootNode); |
| SafeHtml closedImage = nodeView.tree.getClosedImageHtml(isRootNode); |
| int imageWidth = nodeView.tree.getImageWidth(); |
| String paddingDirection = LocaleInfo.getCurrentLocale().isRTL() |
| ? "right" : "left"; |
| int paddingAmount = imageWidth * nodeView.depth; |
| |
| // Create a set of currently open nodes. |
| Set<Object> openNodes = new HashSet<Object>(); |
| int childCount = nodeView.getChildCount(); |
| int end = start + values.size(); |
| for (int i = start; i < end && i < childCount; i++) { |
| CellTreeNodeView<?> child = nodeView.getChildNode(i); |
| // Ignore child nodes that are closed. |
| if (child.isOpen()) { |
| openNodes.add(child.getValueKey()); |
| } |
| } |
| |
| // Render the child nodes. |
| ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey(); |
| TreeViewModel model = nodeView.tree.getTreeViewModel(); |
| for (int i = start; i < end; i++) { |
| C value = values.get(i - start); |
| Object key = keyProvider.getKey(value); |
| boolean isOpen = openNodes.contains(key); |
| |
| // Outer div contains image, value, and children (when open) |
| StringBuilder outerClasses = new StringBuilder(itemStyle); |
| if (isOpen) { |
| outerClasses.append(openStyle); |
| } |
| if (isRootNode) { |
| outerClasses.append(topStyle); |
| } |
| if (selectionModel != null && selectionModel.isSelected(value)) { |
| outerClasses.append(selectedStyle); |
| } |
| |
| // Inner div contains image and value |
| StringBuilder innerClasses = new StringBuilder(itemStyle); |
| innerClasses.append(itemImageValueStyle); |
| if (isRootNode) { |
| innerClasses.append(topImageValueStyle); |
| } |
| // Add the open/close icon. |
| SafeHtml image; |
| if (isOpen) { |
| image = openImage; |
| } else if (model.isLeaf(value)) { |
| image = LEAF_IMAGE; |
| } else { |
| image = closedImage; |
| } |
| // Render cell contents |
| SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder(); |
| Context context = new Context(i, 0, key); |
| cell.render(context, value, cellBuilder); |
| |
| SafeHtml innerDiv = template.innerDiv(paddingDirection, imageWidth, |
| innerClasses.toString(), image, itemValueStyle, |
| cellBuilder.toSafeHtml()); |
| |
| sb.append(template.outerDiv(paddingDirection, paddingAmount, |
| outerClasses.toString(), innerDiv)); |
| } |
| } |
| |
| public void replaceAllChildren(List<C> values, SafeHtml html, |
| boolean stealFocus) { |
| // Hide the child container so we can animate it. |
| if (nodeView.tree.isAnimationEnabled()) { |
| nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE); |
| } |
| |
| // Replace the child nodes. |
| nodeView.tree.isRefreshing = true; |
| Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0); |
| AbstractHasData.replaceAllChildren(nodeView.tree, childContainer, html); |
| nodeView.tree.isRefreshing = false; |
| |
| // Trim the list of children. |
| int size = values.size(); |
| int childCount = nodeView.children.size(); |
| while (childCount > size) { |
| childCount--; |
| nodeView.children.remove(childCount); |
| } |
| |
| // Reattach the open nodes. |
| loadChildState(values, 0, savedViews); |
| |
| // If this is the root node, move keyboard focus to the first child. |
| if (nodeView.isRootNode() |
| && nodeView.tree.getKeyboardSelectedNode() == nodeView |
| && values.size() > 0) { |
| nodeView.tree.keyboardSelect(nodeView.children.get(0), false); |
| } |
| |
| // Animate the child container open. |
| if (nodeView.tree.isAnimationEnabled()) { |
| nodeView.tree.maybeAnimateTreeNode(nodeView); |
| } |
| } |
| |
| public void replaceChildren(List<C> values, int start, SafeHtml html, |
| boolean stealFocus) { |
| Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, |
| start); |
| |
| nodeView.tree.isRefreshing = true; |
| Element newChildren = AbstractHasData.convertToElements(nodeView.tree, |
| getTmpElem(), html); |
| AbstractHasData.replaceChildren(nodeView.tree, childContainer, |
| newChildren, start, html); |
| nodeView.tree.isRefreshing = false; |
| |
| loadChildState(values, start, savedViews); |
| } |
| |
| public void resetFocus() { |
| nodeView.tree.resetFocus(); |
| } |
| |
| public void setKeyboardSelected(int index, boolean selected, |
| boolean stealFocus) { |
| // Keyboard selection is handled by CellTree. |
| Element elem = childContainer.getChild(index).cast(); |
| setStyleName(getSelectionElement(elem), |
| nodeView.tree.getStyle().cellTreeKeyboardSelectedItem(), selected); |
| } |
| |
| public void setLoadingState(LoadingState state) { |
| nodeView.updateImage(state == LoadingState.LOADING); |
| showOrHide(nodeView.emptyMessageElem, state == LoadingState.LOADED |
| && presenter.isEmpty()); |
| } |
| |
| /** |
| * Reload the open children after rendering new items in this node. |
| * |
| * @param values the values being replaced |
| * @param start the start index |
| * @param savedViews the open nodes |
| */ |
| private void loadChildState(List<C> values, int start, |
| Map<Object, CellTreeNodeView<?>> savedViews) { |
| int len = values.size(); |
| int end = start + len; |
| int childCount = nodeView.getChildCount(); |
| ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey(); |
| |
| Element container = nodeView.ensureChildContainer(); |
| Element childElem = container.getChild(start).cast(); |
| CellTreeNodeView<?> keyboardSelected = nodeView.tree.getKeyboardSelectedNode(); |
| for (int i = start; i < end; i++) { |
| C childValue = values.get(i - start); |
| CellTreeNodeView<C> child = nodeView.createTreeNodeView(nodeInfo, |
| childElem, childValue, null); |
| CellTreeNodeView<?> savedChild = savedViews.remove(keyProvider.getKey(childValue)); |
| // Copy the saved child's state into the new child |
| if (savedChild != null) { |
| child.animationFrame = savedChild.animationFrame; |
| child.contentContainer = savedChild.contentContainer; |
| child.childContainer = savedChild.childContainer; |
| child.children = savedChild.children; |
| child.emptyMessageElem = savedChild.emptyMessageElem; |
| child.nodeInfo = savedChild.nodeInfo; |
| child.nodeInfoLoaded = savedChild.nodeInfoLoaded; |
| child.open = savedChild.open; |
| child.showMoreElem = savedChild.showMoreElem; |
| |
| // Transfer the tree node so that if the user has a handle to it, it |
| // won't be destroyed. |
| child.treeNode = savedChild.treeNode; |
| if (child.treeNode != null) { |
| child.treeNode.nodeView = child; |
| } |
| |
| // Swap the node view in the child. We reuse the same NodeListView |
| // so that we don't have to unset and register a new view with the |
| // NodeInfo, which would inevitably cause the NodeInfo to push |
| // new data. |
| child.listView = savedChild.listView; |
| if (child.listView != null) { |
| child.listView.nodeView = child; |
| } |
| |
| // Set the new parent of the grandchildren. |
| if (child.children != null) { |
| for (CellTreeNodeView<?> grandchild : child.children) { |
| grandchild.parentNode = child; |
| } |
| } |
| |
| // Transfer the keyboard selected node. |
| if (keyboardSelected == savedChild) { |
| keyboardSelected = child; |
| } |
| |
| // Copy the child container element to the new child |
| child.getElement().appendChild(savedChild.ensureAnimationFrame()); |
| |
| // Mark the old child as destroy without actually destroying it. |
| savedChild.isDestroyed = true; |
| } |
| |
| if (childCount > i) { |
| nodeView.children.set(i, child); |
| } else { |
| nodeView.children.add(child); |
| } |
| childElem = childElem.getNextSiblingElement(); |
| } |
| |
| // Move the keyboard selected node if it is this node or a child of this |
| // node. |
| CellTreeNodeView<?> curNode = keyboardSelected; |
| while (curNode != null) { |
| if (curNode == nodeView) { |
| nodeView.tree.keyboardSelect(keyboardSelected, false); |
| break; |
| } |
| curNode = curNode.parentNode; |
| } |
| } |
| |
| /** |
| * Save the state of the open child nodes within the range of the |
| * specified values. Use {@link #loadChildState(List, int, Map)} to |
| * re-attach the open nodes after they have been replaced. |
| * |
| * @param values the values being replaced |
| * @param start the start index |
| * @return the map of open nodes |
| */ |
| private Map<Object, CellTreeNodeView<?>> saveChildState(List<C> values, |
| int start) { |
| // Ensure that we have a children array. |
| if (nodeView.children == null) { |
| nodeView.children = new ArrayList<CellTreeNodeView<?>>(); |
| } |
| |
| // Construct a map of former child views based on their value keys. |
| int len = values.size(); |
| int end = start + len; |
| int childCount = nodeView.getChildCount(); |
| CellTreeNodeView<?> keyboardSelected = nodeView.tree.getKeyboardSelectedNode(); |
| Map<Object, CellTreeNodeView<?>> openNodes = new HashMap<Object, CellTreeNodeView<?>>(); |
| for (int i = start; i < end && i < childCount; i++) { |
| CellTreeNodeView<?> child = nodeView.getChildNode(i); |
| if (child.isOpen() || child == keyboardSelected) { |
| // Save child nodes that are open or keyboard selected. |
| openNodes.put(child.getValueKey(), child); |
| } else { |
| // Cleanup child nodes that are closed. |
| child.cleanup(true); |
| } |
| } |
| |
| // Trim the saved views down to the children that still exists. |
| ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey(); |
| Map<Object, CellTreeNodeView<?>> savedViews = new HashMap<Object, CellTreeNodeView<?>>(); |
| for (C childValue : values) { |
| // Remove any child elements that correspond to prior children |
| // so the call to setInnerHtml will not destroy them |
| Object key = keyProvider.getKey(childValue); |
| CellTreeNodeView<?> savedView = openNodes.remove(key); |
| if (savedView != null) { |
| savedView.ensureAnimationFrame().removeFromParent(); |
| savedViews.put(key, savedView); |
| } |
| } |
| |
| // Cleanup the remaining open nodes that are not in the new data set. |
| for (CellTreeNodeView<?> lostNode : openNodes.values()) { |
| lostNode.cleanup(true); |
| } |
| |
| return savedViews; |
| } |
| } |
| |
| private final Cell<C> cell; |
| private final int defaultPageSize; |
| private HandlerManager handlerManger = new HandlerManager(this); |
| private final NodeInfo<C> nodeInfo; |
| private CellTreeNodeView<?> nodeView; |
| private final HasDataPresenter<C> presenter; |
| |
| public NodeCellList(final NodeInfo<C> nodeInfo, |
| final CellTreeNodeView<?> nodeView, int pageSize) { |
| this.defaultPageSize = pageSize; |
| this.nodeInfo = nodeInfo; |
| this.nodeView = nodeView; |
| cell = nodeInfo.getCell(); |
| |
| // Create a presenter. |
| presenter = new HasDataPresenter<C>(this, new View( |
| nodeView.ensureChildContainer()), pageSize, nodeInfo.getProvidesKey()); |
| |
| // Disable keyboard selection because it is handled by CellTree. |
| presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED); |
| |
| // Use a pager to update buttons. |
| presenter.addRowCountChangeHandler(new RowCountChangeEvent.Handler() { |
| public void onRowCountChange(RowCountChangeEvent event) { |
| int rowCount = event.getNewRowCount(); |
| boolean isExact = event.isNewRowCountExact(); |
| int pageSize = getVisibleRange().getLength(); |
| showOrHide(nodeView.showMoreElem, isExact && rowCount > pageSize); |
| } |
| }); |
| } |
| |
| public HandlerRegistration addCellPreviewHandler(Handler<C> handler) { |
| return presenter.addCellPreviewHandler(handler); |
| } |
| |
| public HandlerRegistration addRangeChangeHandler( |
| RangeChangeEvent.Handler handler) { |
| return presenter.addRangeChangeHandler(handler); |
| } |
| |
| public HandlerRegistration addRowCountChangeHandler( |
| RowCountChangeEvent.Handler handler) { |
| return presenter.addRowCountChangeHandler(handler); |
| } |
| |
| /** |
| * Cleanup this node view. |
| */ |
| public void cleanup() { |
| presenter.clearSelectionModel(); |
| } |
| |
| public void fireEvent(GwtEvent<?> event) { |
| handlerManger.fireEvent(event); |
| } |
| |
| public int getDefaultPageSize() { |
| return defaultPageSize; |
| } |
| |
| public int getRowCount() { |
| return presenter.getRowCount(); |
| } |
| |
| public SelectionModel<? super C> getSelectionModel() { |
| return presenter.getSelectionModel(); |
| } |
| |
| public C getVisibleItem(int indexOnPage) { |
| return presenter.getVisibleItem(indexOnPage); |
| } |
| |
| public int getVisibleItemCount() { |
| return presenter.getVisibleItemCount(); |
| } |
| |
| public List<C> getVisibleItems() { |
| return presenter.getVisibleItems(); |
| } |
| |
| public Range getVisibleRange() { |
| return presenter.getVisibleRange(); |
| } |
| |
| public boolean isRowCountExact() { |
| return presenter.isRowCountExact(); |
| } |
| |
| public final void setRowCount(int count) { |
| setRowCount(count, true); |
| } |
| |
| public void setRowCount(int size, boolean isExact) { |
| presenter.setRowCount(size, isExact); |
| } |
| |
| public void setRowData(int start, List<? extends C> values) { |
| presenter.setRowData(start, values); |
| } |
| |
| public void setSelectionModel(final SelectionModel<? super C> selectionModel) { |
| presenter.setSelectionModel(selectionModel); |
| } |
| |
| public final void setVisibleRange(int start, int length) { |
| setVisibleRange(new Range(start, length)); |
| } |
| |
| public void setVisibleRange(Range range) { |
| presenter.setVisibleRange(range); |
| } |
| |
| public void setVisibleRangeAndClearData(Range range, |
| boolean forceRangeChangeEvent) { |
| presenter.setVisibleRangeAndClearData(range, forceRangeChangeEvent); |
| } |
| } |
| |
| /** |
| * An implementation of {@link TreeNode} that delegates to a |
| * {@link CellTreeNodeView}. This class is intentionally static because we |
| * might move it to a new {@link CellTreeNodeView}, and we don't want |
| * non-static references to the old {@link CellTreeNodeView}. |
| */ |
| private static class TreeNodeImpl implements TreeNode { |
| |
| private CellTreeNodeView<?> nodeView; |
| |
| public TreeNodeImpl(CellTreeNodeView<?> nodeView) { |
| this.nodeView = nodeView; |
| } |
| |
| public int getChildCount() { |
| assertNotDestroyed(); |
| flush(); |
| return nodeView.getChildCount(); |
| } |
| |
| public Object getChildValue(int index) { |
| assertNotDestroyed(); |
| checkChildBounds(index); |
| flush(); |
| return nodeView.getChildNode(index).value; |
| } |
| |
| public int getIndex() { |
| assertNotDestroyed(); |
| return (nodeView.parentNode == null) ? 0 |
| : nodeView.parentNode.children.indexOf(nodeView); |
| } |
| |
| public TreeNode getParent() { |
| assertNotDestroyed(); |
| return getParentImpl(); |
| } |
| |
| public Object getValue() { |
| return nodeView.value; |
| } |
| |
| public boolean isChildLeaf(int index) { |
| assertNotDestroyed(); |
| checkChildBounds(index); |
| flush(); |
| return nodeView.getChildNode(index).isLeaf(); |
| } |
| |
| public boolean isChildOpen(int index) { |
| assertNotDestroyed(); |
| checkChildBounds(index); |
| flush(); |
| return nodeView.getChildNode(index).isOpen(); |
| } |
| |
| public boolean isDestroyed() { |
| if (!nodeView.isDestroyed) { |
| /* |
| * Flush the parent display because the user may have replaced this |
| * node, which would destroy it. |
| */ |
| TreeNodeImpl parent = getParentImpl(); |
| if (parent != null && !parent.isDestroyed()) { |
| parent.flush(); |
| } |
| } |
| return nodeView.isDestroyed || !nodeView.isOpen(); |
| } |
| |
| public TreeNode setChildOpen(int index, boolean open) { |
| return setChildOpen(index, open, true); |
| } |
| |
| public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) { |
| assertNotDestroyed(); |
| checkChildBounds(index); |
| CellTreeNodeView<?> child = nodeView.getChildNode(index); |
| return child.setOpen(open, fireEvents) ? child.treeNode : null; |
| } |
| |
| /** |
| * Assert that the node has not been destroyed. |
| */ |
| private void assertNotDestroyed() { |
| if (isDestroyed()) { |
| throw new IllegalStateException("TreeNode no longer exists."); |
| } |
| } |
| |
| /** |
| * Check the child bounds. |
| * |
| * @param index the index of the child |
| * @throws IndexOutOfBoundsException if the child is not in range |
| */ |
| private void checkChildBounds(int index) { |
| if ((index < 0) || (index >= getChildCount())) { |
| throw new IndexOutOfBoundsException(); |
| } |
| } |
| |
| /** |
| * Flush pending changes in the view. |
| */ |
| private void flush() { |
| if (nodeView.listView != null) { |
| nodeView.listView.presenter.flush(); |
| } |
| } |
| |
| /** |
| * Get the parent node without checking if this node is destroyed. |
| * |
| * @return the parent node, or null if the node has no parent |
| */ |
| private TreeNodeImpl getParentImpl() { |
| return nodeView.isRootNode() ? null : nodeView.parentNode.treeNode; |
| } |
| } |
| |
| /** |
| * The element used in place of an image when a node has no children. |
| */ |
| private static final SafeHtml LEAF_IMAGE = SafeHtmlUtils.fromSafeConstant("<div style='position:absolute;display:none;'></div>"); |
| |
| /** |
| * The temporary element used to render child items. |
| */ |
| private static com.google.gwt.user.client.Element tmpElem; |
| |
| /** |
| * Returns the element that parents the cell contents of the node. |
| * |
| * @param nodeElem the element that represents the node |
| * @return the cell parent within the node |
| */ |
| private static Element getCellParent(Element nodeElem) { |
| return getSelectionElement(nodeElem).getFirstChildElement().getChild(1).cast(); |
| } |
| |
| /** |
| * Returns the element that selection is applied to. |
| * |
| * @param nodeElem the element that represents the node |
| * @return the cell parent within the node |
| */ |
| private static Element getImageElement(Element nodeElem) { |
| return getSelectionElement(nodeElem).getFirstChildElement().getFirstChildElement(); |
| } |
| |
| /** |
| * Returns the element that selection is applied to. |
| * |
| * @param nodeElem the element that represents the node |
| * @return the cell parent within the node |
| */ |
| private static Element getSelectionElement(Element nodeElem) { |
| return nodeElem.getFirstChildElement(); |
| } |
| |
| /** |
| * Return the temporary element used to create elements. |
| */ |
| private static com.google.gwt.user.client.Element getTmpElem() { |
| if (tmpElem == null) { |
| tmpElem = Document.get().createDivElement().cast(); |
| } |
| return tmpElem; |
| } |
| |
| /** |
| * Show or hide an element. |
| * |
| * @param element the element to show or hide |
| * @param show true to show, false to hide |
| */ |
| private static void showOrHide(Element element, boolean show) { |
| if (show) { |
| element.getStyle().clearDisplay(); |
| } else { |
| element.getStyle().setDisplay(Display.NONE); |
| } |
| } |
| |
| /** |
| * True during the time a node should be animated. |
| */ |
| private boolean animate; |
| |
| /** |
| * A reference to the element that is used to animate nodes. Parent of the |
| * contentContainer. |
| */ |
| private Element animationFrame; |
| |
| /** |
| * A reference to the element that contains the children. Parent to the actual |
| * child nodes. |
| */ |
| private Element childContainer; |
| |
| /** |
| * A list of child views. |
| */ |
| private List<CellTreeNodeView<?>> children; |
| |
| /** |
| * A reference to the element that contains all content. Parent of the |
| * childContainer and the show/hide elements. |
| */ |
| private Element contentContainer; |
| |
| /** |
| * The depth of this node in the tree. |
| */ |
| private final int depth; |
| |
| /** |
| * The element used when there are no children to display. |
| */ |
| private Element emptyMessageElem; |
| |
| /** |
| * Set to true when the node is destroyed. |
| */ |
| private boolean isDestroyed; |
| |
| /** |
| * The list view used to display the nodes. |
| */ |
| private NodeCellList<?> listView; |
| |
| /** |
| * The info about children of this node. |
| */ |
| private NodeInfo<?> nodeInfo; |
| |
| /** |
| * Indicates whether or not we've loaded the node info. |
| */ |
| private boolean nodeInfoLoaded; |
| |
| /** |
| * Indicates whether or not this node is open. |
| */ |
| private boolean open; |
| |
| /** |
| * The parent {@link CellTreeNodeView}. |
| */ |
| private CellTreeNodeView<?> parentNode; |
| |
| /** |
| * The {@link NodeInfo} of the parent node. |
| */ |
| private final NodeInfo<T> parentNodeInfo; |
| |
| /** |
| * The element used to display more children. |
| */ |
| private AnchorElement showMoreElem; |
| |
| /** |
| * The {@link CellTree} that this node belongs to. |
| */ |
| private final CellTree tree; |
| |
| /** |
| * The publicly visible tree node. The {@link CellTreeNodeView} doesn't |
| * implement {@link TreeNode} directly because we want to transfer the user's |
| * handle to the {@link TreeNode} to the new {@link CellTreeNodeView}. |
| */ |
| private TreeNodeImpl treeNode; |
| |
| /** |
| * This node's value. |
| */ |
| private T value; |
| |
| /** |
| * Construct a {@link CellTreeNodeView}. |
| * |
| * @param tree the parent {@link CellTreeNodeView} |
| * @param parent the parent {@link CellTreeNodeView} |
| * @param parentNodeInfo the {@link NodeInfo} of the parent |
| * @param elem the outer element of this {@link CellTreeNodeView} |
| * @param value the value of this node |
| */ |
| CellTreeNodeView(final CellTree tree, final CellTreeNodeView<?> parent, |
| NodeInfo<T> parentNodeInfo, Element elem, T value) { |
| this.tree = tree; |
| this.parentNode = parent; |
| this.parentNodeInfo = parentNodeInfo; |
| this.depth = parentNode == null ? 0 : parentNode.depth + 1; |
| this.value = value; |
| setElement(elem); |
| } |
| |
| public int getChildCount() { |
| return children == null ? 0 : children.size(); |
| } |
| |
| public CellTreeNodeView<?> getChildNode(int childIndex) { |
| return children.get(childIndex); |
| } |
| |
| public boolean isLeaf() { |
| return tree.isLeaf(value); |
| } |
| |
| /** |
| * Check whether or not this node is open. |
| * |
| * @return true if open, false if closed |
| */ |
| public boolean isOpen() { |
| return open; |
| } |
| |
| /** |
| * Sets whether this item's children are displayed. |
| * |
| * @param open whether the item is open |
| * @param fireEvents true to fire events if the state changes |
| * @return true if successfully opened, false otherwise. |
| */ |
| public boolean setOpen(boolean open, boolean fireEvents) { |
| // Early out. |
| if (this.open == open) { |
| return this.open; |
| } |
| |
| // If this node is a leaf node, do not call TreeViewModel.getNodeInfo(). |
| if (open && isLeaf()) { |
| return false; |
| } |
| |
| // The animation clears the innerHtml of the childContainer. If we reopen a |
| // node as its closing, it is possible that the new data will be set |
| // synchronously, so we have to cancel the animation before attaching the |
| // data display to the node info. |
| tree.cancelTreeNodeAnimation(); |
| this.animate = true; |
| this.open = open; |
| if (open) { |
| if (!nodeInfoLoaded) { |
| nodeInfoLoaded = true; |
| nodeInfo = tree.getNodeInfo(value); |
| |
| // Sink events for the new node. |
| if (nodeInfo != null) { |
| Set<String> eventsToSink = new HashSet<String>(); |
| // Listen for focus and blur for keyboard navigation |
| eventsToSink.add("focus"); |
| eventsToSink.add("blur"); |
| |
| Set<String> consumedEvents = nodeInfo.getCell().getConsumedEvents(); |
| if (consumedEvents != null) { |
| eventsToSink.addAll(consumedEvents); |
| } |
| CellBasedWidgetImpl.get().sinkEvents(tree, eventsToSink); |
| } |
| } |
| |
| // If we don't have any nodeInfo, we must be a leaf node. |
| if (nodeInfo != null) { |
| // Add a loading message. |
| ensureChildContainer(); |
| showOrHide(showMoreElem, false); |
| showOrHide(emptyMessageElem, false); |
| if (!isRootNode()) { |
| setStyleName(getCellParent(), tree.getStyle().cellTreeOpenItem(), |
| true); |
| } |
| ensureAnimationFrame().getStyle().setProperty("display", ""); |
| onOpen(nodeInfo); |
| |
| // Fire an event. |
| if (fireEvents) { |
| OpenEvent.fire(tree, getTreeNode()); |
| } |
| } else { |
| this.open = false; |
| } |
| } else { |
| if (!isRootNode()) { |
| setStyleName(getCellParent(), tree.getStyle().cellTreeOpenItem(), false); |
| } |
| cleanup(false); |
| tree.maybeAnimateTreeNode(this); |
| updateImage(false); |
| |
| // Keyboard select this node if the open node was a child. |
| CellTreeNodeView<?> keySelected = tree.getKeyboardSelectedNode(); |
| while (keySelected != null) { |
| if (keySelected == this) { |
| tree.keyboardSelect(this, true); |
| break; |
| } |
| keySelected = keySelected.getParentNode(); |
| } |
| |
| // Fire an event. |
| if (fireEvents) { |
| CloseEvent.fire(tree, getTreeNode()); |
| } |
| } |
| |
| return this.open; |
| } |
| |
| /** |
| * Unregister the list handler and destroy all child nodes. |
| * |
| * @param destroy true to destroy this node |
| */ |
| protected void cleanup(boolean destroy) { |
| // Unregister the list handler. |
| if (listView != null) { |
| listView.cleanup(); |
| nodeInfo.unsetDataDisplay(); |
| listView = null; |
| } |
| |
| // Recursively destroy children. |
| if (children != null) { |
| for (CellTreeNodeView<?> child : children) { |
| child.cleanup(true); |
| } |
| children = null; |
| } |
| |
| // Destroy this node. |
| if (destroy) { |
| isDestroyed = true; |
| |
| // If this is the keyboard selected node, select the parent. The children |
| // have already been cleaned, so the selected node cannot be under this |
| // node. |
| if (this == tree.getKeyboardSelectedNode()) { |
| tree.keyboardSelect(parentNode, false); |
| } |
| } |
| } |
| |
| protected boolean consumeAnimate() { |
| boolean hasAnimate = animate; |
| animate = false; |
| return hasAnimate; |
| } |
| |
| /** |
| * Returns an instance of TreeNodeView of the same subclass as the calling |
| * object. |
| * |
| * @param <C> the data type of the node's children |
| * @param nodeInfo a NodeInfo object describing the child nodes |
| * @param childElem the DOM element used to parent the new TreeNodeView |
| * @param childValue the child's value |
| * @param viewData view data associated with the node |
| * @return a TreeNodeView of suitable type |
| */ |
| protected <C> CellTreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo, |
| Element childElem, C childValue, Object viewData) { |
| return new CellTreeNodeView<C>(tree, this, nodeInfo, childElem, childValue); |
| } |
| |
| /** |
| * Fire an event to the {@link com.google.gwt.cell.client.AbstractCell}. |
| * |
| * @param event the native event |
| */ |
| @SuppressWarnings("unchecked") |
| protected void fireEventToCell(NativeEvent event) { |
| if (parentNodeInfo == null) { |
| return; |
| } |
| |
| Cell<T> parentCell = parentNodeInfo.getCell(); |
| String eventType = event.getType(); |
| Element cellParent = getCellParent(); |
| Object key = getValueKey(); |
| Context context = new Context(getIndex(), 0, key); |
| boolean cellWasEditing = parentCell.isEditing(context, cellParent, value); |
| |
| // Update selection. |
| boolean isSelectionHandled = parentCell.handlesSelection() |
| || KeyboardSelectionPolicy.BOUND_TO_SELECTION == tree.getKeyboardSelectionPolicy(); |
| HasData<T> display = (HasData<T>) parentNode.listView; |
| CellPreviewEvent<T> previewEvent = CellPreviewEvent.fire(display, event, |
| display, context, value, cellWasEditing, isSelectionHandled); |
| |
| // Forward the event to the cell. |
| if (previewEvent.isCanceled() |
| || !cellParent.isOrHasChild(Element.as(event.getEventTarget()))) { |
| return; |
| } |
| Set<String> consumedEvents = parentCell.getConsumedEvents(); |
| if (consumedEvents != null && consumedEvents.contains(eventType)) { |
| parentCell.onBrowserEvent(context, cellParent, value, event, |
| parentNodeInfo.getValueUpdater()); |
| tree.cellIsEditing = parentCell.isEditing(context, cellParent, value); |
| if (cellWasEditing && !tree.cellIsEditing) { |
| CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() { |
| public void execute() { |
| tree.setFocus(true); |
| } |
| }); |
| } |
| } |
| } |
| |
| /** |
| * Returns the element that parents the cell contents of this node. |
| */ |
| protected Element getCellParent() { |
| return getCellParent(getElement()); |
| } |
| |
| /** |
| * Returns the element corresponding to the open/close image. |
| * |
| * @return the open/close image element |
| */ |
| protected Element getImageElement() { |
| return getImageElement(getElement()); |
| } |
| |
| /** |
| * Returns the element that selection styles are applied to. The element |
| * includes the open/close image and the rendered value and spans the width of |
| * the tree. |
| * |
| * @return the selection element |
| */ |
| protected Element getSelectionElement() { |
| return getSelectionElement(getElement()); |
| } |
| |
| /** |
| * Returns the key for the value of this node using the parent's |
| * implementation of NodeInfo.getKey(). |
| */ |
| protected Object getValueKey() { |
| return parentNodeInfo.getProvidesKey().getKey(value); |
| } |
| |
| /** |
| * Set up the node when it is opened. |
| * |
| * @param nodeInfo the {@link NodeInfo} that provides information about the |
| * child values |
| * @param <C> the child data type of the node |
| */ |
| protected <C> void onOpen(final NodeInfo<C> nodeInfo) { |
| NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this, |
| tree.getDefaultNodeSize()); |
| listView = view; |
| view.setSelectionModel(nodeInfo.getSelectionModel()); |
| nodeInfo.setDataDisplay(view); |
| } |
| |
| /** |
| * Ensure that the animation frame exists and return it. |
| * |
| * @return the animation frame |
| */ |
| Element ensureAnimationFrame() { |
| if (animationFrame == null) { |
| animationFrame = Document.get().createDivElement(); |
| animationFrame.getStyle().setOverflow(Overflow.HIDDEN); |
| animationFrame.getStyle().setDisplay(Display.NONE); |
| getElement().appendChild(animationFrame); |
| } |
| return animationFrame; |
| } |
| |
| /** |
| * Ensure that the child container exists and return it. |
| * |
| * @return the child container |
| */ |
| Element ensureChildContainer() { |
| if (childContainer == null) { |
| childContainer = Document.get().createDivElement(); |
| ensureContentContainer().insertFirst(childContainer); |
| } |
| return childContainer; |
| } |
| |
| /** |
| * Ensure that the content container exists and return it. |
| * |
| * @return the content container |
| */ |
| Element ensureContentContainer() { |
| if (contentContainer == null) { |
| contentContainer = Document.get().createDivElement(); |
| ensureAnimationFrame().appendChild(contentContainer); |
| |
| // TODO(jlabanca): I18N no data string. |
| emptyMessageElem = Document.get().createDivElement(); |
| emptyMessageElem.setInnerHTML("no data"); |
| setStyleName(emptyMessageElem, tree.getStyle().cellTreeEmptyMessage(), |
| true); |
| showOrHide(emptyMessageElem, false); |
| contentContainer.appendChild(emptyMessageElem); |
| |
| showMoreElem = Document.get().createAnchorElement(); |
| showMoreElem.setHref("javascript:;"); |
| showMoreElem.setInnerText("Show more"); |
| setStyleName(showMoreElem, tree.getStyle().cellTreeShowMoreButton(), true); |
| showOrHide(showMoreElem, false); |
| contentContainer.appendChild(showMoreElem); |
| } |
| return contentContainer; |
| } |
| |
| /** |
| * Return the index of this node in its parent. |
| */ |
| int getIndex() { |
| return parentNode == null ? 0 : parentNode.indexOf(this); |
| } |
| |
| /** |
| * Return the parent node, or null if this node is the root. |
| */ |
| CellTreeNodeView<?> getParentNode() { |
| return parentNode; |
| } |
| |
| Element getShowMoreElement() { |
| return showMoreElem; |
| } |
| |
| /** |
| * Get a {@link TreeNode} with a public API for this node view. |
| * |
| * @return the {@link TreeNode} |
| */ |
| TreeNode getTreeNode() { |
| if (treeNode == null) { |
| treeNode = new TreeNodeImpl(this); |
| } |
| return treeNode; |
| } |
| |
| int indexOf(CellTreeNodeView<?> child) { |
| return children.indexOf(child); |
| } |
| |
| boolean isDestroyed() { |
| return isDestroyed; |
| } |
| |
| /** |
| * Check if this node is a root node. |
| * |
| * @return true if a root node |
| */ |
| boolean isRootNode() { |
| return parentNode == null; |
| } |
| |
| /** |
| * Check if the value of this node is selected. |
| * |
| * @return true if selected, false if not |
| */ |
| boolean isSelected() { |
| if (parentNodeInfo != null) { |
| SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel(); |
| if (selectionModel != null) { |
| return selectionModel.isSelected(value); |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Reset focus on this node. |
| * |
| * @return true of the cell takes focus, false if not |
| */ |
| boolean resetFocusOnCell() { |
| if (parentNodeInfo != null) { |
| Context context = new Context(getIndex(), 0, getValueKey()); |
| Cell<T> cell = parentNodeInfo.getCell(); |
| return cell.resetFocus(context, getCellParent(), value); |
| } |
| return false; |
| } |
| |
| /** |
| * Select or deselect this node with the keyboard. |
| * |
| * @param selected true if selected, false if not |
| * @param stealFocus true to steal focus |
| */ |
| void setKeyboardSelected(boolean selected, boolean stealFocus) { |
| if (tree.isKeyboardSelectionDisabled()) { |
| return; |
| } |
| |
| // Apply the selected style. |
| if (!selected || tree.isFocused || stealFocus) { |
| setKeyboardSelectedStyle(selected); |
| } |
| |
| // Make the node focusable or not. |
| Element cellParent = getCellParent(); |
| if (!selected) { |
| // Chrome: Elements remain focusable after removing the tabIndex, so set |
| // it to -1 first. |
| cellParent.setTabIndex(-1); |
| cellParent.removeAttribute("tabIndex"); |
| cellParent.removeAttribute("accessKey"); |
| } else { |
| FocusImpl focusImpl = FocusImpl.getFocusImplForWidget(); |
| com.google.gwt.user.client.Element cellElem = cellParent.cast(); |
| focusImpl.setTabIndex(cellElem, tree.getTabIndex()); |
| char accessKey = tree.getAccessKey(); |
| if (accessKey != 0) { |
| focusImpl.setAccessKey(cellElem, accessKey); |
| } |
| if (stealFocus && !tree.cellIsEditing) { |
| cellElem.focus(); |
| } |
| } |
| |
| // Update the selection model. |
| if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == tree.getKeyboardSelectionPolicy()) { |
| setSelected(selected); |
| } |
| } |
| |
| /** |
| * Add or remove the keyboard selected style. |
| * |
| * @param selected true if selected, false if not |
| */ |
| void setKeyboardSelectedStyle(boolean selected) { |
| if (!isRootNode()) { |
| Element selectionElem = getSelectionElement(getElement()); |
| if (selectionElem != null) { |
| setStyleName(selectionElem, |
| tree.getStyle().cellTreeKeyboardSelectedItem(), selected); |
| } |
| } |
| } |
| |
| /** |
| * Select or deselect this node. |
| * |
| * @param selected true to select, false to deselect |
| */ |
| void setSelected(boolean selected) { |
| if (parentNodeInfo != null) { |
| SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel(); |
| if (selectionModel != null) { |
| selectionModel.setSelected(value, selected); |
| } |
| } |
| } |
| |
| void showFewer() { |
| Range range = listView.getVisibleRange(); |
| int defaultPageSize = listView.getDefaultPageSize(); |
| int maxSize = Math.max(defaultPageSize, range.getLength() - defaultPageSize); |
| listView.setVisibleRange(range.getStart(), maxSize); |
| } |
| |
| void showMore() { |
| Range range = listView.getVisibleRange(); |
| int pageSize = range.getLength() + listView.getDefaultPageSize(); |
| listView.setVisibleRange(range.getStart(), pageSize); |
| } |
| |
| /** |
| * Update the image based on the current state. |
| * |
| * @param isLoading true if still loading data |
| */ |
| private void updateImage(boolean isLoading) { |
| // Early out if this is a root node. |
| if (isRootNode()) { |
| return; |
| } |
| |
| // Replace the image element with a new one. |
| boolean isTopLevel = parentNode.isRootNode(); |
| SafeHtml html = tree.getClosedImageHtml(isTopLevel); |
| if (open) { |
| html = isLoading ? tree.getLoadingImageHtml() |
| : tree.getOpenImageHtml(isTopLevel); |
| } |
| if (nodeInfoLoaded && nodeInfo == null) { |
| html = LEAF_IMAGE; |
| } |
| Element tmp = Document.get().createDivElement(); |
| tmp.setInnerHTML(html.asString()); |
| Element imageElem = tmp.getFirstChildElement(); |
| |
| Element oldImg = getImageElement(); |
| oldImg.getParentElement().replaceChild(imageElem, oldImg); |
| } |
| } |