| /* |
| * 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 <!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. |
| */ |
| @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--; |
| } |
| } |
| } |