/* * Copyright 2000-2022 Vaadin Ltd. * * 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.vaadin.ui; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.stream.Stream; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import com.vaadin.data.HasHierarchicalDataProvider; import com.vaadin.data.HasValue; import com.vaadin.data.PropertyDefinition; import com.vaadin.data.PropertySet; import com.vaadin.data.TreeData; import com.vaadin.data.ValueProvider; import com.vaadin.data.provider.DataProvider; import com.vaadin.data.provider.HierarchicalDataCommunicator; import com.vaadin.data.provider.HierarchicalDataProvider; import com.vaadin.data.provider.HierarchicalQuery; import com.vaadin.data.provider.TreeDataProvider; import com.vaadin.event.CollapseEvent; import com.vaadin.event.CollapseEvent.CollapseListener; import com.vaadin.event.ExpandEvent; import com.vaadin.event.ExpandEvent.ExpandListener; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.grid.ScrollDestination; import com.vaadin.shared.ui.treegrid.FocusParentRpc; import com.vaadin.shared.ui.treegrid.FocusRpc; import com.vaadin.shared.ui.treegrid.NodeCollapseRpc; import com.vaadin.shared.ui.treegrid.TreeGridClientRpc; import com.vaadin.shared.ui.treegrid.TreeGridState; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; import com.vaadin.ui.declarative.DesignFormatter; /** * A grid component for displaying hierarchical tabular data. * * Visual hierarchy depth positioning of rows is done via styles, see * _treegrid.scss from Valo theme. * * @author Vaadin Ltd * @since 8.1 * * @param * the grid bean type */ public class TreeGrid extends Grid implements HasHierarchicalDataProvider { /** * Creates a new {@code TreeGrid} without support for creating columns based * on property names. Use an alternative constructor, such as * {@link TreeGrid#TreeGrid(Class)}, to create a {@code TreeGrid} that * automatically sets up columns based on the type of presented data. */ public TreeGrid() { this(new HierarchicalDataCommunicator<>()); } /** * Creates a new {@code TreeGrid} that uses reflection based on the provided * bean type to automatically set up an initial set of columns. All columns * will be configured using the same {@link Object#toString()} renderer that * is used by {@link #addColumn(ValueProvider)}. * * @param beanType * the bean type to use, not {@code null} */ public TreeGrid(Class beanType) { super(beanType, new HierarchicalDataCommunicator<>()); registerTreeGridRpc(); } /** * Creates a new {@code TreeGrid} using the given * {@code HierarchicalDataProvider}, without support for creating columns * based on property names. Use an alternative constructor, such as * {@link TreeGrid#TreeGrid(Class)}, to create a {@code TreeGrid} that * automatically sets up columns based on the type of presented data. * * @param dataProvider * the data provider, not {@code null} */ public TreeGrid(HierarchicalDataProvider dataProvider) { this(); setDataProvider(dataProvider); } /** * Creates a {@code TreeGrid} using a custom {@link PropertySet} * implementation and custom data communicator. *

* Property set is used for configuring the initial columns and resolving * property names for {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. * * @param propertySet * the property set implementation to use, not {@code null} * @param dataCommunicator * the data communicator to use, not {@code null} */ protected TreeGrid(PropertySet propertySet, HierarchicalDataCommunicator dataCommunicator) { super(propertySet, dataCommunicator); registerTreeGridRpc(); } /** * Creates a new TreeGrid with the given data communicator and without * support for creating columns based on property names. * * @param dataCommunicator * the custom data communicator to set */ protected TreeGrid(HierarchicalDataCommunicator dataCommunicator) { this(new PropertySet() { @Override public Stream> getProperties() { // No columns configured by default return Stream.empty(); } @Override public Optional> getProperty(String name) { throw new IllegalStateException( "A TreeGrid created without a bean type class literal or a custom property set" + " doesn't support finding properties by name."); } }, dataCommunicator); } /** * Creates a {@code TreeGrid} using a custom {@link PropertySet} * implementation for creating a default set of columns and for resolving * property names with {@link #addColumn(String)} and * {@link Column#setEditorComponent(HasValue)}. *

* This functionality is provided as static method instead of as a public * constructor in order to make it possible to use a custom property set * without creating a subclass while still leaving the public constructors * focused on the common use cases. * * @see TreeGrid#TreeGrid() * @see TreeGrid#TreeGrid(Class) * * @param * the tree grid bean type * @param propertySet * the property set implementation to use, not {@code null} * @return a new tree grid using the provided property set, not {@code null} */ public static TreeGrid withPropertySet( PropertySet propertySet) { return new TreeGrid(propertySet, new HierarchicalDataCommunicator<>()); } private void registerTreeGridRpc() { registerRpc((NodeCollapseRpc) (rowKey, rowIndex, collapse, userOriginated) -> { T item = getDataCommunicator().getKeyMapper().get(rowKey); if (collapse && getDataCommunicator().isExpanded(item)) { getDataCommunicator().collapse(item, rowIndex); fireCollapseEvent( getDataCommunicator().getKeyMapper().get(rowKey), userOriginated); } else if (!collapse && !getDataCommunicator().isExpanded(item)) { getDataCommunicator().expand(item, rowIndex); fireExpandEvent( getDataCommunicator().getKeyMapper().get(rowKey), userOriginated); } }); registerRpc((FocusParentRpc) (rowKey, cellIndex) -> { Integer parentIndex = getDataCommunicator().getParentIndex( getDataCommunicator().getKeyMapper().get(rowKey)); if (parentIndex != null) { getRpcProxy(FocusRpc.class).focusCell(parentIndex, cellIndex); } }); } /** * This method is inherited from Grid but should never be called directly * with a TreeGrid. */ @Override @Deprecated public void scrollTo(int row) throws IllegalArgumentException { super.scrollTo(row); } /** * This method is inherited from Grid but should never be called directly * with a TreeGrid. */ @Deprecated @Override public void scrollTo(int row, ScrollDestination destination) { super.scrollTo(row, destination); } /** * Adds an ExpandListener to this TreeGrid. * * @see ExpandEvent * * @param listener * the listener to add * @return a registration for the listener */ public Registration addExpandListener(ExpandListener listener) { return addListener(ExpandEvent.class, listener, ExpandListener.EXPAND_METHOD); } /** * Adds a CollapseListener to this TreeGrid. * * @see CollapseEvent * * @param listener * the listener to add * @return a registration for the listener */ public Registration addCollapseListener(CollapseListener listener) { return addListener(CollapseEvent.class, listener, CollapseListener.COLLAPSE_METHOD); } @Override public void setDataProvider(DataProvider dataProvider) { if (!(dataProvider instanceof HierarchicalDataProvider)) { throw new IllegalArgumentException( "TreeGrid only accepts hierarchical data providers"); } getRpcProxy(TreeGridClientRpc.class).clearPendingExpands(); super.setDataProvider(dataProvider); } /** * Get the currently set hierarchy column. * * @return the currently set hierarchy column, or {@code null} if no column * has been explicitly set */ public Column getHierarchyColumn() { return getColumnByInternalId(getState(false).hierarchyColumnId); } /** * Set the column that displays the hierarchy of this grid's data. By * default the hierarchy will be displayed in the first column. *

* Setting a hierarchy column by calling this method also sets the column to * be visible and not hidable. *

* Note: Changing the Renderer of the hierarchy column is * not supported. * * @param column * the column to use for displaying hierarchy */ public void setHierarchyColumn(Column column) { Objects.requireNonNull(column, "column may not be null"); if (!getColumns().contains(column)) { throw new IllegalArgumentException( "Given column is not a column of this TreeGrid"); } column.setHidden(false); column.setHidable(false); getState().hierarchyColumnId = getInternalIdForColumn(column); } /** * Set the column that displays the hierarchy of this grid's data. By * default the hierarchy will be displayed in the first column. *

* Setting a hierarchy column by calling this method also sets the column to * be visible and not hidable. *

* Note: Changing the Renderer of the hierarchy column is * not supported. * * @see Column#setId(String) * * @param id * id of the column to use for displaying hierarchy */ public void setHierarchyColumn(String id) { Objects.requireNonNull(id, "id may not be null"); if (getColumn(id) == null) { throw new IllegalArgumentException("No column found for given id"); } setHierarchyColumn(getColumn(id)); } /** * Sets the item collapse allowed provider for this TreeGrid. The provider * should return {@code true} for any item that the user can collapse. *

* Note: This callback will be accessed often when sending * data to the client. The callback should not do any costly operations. *

* This method is a shortcut to method with the same name in * {@link HierarchicalDataCommunicator}. * * @param provider * the item collapse allowed provider, not {@code null} * * @see HierarchicalDataCommunicator#setItemCollapseAllowedProvider(ItemCollapseAllowedProvider) */ public void setItemCollapseAllowedProvider( ItemCollapseAllowedProvider provider) { getDataCommunicator().setItemCollapseAllowedProvider(provider); } /** * Expands the given items. *

* If an item is currently expanded, does nothing. If an item does not have * any children, does nothing. * * @param items * the items to expand */ @SuppressWarnings("unchecked") public void expand(T... items) { expand(Arrays.asList(items)); } /** * Expands the given items. *

* If an item is currently expanded, does nothing. If an item does not have * any children, does nothing. * * @param items * the items to expand */ public void expand(Collection items) { HierarchicalDataCommunicator communicator = getDataCommunicator(); items.forEach(item -> { if (!communicator.isExpanded(item) && communicator.hasChildren(item)) { communicator.expand(item); fireExpandEvent(item, false); } }); } /** * Expands the given items and their children recursively until the given * depth. *

* {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code expandRecursively(items, 0)} expands only * the given items while {@code expandRecursively(items, 2)} expands the * given items as well as their children and grandchildren. *

* This method will not fire events for expanded nodes. * * @param items * the items to expand recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void expandRecursively(Collection items, int depth) { expandRecursively(items.stream(), depth); } /** * Expands the given items and their children recursively until the given * depth. *

* {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code expandRecursively(items, 0)} expands only * the given items while {@code expandRecursively(items, 2)} expands the * given items as well as their children and grandchildren. *

* This method will not fire events for expanded nodes. * * @param items * the items to expand recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void expandRecursively(Stream items, int depth) { if (depth < 0) { return; } HierarchicalDataCommunicator communicator = getDataCommunicator(); items.forEach(item -> { if (communicator.hasChildren(item)) { communicator.expand(item, false); expandRecursively( getDataProvider().fetchChildren( new HierarchicalQuery<>(null, item)), depth - 1); } }); getDataProvider().refreshAll(); } /** * Collapse the given items. *

* For items that are already collapsed, does nothing. * * @param items * the collection of items to collapse */ @SuppressWarnings("unchecked") public void collapse(T... items) { collapse(Arrays.asList(items)); } /** * Collapse the given items. *

* For items that are already collapsed, does nothing. * * @param items * the collection of items to collapse */ public void collapse(Collection items) { HierarchicalDataCommunicator communicator = getDataCommunicator(); items.forEach(item -> { if (communicator.isExpanded(item)) { communicator.collapse(item); fireCollapseEvent(item, false); } }); } /** * Collapse the given items and their children recursively until the given * depth. *

* {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code collapseRecursively(items, 0)} collapses * only the given items while {@code collapseRecursively(items, 2)} * collapses the given items as well as their children and grandchildren. *

* This method will not fire events for collapsed nodes. * * @param items * the items to collapse recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void collapseRecursively(Collection items, int depth) { collapseRecursively(items.stream(), depth); } /** * Collapse the given items and their children recursively until the given * depth. *

* {@code depth} describes the maximum distance between a given item and its * descendant, meaning that {@code collapseRecursively(items, 0)} collapses * only the given items while {@code collapseRecursively(items, 2)} * collapses the given items as well as their children and grandchildren. *

* This method will not fire events for collapsed nodes. * * @param items * the items to collapse recursively * @param depth * the maximum depth of recursion * @since 8.4 */ public void collapseRecursively(Stream items, int depth) { if (depth < 0) { return; } HierarchicalDataCommunicator communicator = getDataCommunicator(); items.forEach(item -> { if (communicator.hasChildren(item)) { collapseRecursively( getDataProvider().fetchChildren( new HierarchicalQuery<>(null, item)), depth - 1); communicator.collapse(item, false); } }); getDataProvider().refreshAll(); } /** * Returns whether a given item is expanded or collapsed. * * @param item * the item to check * @return true if the item is expanded, false if collapsed */ public boolean isExpanded(T item) { return getDataCommunicator().isExpanded(item); } @Override protected TreeGridState getState() { return (TreeGridState) super.getState(); } @Override protected TreeGridState getState(boolean markAsDirty) { return (TreeGridState) super.getState(markAsDirty); } @Override public HierarchicalDataCommunicator getDataCommunicator() { return (HierarchicalDataCommunicator) super.getDataCommunicator(); } @Override public HierarchicalDataProvider getDataProvider() { if (!(super.getDataProvider() instanceof HierarchicalDataProvider)) { return null; } return (HierarchicalDataProvider) super.getDataProvider(); } @Override protected void doReadDesign(Element design, DesignContext context) { super.doReadDesign(design, context); Attributes attrs = design.attributes(); if (attrs.hasKey("hierarchy-column")) { setHierarchyColumn(DesignAttributeHandler .readAttribute("hierarchy-column", attrs, String.class)); } } @Override protected void readData(Element body, List> providers) { getSelectionModel().deselectAll(); List selectedItems = new ArrayList<>(); TreeData data = new TreeData(); for (Element row : body.children()) { T item = deserializeDeclarativeRepresentation(row.attr("item")); T parent = null; if (row.hasAttr("parent")) { parent = deserializeDeclarativeRepresentation( row.attr("parent")); } data.addItem(parent, item); if (row.hasAttr("selected")) { selectedItems.add(item); } Elements cells = row.children(); int i = 0; for (Element cell : cells) { providers.get(i).addValue(item, cell.html()); i++; } } setDataProvider(new TreeDataProvider<>(data)); selectedItems.forEach(getSelectionModel()::select); } @Override protected void doWriteDesign(Element design, DesignContext designContext) { super.doWriteDesign(design, designContext); if (getColumnByInternalId(getState(false).hierarchyColumnId) != null) { String hierarchyColumn = getColumnByInternalId( getState(false).hierarchyColumnId).getId(); DesignAttributeHandler.writeAttribute("hierarchy-column", design.attributes(), hierarchyColumn, null, String.class, designContext); } } @Override protected void writeData(Element body, DesignContext designContext) { getDataProvider().fetch(new HierarchicalQuery<>(null, null)) .forEach(item -> writeRow(body, item, null, designContext)); } private void writeRow(Element container, T item, T parent, DesignContext context) { Element tableRow = container.appendElement("tr"); tableRow.attr("item", serializeDeclarativeRepresentation(item)); if (parent != null) { tableRow.attr("parent", serializeDeclarativeRepresentation(parent)); } if (getSelectionModel().isSelected(item)) { tableRow.attr("selected", true); } for (Column column : getColumns()) { Object value = column.getValueProvider().apply(item); tableRow.appendElement("td") .append(Optional.ofNullable(value).map(Object::toString) .map(DesignFormatter::encodeForTextNode) .orElse("")); } getDataProvider().fetch(new HierarchicalQuery<>(null, item)).forEach( childItem -> writeRow(container, childItem, item, context)); } /** * Emit an expand event. * * @param item * the item that was expanded * @param userOriginated * whether the expand was triggered by a user interaction or the * server */ private void fireExpandEvent(T item, boolean userOriginated) { fireEvent(new ExpandEvent<>(this, item, userOriginated)); } /** * Emit a collapse event. * * @param item * the item that was collapsed * @param userOriginated * whether the collapse was triggered by a user interaction or * the server */ private void fireCollapseEvent(T item, boolean userOriginated) { fireEvent(new CollapseEvent<>(this, item, userOriginated)); } /** * Gets the item collapse allowed provider. * * @return the item collapse allowed provider */ public ItemCollapseAllowedProvider getItemCollapseAllowedProvider() { return getDataCommunicator().getItemCollapseAllowedProvider(); } }