Refactoring Tree code. SideBySideTreeView now uses SimpleCellList, which a protected method that allows users to use any ListView. SideBySideTreeNodeView shares a lot of code with SimpleCellList. Added animations to SideBySideTreeView. NodeInfo now includes a SelectionModel, but still allows a single instance to be used across an entire tree of different types. NodeInfo now returns one Cell instead of a list of HasCell, but CompositeCell has been added to combine HasCells into a single Cell. Review at http://gwt-code-reviews.appspot.com/390801 git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7967 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java index 32271d7..1248a46 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java +++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/Cell.java
@@ -1,12 +1,12 @@ /* * 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 @@ -20,7 +20,7 @@ /** * A light weight representation of a renderable object. - * + * * @param <C> the type that this Cell represents * @param <V> the type of view data that this cell consumes */ @@ -35,9 +35,18 @@ } /** + * Check if this cell depends on the selection state. + * + * @return true if dependant on selection, false if not + */ + public boolean dependsOnSelection() { + return false; + } + + /** * Handle a browser event that took place within the cell. The default * implementation returns null. - * + * * @param parent the parent Element * @param value the value associated with the cell * @param viewData the view data associated with the cell, or null @@ -53,7 +62,7 @@ /** * Render a cell as HTML into a StringBuilder, suitable for passing to * {@link Element#setInnerHTML} on a container element. - * + * * @param value the cell value to be rendered * @param viewData view data associated with the cell * @param sb the StringBuilder to be written to
diff --git a/bikeshed/src/com/google/gwt/bikeshed/cells/client/CompositeCell.java b/bikeshed/src/com/google/gwt/bikeshed/cells/client/CompositeCell.java new file mode 100644 index 0000000..902fb8d --- /dev/null +++ b/bikeshed/src/com/google/gwt/bikeshed/cells/client/CompositeCell.java
@@ -0,0 +1,159 @@ +/* + * 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.bikeshed.cells.client; + +import com.google.gwt.bikeshed.list.client.HasCell; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.NativeEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * <p> + * A {@link Cell} that is composed of other {@link Cell}s. + * </p> + * <p> + * When this cell is rendered, it will render each component {@link Cell} inside + * a span. If the component {@link Cell} uses block level elements (such as a + * Div), the component cells will stack vertically. + * </p> + * + * @param <C> the type that this Cell represents + * @param <V> the type of view data that this cell consumes + */ +public class CompositeCell<C, V> extends Cell<C, V> { + + /** + * The cells that compose this {@link Cell}. + */ + private List<HasCell<C, ?, V>> hasCells = new ArrayList<HasCell<C, ?, V>>(); + + /** + * Add a {@link HasCell} to the composite. + * + * @param hasCell the {@link HasCell} to add + */ + public void addHasCell(HasCell<C, ?, V> hasCell) { + hasCells.add(hasCell); + } + + @Override + public boolean consumesEvents() { + // TODO(jlabanca): Should we cache this value? Can it change? + for (HasCell<C, ?, V> hasCell : hasCells) { + if (hasCell.getCell().consumesEvents()) { + return true; + } + } + return false; + } + + @Override + public boolean dependsOnSelection() { + // TODO(jlabanca): Should we cache this value? Can it change? + for (HasCell<C, ?, V> hasCell : hasCells) { + if (hasCell.getCell().dependsOnSelection()) { + return true; + } + } + return false; + } + + /** + * Insert a {@link HasCell} into the composite. + * + * @param index the index to insert into + * @param hasCell the {@link HasCell} to insert + */ + public void insertHasCell(int index, HasCell<C, ?, V> hasCell) { + hasCells.add(index, hasCell); + } + + @Override + public V onBrowserEvent(Element parent, C value, V viewData, + NativeEvent event, ValueUpdater<C, V> valueUpdater) { + int index = 0; + Element target = event.getEventTarget().cast(); + Element wrapper = parent.getFirstChildElement(); + while (wrapper != null) { + if (wrapper.isOrHasChild(target)) { + return onBrowserEventImpl(wrapper, value, viewData, event, + valueUpdater, hasCells.get(index)); + } + + index++; + wrapper = wrapper.getNextSiblingElement(); + } + return viewData; + } + + /** + * Remove a {@link HasCell} from the composite. + * + * @param hasCell the {@link HasCell} to remove + */ + public void removeHasCell(HasCell<C, ?, V> hasCell) { + hasCells.remove(hasCell); + } + + @Override + public void render(C value, V viewData, StringBuilder sb) { + for (HasCell<C, ?, V> hasCell : hasCells) { + render(value, viewData, sb, hasCell); + } + } + + @Override + public void setValue(Element parent, C object, V viewData) { + for (HasCell<C, ?, V> hasCell : hasCells) { + setValueImpl(parent, object, viewData, hasCell); + } + } + + protected <X> void render(C value, V viewData, StringBuilder sb, + HasCell<C, X, V> hasCell) { + Cell<X, V> cell = hasCell.getCell(); + sb.append("<span>"); + cell.render(hasCell.getValue(value), viewData, sb); + sb.append("</span>"); + } + + private <X> V onBrowserEventImpl(Element parent, final C object, V viewData, + NativeEvent event, final ValueUpdater<C, V> valueUpdater, + final HasCell<C, X, V> hasCell) { + ValueUpdater<X, V> tempUpdater = null; + final FieldUpdater<C, X, V> fieldUpdater = hasCell.getFieldUpdater(); + if (fieldUpdater != null) { + tempUpdater = new ValueUpdater<X, V>() { + public void update(X value, V viewData) { + fieldUpdater.update(-1, object, value, viewData); + if (valueUpdater != null) { + valueUpdater.update(object, viewData); + } + } + }; + } + Cell<X, V> cell = hasCell.getCell(); + return cell.onBrowserEvent(parent, hasCell.getValue(object), viewData, + event, tempUpdater); + } + + private <X> void setValueImpl(Element parent, C object, V viewData, + HasCell<C, X, V> hasCell) { + hasCell.getCell().setValue(parent, hasCell.getValue(object), viewData); + } +}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java index c4a4228..69315ca 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/HasCell.java
@@ -1,12 +1,12 @@ /* * 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 @@ -22,15 +22,13 @@ * An interface for extracting a value from an underlying data type, provide a * cell to render that value, and provide a FieldUpdater to perform notification * of updates to the cell. - * + * * @param <T> the underlying data type * @param <C> the cell data type * @param <V> the view data type */ public interface HasCell<T, C, V> { - boolean dependsOnSelection(); - Cell<C, V> getCell(); FieldUpdater<T, C, V> getFieldUpdater();
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java index 3b13cb3..108f9e1 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/ListView.java
@@ -1,12 +1,12 @@ /* * 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 @@ -16,19 +16,20 @@ package com.google.gwt.bikeshed.list.client; import com.google.gwt.bikeshed.list.shared.Range; +import com.google.gwt.bikeshed.list.shared.SelectionModel; import java.util.List; /** * A list view. - * + * * @param <T> the data type of each row */ public interface ListView<T> { /** * A list view delegate, implemented by classes that supply data to a view. - * + * * @param <T> the data type of each row */ public interface Delegate<T> { @@ -39,7 +40,21 @@ Range getRange(); + /** + * Set a range of data in the view. + * + * @param start the start index of the data + * @param length the length of the data + * @param values the values within the range + */ void setData(int start, int length, List<T> values); void setDataSize(int size, boolean isExact); + + /** + * Set the {@link SelectionModel} used by this {@link ListView}. + * + * @param selectionModel the {@link SelectionModel} + */ + void setSelectionModel(SelectionModel<? super T> selectionModel); }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java index 6a1e047..8d98aa1 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
@@ -1,12 +1,12 @@ /* * 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 @@ -41,7 +41,7 @@ /** * A list view that supports paging and columns. - * + * * @param <T> the data type of each row */ public class PagingTableListView<T> extends Widget implements ListView<T> { @@ -70,7 +70,8 @@ private ProvidesKey<T> providesKey; private HandlerRegistration selectionHandler; - private SelectionModel<T> selectionModel; + private SelectionModel<? super T> selectionModel; + private TableElement table; private TableSectionElement tbody; private TableSectionElement tfoot; @@ -86,7 +87,7 @@ /** * Constructs a table with the given page size. - * + * * @param pageSize the page size */ public PagingTableListView(final int pageSize) { @@ -349,6 +350,8 @@ TableCellElement cell = row.getCells().getItem(c); StringBuilder sb = new StringBuilder(); columns.get(c).render(q, sb); + // TODO(jlabanca): Render as one HTML string instead of embedding HTML + // in each cell. cell.setInnerHTML(sb.toString()); // TODO: Really total hack! There's gotta be a better way... @@ -362,7 +365,7 @@ public void setDataSize(int size, boolean isExact) { this.size = size; - refresh(); + updateRowVisibility(); } public void setDelegate(Delegate<T> delegate) { @@ -371,9 +374,9 @@ /** * Set the number of rows per page and refresh the table. - * + * * @param pageSize the page size - * + * * @throw {@link IllegalArgumentException} if pageSize is negative or 0 */ public void setPageSize(int pageSize) { @@ -396,9 +399,9 @@ } /** - * Set the starting index of the current visible page. The actual page - * start will be clamped in the range [0, getSize() - 1]. - * + * Set the starting index of the current visible page. The actual page start + * will be clamped in the range [0, getSize() - 1]. + * * @param pageStart the index of the row that should appear at the start of * the page */ @@ -410,19 +413,16 @@ /** * Sets the {@link ProvidesKey} instance that will be used to generate keys * for each record object as needed. - * - * @param providesKey an instance of {@link ProvidesKey<T>} used to generate - * keys for record objects. + * + * @param providesKey an instance of {@link ProvidesKey} used to generate keys + * for record objects. */ - // TODO - when is this valid? Do we rehash column view data if it changes? + // TODO - when is this valid? Do we rehash column view data if it changes? public void setProvidesKey(ProvidesKey<T> providesKey) { this.providesKey = providesKey; } - /** - * Sets the selection model. - */ - public void setSelectionModel(SelectionModel<T> selectionModel) { + public void setSelectionModel(SelectionModel<? super T> selectionModel) { if (selectionHandler != null) { selectionHandler.removeHandler(); selectionHandler = null;
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java index 06156ca..d83fd87 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleCellList.java
@@ -1,12 +1,12 @@ /* * 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 @@ -17,172 +17,166 @@ import com.google.gwt.bikeshed.cells.client.Cell; import com.google.gwt.bikeshed.cells.client.ValueUpdater; +import com.google.gwt.bikeshed.list.client.impl.SimpleCellListImpl; import com.google.gwt.bikeshed.list.shared.Range; -import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter.DefaultRange; +import com.google.gwt.bikeshed.list.shared.SelectionModel; import com.google.gwt.dom.client.DivElement; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Style.Display; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Widget; -import java.util.ArrayList; import java.util.List; /** * A single column list of cells. - * + * * @param <T> the data type of list items */ public class SimpleCellList<T> extends Widget implements ListView<T> { + /** + * Style name applied to even rows. + * TODO(jlabanca): These style names only apply to SideBySideTreeView. + */ + private static final String STYLENNAME_EVEN = "gwt-sstree-evenRow"; + + /** + * Style name applied to odd rows. + * TODO(jlabanca): These style names only apply to SideBySideTreeView. + */ + private static final String STYLENNAME_ODD = "gwt-sstree-oddRow"; + + /** + * Style name applied to selected rows. + * TODO(jlabanca): These style names only apply to SideBySideTreeView. + */ + private static final String STYLENNAME_SELECTED = "gwt-sstree-selectedItem"; + private final Cell<T, Void> cell; - private final ArrayList<T> data = new ArrayList<T>(); - private Delegate<T> delegate; - private int increment; - private int initialMaxSize; - private int maxSize; - private int seq; // for debugging - TODO: remove - private final Element showFewerElem; - private final Element showMoreElem; - private int size; - private final Element tmpElem; + private final SimpleCellListImpl<T> impl; private ValueUpdater<T, Void> valueUpdater; public SimpleCellList(Cell<T, Void> cell, int maxSize, int increment) { - this.initialMaxSize = this.maxSize = maxSize; - this.increment = increment; this.cell = cell; - this.seq = 0; - tmpElem = Document.get().createDivElement(); + // Create the DOM hierarchy. + Element childContainer = Document.get().createDivElement(); - showMoreElem = Document.get().createDivElement(); - showMoreElem.setInnerHTML("<button>Show more</button>"); + Element showMoreElem = Document.get().createPushButtonElement(); + showMoreElem.setInnerText("Show more"); - showFewerElem = Document.get().createDivElement(); - showFewerElem.setInnerHTML("<button>Show fewer</button>"); + Element showFewerElem = Document.get().createPushButtonElement(); + showFewerElem.setInnerText("Show fewer"); - showOrHide(showMoreElem, false); - showOrHide(showFewerElem, false); + Element emptyMessageElem = Document.get().createDivElement(); + emptyMessageElem.setInnerHTML("<i>no data</i>"); // TODO: find some way for cells to communicate what they're interested in. DivElement outerDiv = Document.get().createDivElement(); - DivElement innerDiv = Document.get().createDivElement(); - outerDiv.appendChild(innerDiv); + outerDiv.appendChild(childContainer); + outerDiv.appendChild(emptyMessageElem); outerDiv.appendChild(showFewerElem); outerDiv.appendChild(showMoreElem); setElement(outerDiv); - sinkEvents(Event.ONCLICK); - sinkEvents(Event.ONCHANGE); + sinkEvents(Event.ONCLICK | Event.ONCHANGE | Event.MOUSEEVENTS); + + // Create the implementation. + impl = new SimpleCellListImpl<T>(this, cell, maxSize, increment, + childContainer, emptyMessageElem, showMoreElem, showFewerElem) { + + @Override + protected void emitHtml(StringBuilder sb, List<T> values, int start, + Cell<T, Void> cell, SelectionModel<? super T> selectionModel) { + 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); + sb.append("<div __idx='").append(i).append("'"); + sb.append(" class='"); + sb.append(i % 2 == 0 ? STYLENNAME_EVEN : STYLENNAME_ODD); + if (isSelected) { + sb.append(" ").append(STYLENNAME_SELECTED); + } + sb.append("'>"); + cell.render(value, null, sb); + sb.append("</div>"); + } + } + + @Override + protected void setSelected(Element elem, boolean selected) { + setStyleName(elem, STYLENNAME_SELECTED, selected); + } + }; } public Range getRange() { - return new DefaultRange(0, maxSize); + return impl.getRange(); } @Override public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + Element target = event.getEventTarget().cast(); - if (target.getParentElement() == showMoreElem) { - this.maxSize += increment; - sizeChanged(); - if (delegate != null) { - delegate.onRangeChanged(this); + int type = event.getTypeInt(); + if (type == Event.ONCLICK) { + // Open the node when the open image is clicked. + Element showFewerElem = impl.getShowFewerElem(); + Element showMoreElem = impl.getShowMoreElem(); + if (showFewerElem != null && showFewerElem.isOrHasChild(target)) { + impl.showFewer(); + return; + } else if (showMoreElem != null && showMoreElem.isOrHasChild(target)) { + impl.showMore(); + return; } - } else if (target.getParentElement() == showFewerElem) { - this.maxSize = Math.max(initialMaxSize, maxSize - increment); - sizeChanged(); - if (delegate != null) { - delegate.onRangeChanged(this); - } - } else { - String idxString = ""; - while ((target != null) - && ((idxString = target.getAttribute("__idx")).length() == 0)) { - target = target.getParentElement(); - } - if (idxString.length() > 0) { - int idx = Integer.parseInt(idxString); - cell.onBrowserEvent(target, data.get(idx), null, event, valueUpdater); + } + + // Forward the event to the cell. + String idxString = ""; + while ((target != null) + && ((idxString = target.getAttribute("__idx")).length() == 0)) { + target = target.getParentElement(); + } + if (idxString.length() > 0) { + int idx = Integer.parseInt(idxString); + T value = impl.getValue(idx); + cell.onBrowserEvent(target, value, null, event, valueUpdater); + if (!cell.consumesEvents() && type == Event.ONMOUSEDOWN) { + SelectionModel<? super T> selectionModel = impl.getSelectionModel(); + if (selectionModel != null) { + selectionModel.setSelected(value, true); + } } } } public void setData(int start, int length, List<T> values) { - // Construct a run of element from start (inclusive) to start + length (exclusive) - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < length; i++) { - sb.append("<div __idx='" + (start + i) + "' __seq='" + seq++ + "'>"); - cell.render(values.get(i), null, sb); - sb.append("</div>"); - } - - Element parent = getElement().getFirstChildElement(); - if (start == 0 && length == maxSize) { - parent.setInnerHTML(sb.toString()); - } else { - makeElements(); - tmpElem.setInnerHTML(sb.toString()); - for (int i = 0; i < length; i++) { - Element child = parent.getChild(start + i).cast(); - parent.replaceChild(tmpElem.getChild(0), child); - } - } + impl.setData(values, start); } public void setDataSize(int size, boolean isExact) { - this.size = size; - sizeChanged(); + impl.setDataSize(size); } public void setDelegate(Delegate<T> delegate) { - this.delegate = delegate; + impl.setDelegate(delegate); } + public void setSelectionModel(final SelectionModel<? super T> selectionModel) { + impl.setSelectionModel(selectionModel); + } + + /** + * Set the value updater to use when cells modify items. + * + * @param valueUpdater the {@link ValueUpdater} + */ public void setValueUpdater(ValueUpdater<T, Void> valueUpdater) { this.valueUpdater = valueUpdater; } - - private void makeElements() { - Element parent = getElement().getFirstChildElement(); - int childCount = parent.getChildCount(); - - int actualSize = Math.min(size, maxSize); - if (actualSize > childCount) { - // Create new elements with a "loading..." message - StringBuilder sb = new StringBuilder(); - int newElements = actualSize - childCount; - for (int i = 0; i < newElements; i++) { - sb.append("<div __idx='" + (childCount + i) + "'><i>loading...</i></div>"); - } - - if (childCount == 0) { - parent.setInnerHTML(sb.toString()); - } else { - tmpElem.setInnerHTML(sb.toString()); - for (int i = 0; i < newElements; i++) { - parent.appendChild(tmpElem.getChild(0)); - } - } - } else if (actualSize < childCount) { - // Remove excess elements - while (actualSize < childCount) { - parent.getChild(--childCount).removeFromParent(); - } - } - } - - private void showOrHide(Element element, boolean show) { - if (show) { - element.getStyle().clearDisplay(); - } else { - element.getStyle().setDisplay(Display.NONE); - } - } - - private void sizeChanged() { - showOrHide(showMoreElem, size > maxSize); - showOrHide(showFewerElem, maxSize > initialMaxSize); - } }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/impl/SimpleCellListImpl.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/impl/SimpleCellListImpl.java new file mode 100644 index 0000000..726aecc --- /dev/null +++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/impl/SimpleCellListImpl.java
@@ -0,0 +1,340 @@ +/* + * 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.bikeshed.list.client.impl; + +import com.google.gwt.bikeshed.cells.client.Cell; +import com.google.gwt.bikeshed.list.client.ListView; +import com.google.gwt.bikeshed.list.client.ListView.Delegate; +import com.google.gwt.bikeshed.list.shared.Range; +import com.google.gwt.bikeshed.list.shared.SelectionModel; +import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter.DefaultRange; +import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent; +import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeHandler; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.dom.client.Style.Display; +import com.google.gwt.event.shared.HandlerRegistration; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Implementation of {@link SimpleCellListImpl}. This class is subject to change + * or deletion. Do not rely on this class. + * + * @param <T> the data type of items in the list + */ +public abstract class SimpleCellListImpl<T> { + + private final Cell<T, Void> cell; + private final Element childContainer; + private final List<T> data = new ArrayList<T>(); + private final Set<Object> selectedKeys = new HashSet<Object>(); + private Delegate<T> delegate; + private final Element emptyMessageElem; + private final int increment; + private final int initialMaxSize; + private final ListView<T> listView; + private int maxSize; + private HandlerRegistration selectionHandler; + private SelectionModel<? super T> selectionModel; + private final Element showFewerElem; + private final Element showMoreElem; + private int size; + private final Element tmpElem; + + public SimpleCellListImpl(ListView<T> listView, Cell<T, Void> cell, + int maxSize, int increment, Element childContainer, + Element emptyMessageElem, Element showMoreElem, Element showFewerElem) { + this.cell = cell; + this.childContainer = childContainer; + this.emptyMessageElem = emptyMessageElem; + this.increment = increment; + this.initialMaxSize = maxSize; + this.listView = listView; + this.maxSize = maxSize; + this.showFewerElem = showFewerElem; + this.showMoreElem = showMoreElem; + tmpElem = Document.get().createDivElement(); + + showOrHide(showMoreElem, false); + showOrHide(showFewerElem, false); + showOrHide(emptyMessageElem, false); + } + + public Range getRange() { + return new DefaultRange(0, maxSize); + } + + public SelectionModel<? super T> getSelectionModel() { + return selectionModel; + } + + public Element getShowFewerElem() { + return showFewerElem; + } + + public Element getShowMoreElem() { + return showMoreElem; + } + + public T getValue(int i) { + return data.get(i); + } + + /** + * Set the data in the list. + * + * @param values the new data + * @param start the start index + */ + public void setData(List<T> values, int start) { + int oldSize = size; + int len = values.size(); + int end = start + len; + + // The size must be at least as large as the data. + if (end > oldSize) { + size = end; + sizeChanged(); + } + + // Create placeholders up to the specified index. + while (data.size() < start) { + data.add(null); + } + + // Insert the new values into the data array. + for (int i = start; i < end; i++) { + T value = values.get(i - start); + if (i < data.size()) { + data.set(i, value); + } else { + data.add(value); + + // Update our local cache of selected values. We only need to consider + // new values at this point. If any existing value changes its selection + // state, we'll find out from the selection model. + if (selectionModel != null && selectionModel.isSelected(value)) { + selectedKeys.add(getKey(value)); + } + } + } + + // Construct a run of element from start (inclusive) to start + len + // (exclusive) + StringBuilder sb = new StringBuilder(); + emitHtml(sb, values, start, cell, selectionModel); + + // Replace the DOM elements with the new rendered cells. + if (oldSize == 0 || (start == 0 && len >= oldSize)) { + childContainer.setInnerHTML(sb.toString()); + } else { + makeElements(); + tmpElem.setInnerHTML(sb.toString()); + Element toReplace = childContainer.getChild(start).cast(); + for (int i = start; i < end; i++) { + // The child will be removed from tmpElem, so always use index 0. + Element nextSibling = toReplace.getNextSiblingElement(); + childContainer.replaceChild(tmpElem.getChild(0), toReplace); + toReplace = nextSibling; + } + } + } + + /** + * Set the overall size of the list. + * + * @param size the overall size + */ + public void setDataSize(int size) { + this.size = size; + int toRemove = data.size() - size; + for (int i = 0; i < toRemove; i++) { + removeLastItem(); + } + sizeChanged(); + } + + public void setDelegate(Delegate<T> delegate) { + this.delegate = delegate; + } + + public void setSelectionModel(final SelectionModel<? super T> selectionModel) { + // Remove the old selection model. + if (selectionHandler != null) { + selectionHandler.removeHandler(); + selectionHandler = null; + } + + // Set the new selection model. + this.selectionModel = selectionModel; + if (selectionModel != null) { + selectionHandler = selectionModel.addSelectionChangeHandler(new SelectionChangeHandler() { + public void onSelectionChange(SelectionChangeEvent event) { + // Determine if our selection states are stale. + boolean dependsOnSelection = cell.dependsOnSelection(); + boolean refreshRequired = false; + Element cellElem = childContainer.getFirstChildElement(); + for (T value : data) { + boolean selected = selectionModel.isSelected(value); + Object key = getKey(value); + if (selected != selectedKeys.contains(key)) { + refreshRequired = true; + if (selected) { + selectedKeys.add(key); + } else { + selectedKeys.remove(key); + } + if (!dependsOnSelection) { + // The cell doesn't depend on Selection, so we only need to + // update the style. + setSelected(cellElem, selected); + } + } + cellElem = cellElem.getNextSiblingElement(); + } + + // Refresh the entire list if needed. + if (refreshRequired && dependsOnSelection) { + setData(data, 0); + } + } + }); + } + } + + /** + * Show fewer items. + */ + public void showFewer() { + this.maxSize = Math.max(initialMaxSize, maxSize - increment); + sizeChanged(); + if (delegate != null) { + delegate.onRangeChanged(listView); + } + } + + /** + * Show more items. + */ + public void showMore() { + this.maxSize += increment; + sizeChanged(); + if (delegate != null) { + delegate.onRangeChanged(listView); + } + } + + /** + * Construct the HTML that represents the list of items. + * + * @param sb the {@link StringBuilder} to build into + * @param values the values to render + * @param start the start index + * @param cell the cell to use as a renderer + * @param selectionModel the {@link SelectionModel} + */ + protected abstract void emitHtml(StringBuilder sb, List<T> values, int start, + Cell<T, Void> cell, SelectionModel<? super T> selectionModel); + + /** + * Remove the last element from the list. + */ + protected void removeLastItem() { + data.remove(data.size() - 1); + childContainer.getLastChild().removeFromParent(); + } + + /** + * Mark an element as selected or unselected. This is called when a cells + * selection state changes, but the cell does not depend on selection. + * + * @param elem the element to modify + * @param selected true if selected, false if not + */ + protected abstract void setSelected(Element elem, boolean selected); + + /** + * Get the key for a given item. + * + * @param value the item + * @return the key, or null if there is no selection model + */ + private Object getKey(T value) { + return selectionModel == null ? null + : selectionModel.getKeyProvider().getKey(value); + } + + /** + * Create placeholder elements that will be replaced with data. This is used s + * when replacing a subset of the list. + */ + private void makeElements() { + int childCount = childContainer.getChildCount(); + int actualSize = Math.min(data.size(), maxSize); + if (actualSize > childCount) { + // Create new elements with a "loading..." message + StringBuilder sb = new StringBuilder(); + int newElements = actualSize - childCount; + for (int i = 0; i < newElements; i++) { + // TODO(jlabanca): Make this I18N friendly. + sb.append("<div __idx='" + (childCount + i) + + "'><i>loading...</i></div>"); + } + + if (childCount == 0) { + childContainer.setInnerHTML(sb.toString()); + } else { + tmpElem.setInnerHTML(sb.toString()); + for (int i = 0; i < newElements; i++) { + childContainer.appendChild(tmpElem.getChild(0)); + } + } + } else if (actualSize < childCount) { + // Remove excess elements + while (actualSize < childCount) { + removeLastItem(); + childCount--; + } + } + } + + /** + * Show or hide an element. + * + * @param element the element + * @param show true to show, false to hide + */ + private void showOrHide(Element element, boolean show) { + if (show) { + element.getStyle().clearDisplay(); + } else { + element.getStyle().setDisplay(Display.NONE); + } + } + + /** + * Called when the size of the list changes. + */ + private void sizeChanged() { + showOrHide(showMoreElem, size > maxSize); + showOrHide(showFewerElem, maxSize > initialMaxSize); + showOrHide(emptyMessageElem, size == 0); + } +}
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MultiSelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/MultiSelectionModel.java similarity index 75% rename from bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MultiSelectionModel.java rename to bikeshed/src/com/google/gwt/bikeshed/list/shared/MultiSelectionModel.java index 6ebe91f..9d6d776 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MultiSelectionModel.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/MultiSelectionModel.java
@@ -1,19 +1,19 @@ /* * 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.sample.bikeshed.cookbook.client; +package com.google.gwt.bikeshed.list.shared; import com.google.gwt.bikeshed.list.shared.SelectionModel.AbstractSelectionModel; @@ -22,26 +22,34 @@ /** * A simple selection model that allows multiple objects to be selected. - * + * * @param <T> the record data type */ public class MultiSelectionModel<T> extends AbstractSelectionModel<T> { private Set<T> selectedSet = new TreeSet<T>(); + private Set<Object> selectedKeys = new TreeSet<Object>(); + /** + * Get the set of selected items. + * + * @return the set of selected items + */ public Set<T> getSelectedSet() { return selectedSet; } public boolean isSelected(T object) { - return selectedSet.contains(object); + return selectedKeys.contains(getKeyProvider().getKey(object)); } public void setSelected(T object, boolean selected) { if (selected) { selectedSet.add(object); + selectedKeys.add(getKeyProvider().getKey(object)); } else { selectedSet.remove(object); + selectedKeys.remove(getKeyProvider().getKey(object)); } scheduleSelectionChangeEvent(); }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java index 3b3a0ee..e6ef41f 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
@@ -1,12 +1,12 @@ /* * 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 @@ -25,7 +25,7 @@ /** * A model for selection within a list. - * + * * @param <T> the data type of records in the list */ public interface SelectionModel<T> extends HasHandlers { @@ -37,7 +37,7 @@ /** * Called when {@link SelectionChangeEvent} is fired. - * + * * @param event the {@link SelectionChangeEvent} that was fired */ void onSelectionChange(SelectionChangeEvent event); @@ -57,7 +57,7 @@ /** * Fires a selection change event on all registered handlers in the handler * manager. If no such handlers exist, this method will do nothing. - * + * * @param source the source of the handlers */ static void fire(SelectionModel<?> source) { @@ -69,7 +69,7 @@ /** * Gets the type associated with this event. - * + * * @return returns the handler type */ public static Type<SelectionChangeHandler> getType() { @@ -99,7 +99,7 @@ /** * A default implementation of SelectionModel that provides listener addition * and removal. - * + * * @param <T> the data type of records in the list */ public abstract class AbstractSelectionModel<T> implements SelectionModel<T> { @@ -127,7 +127,7 @@ */ public ProvidesKey<T> getKeyProvider() { if (keyProvider == null) { - keyProvider = new ProvidesKey<T>() { + keyProvider = new ProvidesKey<T>() { public Object getKey(T item) { return item; } @@ -137,6 +137,15 @@ } /** + * Set the key provider for items in this model. + * + * @param keyProvider the {@link ProvidesKey} + */ + public void setKeyProvider(ProvidesKey<T> keyProvider) { + this.keyProvider = keyProvider; + } + + /** * Schedules a {@link SelectionModel.SelectionChangeEvent} to fire at the * end of the current event loop. */ @@ -155,21 +164,21 @@ /** * Adds a {@link SelectionChangeEvent} handler. - * + * * @param handler the handler * @return the registration for the event */ HandlerRegistration addSelectionChangeHandler(SelectionChangeHandler handler); /** - * Returns a ProvidesKey instance that may be used to provide a unique - * key for each record. + * Returns a ProvidesKey instance that may be used to provide a unique key for + * each record. */ ProvidesKey<T> getKeyProvider(); /** * Check if an object is selected. - * + * * @param object the object * @return true if selected, false if not */ @@ -177,7 +186,7 @@ /** * Set the selected state of an object. - * + * * @param object the object to select or deselect * @param selected true to select, false to deselect */
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SingleSelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SingleSelectionModel.java similarity index 72% rename from bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SingleSelectionModel.java rename to bikeshed/src/com/google/gwt/bikeshed/list/shared/SingleSelectionModel.java index 7fae547..293c4c3 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SingleSelectionModel.java +++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SingleSelectionModel.java
@@ -1,30 +1,31 @@ /* * 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.sample.bikeshed.cookbook.client; +package com.google.gwt.bikeshed.list.shared; import com.google.gwt.bikeshed.list.shared.SelectionModel.AbstractSelectionModel; /** * A simple selection model that allows only one object to be selected a a time. - * + * * @param <T> the record data type */ public final class SingleSelectionModel<T> extends AbstractSelectionModel<T> { private T curSelection; + private Object curKey; /** * Gets the currently-selected object. @@ -34,14 +35,22 @@ } public boolean isSelected(T object) { - return object.equals(curSelection); + if (curSelection == null || curKey == null || object == null) { + return false; + } + ProvidesKey<T> keyProvider = getKeyProvider(); + Object newKey = keyProvider.getKey(object); + return curKey.equals(newKey); } public void setSelected(T object, boolean selected) { + Object key = getKeyProvider().getKey(object); if (selected) { curSelection = object; - } else if (object.equals(curSelection)) { + curKey = key; + } else if (curKey != null && curKey.equals(key)) { curSelection = null; + curKey = null; } scheduleSelectionChangeEvent(); }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java deleted file mode 100644 index 36e2b4c..0000000 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeNodeView.java +++ /dev/null
@@ -1,253 +0,0 @@ -/* - * 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.bikeshed.tree.client; - -import com.google.gwt.bikeshed.list.client.HasCell; -import com.google.gwt.bikeshed.tree.client.TreeViewModel.NodeInfo; -import com.google.gwt.dom.client.Document; -import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Style; -import com.google.gwt.dom.client.Style.Display; -import com.google.gwt.dom.client.Style.Position; -import com.google.gwt.dom.client.Style.Unit; -import com.google.gwt.user.client.ui.RequiresResize; - -import java.util.List; - -/** - * A tree view that displays each level in a side-by-side manner. - * - * @param <T> the type that this {@link TreeNodeView} contains - */ -public class SideBySideTreeNodeView<T> extends TreeNodeView<T> { - - private int columnWidth; - - private final int imageLeft; - - private int level; - - private int maxColumns; - - private String path; - - /** - * Construct a {@link TreeNodeView}. - * - * @param tree the parent {@link TreeView} - * @param parent the parent {@link TreeNodeView} - * @param parentNodeInfo the {@link NodeInfo} of the parent - * @param elem the outer element of this {@link TreeNodeView} - * @param value the value of this node - * @param maxColumns the maximum number of columns to display - */ - SideBySideTreeNodeView(final TreeView tree, final SideBySideTreeNodeView<?> parent, - NodeInfo<T> parentNodeInfo, Element elem, T value, int level, String path, - int columnWidth, int maxColumns) { - super(tree, parent, parentNodeInfo, value); - this.imageLeft = columnWidth - 16 - tree.getImageWidth(); - this.level = level; - this.path = path; - this.columnWidth = columnWidth; - this.maxColumns = maxColumns; - - setElement(elem); - } - - @Override - protected <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo, - Element childElem, C childValue, Void viewData, int idx) { - return new SideBySideTreeNodeView<C>(getTree(), this, nodeInfo, childElem, - childValue, level + 1, path + "-" + idx, columnWidth, maxColumns); - } - - @Override - protected <C> void emitHtml(StringBuilder sb, List<C> childValues, - List<HasCell<C, ?, Void>> hasCells, List<TreeNodeView<?>> savedViews) { - TreeView tree = getTree(); - TreeViewModel model = tree.getTreeViewModel(); - int imageWidth = tree.getImageWidth(); - - int idx = 0; - for (C childValue : childValues) { - sb.append("<div id=\"" + path + "-" + idx + - "\" class=\"gwt-sstree-unselectedItem gwt-sstree-" + - ((idx % 2) == 0 ? "even" : "odd") + "Row\"" + - " style=\"position:relative;padding-right:"); - sb.append(imageWidth); - sb.append("px;\">"); - if (savedViews.get(idx) != null) { - sb.append(tree.getOpenImageHtml(imageLeft)); - } else if (model.isLeaf(childValue, this)) { - sb.append(LEAF_IMAGE); - } else { - sb.append(tree.getClosedImageHtml(imageLeft)); - } - sb.append("<div class=\"gwt-sstree-cell\">"); - for (int i = 0; i < hasCells.size(); i++) { - sb.append("<span __idx='"); - sb.append(i); - sb.append("'>"); - render(sb, childValue, hasCells.get(i)); - sb.append("</span>"); - } - sb.append("</div></div>"); - - idx++; - } - } - - /** - * Ensure that the child container exists and return it. - * - * @return the child container - */ - @Override - protected Element ensureChildContainer() { - if (getChildContainer() == null) { - // Create the container within the top-level widget element. - Element container = createContainer(level); - container.setInnerHTML(""); - Element animFrame = container.appendChild( - Document.get().createDivElement()); - animFrame.getStyle().setPosition(Position.RELATIVE); - animFrame.setId("animFrame"); - setChildContainer(animFrame.appendChild(Document.get().createDivElement())); - } - - // TODO(jgw): Kind of a hack. We should probably be propagating onResize() - // down from the TreeView, but this is simpler for the moment. - TreeView tree = getTree(); - if (tree instanceof RequiresResize) { - ((RequiresResize) tree).onResize(); - } - - return getChildContainer(); - } - - /** - * @return the element that contains the rendered cell - */ - @Override - protected Element getCellParent() { - return getElement().getChild(1).cast(); - } - - @Override - protected Element getContainer() { - return getTree().getElement().getChild(level).cast(); - } - - /** - * @return the image element - */ - @Override - protected Element getImageElement() { - return getElement().getFirstChildElement(); - } - - @Override - protected int getImageLeft() { - return imageLeft; - } - - @Override - protected void postClose() { - destroyContainer(level); - } - - @Override - protected void preOpen() { - // Close siblings of this node - TreeNodeView<?> parentNode = getParentTreeNodeView(); - if (parentNode != null) { - int numSiblings = parentNode.getChildCount(); - for (int i = 0; i < numSiblings; i++) { - Element container = parentNode.getChildContainer().getChild(i).cast(); - - TreeNodeView<?> sibling = parentNode.getChildTreeNodeView(i); - if (sibling == this) { - container.setClassName("gwt-sstree-selectedItem"); - } else { - if (sibling.getState()) { - sibling.setState(false); - } - - container.setClassName("gwt-sstree-unselectedItem " + - (((i % 2) == 0) ? "gwt-sstree-evenRow" : "gwt-sstree-oddRow")); - } - } - } - } - - /** - * Returns the container for child nodes at the given level. - */ - private Element createContainer(int level) { - // Resize the root element - Element rootElement = getTree().getElement(); - rootElement.getStyle().setWidth(Math.min(maxColumns, level + 1) * columnWidth, Unit.PX); - - // Create children of the root container as needed. - int childCount = rootElement.getChildCount(); - while (childCount <= level) { - Element div = rootElement.appendChild(Document.get().createDivElement()); - div.setClassName("gwt-sstree-column"); - Style style = div.getStyle(); - style.setPosition(Position.ABSOLUTE); - style.setTop(0, Unit.PX); - style.setWidth(columnWidth, Unit.PX); - - childCount++; - } - - Element child = rootElement.getFirstChild().cast(); - - int x = Math.min(0, maxColumns - (level + 1)); - while (child != null) { - Style style = child.getStyle(); - style.setLeft(x * columnWidth, Unit.PX); - if (x < 0) { - style.setDisplay(Display.NONE); - } else { - style.clearDisplay(); - } - child = child.getNextSibling().cast(); - x++; - } - - return rootElement.getChild(level).cast(); - } - - /** - * Destroys the containers for child nodes at the given level and all - * subsequent levels. - */ - private void destroyContainer(int level) { - // Resize the root element - Element rootElement = getTree().getElement(); - rootElement.getStyle().setWidth((level + 1) * columnWidth, Unit.PX); - - // Create children of the root container as needed. - int childCount = rootElement.getChildCount(); - while (childCount > level) { - rootElement.removeChild(rootElement.getLastChild()); - childCount--; - } - - setChildContainer(null); - } -}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java index 2f048db..500c007 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java +++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/SideBySideTreeView.java
@@ -1,12 +1,12 @@ /* * 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 @@ -15,143 +15,615 @@ */ package com.google.gwt.bikeshed.tree.client; -import com.google.gwt.core.client.Scheduler; +import com.google.gwt.animation.client.Animation; +import com.google.gwt.bikeshed.cells.client.Cell; +import com.google.gwt.bikeshed.cells.client.ValueUpdater; +import com.google.gwt.bikeshed.list.client.ListView; +import com.google.gwt.bikeshed.list.client.SimpleCellList; +import com.google.gwt.bikeshed.list.shared.ProvidesKey; +import com.google.gwt.bikeshed.list.shared.Range; +import com.google.gwt.bikeshed.list.shared.SelectionModel; +import com.google.gwt.bikeshed.tree.client.TreeViewModel.NodeInfo; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.Node; -import com.google.gwt.dom.client.NodeList; -import com.google.gwt.dom.client.Style; +import com.google.gwt.dom.client.NativeEvent; +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.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.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 java.util.ArrayList; +import java.util.List; /** * A view of a tree. */ -public class SideBySideTreeView extends TreeView { - - protected int columnWidth; - - protected int maxColumns; +public class SideBySideTreeView extends TreeView implements ProvidesResize, + RequiresResize, HasAnimation { /** - * Construct a new {@link TreeView} that will display as many columns as - * needed. - * - * @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 columnWidth + * The element used in place of an image when a node has no children. */ - public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue, - int columnWidth) { - this(viewModel, rootValue, columnWidth, Integer.MAX_VALUE); + private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>"; + + /** + * The style name assigned to each column. + */ + private static final String STYLENAME_COLUMN = "gwt-sstree-column"; + + /** + * The style name assigned to each column. + */ + private static final String STYLENAME_OPEN = "gwt-sstree-openItem"; + + /** + * The prefix of the ID assigned to open cells. + */ + private static final String ID_PREFIX_OPEN = "__gwt-sstree-open-"; + + /** + * 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 (isAnimationEnabled()) { + // Animate the scrolling. + startScrollLeft = elem.getScrollLeft(); + run(250); + } else { + // Scroll instantly. + onComplete(); + } + } } /** + * A wrapper around a cell that adds an open button. + * + * @param <C> the data type of the cell + */ + private class CellDecorator<C> extends Cell<C, Void> { + + /** + * The cell used to render the inner contents. + */ + private final Cell<C, Void> cell; + + /** + * The level of this list view. + */ + private final int level; + + /** + * The key of the currently open item. + */ + private Object openKey; + + /** + * The key provider for the node. + */ + private final ProvidesKey<C> providesKey; + + /** + * Construct a new {@link CellDecorator}. + * + * @param nodeInfo the {@link NodeInfo} associated with the cell + * @param level the level of items rendered by this decorator + */ + public CellDecorator(NodeInfo<C> nodeInfo, int level) { + this.cell = nodeInfo.getCell(); + this.level = level; + this.providesKey = nodeInfo.getProvidesKey(); + } + + @Override + public boolean consumesEvents() { + return cell.consumesEvents(); + } + + @Override + public boolean dependsOnSelection() { + return cell.dependsOnSelection(); + } + + public Object getOpenKey() { + return openKey; + } + + @Override + public Void onBrowserEvent(Element parent, C value, Void viewData, + NativeEvent event, ValueUpdater<C, Void> valueUpdater) { + Element target = event.getEventTarget().cast(); + if (getImageElement(parent).isOrHasChild(target)) { + if (Event.getTypeInt(event.getType()) == Event.ONMOUSEDOWN) { + trimToLevel(level); + + // Remove style from currently open item. + Element curOpenItem = Document.get().getElementById(getOpenId()); + if (curOpenItem != null) { + replaceImageElement(curOpenItem.getParentElement(), false); + } + + // Save the key of the new open item and update the Element. + openKey = providesKey.getKey(value); + replaceImageElement(parent, true); + + // Add a tree node for the next level. + appendTreeNode(getTreeViewModel().getNodeInfo(value)); + } + return viewData; + } else { + return cell.onBrowserEvent(getCellParent(parent), value, viewData, + event, valueUpdater); + } + } + + @Override + public void render(C value, Void viewData, StringBuilder sb) { + boolean isOpen = (openKey == null) ? false + : openKey.equals(providesKey.getKey(value)); + int imageWidth = getImageWidth(); + sb.append("<div style='position:relative;padding-right:"); + sb.append(imageWidth); + sb.append("px;'"); + if (isOpen) { + sb.append(" class='").append(STYLENAME_OPEN).append("'"); + sb.append(" id='").append(getOpenId()).append("'"); + } + sb.append(">"); + if (isOpen) { + sb.append(getOpenImageHtml()); + } else if (getTreeViewModel().isLeaf(value)) { + sb.append(LEAF_IMAGE); + } else { + sb.append(getClosedImageHtml()); + } + sb.append("<div>"); + cell.render(value, viewData, sb); + sb.append("</div></div>"); + } + + @Override + public void setValue(Element parent, C value, Void viewData) { + cell.setValue(getCellParent(parent), value, viewData); + } + + /** + * Get the parent element of the decorated cell. + * + * @param parent the parent of this cell + * @return the decorated cell's parent + */ + private Element getCellParent(Element parent) { + return parent.getFirstChildElement().getChild(1).cast(); + } + + /** + * Get the image element of the decorated cell. + * + * @param parent the parent of this cell + * @return the image element + */ + private Element getImageElement(Element parent) { + return parent.getFirstChildElement().getFirstChildElement(); + } + + /** + * Get the ID of the open element. + * + * @return the ID + */ + private String getOpenId() { + return ID_PREFIX_OPEN + level + "-" + uniqueId; + } + + /** + * Replace the image element of a cell. + * + * @param parent the parent element of the cell + * @param open true if open, false if closed + */ + private void replaceImageElement(Element parent, boolean open) { + // Update the style name and ID. + Element wrapper = parent.getFirstChildElement(); + if (open) { + wrapper.addClassName(STYLENAME_OPEN); + wrapper.setId(getOpenId()); + } else { + wrapper.removeClassName(STYLENAME_OPEN); + wrapper.setId(""); + } + + // Replace the image element. + String html = open ? getOpenImageHtml() : getClosedImageHtml(); + Element tmp = Document.get().createDivElement(); + tmp.setInnerHTML(html); + Element imageElem = tmp.getFirstChildElement(); + Element oldImg = getImageElement(parent); + wrapper.replaceChild(imageElem, oldImg); + } + } + + /** + * A node in the tree. + * + * @param <C> the data type of the children of the node + */ + private class TreeNode<C> { + private CellDecorator<C> cell; + private ListView<C> listView; + private NodeInfo<C> nodeInfo; + private Widget widget; + + /** + * Construct a new {@link TreeNode}. + * + * @param nodeInfo the nodeInfo for the children nodes + * @param listView the list view assocated with the node + * @param widget the widget that represents the list view + */ + public TreeNode(NodeInfo<C> nodeInfo, ListView<C> listView, + CellDecorator<C> cell, Widget widget) { + this.cell = cell; + this.listView = listView; + this.nodeInfo = nodeInfo; + this.widget = widget; + } + + /** + * Get the {@link CellDecorator} used to render the node. + * + * @return the cell decorator + */ + public CellDecorator<C> getCell() { + return cell; + } + + /** + * Get the widget that represents this {@link TreeNode}. + * + * @return the widget + */ + public Widget getWidget() { + return widget; + } + + /** + * Unregister the list view and remove it from the widget. + */ + void cleanup() { + listView.setSelectionModel(null); + nodeInfo.unsetView(); + getSplitLayoutPanel().remove(widget); + } + } + + /** + * The counter used to assigned unique IDs. + */ + private static int NEXT_ID = 0; + + /** + * The animation used for scrolling. + */ + private final ScrollAnimation animation = new ScrollAnimation(); + + /** + * The default width of new columns. + */ + private int defaultWidth = 200; + + /** + * The HTML used to generate the closed image. + */ + private String closedImageHtml; + + /** + * The unique ID assigned to this tree view. + */ + private final int uniqueId = NEXT_ID++; + + /** + * A boolean indicating whether or not animations are enabled. + */ + private boolean isAnimationEnabled; + + /** + * The minimum width of new columns. + */ + private int minWidth = getImageWidth() + 20; + + /** + * The HTML used to generate the open image. + */ + private String openImageHtml; + + /** + * The element used to maintain the scrollbar when columns are removed. + */ + private Element scrollLock; + + /** + * The visible {@link TreeNode}. + */ + private List<TreeNode<?>> treeNodes = new ArrayList<TreeNode<?>>(); + + /** * Construct a new {@link TreeView}. - * + * * @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 columnWidth the width of each column - * @param maxColumns the maximum number of columns to display horizontally */ - public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue, - int columnWidth, int maxColumns) { - super(viewModel); + public <T> SideBySideTreeView(TreeViewModel viewModel, T rootValue) { + super(viewModel, new SplitLayoutPanel()); + getElement().getStyle().setOverflow(Overflow.AUTO); + setStyleName("gwt-SideBySideTreeView"); - this.columnWidth = columnWidth; - this.maxColumns = maxColumns; + // 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().setBackgroundColor("red"); + scrollLock.getStyle().setTop(0, Unit.PX); + scrollLock.getStyle().setLeft(0, Unit.PX); + scrollLock.getStyle().setHeight(1, Unit.PX); + scrollLock.getStyle().setWidth(1, Unit.PX); + getElement().appendChild(scrollLock); - Element rootElement = Document.get().createDivElement(); - rootElement.setClassName("gwt-sstree"); - Style style = rootElement.getStyle(); - style.setPosition(Position.RELATIVE); - style.setWidth(columnWidth, Unit.PX); - setElement(rootElement); + // Associate the first ListView with the rootValue. + appendTreeNode(viewModel.getNodeInfo(rootValue)); - // Add event handlers. - sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP | Event.ONCHANGE); + // Catch scroll events. + sinkEvents(Event.ONSCROLL); + } - // Associate a view with the item. - TreeNodeView<T> root = new SideBySideTreeNodeView<T>(this, null, null, - rootElement, rootValue, 0, "gwt-sstree", columnWidth, maxColumns); - setRootNode(root); - root.setState(true); + /** + * 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; + } + + public boolean isAnimationEnabled() { + return isAnimationEnabled; } @Override public void onBrowserEvent(Event event) { - super.onBrowserEvent(event); - - int eventType = DOM.eventGetType(event); - switch (eventType) { - case Event.ONMOUSEUP: case Event.ONCHANGE: - Element currentTarget = event.getCurrentEventTarget().cast(); - if (currentTarget == getElement()) { - Element target = event.getEventTarget().cast(); - String id = target.getId(); - boolean inCell = target.getClassName().equals("gwt-sstree-cell"); - while (id == null || !id.startsWith("gwt-sstree")) { - target = target.getParentElement(); - if (target == null) { - return; - } - - id = target.getId(); - inCell |= target.getClassName().equals("gwt-sstree-cell"); - } - - if (id.startsWith("gwt-sstree-")) { - id = id.substring(11); - String[] path = id.split("-"); - - TreeNodeView<?> nodeView = getRootTreeNodeView(); - for (String s : path) { - nodeView = nodeView.getChildTreeNodeView(Integer.parseInt(s)); - } - if (inCell) { - nodeView.fireEventToCell(event); - - // TODO(jgw): Kind of a hacky way to set selection. Need to generalize - // this to some sort of keyboard/mouse->selection controller. - if (getSelectionModel() != null) { - String type = event.getType(); - if ("mouseup".equals(type)) { - getSelectionModel().setSelected(nodeView.getValue(), true); - } - } - } else { - nodeView.setState(!nodeView.getState()); - } - } - } + switch (DOM.eventGetType(event)) { + case Event.ONSCROLL: + // Shorten the scroll bar is possible. + adjustScrollLock(); break; } + super.onBrowserEvent(event); } public void onResize() { - if (!isAttached()) { - return; - } + getSplitLayoutPanel().onResize(); + } - int height = getElement().getOffsetHeight(); - NodeList<Node> children = getElement().getChildNodes(); - for (int i = 0; i < children.getLength(); ++i) { - Element child = children.getItem(i).cast(); - child.getStyle().setHeight(height, Unit.PX); + 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; + } + + /** + * Set the minimum width of columns. + * + * @param minWidth the minimum width in pixels + */ + public void setMinimumColumnWidth(int minWidth) { + this.minWidth = minWidth; + } + + /** + * Create a {@link ListView} that will display items. The {@link ListView} + * must extend {@link com.google.gwt.user.client.ui.Widget}. + * + * @param <C> the item type in the list view + * @param nodeInfo the node info with child data + * @param cell the cell to use in the list view + * @return the {@link ListView} + */ + protected <C> ListView<C> createListView(NodeInfo<C> nodeInfo, + Cell<C, Void> cell) { + SimpleCellList<C> listView = new SimpleCellList<C>(cell, 100, 100); + listView.setValueUpdater(nodeInfo.getValueUpdater()); + return listView; + } + + /** + * Adjust the size of the scroll lock element based on the new position of the + * scroll bar. + */ + private void adjustScrollLock() { + int scrollLeft = getElement().getScrollLeft(); + if (scrollLeft > 0) { + int clientWidth = getElement().getClientWidth(); + scrollLock.getStyle().setWidth(scrollLeft + clientWidth, Unit.PX); + } else { + scrollLock.getStyle().setWidth(1.0, Unit.PX); } } - @Override - protected void onLoad() { - // TODO(jgw): This is a total hack. - Scheduler.get().scheduleFinally(new Scheduler.ScheduledCommand() { - public void execute() { - onResize(); + /** + * Create a new {@link TreeNode} and append it to the end of the LayoutPanel. + * + * @param <C> the data type of the children + * @param nodeInfo the info about the node + */ + private <C> void appendTreeNode(final NodeInfo<C> nodeInfo) { + // Create the list view and its scrollable container. + final int level = treeNodes.size(); + CellDecorator<C> cell = new CellDecorator<C>(nodeInfo, level); + final ListView<C> listView = createListView(nodeInfo, cell); + ScrollPanel scrollable = new ScrollPanel((Widget) listView); + scrollable.setStyleName(STYLENAME_COLUMN); + + // Create a delegate list view so we can trap data changes. + ListView<C> listViewDelegate = new ListView<C>() { + public Range getRange() { + return listView.getRange(); } - }); + + public void setData(int start, int length, List<C> values) { + // Trim to the current level if the open node no longer exists. + TreeNode<?> node = treeNodes.get(level); + Object openKey = node.getCell().openKey; + if (openKey != null) { + boolean stillExists = false; + ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey(); + for (C value : values) { + if (openKey.equals(keyProvider.getKey(value))) { + stillExists = true; + break; + } + } + if (!stillExists) { + trimToLevel(level); + } + } + + // Refresh the list. + listView.setData(start, length, values); + } + + public void setDataSize(int size, boolean isExact) { + listView.setDataSize(size, isExact); + } + + public void setDelegate(Delegate<C> delegate) { + listView.setDelegate(delegate); + } + + public void setSelectionModel(SelectionModel<? super C> selectionModel) { + listView.setSelectionModel(selectionModel); + } + }; + + // Create a TreeNode. + TreeNode<C> treeNode = new TreeNode<C>(nodeInfo, listViewDelegate, cell, + scrollable); + treeNodes.add(treeNode); + + // Attach the view to the selection model and node info. + listView.setSelectionModel(nodeInfo.getSelectionModel()); + nodeInfo.setView(listViewDelegate); + + // Add the ListView to the LayoutPanel. + SplitLayoutPanel splitPanel = getSplitLayoutPanel(); + splitPanel.insertWest(scrollable, defaultWidth, null); + splitPanel.setWidgetMinSize(scrollable, minWidth); + splitPanel.forceLayout(); + + // Scroll to the right. + animation.scrollToEnd(); + } + + /** + * @return the HTML to render the closed image. + */ + private String getClosedImageHtml() { + if (closedImageHtml == null) { + AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeClosed()); + closedImageHtml = proto.getHTML().replace("style='", + "style='position:absolute;right:0px;top:0px;"); + } + return closedImageHtml; + } + + /** + * @return the HTML to render the open image. + */ + private String getOpenImageHtml() { + if (openImageHtml == null) { + AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeOpen()); + openImageHtml = proto.getHTML().replace("style='", + "style='position:absolute;right:0px;top:0px;"); + } + return openImageHtml; + } + + /** + * 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 ListView} 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 listViews that are no longer needed. + int curLevel = treeNodes.size() - 1; + while (curLevel > level) { + TreeNode<?> removed = treeNodes.remove(curLevel); + removed.cleanup(); + curLevel--; + } } }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java index 73d662b..46399e6 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java +++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeNodeView.java
@@ -1,12 +1,12 @@ /* * 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 @@ -15,122 +15,616 @@ */ package com.google.gwt.bikeshed.tree.client; -import com.google.gwt.bikeshed.list.client.HasCell; +import com.google.gwt.bikeshed.cells.client.Cell; +import com.google.gwt.bikeshed.list.client.ListView; +import com.google.gwt.bikeshed.list.client.impl.SimpleCellListImpl; +import com.google.gwt.bikeshed.list.shared.ProvidesKey; +import com.google.gwt.bikeshed.list.shared.Range; import com.google.gwt.bikeshed.list.shared.SelectionModel; import com.google.gwt.bikeshed.tree.client.TreeViewModel.NodeInfo; 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.dom.client.Style.Position; +import com.google.gwt.user.client.ui.UIObject; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A view of a tree node. - * - * @param <T> the type that this {@link TreeNodeView} contains + * + * @param <T> the type that this view contains */ -public class StandardTreeNodeView<T> extends TreeNodeView<T> { +class StandardTreeNodeView<T> extends UIObject { /** - * Construct a {@link TreeNodeView}. - * - * @param tree the parent {@link TreeView} - * @param parent the parent {@link TreeNodeView} + * The element used in place of an image when a node has no children. + */ + private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>"; + + /** + * Style name applied to selected rows. + */ + private static final String STYLENNAME_SELECTED = "gwt-tree-selectedItem"; + + /** + * 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 nodeElem.getChild(1).cast(); + } + + /** + * 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); + } + } + + /** + * The {@link ListView} used to show children. + * + * @param <C> the child item type + */ + private static class NodeListView<C> implements ListView<C> { + + private final SimpleCellListImpl<C> impl; + private StandardTreeNodeView<?> nodeView; + private Map<Object, StandardTreeNodeView<?>> savedViews; + + public NodeListView(final NodeInfo<C> nodeInfo, + final StandardTreeNodeView<?> nodeView) { + this.nodeView = nodeView; + + impl = new SimpleCellListImpl<C>(this, nodeInfo.getCell(), 100, 50, + nodeView.ensureChildContainer(), nodeView.emptyMessageElem, + nodeView.showMoreElem, nodeView.showFewerElem) { + + @Override + public void setData(List<C> values, int start) { + // Ensure that we have a children array. + if (nodeView.children == null) { + nodeView.children = new ArrayList<StandardTreeNodeView<?>>(); + } + + // Construct a map of former child views based on their value keys. + int len = values.size(); + int end = start + len; + int childCount = nodeView.getChildCount(); + Map<Object, StandardTreeNodeView<?>> openNodes = new HashMap<Object, StandardTreeNodeView<?>>(); + for (int i = start; i < end && i < childCount; i++) { + StandardTreeNodeView<?> child = nodeView.getChildNode(i); + // Ignore child nodes that are closed. + if (child.isOpen()) { + openNodes.put(child.getValueKey(), child); + } + } + + // Hide the child container so we can animate it. + if (nodeView.tree.isAnimationEnabled()) { + nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE); + } + + // Trim the saved views down to the children that still exists. + ProvidesKey<C> providesKey = nodeInfo.getProvidesKey(); + savedViews = new HashMap<Object, StandardTreeNodeView<?>>(); + for (C childValue : values) { + // Remove any child elements that correspond to prior children + // so the call to setInnerHtml will not destroy them + Object key = providesKey.getKey(childValue); + StandardTreeNodeView<?> savedView = openNodes.remove(key); + if (savedView != null) { + savedView.ensureAnimationFrame().removeFromParent(); + savedViews.put(key, savedView); + } + } + + // Create the new cells. + super.setData(values, start); + + // Create the child TreeNodeViews from the new elements. + Element childElem = nodeView.ensureChildContainer().getFirstChildElement(); + for (int i = start; i < end; i++) { + C childValue = values.get(i - start); + StandardTreeNodeView<C> child = nodeView.createTreeNodeView( + nodeInfo, childElem, childValue, null); + StandardTreeNodeView<?> savedChild = savedViews.remove(providesKey.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.showFewerElem = savedChild.showFewerElem; + child.showMoreElem = savedChild.showMoreElem; + + // 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. + savedChild.listView.setNodeView(child); + + // Copy the child container element to the new child + child.getElement().appendChild(savedChild.ensureAnimationFrame()); + } + + if (childCount > i) { + if (savedChild == null) { + // Cleanup the child node if we aren't going to reuse it. + nodeView.children.get(i).cleanup(); + } + nodeView.children.set(i, child); + } else { + nodeView.children.add(child); + } + childElem = childElem.getNextSiblingElement(); + } + + // Clear temporary state. + savedViews = null; + + // Animate the child container open. + if (nodeView.tree.isAnimationEnabled()) { + nodeView.tree.maybeAnimateTreeNode(nodeView); + } + } + + @Override + protected void emitHtml(StringBuilder sb, List<C> values, int start, + Cell<C, Void> cell, SelectionModel<? super C> selectionModel) { + ProvidesKey<C> providesKey = nodeInfo.getProvidesKey(); + TreeViewModel model = nodeView.tree.getTreeViewModel(); + int imageWidth = nodeView.tree.getImageWidth(); + for (C value : values) { + Object key = providesKey.getKey(value); + sb.append("<div style=\"position:relative;padding-left:"); + sb.append(imageWidth); + sb.append("px;\">"); + if (savedViews.get(key) != null) { + sb.append(nodeView.tree.getOpenImageHtml()); + } else if (model.isLeaf(value)) { + sb.append(LEAF_IMAGE); + } else { + sb.append(nodeView.tree.getClosedImageHtml()); + } + if (selectionModel != null && selectionModel.isSelected(value)) { + sb.append("<div class='").append(STYLENNAME_SELECTED).append("'>"); + } else { + sb.append("<div>"); + } + cell.render(value, null, sb); + sb.append("</div></div>"); + } + } + + @Override + protected void removeLastItem() { + StandardTreeNodeView<?> child = nodeView.children.remove(nodeView.children.size() - 1); + child.cleanup(); + super.removeLastItem(); + } + + @Override + protected void setSelected(Element elem, boolean selected) { + setStyleName(getCellParent(elem), STYLENNAME_SELECTED, selected); + } + }; + } + + /** + * Cleanup this node view. + */ + public void cleanup() { + impl.setSelectionModel(null); + } + + public Range getRange() { + return impl.getRange(); + } + + public void setData(int start, int length, List<C> values) { + impl.setData(values, start); + } + + public void setDataSize(int size, boolean isExact) { + impl.setDataSize(size); + } + + public void setDelegate(Delegate<C> delegate) { + impl.setDelegate(delegate); + } + + public void setSelectionModel(final SelectionModel<? super C> selectionModel) { + impl.setSelectionModel(selectionModel); + } + + /** + * Assign this {@link ListView} to a new {@link StandardTreeNodeView}. + * + * @param nodeView the new node view + */ + private void setNodeView(StandardTreeNodeView<?> nodeView) { + this.nodeView.listView = null; + this.nodeView = nodeView; + nodeView.listView = this; + } + } + + /** + * 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<StandardTreeNodeView<?>> children; + + /** + * A reference to the element that contains all content. Parent of the + * childContainer and the show/hide elements. + */ + private Element contentContainer; + + /** + * The element used when there are no children to display. + */ + private Element emptyMessageElem; + + /** + * The list view used to display the nodes. + */ + private NodeListView<?> 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 StandardTreeNodeView}. + */ + private final StandardTreeNodeView<?> parentNode; + + /** + * The {@link NodeInfo} of the parent node. + */ + private final NodeInfo<T> parentNodeInfo; + + /** + * The element used to display less children. + */ + private Element showFewerElem; + + /** + * The element used to display more children. + */ + private Element showMoreElem; + + /** + * The {@link TreeView} that this node belongs to. + */ + private final StandardTreeView tree; + + /** + * This node's value. + */ + private T value; + + /** + * Construct a {@link StandardTreeNodeView}. + * + * @param tree the parent {@link StandardTreeNodeView} + * @param parent the parent {@link StandardTreeNodeView} * @param parentNodeInfo the {@link NodeInfo} of the parent - * @param elem the outer element of this {@link TreeNodeView} + * @param elem the outer element of this {@link StandardTreeNodeView} * @param value the value of this node */ - StandardTreeNodeView(final TreeView tree, + StandardTreeNodeView(final StandardTreeView tree, final StandardTreeNodeView<?> parent, NodeInfo<T> parentNodeInfo, Element elem, T value) { - super(tree, parent, parentNodeInfo, value); + this.tree = tree; + this.parentNode = parent; + this.parentNodeInfo = parentNodeInfo; + this.value = value; setElement(elem); } - @Override - protected <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo, - Element childElem, C childValue, Void viewData, int idx) { - return new StandardTreeNodeView<C>(getTree(), this, nodeInfo, childElem, + public int getChildCount() { + return children == null ? 0 : children.size(); + } + + public StandardTreeNodeView<?> getChildNode(int childIndex) { + return children.get(childIndex); + } + + /** + * Check whether or not this node is open. + * + * @return true if open, false if closed + */ + public boolean isOpen() { + return open; + } + + /** + * Select this node. + */ + public void select() { + SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel(); + if (selectionModel != null) { + selectionModel.setSelected(value, true); + } + } + + /** + * Sets whether this item's children are displayed. + * + * @param open whether the item is open + */ + public void setOpen(boolean open) { + // Early out. + if (this.open == open) { + return; + } + + this.animate = true; + this.open = open; + if (open) { + if (!nodeInfoLoaded) { + nodeInfoLoaded = true; + nodeInfo = tree.getTreeViewModel().getNodeInfo(value); + } + + // If we don't have any nodeInfo, we must be a leaf node. + if (nodeInfo != null) { + // Add a loading message. + ensureChildContainer().setInnerHTML(tree.getLoadingHtml()); + showOrHide(showFewerElem, false); + showOrHide(showMoreElem, false); + showOrHide(emptyMessageElem, false); + ensureAnimationFrame().getStyle().setProperty("display", ""); + onOpen(nodeInfo); + } + } else { + cleanup(); + tree.maybeAnimateTreeNode(this); + } + + // Update the image. + updateImage(); + } + + /** + * Unregister the list handler and destroy all child nodes. + */ + protected void cleanup() { + // Unregister the list handler. + if (listView != null) { + listView.cleanup(); + nodeInfo.unsetView(); + listView = null; + } + + // Recursively kill children. + if (children != null) { + for (StandardTreeNodeView<?> child : children) { + child.cleanup(); + } + children = null; + } + } + + 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> StandardTreeNodeView<C> createTreeNodeView( + NodeInfo<C> nodeInfo, Element childElem, C childValue, Void viewData) { + return new StandardTreeNodeView<C>(tree, this, nodeInfo, childElem, childValue); } - @Override - protected <C> void emitHtml(StringBuilder sb, List<C> childValues, - List<HasCell<C, ?, Void>> hasCells, List<TreeNodeView<?>> savedViews) { - TreeView tree = getTree(); - TreeViewModel model = tree.getTreeViewModel(); - int imageWidth = tree.getImageWidth(); - - SelectionModel<Object> selectionModel = tree.getSelectionModel(); - - int idx = 0; - for (C childValue : childValues) { - sb.append("<div style=\"position:relative;padding-left:"); - sb.append(imageWidth); - sb.append("px;\">"); - if (savedViews.get(idx) != null) { - sb.append(tree.getOpenImageHtml(0)); - } else if (model.isLeaf(childValue, this)) { - sb.append(LEAF_IMAGE); - } else { - sb.append(tree.getClosedImageHtml(0)); - } - if (selectionModel != null && selectionModel.isSelected(childValue)) { - sb.append("<div class='gwt-stree-selectedItem'>"); - } else { - sb.append("<div>"); - } - - for (int i = 0; i < hasCells.size(); i++) { - sb.append("<span __idx='"); - sb.append(i); - sb.append("'>"); - render(sb, childValue, hasCells.get(i)); - sb.append("</span>"); - } - - sb.append("</div></div>"); + /** + * Fire an event to the {@link com.google.gwt.bikeshed.cells.client.Cell}. + * + * @param event the native event + * @return true if the cell consumes the event, false if not + */ + protected boolean fireEventToCell(NativeEvent event) { + if (parentNodeInfo != null) { + Element cellParent = getCellParent(); + Cell<T, Void> parentCell = parentNodeInfo.getCell(); + parentCell.onBrowserEvent(cellParent, value, null, event, + parentNodeInfo.getValueUpdater()); + return parentCell.consumesEvents(); } + return false; } /** - * Ensure that the child container exists and return it. - * - * @return the child container + * Returns the element that parents the cell contents of this node. */ - @Override - protected Element ensureChildContainer() { - if (getChildContainer() == null) { - // If this is a root node or the element does not exist, create it. - Element animFrame = getElement().appendChild( - Document.get().createDivElement()); - animFrame.getStyle().setPosition(Position.RELATIVE); - animFrame.getStyle().setOverflow(Overflow.HIDDEN); - animFrame.setId("animFrame"); - setChildContainer(animFrame.appendChild(Document.get().createDivElement())); - } - return getChildContainer(); - } - - /** - * @return the element that contains the rendered cell - */ - @Override protected Element getCellParent() { - return getElement().getChild(1).cast(); + return getCellParent(getElement()); } /** - * @return the image element + * Returns the element corresponding to the open/close image. + * + * @return the open/close image element */ - @Override protected Element getImageElement() { return getElement().getFirstChildElement(); } - @Override - protected void postClose() { - getTree().maybeAnimateTreeNode(this); + /** + * 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) { + NodeListView<C> view = new NodeListView<C>(nodeInfo, this); + listView = view; + view.setSelectionModel(nodeInfo.getSelectionModel()); + nodeInfo.setView(view); + } + + /** + * Update the image based on the current state. + */ + protected void updateImage() { + // Early out if this is a root node. + if (parentNode == null) { + return; + } + + // Replace the image element with a new one. + String html = open ? tree.getOpenImageHtml() : tree.getClosedImageHtml(); + if (nodeInfoLoaded && nodeInfo == null) { + html = LEAF_IMAGE; + } + Element tmp = Document.get().createDivElement(); + tmp.setInnerHTML(html); + Element imageElem = tmp.getFirstChildElement(); + + Element oldImg = getImageElement(); + oldImg.getParentElement().replaceChild(imageElem, oldImg); + } + + /** + * Ensure that the animation frame exists and return it. + * + * @return the animation frame + */ + Element ensureAnimationFrame() { + if (animationFrame == null) { + animationFrame = Document.get().createDivElement(); + animationFrame.getStyle().setPosition(Position.RELATIVE); + animationFrame.getStyle().setOverflow(Overflow.HIDDEN); + animationFrame.setId("animFrame"); + 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); + + emptyMessageElem = Document.get().createDivElement(); + emptyMessageElem.setInnerHTML("<i>no data</i>"); + showOrHide(emptyMessageElem, false); + contentContainer.appendChild(emptyMessageElem); + + showMoreElem = Document.get().createPushButtonElement(); + showMoreElem.setInnerText("Show more"); + showOrHide(showMoreElem, false); + contentContainer.appendChild(showMoreElem); + + showFewerElem = Document.get().createPushButtonElement(); + showFewerElem.setInnerText("Show fewer"); + showOrHide(showFewerElem, false); + contentContainer.appendChild(showFewerElem); + } + return contentContainer; + } + + Element getShowFewerElement() { + return showFewerElem; + } + + Element getShowMoreElement() { + return showMoreElem; + } + + void showFewer() { + listView.impl.showFewer(); + } + + void showMore() { + listView.impl.showMore(); } }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java index a2a78ff..1be9854 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java +++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/StandardTreeView.java
@@ -1,12 +1,12 @@ /* * 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 @@ -15,12 +15,13 @@ */ package com.google.gwt.bikeshed.tree.client; -import com.google.gwt.dom.client.Document; +import com.google.gwt.animation.client.Animation; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style.Display; import com.google.gwt.dom.client.Style.Position; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.AbstractImagePrototype; import com.google.gwt.user.client.ui.HasAnimation; import java.util.ArrayList; @@ -31,14 +32,49 @@ public class StandardTreeView extends TreeView implements HasAnimation { /** - * A {@link TreeView.TreeNodeViewAnimation} that reveals the contents of child - * nodes. + * A node animation. */ - public static class RevealAnimation extends TreeNodeViewAnimation { + public abstract static class NodeAnimation extends Animation { + + /** + * The default animation delay in milliseconds. + */ + private static final int DEFAULT_ANIMATION_DURATION = 450; + + /** + * The duration of the animation. + */ + private int duration = DEFAULT_ANIMATION_DURATION; + + NodeAnimation() { + } + + /** + * Animate a tree node into its new state. + * + * @param node the node to animate + * @param isAnimationEnabled true to animate + */ + abstract void animate(StandardTreeNodeView<?> node, + boolean isAnimationEnabled); + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + } + + /** + * A {@link NodeAnimation} that reveals the contents of child nodes. + */ + public static class RevealAnimation extends NodeAnimation { /** * Create a new {@link RevealAnimation}. - * + * * @return the new animation */ public static RevealAnimation create() { @@ -46,44 +82,52 @@ } /** - * The container that holds the child container. + * The container that holds the content, includind the children. */ - Element animFrame; - /** - * The container that holds the children. - */ - Element childContainer; + Element contentContainer; /** * The target height when opening, the start height when closing. */ int height; + /** * True if the node is opening, false if closing. */ boolean opening; /** + * The container that holds the child container. + */ + private Element animFrame; + + /** + * The container that holds the children. + */ + private Element childContainer; + + /** * Not instantiable. */ private RevealAnimation() { } /** - * Animate a {@link TreeNodeView} into its new state. - * - * @param node the {@link TreeNodeView} to animate + * Animate a {@link StandardTreeNodeView} into its new state. + * + * @param node the {@link StandardTreeNodeView} to animate * @param isAnimationEnabled true to animate */ @Override - public void animate(TreeNodeView<?> node, boolean isAnimationEnabled) { + void animate(StandardTreeNodeView<?> node, boolean isAnimationEnabled) { // Cancel any pending animations. cancel(); // Initialize the fields. - this.opening = node.getState(); - childContainer = node.getChildContainer(); - animFrame = childContainer.getParentElement(); + this.opening = node.isOpen(); + animFrame = node.ensureAnimationFrame(); + contentContainer = node.ensureContentContainer(); + childContainer = node.ensureChildContainer(); if (isAnimationEnabled) { // Animated. @@ -111,9 +155,9 @@ if (opening) { animFrame.getStyle().setHeight(1.0, Unit.PX); animFrame.getStyle().clearDisplay(); - height = childContainer.getScrollHeight(); + height = contentContainer.getScrollHeight(); } else { - height = childContainer.getOffsetHeight(); + height = contentContainer.getOffsetHeight(); } } @@ -139,18 +183,19 @@ childContainer.setInnerHTML(""); } animFrame.getStyle().clearHeight(); + this.contentContainer = null; this.childContainer = null; this.animFrame = null; } } /** - * A {@link TreeView.TreeNodeViewAnimation} that slides children into view. + * A {@link NodeAnimation} that slides children into view. */ public static class SlideAnimation extends RevealAnimation { /** * Create a new {@link RevealAnimation}. - * + * * @return the new animation */ public static SlideAnimation create() { @@ -165,9 +210,9 @@ @Override protected void onComplete() { - childContainer.getStyle().clearPosition(); - childContainer.getStyle().clearTop(); - childContainer.getStyle().clearWidth(); + contentContainer.getStyle().clearPosition(); + contentContainer.getStyle().clearTop(); + contentContainer.getStyle().clearWidth(); super.onComplete(); } @@ -175,11 +220,11 @@ protected void onStart() { super.onStart(); if (opening) { - childContainer.getStyle().setTop(-height, Unit.PX); + contentContainer.getStyle().setTop(-height, Unit.PX); } else { - childContainer.getStyle().setTop(0, Unit.PX); + contentContainer.getStyle().setTop(0, Unit.PX); } - childContainer.getStyle().setPosition(Position.RELATIVE); + contentContainer.getStyle().setPosition(Position.RELATIVE); } @Override @@ -187,38 +232,92 @@ super.onUpdate(progress); if (opening) { double curTop = (1.0 - progress) * -height; - childContainer.getStyle().setTop(curTop, Unit.PX); + contentContainer.getStyle().setTop(curTop, Unit.PX); } else { double curTop = progress * -height; - childContainer.getStyle().setTop(curTop, Unit.PX); + contentContainer.getStyle().setTop(curTop, Unit.PX); } } } /** + * The animation. + */ + private NodeAnimation animation; + + /** + * The HTML used to generate the closed image. + */ + private String closedImageHtml; + + /** + * Indicates whether or not animations are enabled. + */ + private boolean isAnimationEnabled; + + /** + * The message displayed while child nodes are loading. + */ + // TODO(jlabanca): I18N loading HTML, or remove the text. + private String loadingHtml = "Loading..."; + + /** + * The HTML used to generate the open image. + */ + private String openImageHtml; + + /** + * The hidden root node in the tree. + */ + private StandardTreeNodeView<?> rootNode; + + /** * Construct a new {@link TreeView}. - * + * * @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> StandardTreeView(TreeViewModel viewModel, T rootValue) { super(viewModel); - setElement(Document.get().createDivElement()); - setStyleName("gwt-TreeView"); + setStyleName("gwt-StandardTreeView"); // We use one animation for the entire tree. setAnimation(SlideAnimation.create()); // Add event handlers. - sinkEvents(Event.ONCLICK | Event.ONMOUSEDOWN | Event.ONMOUSEUP - | Event.ONCHANGE); + sinkEvents(Event.ONCLICK | Event.ONCHANGE | Event.MOUSEEVENTS); // Associate a view with the item. - TreeNodeView<T> root = new StandardTreeNodeView<T>(this, null, null, - getElement(), rootValue); - setRootNode(root); - root.setState(true); + StandardTreeNodeView<T> root = new StandardTreeNodeView<T>(this, null, + null, getElement(), rootValue); + rootNode = root; + root.setOpen(true); + } + + /** + * Get the animation used to open and close nodes in this tree if animations + * are enabled. + * + * @return the animation + * @see #isAnimationEnabled() + */ + public NodeAnimation getAnimation() { + return animation; + } + + /** + * Get the HTML string that is displayed while nodes wait for their children + * to load. + * + * @return the loading HTML string + */ + public String getLoadingHtml() { + return loadingHtml; + } + + public boolean isAnimationEnabled() { + return isAnimationEnabled; } @Override @@ -226,57 +325,126 @@ super.onBrowserEvent(event); Element target = event.getEventTarget().cast(); - TreeNodeView<?> rootNode = getRootTreeNodeView(); ArrayList<Element> chain = new ArrayList<Element>(); - ArrayList<String> ids = new ArrayList<String>(); - collectElementChain(chain, ids, getElement(), target); + collectElementChain(chain, getElement(), target); - TreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode); + StandardTreeNodeView<?> nodeView = findItemByChain(chain, 0, rootNode); if (nodeView != null && nodeView != rootNode) { - if (nodeView.getImageElement().isOrHasChild(target)) { - if ("click".equals(event.getType())) { - nodeView.setState(!nodeView.getState()); + if ("click".equals(event.getType())) { + // Open the node when the open image is clicked. + Element showFewerElem = nodeView.getShowFewerElement(); + Element showMoreElem = nodeView.getShowMoreElement(); + if (nodeView.getImageElement().isOrHasChild(target)) { + nodeView.setOpen(!nodeView.isOpen()); + return; + } else if (showFewerElem != null && showFewerElem.isOrHasChild(target)) { + nodeView.showFewer(); + return; + } else if (showMoreElem != null && showMoreElem.isOrHasChild(target)) { + nodeView.showMore(); + return; } - } else if (nodeView.getCellParent().isOrHasChild(target)) { - nodeView.fireEventToCell(event); + } - // TODO(jgw): Kind of a hacky way to set selection. Need to generalize - // this to some sort of keyboard/mouse->selection controller. - if (getSelectionModel() != null) { - if ("click".equals(event.getType())) { - getSelectionModel().setSelected(nodeView.getValue(), true); - } + // Forward the event to the cell. + if (nodeView.getCellParent().isOrHasChild(target)) { + boolean consumesEvent = nodeView.fireEventToCell(event); + if (!consumesEvent && "click".equals(event.getType())) { + nodeView.select(); } } } } /** + * Set the animation used to open and close nodes in this tree. You must call + * {@link #setAnimationEnabled(boolean)} to enable or disable animation. + * + * @param animation a {@link NodeAnimation} + * @see #setAnimationEnabled(boolean) + */ + public void setAnimation(NodeAnimation animation) { + assert animation != null : "animation cannot be null"; + this.animation = animation; + } + + public void setAnimationEnabled(boolean enable) { + this.isAnimationEnabled = enable; + if (!enable && animation != null) { + animation.cancel(); + } + } + + /** + * Set the HTML string that will be displayed when a node is waiting for its + * child nodes to load. + * + * @param loadingHtml the HTML string + */ + public void setLoadingHtml(String loadingHtml) { + this.loadingHtml = loadingHtml; + } + + /** + * @return the HTML to render the closed image. + */ + String getClosedImageHtml() { + if (closedImageHtml == null) { + AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeClosed()); + closedImageHtml = proto.getHTML().replace("style='", + "style='position:absolute;left:0px;top:0px;"); + } + return closedImageHtml; + } + + /** + * @return the HTML to render the open image. + */ + String getOpenImageHtml() { + if (openImageHtml == null) { + AbstractImagePrototype proto = AbstractImagePrototype.create(getResources().treeOpen()); + openImageHtml = proto.getHTML().replace("style='", + "style='position:absolute;left:0px;top:0px;"); + } + return openImageHtml; + } + + /** + * Animate the current state of a {@link StandardTreeNodeView} in this tree. + * + * @param node the node to animate + */ + void maybeAnimateTreeNode(StandardTreeNodeView<?> node) { + if (animation != null) { + animation.animate(node, node.consumeAnimate() && isAnimationEnabled()); + } + } + + /** * Collects parents going up the element tree, terminated at the tree root. */ - private void collectElementChain(ArrayList<Element> chain, - ArrayList<String> ids, Element hRoot, Element hElem) { + private void collectElementChain(ArrayList<Element> chain, Element hRoot, + Element hElem) { if ((hElem == null) || (hElem == hRoot)) { return; } - collectElementChain(chain, ids, hRoot, hElem.getParentElement()); + collectElementChain(chain, hRoot, hElem.getParentElement()); chain.add(hElem); - ids.add(hElem.getId()); } - private TreeNodeView<?> findItemByChain(ArrayList<Element> chain, int idx, - TreeNodeView<?> parent) { + private StandardTreeNodeView<?> findItemByChain(ArrayList<Element> chain, + int idx, StandardTreeNodeView<?> parent) { if (idx == chain.size()) { return parent; } Element hCurElem = chain.get(idx); for (int i = 0, n = parent.getChildCount(); i < n; ++i) { - TreeNodeView<?> child = parent.getChildTreeNodeView(i); + StandardTreeNodeView<?> child = parent.getChildNode(i); if (child.getElement() == hCurElem) { - TreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child); + StandardTreeNodeView<?> retItem = findItemByChain(chain, idx + 1, child); if (retItem == null) { return child; }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java deleted file mode 100644 index 690b556..0000000 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNode.java +++ /dev/null
@@ -1,34 +0,0 @@ -/* - * 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.bikeshed.tree.client; - -/** - * A interface allowing navigation within a tree view. - * - * @param <T> the type of data stored at this node - */ -public interface TreeNode<T> { - - TreeNode<?> getChildNode(int childIndex); - - int getChildCount(); - - TreeNode<?> getParentNode(); - - T getValue(); - - boolean isOpen(); -}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java deleted file mode 100644 index 0def3f8..0000000 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java +++ /dev/null
@@ -1,566 +0,0 @@ -/* - * 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.bikeshed.tree.client; - -import com.google.gwt.bikeshed.list.client.HasCell; -import com.google.gwt.bikeshed.list.client.ListView; -import com.google.gwt.bikeshed.list.shared.ProvidesKey; -import com.google.gwt.bikeshed.list.shared.Range; -import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter.DefaultRange; -import com.google.gwt.bikeshed.tree.client.TreeViewModel.NodeInfo; -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.user.client.Window; -import com.google.gwt.user.client.ui.UIObject; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * A view of a tree node. - * - * @param <T> the type that this {@link TreeNodeView} contains - */ -public abstract class TreeNodeView<T> extends UIObject implements TreeNode<T> { - - /** - * A NodeInfo and list of data items of matching type. - * - * @param <C> the data type - */ - static class SavedInfo<C> { - NodeInfo<C> nodeInfo; - List<C> values; - } - - /** - * The element used in place of an image when a node has no children. - */ - public static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>"; - - /** - * True during the time a node should be animated. - */ - private boolean animate; - - /** - * A reference to the element that contains the children. - */ - private Element childContainer; - - /** - * A reference to the element that contains the children. - */ - private List<TreeNodeView<?>> children; - - /** - * The list registration for the list of children. - */ - private ListView<?> 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 SideBySideTreeNodeView}. - */ - private final TreeNodeView<?> parentNode; - - /** - * The parent {@link SideBySideTreeNodeView}. - */ - private final NodeInfo<T> parentNodeInfo; - - /** - * The NodeInfo for this node along with saved child data values. - */ - private SavedInfo<?> savedInfo; - - /** - * The info about this node. - */ - private final TreeView tree; - - /** - * This node's value. - */ - private T value; - - public TreeNodeView(TreeView tree, TreeNodeView<?> parent, NodeInfo<T> parentNodeInfo, T value) { - this.tree = tree; - this.parentNode = parent; - this.parentNodeInfo = parentNodeInfo; - this.value = value; - } - - public int getChildCount() { - return children == null ? 0 : children.size(); - } - - public TreeNode<?> getChildNode(int childIndex) { - return children.get(childIndex); - } - - public TreeNodeView<?> getChildTreeNodeView(int childIndex) { - return children.get(childIndex); - } - - public TreeNode<?> getParentNode() { - return parentNode; - } - - public TreeNodeView<?> getParentTreeNodeView() { - return parentNode; - } - - /** - * Check whether or not this {@link TreeNodeView} is open. - * - * @return true if open, false if closed - */ - public boolean getState() { - return open; - } - - /** - * Get the value contained in this node. - * - * @return the value of the node - */ - public T getValue() { - return value; - } - - /** - * Returns true if the node is open. - */ - public boolean isOpen() { - return open; - } - - /** - * Check if this is a root node at the top of the tree. - * - * @return true if a root node, false if not - */ - public boolean isRootNode() { - return getParentNode() == null; - } - - /** - * Sets whether this item's children are displayed. - * - * @param open whether the item is open - */ - public void setState(boolean open) { - setState(open, true); - } - - /** - * Sets whether this item's children are displayed. - * - * @param open whether the item is open - * @param fireEvents <code>true</code> to allow open/close events to be - */ - public void setState(boolean open, boolean fireEvents) { - // TODO(jlabanca) - allow people to add open/close handlers. - - // Early out. - if (this.open == open) { - return; - } - - this.animate = true; - this.open = open; - if (open) { - if (!nodeInfoLoaded) { - nodeInfo = tree.getTreeViewModel().getNodeInfo(getValue(), this); - nodeInfoLoaded = true; - } - - preOpen(); - // If we don't have any nodeInfo, we must be a leaf node. - if (nodeInfo != null) { - onOpen(nodeInfo); - } - } else { - cleanup(); - postClose(); - } - - // Update the image. - updateImage(); - } - - /** - * Unregister the list handler and destroy all child nodes. - */ - protected void cleanup() { - // Unregister the list handler. - if (listView != null) { - nodeInfo.unsetView(); - listView = null; - } - - // Recursively kill children. - if (children != null) { - for (TreeNodeView<?> child : children) { - child.cleanup(); - } - children = null; - } - } - - 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 - * @param idx the index of the child within its parent node - * @return a TreeNodeView of suitable type - */ - protected abstract <C> TreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo, - Element childElem, C childValue, Void viewData, int idx); - - /** - * Write the HTML for a list of child values into the given StringBuilder. - * - * @param <C> the data type of the child nodes - * @param sb a StringBuilder to write to - * @param childValues a List of child node values - * @param hasCells the cells and values to use for rendering each child value - * @param savedViews a List of TreeNodeView instances corresponding to - * the child values; a non-null value indicates a TreeNodeView previously - * associated with a given child value - */ - protected abstract <C> void emitHtml(StringBuilder sb, List<C> childValues, - List<HasCell<C, ?, Void>> hasCells, List<TreeNodeView<?>> savedViews); - - /** - * Ensure that the animation frame exists and return it. - * - * @return the animation frame - */ - protected Element ensureAnimationFrame() { - return ensureChildContainer().getParentElement(); - } - - /** - * Ensure that the child container exists and return it. - * - * @return the child container - */ - protected abstract Element ensureChildContainer(); - - /** - * Fire an event to the {@link com.google.gwt.bikeshed.cells.client.Cell}. - * - * @param event the native event - */ - protected void fireEventToCell(NativeEvent event) { - if (parentNodeInfo != null) { - Element cellParent = getCellParent(); - parentNodeInfo.onBrowserEvent(cellParent, value, event); - } else { - Window.alert("parentNodeInfo == null"); - } - } - - /** - * Returns the element that parents the cell contents of this node. - */ - protected abstract Element getCellParent(); - - /** - * Returns the element that contains the children of this node. - */ - protected Element getChildContainer() { - return childContainer; - } - - /** - * Returns the element that contains this node. - * The default implementation returns the value of getElement(). - */ - protected Element getContainer() { - return getElement(); - } - - /** - * Returns the element corresponding to the open/close image. - */ - protected abstract Element getImageElement(); - - /** - * Returns the left position of the open/close image within a tree item node. - * The default implementation returns 0. - */ - protected int getImageLeft() { - return 0; - } - - /** - * Returns the nodeInfo for this node's parent. - */ - protected NodeInfo<T> getParentNodeInfo() { - return parentNodeInfo; - } - - /** - * Returns the key for the value of this node using the parent's - * implementation of NodeInfo.getKey(). - */ - protected Object getValueKey() { - return getParentNodeInfo().getProvidesKey().getKey(getValue()); - } - - /** - * Set up the node when it is opened. Delegates to createMap(), emitHtml(), - * and createTreeNodeView() for subclass-specific functionality. - * - * @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) { - // Add a loading message. - ensureChildContainer().setInnerHTML(tree.getLoadingHtml()); - ensureAnimationFrame().getStyle().setProperty("display", ""); - - // Create a SavedInfo object to store the NodeInfo and data values - final SavedInfo<C> localSavedInfo = new SavedInfo<C>(); - localSavedInfo.nodeInfo = nodeInfo; - localSavedInfo.values = new ArrayList<C>(); - TreeNodeView.this.savedInfo = localSavedInfo; - - // Get the node info. - final ProvidesKey<C> providesKey = nodeInfo.getProvidesKey(); - ListView<C> view = new ListView<C>() { - public Range getRange() { - return new DefaultRange(0, 100); - } - - public void setData(int start, int length, List<C> values) { - // TODO - handle event start and length properly - int end = start + length; - - // Ensure savedInfo has a place to store the data values - while (localSavedInfo.values.size() < end) { - savedInfo.values.add(null); - } - // Save child values into savedInfo - int index = start; - for (C childValue : values) { - localSavedInfo.values.set(index++, childValue); - } - - // Construct a map of former child views based on their value keys. - Map<Object, TreeNodeView<?>> map = new HashMap<Object, TreeNodeView<?>>(); - if (children != null) { - for (TreeNodeView<?> child : children) { - // Ignore child nodes that are closed - if (child.getState()) { - map.put(child.getValueKey(), child); - } - } - } - - // Hide the child container so we can animate it. - if (tree.isAnimationEnabled()) { - ensureAnimationFrame().getStyle().setDisplay(Display.NONE); - } - - List<TreeNodeView<?>> savedViews = new ArrayList<TreeNodeView<?>>(); - for (C childValue : values) { - // Remove any child elements that correspond to prior children - // so the call to setInnerHtml will not destroy them - TreeNodeView<?> savedView = map.get(providesKey.getKey(childValue)); - if (savedView != null) { - savedView.getContainer().getFirstChild().removeFromParent(); - } - savedViews.add(savedView); - } - - // Construct the child contents. - StringBuilder sb = new StringBuilder(); - emitHtml(sb, values, nodeInfo.getHasCells(), savedViews); - childContainer.setInnerHTML(sb.toString()); - - // Create the child TreeNodeViews from the new elements. - children = new ArrayList<TreeNodeView<?>>(); - Element childElem = childContainer.getFirstChildElement(); - int idx = 0; - for (C childValue : values) { - TreeNodeView<C> child = createTreeNodeView(nodeInfo, childElem, - childValue, null, idx); - TreeNodeView<?> savedChild = map.get(providesKey.getKey(childValue)); - // Copy the saved child's state into the new child - if (savedChild != null) { - child.children = savedChild.children; - child.childContainer = savedChild.childContainer; - child.nodeInfo = savedChild.nodeInfo; - child.nodeInfoLoaded = savedChild.nodeInfoLoaded; - child.open = savedChild.getState(); - - // Copy the child container element to the new child - // TODO(rice) clean up this expression - child.getContainer().appendChild(savedChild.childContainer.getParentElement()); - } - - children.add(child); - childElem = childElem.getNextSiblingElement(); - - idx++; - } - - // Animate the child container open. - if (tree.isAnimationEnabled()) { - tree.maybeAnimateTreeNode(TreeNodeView.this); - } - } - - public void setDataSize(int size, boolean isExact) { - if (size == 0 && isExact) { - ensureChildContainer().setInnerHTML("<i>no data</i>"); - if (children != null) { - for (TreeNodeView<?> child : children) { - child.cleanup(); - } - children = null; - } - } - } - - public void setDelegate(ListView.Delegate<C> delegate) { - // Range never actually changes so no need to store the delegate - } - }; - nodeInfo.setView(view); - this.listView = view; - } - - /** - * Called from setState(boolean, boolean) following the call to cleanup(). - */ - protected void postClose() { - } - - /** - * Called from setState(boolean, boolean) prior to the call to onOpen(). - */ - protected void preOpen() { - } - - /** - * Refresh any cells that depend on the selection state. The default - * implementation works for {@link StandardTreeNodeView} and - * {@link SideBySideTreeNodeView}; other subclasses will need to implement - * their own versions of this method. - */ - protected void refreshSelection() { - refreshSelection(savedInfo); - } - - protected void setChildContainer(Element childContainer) { - this.childContainer = childContainer; - } - - /** - * Update the image based on the current state. - */ - protected void updateImage() { - // Early out if this is a root node. - if (isRootNode()) { - return; - } - - // Replace the image element with a new one. - int imageLeft = getImageLeft(); - String html = open ? tree.getOpenImageHtml(imageLeft) : tree.getClosedImageHtml(imageLeft); - if (nodeInfoLoaded && nodeInfo == null) { - html = LEAF_IMAGE; - } - Element tmp = Document.get().createDivElement(); - tmp.setInnerHTML(html); - Element imageElem = tmp.getFirstChildElement(); - getElement().replaceChild(imageElem, getImageElement()); - } - - TreeView getTree() { - return tree; - } - - <C, X> void render(StringBuilder sb, C childValue, - HasCell<C, X, Void> hc) { - hc.getCell().render(hc.getValue(childValue), null, sb); - } - - /** - * Refresh any cells that depend on the selection state. Note that this - * implementation is dependent on the particular DOM structure used by - * {@link StandardTreeNodeView} and {@link SideBySideTreeNodeView}. - * - * @param <C> the child data type - * @param savedInfo a SavedInfo object containing a NodeInfo instance - * and a list of saved child data values - */ - private <C> void refreshSelection(SavedInfo<C> savedInfo) { - List<HasCell<C, ?, Void>> hasCells = savedInfo.nodeInfo.getHasCells(); - Element outerDiv = childContainer.getFirstChildElement(); - int index = 0; - while (outerDiv != null) { - C childValue = savedInfo.values.get(index); - Element span = outerDiv.getFirstChildElement().getNextSiblingElement().getFirstChildElement(); - - for (HasCell<C, ?, Void> hasCell : hasCells) { - if (hasCell.dependsOnSelection()) { - StringBuilder sb = new StringBuilder(); - render(sb, childValue, hasCell); - span.setInnerHTML(sb.toString()); - } - span = span.getNextSiblingElement(); - } - outerDiv = outerDiv.getNextSibling().cast(); - index++; - } - } -}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java index 55d9f16..089c4a6 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java +++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeView.java
@@ -1,12 +1,12 @@ /* * 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 @@ -15,58 +15,22 @@ */ package com.google.gwt.bikeshed.tree.client; -import com.google.gwt.animation.client.Animation; -import com.google.gwt.bikeshed.list.shared.SelectionModel; -import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent; -import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeHandler; import com.google.gwt.core.client.GWT; import com.google.gwt.resources.client.ClientBundle; import com.google.gwt.resources.client.ImageResource; -import com.google.gwt.user.client.ui.AbstractImagePrototype; +import com.google.gwt.user.client.ui.Composite; +import com.google.gwt.user.client.ui.SimplePanel; import com.google.gwt.user.client.ui.Widget; /** * A view of a tree. */ -public abstract class TreeView extends Widget { - - /** - * An Animation of a {@link TreeNodeView}. - */ - public abstract static class TreeNodeViewAnimation extends Animation { - - /** - * The default animation delay in milliseconds. - */ - private static final int DEFAULT_ANIMATION_DURATION = 450; - - /** - * The duration of the animation. - */ - private int duration = DEFAULT_ANIMATION_DURATION; - - /** - * Animate a {@link TreeNodeView} into its new state. - * - * @param node the {@link TreeNodeView} to animate - * @param isAnimationEnabled true to animate - */ - public abstract void animate(TreeNodeView<?> node, - boolean isAnimationEnabled); - - public int getDuration() { - return duration; - } - - public void setDuration(int duration) { - this.duration = duration; - } - } +public abstract class TreeView extends Composite { /** * A ClientBundle that provides images for this widget. */ - interface Resources extends ClientBundle { + public static interface Resources extends ClientBundle { /** * An image indicating a closed branch. @@ -79,49 +43,12 @@ ImageResource treeOpen(); } - private class TreeSelectionHandler implements SelectionChangeHandler { - public void onSelectionChange(SelectionChangeEvent event) { - refreshSelection(); - } - } - private static final Resources DEFAULT_RESOURCES = GWT.create(Resources.class); /** - * The animation. + * The {@link Resources} used in the tree. */ - private TreeNodeViewAnimation animation; - - /** - * The HTML used to generate the closed image. - */ - private String closedImageHtml; - - /** - * Indicates whether or not animations are enabled. - */ - private boolean isAnimationEnabled; - - /** - * The message displayed while child nodes are loading. - */ - // TODO(jlabanca): I18N this, or remove the text - private String loadingHtml = "Loading..."; - - /** - * The HTML used to generate the open image. - */ - private String openImageHtml; - - /** - * The hidden root node in the tree. - */ - private TreeNodeView<?> rootNode; - - /** - * The {@link SelectionModel} for the tree. - */ - private SelectionModel<Object> selectionModel; + private Resources resources = DEFAULT_RESOURCES; /** * The {@link TreeViewModel} that backs the tree. @@ -130,96 +57,26 @@ /** * Construct a new {@link TreeView}. - * + * * @param viewModel the {@link TreeViewModel} that backs the tree */ + // TODO(jlabanca): Should we nuke this class? public TreeView(TreeViewModel viewModel) { + this(viewModel, new SimplePanel()); + } + + protected TreeView(TreeViewModel viewModel, Widget widget) { this.viewModel = viewModel; - } - - /** - * Get the animation used to open and close nodes in this tree if animations - * are enabled. - * - * @return the animation - * @see #isAnimationEnabled() - */ - public TreeNodeViewAnimation getAnimation() { - return animation; - } - - public TreeNode<?> getRootNode() { - return rootNode; - } - - /** - * Returns the {@link SelectionModel} containing the selection state for - * this tree, or null if none is present. - */ - public SelectionModel<Object> getSelectionModel() { - return selectionModel; + initWidget(widget); } public TreeViewModel getTreeViewModel() { return viewModel; } - public boolean isAnimationEnabled() { - return isAnimationEnabled; - } - - /** - * Refresh any visible cells of this tree that depend on the selection state. - */ - public void refreshSelection() { - refreshSelection(rootNode); - } - - /** - * Set the animation used to open and close nodes in this tree. You must call - * {@link #setAnimationEnabled(boolean)} to enable or disable animation. - * - * @param animation a {@link TreeNodeViewAnimation} - * @see #setAnimationEnabled(boolean) - */ - public void setAnimation(TreeNodeViewAnimation animation) { - assert animation != null : "animation cannot be null"; - this.animation = animation; - } - - public void setAnimationEnabled(boolean enable) { - this.isAnimationEnabled = enable; - if (!enable && animation != null) { - animation.cancel(); - } - } - - public void setSelectionModel(SelectionModel<Object> selectionModel) { - this.selectionModel = selectionModel; - // Attach a selection handler. - if (selectionModel != null) { - selectionModel.addSelectionChangeHandler(new TreeSelectionHandler()); - } - } - - /** - * @return the HTML to render the closed image. - */ - protected String getClosedImageHtml(int left) { - if (closedImageHtml == null) { - AbstractImagePrototype proto = - AbstractImagePrototype.create(DEFAULT_RESOURCES.treeClosed()); - // CHECKSTYLE_OFF - closedImageHtml = proto.getHTML().replace("style='", - "style='position:absolute;left:" + left + "px;top:0px;"); - // CHECKSTYLE_ON - } - return closedImageHtml; - } - /** * Get the width required for the images. - * + * * @return the maximum width required for images. */ protected int getImageWidth() { @@ -228,67 +85,11 @@ } /** - * Get the HTML string that is displayed while nodes wait for their children - * to load. - * - * @return the loading HTML string + * Get the {@link Resources} used in the tree. + * + * @return the resources */ - protected String getLoadingHtml() { - return loadingHtml; - } - - /** - * @return the HTML to render the open image. - */ - protected String getOpenImageHtml(int left) { - if (openImageHtml == null) { - AbstractImagePrototype proto = - AbstractImagePrototype.create(DEFAULT_RESOURCES.treeOpen()); - // CHECKSTYLE_OFF - openImageHtml = proto.getHTML().replace("style='", - "style='position:absolute;left:" + left + "px;top:0px;"); - // CHECKSTYLE_ON - } - return openImageHtml; - } - - protected TreeNodeView<?> getRootTreeNodeView() { - return rootNode; - } - - /** - * Animate the current state of a {@link TreeNodeView} in this tree. - * - * @param node the node to animate - */ - protected void maybeAnimateTreeNode(TreeNodeView<?> node) { - if (animation != null) { - animation.animate(node, node.consumeAnimate() && isAnimationEnabled()); - } - } - - /** - * Set the HTML string that will be displayed when a node is waiting for its - * child nodes to load. - * - * @param loadingHtml the HTML string - */ - protected void setLoadingHtml(String loadingHtml) { - this.loadingHtml = loadingHtml; - } - - protected void setRootNode(TreeNodeView<?> rootNode) { - this.rootNode = rootNode; - } - - private void refreshSelection(TreeNodeView<?> node) { - node.refreshSelection(); - int count = node.getChildCount(); - for (int i = 0; i < count; i++) { - TreeNodeView<?> child = node.getChildTreeNodeView(i); - if (child.isOpen()) { - refreshSelection(child); - } - } + protected Resources getResources() { + return resources; } }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java index 332b6dc..85df9db 100644 --- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java +++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
@@ -1,12 +1,12 @@ /* * 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 @@ -16,17 +16,12 @@ package com.google.gwt.bikeshed.tree.client; import com.google.gwt.bikeshed.cells.client.Cell; -import com.google.gwt.bikeshed.cells.client.FieldUpdater; import com.google.gwt.bikeshed.cells.client.ValueUpdater; -import com.google.gwt.bikeshed.list.client.HasCell; import com.google.gwt.bikeshed.list.client.ListView; import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter; import com.google.gwt.bikeshed.list.shared.ProvidesKey; -import com.google.gwt.dom.client.Element; -import com.google.gwt.dom.client.NativeEvent; - -import java.util.ArrayList; -import java.util.List; +import com.google.gwt.bikeshed.list.shared.SelectionModel; +import com.google.gwt.bikeshed.list.shared.SingleSelectionModel; /** * A model of a tree. @@ -37,87 +32,57 @@ * Default implementation of {@link NodeInfo}. */ class DefaultNodeInfo<T> implements NodeInfo<T> { - private List<HasCell<T, ?, Void>> hasCells = new ArrayList<HasCell<T, ?, Void>>(); + + private Cell<T, Void> cell; private AbstractListViewAdapter<T> listViewAdapter; + private SelectionModel<? super T> selectionModel; + private ValueUpdater<T, Void> valueUpdater; private ListView<T> view; /** * Construct a new {@link DefaultNodeInfo}. - * + * * @param adapter the {@link AbstractListViewAdapter} that provides the * child values * @param cell the {@link Cell} used to render the child values - * @param dependsOnSelection true if the contents of the cell may require - * an update when the selection changes */ public DefaultNodeInfo(AbstractListViewAdapter<T> adapter, - Cell<T, Void> cell, boolean dependsOnSelection) { - this(adapter, cell, dependsOnSelection, null); + Cell<T, Void> cell) { + this(adapter, cell, new SingleSelectionModel<T>(), null); } /** - * Construct a new {@link DefaultNodeInfo} with a single cell and a - * {@link ValueUpdater}. - * + * Construct a new {@link DefaultNodeInfo}. + * * @param adapter the {@link AbstractListViewAdapter} that provides the * child values - * @param cell the {@link Cell} used to render the child values - * @param dependsOnSelection true if the contents of the cell may require - * an update when the selection changes + * @param cell the {@link Cell} used to render the child values update when + * the selection changes * @param valueUpdater the {@link ValueUpdater} */ public DefaultNodeInfo(AbstractListViewAdapter<T> adapter, - final Cell<T, Void> cell, final boolean dependsOnSelection, + final Cell<T, Void> cell, SelectionModel<? super T> selectionModel, final ValueUpdater<T, Void> valueUpdater) { - hasCells.add(new HasCell<T, T, Void>() { - public boolean dependsOnSelection() { - return dependsOnSelection; - } - - public Cell<T, Void> getCell() { - return cell; - } - - public FieldUpdater<T, T, Void> getFieldUpdater() { - return valueUpdater == null ? null : new FieldUpdater<T, T, Void>() { - public void update(int index, T object, T value, Void viewData) { - valueUpdater.update(value, viewData); - } - }; - } - - public T getValue(T object) { - return object; - } - }); this.listViewAdapter = adapter; + this.cell = cell; + this.selectionModel = selectionModel; + this.valueUpdater = valueUpdater; } - public DefaultNodeInfo(AbstractListViewAdapter<T> adapter, - List<HasCell<T, ?, Void>> hasCells) { - this.hasCells.addAll(hasCells); - this.listViewAdapter = adapter; - } - - public List<HasCell<T, ?, Void>> getHasCells() { - return hasCells; + public Cell<T, Void> getCell() { + return cell; } public ProvidesKey<T> getProvidesKey() { return listViewAdapter; } - public void onBrowserEvent(Element elem, final T object, NativeEvent event) { - Element target = event.getEventTarget().cast(); - String idxString = ""; - while ((target != null) - && ((idxString = target.getAttribute("__idx")).length() == 0)) { - target = target.getParentElement(); - } - if (idxString.length() > 0) { - int idx = Integer.parseInt(idxString); - dispatch(target, object, event, hasCells.get(idx)); - } + public SelectionModel<? super T> getSelectionModel() { + return selectionModel; + } + + public ValueUpdater<T, Void> getValueUpdater() { + return valueUpdater; } public void setView(ListView<T> view) { @@ -128,39 +93,56 @@ public void unsetView() { listViewAdapter.removeView(view); } - - private <X> void dispatch(Element target, final T object, - NativeEvent event, HasCell<T, X, Void> hc) { - final FieldUpdater<T, X, Void> fieldUpdater = hc.getFieldUpdater(); - hc.getCell().onBrowserEvent(target, hc.getValue(object), null, event, - fieldUpdater == null ? null : new ValueUpdater<X, Void>() { - public void update(X value, Void viewData) { - fieldUpdater.update(0, object, value, viewData); - } - }); - } } /** - * The info needed to create a {@link TreeNodeView}. + * The info needed to create the children of a tree node. */ interface NodeInfo<T> { - List<HasCell<T, ?, Void>> getHasCells(); + /** + * Get the {@link Cell} used to render the children of this node. + * + * @return the {@link Cell} + */ + Cell<T, Void> getCell(); + /** + * Return the key provider for children of this node. + * + * @return the {@link ProvidesKey} + */ ProvidesKey<T> getProvidesKey(); /** - * Handle an event that is fired on one of the children of this item. - * - * @param elem the parent element of the item - * @param object the data value of the item - * @param event the event that was fired + * Get the {@link SelectionModel} used for the children of this node. To + * unify selection across all items of the same type, or across the entire + * tree, return the same instance of {@link SelectionModel} from all + * {@link NodeInfo}. + * + * @return the {@link SelectionModel} */ - void onBrowserEvent(Element elem, final T object, NativeEvent event); + SelectionModel<? super T> getSelectionModel(); + /** + * Get the value updater associated with the cell. + * + * @return the value updater + */ + ValueUpdater<T, Void> getValueUpdater(); + + /** + * Set the view that is listening to this {@link NodeInfo}. The + * implementation should attach the view to the source of data. + * + * @param view the {@link ListView} + */ void setView(ListView<T> view); + /** + * Unset the view from the {@link NodeInfo}. The implementation should + * detach the view from the source of data. + */ void unsetView(); } @@ -168,20 +150,18 @@ * Get the {@link NodeInfo} that will provide the {@link ProvidesKey}, * {@link Cell}, and {@link ListView}s to retrieve and display the children of * the specified value. - * + * * @param value the value in the parent node - * @param treeNode the {@link TreeNode} that contains the value * @return the {@link NodeInfo} */ - <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode); + <T> NodeInfo<?> getNodeInfo(T value); /** * Check if the value is known to be a leaf node. - * + * * @param value the value at the node - * @param treeNode the {@link TreeNode} that contains the value - * + * * @return true if the node is known to be a leaf node, false otherwise */ - boolean isLeaf(Object value, TreeNode<?> treeNode); + boolean isLeaf(Object value); }
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java index 1fd42e9..b163ded 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/BasicTreeRecipe.java
@@ -1,12 +1,12 @@ /* * 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 @@ -15,6 +15,7 @@ */ package com.google.gwt.sample.bikeshed.cookbook.client; +import com.google.gwt.bikeshed.list.shared.MultiSelectionModel; import com.google.gwt.bikeshed.list.shared.SelectionModel; import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent; import com.google.gwt.bikeshed.tree.client.StandardTreeView; @@ -37,17 +38,15 @@ FlowPanel p = new FlowPanel(); final Label label = new Label(); - final MultiSelectionModel<Object> selectionModel = new MultiSelectionModel<Object>(); + final MultiSelectionModel<String> selectionModel = new MultiSelectionModel<String>(); selectionModel.addSelectionChangeHandler(new SelectionModel.SelectionChangeHandler() { public void onSelectionChange(SelectionChangeEvent event) { - label.setText("Selected " - + selectionModel.getSelectedSet().toString()); + label.setText("Selected " + selectionModel.getSelectedSet().toString()); } }); StandardTreeView stree = new StandardTreeView(new MyTreeViewModel( selectionModel), "..."); - stree.setSelectionModel(selectionModel); stree.setAnimationEnabled(true); p.add(stree);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java index 8d12fd1..5c32123 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/Cookbook.java
@@ -1,12 +1,12 @@ /* * 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 @@ -18,9 +18,9 @@ import com.google.gwt.bikeshed.cells.client.Cell; import com.google.gwt.bikeshed.list.shared.ListViewAdapter; import com.google.gwt.bikeshed.list.shared.SelectionModel; +import com.google.gwt.bikeshed.list.shared.SingleSelectionModel; import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent; import com.google.gwt.bikeshed.tree.client.StandardTreeView; -import com.google.gwt.bikeshed.tree.client.TreeNode; import com.google.gwt.bikeshed.tree.client.TreeViewModel; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; @@ -58,21 +58,27 @@ private static final class RecipeTreeModel implements TreeViewModel { private ListViewAdapter<Category> adapter = new ListViewAdapter<Category>(); + private SelectionModel<Object> selectionModel; - public <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode) { + public RecipeTreeModel(SelectionModel<Object> selectionModel) { + this.selectionModel = selectionModel; + } + + public <T> NodeInfo<?> getNodeInfo(T value) { if (value == null) { // Categories at the root. - return new DefaultNodeInfo<Category>(adapter, new CategoryCell(), false); + return new DefaultNodeInfo<Category>(adapter, new CategoryCell(), + selectionModel, null); } else if (value instanceof Category) { // Demos for each category. Category category = (Category) value; return new DefaultNodeInfo<Recipe>(new ListViewAdapter<Recipe>( - category.getRecipes()), new RecipeCell(), false); + category.getRecipes()), new RecipeCell(), selectionModel, null); } return null; } - public boolean isLeaf(Object value, TreeNode<?> treeNode) { + public boolean isLeaf(Object value) { // The root and categories have children. if (value == null || value instanceof Category) { return false; @@ -89,17 +95,18 @@ @UiField StandardTreeView recipeTree; @UiField LayoutPanel container; - private RecipeTreeModel recipeTreeModel = new RecipeTreeModel(); - private SingleSelectionModel<Object> treeSelection; + private RecipeTreeModel recipeTreeModel; private SimpleCellListRecipe defaultRecipe; private Recipe curRecipe; public void onModuleLoad() { + // Initialize the UI. + final SingleSelectionModel<Object> treeSelection = new SingleSelectionModel<Object>(); + recipeTreeModel = new RecipeTreeModel(treeSelection); RootLayoutPanel.get().add(binder.createAndBindUi(this)); createRecipes(recipeTreeModel.adapter.getList()); - treeSelection = new SingleSelectionModel<Object>(); - recipeTree.setSelectionModel(treeSelection); + // Select a recipe on selection. treeSelection.addSelectionChangeHandler(new SelectionModel.SelectionChangeHandler() { public void onSelectionChange(SelectionChangeEvent event) { Object o = treeSelection.getSelectedObject(); @@ -120,21 +127,13 @@ private void createRecipes(List<Category> cats) { defaultRecipe = new SimpleCellListRecipe(); - cats.add(new Category("Lists", new Recipe[] { - defaultRecipe - })); + cats.add(new Category("Lists", new Recipe[] {defaultRecipe})); cats.add(new Category("Tables", new Recipe[] { - new BasicTableRecipe(), - new EditableTableRecipe(), - })); + new BasicTableRecipe(), new EditableTableRecipe(),})); cats.add(new Category("Trees", new Recipe[] { - new BasicTreeRecipe(), - new SideBySideTreeRecipe(), - })); + new BasicTreeRecipe(), new SideBySideTreeRecipe(),})); cats.add(new Category("Other", new Recipe[] { - new ValidationRecipe(), - new MailRecipe(), - })); + new ValidationRecipe(), new MailRecipe(),})); } private void showRecipe(Recipe recipe) {
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java index 6783775..93fe842 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MyTreeViewModel.java
@@ -1,12 +1,12 @@ /* * 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 @@ -18,13 +18,13 @@ import com.google.gwt.bikeshed.cells.client.ButtonCell; import com.google.gwt.bikeshed.cells.client.Cell; import com.google.gwt.bikeshed.cells.client.CheckboxCell; +import com.google.gwt.bikeshed.cells.client.CompositeCell; import com.google.gwt.bikeshed.cells.client.FieldUpdater; import com.google.gwt.bikeshed.cells.client.ValueUpdater; import com.google.gwt.bikeshed.list.client.HasCell; import com.google.gwt.bikeshed.list.client.ListView; import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter; import com.google.gwt.bikeshed.list.shared.SelectionModel; -import com.google.gwt.bikeshed.tree.client.TreeNode; import com.google.gwt.bikeshed.tree.client.TreeViewModel; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.Timer; @@ -39,7 +39,8 @@ */ public class MyTreeViewModel implements TreeViewModel { - private static class IntegerListViewAdapter extends AbstractListViewAdapter<Integer> { + private static class IntegerListViewAdapter extends + AbstractListViewAdapter<Integer> { int wordLength; public IntegerListViewAdapter(int wordLength) { @@ -55,7 +56,8 @@ } } - private static class StringListViewAdapter extends AbstractListViewAdapter<String> { + private static class StringListViewAdapter extends + AbstractListViewAdapter<String> { String value; public StringListViewAdapter(final String value) { @@ -103,17 +105,19 @@ } }; - private List<HasCell<String, ?, Void>> hasCells = - new ArrayList<HasCell<String, ?, Void>>(); + private CompositeCell<String, Void> compositeCell = new CompositeCell<String, Void>(); + private SelectionModel<String> selectionModel; - public MyTreeViewModel(final SelectionModel<Object> selectionModel) { - hasCells.add(new HasCell<String, Boolean, Void>() { - public boolean dependsOnSelection() { - return true; - } - + public MyTreeViewModel(final SelectionModel<String> selectionModel) { + this.selectionModel = selectionModel; + compositeCell.addHasCell(new HasCell<String, Boolean, Void>() { public Cell<Boolean, Void> getCell() { - return new CheckboxCell(); + return new CheckboxCell() { + @Override + public boolean dependsOnSelection() { + return true; + } + }; } public FieldUpdater<String, Boolean, Void> getFieldUpdater() { @@ -129,11 +133,7 @@ return selectionModel.isSelected(object); } }); - hasCells.add(new HasCell<String, String, Void>() { - public boolean dependsOnSelection() { - return false; - } - + compositeCell.addHasCell(new HasCell<String, String, Void>() { public Cell<String, Void> getCell() { return ButtonCell.getInstance(); } @@ -153,7 +153,7 @@ }); } - public <T> NodeInfo<?> getNodeInfo(T value, TreeNode<T> treeNode) { + public <T> NodeInfo<?> getNodeInfo(T value) { if (value instanceof String) { return getNodeInfoHelper((String) value); } @@ -163,17 +163,20 @@ throw new IllegalArgumentException("Unsupported object type: " + type); } - public boolean isLeaf(Object value, TreeNode<?> parentNode) { + public boolean isLeaf(Object value) { return value instanceof Integer; } private NodeInfo<?> getNodeInfoHelper(final String value) { if (value.endsWith("...")) { - AbstractListViewAdapter<String> adapter = new StringListViewAdapter(value.toString()); - return new DefaultNodeInfo<String>(adapter, hasCells); + AbstractListViewAdapter<String> adapter = new StringListViewAdapter( + value.toString()); + return new DefaultNodeInfo<String>(adapter, compositeCell, + selectionModel, null); } else { - AbstractListViewAdapter<Integer> adapter = new IntegerListViewAdapter(value.length()); - return new DefaultNodeInfo<Integer>(adapter, INTEGER_CELL, false, + AbstractListViewAdapter<Integer> adapter = new IntegerListViewAdapter( + value.length()); + return new DefaultNodeInfo<Integer>(adapter, INTEGER_CELL, null, new ValueUpdater<Integer, Void>() { public void update(Integer value, Void viewData) { Window.alert("Integer = " + value);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java index 8e7e234..5c18d2e 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SideBySideTreeRecipe.java
@@ -1,12 +1,12 @@ /* * 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 @@ -15,6 +15,7 @@ */ package com.google.gwt.sample.bikeshed.cookbook.client; +import com.google.gwt.bikeshed.list.shared.MultiSelectionModel; import com.google.gwt.bikeshed.list.shared.SelectionModel; import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent; import com.google.gwt.bikeshed.tree.client.SideBySideTreeView; @@ -37,7 +38,7 @@ FlowPanel p = new FlowPanel(); final Label label = new Label(); - final MultiSelectionModel<Object> selectionModel = new MultiSelectionModel<Object>(); + final MultiSelectionModel<String> selectionModel = new MultiSelectionModel<String>(); selectionModel.addSelectionChangeHandler(new SelectionModel.SelectionChangeHandler() { public void onSelectionChange(SelectionChangeEvent event) { label.setText("Selected " + selectionModel.getSelectedSet().toString()); @@ -45,8 +46,8 @@ }); SideBySideTreeView sstree = new SideBySideTreeView(new MyTreeViewModel( - selectionModel), "...", 100, 4); - sstree.setSelectionModel(selectionModel); + selectionModel), "..."); + sstree.setAnimationEnabled(true); sstree.setHeight("200px"); p.add(sstree);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java index cbc7b94..d7e66ee 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
@@ -1,12 +1,12 @@ /* * 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 @@ -21,7 +21,6 @@ import com.google.gwt.bikeshed.list.shared.ListViewAdapter; import com.google.gwt.bikeshed.list.shared.Range; import com.google.gwt.bikeshed.tree.client.SideBySideTreeView; -import com.google.gwt.bikeshed.tree.client.TreeNode; import com.google.gwt.core.client.EntryPoint; import com.google.gwt.core.client.GWT; import com.google.gwt.event.logical.shared.CloseEvent; @@ -54,7 +53,8 @@ */ public class StocksDesktop implements EntryPoint, Updater { - interface Binder extends UiBinder<Widget, StocksDesktop> { } + interface Binder extends UiBinder<Widget, StocksDesktop> { + } private static final Binder binder = GWT.create(Binder.class); @@ -67,13 +67,19 @@ return NumberFormat.getCurrencyFormat("USD").format(price / 100.0); } - @UiField Label cashLabel; + @UiField + Label cashLabel; - @UiField FavoritesWidget favoritesWidget; - @UiField Label netWorthLabel; - @UiField PlayerScoresWidget playerScoresWidget; - @UiField StockQueryWidget queryWidget; - @UiField SideBySideTreeView transactionTree; + @UiField + FavoritesWidget favoritesWidget; + @UiField + Label netWorthLabel; + @UiField + PlayerScoresWidget playerScoresWidget; + @UiField + StockQueryWidget queryWidget; + @UiField + SideBySideTreeView transactionTree; /** * The popup used to purchase stock. @@ -84,8 +90,7 @@ private AsyncListViewAdapter<StockQuote> favoritesListViewAdapter; private AsyncListViewAdapter<PlayerInfo> playerScoresListViewAdapter; private AsyncListViewAdapter<StockQuote> searchListViewAdapter; - private Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker = - new HashMap<String, ListViewAdapter<Transaction>>(); + private Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker = new HashMap<String, ListViewAdapter<Transaction>>(); private ListViewAdapter<Transaction> transactionListViewAdapter; private List<Transaction> transactions; @@ -145,19 +150,22 @@ // Hook up handlers to columns and the buy/sell popup. Columns.favoriteColumn.setFieldUpdater(new FieldUpdater<StockQuote, Boolean, Void>() { - public void update(int index, StockQuote object, Boolean value, Void viewData) { + public void update(int index, StockQuote object, Boolean value, + Void viewData) { setFavorite(object.getTicker(), value); } }); Columns.buyColumn.setFieldUpdater(new FieldUpdater<StockQuote, String, Void>() { - public void update(int index, StockQuote quote, String value, Void viewData) { + public void update(int index, StockQuote quote, String value, + Void viewData) { buy(quote); } }); Columns.sellColumn.setFieldUpdater(new FieldUpdater<StockQuote, String, Void>() { - public void update(int index, StockQuote quote, String value, Void viewData) { + public void update(int index, StockQuote quote, String value, + Void viewData) { sell(quote); } }); @@ -176,7 +184,7 @@ /** * Process the {@link StockResponse} from the server. - * + * * @param response the stock response */ public void processStockResponse(StockResponse response) { @@ -212,7 +220,7 @@ /** * Set or unset a ticker symbol as a 'favorite'. - * + * * @param ticker the ticker symbol * @param favorite if true, make the stock a favorite */ @@ -230,7 +238,8 @@ } }); } else { - dataService.removeFavorite(ticker, favoritesListViewAdapter.getRanges()[0], + dataService.removeFavorite(ticker, + favoritesListViewAdapter.getRanges()[0], new AsyncCallback<StockResponse>() { public void onFailure(Throwable caught) { handleRpcError(caught, "Error removing favorite"); @@ -289,9 +298,7 @@ Range[] searchRanges = searchListViewAdapter.getRanges(); Range[] favoritesRanges = favoritesListViewAdapter.getRanges(); - String sectorName = getSectorName(); - SectorListViewAdapter sectorListViewAdapter = sectorName != null - ? treeModel.getSectorListViewAdapter(sectorName) : null; + SectorListViewAdapter sectorListViewAdapter = treeModel.getSectorListViewAdapter(); Range[] sectorRanges = sectorListViewAdapter == null ? null : sectorListViewAdapter.getRanges(); @@ -303,8 +310,8 @@ String searchQuery = queryWidget.getSearchQuery(); StockRequest request = new StockRequest(searchQuery, - sectorListViewAdapter != null ? sectorListViewAdapter.getSector() : null, - searchRanges[0], favoritesRanges[0], sectorRanges != null + sectorListViewAdapter != null ? sectorListViewAdapter.getSector() + : null, searchRanges[0], favoritesRanges[0], sectorRanges != null && sectorRanges.length > 0 ? sectorRanges[0] : null); dataService.getStockQuotes(request, new AsyncCallback<StockResponse>() { public void onFailure(Throwable caught) { @@ -336,26 +343,15 @@ @UiFactory SideBySideTreeView createTransactionTree() { - return new SideBySideTreeView(treeModel, null, 200); - } - - // Hack - walk the transaction tree to find the current viewed sector - private String getSectorName() { - int children = transactionTree.getRootNode().getChildCount(); - for (int i = 0; i < children; i++) { - TreeNode<?> childNode = transactionTree.getRootNode().getChildNode(i); - if (childNode.isOpen()) { - return (String) childNode.getValue(); - } - } - - return null; + SideBySideTreeView treeView = new SideBySideTreeView(treeModel, null); + treeView.setAnimationEnabled(true); + return treeView; } /** * Display a message to the user when an RPC call fails. - * - * @param caughtbad the exception + * + * @param caught the exception * @param displayMessage the message to display to the user, or null to * display a default message * @return true if recoverable, false if not @@ -395,7 +391,7 @@ // Update the sector list. StockQuoteList sectorList = response.getSector(); if (sectorList != null) { - SectorListViewAdapter sectorListViewAdapter = treeModel.getSectorListViewAdapter(getSectorName()); + SectorListViewAdapter sectorListViewAdapter = treeModel.getSectorListViewAdapter(); if (sectorListViewAdapter != null) { sectorListViewAdapter.updateDataSize(response.getNumSector(), true); sectorListViewAdapter.updateViewData(sectorList.getStartIndex(),
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java index c213e10..943370c 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
@@ -1,12 +1,12 @@ /* * 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 @@ -23,7 +23,8 @@ import com.google.gwt.bikeshed.list.shared.AbstractListViewAdapter; import com.google.gwt.bikeshed.list.shared.AsyncListViewAdapter; import com.google.gwt.bikeshed.list.shared.ListViewAdapter; -import com.google.gwt.bikeshed.tree.client.TreeNode; +import com.google.gwt.bikeshed.list.shared.ProvidesKey; +import com.google.gwt.bikeshed.list.shared.SingleSelectionModel; import com.google.gwt.bikeshed.tree.client.TreeViewModel; import com.google.gwt.sample.bikeshed.stocks.shared.StockQuote; import com.google.gwt.sample.bikeshed.stocks.shared.Transaction; @@ -57,13 +58,9 @@ } } - static class TransactionCell extends Cell<Transaction, Void> { - @Override - public void render(Transaction value, Void viewData, StringBuilder sb) { - sb.append(value.toString()); - } - } - + /** + * A {@link Cell} used to render a {@link StockQuote}. + */ private static final Cell<StockQuote, Void> STOCK_QUOTE_CELL = new Cell<StockQuote, Void>() { @Override public void render(StockQuote value, Void viewData, StringBuilder sb) { @@ -71,21 +68,52 @@ } }; - private static final Cell<Transaction, Void> TRANSACTION_CELL = new TransactionCell(); + /** + * A {@link Cell} used to render a {@link Transaction}. + */ + private static final Cell<Transaction, Void> TRANSACTION_CELL = new Cell<Transaction, Void>() { + @Override + public void render(Transaction value, Void viewData, StringBuilder sb) { + sb.append(value.toString()); + } + }; + + /** + * The last {@link StockQuote} that was opened. + */ + private StockQuote lastStockQuote; + + /** + * The last Sector that was opened. + */ + private String lastSector; private Map<String, SectorListViewAdapter> sectorListViewAdapters = new HashMap<String, SectorListViewAdapter>(); private AbstractListViewAdapter<StockQuote> stockQuoteListViewAdapter; private ListViewAdapter<String> topLevelListViewAdapter = new ListViewAdapter<String>(); + private SingleSelectionModel<Object> selectionModel = new SingleSelectionModel<Object>(); private Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker; private Updater updater; - public TransactionTreeViewModel(Updater updater, + public TransactionTreeViewModel( + Updater updater, AbstractListViewAdapter<StockQuote> stockQuoteListViewAdapter, Map<String, ListViewAdapter<Transaction>> transactionListViewAdaptersByTicker) { + this.selectionModel.setKeyProvider(new ProvidesKey<Object>() { + public Object getKey(Object item) { + if (item instanceof StockQuote) { + return StockQuote.KEY_PROVIDER.getKey((StockQuote) item); + } else { + return item; + } + } + }); this.updater = updater; this.stockQuoteListViewAdapter = stockQuoteListViewAdapter; + + // Setup the sector list. List<String> topLevelList = topLevelListViewAdapter.getList(); topLevelList.add("Favorites"); topLevelList.add("Dow Jones Industrials"); @@ -93,60 +121,73 @@ this.transactionListViewAdaptersByTicker = transactionListViewAdaptersByTicker; } - public <T> NodeInfo<?> getNodeInfo(T value, final TreeNode<T> treeNode) { + public <T> NodeInfo<?> getNodeInfo(T value) { if (value == null) { + // Return list of sectors. return new TreeViewModel.DefaultNodeInfo<String>(topLevelListViewAdapter, - TextCell.getInstance(), false); + TextCell.getInstance(), selectionModel, null); } else if ("Favorites".equals(value)) { - return new TreeViewModel.DefaultNodeInfo<StockQuote>(stockQuoteListViewAdapter, - STOCK_QUOTE_CELL, false); + // Return favorites. + return new TreeViewModel.DefaultNodeInfo<StockQuote>( + stockQuoteListViewAdapter, STOCK_QUOTE_CELL, selectionModel, null); } else if ("History".equals(value)) { - String ticker = ((StockQuote) treeNode.getParentNode().getValue()).getTicker(); + // Return history of the current stock quote. + String ticker = lastStockQuote.getTicker(); ListViewAdapter<Transaction> adapter = transactionListViewAdaptersByTicker.get(ticker); if (adapter == null) { adapter = new ListViewAdapter<Transaction>(); transactionListViewAdaptersByTicker.put(ticker, adapter); } return new TreeViewModel.DefaultNodeInfo<Transaction>(adapter, - TRANSACTION_CELL, false); + TRANSACTION_CELL, selectionModel, null); } else if ("Actions".equals(value)) { + // Return the actions for the current stock quote. ListViewAdapter<String> adapter = new ListViewAdapter<String>(); List<String> list = adapter.getList(); list.add("Buy"); list.add("Sell"); return new TreeViewModel.DefaultNodeInfo<String>(adapter, - ButtonCell.getInstance(), false, new ValueUpdater<String, Void>() { + ButtonCell.getInstance(), selectionModel, + new ValueUpdater<String, Void>() { public void update(String value, Void viewData) { - StockQuote stockQuote = (StockQuote) treeNode.getParentNode().getValue(); if ("Buy".equals(value)) { - updater.buy(stockQuote); + updater.buy(lastStockQuote); } else { - updater.sell(stockQuote); + updater.sell(lastStockQuote); } } }); } else if (value instanceof String) { - SectorListViewAdapter adapter = new SectorListViewAdapter((String) value); - sectorListViewAdapters.put((String) value, adapter); + // Return the stocks for a given sector. + lastSector = (String) value; + SectorListViewAdapter adapter = new SectorListViewAdapter(lastSector); + sectorListViewAdapters.put(lastSector, adapter); return new TreeViewModel.DefaultNodeInfo<StockQuote>(adapter, - STOCK_QUOTE_CELL, false); + STOCK_QUOTE_CELL, selectionModel, null); } else if (value instanceof StockQuote) { + // Return the submenu for a stock quote. + lastStockQuote = (StockQuote) value; ListViewAdapter<String> adapter = new ListViewAdapter<String>(); List<String> list = adapter.getList(); list.add("Actions"); list.add("History"); return new TreeViewModel.DefaultNodeInfo<String>(adapter, - TextCell.getInstance(), false); + TextCell.getInstance(), selectionModel, null); } throw new IllegalArgumentException(value.toString()); } - public SectorListViewAdapter getSectorListViewAdapter(String value) { - return sectorListViewAdapters.get(value); + /** + * Get the {@link SectorListViewAdapter} for the last selected sector. + * + * @return the {@link SectorListViewAdapter} + */ + public SectorListViewAdapter getSectorListViewAdapter() { + return lastSector == null ? null : sectorListViewAdapters.get(lastSector); } - public boolean isLeaf(Object value, final TreeNode<?> parentNode) { + public boolean isLeaf(Object value) { if (value instanceof Transaction || "Buy".equals(value) || "Sell".equals(value)) { return true;
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java index a9a6bb1..c7db58b 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/server/StockServiceImpl.java
@@ -360,7 +360,7 @@ } } - return new Result(toRet, toRet.size()); + return new Result(toRet, symbols.size()); } // If a query is alpha-only ([A-Za-z]+), return stocks for which:
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java index 0e823a0..c4d0816 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/Styles.java
@@ -20,8 +20,14 @@ import com.google.gwt.resources.client.CssResource; import com.google.gwt.resources.client.CssResource.NotStrict; +/** + * The Styles used in bikeshed. + */ public class Styles { + /** + * Common styles. + */ public interface Common extends CssResource { String box(); String header(); @@ -31,6 +37,9 @@ String table(); } + /** + * Shared resources. + */ public interface Resources extends ClientBundle { @NotStrict @Source("common.css")
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css index 5551e36..5162d1e 100644 --- a/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css +++ b/bikeshed/src/com/google/gwt/sample/bikeshed/style/client/common.css
@@ -71,18 +71,19 @@ background-color: rgb(56, 117, 215); } -/* ss tree */ -.gwt-sstree { +/** Applies to StandardTreeView **/ +.gwt-tree-selectedItem { + background-color: rgb(56, 117, 215); +} + + +/** Applies to SideBySideTreeView **/ +.gwt-SideBySideTreeView { } .gwt-sstree-column { - overflow-y: scroll; - overflow-x: auto; - position: relative; -} - -.gwt-sstree-selectedItem { - background-color: rgb(56, 117, 215); + border-right: 1px solid #aaa; + border-left: 1px solid #aaa; } .gwt-sstree-evenRow { @@ -93,6 +94,10 @@ background-color: rgb(220, 220, 220); } +.gwt-sstree-selectedItem { + background-color: rgb(56, 117, 215); +} + /* date picker */ .gwt-DatePicker { border: 1px solid #A2BBDD;