blob: c98075ef51de0fad4a47a472bbcd43b54a6523a2 [file] [log] [blame]
/*
* 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.animation.client.Animation;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.Style.Visibility;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.OpenEvent;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.resources.client.ClientBundle;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.resources.client.CssResource.ImportedWithPrefix;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.resources.client.ImageResource.ImageOptions;
import com.google.gwt.resources.client.ImageResource.RepeatStyle;
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.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.ui.AbstractImagePrototype;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HasAnimation;
import com.google.gwt.user.client.ui.ProvidesResize;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SplitLayoutPanel;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.view.client.HasData;
import com.google.gwt.view.client.ProvidesKey;
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.List;
/**
* A "browsable" view of a tree in which only a single node per level may be
* open at one time.
*
* <p>
* This widget will <em>only</em> work in standards mode, which requires that
* the HTML page in which it is run have an explicit &lt;!DOCTYPE&gt;
* declaration.
* </p>
*
* <p>
* <h3>Example</h3>
* <dl>
* <dt>Trivial example</dt>
* <dd>{@example com.google.gwt.examples.cellview.CellBrowserExample}</dd>
* <dt>Complex example</dt>
* <dd>{@example com.google.gwt.examples.cellview.CellBrowserExample2}</dd>
* </dl>
*/
public class CellBrowser extends AbstractCellTree implements ProvidesResize,
RequiresResize, HasAnimation {
/**
* A ClientBundle that provides images for this widget.
*/
public interface Resources extends ClientBundle {
/**
* An image indicating a closed branch.
*/
@ImageOptions(flipRtl = true)
ImageResource cellBrowserClosed();
/**
* An image indicating an open branch.
*/
@ImageOptions(flipRtl = true)
ImageResource cellBrowserOpen();
/**
* The background used for open items.
*/
@ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
ImageResource cellBrowserOpenBackground();
/**
* The background used for selected items.
*/
@Source("cellTreeSelectedBackground.png")
@ImageOptions(repeatStyle = RepeatStyle.Horizontal, flipRtl = true)
ImageResource cellBrowserSelectedBackground();
/**
* The styles used in this widget.
*/
@Source(Style.DEFAULT_CSS)
Style cellBrowserStyle();
}
/**
* Styles used by this widget.
*/
@ImportedWithPrefix("gwt-CellBrowser")
public interface Style extends CssResource {
/**
* The path to the default CSS styles used by this resource.
*/
String DEFAULT_CSS = "com/google/gwt/user/cellview/client/CellBrowser.css";
/**
* Applied to all columns.
*/
String cellBrowserColumn();
/**
* Applied to even list items.
*/
String cellBrowserEvenItem();
/**
* Applied to the first column.
*/
String cellBrowserFirstColumn();
/***
* Applied to keyboard selected items.
*/
String cellBrowserKeyboardSelectedItem();
/**
* Applied to odd list items.
*/
String cellBrowserOddItem();
/***
* Applied to open items.
*/
String cellBrowserOpenItem();
/***
* Applied to selected items.
*/
String cellBrowserSelectedItem();
/**
* Applied to the widget.
*/
String cellBrowserWidget();
}
interface Template extends SafeHtmlTemplates {
@Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"position:relative;padding-right:{2}px;outline:none;\">{3}<div>{4}</div></div>")
SafeHtml div(int idx, String classes, int imageWidth, SafeHtml imageHtml,
SafeHtml cellContents);
@Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"position:relative;padding-right:{2}px;outline:none;\" tabindex=\"{3}\">{4}<div>{5}</div></div>")
SafeHtml divFocusable(int idx, String classes, int imageWidth,
int tabIndex, SafeHtml imageHtml, SafeHtml cellContents);
@Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"position:relative;padding-right:{2}px;outline:none;\" tabindex=\"{3}\" accessKey=\"{4}\">{5}<div>{6}</div></div>")
SafeHtml divFocusableWithKey(int idx, String classes, int imageWidth,
int tabIndex, char accessKey, SafeHtml imageHtml, SafeHtml cellContents);
@Template("<div style=\"position:absolute;{0}:0px;width:{1}px;"
+ "height:{2}px;\">{3}</div>")
SafeHtml imageWrapper(String direction, int width, int height,
SafeHtml image);
}
/**
* A custom version of cell list used by the browser. Visible for testing.
*
* @param <T> the data type of list items
*/
class BrowserCellList<T> extends CellList<T> {
/**
* The level of this list view.
*/
private final int level;
/**
* The key of the currently focused item.
*/
private Object focusedKey;
/**
* The value of the currently focused item.
*/
private T focusedValue;
/**
* Indicates whether or not the focused value is open.
*/
private boolean isFocusedOpen;
public BrowserCellList(final Cell<T> cell, int level,
ProvidesKey<T> keyProvider) {
super(cell, cellListResources, keyProvider);
this.level = level;
}
@Override
protected Element getCellParent(Element item) {
return item.getFirstChildElement().getNextSiblingElement();
}
@Override
protected void onBrowserEvent2(Event event) {
super.onBrowserEvent2(event);
// Handle keyboard navigation between lists.
String eventType = event.getType();
if ("keydown".equals(eventType) && !isKeyboardNavigationSuppressed()) {
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyCodes.KEY_LEFT:
if (LocaleInfo.getCurrentLocale().isRTL()) {
keyboardNavigateDeep();
} else {
keyboardNavigateShallow();
}
return;
case KeyCodes.KEY_RIGHT:
if (LocaleInfo.getCurrentLocale().isRTL()) {
keyboardNavigateShallow();
} else {
keyboardNavigateDeep();
}
return;
}
}
}
@Override
protected void renderRowValues(SafeHtmlBuilder sb, List<T> values,
int start, SelectionModel<? super T> selectionModel) {
Cell<T> cell = getCell();
String keyboardSelectedItem = " "
+ style.cellBrowserKeyboardSelectedItem();
String selectedItem = " " + style.cellBrowserSelectedItem();
String openItem = " " + style.cellBrowserOpenItem();
String evenItem = style.cellBrowserEvenItem();
String oddItem = style.cellBrowserOddItem();
int keyboardSelectedRow = getKeyboardSelectedRow() + getPageStart();
int length = values.size();
int end = start + length;
for (int i = start; i < end; i++) {
T value = values.get(i - start);
Object key = getValueKey(value);
boolean isSelected = selectionModel == null ? false
: selectionModel.isSelected(value);
boolean isOpen = (focusedKey == null || !isFocusedOpen) ? false
: focusedKey.equals(key);
StringBuilder classesBuilder = new StringBuilder();
classesBuilder.append(i % 2 == 0 ? evenItem : oddItem);
if (isOpen) {
classesBuilder.append(openItem);
}
if (isSelected) {
classesBuilder.append(selectedItem);
}
SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
cell.render(value, null, cellBuilder);
// Figure out which image to use.
SafeHtml image;
if (isOpen) {
image = openImageHtml;
} else if (isLeaf(value)) {
image = LEAF_IMAGE;
} else {
image = closedImageHtml;
}
if (i == keyboardSelectedRow) {
// This is the focused item.
if (isFocused) {
classesBuilder.append(keyboardSelectedItem);
}
char accessKey = getAccessKey();
if (accessKey != 0) {
sb.append(template.divFocusableWithKey(i,
classesBuilder.toString(), imageWidth, getTabIndex(),
getAccessKey(), image, cellBuilder.toSafeHtml()));
} else {
sb.append(template.divFocusable(i, classesBuilder.toString(),
imageWidth, getTabIndex(), image, cellBuilder.toSafeHtml()));
}
} else {
sb.append(template.div(i, classesBuilder.toString(), imageWidth,
image, cellBuilder.toSafeHtml()));
}
}
}
@Override
void doKeyboardSelection(Event event, T value, int indexOnPage) {
super.doKeyboardSelection(event, value, indexOnPage);
// Open the selected row. If keyboard selection updates the selection
// model, this is a no-op.
setChildState(this, value, true, true, true);
}
/**
* Navigate to a deeper node.
*/
private void keyboardNavigateDeep() {
if (isKeyboardSelectionDisabled()) {
return;
}
// Move to the child node.
if (level < treeNodes.size() - 1) {
TreeNodeImpl<?> treeNode = treeNodes.get(level + 1);
treeNode.display.setFocus(true);
// Select the element.
int selected = getKeyboardSelectedRow();
if (isRowWithinBounds(selected)) {
T value = getDisplayedItem(selected);
setChildState(this, value, true, true, true);
}
}
}
/**
* Navigate to a shallower node.
*/
private void keyboardNavigateShallow() {
if (isKeyboardSelectionDisabled()) {
return;
}
// Move to the parent node.
if (level > 0) {
TreeNodeImpl<?> treeNode = treeNodes.get(level - 1);
treeNode.display.setFocus(true);
}
}
}
/**
* A node in the tree.
*
* @param <C> the data type of the children of the node
*/
class TreeNodeImpl<C> implements TreeNode {
private final BrowserCellList<C> display;
private NodeInfo<C> nodeInfo;
private final Object value;
private final HandlerRegistration valueChangeHandler;
private final Widget widget;
/**
* Construct a new {@link TreeNodeImpl}.
*
* @param nodeInfo the nodeInfo for the children nodes
* @param value the value of the node
* @param display the display associated with the node
* @param widget the widget that wraps the display
*/
public TreeNodeImpl(final NodeInfo<C> nodeInfo, Object value,
final BrowserCellList<C> display, Widget widget) {
this.display = display;
this.nodeInfo = nodeInfo;
this.value = value;
this.widget = widget;
// Trim to the current level if the open node disappears.
valueChangeHandler = display.addValueChangeHandler(new ValueChangeHandler<List<C>>() {
public void onValueChange(ValueChangeEvent<List<C>> event) {
Object focusedKey = display.focusedKey;
if (focusedKey != null) {
boolean stillExists = false;
List<C> displayValues = event.getValue();
for (C displayValue : displayValues) {
if (focusedKey.equals(display.getValueKey(displayValue))) {
stillExists = true;
break;
}
}
if (!stillExists) {
trimToLevel(display.level);
}
}
}
});
}
public int getChildCount() {
assertNotDestroyed();
return display.getChildCount();
}
public C getChildValue(int index) {
assertNotDestroyed();
checkChildBounds(index);
return display.getDisplayedItem(index);
}
public int getIndex() {
assertNotDestroyed();
TreeNodeImpl<?> parent = getParent();
return (parent == null) ? 0 : parent.getOpenIndex();
}
public TreeNodeImpl<?> getParent() {
assertNotDestroyed();
return (display.level == 0) ? null : treeNodes.get(display.level - 1);
}
public Object getValue() {
return value;
}
public boolean isChildLeaf(int index) {
assertNotDestroyed();
checkChildBounds(index);
return isLeaf(getChildValue(index));
}
public boolean isChildOpen(int index) {
assertNotDestroyed();
checkChildBounds(index);
return (display.focusedKey == null || !display.isFocusedOpen)
? false
: display.focusedKey.equals(display.getValueKey(getChildValue(index)));
}
public boolean isDestroyed() {
return nodeInfo == null;
}
public TreeNode setChildOpen(int index, boolean open) {
return setChildOpen(index, open, true);
}
public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) {
assertNotDestroyed();
checkChildBounds(index);
return setChildState(display, getChildValue(index), open, fireEvents,
true);
}
/**
* Return the key of the value that is focused in this node's display.
*/
Object getFocusedKey() {
return display.focusedKey;
}
/**
* Return true if the focused value is open, false if not.
*/
boolean isFocusedOpen() {
return display.isFocusedOpen;
}
/**
* 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();
}
}
/**
* Unregister the list view and remove it from the widget.
*/
private void destroy() {
valueChangeHandler.removeHandler();
display.setSelectionModel(null);
nodeInfo.unsetDataDisplay();
getSplitLayoutPanel().remove(widget);
nodeInfo = null;
}
/**
* Get the index of the open item.
*
* @return the index of the open item, or -1 if not found
*/
private int getOpenIndex() {
return display.isFocusedOpen ? display.indexOf(display.focusedValue)
: null;
}
}
/**
* An implementation of {@link CellList.Resources} that delegates to
* {@link CellBrowser.Resources}.
*/
private static class CellListResourcesImpl implements CellList.Resources {
private final CellBrowser.Resources delegate;
private final CellListStyleImpl style;
public CellListResourcesImpl(CellBrowser.Resources delegate) {
this.delegate = delegate;
this.style = new CellListStyleImpl(delegate.cellBrowserStyle());
}
public ImageResource cellListSelectedBackground() {
return delegate.cellBrowserSelectedBackground();
}
public CellList.Style cellListStyle() {
return style;
}
}
/**
* An implementation of {@link CellList.Style} that delegates to
* {@link CellBrowser.Style}.
*/
private static class CellListStyleImpl implements CellList.Style {
private final CellBrowser.Style delegate;
public CellListStyleImpl(CellBrowser.Style delegate) {
this.delegate = delegate;
}
public String cellListEvenItem() {
return delegate.cellBrowserEvenItem();
}
public String cellListKeyboardSelectedItem() {
return delegate.cellBrowserKeyboardSelectedItem();
}
public String cellListOddItem() {
return delegate.cellBrowserOddItem();
}
public String cellListSelectedItem() {
return delegate.cellBrowserSelectedItem();
}
public String cellListWidget() {
// Do not apply any style to the list itself.
return null;
}
public boolean ensureInjected() {
return delegate.ensureInjected();
}
public String getName() {
return delegate.getName();
}
public String getText() {
return delegate.getText();
}
}
/**
* The animation used to scroll to the newly added list view.
*/
private class ScrollAnimation extends Animation {
/**
* The starting scroll position.
*/
private int startScrollLeft;
/**
* The ending scroll position.
*/
private int targetScrollLeft;
@Override
protected void onComplete() {
getElement().setScrollLeft(targetScrollLeft);
}
@Override
protected void onUpdate(double progress) {
int diff = targetScrollLeft - startScrollLeft;
getElement().setScrollLeft(startScrollLeft + (int) (diff * progress));
}
void scrollToEnd() {
Element elem = getElement();
targetScrollLeft = elem.getScrollWidth() - elem.getClientWidth();
if (LocaleInfo.getCurrentLocale().isRTL()) {
targetScrollLeft *= -1;
}
if (isAnimationEnabled()) {
// Animate the scrolling.
startScrollLeft = elem.getScrollLeft();
run(250);
} else {
// Scroll instantly.
onComplete();
}
}
}
private static Resources DEFAULT_RESOURCES;
/**
* 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>");
private static Template template;
private static Resources getDefaultResources() {
if (DEFAULT_RESOURCES == null) {
DEFAULT_RESOURCES = GWT.create(Resources.class);
}
return DEFAULT_RESOURCES;
}
/**
* The visible {@link TreeNodeImpl}s. Visible for testing.
*/
final List<TreeNodeImpl<?>> treeNodes = new ArrayList<TreeNodeImpl<?>>();
/**
* The animation used for scrolling.
*/
private final ScrollAnimation animation = new ScrollAnimation();
/**
* The resources used by the {@link CellList}.
*/
private final CellList.Resources cellListResources;
/**
* The HTML used to generate the closed image.
*/
private final SafeHtml closedImageHtml;
/**
* The default width of new columns.
*/
private int defaultWidth = 200;
/**
* The maximum width of the open and closed images.
*/
private final int imageWidth;
/**
* A boolean indicating whether or not animations are enabled.
*/
private boolean isAnimationEnabled;
/**
* The minimum width of new columns.
*/
private int minWidth;
/**
* The HTML used to generate the open image.
*/
private final SafeHtml openImageHtml;
/**
* The element used to maintain the scrollbar when columns are removed.
*/
private Element scrollLock;
/**
* The styles used by this widget.
*/
private final Style style;
/**
* Construct a new {@link CellBrowser}.
*
* @param <T> the type of data in the root node
* @param viewModel the {@link TreeViewModel} that backs the tree
* @param rootValue the hidden root value of the tree
*/
public <T> CellBrowser(TreeViewModel viewModel, T rootValue) {
this(viewModel, rootValue, getDefaultResources());
}
/**
* Construct a new {@link CellBrowser} with the specified {@link Resources}.
*
* @param <T> the type of data in the root node
* @param viewModel the {@link TreeViewModel} that backs the tree
* @param rootValue the hidden root value of the tree
* @param resources the {@link Resources} used for images
*/
public <T> CellBrowser(TreeViewModel viewModel, T rootValue,
Resources resources) {
super(viewModel);
if (template == null) {
template = GWT.create(Template.class);
}
this.style = resources.cellBrowserStyle();
this.style.ensureInjected();
this.cellListResources = new CellListResourcesImpl(resources);
initWidget(new SplitLayoutPanel());
getElement().getStyle().setOverflow(Overflow.AUTO);
setStyleName(this.style.cellBrowserWidget());
// Initialize the open and close images strings.
ImageResource treeOpen = resources.cellBrowserOpen();
ImageResource treeClosed = resources.cellBrowserClosed();
openImageHtml = getImageHtml(treeOpen);
closedImageHtml = getImageHtml(treeClosed);
imageWidth = Math.max(treeOpen.getWidth(), treeClosed.getWidth());
minWidth = imageWidth + 20;
// Add a placeholder to maintain the scroll width.
scrollLock = Document.get().createDivElement();
scrollLock.getStyle().setPosition(Position.ABSOLUTE);
scrollLock.getStyle().setVisibility(Visibility.HIDDEN);
scrollLock.getStyle().setZIndex(-32767);
scrollLock.getStyle().setTop(0, Unit.PX);
if (LocaleInfo.getCurrentLocale().isRTL()) {
scrollLock.getStyle().setRight(0, Unit.PX);
} else {
scrollLock.getStyle().setLeft(0, Unit.PX);
}
scrollLock.getStyle().setHeight(1, Unit.PX);
scrollLock.getStyle().setWidth(1, Unit.PX);
getElement().appendChild(scrollLock);
// Associate the first view with the rootValue.
appendTreeNode(getNodeInfo(rootValue), rootValue);
// Catch scroll events.
sinkEvents(Event.ONSCROLL);
}
/**
* Get the default width of new columns.
*
* @return the default width in pixels
*/
public int getDefaultColumnWidth() {
return defaultWidth;
}
/**
* Get the minimum width of columns.
*
* @return the minimum width in pixels
*/
public int getMinimumColumnWidth() {
return minWidth;
}
@Override
public TreeNode getRootTreeNode() {
return treeNodes.get(0);
}
public boolean isAnimationEnabled() {
return isAnimationEnabled;
}
@Override
public void onBrowserEvent(Event event) {
switch (DOM.eventGetType(event)) {
case Event.ONSCROLL:
// Shorten the scroll bar is possible.
adjustScrollLock();
break;
}
super.onBrowserEvent(event);
}
public void onResize() {
getSplitLayoutPanel().onResize();
}
public void setAnimationEnabled(boolean enable) {
this.isAnimationEnabled = enable;
}
/**
* Set the default width of new columns.
*
* @param width the default width in pixels
*/
public void setDefaultColumnWidth(int width) {
this.defaultWidth = width;
}
@Override
public void setKeyboardSelectionPolicy(KeyboardSelectionPolicy policy) {
super.setKeyboardSelectionPolicy(policy);
for (TreeNodeImpl<?> treeNode : treeNodes) {
treeNode.display.setKeyboardSelectionPolicy(policy);
}
}
/**
* Set the minimum width of columns.
*
* @param minWidth the minimum width in pixels
*/
public void setMinimumColumnWidth(int minWidth) {
this.minWidth = minWidth;
}
/**
* Create a pager to control the list view.
*
* @param <C> the item type in the list view
* @param display the list view to add paging too
* @return the pager
*/
protected <C> Widget createPager(HasData<C> display) {
PageSizePager pager = new PageSizePager(
display.getVisibleRange().getLength());
pager.setDisplay(display);
return pager;
}
/**
* Adjust the size of the scroll lock element based on the new position of the
* scroll bar.
*/
private void adjustScrollLock() {
int scrollLeft = Math.abs(getElement().getScrollLeft());
if (scrollLeft > 0) {
int clientWidth = getElement().getClientWidth();
scrollLock.getStyle().setWidth(scrollLeft + clientWidth, Unit.PX);
} else {
scrollLock.getStyle().setWidth(1.0, Unit.PX);
}
}
/**
* Create a new {@link TreeNodeImpl} and append it to the end of the
* LayoutPanel.
*
* @param <C> the data type of the children
* @param nodeInfo the info about the node
* @param value the value of the open node
*/
private <C> void appendTreeNode(final NodeInfo<C> nodeInfo, Object value) {
// Create the list view.
final int level = treeNodes.size();
final BrowserCellList<C> view = createDisplay(nodeInfo, level);
// Create a pager and wrap the components in a scrollable container. Set the
// tabIndex to -1 so the user can tab between lists without going through
// the scrollable.
ScrollPanel scrollable = new ScrollPanel();
scrollable.getElement().setTabIndex(-1);
final Widget pager = createPager(view);
if (pager != null) {
FlowPanel flowPanel = new FlowPanel();
flowPanel.add(view);
flowPanel.add(pager);
scrollable.setWidget(flowPanel);
} else {
scrollable.setWidget(view);
}
scrollable.setStyleName(style.cellBrowserColumn());
if (level == 0) {
scrollable.addStyleName(style.cellBrowserFirstColumn());
}
// Create a TreeNode.
TreeNodeImpl<C> treeNode = new TreeNodeImpl<C>(nodeInfo, value, view,
scrollable);
treeNodes.add(treeNode);
// Attach the view to the selection model and node info.
view.setSelectionModel(nodeInfo.getSelectionModel());
nodeInfo.setDataDisplay(view);
// Add the view to the LayoutPanel.
SplitLayoutPanel splitPanel = getSplitLayoutPanel();
splitPanel.insertLineStart(scrollable, defaultWidth, null);
splitPanel.setWidgetMinSize(scrollable, minWidth);
splitPanel.forceLayout();
// Scroll to the right.
animation.scrollToEnd();
}
/**
* Create a {@link HasData} that will display items. The {@link HasData} must
* extend {@link Widget}.
*
* @param <C> the item type in the list view
* @param nodeInfo the node info with child data
* @param level the level of the list
* @return the {@link HasData}
*/
private <C> BrowserCellList<C> createDisplay(NodeInfo<C> nodeInfo, int level) {
BrowserCellList<C> display = new BrowserCellList<C>(nodeInfo.getCell(),
level, nodeInfo.getProvidesKey());
display.setValueUpdater(nodeInfo.getValueUpdater());
display.setKeyboardSelectionPolicy(getKeyboardSelectionPolicy());
return display;
}
/**
* Get the HTML representation of an image.
*
* @param res the {@link ImageResource} to render as HTML
* @return the rendered HTML
*/
private SafeHtml getImageHtml(ImageResource res) {
// Right-justify image if LTR, left-justify if RTL
AbstractImagePrototype proto = AbstractImagePrototype.create(res);
SafeHtml image = SafeHtmlUtils.fromTrustedString(proto.getHTML());
return template.imageWrapper((LocaleInfo.getCurrentLocale().isRTL()
? "left" : "right"), res.getWidth(), res.getHeight(), image);
}
/**
* Get the {@link SplitLayoutPanel} used to lay out the views.
*
* @return the {@link SplitLayoutPanel}
*/
private SplitLayoutPanel getSplitLayoutPanel() {
return (SplitLayoutPanel) getWidget();
}
/**
* Set the open state of a tree node.
*
* @param cellList the CellList that changed state.
* @param value the value to open
* @param open true to open, false to close
* @param fireEvents true to fireEvents
* @return the open {@link TreeNode}, or null if not opened
*/
private <C> TreeNode setChildState(BrowserCellList<C> cellList, C value,
boolean open, boolean fireEvents, boolean redraw) {
// Get the key of the value to open.
Object newKey = cellList.getValueKey(value);
if (open) {
if (newKey == null) {
// Early exit if opening but the specified node has no key.
return null;
} else if (newKey.equals(cellList.focusedKey)) {
// Early exit if opening but the specified node is already open.
return cellList.isFocusedOpen ? treeNodes.get(cellList.level + 1)
: null;
}
// Close the currently open node.
if (cellList.focusedKey != null) {
setChildState(cellList, cellList.focusedValue, false, fireEvents, false);
}
// Update the cell so it renders the styles correctly.
cellList.focusedValue = value;
cellList.focusedKey = cellList.getValueKey(value);
// Add the child node.
NodeInfo<?> childNodeInfo = isLeaf(value) ? null : getNodeInfo(value);
if (childNodeInfo != null) {
cellList.isFocusedOpen = true;
appendTreeNode(childNodeInfo, value);
} else {
cellList.isFocusedOpen = false;
}
// Refresh the display to update the styles for this node.
if (redraw) {
treeNodes.get(cellList.level).display.redraw();
}
if (cellList.isFocusedOpen) {
TreeNodeImpl<?> node = treeNodes.get(cellList.level + 1);
if (fireEvents) {
OpenEvent.fire(this, node);
}
return node.isDestroyed() ? null : node;
}
return null;
} else {
// Early exit if closing and the specified node or all nodes are closed.
if (cellList.focusedKey == null || !cellList.focusedKey.equals(newKey)) {
return null;
}
// Close the node.
TreeNode closedNode = cellList.isFocusedOpen
? treeNodes.get(cellList.level + 1) : null;
trimToLevel(cellList.level);
cellList.focusedKey = null;
cellList.focusedValue = null;
cellList.isFocusedOpen = false;
// Refresh the display to update the styles for this node.
if (redraw) {
treeNodes.get(cellList.level).display.redraw();
}
if (fireEvents && closedNode != null) {
CloseEvent.fire(this, closedNode);
}
}
return null;
}
/**
* Reduce the number of {@link HasData}s down to the specified level.
*
* @param level the level to trim to
*/
private void trimToLevel(int level) {
// Add a placeholder to maintain the same scroll width.
adjustScrollLock();
// Remove the views that are no longer needed.
int curLevel = treeNodes.size() - 1;
while (curLevel > level) {
TreeNodeImpl<?> removed = treeNodes.remove(curLevel);
removed.destroy();
curLevel--;
}
}
}