/* * 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.cell.client.Cell.Context; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.BrowserEvents; 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.safecss.shared.SafeStyles; import com.google.gwt.safecss.shared.SafeStylesBuilder; import com.google.gwt.safecss.shared.SafeStylesUtils; 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 <!DOCTYPE> * 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. */ // Use RepeatStyle.BOTH to ensure that we do not bundle the image. @ImageOptions(repeatStyle = RepeatStyle.Both, flipRtl = true) ImageResource cellBrowserOpenBackground(); /** * The background used for selected items. */ // Use RepeatStyle.BOTH to ensure that we do not bundle the image. @Source("cellTreeSelectedBackground.png") @ImageOptions(repeatStyle = RepeatStyle.Both, 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=\"{2}position:relative;outline:none;\">{3}<div>{4}</div></div>") SafeHtml div(int idx, String classes, SafeStyles padding, SafeHtml imageHtml, SafeHtml cellContents); @Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\"" + " style=\"{2}position:relative;outline:none;\" tabindex=\"{3}\">{4}<div>{5}</div></div>") SafeHtml divFocusable(int idx, String classes, SafeStyles padding, int tabIndex, SafeHtml imageHtml, SafeHtml cellContents); @Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\"" + " style=\"{2}position:relative;outline:none;\" tabindex=\"{3}\" accessKey=\"{4}\">{5}<div>{6}</div></div>") SafeHtml divFocusableWithKey(int idx, String classes, SafeStyles padding, int tabIndex, char accessKey, SafeHtml imageHtml, SafeHtml cellContents); @Template("<div style=\"{0}position:absolute;\">{1}</div>") SafeHtml imageWrapper(SafeStyles css, 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 currently selected value in this list. */ private T selectedValue; /** * A boolean indicating that this widget is no longer used. */ private boolean isDestroyed; /** * Indicates whether or not the focused value is open. */ private boolean isFocusedOpen; /** * Temporary element used to create elements from HTML. */ private final Element tmpElem = Document.get().createDivElement(); public BrowserCellList(final Cell<T> cell, int level, ProvidesKey<T> keyProvider) { super(cell, cellListResources, keyProvider); this.level = level; } protected void deselectValue() { SelectionModel<? super T> selectionModel = getSelectionModel(); if (selectionModel != null && selectedValue != null) { selectionModel.setSelected(selectedValue, false); } } @Override protected Element getCellParent(Element item) { return item.getFirstChildElement().getNextSiblingElement(); } @Override protected boolean isKeyboardNavigationSuppressed() { /* * Keyboard selection is never disabled in this list because we use it to * track the open node, but we want to suppress keyboard navigation if the * user disables it. */ return KeyboardSelectionPolicy.DISABLED == CellBrowser.this.getKeyboardSelectionPolicy() || super.isKeyboardNavigationSuppressed(); } @Override protected void onBrowserEvent2(Event event) { super.onBrowserEvent2(event); // Handle keyboard navigation between lists. String eventType = event.getType(); if (BrowserEvents.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); boolean isSelected = selectionModel == null ? false : selectionModel.isSelected(value); boolean isOpen = isOpen(i); 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(); Context context = new Context(i, 0, getValueKey(value)); cell.render(context, value, cellBuilder); // Figure out which image to use. SafeHtml image; if (isOpen) { image = openImageHtml; } else if (isLeaf(value)) { image = LEAF_IMAGE; } else { image = closedImageHtml; } SafeStyles padding = SafeStylesUtils.fromTrustedString("padding-right: " + imageWidth + "px;"); 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(), padding, getTabIndex(), getAccessKey(), image, cellBuilder.toSafeHtml())); } else { sb.append(template.divFocusable(i, classesBuilder.toString(), padding, getTabIndex(), image, cellBuilder.toSafeHtml())); } } else { sb.append(template.div(i, classesBuilder.toString(), padding, image, cellBuilder .toSafeHtml())); } } // Update the child state. updateChildState(this, true); } @Override protected void setKeyboardSelected(int index, boolean selected, boolean stealFocus) { super.setKeyboardSelected(index, selected, stealFocus); if (!isRowWithinBounds(index)) { return; } // Update the style. Element elem = getRowElement(index); T value = getPresenter().getVisibleItem(index); boolean isOpen = selected && isOpen(index); setStyleName(elem, style.cellBrowserOpenItem(), isOpen); // Update the image. SafeHtml image = null; if (isOpen) { image = openImageHtml; } else if (getTreeViewModel().isLeaf(value)) { image = LEAF_IMAGE; } else { image = closedImageHtml; } tmpElem.setInnerHTML(image.asString()); elem.replaceChild(tmpElem.getFirstChildElement(), elem.getFirstChildElement()); // Update the open state. updateChildState(this, true); } /** * Set the selected value in this list. If there is already a selected * value, the old value will be deselected. * * @param value the selected value */ protected void setSelectedValue(T value) { // Early exit if the value is unchanged. Object oldKey = getValueKey(selectedValue); Object newKey = getValueKey(value); if (newKey != null && newKey.equals(oldKey)) { return; } // Deselect the current value. Only one thing is selected at a time. deselectValue(); // Select the new value. SelectionModel<? super T> selectionModel = getSelectionModel(); if (selectionModel != null) { selectedValue = value; selectionModel.setSelected(selectedValue, true); } } /** * Check if the specified index is currently open. An index is open if it is * the keyboard selected index, there is an associated keyboard selected * value, and the value is not a leaf. * * @param index the index * @return true if open, false if not */ private boolean isOpen(int index) { T value = getPresenter().getKeyboardSelectedRowValue(); return index == getKeyboardSelectedRow() && value != null && !getTreeViewModel().isLeaf(value); } /** * 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.getPresenter().setKeyboardSelectedRow( treeNode.display.getKeyboardSelectedRow(), 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>>() { @Override 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); } } } }); } @Override public int getChildCount() { assertNotDestroyed(); return display.getPresenter().getVisibleItemCount(); } @Override public C getChildValue(int index) { assertNotDestroyed(); checkChildBounds(index); return display.getVisibleItem(index); } @Override public int getIndex() { assertNotDestroyed(); TreeNodeImpl<?> parent = getParent(); return (parent == null) ? 0 : parent.getOpenIndex(); } @Override public TreeNodeImpl<?> getParent() { assertNotDestroyed(); return getParentImpl(); } @Override public Object getValue() { return value; } @Override public boolean isChildLeaf(int index) { assertNotDestroyed(); checkChildBounds(index); return isLeaf(getChildValue(index)); } @Override public boolean isChildOpen(int index) { assertNotDestroyed(); checkChildBounds(index); return (display.focusedKey == null || !display.isFocusedOpen) ? false : display.focusedKey .equals(display.getValueKey(getChildValue(index))); } @Override public boolean isDestroyed() { if (nodeInfo != null) { /* * 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.display.getPresenter().flush(); } } return nodeInfo == null; } @Override public TreeNode setChildOpen(int index, boolean open) { return setChildOpen(index, open, true); } @Override public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) { assertNotDestroyed(); checkChildBounds(index); if (open) { // Open the child node. display.getPresenter().setKeyboardSelectedRow(index, false, true); return updateChildState(display, fireEvents); } else { // Close the child node if it is currently open. if (index == display.getKeyboardSelectedRow()) { display.getPresenter().clearKeyboardSelectedRowValue(); updateChildState(display, fireEvents); } return null; } } BrowserCellList<C> getDisplay() { return display; } /** * 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() { display.isDestroyed = true; valueChangeHandler.removeHandler(); display.deselectValue(); 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.getKeyboardSelectedRow() : -1; } /** * 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 (display.level == 0) ? null : treeNodes.get(display.level - 1); } } /** * 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()); } @Override public ImageResource cellListSelectedBackground() { return delegate.cellBrowserSelectedBackground(); } @Override 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; } @Override public String cellListEvenItem() { return delegate.cellBrowserEvenItem(); } @Override public String cellListKeyboardSelectedItem() { return delegate.cellBrowserKeyboardSelectedItem(); } @Override public String cellListOddItem() { return delegate.cellBrowserOddItem(); } @Override public String cellListSelectedItem() { return delegate.cellBrowserSelectedItem(); } @Override public String cellListWidget() { // Do not apply any style to the list itself. return null; } @Override public boolean ensureInjected() { return delegate.ensureInjected(); } @Override public String getName() { return delegate.getName(); } @Override 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, elem); } 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 * @see #setDefaultColumnWidth(int) */ public int getDefaultColumnWidth() { return defaultWidth; } /** * Get the minimum width of columns. * * @return the minimum width in pixels * @see #setMinimumColumnWidth(int) */ public int getMinimumColumnWidth() { return minWidth; } @Override public TreeNode getRootTreeNode() { return treeNodes.get(0); } @Override 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); } @Override public void onResize() { getSplitLayoutPanel().onResize(); } @Override public void setAnimationEnabled(boolean enable) { this.isAnimationEnabled = enable; } /** * Set the default width of new columns. * * @param width the default width in pixels * @see #getDefaultColumnWidth() */ public void setDefaultColumnWidth(int width) { this.defaultWidth = width; } /** * Set the minimum width of columns. * * @param minWidth the minimum width in pixels * @see #getMinimumColumnWidth() */ 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> TreeNode 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. Nullify the default * selection manager because it is provided by the node info. */ view.setSelectionModel(nodeInfo.getSelectionModel(), null); 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(); return treeNode; } /** * 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()); /* * A CellBrowser has a single keyboard selection policy and multiple lists, * so we're not using the selection policy in each list. Leave them on all * the time because we use keyboard selection to keep track of which item is * open (selected) at each level. */ display.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.ENABLED); 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()); SafeStylesBuilder cssBuilder = new SafeStylesBuilder(); if (LocaleInfo.getCurrentLocale().isRTL()) { cssBuilder.appendTrustedString("left:0px;"); } else { cssBuilder.appendTrustedString("right:0px;"); } cssBuilder.appendTrustedString("width: " + res.getWidth() + "px;"); cssBuilder.appendTrustedString("height: " + res.getHeight() + "px;"); return template.imageWrapper(cssBuilder.toSafeStyles(), image); } /** * Get the {@link SplitLayoutPanel} used to lay out the views. * * @return the {@link SplitLayoutPanel} */ private SplitLayoutPanel getSplitLayoutPanel() { return (SplitLayoutPanel) getWidget(); } /** * 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--; } // Nullify the focused key at the level. if (level < treeNodes.size()) { TreeNodeImpl<?> node = treeNodes.get(level); node.display.focusedKey = null; node.display.isFocusedOpen = false; } } /** * Update the state of a child node based on the keyboard selection of the * specified {@link BrowserCellList}. This method will open/close child * {@link TreeNode}s as needed. * * @param cellList the CellList that changed state. * @param fireEvents true to fireEvents * @return the open {@link TreeNode}, or null if not opened */ private <C> TreeNode updateChildState(BrowserCellList<C> cellList, boolean fireEvents) { /* * Verify that the specified list is still in the browser. It possible for * the list to receive deferred updates after it has been removed */ if (cellList.isDestroyed) { return null; } // Get the key of the value to open. C newValue = cellList.getPresenter().getKeyboardSelectedRowValue(); Object newKey = cellList.getValueKey(newValue); // Close the current open node. TreeNode closedNode = null; if (cellList.focusedKey != null && cellList.isFocusedOpen && !cellList.focusedKey.equals(newKey)) { // Get the node to close. closedNode = (treeNodes.size() > cellList.level + 1) ? treeNodes.get(cellList.level + 1) : null; // Close the node. trimToLevel(cellList.level); } // Open the new node. TreeNode openNode = null; boolean justOpenedNode = false; if (newKey != null) { if (newKey.equals(cellList.focusedKey)) { // The node is already open. openNode = cellList.isFocusedOpen ? treeNodes.get(cellList.level + 1) : null; } else { // Select this value. if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy()) { cellList.setSelectedValue(newValue); } // Add the child node if this node has children. cellList.focusedKey = newKey; NodeInfo<?> childNodeInfo = isLeaf(newValue) ? null : getNodeInfo(newValue); if (childNodeInfo != null) { cellList.isFocusedOpen = true; justOpenedNode = true; openNode = appendTreeNode(childNodeInfo, newValue); } } } /* * Fire event. We fire events after updating the view in case user event * handlers modify the open state of nodes, which would interrupt the * process. */ if (fireEvents) { if (closedNode != null) { CloseEvent.fire(this, closedNode); } if (openNode != null && justOpenedNode) { OpenEvent.fire(this, openNode); } } // Return the open node if it is still open. return (openNode == null || openNode.isDestroyed()) ? null : openNode; } }