| /******************************************************************************* |
| * Copyright (c) 2012, 2020 Original authors and others. |
| * |
| * This program and the accompanying materials are made |
| * available under the terms of the Eclipse Public License 2.0 |
| * which is available at https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * Original authors and others - initial API and implementation |
| * Dirk Fauth <dirk.fauth@googlemail.com> - Added scaling |
| ******************************************************************************/ |
| package org.eclipse.nebula.widgets.nattable.group; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.List; |
| import java.util.Properties; |
| |
| import org.eclipse.nebula.widgets.nattable.NatTable; |
| import org.eclipse.nebula.widgets.nattable.grid.GridRegion; |
| import org.eclipse.nebula.widgets.nattable.group.config.DefaultRowGroupHeaderLayerConfiguration; |
| import org.eclipse.nebula.widgets.nattable.group.model.IRowGroup; |
| import org.eclipse.nebula.widgets.nattable.group.model.IRowGroupModel; |
| import org.eclipse.nebula.widgets.nattable.layer.AbstractLayerTransform; |
| import org.eclipse.nebula.widgets.nattable.layer.DataLayer; |
| import org.eclipse.nebula.widgets.nattable.layer.ILayer; |
| import org.eclipse.nebula.widgets.nattable.layer.LabelStack; |
| import org.eclipse.nebula.widgets.nattable.layer.SizeConfig; |
| import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell; |
| import org.eclipse.nebula.widgets.nattable.layer.cell.LayerCell; |
| import org.eclipse.nebula.widgets.nattable.layer.cell.TransformedLayerCell; |
| import org.eclipse.nebula.widgets.nattable.layer.command.ConfigureScalingCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralRefreshEvent; |
| import org.eclipse.nebula.widgets.nattable.selection.SelectRowGroupCommandHandler; |
| import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer; |
| import org.eclipse.nebula.widgets.nattable.style.DisplayMode; |
| import org.eclipse.nebula.widgets.nattable.style.SelectionStyleLabels; |
| |
| /** |
| * Adds the Row grouping functionality to the row headers. Also persists the |
| * state of the row groups when {@link NatTable#saveState(String, Properties)} |
| * is invoked. |
| * |
| * Internally uses the {@link IRowGroupModel} to track the row groups. |
| */ |
| public class RowGroupHeaderLayer<T> extends AbstractLayerTransform { |
| |
| private final SizeConfig columnWidthConfig = new SizeConfig(DataLayer.DEFAULT_COLUMN_WIDTH); |
| private final IRowGroupModel<T> model; |
| private final SelectionLayer selectionLayer; |
| private final ILayer rowHeaderLayer; |
| |
| public RowGroupHeaderLayer( |
| ILayer rowHeaderLayer, |
| SelectionLayer selectionLayer, |
| IRowGroupModel<T> rowGroupModel) { |
| |
| this(rowHeaderLayer, selectionLayer, rowGroupModel, true); |
| } |
| |
| public RowGroupHeaderLayer( |
| ILayer rowHeaderLayer, |
| SelectionLayer selectionLayer, |
| IRowGroupModel<T> rowGroupModel, |
| boolean useDefaultConfiguration) { |
| |
| super(rowHeaderLayer); |
| this.rowHeaderLayer = rowHeaderLayer; |
| this.selectionLayer = selectionLayer; |
| this.model = rowGroupModel; |
| |
| registerCommandHandlers(); |
| |
| if (useDefaultConfiguration) { |
| addConfiguration(new DefaultRowGroupHeaderLayerConfiguration()); |
| } |
| } |
| |
| public IRowGroupModel<T> getModel() { |
| return this.model; |
| } |
| |
| // Persistence |
| |
| @Override |
| public void loadState(String prefix, Properties properties) { |
| super.loadState(prefix, properties); |
| this.model.loadState(prefix, properties); |
| fireLayerEvent(new RowStructuralRefreshEvent(this)); |
| } |
| |
| @Override |
| public void saveState(String prefix, Properties properties) { |
| super.saveState(prefix, properties); |
| this.model.saveState(prefix, properties); |
| } |
| |
| // Configuration |
| |
| @Override |
| protected void registerCommandHandlers() { |
| registerCommandHandler(new SelectRowGroupCommandHandler<>(this.model, this.selectionLayer, this)); |
| registerCommandHandler(new ConfigureScalingCommandHandler(this.columnWidthConfig, null)); |
| } |
| |
| // Horizontal features |
| |
| // Columns |
| |
| @Override |
| public int getColumnCount() { |
| return this.rowHeaderLayer.getColumnCount() + 1; |
| } |
| |
| @Override |
| public int getPreferredColumnCount() { |
| return this.rowHeaderLayer.getPreferredColumnCount() + 1; |
| } |
| |
| @Override |
| public int getColumnIndexByPosition(int columnPosition) { |
| if (columnPosition == 0) { |
| return columnPosition; |
| } else { |
| return this.rowHeaderLayer.getColumnIndexByPosition(columnPosition - 1); |
| } |
| } |
| |
| @Override |
| public int localToUnderlyingColumnPosition(int localColumnPosition) { |
| if (localColumnPosition == 0) { |
| return localColumnPosition; |
| } |
| return localColumnPosition - 1; |
| } |
| |
| // Width |
| |
| @Override |
| public int getWidth() { |
| return this.columnWidthConfig.getAggregateSize(1) |
| + this.rowHeaderLayer.getWidth(); |
| } |
| |
| @Override |
| public int getPreferredWidth() { |
| return this.columnWidthConfig.getAggregateSize(1) |
| + this.rowHeaderLayer.getPreferredWidth(); |
| } |
| |
| @Override |
| public int getColumnWidthByPosition(int columnPosition) { |
| if (columnPosition == 0) { |
| return this.columnWidthConfig.getSize(columnPosition); |
| } else { |
| return this.rowHeaderLayer.getColumnWidthByPosition(columnPosition - 1); |
| } |
| } |
| |
| public void setColumnWidth(int columnWidth) { |
| this.columnWidthConfig.setSize(0, columnWidth); |
| } |
| |
| // Column resize |
| |
| @Override |
| public boolean isColumnPositionResizable(int columnPosition) { |
| if (columnPosition == 0) { |
| return this.columnWidthConfig.isPositionResizable(columnPosition); |
| } else { |
| return this.rowHeaderLayer.isRowPositionResizable(columnPosition - 1); |
| } |
| } |
| |
| // X |
| |
| @Override |
| public int getColumnPositionByX(int x) { |
| int col0Width = getColumnWidthByPosition(0); |
| if (x < col0Width) { |
| return 0; |
| } else { |
| return 1 + this.rowHeaderLayer.getColumnPositionByX(x - col0Width); |
| } |
| } |
| |
| @Override |
| public int getStartXOfColumnPosition(int columnPosition) { |
| if (columnPosition == 0) { |
| return this.columnWidthConfig.getAggregateSize(columnPosition); |
| } else { |
| return getColumnWidthByPosition(0) |
| + this.rowHeaderLayer.getStartXOfColumnPosition(columnPosition - 1); |
| } |
| } |
| |
| // Cell features |
| |
| /** |
| * If a cell belongs to a column group: column position - set to the start |
| * position of the group span - set to the width/size of the row group |
| * |
| * NOTE: gc.setClip() is used in the CompositeLayerPainter to ensure that |
| * partially visible Column group header cells are rendered properly. |
| */ |
| @Override |
| public ILayerCell getCellByPosition(int columnPosition, int rowPosition) { |
| int bodyRowIndex = getRowIndexByPosition(rowPosition); |
| |
| // Row group header cell |
| if (RowGroupUtils.isPartOfAGroup(this.model, bodyRowIndex)) { |
| if (columnPosition == 0) { |
| return new LayerCell( |
| this, |
| columnPosition, |
| getStartPositionOfGroup(rowPosition), |
| columnPosition, |
| rowPosition, |
| 1, |
| getRowSpan(rowPosition)); |
| } else { |
| return new LayerCell(this, columnPosition, rowPosition); |
| } |
| } else { |
| // render row header w/ columnspan = 2 |
| // as in this case we ask the row header layer for the cell position |
| // and the row header layer asks his data provider for the column |
| // count which should always return 1, we ask for row position 0 |
| ILayerCell cell = this.rowHeaderLayer.getCellByPosition(0, rowPosition); |
| if (cell != null) { |
| cell = new TransformedLayerCell(cell) { |
| @Override |
| public ILayer getLayer() { |
| return RowGroupHeaderLayer.this; |
| } |
| |
| @Override |
| public int getColumnSpan() { |
| return 2; |
| } |
| }; |
| } |
| return cell; |
| } |
| } |
| |
| /** |
| * Calculates the span of a cell in a Row Group. Takes into account |
| * collapsing and hidden rows in the group. |
| * |
| * @param rowPosition |
| * position of any row belonging to the group |
| */ |
| protected int getRowSpan(int rowPosition) { |
| int rowIndex = getRowIndexByPosition(rowPosition); |
| |
| // Get the row and the group from our cache and model. |
| IRowGroup<T> rowGroup = RowGroupUtils.getRowGroupForRowIndex(this.model, rowIndex); |
| |
| int sizeOfGroup = RowGroupUtils.sizeOfGroup(this.model, rowIndex); |
| |
| if (RowGroupUtils.isCollapsed(this.model, rowGroup)) { |
| int sizeOfStaticRows = rowGroup.getOwnStaticMemberRows().size(); |
| if (sizeOfStaticRows == 0) { |
| return 1; |
| } else { |
| sizeOfGroup = sizeOfStaticRows; |
| } |
| } |
| |
| int startPositionOfGroup = getStartPositionOfGroup(rowPosition); |
| int endPositionOfGroup = startPositionOfGroup + sizeOfGroup; |
| List<Integer> rowIndexesInGroup = RowGroupUtils.getRowIndexesInGroup(this.model, rowIndex); |
| |
| for (int i = startPositionOfGroup; i < endPositionOfGroup; i++) { |
| int index = getRowIndexByPosition(i); |
| if (!rowIndexesInGroup.contains(Integer.valueOf(index))) { |
| sizeOfGroup--; |
| } |
| } |
| |
| return sizeOfGroup; |
| } |
| |
| /** |
| * Figures out the start position of the group. |
| * |
| * @param selectionLayerColumnPosition |
| * of any column belonging to the group |
| * @return first position of the column group |
| */ |
| private int getStartPositionOfGroup(int rowPosition) { |
| int bodyRowIndex = getRowIndexByPosition(rowPosition); |
| int leastPossibleStartPositionOfGroup = |
| Math.max(0, (rowPosition - RowGroupUtils.sizeOfGroup(this.model, bodyRowIndex))); |
| int i = 0; |
| for (i = leastPossibleStartPositionOfGroup; i < rowPosition; i++) { |
| if (RowGroupUtils.isInTheSameGroup( |
| getRowIndexByPosition(i), |
| bodyRowIndex, |
| this.model)) { |
| break; |
| } |
| } |
| return i; |
| } |
| |
| @Override |
| public String getDisplayModeByPosition(int columnPosition, int rowPosition) { |
| int rowIndex = getRowIndexByPosition(rowPosition); |
| if (columnPosition == 0 |
| && RowGroupUtils.isPartOfAGroup(this.model, rowIndex)) { |
| return DisplayMode.NORMAL; |
| } else { |
| return this.rowHeaderLayer.getDisplayModeByPosition(columnPosition - 1, rowPosition); |
| } |
| } |
| |
| @Override |
| public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) { |
| int rowIndex = getRowIndexByPosition(rowPosition); |
| if (columnPosition == 0 && RowGroupUtils.isPartOfAGroup(this.model, rowIndex)) { |
| LabelStack stack = new LabelStack(); |
| if (getConfigLabelAccumulator() != null) { |
| getConfigLabelAccumulator().accumulateConfigLabels(stack, columnPosition, rowPosition); |
| } |
| stack.addLabel(GridRegion.ROW_GROUP_HEADER); |
| |
| IRowGroup<T> group = RowGroupUtils.getRowGroupForRowIndex(this.model, rowIndex); |
| if (RowGroupUtils.isCollapsed(this.model, group)) { |
| stack.addLabelOnTop(DefaultRowGroupHeaderLayerConfiguration.GROUP_COLLAPSED_CONFIG_TYPE); |
| } else { |
| stack.addLabelOnTop(DefaultRowGroupHeaderLayerConfiguration.GROUP_EXPANDED_CONFIG_TYPE); |
| } |
| |
| List<Integer> selectedRowIndexes = |
| convertToRowIndexes(this.selectionLayer.getFullySelectedRowPositions()); |
| if (selectedRowIndexes.contains(rowIndex)) { |
| stack.addLabelOnTop(SelectionStyleLabels.ROW_FULLY_SELECTED_STYLE); |
| } |
| |
| return stack; |
| } else { |
| return this.rowHeaderLayer.getConfigLabelsByPosition(columnPosition - 1, rowPosition); |
| } |
| } |
| |
| private List<Integer> convertToRowIndexes(final int[] rowPositions) { |
| final List<Integer> rowIndexes = new ArrayList<>(rowPositions.length); |
| for (final Integer rowPosition : rowPositions) { |
| rowIndexes.add(this.selectionLayer.getRowIndexByPosition(rowPosition)); |
| } |
| return rowIndexes; |
| } |
| |
| @Override |
| public Object getDataValueByPosition(int columnPosition, int rowPosition) { |
| int rowIndex = getRowIndexByPosition(rowPosition); |
| if (columnPosition == 0 |
| && RowGroupUtils.isPartOfAGroup(this.model, rowIndex)) { |
| return RowGroupUtils.getRowGroupNameForIndex(this.model, rowIndex); |
| } else { |
| return this.rowHeaderLayer.getDataValueByPosition(columnPosition - 1, rowPosition); |
| } |
| } |
| |
| @Override |
| public LabelStack getRegionLabelsByXY(int x, int y) { |
| int rowIndex = getRowIndexByPosition(getRowPositionByY(y)); |
| if (RowGroupUtils.isPartOfAGroup(this.model, rowIndex) |
| && x < getColumnWidthByPosition(0)) { |
| return new LabelStack(GridRegion.ROW_GROUP_HEADER); |
| } else { |
| return this.rowHeaderLayer.getRegionLabelsByXY(x - getColumnWidthByPosition(0), y); |
| } |
| } |
| |
| public void collapseRowGroupByIndex(int rowIndex) { |
| RowGroupUtils.getRowGroupForRowIndex(this.model, rowIndex).collapse(); |
| } |
| |
| public void clearAllGroups() { |
| this.model.clear(); |
| } |
| |
| @Override |
| public Collection<String> getProvidedLabels() { |
| Collection<String> labels = super.getProvidedLabels(); |
| |
| labels.add(GridRegion.ROW_GROUP_HEADER); |
| labels.add(DefaultRowGroupHeaderLayerConfiguration.GROUP_COLLAPSED_CONFIG_TYPE); |
| labels.add(DefaultRowGroupHeaderLayerConfiguration.GROUP_EXPANDED_CONFIG_TYPE); |
| |
| return labels; |
| } |
| } |