All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.ui.components.grid.StaticSection Maven / Gradle / Ivy

There is a newer version: 8.27.3
Show newest version
/*
 * Copyright (C) 2000-2024 Vaadin Ltd
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See  for the full
 * license.
 */
package com.vaadin.ui.components.grid;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.vaadin.shared.ui.ContentMode;
import com.vaadin.shared.ui.grid.GridStaticCellType;
import com.vaadin.shared.ui.grid.SectionState;
import com.vaadin.shared.ui.grid.SectionState.CellState;
import com.vaadin.shared.ui.grid.SectionState.RowState;
import com.vaadin.ui.Component;
import com.vaadin.ui.Grid;
import com.vaadin.ui.Grid.Column;
import com.vaadin.ui.declarative.DesignAttributeHandler;
import com.vaadin.ui.declarative.DesignContext;
import com.vaadin.ui.declarative.DesignException;
import com.vaadin.ui.declarative.DesignFormatter;

/**
 * Represents the header or footer section of a Grid.
 *
 * @author Vaadin Ltd.
 *
 * @param 
 *            the type of the rows in the section
 *
 * @since 8.0
 */
public abstract class StaticSection>
        implements Serializable {

    /**
     * Abstract base class for Grid header and footer rows.
     *
     * @param 
     *            the type of the cells in the row
     */
    public abstract static class StaticRow
            implements Serializable {

        private final RowState rowState = new RowState();
        private final StaticSection section;
        private final Map cells = new LinkedHashMap<>();

        /**
         * Creates a new row belonging to the given section.
         *
         * @param section
         *            the section of the row
         */
        protected StaticRow(StaticSection section) {
            this.section = section;
        }

        /**
         * Creates and returns a new instance of the cell type.
         *
         * @return the created cell
         */
        protected abstract CELL createCell();

        /**
         * Returns the declarative tag name used for the cells in this row.
         *
         * @return the cell tag name
         */
        protected abstract String getCellTagName();

        /**
         * Adds a cell to this section, corresponding to the given user-defined
         * column id.
         *
         * @param columnId
         *            the id of the column for which to add a cell
         */
        protected void addCell(String columnId) {
            Column column = section.getGrid().getColumn(columnId);
            Objects.requireNonNull(column,
                    "No column matching given identifier");
            addCell(column);
        }

        /**
         * Adds a cell to this section for given column.
         *
         * @param column
         *            the column for which to add a cell
         */
        protected void addCell(Column column) {
            if (!section.getGrid().getColumns().contains(column)) {
                throw new IllegalArgumentException(
                        "Given column does not exist in this Grid");
            }
            internalAddCell(section.getInternalIdForColumn(column));
        }

        /**
         * Adds a cell to this section, corresponding to the given internal
         * column id.
         *
         * @param internalId
         *            the internal id of the column for which to add a cell
         */
        protected void internalAddCell(String internalId) {
            CELL cell = createCell();
            cell.setColumnId(internalId);
            cells.put(internalId, cell);
            rowState.cells.put(internalId, cell.getCellState());
        }

        /**
         * Removes the cell from this section that corresponds to the given
         * column id. If there is no such cell, does nothing.
         *
         * @param columnId
         *            the id of the column from which to remove the cell
         */
        protected void removeCell(String columnId) {
            CELL cell = cells.remove(columnId);
            if (cell != null) {
                rowState.cells.remove(columnId);
                for (Iterator> iterator = rowState.cellGroups
                        .values().iterator(); iterator.hasNext();) {
                    Set group = iterator.next();
                    group.remove(columnId);
                    if (group.size() < 2) {
                        iterator.remove();
                    }
                }
                cell.detach();
            }
        }

        /**
         * Returns the shared state of this row.
         *
         * @return the row state
         */
        protected RowState getRowState() {
            return rowState;
        }

        /**
         * Returns the cell in this section that corresponds to the given column
         * id.
         *
         * @see Column#setId(String)
         *
         * @param columnId
         *            the id of the column
         * @return the cell for the given column
         *
         * @throws IllegalArgumentException
         *             if no cell was found for the column id
         */
        public CELL getCell(String columnId) {
            Column column = section.getGrid().getColumn(columnId);
            Objects.requireNonNull(column,
                    "No column matching given identifier");
            return getCell(column);
        }

        /**
         * Returns the cell in this section that corresponds to the given
         * column.
         *
         * @param column
         *            the column
         * @return the cell for the given column
         *
         * @throws IllegalArgumentException
         *             if no cell was found for the column
         */
        public CELL getCell(Column column) {
            return internalGetCell(section.getInternalIdForColumn(column));
        }

        /**
         * Returns the custom style name for this row.
         *
         * @return the style name or null if no style name has been set
         */
        public String getStyleName() {
            return getRowState().styleName;
        }

        /**
         * Sets a custom style name for this row.
         *
         * @param styleName
         *            the style name to set or null to not use any style name
         */
        public void setStyleName(String styleName) {
            getRowState().styleName = styleName;
        }

        /**
         * Returns the cell in this section that corresponds to the given
         * internal column id.
         *
         * @param internalId
         *            the internal id of the column
         * @return the cell for the given column
         *
         * @throws IllegalArgumentException
         *             if no cell was found for the column id
         */
        protected CELL internalGetCell(String internalId) {
            CELL cell = cells.get(internalId);
            if (cell == null) {
                throw new IllegalArgumentException(
                        "No cell found for column id " + internalId);
            }
            return cell;
        }

        /**
         * Reads the declarative design from the given table row element.
         *
         * @since 7.5.0
         * @param trElement
         *            Element to read design from
         * @param designContext
         *            the design context
         * @throws DesignException
         *             if the given table row contains unexpected children
         */
        protected void readDesign(Element trElement,
                DesignContext designContext) throws DesignException {
            Elements cellElements = trElement.children();
            for (int i = 0; i < cellElements.size(); i++) {
                Element element = cellElements.get(i);
                if (!element.tagName().equals(getCellTagName())) {
                    throw new DesignException(
                            "Unexpected element in tr while expecting "
                                    + getCellTagName() + ": "
                                    + element.tagName());
                }

                int colspan = DesignAttributeHandler.readAttribute("colspan",
                        element.attributes(), 1, int.class);

                String columnIdsString = DesignAttributeHandler.readAttribute(
                        "column-ids", element.attributes(), "", String.class);
                if (columnIdsString.trim().isEmpty()) {
                    throw new DesignException(
                            "Unexpected 'column-ids' attribute value '"
                                    + columnIdsString
                                    + "'. It cannot be empty and must "
                                    + "be comma separated column identifiers");
                }
                String[] columnIds = columnIdsString.split(",");
                if (columnIds.length != colspan) {
                    throw new DesignException(
                            "Unexpected 'colspan' attribute value '" + colspan
                                    + "' whereas there is " + columnIds.length
                                    + " column identifiers specified : '"
                                    + columnIdsString + "'");
                }

                CELL cell;
                if (colspan > 1) {
                    Set columnGroup = new HashSet<>();
                    for (String columnId : columnIds) {
                        addCell(columnId);
                        // convert the public columnIds into internal columnIds
                        columnGroup.add(getCell(columnId).getColumnId());
                    }
                    cell = createCell();
                    addMergedCell(cell, columnGroup);
                } else {
                    String columnId = columnIds[0];
                    addCell(columnId);
                    cell = getCell(columnId);
                }
                cell.readDesign(element, designContext);
            }
        }

        /**
         * Writes the declarative design to the given table row element.
         *
         * @since 7.5.0
         * @param trElement
         *            Element to write design to
         * @param designContext
         *            the design context
         */
        protected void writeDesign(Element trElement,
                DesignContext designContext) {
            Set visited = new HashSet<>();
            for (Entry entry : cells.entrySet()) {
                if (visited.contains(entry.getKey())) {
                    continue;
                }
                visited.add(entry.getKey());

                Element cellElement = trElement.appendElement(getCellTagName());

                Optional>> groupCell = getRowState().cellGroups
                        .entrySet().stream().filter(groupEntry -> groupEntry
                                .getValue().contains(entry.getKey()))
                        .findFirst();
                Stream columnIds = Stream.of(entry.getKey());

                if (groupCell.isPresent()) {
                    Set orderedSet = new LinkedHashSet<>(
                            cells.keySet());
                    orderedSet.retainAll(groupCell.get().getValue());
                    columnIds = orderedSet.stream();
                    visited.addAll(orderedSet);
                    cellElement.attr("colspan", "" + orderedSet.size());
                    writeCellState(cellElement, designContext,
                            groupCell.get().getKey());
                } else {
                    writeCellState(cellElement, designContext,
                            entry.getValue().getCellState());
                }
                cellElement.attr("column-ids",
                        columnIds.map(section::getColumnByInternalId)
                                .map(Column::getId)
                                .collect(Collectors.joining(",")));
            }
        }

        /**
         *
         * Writes declarative design for the cell using its {@code state} to the
         * given table cell element.
         * 

* The method is used instead of StaticCell::writeDesign because * sometimes there is no a reference to the cell which should be written * (merged cell) but only its state is available (the cell is virtual * and is not stored). * * @param cellElement * Element to write design to * @param context * the design context * @param state * a cell state */ protected void writeCellState(Element cellElement, DesignContext context, CellState state) { switch (state.type) { case TEXT: cellElement.attr("plain-text", true); cellElement .appendText(Optional.ofNullable(state.text).orElse("")); break; case HTML: cellElement.append(Optional.ofNullable(state.html).orElse("")); break; case WIDGET: cellElement.appendChild( context.createElement((Component) state.connector)); break; } } void detach() { for (CELL cell : cells.values()) { cell.detach(); } for (CellState cellState : rowState.cellGroups.keySet()) { if (cellState.type == GridStaticCellType.WIDGET && cellState.connector != null) { ((Component) cellState.connector).setParent(null); cellState.connector = null; } } } void checkIfAlreadyMerged(String columnId) { for (Set cellGroup : getRowState().cellGroups.values()) { if (cellGroup.contains(columnId)) { throw new IllegalArgumentException( "Cell " + columnId + " is already merged"); } } if (!cells.containsKey(columnId)) { throw new IllegalArgumentException( "Cell " + columnId + " does not exist on this row"); } } void addMergedCell(CELL newCell, Set columnGroup) { rowState.cellGroups.put(newCell.getCellState(), columnGroup); } public Collection getComponents() { List components = new ArrayList<>(); cells.forEach((id, cell) -> { if (cell.getCellType() == GridStaticCellType.WIDGET) { components.add(cell.getComponent()); } }); rowState.cellGroups.forEach((cellState, columnIds) -> { if (cellState.connector != null) { components.add((Component) cellState.connector); } }); return components; } } /** * A header or footer cell. Has a simple textual caption. */ abstract static class StaticCell implements Serializable { private final CellState cellState = new CellState(); private final StaticRow row; protected StaticCell(StaticRow row) { this.row = row; } void setColumnId(String id) { cellState.columnId = id; } public String getColumnId() { return cellState.columnId; } /** * Gets the row where this cell is. * * @return row for this cell */ public StaticRow getRow() { return row; } /** * Returns the shared state of this cell. * * @return the cell state */ protected CellState getCellState() { return cellState; } /** * Sets the textual caption of this cell. * * @param text * a plain text caption, not null */ public void setText(String text) { Objects.requireNonNull(text, "text cannot be null"); removeComponentIfPresent(); cellState.text = text; cellState.type = GridStaticCellType.TEXT; row.section.markAsDirty(); } /** * Returns the textual caption of this cell. * * @return the plain text caption */ public String getText() { return cellState.text; } /** * Returns the HTML content displayed in this cell. * * @return the html * */ public String getHtml() { if (cellState.type != GridStaticCellType.HTML) { throw new IllegalStateException( "Cannot fetch HTML from a cell with type " + cellState.type); } return cellState.html; } /** * Sets the HTML content displayed in this cell. * * @param html * the html to set, not null */ public void setHtml(String html) { Objects.requireNonNull(html, "html cannot be null"); removeComponentIfPresent(); cellState.html = html; cellState.type = GridStaticCellType.HTML; row.section.markAsDirty(); } /** * Returns the component displayed in this cell. * * @return the component */ public Component getComponent() { if (cellState.type != GridStaticCellType.WIDGET) { throw new IllegalStateException( "Cannot fetch Component from a cell with type " + cellState.type); } return (Component) cellState.connector; } /** * Sets the component displayed in this cell. * * @param component * the component to set, not null */ public void setComponent(Component component) { Objects.requireNonNull(component, "component cannot be null"); removeComponentIfPresent(); component.setParent(row.section.getGrid()); cellState.connector = component; cellState.type = GridStaticCellType.WIDGET; row.section.markAsDirty(); } /** * Returns the type of content stored in this cell. * * @return cell content type */ public GridStaticCellType getCellType() { return cellState.type; } /** * Returns the custom style name for this cell. * * @return the style name or null if no style name has been set */ public String getStyleName() { return cellState.styleName; } /** * Sets a custom style name for this cell. * * @param styleName * the style name to set or null to not use any style name */ public void setStyleName(String styleName) { cellState.styleName = styleName; row.section.markAsDirty(); } /** * Reads the declarative design from the given table cell element. * * @since 7.5.0 * @param cellElement * Element to read design from * @param designContext * the design context */ protected void readDesign(Element cellElement, DesignContext designContext) { if (!cellElement.hasAttr("plain-text")) { if (!cellElement.children().isEmpty() && cellElement.child(0).tagName().contains("-")) { setComponent( designContext.readDesign(cellElement.child(0))); } else { setHtml(cellElement.html()); } } else { // text – need to unescape HTML entities setText(DesignFormatter.decodeFromTextNode(cellElement.html())); } } private void removeComponentIfPresent() { Component component = (Component) cellState.connector; if (component != null) { component.setParent(null); cellState.connector = null; } } void detach() { removeComponentIfPresent(); } /** * Gets the tooltip for the cell. *

* The tooltip is shown in the mode returned by * {@link #getDescriptionContentMode()}. * * @since 8.4 */ public String getDescription() { return cellState.description; } /** * Sets the tooltip for the cell. *

* By default, tooltips are shown as plain text. For HTML tooltips, see * {@link #setDescription(String, ContentMode)} or * {@link #setDescriptionContentMode(ContentMode)}. * * @param description * the tooltip to show when hovering the cell * @since 8.4 */ public void setDescription(String description) { cellState.description = description; } /** * Sets the tooltip for the cell to be shown with the given content * mode. * * @see ContentMode * @param description * the tooltip to show when hovering the cell * @param descriptionContentMode * the content mode to use for the tooltip (HTML or plain * text) * @since 8.4 */ public void setDescription(String description, ContentMode descriptionContentMode) { setDescription(description); setDescriptionContentMode(descriptionContentMode); } /** * Gets the content mode for the tooltip. * * @see ContentMode * @return the content mode for the tooltip * @since 8.4 */ public ContentMode getDescriptionContentMode() { return cellState.descriptionContentMode; } /** * Sets the content mode for the tooltip. * * @see ContentMode * @param descriptionContentMode * the content mode for the tooltip * @since 8.4 */ public void setDescriptionContentMode( ContentMode descriptionContentMode) { cellState.descriptionContentMode = descriptionContentMode; } } private final List rows = new ArrayList<>(); /** * Creates a new row instance. * * @return the new row */ protected abstract ROW createRow(); /** * Returns the shared state of this section. * * @param markAsDirty * {@code true} to mark the state as modified, {@code false} * otherwise * @return the section state */ protected abstract SectionState getState(boolean markAsDirty); protected abstract Grid getGrid(); protected abstract Column getColumnByInternalId(String internalId); protected abstract String getInternalIdForColumn(Column column); /** * Marks the state of this section as modified. */ protected void markAsDirty() { getState(true); } /** * Adds a new row at the given index. * * @param index * the index of the new row * @return the added row * @throws IndexOutOfBoundsException * if {@code index < 0 || index > getRowCount()} */ public ROW addRowAt(int index) { ROW row = createRow(); rows.add(index, row); getState(true).rows.add(index, row.getRowState()); getGrid().getColumns().stream().forEach(row::addCell); return row; } /** * Removes the row at the given index. * * @param index * the index of the row to remove * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getRowCount()} */ public void removeRow(int index) { ROW row = rows.remove(index); row.detach(); getState(true).rows.remove(index); } /** * Removes the given row from this section. * * @param row * the row to remove, not null * @throws IllegalArgumentException * if this section does not contain the row */ public void removeRow(Object row) { Objects.requireNonNull(row, "row cannot be null"); int index = rows.indexOf(row); if (index < 0) { throw new IllegalArgumentException( "Section does not contain the given row"); } removeRow(index); } /** * Returns the row at the given index. * * @param index * the index of the row * @return the row at the index * @throws IndexOutOfBoundsException * if {@code index < 0 || index >= getRowCount()} */ public ROW getRow(int index) { return rows.get(index); } /** * Returns the number of rows in this section. * * @return the number of rows */ public int getRowCount() { return rows.size(); } /** * Adds a cell corresponding to the given column id to this section. * * @param columnId * the id of the column for which to add a cell */ public void addColumn(String columnId) { for (ROW row : rows) { row.internalAddCell(columnId); } } /** * Removes the cell corresponding to the given column id. * * @param columnId * the id of the column whose cell to remove */ public void removeColumn(String columnId) { for (ROW row : rows) { row.removeCell(columnId); } markAsDirty(); } /** * Writes the declarative design to the given table section element. * * @param tableSectionElement * Element to write design to * @param designContext * the design context */ public void writeDesign(Element tableSectionElement, DesignContext designContext) { for (ROW row : getRows()) { Element tr = tableSectionElement.appendElement("tr"); row.writeDesign(tr, designContext); } } /** * Reads the declarative design from the given table section element. * * @since 7.5.0 * @param tableSectionElement * Element to read design from * @param designContext * the design context * @throws DesignException * if the table section contains unexpected children */ public void readDesign(Element tableSectionElement, DesignContext designContext) throws DesignException { while (getRowCount() > 0) { removeRow(0); } for (Element row : tableSectionElement.children()) { if (!row.tagName().equals("tr")) { throw new DesignException("Unexpected element in " + tableSectionElement.tagName() + ": " + row.tagName()); } addRowAt(getRowCount()).readDesign(row, designContext); } } /** * Returns an unmodifiable list of the rows in this section. * * @return the rows in this section */ protected List getRows() { return Collections.unmodifiableList(rows); } /** * Sets the visibility of this section. * * @param visible * {@code true} if visible; {@code false} if not * * @since 8.1.1 */ public void setVisible(boolean visible) { if (getState(false).visible != visible) { getState(true).visible = visible; } } /** * Gets the visibility of this section. * * @return {@code true} if visible; {@code false} if not * * @since 8.1.1 */ public boolean isVisible() { return getState(false).visible; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy