/* * 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.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.jsoup.nodes.Attributes; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import com.vaadin.event.LayoutEvents.LayoutClickEvent; import com.vaadin.event.LayoutEvents.LayoutClickListener; import com.vaadin.event.LayoutEvents.LayoutClickNotifier; import com.vaadin.shared.Connector; import com.vaadin.shared.EventId; import com.vaadin.shared.MouseEventDetails; import com.vaadin.shared.Registration; import com.vaadin.shared.ui.MarginInfo; import com.vaadin.shared.ui.gridlayout.GridLayoutServerRpc; import com.vaadin.shared.ui.gridlayout.GridLayoutState; import com.vaadin.shared.ui.gridlayout.GridLayoutState.ChildComponentData; import com.vaadin.ui.declarative.DesignAttributeHandler; import com.vaadin.ui.declarative.DesignContext; /** * A layout where the components are laid out on a grid using cell coordinates. * *

* The GridLayout also maintains a cursor for adding components in * left-to-right, top-to-bottom order. *

* *

* Each component in a GridLayout uses a defined * {@link GridLayout.Area area} (column1,row1,column2,row2) from the grid. The * components may not overlap with the existing components - if you try to do so * you will get an {@link OverlapsException}. Adding a component with cursor * automatically extends the grid by increasing the grid height. *

* *

* The grid coordinates, which are specified by a row and column index, always * start from 0 for the topmost row and the leftmost column. *

* * @author Vaadin Ltd. * @since 3.0 */ @SuppressWarnings("serial") public class GridLayout extends AbstractLayout implements Layout.AlignmentHandler, Layout.SpacingHandler, Layout.MarginHandler, LayoutClickNotifier { private GridLayoutServerRpc rpc = (MouseEventDetails mouseDetails, Connector clickedConnector) -> fireEvent( LayoutClickEvent.createEvent(GridLayout.this, mouseDetails, clickedConnector)); /** * Cursor X position: this is where the next component with unspecified x,y * is inserted */ private int cursorX = 0; /** * Cursor Y position: this is where the next component with unspecified x,y * is inserted */ private int cursorY = 0; private final LinkedList components = new LinkedList<>(); private Map columnExpandRatio = new HashMap<>(); private Map rowExpandRatio = new HashMap<>(); private Alignment defaultComponentAlignment = Alignment.TOP_LEFT; /** * Constructor for a grid of given size (number of columns and rows). * * The grid may grow or shrink later. Grid grows automatically if you add * components outside its area. * * @param columns * Number of columns in the grid. * @param rows * Number of rows in the grid. */ public GridLayout(int columns, int rows) { setColumns(columns); setRows(rows); registerRpc(rpc); } /** * Constructs an empty (1x1) grid layout that is extended as needed. */ public GridLayout() { this(1, 1); } /** * Constructs a GridLayout of given size (number of columns and rows) and * adds the given components in order to the grid. * * @see #addComponents(Component...) * * @param columns * Number of columns in the grid. * @param rows * Number of rows in the grid. * @param children * Components to add to the grid. */ public GridLayout(int columns, int rows, Component... children) { this(columns, rows); addComponents(children); } @Override protected GridLayoutState getState() { return (GridLayoutState) super.getState(); } @Override protected GridLayoutState getState(boolean markAsDirty) { return (GridLayoutState) super.getState(markAsDirty); } /** *

* Adds a component to the grid in the specified area. The area is defined * by specifying the upper left corner (column1, row1) and the lower right * corner (column2, row2) of the area. The coordinates are zero-based. *

* *

* If the area overlaps with any of the existing components already present * in the grid, the operation will fail and an {@link OverlapsException} is * thrown. *

* * @param component * the component to be added, not null. * @param column1 * the column of the upper left corner of the area c * is supposed to occupy. The leftmost column has index 0. * @param row1 * the row of the upper left corner of the area c is * supposed to occupy. The topmost row has index 0. * @param column2 * the column of the lower right corner of the area * c is supposed to occupy. * @param row2 * the row of the lower right corner of the area c * is supposed to occupy. * @throws OverlapsException * if the new component overlaps with any of the components * already in the grid. * @throws OutOfBoundsException * if the cells are outside the grid area. */ public void addComponent(Component component, int column1, int row1, int column2, int row2) throws OverlapsException, OutOfBoundsException { if (component == null) { throw new NullPointerException("Component must not be null"); } // Checks that the component does not already exist in the container if (components.contains(component)) { throw new IllegalArgumentException( "Component is already in the container"); } // Creates the area final Area area = new Area(component, column1, row1, column2, row2); // Checks the validity of the coordinates if (column2 < column1 || row2 < row1) { throw new IllegalArgumentException(String.format( "Illegal coordinates for the component: %s!<=%s, %s!<=%s", column1, column2, row1, row2)); } if (column1 < 0 || row1 < 0 || column2 >= getColumns() || row2 >= getRows()) { throw new OutOfBoundsException(area); } // Checks that newItem does not overlap with existing items checkExistingOverlaps(area); // Inserts the component to right place at the list // Respect top-down, left-right ordering // component.setParent(this); final Map childDataMap = getState().childData; int index = 0; boolean done = false; for (Component c : components) { final ChildComponentData existingArea = childDataMap.get(c); if ((existingArea.row1 >= row1 && existingArea.column1 > column1) || existingArea.row1 > row1) { components.add(index, component); done = true; break; } index++; } if (!done) { components.addLast(component); } childDataMap.put(component, area.childData); // Attempt to add to super try { super.addComponent(component); } catch (IllegalArgumentException e) { childDataMap.remove(component); components.remove(component); throw e; } // update cursor position, if it's within this area; use first position // outside this area, even if it's occupied if (cursorX >= column1 && cursorX <= column2 && cursorY >= row1 && cursorY <= row2) { // cursor within area cursorX = column2 + 1; // one right of area if (cursorX >= getColumns()) { // overflowed columns cursorX = 0; // first col // move one row down, or one row under the area cursorY = (column1 == 0 ? row2 : row1) + 1; } else { cursorY = row1; } } } /** * Tests if the given area overlaps with any of the items already on the * grid. * * @param area * the Area to be checked for overlapping. * @throws OverlapsException * if area overlaps with any existing area. */ private void checkExistingOverlaps(Area area) throws OverlapsException { for (Entry entry : getState().childData .entrySet()) { if (componentsOverlap(entry.getValue(), area.childData)) { // Component not added, overlaps with existing component throw new OverlapsException( new Area(entry.getValue(), (Component) entry.getKey())); } } } /** * Adds the component to the grid in cells column1,row1 (NortWest corner of * the area.) End coordinates (SouthEast corner of the area) are the same as * column1,row1. The coordinates are zero-based. Component width and height * is 1. * * @param component * the component to be added, not null. * @param column * the column index, starting from 0. * @param row * the row index, starting from 0. * @throws OverlapsException * if the new component overlaps with any of the components * already in the grid. * @throws OutOfBoundsException * if the cell is outside the grid area. */ public void addComponent(Component component, int column, int row) throws OverlapsException, OutOfBoundsException { this.addComponent(component, column, row, column, row); } /** * Forces the next component to be added at the beginning of the next line. * *

* Sets the cursor column to 0 and increments the cursor row by one. *

* *

* By calling this function you can ensure that no more components are added * right of the previous component. *

* * @see #space() */ public void newLine() { cursorX = 0; cursorY++; } /** * Moves the cursor forward by one. If the cursor goes out of the right grid * border, it is moved to the first column of the next row. * * @see #newLine() */ public void space() { cursorX++; if (cursorX >= getColumns()) { cursorX = 0; cursorY++; } } /** * Adds the component into this container to the cursor position. If the * cursor position is already occupied, the cursor is moved forwards to find * free position. If the cursor goes out from the bottom of the grid, the * grid is automatically extended. * * @param component * the component to be added, not null. */ @Override public void addComponent(Component component) { if (component == null) { throw new IllegalArgumentException("Component must not be null"); } // Finds first available place from the grid Area area; boolean done = false; while (!done) { try { area = new Area(component, cursorX, cursorY, cursorX, cursorY); checkExistingOverlaps(area); done = true; } catch (final OverlapsException e) { space(); } } // Extends the grid if needed if (cursorX >= getColumns()) { setColumns(cursorX + 1); } if (cursorY >= getRows()) { setRows(cursorY + 1); } addComponent(component, cursorX, cursorY); } /** * Removes the specified component from the layout. * * @param component * the component to be removed. */ @Override public void removeComponent(Component component) { // Check that the component is contained in the container if (component == null || !components.contains(component)) { return; } getState().childData.remove(component); components.remove(component); super.removeComponent(component); } /** * Removes the component specified by its cell coordinates. * * @param column * the component's column, starting from 0. * @param row * the component's row, starting from 0. */ public void removeComponent(int column, int row) { // Finds the area for (final Component component : components) { final ChildComponentData childData = getState().childData .get(component); if (childData.column1 == column && childData.row1 == row) { removeComponent(component); return; } } } /** * Gets an Iterator for the components contained in the layout. By using the * Iterator it is possible to step through the contents of the layout. * * @return the Iterator of the components inside the layout. */ @Override public Iterator iterator() { return Collections.unmodifiableCollection(components).iterator(); } /** * Gets the number of components contained in the layout. Consistent with * the iterator returned by {@link #getComponentIterator()}. * * @return the number of contained components */ @Override public int getComponentCount() { return components.size(); } @Override public void beforeClientResponse(boolean initial) { super.beforeClientResponse(initial); getState().colExpand = new float[getColumns()]; float colSum = getExpandRatioSum(columnExpandRatio); if (colSum == 0) { // no cols have been expanded for (int i = 0; i < getColumns(); i++) { getState().colExpand[i] = 1f; } } else { for (int i = 0; i < getColumns(); i++) { getState().colExpand[i] = getColumnExpandRatio(i); } } getState().rowExpand = new float[getRows()]; float rowSum = getExpandRatioSum(rowExpandRatio); if (rowSum == 0) { // no rows have been expanded for (int i = 0; i < getRows(); i++) { getState().rowExpand[i] = 1f; } } else { for (int i = 0; i < getRows(); i++) { getState().rowExpand[i] = getRowExpandRatio(i); } } } private float getExpandRatioSum(Map ratioMap) { float sum = 0; for (Float expandRatio : ratioMap.values()) { sum += expandRatio; } return sum; } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.AlignmentHandler#getComponentAlignment(com * .vaadin.ui.Component) */ @Override public Alignment getComponentAlignment(Component childComponent) { ChildComponentData childComponentData = getState(false).childData .get(childComponent); if (childComponentData == null) { throw new IllegalArgumentException( "The given component is not a child of this layout"); } else { return new Alignment(childComponentData.alignment); } } /** * Defines a rectangular area of cells in a GridLayout. * *

* Also maintains a reference to the component contained in the area. *

* *

* The area is specified by the cell coordinates of its upper left corner * (column1,row1) and lower right corner (column2,row2). As otherwise with * GridLayout, the column and row coordinates start from zero. *

* * @author Vaadin Ltd. * @since 3.0 */ public class Area implements Serializable { private final ChildComponentData childData; private final Component component; /** *

* Construct a new area on a grid. *

* * @param component * the component connected to the area. * @param column1 * The column of the upper left corner cell of the area. The * leftmost column has index 0. * @param row1 * The row of the upper left corner cell of the area. The * topmost row has index 0. * @param column2 * The column of the lower right corner cell of the area. The * leftmost column has index 0. * @param row2 * The row of the lower right corner cell of the area. The * topmost row has index 0. */ public Area(Component component, int column1, int row1, int column2, int row2) { this.component = component; childData = new ChildComponentData(); childData.alignment = getDefaultComponentAlignment().getBitMask(); childData.column1 = column1; childData.row1 = row1; childData.column2 = column2; childData.row2 = row2; } public Area(ChildComponentData childData, Component component) { this.childData = childData; this.component = component; } /** * Tests if this Area overlaps with another Area. * * @param other * the other Area that is to be tested for overlap with this * area * @return true if other area overlaps with * this on, false if it does not. */ public boolean overlaps(Area other) { return componentsOverlap(childData, other.childData); } /** * Gets the component connected to the area. * * @return the Component. */ public Component getComponent() { return component; } /** * Gets the column of the top-left corner cell. * * @return the column of the top-left corner cell. */ public int getColumn1() { return childData.column1; } /** * Gets the column of the bottom-right corner cell. * * @return the column of the bottom-right corner cell. */ public int getColumn2() { return childData.column2; } /** * Gets the row of the top-left corner cell. * * @return the row of the top-left corner cell. */ public int getRow1() { return childData.row1; } /** * Gets the row of the bottom-right corner cell. * * @return the row of the bottom-right corner cell. */ public int getRow2() { return childData.row2; } @Override public String toString() { return String.format("Area{%s,%s - %s,%s}", getColumn1(), getRow1(), getColumn2(), getRow2()); } } private static boolean componentsOverlap(ChildComponentData a, ChildComponentData b) { return a.column1 <= b.column2 && a.row1 <= b.row2 && a.column2 >= b.column1 && a.row2 >= b.row1; } /** * Gridlayout does not support laying components on top of each other. An * OverlapsException is thrown when a component already exists * (even partly) at the same space on a grid with the new component. * * @author Vaadin Ltd. * @since 3.0 */ public class OverlapsException extends RuntimeException { private final Area existingArea; /** * Constructs an OverlapsException. * * @param existingArea * the existing area that needs overlapping */ public OverlapsException(Area existingArea) { this.existingArea = existingArea; } @Override public String getMessage() { StringBuilder sb = new StringBuilder(); Component component = existingArea.getComponent(); sb.append(component); sb.append("( type = "); sb.append(component.getClass().getName()); if (component.getCaption() != null) { sb.append(", caption = \""); sb.append(component.getCaption()); sb.append("\""); } sb.append(')'); sb.append(" is already added to "); sb.append(existingArea.childData.column1); sb.append(','); sb.append(existingArea.childData.column1); sb.append(','); sb.append(existingArea.childData.row1); sb.append(','); sb.append(existingArea.childData.row2); sb.append("(column1, column2, row1, row2)."); return sb.toString(); } /** * Gets the area . * * @return the existing area. */ public Area getArea() { return existingArea; } } /** * An Exception object which is thrown when an area exceeds the * bounds of the grid. * * @author Vaadin Ltd. * @since 3.0 */ public class OutOfBoundsException extends RuntimeException { private final Area areaOutOfBounds; /** * Constructs an OoutOfBoundsException with the specified * detail message. * * @param areaOutOfBounds * the area that exceeds the bounds of the grid */ public OutOfBoundsException(Area areaOutOfBounds) { super(String.format("%s, layout dimension: %sx%s", areaOutOfBounds, getColumns(), getRows())); this.areaOutOfBounds = areaOutOfBounds; } /** * Gets the area that is out of bounds. * * @return the area out of Bound. */ public Area getArea() { return areaOutOfBounds; } } /** * Sets the number of columns in the grid. The column count can not be * reduced if there are any areas that would be outside of the shrunk grid. * * @param columns * the new number of columns in the grid. */ public void setColumns(int columns) { // The the param if (columns < 1) { throw new IllegalArgumentException( "The number of columns and rows in the grid must be at least 1"); } // In case of no change if (getColumns() == columns) { return; } // Checks for overlaps if (getColumns() > columns) { for (Entry entry : getState().childData .entrySet()) { if (entry.getValue().column2 >= columns) { throw new OutOfBoundsException(new Area(entry.getValue(), (Component) entry.getKey())); } } } // Forget expands for removed columns if (columns < getColumns()) { for (int i = columns; i < getColumns(); i++) { columnExpandRatio.remove(i); getState().explicitColRatios.remove(i); } } getState().columns = columns; } /** * Get the number of columns in the grid. * * @return the number of columns in the grid. */ public int getColumns() { return getState(false).columns; } /** * Sets the number of rows in the grid. The number of rows can not be * reduced if there are any areas that would be outside of the shrunk grid. * * @param rows * the new number of rows in the grid. */ public void setRows(int rows) { // The the param if (rows < 1) { throw new IllegalArgumentException( "The number of columns and rows in the grid must be at least 1"); } // In case of no change if (getRows() == rows) { return; } // Checks for overlaps if (getRows() > rows) { for (Entry entry : getState().childData .entrySet()) { if (entry.getValue().row2 >= rows) { throw new OutOfBoundsException(new Area(entry.getValue(), (Component) entry.getKey())); } } } // Forget expands for removed rows if (rows < getRows()) { for (int i = rows; i < getRows(); i++) { rowExpandRatio.remove(i); getState().explicitRowRatios.remove(i); } } getState().rows = rows; } /** * Get the number of rows in the grid. * * @return the number of rows in the grid. */ public int getRows() { return getState(false).rows; } /** * Gets the current x-position (column) of the cursor. * *

* The cursor position points the position for the next component that is * added without specifying its coordinates (grid cell). When the cursor * position is occupied, the next component will be added to first free * position after the cursor. *

* * @return the grid column the cursor is on, starting from 0. */ public int getCursorX() { return cursorX; } /** * Sets the current cursor x-position. This is usually handled automatically * by GridLayout. * * @param cursorX * current cursor x-position */ public void setCursorX(int cursorX) { this.cursorX = cursorX; } /** * Gets the current y-position (row) of the cursor. * *

* The cursor position points the position for the next component that is * added without specifying its coordinates (grid cell). When the cursor * position is occupied, the next component will be added to the first free * position after the cursor. *

* * @return the grid row the Cursor is on. */ public int getCursorY() { return cursorY; } /** * Sets the current y-coordinate (row) of the cursor. This is usually * handled automatically by GridLayout. * * @param cursorY * the row number, starting from 0 for the topmost row. */ public void setCursorY(int cursorY) { this.cursorY = cursorY; } /* Documented in superclass */ @Override public void replaceComponent(Component oldComponent, Component newComponent) { // Gets the locations ChildComponentData oldLocation = getState().childData.get(oldComponent); ChildComponentData newLocation = getState().childData.get(newComponent); if (oldLocation == null) { addComponent(newComponent); } else if (newLocation == null) { removeComponent(oldComponent); addComponent(newComponent, oldLocation.column1, oldLocation.row1, oldLocation.column2, oldLocation.row2); } else { int oldAlignment = oldLocation.alignment; oldLocation.alignment = newLocation.alignment; newLocation.alignment = oldAlignment; getState().childData.put(newComponent, oldLocation); getState().childData.put(oldComponent, newLocation); } } /* * Removes all components from this container. * * @see com.vaadin.ui.ComponentContainer#removeAllComponents() */ @Override public void removeAllComponents() { super.removeAllComponents(); cursorX = 0; cursorY = 0; } @Override public void setComponentAlignment(Component childComponent, Alignment alignment) { ChildComponentData childComponentData = getState().childData .get(childComponent); if (childComponentData == null) { throw new IllegalArgumentException( "Component must be added to layout before using setComponentAlignment()"); } else { if (alignment == null) { childComponentData.alignment = GridLayoutState.ALIGNMENT_DEFAULT .getBitMask(); } else { childComponentData.alignment = alignment.getBitMask(); } } } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.SpacingHandler#setSpacing(boolean) */ @Override public void setSpacing(boolean spacing) { getState().spacing = spacing; } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.SpacingHandler#isSpacing() */ @Override public boolean isSpacing() { return getState(false).spacing; } /** * Inserts an empty row at the specified position in the grid. * * @param row * Index of the row before which the new row will be inserted. * The leftmost row has index 0. */ public void insertRow(int row) { if (row > getRows()) { throw new IllegalArgumentException("Cannot insert row at " + row + " in a gridlayout with height " + getRows()); } for (ChildComponentData existingArea : getState().childData.values()) { // Areas ending below the row needs to be moved down or stretched if (existingArea.row2 >= row) { existingArea.row2++; // Stretch areas that span over the selected row if (existingArea.row1 >= row) { existingArea.row1++; } } } if (cursorY >= row) { cursorY++; } setRows(getRows() + 1); markAsDirty(); } /** * Removes a row and all the components in the row. * *

* Components which span over several rows are removed if the selected row * is on the first row of such a component. *

* *

* If the last row is removed then all remaining components will be removed * and the grid will be reduced to one row. The cursor will be moved to the * upper left cell of the grid. *

* * @param row * Index of the row to remove. The leftmost row has index 0. */ public void removeRow(int row) { if (row >= getRows()) { throw new IllegalArgumentException("Cannot delete row " + row + " from a gridlayout with height " + getRows()); } // Remove all components in row for (int col = 0; col < getColumns(); col++) { removeComponent(col, row); } // Shrink or remove areas in the selected row for (ChildComponentData existingArea : getState().childData.values()) { if (existingArea.row2 >= row) { existingArea.row2--; if (existingArea.row1 > row) { existingArea.row1--; } } } if (getRows() == 1) { /* * Removing the last row means that the dimensions of the Grid * layout will be truncated to 1 empty row and the cursor is moved * to the first cell */ cursorX = 0; cursorY = 0; } else { setRows(getRows() - 1); if (cursorY > row) { cursorY--; } } markAsDirty(); } /** * Sets the expand ratio of given column. * *

* The expand ratio defines how excess space is distributed among columns. * Excess space means space that is left over from components that are not * sized relatively. By default, the excess space is distributed evenly. *

* *

* Note, that width of this GridLayout needs to be defined (fixed or * relative, as opposed to undefined height) for this method to have any * effect. *

* Note that checking for relative width for the child components is done on * the server so you cannot set a child component to have undefined width on * the server and set it to 100% in CSS. You must set it to * 100% on the server. * * @see #setWidth(float, Unit) * * @param columnIndex * The column index, starting from 0 for the leftmost row. * @param ratio * the expand ratio */ public void setColumnExpandRatio(int columnIndex, float ratio) { columnExpandRatio.put(columnIndex, ratio); getState().explicitColRatios.add(columnIndex); markAsDirty(); } /** * Returns the expand ratio of given column. * * @see #setColumnExpandRatio(int, float) * * @param columnIndex * The column index, starting from 0 for the leftmost row. * @return the expand ratio, 0.0f by default */ public float getColumnExpandRatio(int columnIndex) { Float r = columnExpandRatio.get(columnIndex); return r == null ? 0 : r.floatValue(); } /** * Sets the expand ratio of given row. * *

* Expand ratio defines how excess space is distributed among rows. Excess * space means the space left over from components that are not sized * relatively. By default, the excess space is distributed evenly. *

* *

* Note, that height of this GridLayout needs to be defined (fixed or * relative, as opposed to undefined height) for this method to have any * effect. *

* Note that checking for relative height for the child components is done * on the server so you cannot set a child component to have undefined * height on the server and set it to 100% in CSS. You must set * it to 100% on the server. * * @see #setHeight(float, Unit) * * @param rowIndex * The row index, starting from 0 for the topmost row. * @param ratio * the expand ratio */ public void setRowExpandRatio(int rowIndex, float ratio) { rowExpandRatio.put(rowIndex, ratio); getState().explicitRowRatios.add(rowIndex); markAsDirty(); } /** * Returns the expand ratio of given row. * * @see #setRowExpandRatio(int, float) * * @param rowIndex * The row index, starting from 0 for the topmost row. * @return the expand ratio, 0.0f by default */ public float getRowExpandRatio(int rowIndex) { Float r = rowExpandRatio.get(rowIndex); return r == null ? 0 : r.floatValue(); } /** * Gets the Component at given index. * * @param x * The column index, starting from 0 for the leftmost column. * @param y * The row index, starting from 0 for the topmost row. * @return Component in given cell or null if empty */ public Component getComponent(int x, int y) { for (Entry entry : getState( false).childData.entrySet()) { ChildComponentData childData = entry.getValue(); if (childData.column1 <= x && x <= childData.column2 && childData.row1 <= y && y <= childData.row2) { return (Component) entry.getKey(); } } return null; } /** * Returns information about the area where given component is laid in the * GridLayout. * * @param component * the component whose area information is requested. * @return an Area object that contains information how component is laid in * the grid */ public Area getComponentArea(Component component) { ChildComponentData childComponentData = getState(false).childData .get(component); if (childComponentData == null) { return null; } else { return new Area(childComponentData, component); } } @Override public Registration addLayoutClickListener(LayoutClickListener listener) { return addListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER, LayoutClickEvent.class, listener, LayoutClickListener.clickMethod); } @Override @Deprecated public void removeLayoutClickListener(LayoutClickListener listener) { removeListener(EventId.LAYOUT_CLICK_EVENT_IDENTIFIER, LayoutClickEvent.class, listener); } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.MarginHandler#setMargin(boolean) */ @Override public void setMargin(boolean enabled) { setMargin(new MarginInfo(enabled)); } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.MarginHandler#setMargin(com.vaadin.shared.ui. * MarginInfo ) */ @Override @SuppressWarnings("deprecation") public void setMargin(MarginInfo marginInfo) { getState().marginsBitmask = marginInfo.getBitMask(); } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.MarginHandler#getMargin() */ @Override @SuppressWarnings("deprecation") public MarginInfo getMargin() { return new MarginInfo(getState(false).marginsBitmask); } /* * (non-Javadoc) * * @see com.vaadin.ui.Layout.AlignmentHandler#getDefaultComponentAlignment() */ @Override public Alignment getDefaultComponentAlignment() { return defaultComponentAlignment; } /* * (non-Javadoc) * * @see * com.vaadin.ui.Layout.AlignmentHandler#setDefaultComponentAlignment(com * .vaadin.ui.Alignment) */ @Override public void setDefaultComponentAlignment(Alignment defaultAlignment) { defaultComponentAlignment = defaultAlignment; } /** * Sets whether empty rows and columns should be considered as non-existent * when rendering or not. If this is set to true then the spacing between * multiple empty columns (or rows) will be collapsed. * * The default behavior is to consider all rows and columns as visible * * NOTE that this must be set before the initial rendering takes place. * Updating this on the fly is not supported. * * @since 7.3 * @param hideEmptyRowsAndColumns * true to hide empty rows and columns, false to leave them as-is */ public void setHideEmptyRowsAndColumns(boolean hideEmptyRowsAndColumns) { getState().hideEmptyRowsAndColumns = hideEmptyRowsAndColumns; } /** * Checks whether whether empty rows and columns should be considered as * non-existent when rendering or not. * * @see #setHideEmptyRowsAndColumns(boolean) * @since 7.3 * @return true if empty rows and columns are hidden, false otherwise */ public boolean isHideEmptyRowsAndColumns() { return getState(false).hideEmptyRowsAndColumns; } /** * {@inheritDoc} *

* After reading the design, cursorY is set to point to a row outside of the * GridLayout area. CursorX is reset to 0. */ @Override public void readDesign(Element design, DesignContext designContext) { super.readDesign(design, designContext); setMargin(readMargin(design, getMargin(), designContext)); if (design.childNodeSize() > 0) { // Touch content only if there is some content specified. This is // needed to be able to use extended GridLayouts which add // components in the constructor (e.g. Designs based on GridLayout). readChildComponents(design.children(), designContext); } // Set cursor position explicitly setCursorY(getRows()); setCursorX(0); } private void readChildComponents(Elements childElements, DesignContext designContext) { List rowElements = new ArrayList<>(); List> rows = new ArrayList<>(); // Prepare a 2D map for reading column contents for (Element e : childElements) { if (e.tagName().equalsIgnoreCase("row")) { rowElements.add(e); rows.add(new HashMap<>()); } } setRows(Math.max(rows.size(), 1)); Map alignments = new HashMap<>(); List columnExpandRatios = new ArrayList<>(); for (int row = 0; row < rowElements.size(); ++row) { Element rowElement = rowElements.get(row); // Row Expand if (rowElement.hasAttr("expand")) { float expand = DesignAttributeHandler.readAttribute("expand", rowElement.attributes(), float.class); setRowExpandRatio(row, expand); } Elements cols = rowElement.children(); // Amount of skipped columns due to spanned components int skippedColumns = 0; for (int column = 0; column < cols.size(); ++column) { while (rows.get(row).containsKey(column + skippedColumns)) { // Skip any spanned components skippedColumns++; } Element col = cols.get(column); Component child = null; if (!col.children().isEmpty()) { Element childElement = col.child(0); child = designContext.readDesign(childElement); alignments.put(child, DesignAttributeHandler .readAlignment(childElement.attributes())); // TODO: Currently ignoring any extra children. // Needs Error handling? } // Else: Empty placeholder. No child component. // Handle rowspan and colspan for this child component Attributes attr = col.attributes(); int colspan = DesignAttributeHandler.readAttribute("colspan", attr, 1, int.class); int rowspan = DesignAttributeHandler.readAttribute("rowspan", attr, 1, int.class); for (int rowIndex = row; rowIndex < row + rowspan; ++rowIndex) { for (int colIndex = column; colIndex < column + colspan; ++colIndex) { if (rowIndex == rows.size()) { // Rowspan with not enough rows. Fix by adding rows. rows.add(new HashMap<>()); } rows.get(rowIndex).put(colIndex + skippedColumns, child); } } // Read column expand ratios if handling the first row. if (row == 0) { if (col.hasAttr("expand")) { for (String expand : col.attr("expand").split(",")) { columnExpandRatios.add(Float.parseFloat(expand)); } } else { for (int c = 0; c < colspan; ++c) { columnExpandRatios.add(0f); } } } skippedColumns += (colspan - 1); } } // Calculate highest column count and set columns int colMax = 0; for (Map cols : rows) { if (colMax < cols.size()) { colMax = cols.size(); } } setColumns(Math.max(colMax, 1)); for (int i = 0; i < columnExpandRatios.size(); ++i) { setColumnExpandRatio(i, columnExpandRatios.get(i)); } // Reiterate through the 2D map and add components to GridLayout Set visited = new HashSet<>(); // Ignore any missing components visited.add(null); for (int i = 0; i < rows.size(); ++i) { Map row = rows.get(i); for (int j = 0; j < colMax; ++j) { Component child = row.get(j); if (visited.contains(child)) { // Empty location or already handled child continue; } visited.add(child); // Figure out col and rowspan from 2D map int colspan = 0; while (j + colspan + 1 < row.size() && row.get(j + colspan + 1) == child) { ++colspan; } int rowspan = 0; while (i + rowspan + 1 < rows.size() && rows.get(i + rowspan + 1).get(j) == child) { ++rowspan; } // Add component with area addComponent(child, j, i, j + colspan, i + rowspan); setComponentAlignment(child, alignments.get(child)); } } } @Override public void writeDesign(Element design, DesignContext designContext) { super.writeDesign(design, designContext); GridLayout def = designContext.getDefaultInstance(this); writeMargin(design, getMargin(), def.getMargin(), designContext); if (!designContext.shouldWriteChildren(this, def)) { return; } if (components.isEmpty()) { writeEmptyColsAndRows(design); return; } final Map childData = getState().childData; // Make a 2D map of component areas. Component[][] componentMap = new Component[getState().rows][getState().columns]; final Component dummyComponent = new Label(""); for (Component component : components) { ChildComponentData coords = childData.get(component); for (int row = coords.row1; row <= coords.row2; ++row) { for (int col = coords.column1; col <= coords.column2; ++col) { componentMap[row][col] = component; } } } // Go through the map and write only needed column tags Set visited = new HashSet<>(); // Skip the dummy placeholder visited.add(dummyComponent); for (int i = 0; i < componentMap.length; ++i) { Element row = design.appendElement("row"); // Row Expand DesignAttributeHandler.writeAttribute("expand", row.attributes(), getRowExpandRatio(i), 0.0f, float.class, designContext); int colspan = 1; Element col; for (int j = 0; j < componentMap[i].length; ++j) { Component child = componentMap[i][j]; if (child != null) { if (visited.contains(child)) { // Child has already been written in the design continue; } visited.add(child); Element childElement = designContext.createElement(child); col = row.appendElement("column"); // Write child data into design ChildComponentData coords = childData.get(child); Alignment alignment = getComponentAlignment(child); DesignAttributeHandler.writeAlignment(childElement, alignment); col.appendChild(childElement); if (coords.row1 != coords.row2) { col.attr("rowspan", "" + (1 + coords.row2 - coords.row1)); } colspan = 1 + coords.column2 - coords.column1; if (colspan > 1) { col.attr("colspan", "" + colspan); } } else { boolean hasExpands = false; if (i == 0 && lastComponentOnRow(componentMap[i], j, visited)) { // A column with expand and no content in the end of // first row needs to be present. for (int c = j; c < componentMap[i].length; ++c) { if (getColumnExpandRatio(c) > 0) { hasExpands = true; } } } if (lastComponentOnRow(componentMap[i], j, visited) && !hasExpands) { continue; } // Empty placeholder tag. col = row.appendElement("column"); // Use colspan to make placeholders more pleasant while (j + colspan < componentMap[i].length && componentMap[i][j + colspan] == child) { ++colspan; } int rowspan = getRowSpan(componentMap, i, j, colspan, child); if (colspan > 1) { col.attr("colspan", "" + colspan); } if (rowspan > 1) { col.attr("rowspan", "" + rowspan); } for (int x = 0; x < rowspan; ++x) { for (int y = 0; y < colspan; ++y) { // Mark handled columns componentMap[i + x][j + y] = dummyComponent; } } } // Column expands if (i == 0) { // Only do expands on first row String expands = ""; boolean expandRatios = false; for (int c = 0; c < colspan; ++c) { float colExpand = getColumnExpandRatio(j + c); if (colExpand > 0) { expandRatios = true; } expands += (c > 0 ? "," : "") + colExpand; } if (expandRatios) { col.attr("expand", expands); } } j += colspan - 1; } } } /** * Fills in the design with rows and empty columns. This needs to be done * for empty {@link GridLayout}, because there's no other way to serialize * info about number of columns and rows if there are absolutely no * components in the {@link GridLayout} * * @param design */ private void writeEmptyColsAndRows(Element design) { int rowCount = getState(false).rows; int colCount = getState(false).columns; // only write cols and rows tags if size is not 1x1 if (rowCount == 1 && colCount == 1) { return; } for (int i = 0; i < rowCount; i++) { Element row = design.appendElement("row"); for (int j = 0; j < colCount; j++) { row.appendElement("column"); } } } private int getRowSpan(Component[][] compMap, int i, int j, int colspan, Component child) { int rowspan = 1; while (i + rowspan < compMap.length && compMap[i + rowspan][j] == child) { for (int k = 0; k < colspan; ++k) { if (compMap[i + rowspan][j + k] != child) { return rowspan; } } rowspan++; } return rowspan; } private boolean lastComponentOnRow(Component[] componentArray, int j, Set visited) { while ((++j) < componentArray.length) { Component child = componentArray[j]; if (child != null && !visited.contains(child)) { return false; } } return true; } @Override protected Collection getCustomAttributes() { Collection result = super.getCustomAttributes(); result.add("cursor-x"); result.add("cursor-y"); result.add("rows"); result.add("columns"); result.add("margin"); result.add("margin-left"); result.add("margin-right"); result.add("margin-top"); result.add("margin-bottom"); return result; } }