From d9ca864ff7acf2eedb4edda46276fdb98ad35a7a Mon Sep 17 00:00:00 2001 From: Jonatan Kronqvist Date: Thu, 16 Jun 2011 07:55:38 +0000 Subject: [PATCH] Merged TreeTable into Vaadin core (#5371) svn changeset:19416/svn branch:6.7 --- .../themes/base/treetable/img/arrow-down.png | Bin 0 -> 1008 bytes .../themes/base/treetable/img/arrow-right.png | Bin 0 -> 994 bytes .../themes/base/treetable/treetable.css | 20 + .../terminal/gwt/client/ui/VScrollTable.java | 4 +- .../terminal/gwt/client/ui/VTreeTable.java | 345 +++++++++++ src/com/vaadin/ui/TreeTable.java | 579 ++++++++++++++++++ src/com/vaadin/ui/treetable/Collapsible.java | 66 ++ .../HierarchicalContainerOrderedWrapper.java | 58 ++ .../vaadin/tests/components/table/Tables.java | 52 +- .../components/treetable/TreeTables.java | 291 +++++++++ 10 files changed, 1387 insertions(+), 28 deletions(-) create mode 100644 WebContent/VAADIN/themes/base/treetable/img/arrow-down.png create mode 100644 WebContent/VAADIN/themes/base/treetable/img/arrow-right.png create mode 100644 WebContent/VAADIN/themes/base/treetable/treetable.css create mode 100644 src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java create mode 100644 src/com/vaadin/ui/TreeTable.java create mode 100644 src/com/vaadin/ui/treetable/Collapsible.java create mode 100644 src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java create mode 100644 tests/src/com/vaadin/tests/components/treetable/TreeTables.java diff --git a/WebContent/VAADIN/themes/base/treetable/img/arrow-down.png b/WebContent/VAADIN/themes/base/treetable/img/arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..cba812b799d9ac1da9a30f4654e214a2a82d59f7 GIT binary patch literal 1008 zcmVKLZ*U+Y7{CaTF2B#-+|Md-1Vw z;UD0_g)7}C5qu$tdlf|#+^DpR>U3A|c~)Qj&bhyH&Mhu<+QlHyDG76aWSm@9PEJkB z70*bqo-Q(U72PlxEEL87ib0_HCO$sF0(jW>{rpd(s}#C(V6BAI`;rP>==coODw9*w zAT*FZ83hrDb3od}`u{+BCg$rv+Kf9xAZ`Jx+`vR2?g0aCwOE2q0rXZ9H8TmG({;Bw z&mTay@+S@h8z6O7g(FS27D~n813*7muS#lo1n33pdfD4I3iJTa%Vyt1%+^LP8Xb>0 zb;Q^Cu_mwcXYz-do+5~{&XRvPe|Tg`KT+IY05V`Xv$X>bum)0lPglo}#%$@+)uDWo zADOx{0sIB2Yk@9|#j~aEFoj0p;iK@8IONFEM}VP584nMQM-GLL!r&1y``t=SA;FRy zFY$Sa!Q*f0C?pO?$mUt-Z8=E$sr|-&Y+tso*{?5V>OJkuf;sNFix=KeKw(AF8V(^$pp$Sn8Cc6FT z_7wW=nOp1Hy&j93#~yayJ+thtYTs<7T3apuZr3HxEAgk;Bu2%6*nt###9pym42uD= zQ;fAvOyJ{j5``h8h{op>a@qe|swIAF0HkxuSH^RtlgrA2sg*sg5>NCO>EDdWsc9M0 zbRBMluy$)(``};!)|$n(79E6LZ=vHsTkFZd>Pv8Qn_D;YO>4`#50UyO0vM_Vr;Jyr zMsl-lZKLZ*U+Y7{CaTF2B#-+|Md-1Vw z;UD0_g)7}C5qu$tdlf|#+^DpR>U3A|c~)Qj&bhyH&Mhu<+QlHyDG76aWSm@9PEJkB z70*bqo-Q(U72PlxEEL87ib0_HCO$sF0(jW>{rpd(s}#C(V6BAI`;rP>==coODw9*w zAT*FZ83hrDb3od}`u{+BCg$rv+Kf9xAZ`Jx+`vR2?g0aCwOE2q0rXZ9H8TmG({;Bw z&mTay@+S@h8z6O7g(FS27D~n813*7muS#lo1n33pdfD4I3iJTa%Vyt1%+^LP8Xb>0 zb;Q^Cu_mwcXYz-do+5~{&XRvPe|Tg`KT+IY05V`Xv$X>bum)0lPglo}#%$@+)uDWo zADOx{0sIB2Yk@9|#j~aEFoj0p;iK@8IONFEM}VP584nMQM-GLL!r&1y``t=SA;FRy zFY$Sa!Q*f0C?pO?$mUt-Z8=E$sr|-&Y+tso*{?5V>OJkuf;sNFix=KeKw(AF8V(^$pp$Sn8Cc6FT z_7wW=nOp1Hy&j93#~yayJ+thtYTs<7T3apuZr3HxEAgk;Bu2%6*nt###9pym42uD= zQ;fAvOyJ{j5``h8h{op>a@qe|swIAF0HkxuSH^RtlgrA2sg*sg5>NCO>EDdWsc9M0 zbRBMluy$)(``};!)|$n(79E6LZ=vHsTkFZd>Pv8Qn_D;YO>4`#50UyO0vM_Vr;Jyr zMsl-lZQWj zE5VGM`~UB+vd51fzr$uA6B0X_iG}T+oPu(Zj*bo+h8vMgL it = scrollBody.iterator(); VScrollTableRow r = null; @@ -5452,7 +5452,7 @@ public class VScrollTable extends FlowPanel implements Table, ScrollHandler, * The row to where the selection head should move * @return Returns true if focus was moved successfully, else false */ - private boolean setRowFocus(VScrollTableRow row) { + protected boolean setRowFocus(VScrollTableRow row) { if (selectMode == SELECT_MODE_NONE) { return false; diff --git a/src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java b/src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java new file mode 100644 index 0000000000..974e96e8ae --- /dev/null +++ b/src/com/vaadin/terminal/gwt/client/ui/VTreeTable.java @@ -0,0 +1,345 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.terminal.gwt.client.ui; + +import java.util.Iterator; + +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.ImageElement; +import com.google.gwt.dom.client.SpanElement; +import com.google.gwt.dom.client.Style.Unit; +import com.google.gwt.event.dom.client.KeyCodes; +import com.google.gwt.user.client.Element; +import com.google.gwt.user.client.Event; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.terminal.gwt.client.ApplicationConnection; +import com.vaadin.terminal.gwt.client.RenderSpace; +import com.vaadin.terminal.gwt.client.UIDL; +import com.vaadin.terminal.gwt.client.VConsole; +import com.vaadin.terminal.gwt.client.ui.VScrollTable.VScrollTableBody.VScrollTableRow; +import com.vaadin.terminal.gwt.client.ui.VTreeTable.VTreeTableScrollBody.VTreeTableRow; + +public class VTreeTable extends VScrollTable { + + public static final String ATTRIBUTE_HIERARCHY_COLUMN_INDEX = "hci"; + private boolean collapseRequest; + private boolean selectionPending; + private int colIndexOfHierarchy; + private String collapsedRowKey; + private VTreeTableScrollBody scrollBody; + + @Override + public void updateFromUIDL(UIDL uidl, ApplicationConnection client) { + FocusableScrollPanel widget = null; + int scrollPosition = 0; + if (collapseRequest) { + widget = (FocusableScrollPanel) getWidget(1); + scrollPosition = widget.getScrollPosition(); + } + colIndexOfHierarchy = uidl + .hasAttribute(ATTRIBUTE_HIERARCHY_COLUMN_INDEX) ? uidl + .getIntAttribute(ATTRIBUTE_HIERARCHY_COLUMN_INDEX) : 0; + super.updateFromUIDL(uidl, client); + if (collapseRequest) { + if (collapsedRowKey != null && scrollBody != null) { + VScrollTableRow row = getRenderedRowByKey(collapsedRowKey); + if (row != null) { + setRowFocus(row); + focus(); + } + } + + int scrollPosition2 = widget.getScrollPosition(); + if (scrollPosition != scrollPosition2) { + VConsole.log("TT scrollpos from " + scrollPosition + " to " + + scrollPosition2); + widget.setScrollPosition(scrollPosition); + } + collapseRequest = false; + } + if (uidl.hasAttribute("focusedRow")) { + // TODO figure out if the row needs to focused at all + + // scrolled to parent by the server, focusedRow is probably the sam + // as the first row in view port + } + } + + @Override + protected VScrollTableBody createScrollBody() { + scrollBody = new VTreeTableScrollBody(); + return scrollBody; + } + + class VTreeTableScrollBody extends VScrollTable.VScrollTableBody { + private int identWidth = -1; + + VTreeTableScrollBody() { + super(); + } + + @Override + protected VScrollTableRow createRow(UIDL uidl, char[] aligns2) { + return new VTreeTableRow(uidl, aligns2); + } + + class VTreeTableRow extends + VScrollTable.VScrollTableBody.VScrollTableRow { + + private boolean isTreeCellAdded = false; + private SpanElement treeSpacer; + private boolean open; + private int depth; + private boolean canHaveChildren; + private Widget widgetInHierarchyColumn; + + public VTreeTableRow(UIDL uidl, char[] aligns2) { + super(uidl, aligns2); + } + + @Override + public void addCell(UIDL rowUidl, String text, char align, + String style, boolean textIsHTML, boolean isSorted) { + super.addCell(rowUidl, text, align, style, textIsHTML, isSorted); + + addTreeSpacer(rowUidl); + } + + private boolean addTreeSpacer(UIDL rowUidl) { + if (cellShowsTreeHierarchy(getElement().getChildCount() - 1)) { + Element container = (Element) getElement().getLastChild() + .getFirstChild(); + + if (rowUidl.hasAttribute("icon")) { + // icons are in first content cell in TreeTable + ImageElement icon = Document.get().createImageElement(); + icon.setClassName("v-icon"); + icon.setAlt("icon"); + icon.setSrc(client.translateVaadinUri(rowUidl + .getStringAttribute("icon"))); + container.insertFirst(icon); + } + + String classname = "v-treetable-treespacer"; + if (rowUidl.getBooleanAttribute("ca")) { + canHaveChildren = true; + open = rowUidl.getBooleanAttribute("open"); + classname += open ? " v-treetable-node-open" + : " v-treetable-node-closed"; + } + + treeSpacer = Document.get().createSpanElement(); + + treeSpacer.setClassName(classname); + container.insertFirst(treeSpacer); + depth = rowUidl.hasAttribute("depth") ? rowUidl + .getIntAttribute("depth") : 0; + setIdent(); + isTreeCellAdded = true; + return true; + } + return false; + } + + private boolean cellShowsTreeHierarchy(int curColIndex) { + if (isTreeCellAdded) { + return false; + } + return curColIndex == colIndexOfHierarchy + + (showRowHeaders ? 1 : 0); + } + + @Override + public void onBrowserEvent(Event event) { + if (event.getEventTarget().cast() == treeSpacer + && treeSpacer.getClassName().contains("node")) { + if (event.getTypeInt() == Event.ONMOUSEUP) { + sendToggleCollapsedUpdate(getKey()); + } + return; + } + super.onBrowserEvent(event); + } + + @Override + public void addCell(UIDL rowUidl, Widget w, char align, + String style, boolean isSorted) { + super.addCell(rowUidl, w, align, style, isSorted); + if (addTreeSpacer(rowUidl)) { + widgetInHierarchyColumn = w; + } + + } + + private void setIdent() { + if (getIdentWidth() > 0 && depth != 0) { + treeSpacer.getStyle().setWidth( + (depth + 1) * getIdentWidth(), Unit.PX); + } + } + + @Override + protected void onAttach() { + super.onAttach(); + if (getIdentWidth() < 0) { + detectIdent(this); + } + } + + @Override + public RenderSpace getAllocatedSpace(Widget child) { + if (widgetInHierarchyColumn == child) { + final int hierarchyAndIconWidth = getHierarchyAndIconWidth(); + final RenderSpace allocatedSpace = super + .getAllocatedSpace(child); + return new RenderSpace() { + @Override + public int getWidth() { + return allocatedSpace.getWidth() + - hierarchyAndIconWidth; + } + + @Override + public int getHeight() { + return allocatedSpace.getHeight(); + } + + }; + } + return super.getAllocatedSpace(child); + } + + private int getHierarchyAndIconWidth() { + int consumedSpace = treeSpacer.getOffsetWidth(); + if (treeSpacer.getParentElement().getChildCount() > 2) { + // icon next to tree spacer + consumedSpace += ((com.google.gwt.dom.client.Element) treeSpacer + .getNextSibling()).getOffsetWidth(); + } + return consumedSpace; + } + + } + + private int getIdentWidth() { + return identWidth; + } + + private void detectIdent(VTreeTableRow vTreeTableRow) { + identWidth = vTreeTableRow.treeSpacer.getOffsetWidth(); + if (identWidth == 0) { + identWidth = -1; + return; + } + Iterator iterator = iterator(); + while (iterator.hasNext()) { + VTreeTableRow next = (VTreeTableRow) iterator.next(); + next.setIdent(); + } + } + } + + /** + * Icons rendered into first actual column in TreeTable, not to row header + * cell + */ + @Override + protected String buildCaptionHtmlSnippet(UIDL uidl) { + if (uidl.getTag().equals("column")) { + return super.buildCaptionHtmlSnippet(uidl); + } else { + String s = uidl.getStringAttribute("caption"); + return s; + } + } + + @Override + protected boolean handleNavigation(int keycode, boolean ctrl, boolean shift) { + VTreeTableRow focusedRow = (VTreeTableRow) getFocusedRow(); + if (focusedRow != null) { + if (focusedRow.canHaveChildren + && ((keycode == KeyCodes.KEY_RIGHT && !focusedRow.open) || (keycode == KeyCodes.KEY_LEFT && focusedRow.open))) { + if (!ctrl) { + client.updateVariable(paintableId, "selectCollapsed", true, + false); + } + sendToggleCollapsedUpdate(focusedRow.getKey()); + return true; + } else if (keycode == KeyCodes.KEY_RIGHT && focusedRow.open) { + // already expanded, move selection down if next is on a deeper + // level (is-a-child) + VTreeTableScrollBody body = (VTreeTableScrollBody) focusedRow + .getParent(); + Iterator iterator = body.iterator(); + VTreeTableRow next = null; + while (iterator.hasNext()) { + next = (VTreeTableRow) iterator.next(); + if (next == focusedRow) { + next = (VTreeTableRow) iterator.next(); + break; + } + } + if (next != null) { + if (next.depth > focusedRow.depth) { + selectionPending = true; + return super.handleNavigation(getNavigationDownKey(), + ctrl, shift); + } + } else { + // Note, a minor change here for a bit false behavior if + // cache rows is disabled + last visible row + no childs for + // the node + selectionPending = true; + return super.handleNavigation(getNavigationDownKey(), ctrl, + shift); + } + } else if (keycode == KeyCodes.KEY_LEFT) { + // already collapsed move selection up to parent node + // do on the server side as the parent is not necessary + // rendered on the client, could check if parent is visible if + // a performance issue arises + + client.updateVariable(paintableId, "focusParent", + focusedRow.getKey(), true); + return true; + } + } + return super.handleNavigation(keycode, ctrl, shift); + } + + private void sendToggleCollapsedUpdate(String rowKey) { + collapsedRowKey = rowKey; + collapseRequest = true; + client.updateVariable(paintableId, "toggleCollapsed", rowKey, true); + } + + @Override + public void onBrowserEvent(Event event) { + super.onBrowserEvent(event); + if (event.getTypeInt() == Event.ONKEYUP && selectionPending) { + sendSelectedRows(); + } + } + + @Override + protected void sendSelectedRows() { + super.sendSelectedRows(); + selectionPending = false; + } + + @Override + protected void reOrderColumn(String columnKey, int newIndex) { + super.reOrderColumn(columnKey, newIndex); + // current impl not intelligent enough to survive without visiting the + // server to redraw content + client.sendPendingVariableChanges(); + } + + @Override + public void setStyleName(String style) { + super.setStyleName(style + " v-treetable"); + } + +} diff --git a/src/com/vaadin/ui/TreeTable.java b/src/com/vaadin/ui/TreeTable.java new file mode 100644 index 0000000000..20fc8e44d7 --- /dev/null +++ b/src/com/vaadin/ui/TreeTable.java @@ -0,0 +1,579 @@ +/* +@ITMillApache2LicenseForJavaFiles@ + */ + +package com.vaadin.ui; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import com.google.gwt.user.client.ui.Tree; +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.Container.ItemSetChangeEvent; +import com.vaadin.data.util.ContainerHierarchicalWrapper; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.terminal.PaintException; +import com.vaadin.terminal.PaintTarget; +import com.vaadin.terminal.Resource; +import com.vaadin.terminal.gwt.client.ui.VTreeTable; +import com.vaadin.ui.treetable.Collapsible; +import com.vaadin.ui.treetable.HierarchicalContainerOrderedWrapper; + +/** + * TreeTable extends the {@link Table} component so that it can also visualize a + * hierarchy of its Items in a similar manner that {@link Tree} does. The tree + * hierarchy is always displayed in the first actual column of the TreeTable. + *

+ * The TreeTable supports the usual {@link Table} features like lazy loading, so + * it should be no problem to display lots of items at once. Only required rows + * and some cache rows are sent to the client. + *

+ * TreeTable supports standard {@link Hierarchical} container interfaces, but + * also a more fine tuned version - {@link Collapsible}. A container + * implementing the {@link Collapsible} interface stores the collapsed/expanded + * state internally and can this way scale better on the server side than with + * standard Hierarchical implementations. Developer must however note that + * {@link Collapsible} containers can not be shared among several users as they + * share UI state in the container. + */ +@SuppressWarnings({ "serial" }) +@ClientWidget(VTreeTable.class) +public class TreeTable extends Table implements Hierarchical { + + private interface ContainerStrategy extends Serializable { + public int size(); + + public boolean isNodeOpen(Object itemId); + + public int getDepth(Object itemId); + + public void toggleChildVisibility(Object itemId); + + public Object getIdByIndex(int index); + + public int indexOfId(Object id); + + public Object nextItemId(Object itemId); + + public Object lastItemId(); + + public Object prevItemId(Object itemId); + + public boolean isLastId(Object itemId); + + public Collection getItemIds(); + + public void containerItemSetChange(ItemSetChangeEvent event); + } + + private abstract class AbstractStrategy implements ContainerStrategy { + + /** + * Consider adding getDepth to {@link Collapsible}, might help + * scalability with some container implementations. + */ + public int getDepth(Object itemId) { + int depth = 0; + Hierarchical hierarchicalContainer = getContainerDataSource(); + while (!hierarchicalContainer.isRoot(itemId)) { + depth++; + itemId = hierarchicalContainer.getParent(itemId); + } + return depth; + } + + public void containerItemSetChange(ItemSetChangeEvent event) { + } + + } + + /** + * This strategy is used if current container implements {@link Collapsible} + * . + * + * open-collapsed logic diverted to container, otherwise use default + * implementations. + */ + private class CollapsibleStrategy extends AbstractStrategy { + + private Collapsible c() { + return (Collapsible) getContainerDataSource(); + } + + public void toggleChildVisibility(Object itemId) { + c().setCollapsed(itemId, !c().isCollapsed(itemId)); + } + + public boolean isNodeOpen(Object itemId) { + return !c().isCollapsed(itemId); + } + + public int size() { + return TreeTable.super.size(); + } + + public Object getIdByIndex(int index) { + return TreeTable.super.getIdByIndex(index); + } + + public int indexOfId(Object id) { + return TreeTable.super.indexOfId(id); + } + + public boolean isLastId(Object itemId) { + // using the default impl + return TreeTable.super.isLastId(itemId); + } + + public Object lastItemId() { + // using the default impl + return TreeTable.super.lastItemId(); + } + + public Object nextItemId(Object itemId) { + return TreeTable.super.nextItemId(itemId); + } + + public Object prevItemId(Object itemId) { + return TreeTable.super.prevItemId(itemId); + } + + public Collection getItemIds() { + return TreeTable.super.getItemIds(); + } + + } + + /** + * Strategy for Hierarchical but not Collapsible container like + * {@link HierarchicalContainer}. + * + * Store collapsed/open states internally, fool Table to use preorder when + * accessing items from container via Ordered/Indexed methods. + */ + private class HierarchicalStrategy extends AbstractStrategy { + + private final HashSet openItems = new HashSet(); + + public boolean isNodeOpen(Object itemId) { + return openItems.contains(itemId); + } + + public int size() { + return getPreOrder().size(); + } + + public Collection getItemIds() { + return Collections.unmodifiableCollection(getPreOrder()); + } + + public boolean isLastId(Object itemId) { + return itemId.equals(lastItemId()); + } + + public Object lastItemId() { + if (getPreOrder().size() > 0) { + return getPreOrder().get(getPreOrder().size() - 1); + } else { + return null; + } + } + + public Object nextItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + if (indexOf == -1) { + return null; + } + indexOf++; + if (indexOf == getPreOrder().size()) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + public Object prevItemId(Object itemId) { + int indexOf = getPreOrder().indexOf(itemId); + indexOf--; + if (indexOf < 0) { + return null; + } else { + return getPreOrder().get(indexOf); + } + } + + public void toggleChildVisibility(Object itemId) { + boolean removed = openItems.remove(itemId); + if (!removed) { + openItems.add(itemId); + } + clearPreorderCache(); + } + + private void clearPreorderCache() { + preOrder = null; // clear preorder cache + } + + List preOrder; + + /** + * Preorder of ids currently visible + * + * @return + */ + private List getPreOrder() { + if (preOrder == null) { + preOrder = new ArrayList(); + Collection rootItemIds = getContainerDataSource() + .rootItemIds(); + for (Object id : rootItemIds) { + preOrder.add(id); + addVisibleChildTree(id); + } + } + return preOrder; + } + + private void addVisibleChildTree(Object id) { + if (isNodeOpen(id)) { + Collection children = getContainerDataSource().getChildren( + id); + if (children != null) { + for (Object childId : children) { + preOrder.add(childId); + addVisibleChildTree(childId); + } + } + } + + } + + public int indexOfId(Object id) { + return getPreOrder().indexOf(id); + } + + public Object getIdByIndex(int index) { + return getPreOrder().get(index); + } + + @Override + public void containerItemSetChange(ItemSetChangeEvent event) { + // preorder becomes invalid on sort, item additions etc. + clearPreorderCache(); + super.containerItemSetChange(event); + } + + } + + /** + * Creates an empty TreeTable with a default container. + */ + public TreeTable() { + super(null, new HierarchicalContainer()); + } + + /** + * Creates an empty TreeTable with a default container. + * + * @param caption + * the caption for the TreeTable + */ + public TreeTable(String caption) { + this(); + setCaption(caption); + } + + /** + * Creates a TreeTable instance with given captions and data source. + * + * @param caption + * the caption for the component + * @param dataSource + * the dataSource that is used to list items in the component + */ + public TreeTable(String caption, Container dataSource) { + super(caption, dataSource); + } + + private ContainerStrategy cStrategy; + private Object focusedRowId = null; + private Object hierarchyColumnId; + + private ContainerStrategy getContainerStrategy() { + if (cStrategy == null) { + if (getContainerDataSource() instanceof Collapsible) { + cStrategy = new CollapsibleStrategy(); + } else { + cStrategy = new HierarchicalStrategy(); + } + } + return cStrategy; + } + + @Override + protected void paintRowAttributes(PaintTarget target, Object itemId) + throws PaintException { + super.paintRowAttributes(target, itemId); + target.addAttribute("depth", getContainerStrategy().getDepth(itemId)); + if (getContainerDataSource().areChildrenAllowed(itemId)) { + target.addAttribute("ca", true); + target.addAttribute("open", + getContainerStrategy().isNodeOpen(itemId)); + } + } + + @Override + protected void paintRowIcon(PaintTarget target, Object[][] cells, + int indexInRowbuffer) throws PaintException { + // always paint if present (in parent only if row headers visible) + if (getRowHeaderMode() == ROW_HEADER_MODE_HIDDEN) { + Resource itemIcon = getItemIcon(cells[CELL_ITEMID][indexInRowbuffer]); + if (itemIcon != null) { + target.addAttribute("icon", itemIcon); + } + } else if (cells[CELL_ICON][indexInRowbuffer] != null) { + target.addAttribute("icon", + (Resource) cells[CELL_ICON][indexInRowbuffer]); + } + } + + @Override + public void changeVariables(Object source, Map variables) { + super.changeVariables(source, variables); + + if (variables.containsKey("toggleCollapsed")) { + String object = (String) variables.get("toggleCollapsed"); + Object itemId = itemIdMapper.get(object); + toggleChildVisibility(itemId); + if (variables.containsKey("selectCollapsed")) { + // ensure collapsed is selected unless opened with selection + // head + if (isSelectable()) { + select(itemId); + } + } + } else if (variables.containsKey("focusParent")) { + String key = (String) variables.get("focusParent"); + Object refId = itemIdMapper.get(key); + Object itemId = getParent(refId); + focusParent(itemId); + } + } + + private void focusParent(Object itemId) { + boolean inView = false; + Object inPageId = getCurrentPageFirstItemId(); + for (int i = 0; inPageId != null && i < getPageLength(); i++) { + if (inPageId.equals(itemId)) { + inView = true; + break; + } + inPageId = nextItemId(inPageId); + i++; + } + if (!inView) { + setCurrentPageFirstItemId(itemId); + } + if (isSelectable()) { + if (isMultiSelect()) { + setValue(Collections.singleton(itemId)); + } else { + setValue(itemId); + } + } else { + // just instruct the VTreeTable to set focus the row (not to select) + setFocusedRow(itemId); + } + } + + private void setFocusedRow(Object itemId) { + focusedRowId = itemId; + requestRepaint(); + } + + @Override + public void paintContent(PaintTarget target) throws PaintException { + if (focusedRowId != null) { + target.addAttribute("focusedRow", itemIdMapper.key(focusedRowId)); + focusedRowId = null; + } + if (hierarchyColumnId != null) { + Object[] visibleColumns2 = getVisibleColumns(); + for (int i = 0; i < visibleColumns2.length; i++) { + Object object = visibleColumns2[i]; + if (hierarchyColumnId.equals(object)) { + target.addAttribute( + VTreeTable.ATTRIBUTE_HIERARCHY_COLUMN_INDEX, i); + break; + } + } + } + super.paintContent(target); + } + + private void toggleChildVisibility(Object itemId) { + getContainerStrategy().toggleChildVisibility(itemId); + // ensure that page still has first item in page, ignore buffer refresh + // (forced in this method) + setCurrentPageFirstItemIndex(getCurrentPageFirstItemIndex()); + + requestRepaint(); + } + + @Override + public int size() { + return getContainerStrategy().size(); + } + + @Override + public Hierarchical getContainerDataSource() { + return (Hierarchical) super.getContainerDataSource(); + } + + @Override + public void setContainerDataSource(Container newDataSource) { + cStrategy = null; + if (!(newDataSource instanceof Hierarchical)) { + newDataSource = new ContainerHierarchicalWrapper(newDataSource); + } + + if (!(newDataSource instanceof Ordered)) { + newDataSource = new HierarchicalContainerOrderedWrapper( + (Hierarchical) newDataSource); + } + + super.setContainerDataSource(newDataSource); + } + + @Override + public void containerItemSetChange( + com.vaadin.data.Container.ItemSetChangeEvent event) { + getContainerStrategy().containerItemSetChange(event); + super.containerItemSetChange(event); + } + + @Override + protected Object getIdByIndex(int index) { + return getContainerStrategy().getIdByIndex(index); + } + + @Override + protected int indexOfId(Object itemId) { + return getContainerStrategy().indexOfId(itemId); + } + + @Override + public Object nextItemId(Object itemId) { + return getContainerStrategy().nextItemId(itemId); + } + + @Override + public Object lastItemId() { + return getContainerStrategy().lastItemId(); + } + + @Override + public Object prevItemId(Object itemId) { + return getContainerStrategy().prevItemId(itemId); + } + + @Override + public boolean isLastId(Object itemId) { + return getContainerStrategy().isLastId(itemId); + } + + @Override + public Collection getItemIds() { + return getContainerStrategy().getItemIds(); + } + + public boolean areChildrenAllowed(Object itemId) { + return getContainerDataSource().areChildrenAllowed(itemId); + } + + public Collection getChildren(Object itemId) { + return getContainerDataSource().getChildren(itemId); + } + + public Object getParent(Object itemId) { + return getContainerDataSource().getParent(itemId); + } + + public boolean hasChildren(Object itemId) { + return getContainerDataSource().hasChildren(itemId); + } + + public boolean isRoot(Object itemId) { + return getContainerDataSource().isRoot(itemId); + } + + public Collection rootItemIds() { + return getContainerDataSource().rootItemIds(); + } + + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + return getContainerDataSource().setChildrenAllowed(itemId, + areChildrenAllowed); + } + + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + return getContainerDataSource().setParent(itemId, newParentId); + } + + /** + * Sets the Item specified by given identifier collapsed or expanded. If the + * Item is collapsed, its children is not displayed in for the user. + * + * @param itemId + * the identifier of the Item + * @param collapsed + * true if the Item should be collapsed, false if expanded + */ + public void setCollapsed(Object itemId, boolean collapsed) { + if (isCollapsed(itemId) != collapsed) { + toggleChildVisibility(itemId); + } + } + + /** + * Checks if Item with given identifier is collapsed in the UI. + * + *

+ * + * @param itemId + * the identifier of the checked Item + * @return true if the Item with given id is collapsed + * @see Collapsible#isCollapsed(Object) + */ + public boolean isCollapsed(Object itemId) { + return !getContainerStrategy().isNodeOpen(itemId); + } + + /** + * Explicitly sets the column in which the TreeTable visualizes the + * hierarchy. If hierarchyColumnId is not set, the hierarchy is visualized + * in the first visible column. + * + * @param hierarchyColumnId + */ + public void setHierarchyColumn(Object hierarchyColumnId) { + this.hierarchyColumnId = hierarchyColumnId; + } + + /** + * @return the identifier of column into which the hierarchy will be + * visualized or null if the column is not explicitly defined. + */ + public Object getHierarchyColumnId() { + return hierarchyColumnId; + } + +} diff --git a/src/com/vaadin/ui/treetable/Collapsible.java b/src/com/vaadin/ui/treetable/Collapsible.java new file mode 100644 index 0000000000..649687716c --- /dev/null +++ b/src/com/vaadin/ui/treetable/Collapsible.java @@ -0,0 +1,66 @@ +package com.vaadin.ui.treetable; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.Container.Ordered; +import com.vaadin.data.Item; + +/** + * Container needed by large lazy loading hierarchies displayed in TreeTable. + *

+ * Container of this type gets notified when a subtree is opened/closed in a + * component displaying its content. This allows container to lazy load subtrees + * and release memory when a sub-tree is no longer displayed. + *

+ * Methods from {@link Container.Ordered} (and from {@linkContainer.Indexed} if + * implemented) are expected to work as in "preorder" of the currently visible + * hierarchy. This means for example that the return value of size method + * changes when subtree is collapsed/expanded. In other words items in collapsed + * sub trees should be "ignored" by container when the container is accessed + * with methods introduced in {@link Container.Ordered} or + * {@linkContainer.Indexed}. From the accessors point of view, items in + * collapsed subtrees don't exist. + *

+ * + */ +public interface Collapsible extends Hierarchical, Ordered { + + /** + *

+ * Collapsing the {@link Item} indicated by itemId hides all + * children, and their respective children, from the {@link Container}. + *

+ * + *

+ * If called on a leaf {@link Item}, this method does nothing. + *

+ * + * @param itemId + * the identifier of the collapsed {@link Item} + * @param collapsed + * true if you want to collapse the children below + * this {@link Item}. false if you want to + * uncollapse the children. + */ + public void setCollapsed(Object itemId, boolean collapsed); + + /** + *

+ * Checks whether the {@link Item}, identified by itemId is + * collapsed or not. + *

+ * + *

+ * If an {@link Item} is "collapsed" its children are not included in + * methods used to list Items in this container. + *

+ * + * @param itemId + * The {@link Item}'s identifier that is to be checked. + * @return true iff the {@link Item} identified by + * itemId is currently collapsed, otherwise + * false. + */ + public boolean isCollapsed(Object itemId); + +} diff --git a/src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java b/src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java new file mode 100644 index 0000000000..0c00027f26 --- /dev/null +++ b/src/com/vaadin/ui/treetable/HierarchicalContainerOrderedWrapper.java @@ -0,0 +1,58 @@ +package com.vaadin.ui.treetable; + +import java.util.Collection; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.util.ContainerOrderedWrapper; + +@SuppressWarnings({ "serial", "unchecked" }) +/** + * Helper for TreeTable. Does the same thing as ContainerOrderedWrapper + * to fit into table but retains Hierarchical feature. + */ +public class HierarchicalContainerOrderedWrapper extends + ContainerOrderedWrapper implements Hierarchical { + + private Hierarchical hierarchical; + + public HierarchicalContainerOrderedWrapper(Hierarchical toBeWrapped) { + super(toBeWrapped); + hierarchical = toBeWrapped; + } + + public boolean areChildrenAllowed(Object itemId) { + return hierarchical.areChildrenAllowed(itemId); + } + + public Collection getChildren(Object itemId) { + return hierarchical.getChildren(itemId); + } + + public Object getParent(Object itemId) { + return hierarchical.getParent(itemId); + } + + public boolean hasChildren(Object itemId) { + return hierarchical.hasChildren(itemId); + } + + public boolean isRoot(Object itemId) { + return hierarchical.isRoot(itemId); + } + + public Collection rootItemIds() { + return hierarchical.rootItemIds(); + } + + public boolean setChildrenAllowed(Object itemId, boolean areChildrenAllowed) + throws UnsupportedOperationException { + return hierarchical.setChildrenAllowed(itemId, areChildrenAllowed); + } + + public boolean setParent(Object itemId, Object newParentId) + throws UnsupportedOperationException { + return hierarchical.setParent(itemId, newParentId); + } + +} diff --git a/tests/src/com/vaadin/tests/components/table/Tables.java b/tests/src/com/vaadin/tests/components/table/Tables.java index 4a0b914534..a128879e21 100644 --- a/tests/src/com/vaadin/tests/components/table/Tables.java +++ b/tests/src/com/vaadin/tests/components/table/Tables.java @@ -18,8 +18,8 @@ import com.vaadin.ui.Table.FooterClickListener; import com.vaadin.ui.Table.HeaderClickEvent; import com.vaadin.ui.Table.HeaderClickListener; -public class Tables extends AbstractSelectTestCase implements - ItemClickListener, HeaderClickListener, FooterClickListener, +public class Tables extends AbstractSelectTestCase + implements ItemClickListener, HeaderClickListener, FooterClickListener, ColumnResizeListener { protected static final String CATEGORY_ROWS = "Rows"; @@ -28,12 +28,12 @@ public class Tables extends AbstractSelectTestCase
implements private static final String CATEGORY_VISIBLE_COLUMNS = "Visible columns"; @Override - protected Class
getTestClass() { - return Table.class; + protected Class getTestClass() { + return (Class) Table.class; } /* COMMANDS */ - private Command visibleColumnCommand = new Command() { + private Command visibleColumnCommand = new Command() { public void execute(Table c, Boolean visible, Object propertyId) { List visibleColumns = new ArrayList(Arrays.asList(c .getVisibleColumns())); @@ -50,7 +50,7 @@ public class Tables extends AbstractSelectTestCase
implements } }; - protected Command columnResizeListenerCommand = new Command() { + protected Command columnResizeListenerCommand = new Command() { public void execute(Table c, Boolean value, Object data) { if (value) { @@ -61,9 +61,9 @@ public class Tables extends AbstractSelectTestCase
implements } }; - protected Command headerClickListenerCommand = new Command() { + protected Command headerClickListenerCommand = new Command() { - public void execute(Table c, Boolean value, Object data) { + public void execute(T c, Boolean value, Object data) { if (value) { c.addListener((HeaderClickListener) Tables.this); } else { @@ -72,7 +72,7 @@ public class Tables extends AbstractSelectTestCase
implements } }; - protected Command footerClickListenerCommand = new Command() { + protected Command footerClickListenerCommand = new Command() { public void execute(Table c, Boolean value, Object data) { if (value) { @@ -83,7 +83,7 @@ public class Tables extends AbstractSelectTestCase
implements } }; - protected Command rowHeaderModeCommand = new Command() { + protected Command rowHeaderModeCommand = new Command() { public void execute(Table c, Integer value, Object data) { if (value == Table.ROW_HEADER_MODE_PROPERTY) { @@ -93,7 +93,7 @@ public class Tables extends AbstractSelectTestCase
implements } }; - protected Command footerTextCommand = new Command() { + protected Command footerTextCommand = new Command() { public void execute(Table c, String value, Object data) { for (Object propertyId : c.getContainerPropertyIds()) { @@ -111,18 +111,18 @@ public class Tables extends AbstractSelectTestCase
implements } - protected Command columnAlignmentCommand = new Command() { + protected Command columnAlignmentCommand = new Command() { - public void execute(Table c, Alignments value, Object data) { + public void execute(T c, Alignments value, Object data) { // TODO // for (Object propertyId : c.getContainerPropertyIds()) { // } } }; - private Command contextMenuCommand = new Command() { + private Command contextMenuCommand = new Command() { - public void execute(Table c, final ContextMenu value, Object data) { + public void execute(T c, final ContextMenu value, Object data) { c.removeAllActionHandlers(); if (value != null) { c.addActionHandler(new Handler() { @@ -207,7 +207,7 @@ public class Tables extends AbstractSelectTestCase
implements private void createColumnReorderingAllowedCheckbox(String category) { createBooleanAction("Column reordering allowed", category, true, - new Command() { + new Command() { public void execute(Table c, Boolean value, Object data) { c.setColumnReorderingAllowed(value); } @@ -216,8 +216,8 @@ public class Tables extends AbstractSelectTestCase
implements private void createColumnCollapsingAllowedCheckbox(String category) { createBooleanAction("Column collapsing allowed", category, true, - new Command() { - public void execute(Table c, Boolean value, Object data) { + new Command() { + public void execute(T c, Boolean value, Object data) { c.setColumnCollapsingAllowed(value); } }); @@ -266,8 +266,8 @@ public class Tables extends AbstractSelectTestCase
implements options.put("Header {id} - every second", "Header {id}"); createSelectAction("Texts in header", category, options, "None", - new Command() { - public void execute(Table c, String value, Object data) { + new Command() { + public void execute(T c, String value, Object data) { int nr = 0; for (Object propertyId : c.getContainerPropertyIds()) { nr++; @@ -326,9 +326,9 @@ public class Tables extends AbstractSelectTestCase
implements protected void createFooterVisibilityCheckbox(String category) { createBooleanAction("Footer visible", category, true, - new Command() { + new Command() { - public void execute(Table c, Boolean value, Object data) { + public void execute(T c, Boolean value, Object data) { c.setFooterVisible(value); } }); @@ -343,9 +343,9 @@ public class Tables extends AbstractSelectTestCase
implements options.put("Hidden", Table.COLUMN_HEADER_MODE_HIDDEN); createSelectAction("Header mode", category, options, - "Explicit defaults id", new Command() { + "Explicit defaults id", new Command() { - public void execute(Table c, Integer value, Object data) { + public void execute(T c, Integer value, Object data) { c.setColumnHeaderMode(value); } @@ -361,7 +361,7 @@ public class Tables extends AbstractSelectTestCase
implements options.put("50", 50); createSelectAction("PageLength", category, options, "10", - new Command() { + new Command() { public void execute(Table t, Integer value, Object data) { t.setPageLength(value); @@ -381,7 +381,7 @@ public class Tables extends AbstractSelectTestCase
implements options.put("Multi - ctrl/shift", SelectMode.MULTI); createSelectAction("Selection Mode", category, options, - "Multi - ctrl/shift", new Command() { + "Multi - ctrl/shift", new Command() { public void execute(Table t, SelectMode value, Object data) { switch (value) { diff --git a/tests/src/com/vaadin/tests/components/treetable/TreeTables.java b/tests/src/com/vaadin/tests/components/treetable/TreeTables.java new file mode 100644 index 0000000000..523869e838 --- /dev/null +++ b/tests/src/com/vaadin/tests/components/treetable/TreeTables.java @@ -0,0 +1,291 @@ +package com.vaadin.tests.components.treetable; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; + +import com.vaadin.data.Container; +import com.vaadin.data.Container.Hierarchical; +import com.vaadin.data.util.HierarchicalContainer; +import com.vaadin.tests.components.table.Tables; +import com.vaadin.ui.Table.CellStyleGenerator; +import com.vaadin.ui.Tree.CollapseEvent; +import com.vaadin.ui.Tree.CollapseListener; +import com.vaadin.ui.Tree.ExpandEvent; +import com.vaadin.ui.TreeTable; + +public class TreeTables extends Tables implements CollapseListener { + + @Override + protected Class getTestClass() { + return TreeTable.class; + } + + private int rootItemIds = 3; + private CellStyleGenerator rootGreenSecondLevelRed = new com.vaadin.ui.Table.CellStyleGenerator() { + + public String getStyle(Object itemId, Object propertyId) { + if (propertyId != null) { + return null; + } + + Hierarchical c = getComponent().getContainerDataSource(); + if (c.isRoot(itemId)) { + return "green"; + } + + Object parent = c.getParent(itemId); + if (!c.isRoot(parent)) { + return "red"; + } + + return null; + } + + @Override + public String toString() { + return "Root green, second level red"; + } + + }; + + private CellStyleGenerator evenItemsBold = new CellStyleGenerator() { + + public String getStyle(Object itemId, Object propertyId) { + if (propertyId != null) { + return null; + } + + Hierarchical c = getComponent().getContainerDataSource(); + int idx = 0; + + for (Iterator i = c.getItemIds().iterator(); i.hasNext();) { + Object id = i.next(); + if (id == itemId) { + if (idx % 2 == 1) { + return "bold"; + } else { + return null; + } + } + + idx++; + } + + return null; + } + + @Override + public String toString() { + return "Even items bold"; + }; + + }; + + @Override + protected void createActions() { + super.createActions(); + + // Causes container changes so doing this first.. + createRootItemSelectAction(CATEGORY_DATA_SOURCE); + + createExpandCollapseActions(CATEGORY_FEATURES); + createSelectionModeSelect(CATEGORY_SELECTION); + createChildrenAllowedAction(CATEGORY_DATA_SOURCE); + + createListeners(CATEGORY_LISTENERS); + // createItemStyleGenerator(CATEGORY_FEATURES); + + // TODO: DropHandler + // TODO: DragMode + // TODO: ActionHandler + + } + + @Override + protected Container createContainer(int properties, int items) { + return createHierarchicalContainer(properties, items, rootItemIds); + } + + private void createListeners(String category) { + // createBooleanAction("Expand listener", category, false, + // expandListenerCommand); + // createBooleanAction("Collapse listener", category, false, + // collapseListenerCommand); + createBooleanAction("Item click listener", category, false, + itemClickListenerCommand); + + } + + private Container.Hierarchical createHierarchicalContainer(int properties, + int items, int roots) { + Container.Hierarchical c = new HierarchicalContainer(); + + populateContainer(c, properties, items); + + if (items <= roots) { + return c; + } + + // "roots" roots, each with + // "firstLevel" children, two with no children (one with childAllowed, + // one without) + // ("firstLevel"-2)*"secondLevel" children ("secondLevel"/2 with + // childAllowed, "secondLevel"/2 without) + + // N*M+N*(M-2)*C = items + // items=N(M+MC-2C) + + // Using secondLevel=firstLevel/2 => + // items = roots*(firstLevel+firstLevel*firstLevel/2-2*firstLevel/2) + // =roots*(firstLevel+firstLevel^2/2-firstLevel) + // = roots*firstLevel^2/2 + // => firstLevel = sqrt(items/roots*2) + + int firstLevel = (int) Math.ceil(Math.sqrt(items / roots * 2.0)); + int secondLevel = firstLevel / 2; + + while (roots * (1 + 2 + (firstLevel - 2) * secondLevel) < items) { + // Increase something so we get enough items + secondLevel++; + } + + List itemIds = new ArrayList(c.getItemIds()); + + int nextItemId = roots; + for (int rootIndex = 0; rootIndex < roots; rootIndex++) { + // roots use items 0..roots-1 + Object rootItemId = itemIds.get(rootIndex); + + // force roots to be roots even though they automatically should be + c.setParent(rootItemId, null); + + for (int firstLevelIndex = 0; firstLevelIndex < firstLevel; firstLevelIndex++) { + if (nextItemId >= items) { + break; + } + Object firstLevelItemId = itemIds.get(nextItemId++); + c.setParent(firstLevelItemId, rootItemId); + + if (firstLevelIndex < 2) { + continue; + } + + // firstLevelChildren 2.. have child nodes + for (int secondLevelIndex = 0; secondLevelIndex < secondLevel; secondLevelIndex++) { + if (nextItemId >= items) { + break; + } + + Object secondLevelItemId = itemIds.get(nextItemId++); + c.setParent(secondLevelItemId, firstLevelItemId); + } + } + } + + return c; + } + + private void createRootItemSelectAction(String category) { + LinkedHashMap options = new LinkedHashMap(); + for (int i = 1; i <= 10; i++) { + options.put(String.valueOf(i), i); + } + options.put("20", 20); + options.put("50", 50); + options.put("100", 100); + + createSelectAction("Number of root items", category, options, "3", + rootItemIdsCommand); + } + + private void createExpandCollapseActions(String category) { + LinkedHashMap options = new LinkedHashMap(); + + for (Object id : getComponent().getItemIds()) { + options.put(id.toString(), id); + } + createMultiClickAction("Expand", category, options, expandItemCommand, + null); + // createMultiClickAction("Expand recursively", category, options, + // expandItemRecursivelyCommand, null); + createMultiClickAction("Collapse", category, options, + collapseItemCommand, null); + + } + + private void createChildrenAllowedAction(String category) { + LinkedHashMap options = new LinkedHashMap(); + + for (Object id : getComponent().getItemIds()) { + options.put(id.toString(), id); + } + createMultiToggleAction("Children allowed", category, options, + setChildrenAllowedCommand, true); + + } + + /* + * COMMANDS + */ + private Command rootItemIdsCommand = new Command() { + + public void execute(TreeTable c, Integer value, Object data) { + rootItemIds = value; + updateContainer(); + } + }; + + private Command expandItemCommand = new Command() { + + public void execute(TreeTable c, Object itemId, Object data) { + c.setCollapsed(itemId, false); + } + }; + + private Command collapseItemCommand = new Command() { + + public void execute(TreeTable c, Object itemId, Object data) { + c.setCollapsed(itemId, true); + } + }; + + private Command setChildrenAllowedCommand = new Command() { + + public void execute(TreeTable c, Boolean areChildrenAllowed, + Object itemId) { + c.setChildrenAllowed(itemId, areChildrenAllowed); + } + }; + + // private Command expandListenerCommand = new + // Command() { + // public void execute(TreeTable c, Boolean value, Object data) { + // if (value) { + // c.addListener((ExpandListener) TreeTables.this); + // } else { + // c.removeListener((ExpandListener) TreeTables.this); + // } + // } + // }; + // + // private Command collapseListenerCommand = new + // Command() { + // public void execute(TreeTable c, Boolean value, Object data) { + // if (value) { + // c.addListener((CollapseListener) TreeTables.this); + // } else { + // c.removeListener((CollapseListener) TreeTables.this); + // } + // } + // }; + + public void nodeCollapse(CollapseEvent event) { + log(event.getClass().getSimpleName() + ": " + event.getItemId()); + } + + public void nodeExpand(ExpandEvent event) { + log(event.getClass().getSimpleName() + ": " + event.getItemId()); + } +} -- 2.39.5