Change-Id: Ia52bbef412fc8701f6b862960dfed9c08c17ff7atags/8.0.0.alpha8
@@ -21,6 +21,7 @@ import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import com.google.gwt.dom.client.Element; | |||
@@ -235,6 +236,14 @@ public class GridConnector extends AbstractListingConnector | |||
updateHeaderCellFromState(row.getCell(getColumn(columnId)), | |||
cellState); | |||
}); | |||
for (Map.Entry<CellState, Set<String>> cellGroupEntry : rowState.cellGroups.entrySet()) { | |||
Set<String> group = cellGroupEntry.getValue(); | |||
Grid.Column<?, ?>[] columns = | |||
group.stream().map(idToColumn::get).toArray(size->new Grid.Column<?, ?>[size]); | |||
// Set state to be the same as first in group. | |||
updateHeaderCellFromState(row.join(columns), cellGroupEntry.getKey()); | |||
} | |||
} | |||
} | |||
@@ -442,9 +442,9 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
private StaticSection<?> section; | |||
/** | |||
* Map from set of spanned columns to cell meta data. | |||
* Map from cell meta data to sets of spanned columns . | |||
*/ | |||
private Map<Set<Column<?, ?>>, CELLTYPE> cellGroups = new HashMap<>(); | |||
private Map<CELLTYPE, Set<Column<?, ?>>> cellGroups = new HashMap<>(); | |||
/** | |||
* A custom style name for the row or null if none is set. | |||
@@ -461,9 +461,9 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
* null if not found | |||
*/ | |||
public CELLTYPE getCell(Column<?, ?> column) { | |||
Set<Column<?, ?>> cellGroup = getCellGroupForColumn(column); | |||
if (cellGroup != null) { | |||
return cellGroups.get(cellGroup); | |||
CELLTYPE cell = getMergedCellForColumn(column); | |||
if (cell != null) { | |||
return cell; | |||
} | |||
return cells.get(column); | |||
} | |||
@@ -500,7 +500,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
throw new IllegalArgumentException( | |||
"Given column does not exists on row " | |||
+ column); | |||
} else if (getCellGroupForColumn(column) != null) { | |||
} else if (getMergedCellForColumn(column) != null) { | |||
throw new IllegalStateException( | |||
"Column is already in a group."); | |||
} | |||
@@ -508,7 +508,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
} | |||
CELLTYPE joinedCell = createCell(); | |||
cellGroups.put(columnGroup, joinedCell); | |||
cellGroups.put(joinedCell, columnGroup); | |||
joinedCell.setSection(getSection()); | |||
calculateColspans(); | |||
@@ -549,12 +549,10 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
return join(columns); | |||
} | |||
private Set<Column<?, ?>> getCellGroupForColumn( | |||
private CELLTYPE getMergedCellForColumn( | |||
Column<?, ?> column) { | |||
for (Set<Column<?, ?>> group : cellGroups.keySet()) { | |||
if (group.contains(column)) { | |||
return group; | |||
} | |||
for (Entry<CELLTYPE, Set<Column<?, ?>>> entry : cellGroups.entrySet()) { | |||
if (entry.getValue().contains(column)) return entry.getKey(); | |||
} | |||
return null; | |||
} | |||
@@ -565,22 +563,22 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
cell.setColspan(1); | |||
} | |||
// Set colspan for grouped cells | |||
for (Set<Column<?, ?>> group : cellGroups.keySet()) { | |||
if (!checkMergedCellIsContinuous(group)) { | |||
for (Entry<CELLTYPE, Set<Column<?, ?>>> entry : cellGroups.entrySet()) { | |||
CELLTYPE mergedCell = entry.getKey(); | |||
if (!checkMergedCellIsContinuous(entry.getValue())) { | |||
// on error simply break the merged cell | |||
cellGroups.get(group).setColspan(1); | |||
mergedCell.setColspan(1); | |||
} else { | |||
int colSpan = 0; | |||
for (Column<?, ?> column : group) { | |||
for (Column<?, ?> column : entry.getValue()) { | |||
if (!column.isHidden()) { | |||
colSpan++; | |||
} | |||
} | |||
// colspan can't be 0 | |||
cellGroups.get(group).setColspan(Math.max(1, colSpan)); | |||
mergedCell.setColspan(Math.max(1, colSpan)); | |||
} | |||
} | |||
} | |||
private boolean checkMergedCellIsContinuous( | |||
@@ -2399,7 +2397,7 @@ public class Grid<T> extends ResizeComposite implements HasSelectionHandlers<T>, | |||
* An initial height that is given to new details rows before rendering the | |||
* appropriate widget that we then can be measure | |||
* | |||
* @see GridSpacerUpdater | |||
* @see Grid.GridSpacerUpdater | |||
*/ | |||
private static final double DETAILS_ROW_INITIAL_HEIGHT = 50; | |||
@@ -1540,6 +1540,25 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { | |||
public default HeaderCell getCell(Column<?, ?> column) { | |||
return getCell(column.getId()); | |||
} | |||
/** | |||
* Merges columns cells in a row | |||
* | |||
* @param cells | |||
* The cells to merge. Must be from the same row. | |||
* @return The remaining visible cell after the merge | |||
*/ | |||
HeaderCell join(Set<HeaderCell> cellsToMerge); | |||
/** | |||
* Merges columns cells in a row | |||
* | |||
* @param cells | |||
* The cells to merge. Must be from the same row. | |||
* @return The remaining visible cell after the merge | |||
*/ | |||
HeaderCell join(HeaderCell ... cellsToMerge); | |||
} | |||
/** | |||
@@ -1599,6 +1618,13 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { | |||
* @return cell content type | |||
*/ | |||
public GridStaticCellType getCellType(); | |||
/** | |||
* Gets the column id where this cell is. | |||
* | |||
* @return column id for this cell | |||
*/ | |||
public String getColumnId(); | |||
} | |||
/** | |||
@@ -1894,11 +1920,12 @@ public class Grid<T> extends AbstractListing<T> implements HasComponents { | |||
*/ | |||
public void removeColumn(Column<T, ?> column) { | |||
if (columnSet.remove(column)) { | |||
columnKeys.remove(column.getId()); | |||
removeDataGenerator(column); | |||
getHeader().removeColumn(column.getId()); | |||
String columnId = column.getId(); | |||
columnKeys.remove(columnId); | |||
column.remove(); | |||
getHeader().removeColumn(columnId); | |||
getFooter().removeColumn(columnId); | |||
getState(true).columnOrder.remove(columnId); | |||
} | |||
} | |||
@@ -17,11 +17,15 @@ package com.vaadin.ui.components.grid; | |||
import com.vaadin.ui.Grid; | |||
import java.util.Arrays; | |||
import java.util.HashSet; | |||
import java.util.Set; | |||
/** | |||
* Represents the header section of a Grid. | |||
* | |||
* | |||
* @author Vaadin Ltd. | |||
* | |||
* | |||
* @since 8.0 | |||
*/ | |||
public abstract class Header extends StaticSection<Header.Row> { | |||
@@ -64,7 +68,7 @@ public abstract class Header extends StaticSection<Header.Row> { | |||
/** | |||
* Returns whether this row is the default header row. | |||
* | |||
* | |||
* @return {@code true} if this row is the default row, {@code false} | |||
* otherwise. | |||
*/ | |||
@@ -74,13 +78,52 @@ public abstract class Header extends StaticSection<Header.Row> { | |||
/** | |||
* Sets whether this row is the default header row. | |||
* | |||
* | |||
* @param defaultHeader | |||
* {@code true} to set to default, {@code false} otherwise. | |||
*/ | |||
protected void setDefault(boolean defaultHeader) { | |||
getRowState().defaultHeader = defaultHeader; | |||
} | |||
/** | |||
* Merges columns cells in a row | |||
* | |||
* @param cellsToMerge | |||
* the cells which should be merged | |||
* @return the remaining visible cell after the merge | |||
*/ | |||
@Override | |||
public Grid.HeaderCell join(Set<Grid.HeaderCell> cellsToMerge) { | |||
for (Grid.HeaderCell cell : cellsToMerge) { | |||
checkIfAlreadyMerged(cell.getColumnId()); | |||
} | |||
// Create new cell data for the group | |||
Cell newCell = createCell(); | |||
Set<String> columnGroup = new HashSet<>(); | |||
for (Grid.HeaderCell cell : cellsToMerge) { | |||
columnGroup.add(cell.getColumnId()); | |||
} | |||
addMergedCell(newCell, columnGroup); | |||
markAsDirty(); | |||
return newCell; | |||
} | |||
/** | |||
* Merges columns cells in a row | |||
* | |||
* @param cellsToMerge | |||
* the cells which should be merged | |||
* @return the remaining visible cell after the merge | |||
*/ | |||
@Override | |||
public Grid.HeaderCell join(Grid.HeaderCell... cellsToMerge) { | |||
Set<Grid.HeaderCell> headerCells = new HashSet<>(Arrays.asList(cellsToMerge)); | |||
return join(headerCells); | |||
} | |||
} | |||
@Override | |||
@@ -99,7 +142,7 @@ public abstract class Header extends StaticSection<Header.Row> { | |||
/** | |||
* Returns the default row of this header. The default row displays column | |||
* captions and sort indicators. | |||
* | |||
* | |||
* @return the default row, or {@code null} if there is no default row | |||
*/ | |||
public Row getDefaultRow() { |
@@ -19,10 +19,12 @@ import java.io.Serializable; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.Iterator; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Set; | |||
import com.vaadin.shared.ui.grid.GridStaticCellType; | |||
import com.vaadin.shared.ui.grid.SectionState; | |||
@@ -56,7 +58,7 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> | |||
private final RowState rowState = new RowState(); | |||
private final StaticSection<?> section; | |||
private final Map<Object, CELL> cells = new LinkedHashMap<>(); | |||
private final Map<String, CELL> cells = new LinkedHashMap<>(); | |||
/** | |||
* Creates a new row belonging to the given section. | |||
@@ -102,10 +104,17 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> | |||
* @param columnId | |||
* the id of the column from which to remove the cell | |||
*/ | |||
protected void removeCell(Object columnId) { | |||
protected void removeCell(String columnId) { | |||
CELL cell = cells.remove(columnId); | |||
if (cell != null) { | |||
rowState.cells.remove(cell.getCellState()); | |||
rowState.cells.remove(columnId); | |||
for (Iterator<Set<String>> iterator = rowState.cellGroups.values().iterator(); iterator.hasNext(); ) { | |||
Set<String> group = iterator.next(); | |||
group.remove(columnId); | |||
if(group.size() < 2) { | |||
iterator.remove(); | |||
} | |||
} | |||
} | |||
} | |||
@@ -143,6 +152,23 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> | |||
cell.detach(); | |||
} | |||
} | |||
void checkIfAlreadyMerged(String columnId) { | |||
for (Set<String> 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<String> columnGroup) { | |||
rowState.cellGroups.put(newCell.getCellState(), columnGroup); | |||
} | |||
} | |||
/** | |||
@@ -161,7 +187,7 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> | |||
cellState.columnId = id; | |||
} | |||
String getColumnId() { | |||
public String getColumnId() { | |||
return cellState.columnId; | |||
} | |||
@@ -411,6 +437,7 @@ public abstract class StaticSection<ROW extends StaticSection.StaticRow<?>> | |||
for (ROW row : rows) { | |||
row.removeCell(columnId); | |||
} | |||
markAsDirty(); | |||
} | |||
/** |
@@ -20,6 +20,7 @@ import java.util.ArrayList; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import com.vaadin.shared.Connector; | |||
@@ -37,6 +38,9 @@ public class SectionState implements Serializable { | |||
/** The map from column ids to the cells in this row. */ | |||
public Map<String, CellState> cells = new HashMap<>(); | |||
/** The map from a joint cell to column id sets in this row. */ | |||
public Map<CellState, Set<String>> cellGroups = new HashMap<>(); | |||
/** | |||
* Whether this row is the default header row. Always false for footer | |||
* rows. |
@@ -1,15 +1,5 @@ | |||
package com.vaadin.tests.components.grid.basics; | |||
import java.text.DecimalFormat; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.function.Consumer; | |||
import java.util.stream.Stream; | |||
import com.vaadin.annotations.Widgetset; | |||
import com.vaadin.server.VaadinRequest; | |||
import com.vaadin.shared.Registration; | |||
@@ -36,6 +26,18 @@ import com.vaadin.ui.renderers.HtmlRenderer; | |||
import com.vaadin.ui.renderers.NumberRenderer; | |||
import com.vaadin.ui.renderers.ProgressBarRenderer; | |||
import java.text.DecimalFormat; | |||
import java.util.ArrayList; | |||
import java.util.Collections; | |||
import java.util.HashMap; | |||
import java.util.HashSet; | |||
import java.util.LinkedHashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import java.util.stream.Stream; | |||
@Widgetset("com.vaadin.DefaultWidgetSet") | |||
public class GridBasics extends AbstractTestUIWithLog { | |||
@@ -50,9 +52,9 @@ public class GridBasics extends AbstractTestUIWithLog { | |||
public static final String CELL_STYLE_GENERATOR_EMPTY = "Empty string"; | |||
public static final String CELL_STYLE_GENERATOR_NULL = "Null"; | |||
public static final String[] COLUMN_CAPTIONS = { "Column 0", "Column 1", | |||
public static final String[] COLUMN_CAPTIONS = {"Column 0", "Column 1", | |||
"Column 2", "Row Number", "Date", "HTML String", "Big Random", | |||
"Small Random" }; | |||
"Small Random"}; | |||
private final Command toggleReorderListenerCommand = new Command() { | |||
private Registration registration = null; | |||
@@ -272,6 +274,9 @@ public class GridBasics extends AbstractTestUIWithLog { | |||
selectedItem -> col | |||
.setHidden(selectedItem.isChecked())) | |||
.setCheckable(true); | |||
columnMenu | |||
.addItem("Remove", | |||
selectedItem -> grid.removeColumn(col)); | |||
} | |||
} | |||
@@ -396,7 +401,7 @@ public class GridBasics extends AbstractTestUIWithLog { | |||
} | |||
private <T> void addGridMethodMenu(MenuItem parent, String name, T value, | |||
Consumer<T> method) { | |||
Consumer<T> method) { | |||
parent.addItem(name, menuItem -> method.accept(value)); | |||
} | |||
@@ -437,6 +442,25 @@ public class GridBasics extends AbstractTestUIWithLog { | |||
headerMenu.addItem("Set no default row", menuItem -> { | |||
grid.setDefaultHeaderRow(null); | |||
}); | |||
headerMenu.addItem("Merge Header Cells [0,0..1]", menuItem -> { | |||
mergeHeaderСells(0, "0+1", 0, 1); | |||
}); | |||
headerMenu.addItem("Merge Header Cells [1,1..3]", menuItem -> { | |||
mergeHeaderСells(1, "1+2+3", 1, 2, 3); | |||
}); | |||
headerMenu.addItem("Merge Header Cells [0,6..7]", menuItem -> { | |||
mergeHeaderСells(0, "6+7", 6, 7); | |||
}); | |||
} | |||
private void mergeHeaderСells(int rowIndex, String jointCellText, int... columnIndexes) { | |||
HeaderRow headerRow = grid.getHeaderRow(rowIndex); | |||
List<Column<DataObject, ?>> columns = grid.getColumns(); | |||
Set<Grid.HeaderCell> toMerge = new HashSet<>(); | |||
for (int columnIndex : columnIndexes) { | |||
toMerge.add(headerRow.getCell(columns.get(columnIndex))); | |||
} | |||
headerRow.join(toMerge).setText(jointCellText); | |||
} | |||
private void createFooterMenu(MenuItem footerMenu) { |
@@ -260,6 +260,61 @@ public class GridHeaderFooterTest extends GridBasicsTest { | |||
assertEquals("Column 1", getColumnHidingToggle(1).getText()); | |||
} | |||
@Test | |||
public void testHeaderMergedRemoveColumn() { | |||
selectMenuPath("Component", "Header", "Append header row"); | |||
selectMenuPath("Component", "Header", "Merge Header Cells [0,0..1]"); | |||
GridCellElement c00 = getGridElement().getHeaderCell(0, 0); | |||
assertEquals("0+1", c00.getText()); | |||
assertEquals("Colspan of cell [0,0]", "2", c00.getAttribute("colspan")); | |||
selectMenuPath("Component", "Columns", "Column 1", "Remove"); | |||
selectMenuPath("Component", "Header", "Append header row"); | |||
c00 = getGridElement().getHeaderCell(0, 0); | |||
assertEquals("Column 0", c00.getText()); | |||
assertEquals("Colspan of cell [0,0]", "1", c00.getAttribute("colspan")); | |||
GridCellElement c01 = getGridElement().getHeaderCell(0, 1); | |||
assertEquals("Column 2", c01.getText()); | |||
GridCellElement c10 = getGridElement().getHeaderCell(1, 0); | |||
assertEquals("Header cell 0", c10.getText()); | |||
GridCellElement c11 = getGridElement().getHeaderCell(1, 1); | |||
assertEquals("Header cell 2", c11.getText()); | |||
GridCellElement c20 = getGridElement().getHeaderCell(2, 0); | |||
assertEquals("Header cell 0", c20.getText()); | |||
GridCellElement c21 = getGridElement().getHeaderCell(2, 1); | |||
assertEquals("Header cell 1", c21.getText()); | |||
} | |||
@Test | |||
public void testHeaderMerge() { | |||
selectMenuPath("Component", "Header", "Append header row"); | |||
selectMenuPath("Component", "Header", "Merge Header Cells [0,0..1]"); | |||
selectMenuPath("Component", "Header", "Merge Header Cells [1,1..3]"); | |||
selectMenuPath("Component", "Header", "Merge Header Cells [0,6..7]"); | |||
GridCellElement mergedCell1 = getGridElement().getHeaderCell(0, 0); | |||
assertEquals("0+1", mergedCell1.getText()); | |||
assertEquals("Colspan, cell [0,0]", "2", mergedCell1.getAttribute("colspan")); | |||
GridCellElement mergedCell2 = getGridElement().getHeaderCell(1, 1); | |||
assertEquals("1+2+3", mergedCell2.getText()); | |||
assertEquals("Colspan of cell [1,1]", "3", mergedCell2.getAttribute("colspan")); | |||
GridCellElement mergedCell3 = getGridElement().getHeaderCell(0, 6); | |||
assertEquals("6+7", mergedCell3.getText()); | |||
assertEquals("Colspan of cell [0,6]", "2", mergedCell3.getAttribute("colspan")); | |||
} | |||
private void toggleColumnHidable(int index) { | |||
selectMenuPath("Component", "Columns", "Column " + index, "Hidable"); | |||
} |