/*******************************************************************************
* Copyright (c) 2012, 2016 Original authors and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* 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.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.ColumnGroupModel.ColumnGroup;
import org.eclipse.nebula.widgets.nattable.group.command.ColumnGroupsCommandHandler;
import org.eclipse.nebula.widgets.nattable.group.config.DefaultColumnGroupHeaderLayerConfiguration;
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.IConfigLabelProvider;
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.ColumnStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.RowStructuralRefreshEvent;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
/**
* Adds the Column grouping functionality to the column headers. Also persists
* the state of the column groups when
* {@link NatTable#saveState(String, Properties)} is invoked.
*
* Internally uses the {@link ColumnGroupModel} to track the column groups. See
* ColumnGroupGridExample
*/
public class ColumnGroupHeaderLayer extends AbstractLayerTransform {
private final SizeConfig rowHeightConfig = new SizeConfig(DataLayer.DEFAULT_ROW_HEIGHT);
private final ColumnGroupModel model;
private final ILayer columnHeaderLayer;
/**
* Flag which is used to tell the ColumnGroupHeaderLayer whether to
* calculate the height of the layer dependent on column group configuration
* or not. If it is set to <code>true</code> the column header will check if
* column groups are configured and if not, the height of the column header
* will not show the double height for showing column groups.
*/
private boolean calculateHeight = false;
/**
* Listener that will fire a RowStructuralRefreshEvent in case the
* ColumnGroupModel changes. Is only needed in case the dynamic height
* calculation is enabled.
*/
private IColumnGroupModelListener modelChangeListener;
public ColumnGroupHeaderLayer(
ILayer columnHeaderLayer,
SelectionLayer selectionLayer,
ColumnGroupModel columnGroupModel) {
this(columnHeaderLayer, selectionLayer, columnGroupModel, true);
}
public ColumnGroupHeaderLayer(
final ILayer columnHeaderLayer,
SelectionLayer selectionLayer,
ColumnGroupModel columnGroupModel,
boolean useDefaultConfiguration) {
super(columnHeaderLayer);
this.columnHeaderLayer = columnHeaderLayer;
this.model = columnGroupModel;
registerCommandHandler(new ColumnGroupsCommandHandler(this.model, selectionLayer, this));
registerCommandHandler(new ConfigureScalingCommandHandler(null, this.rowHeightConfig));
if (useDefaultConfiguration) {
addConfiguration(new DefaultColumnGroupHeaderLayerConfiguration(columnGroupModel));
}
this.modelChangeListener = new IColumnGroupModelListener() {
@Override
public void columnGroupModelChanged() {
fireLayerEvent(new RowStructuralRefreshEvent(columnHeaderLayer));
}
};
this.model.registerColumnGroupModelListener(this.modelChangeListener);
}
// Persistence
@Override
public void saveState(String prefix, Properties properties) {
super.saveState(prefix, properties);
this.model.saveState(prefix, properties);
}
@Override
public void loadState(String prefix, Properties properties) {
super.loadState(prefix, properties);
this.model.loadState(prefix, properties);
fireLayerEvent(new ColumnStructuralRefreshEvent(this));
}
// Vertical features
// Rows
@Override
public int getRowCount() {
return this.columnHeaderLayer.getRowCount() + getGroupRowCount();
}
private int getGroupRowCount() {
return isGroupRowIncluded() ? 1 : 0;
}
private int getGroupRowHeight() {
return isGroupRowIncluded() ? this.rowHeightConfig.getAggregateSize(1) : 0;
}
@Override
public int getPreferredRowCount() {
return this.columnHeaderLayer.getPreferredRowCount() + getGroupRowCount();
}
@Override
public int getRowIndexByPosition(int rowPosition) {
if (rowPosition == 0 && isGroupRowIncluded()) {
return rowPosition;
} else {
return this.columnHeaderLayer.getRowIndexByPosition(rowPosition - this.getGroupRowCount());
}
}
@Override
public int localToUnderlyingRowPosition(int localRowPosition) {
if (localRowPosition == 0 && isGroupRowIncluded()) {
return localRowPosition;
} else {
return localRowPosition - getGroupRowCount();
}
}
// Height
@Override
public int getHeight() {
return this.getGroupRowHeight()
+ this.columnHeaderLayer.getHeight();
}
private boolean isGroupRowIncluded() {
return !this.calculateHeight
|| (this.model.getAllIndexesInGroups() != null
&& !this.model.getAllIndexesInGroups().isEmpty());
}
@Override
public int getPreferredHeight() {
return getGroupRowHeight()
+ this.columnHeaderLayer.getPreferredHeight();
}
@Override
public int getRowHeightByPosition(int rowPosition) {
if (rowPosition == 0 && isGroupRowIncluded()) {
return this.rowHeightConfig.getSize(rowPosition);
} else {
return this.columnHeaderLayer.getRowHeightByPosition(rowPosition - getGroupRowCount());
}
}
public void setRowHeight(int rowHeight) {
this.rowHeightConfig.setSize(0, rowHeight);
}
// Row resize
@Override
public boolean isRowPositionResizable(int rowPosition) {
if (rowPosition == 0 && isGroupRowIncluded()) {
return this.rowHeightConfig.isPositionResizable(rowPosition);
} else {
return this.columnHeaderLayer.isRowPositionResizable(rowPosition - getGroupRowCount());
}
}
// Y
@Override
public int getRowPositionByY(int y) {
if (this.isGroupRowIncluded()) {
int row0Height = getRowHeightByPosition(0);
if (y < row0Height) {
return 0;
} else {
return 1 + this.columnHeaderLayer.getRowPositionByY(y - row0Height);
}
} else {
return this.columnHeaderLayer.getRowPositionByY(y);
}
}
@Override
public int getStartYOfRowPosition(int rowPosition) {
if (isGroupRowIncluded()) {
if (rowPosition == 0) {
return this.rowHeightConfig.getAggregateSize(rowPosition);
} else {
return getRowHeightByPosition(0)
+ this.columnHeaderLayer.getStartYOfRowPosition(rowPosition - 1);
}
} else {
return this.columnHeaderLayer.getStartYOfRowPosition(rowPosition);
}
}
// 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 column 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 bodyColumnIndex = getColumnIndexByPosition(columnPosition);
// Column group header cell
if (this.model.isPartOfAGroup(bodyColumnIndex)) {
if (rowPosition == 0) {
return new LayerCell(
this,
getStartPositionOfGroup(columnPosition),
rowPosition,
columnPosition,
rowPosition,
getColumnSpan(columnPosition),
1);
} else {
return new LayerCell(this, columnPosition, rowPosition);
}
} else {
// render column header w/ rowspan = 2
// as in this case we ask the column header layer for the cell
// position and the column header layer asks his data provider for
// the row count which should always return 1, we ask for row
// position 0 instead of using getGroupHeaderRowPosition(), if we
// would use getGroupHeaderRowPosition() the
// ColumnGroupGroupHeaderLayer wouldn't work anymore
ILayerCell cell = this.columnHeaderLayer.getCellByPosition(columnPosition, 0);
if (cell != null) {
final int rowSpan;
if (this.calculateHeight && this.model.size() == 0) {
rowSpan = 1;
} else {
rowSpan = 2;
}
cell = new TransformedLayerCell(cell) {
@Override
public ILayer getLayer() {
return ColumnGroupHeaderLayer.this;
}
@Override
public int getRowSpan() {
return rowSpan;
}
};
}
return cell;
}
}
/**
* Calculates the span of a cell in a Column Group. Takes into account
* collapsing and hidden columns in the group.
*
* @param columnPosition
* position of any column belonging to the group
*/
protected int getColumnSpan(int columnPosition) {
int columnIndex = getColumnIndexByPosition(columnPosition);
ColumnGroup columnGroup = this.model.getColumnGroupByIndex(columnIndex);
int sizeOfGroup = columnGroup.getSize();
if (columnGroup.isCollapsed()) {
int sizeOfStaticColumns = columnGroup.getStaticColumnIndexes().size();
if (sizeOfStaticColumns == 0) {
return 1;
} else {
sizeOfGroup = sizeOfStaticColumns;
}
}
int startPositionOfGroup = getStartPositionOfGroup(columnPosition);
int endPositionOfGroup = startPositionOfGroup + sizeOfGroup;
List<Integer> columnIndexesInGroup = columnGroup.getMembers();
for (int i = startPositionOfGroup; i < endPositionOfGroup; i++) {
int index = getColumnIndexByPosition(i);
if (!columnIndexesInGroup.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 columnPosition) {
int bodyColumnIndex = getColumnIndexByPosition(columnPosition);
ColumnGroup columnGroup = this.model.getColumnGroupByIndex(bodyColumnIndex);
int leastPossibleStartPositionOfGroup = columnPosition - (columnGroup.getSize() - 1);
int i = 0;
for (i = leastPossibleStartPositionOfGroup; i < columnPosition; i++) {
if (ColumnGroupUtils.isInTheSameGroup(
getColumnIndexByPosition(i),
bodyColumnIndex,
this.model)) {
break;
}
}
return i;
}
@Override
public String getDisplayModeByPosition(int columnPosition, int rowPosition) {
int columnIndex = getColumnIndexByPosition(columnPosition);
if (rowPosition == 0 && this.model.isPartOfAGroup(columnIndex)) {
return DisplayMode.NORMAL;
} else {
return this.columnHeaderLayer.getDisplayModeByPosition(columnPosition, rowPosition);
}
}
@Override
public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
int columnIndex = getColumnIndexByPosition(columnPosition);
if (rowPosition == 0 && this.model.isPartOfAGroup(columnIndex)) {
LabelStack stack = new LabelStack();
if (getConfigLabelAccumulator() != null) {
getConfigLabelAccumulator().accumulateConfigLabels(stack, columnPosition, rowPosition);
}
stack.addLabel(GridRegion.COLUMN_GROUP_HEADER);
if (this.model.isPartOfACollapseableGroup(columnIndex)) {
ColumnGroup group = this.model.getColumnGroupByIndex(columnIndex);
if (group.isCollapsed()) {
stack.addLabelOnTop(DefaultColumnGroupHeaderLayerConfiguration.GROUP_COLLAPSED_CONFIG_TYPE);
} else {
stack.addLabelOnTop(DefaultColumnGroupHeaderLayerConfiguration.GROUP_EXPANDED_CONFIG_TYPE);
}
}
return stack;
} else {
return this.columnHeaderLayer.getConfigLabelsByPosition(columnPosition, rowPosition);
}
}
@Override
public Object getDataValueByPosition(int columnPosition, int rowPosition) {
int columnIndex = getColumnIndexByPosition(columnPosition);
if (rowPosition == 0 && this.model.isPartOfAGroup(columnIndex)) {
return this.model.getColumnGroupByIndex(columnIndex).getName();
} else {
return this.columnHeaderLayer.getDataValueByPosition(columnPosition, 0);
}
}
@Override
public LabelStack getRegionLabelsByXY(int x, int y) {
int columnIndex = getColumnIndexByPosition(getColumnPositionByX(x));
if (this.model.isPartOfAGroup(columnIndex) && y < getRowHeightByPosition(0)) {
return new LabelStack(GridRegion.COLUMN_GROUP_HEADER);
} else {
return this.columnHeaderLayer.getRegionLabelsByXY(x, y - getRowHeightByPosition(0));
}
}
// ColumnGroupModel delegates
public void addColumnsIndexesToGroup(String colGroupName, int... colIndexes) {
this.model.addColumnsIndexesToGroup(colGroupName, colIndexes);
}
public void clearAllGroups() {
this.model.clear();
}
public void setStaticColumnIndexesByGroup(String colGroupName, int... staticColumnIndexes) {
this.model.setStaticColumnIndexesByGroup(colGroupName, staticColumnIndexes);
}
public boolean isColumnInGroup(int bodyColumnIndex) {
return this.model.isPartOfAGroup(bodyColumnIndex);
}
/**
* @see ColumnGroup#setUnbreakable(boolean)
*/
public void setGroupUnbreakable(int columnIndex) {
ColumnGroup columnGroup = this.model.getColumnGroupByIndex(columnIndex);
columnGroup.setUnbreakable(true);
}
public void setGroupAsCollapsed(int columnIndex) {
ColumnGroup columnGroup = this.model.getColumnGroupByIndex(columnIndex);
columnGroup.setCollapsed(true);
}
public boolean isCalculateHeight() {
return this.calculateHeight;
}
public void setCalculateHeight(boolean calculateHeight) {
boolean changed = calculateHeight != this.calculateHeight;
this.calculateHeight = calculateHeight;
if (changed) {
if (calculateHeight) {
this.model.registerColumnGroupModelListener(this.modelChangeListener);
} else {
this.model.unregisterColumnGroupModelListener(this.modelChangeListener);
}
this.fireLayerEvent(new RowStructuralRefreshEvent(this));
}
}
/**
* @since 1.4
*/
@Override
public Collection<String> getProvidedLabels() {
Collection<String> labels = super.getProvidedLabels();
labels.add(GridRegion.COLUMN_GROUP_HEADER);
labels.add(DefaultColumnGroupHeaderLayerConfiguration.GROUP_COLLAPSED_CONFIG_TYPE);
labels.add(DefaultColumnGroupHeaderLayerConfiguration.GROUP_EXPANDED_CONFIG_TYPE);
// add the labels configured via IConfigLabelAccumulator
if (getConfigLabelAccumulator() != null
&& getConfigLabelAccumulator() instanceof IConfigLabelProvider) {
labels.addAll(((IConfigLabelProvider) getConfigLabelAccumulator()).getProvidedLabels());
}
return labels;
}
}