blob: dd992f2aa09d5cd82f3cf4a519225d04b7525fe6 [file] [log] [blame]
/*****************************************************************************
* Copyright (c) 2018, 2020 Dirk Fauth.
*
* 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:
* Dirk Fauth <dirk.fauth@googlemail.com> - Initial API and implementation
*****************************************************************************/
package org.eclipse.nebula.widgets.nattable.hierarchical;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.collections.api.list.primitive.MutableIntList;
import org.eclipse.collections.api.set.primitive.MutableIntSet;
import org.eclipse.collections.impl.factory.primitive.IntLists;
import org.eclipse.collections.impl.factory.primitive.IntSets;
import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.coordinate.PositionCoordinate;
import org.eclipse.nebula.widgets.nattable.coordinate.Range;
import org.eclipse.nebula.widgets.nattable.grid.command.ClientAreaResizeCommand;
import org.eclipse.nebula.widgets.nattable.hideshow.AbstractRowHideShowLayer;
import org.eclipse.nebula.widgets.nattable.hideshow.command.ColumnHideCommand;
import org.eclipse.nebula.widgets.nattable.hideshow.command.MultiColumnHideCommand;
import org.eclipse.nebula.widgets.nattable.hideshow.command.RowHideCommand;
import org.eclipse.nebula.widgets.nattable.hideshow.command.RowPositionHideCommand;
import org.eclipse.nebula.widgets.nattable.hideshow.event.HideRowPositionsEvent;
import org.eclipse.nebula.widgets.nattable.hideshow.event.ShowRowPositionsEvent;
import org.eclipse.nebula.widgets.nattable.hideshow.indicator.HideIndicatorConstants;
import org.eclipse.nebula.widgets.nattable.hierarchical.command.HierarchicalTreeCollapseAllCommandHandler;
import org.eclipse.nebula.widgets.nattable.hierarchical.command.HierarchicalTreeExpandAllCommandHandler;
import org.eclipse.nebula.widgets.nattable.hierarchical.command.HierarchicalTreeExpandCollapseCommandHandler;
import org.eclipse.nebula.widgets.nattable.hierarchical.command.HierarchicalTreeExpandToLevelCommandHandler;
import org.eclipse.nebula.widgets.nattable.hierarchical.config.DefaultHierarchicalTreeLayerConfiguration;
import org.eclipse.nebula.widgets.nattable.hover.command.ClearHoverStylingCommand;
import org.eclipse.nebula.widgets.nattable.hover.command.HoverStylingCommand;
import org.eclipse.nebula.widgets.nattable.layer.IDpiConverter;
import org.eclipse.nebula.widgets.nattable.layer.ILayer;
import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
import org.eclipse.nebula.widgets.nattable.layer.LayerUtil;
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.SpanningLayerCell;
import org.eclipse.nebula.widgets.nattable.layer.cell.TranslatedLayerCell;
import org.eclipse.nebula.widgets.nattable.layer.command.ConfigureScalingCommand;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.layer.event.IStructuralChangeEvent;
import org.eclipse.nebula.widgets.nattable.painter.cell.CellPainterWrapper;
import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
import org.eclipse.nebula.widgets.nattable.painter.cell.decorator.CellPainterDecorator;
import org.eclipse.nebula.widgets.nattable.reorder.command.ColumnReorderCommand;
import org.eclipse.nebula.widgets.nattable.reorder.command.MultiColumnReorderCommand;
import org.eclipse.nebula.widgets.nattable.search.event.SearchEvent;
import org.eclipse.nebula.widgets.nattable.selection.ITraversalStrategy;
import org.eclipse.nebula.widgets.nattable.selection.MoveCellSelectionCommandHandler;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectCellCommand;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectColumnCommand;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectRegionCommand;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.tree.TreeLayer;
import org.eclipse.nebula.widgets.nattable.tree.config.DefaultTreeLayerConfiguration;
import org.eclipse.nebula.widgets.nattable.tree.config.TreeConfigAttributes;
import org.eclipse.nebula.widgets.nattable.tree.painter.IndentedTreeImagePainter;
import org.eclipse.nebula.widgets.nattable.util.ArrayUtil;
import org.eclipse.swt.graphics.Rectangle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This layer is used to show a hierarchical object model in a tree structure.
* It does not show structure node rows but a flattened (de-normalized) view of
* the hierarchical object model.
*
* @since 1.6
*/
public class HierarchicalTreeLayer extends AbstractRowHideShowLayer {
private static final Logger LOG = LoggerFactory.getLogger(HierarchicalTreeLayer.class);
/**
* Label that gets applied to cells in the level header columns.
*/
public static final String LEVEL_HEADER_CELL = "LEVEL_HEADER_CELL"; //$NON-NLS-1$
/**
* Label that gets applied to child cells of a collapsed parent object.
*/
public static final String COLLAPSED_CHILD = "COLLAPSED_CHILD"; //$NON-NLS-1$
/**
* Label that gets applied to child level cells if the row object does not
* contain an object for that level.
*/
public static final String NO_OBJECT_IN_LEVEL = "NO_OBJECT_IN_LEVEL"; //$NON-NLS-1$
/**
* The underlying list needed for expand/collapse operations.
*/
private List<HierarchicalWrapper> underlyingList;
/**
* The property names used to access the column values of row objects via
* reflection.
*
* @see org.eclipse.nebula.widgets.nattable.data.ExtendedReflectiveColumnPropertyAccessor
*/
private final String[] propertyNames;
/**
* SelectionLayer that is used to determine if a cell in a row inside a
* level is selected. Needed to highlight a level header cell in
* {@link DisplayMode#SELECT}.
*/
private final SelectionLayer selectionLayer;
/**
* Array of the column positions that are level header.
*/
private int[] levelHeaderPositions;
/**
* Mapping of level to the first column index of the level which is showing
* the tree nodes.
*/
private Map<Integer, Integer> nodeColumnMapping = new LinkedHashMap<>();
/**
* Mapping of the level to the list of all columns belonging to a level.
*/
private Map<Integer, List<Integer>> levelIndexMapping = new LinkedHashMap<>();
/**
* Set of tree node coordinates based on indexes that are collapsed.
*/
protected final Set<HierarchicalTreeNode> collapsedNodes = new HashSet<>();
/**
* Collection of all row indexes that are hidden if tree nodes are
* collapsed.
*/
private MutableIntSet hiddenRowIndexes = IntSets.mutable.empty();
/**
* The index of the first column that shows the leaf level.
*/
private int leafLevelColumnIndex = 0;
/**
* The column width of the level header columns in pixel.
*/
private int levelHeaderWidth = 20;
/**
* Converter that is used to ensure the column width of the level header
* columns scaled correctly.
*/
private IDpiConverter dpiConverter;
/**
* Flag to configure whether the tree column should be identified by
* position or by index. Default is position.
*/
private boolean useTreeColumnIndex = false;
/**
* Flag to configure whether the tree level header should be shown or not.
*/
private boolean showTreeLevelHeader = true;
/**
* Flag to configure whether {@link #getConfigLabelsByPosition(int, int)}
* should add the {@link #COLLAPSED_CHILD} label to the {@link LabelStack}.
* Enabling this configuration allows a different configuration for child
* cells of collapsed rows, e.g. different styles like no content painter or
* different background.
*/
private boolean handleCollapsedChildren = true;
/**
* Flag to configure whether {@link #getConfigLabelsByPosition(int, int)}
* should add the {@link #NO_OBJECT_IN_LEVEL} label to the
* {@link LabelStack}. Enabling this configuration allows a different
* configuration for child cells of row objects that have no object for a
* child level, e.g. making those cells not editable and different styles
* like no content painter or different background.
*/
private boolean handleNoObjectsInLevel = true;
/**
* Flag to configure if collapsed nodes should be kept in the
* {@link #collapsedNodes} even if the row object is not contained in the
* underlying list anymore. This can for example happen when using a
* FilterList, as filtering will remove the row objects from that list.
* Without using a FilterList or supporting deleting rows, it is suggested
* to set this flag to <code>false</code> to avoid memory leaks on really
* deleting an object.
*/
private boolean retainRemovedRowObjectNodes = true;
/**
* Flag to configure if collapsed nodes should be expanded if they contain
* rows that are found on search. Default is <code>true</code>. If set to
* <code>false</code> only the found row will be made visible by still
* keeping the nodes collapsed.
*/
private boolean expandOnSearch = true;
/**
* Flag to configure whether columns in sub levels should be selected when
* selecting a level header cell or if only the cells in the same level
* should be selected. Default is <code>false</code>.
*/
private boolean selectSubLevels = false;
/**
*
* @param underlyingLayer
* The underlying layer this layer is stacked on.
* @param underlyingList
* The collection with the {@link HierarchicalWrapper} objects
* that is shown in the table. Needed to perform expand/collapse
* actions.
* @param propertyNames
* The property names to access the object properties of the
* wrapped objects inside the {@link HierarchicalWrapper}. Needed
* to determine the levels.
*/
public HierarchicalTreeLayer(
IUniqueIndexLayer underlyingLayer,
List<HierarchicalWrapper> underlyingList,
String[] propertyNames) {
this(underlyingLayer, underlyingList, propertyNames, null, true);
}
/**
*
* @param underlyingLayer
* The underlying layer this layer is stacked on.
* @param underlyingList
* The collection with the {@link HierarchicalWrapper} objects
* that is shown in the table. Needed to perform expand/collapse
* actions.
* @param propertyNames
* The property names to access the object properties of the
* wrapped objects inside the {@link HierarchicalWrapper}. Needed
* to determine the levels.
* @param useDefaultConfiguration
* <code>true</code> if the
* {@link DefaultHierarchicalTreeLayerConfiguration} should be
* added, <code>false</code> if not.
*/
public HierarchicalTreeLayer(
IUniqueIndexLayer underlyingLayer,
List<HierarchicalWrapper> underlyingList,
String[] propertyNames,
boolean useDefaultConfiguration) {
this(underlyingLayer, underlyingList, propertyNames, null, useDefaultConfiguration);
}
/**
*
* @param underlyingLayer
* The underlying layer this layer is stacked on.
* @param underlyingList
* The collection with the {@link HierarchicalWrapper} objects
* that is shown in the table. Needed to perform expand/collapse
* actions.
* @param propertyNames
* The property names to access the object properties of the
* wrapped objects inside the {@link HierarchicalWrapper}. Needed
* to determine the levels.
* @param selectionLayer
* The {@link SelectionLayer} needed to calculate selections for
* the level header column. Can be <code>null</code> which leads
* to not showing selections in the level header.
*/
public HierarchicalTreeLayer(
IUniqueIndexLayer underlyingLayer,
List<HierarchicalWrapper> underlyingList,
String[] propertyNames,
SelectionLayer selectionLayer) {
this(underlyingLayer, underlyingList, propertyNames, selectionLayer, true);
}
/**
*
* @param underlyingLayer
* The underlying layer this layer is stacked on.
* @param underlyingList
* The collection with the {@link HierarchicalWrapper} objects
* that is shown in the table. Needed to perform expand/collapse
* actions.
* @param propertyNames
* The property names to access the object properties of the
* wrapped objects inside the {@link HierarchicalWrapper}. Needed
* to determine the levels.
* @param selectionLayer
* The {@link SelectionLayer} needed to calculate selections for
* the level header column. Can be <code>null</code> which leads
* to not showing selections in the level header.
* @param useDefaultConfiguration
* <code>true</code> if the
* {@link DefaultHierarchicalTreeLayerConfiguration} should be
* added, <code>false</code> if not.
*/
public HierarchicalTreeLayer(
IUniqueIndexLayer underlyingLayer,
List<HierarchicalWrapper> underlyingList,
String[] propertyNames,
SelectionLayer selectionLayer,
boolean useDefaultConfiguration) {
super(underlyingLayer);
this.underlyingList = underlyingList;
this.propertyNames = propertyNames;
this.selectionLayer = selectionLayer;
// inspect the propertyNames to identify the first columns per level
// remember the highest level based on the split
// we need to remove it at the end because the highest level is in fact
// the leaf level
if (propertyNames.length > 0) {
int currentLevel = 1;
this.nodeColumnMapping.put(0, 0);
List<Integer> columns = new ArrayList<>();
columns.add(0);
this.levelIndexMapping.put(0, columns);
for (int col = 1; col < propertyNames.length; col++) {
String[] split = propertyNames[col].split(HierarchicalHelper.PROPERTY_SEPARATOR_REGEX);
if (split.length == currentLevel) {
columns.add(col);
} else if (split.length > currentLevel) {
this.nodeColumnMapping.put(currentLevel, col);
columns = new ArrayList<>();
columns.add(col);
this.levelIndexMapping.put(currentLevel, columns);
currentLevel++;
this.leafLevelColumnIndex = col;
}
}
calculateLevelColumnHeaderPositions();
}
if (useDefaultConfiguration) {
// extends DefaultTreeLayerConfiguration but uses different handler
// that support row and column
addConfiguration(new DefaultHierarchicalTreeLayerConfiguration(this));
}
registerCommandHandler(new HierarchicalTreeExpandCollapseCommandHandler(this));
registerCommandHandler(new HierarchicalTreeExpandAllCommandHandler(this));
registerCommandHandler(new HierarchicalTreeCollapseAllCommandHandler(this));
registerCommandHandler(new HierarchicalTreeExpandToLevelCommandHandler(this));
// register a specialized MoveCellSelectionCommandHandler to ensure that
// the selection movement is always performed on visible cells
if (this.selectionLayer != null) {
registerCommandHandler(
new MoveCellSelectionCommandHandler(
this.selectionLayer,
new HierarchicalTraversalStrategy(ITraversalStrategy.AXIS_TRAVERSAL_STRATEGY, this)));
}
}
@Override
public boolean doCommand(ILayerCommand command) {
if (command.convertToTargetLayer(this)) {
if (command instanceof SelectCellCommand) {
// perform selection of level on level header click
SelectCellCommand selection = (SelectCellCommand) command;
if (isLevelHeaderColumn(selection.getColumnPosition())) {
ILayerCell clickedCell = getCellByPosition(selection.getColumnPosition(), selection.getRowPosition());
// calculate number of header columns to the right
SelectRegionCommand selectRegion = new SelectRegionCommand(this,
clickedCell.getColumnPosition() + 1,
clickedCell.getOriginRowPosition(),
getNumberOfColumnsToSelect(selection.getColumnPosition()),
clickedCell.getRowSpan(),
selection.isShiftMask(),
selection.isControlMask());
return this.underlyingLayer.doCommand(selectRegion);
}
} else if (command instanceof SelectColumnCommand) {
SelectColumnCommand selection = (SelectColumnCommand) command;
if (isLevelHeaderColumn(selection.getColumnPosition())) {
int level = 0;
for (; level < this.levelHeaderPositions.length; level++) {
int pos = this.levelHeaderPositions[level];
if (pos == selection.getColumnPosition()) {
break;
}
}
SelectRegionCommand selectRegion = new SelectRegionCommand(this,
selection.getColumnPosition() + 1,
0,
getNumberOfColumnsToSelect(selection.getColumnPosition()),
Integer.MAX_VALUE,
selection.isWithShiftMask(),
selection.isWithControlMask());
return this.underlyingLayer.doCommand(selectRegion);
}
} else if (command instanceof ConfigureScalingCommand) {
this.dpiConverter = ((ConfigureScalingCommand) command).getHorizontalDpiConverter();
} else if (command instanceof ClientAreaResizeCommand && command.convertToTargetLayer(this)) {
ClientAreaResizeCommand clientAreaResizeCommand = (ClientAreaResizeCommand) command;
Rectangle possibleArea = clientAreaResizeCommand.getScrollable().getClientArea();
// remove the tree level header width from the client area to
// ensure that the percentage calculation is correct
possibleArea.width = possibleArea.width - (this.levelHeaderPositions.length * getScaledLevelHeaderWidth());
clientAreaResizeCommand.setCalcArea(possibleArea);
} else if (command instanceof ColumnReorderCommand) {
ColumnReorderCommand crCommand = ((ColumnReorderCommand) command);
if (!isValidTargetColumnPosition(crCommand.getFromColumnPosition(), crCommand.getToColumnPosition())) {
// in case the target position is not valid we consume the
// command without doing anything
return true;
}
if (isLevelHeaderColumn(crCommand.getToColumnPosition())) {
// we need to increase the column position by 1 to handle
// the tree level header
return super.doCommand(
new ColumnReorderCommand(this, crCommand.getFromColumnPosition(), crCommand.getToColumnPosition() + 1));
}
} else if (command instanceof MultiColumnReorderCommand) {
MultiColumnReorderCommand crCommand = ((MultiColumnReorderCommand) command);
for (int fromColumnPosition : crCommand.getFromColumnPositions()) {
if (!isValidTargetColumnPosition(fromColumnPosition, crCommand.getToColumnPosition())) {
// if any position would be invalid for the reorder, the
// command would be skipped
return true;
}
}
if (isLevelHeaderColumn(crCommand.getToColumnPosition())) {
// we need to increase the column position by 1 to handle
// the tree level header
return super.doCommand(
new MultiColumnReorderCommand(this, crCommand.getFromColumnPositionsArray(), crCommand.getToColumnPosition() + 1));
}
} else if (command instanceof ColumnHideCommand) {
if (isLevelHeaderColumn(((ColumnHideCommand) command).getColumnPosition())) {
// it is not supported to hide a level header column
// we therefore consume the command without an action
return true;
}
} else if (command instanceof MultiColumnHideCommand) {
Collection<Integer> positions = ((MultiColumnHideCommand) command).getColumnPositions();
boolean modified = false;
for (Iterator<Integer> it = positions.iterator(); it.hasNext();) {
Integer pos = it.next();
if (isLevelHeaderColumn(pos)) {
modified = true;
it.remove();
}
}
if (modified && !positions.isEmpty()) {
// if a level header column position was contained we
// need to fire a new command that does not contain that
// position
int[] newPositions = new int[positions.size()];
int i = 0;
for (int p : positions) {
newPositions[i] = p;
i++;
}
return doCommand(new MultiColumnHideCommand(this, newPositions));
}
} else if (command instanceof RowPositionHideCommand) {
RowPositionHideCommand cmd = (RowPositionHideCommand) command;
if (isLevelHeaderColumn(cmd.getColumnPosition())) {
return super.doCommand(new RowPositionHideCommand(this, cmd.getColumnPosition() + 1, cmd.getRowPosition()));
} else {
return doCommand(new RowHideCommand(this, cmd.getRowPosition()));
}
} else if (command instanceof HoverStylingCommand) {
// when hovering over a level header we simply clear hover
// styling
if (isLevelHeaderColumn(((HoverStylingCommand) command).getColumnPosition())) {
return super.doCommand(new ClearHoverStylingCommand());
}
}
}
return super.doCommand(command);
}
@Override
public void handleLayerEvent(ILayerEvent event) {
if (event instanceof IStructuralChangeEvent) {
IStructuralChangeEvent structuralChangeEvent = (IStructuralChangeEvent) event;
if (structuralChangeEvent.isVerticalStructureChanged()) {
// recalculate node row indexes
// build a new collection of nodes to avoid duplication clashes
// as nodes are equal per column and row index
int negativeIndex = -1;
Set<HierarchicalTreeNode> updatedCollapsedNodes = new HashSet<>();
for (HierarchicalTreeNode node : this.collapsedNodes) {
int newRowIndex = findTopRowIndex(node.columnIndex, node.rowObject);
// add the updated node if the row object still exists in
// the underlying collection
if (newRowIndex >= 0) {
updatedCollapsedNodes.add(
new HierarchicalTreeNode(
node.columnIndex,
newRowIndex,
this.underlyingList.get(newRowIndex)));
} else if (this.retainRemovedRowObjectNodes) {
updatedCollapsedNodes.add(
new HierarchicalTreeNode(
node.columnIndex,
negativeIndex,
node.rowObject));
negativeIndex--;
}
}
this.collapsedNodes.clear();
this.collapsedNodes.addAll(updatedCollapsedNodes);
// recalculate hidden rows based on updated collapsed nodes
int[] updatedHiddenRows = this.collapsedNodes.stream()
.map(node -> getChildIndexes(node.columnIndex, node.rowIndex))
.flatMapToInt(Arrays::stream)
.toArray();
this.hiddenRowIndexes = IntSets.mutable.of(updatedHiddenRows);
} else if (structuralChangeEvent.isHorizontalStructureChanged()) {
// if the column structure was changed we need to recalculate
// the header positions, e.g. on column hide or show
calculateLevelColumnHeaderPositions();
}
} else if (event instanceof SearchEvent) {
PositionCoordinate coord = ((SearchEvent) event).getCellCoordinate();
if (coord != null) {
int foundIndex = coord.getLayer().getRowIndexByPosition(coord.rowPosition);
if (this.hiddenRowIndexes.contains(foundIndex)) {
if (this.expandOnSearch) {
// level header positions - 2 because the leaf level is
// not collapsible
for (int level = this.nodeColumnMapping.size() - 2; level >= 0; level--) {
ILayerCell nodeCell = coord.getLayer().getCellByPosition(
this.nodeColumnMapping.get(level),
coord.rowPosition);
int colIdx = coord.getLayer().getColumnIndexByPosition(nodeCell.getOriginColumnPosition());
int rowIdx = coord.getLayer().getRowIndexByPosition(nodeCell.getOriginRowPosition());
if (this.collapsedNodes.contains(new HierarchicalTreeNode(colIdx, rowIdx, null))) {
expandOrCollapse(colIdx, rowIdx);
}
}
} else {
// only make the single row visible again
this.hiddenRowIndexes.remove(foundIndex);
}
} else {
int lvl = getLevelByColumnIndex(coord.getLayer().getColumnIndexByPosition(coord.columnPosition));
for (int level = 0; level <= lvl; level++) {
ILayerCell nodeCell = coord.getLayer().getCellByPosition(
this.nodeColumnMapping.get(level),
coord.rowPosition);
int colIdx = coord.getLayer().getColumnIndexByPosition(nodeCell.getOriginColumnPosition());
int rowIdx = coord.getLayer().getRowIndexByPosition(nodeCell.getOriginRowPosition());
if (this.collapsedNodes.contains(new HierarchicalTreeNode(colIdx, rowIdx, null))) {
expandOrCollapse(colIdx, rowIdx);
}
}
}
invalidateCache();
fireLayerEvent(new ShowRowPositionsEvent(this, foundIndex));
}
}
super.handleLayerEvent(event);
}
@Override
public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
// for level header we do not need to call super
if (isLevelHeaderColumn(columnPosition)) {
LabelStack labelStack = new LabelStack(LEVEL_HEADER_CELL);
// if all cells in the last level are hidden, we need to add the
// hide indicator to the level header
if (columnPosition + 1 == getColumnCount()
|| getStartXOfColumnPosition(columnPosition + 1) == getWidth()) {
labelStack.addLabel(HideIndicatorConstants.COLUMN_RIGHT_HIDDEN);
}
return labelStack;
}
LabelStack configLabels = super.getConfigLabelsByPosition(columnPosition, rowPosition);
if (isTreeColumn(columnPosition)) {
configLabels.addLabelOnTop(TreeLayer.TREE_COLUMN_CELL);
ILayerCell cell = getCellByPosition(columnPosition, rowPosition);
if (cell != null) {
// always level 0 as we do not show all levels in one column and
// therefore we do not need indentation
configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + 0);
if (cell.getRowSpan() > 1) {
configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_EXPANDED_CONFIG_TYPE);
} else if (isCollapsed(columnPosition, rowPosition)) {
configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_COLLAPSED_CONFIG_TYPE);
} else {
// we get here for example if handleCollapsedChildren is
// false and the parent is collapsed
// we can not show a handle to expand/collapse the child if
// the parent is collapsed
configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_LEAF_CONFIG_TYPE);
}
}
}
if (this.handleCollapsedChildren) {
boolean directLevelHeader = true;
boolean headerCollapsed = false;
for (int i = this.levelHeaderPositions.length - 1; i >= 0; i--) {
int pos = this.levelHeaderPositions[i];
if (pos < columnPosition) {
int firstLevelColumnPosition = pos + (isShowTreeLevelHeader() ? 1 : 0);
// the first header we find is the direct one
if (directLevelHeader) {
directLevelHeader = false;
} else if (isCollapsed(firstLevelColumnPosition, rowPosition)) {
headerCollapsed = true;
}
}
}
if (headerCollapsed) {
configLabels.addLabelOnTop(COLLAPSED_CHILD);
// remove tree labels to avoid specialized handling and
// rendering
configLabels.removeLabel(TreeLayer.TREE_COLUMN_CELL);
configLabels.removeLabel(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + 0);
configLabels.removeLabel(DefaultTreeLayerConfiguration.TREE_EXPANDED_CONFIG_TYPE);
configLabels.removeLabel(DefaultTreeLayerConfiguration.TREE_COLLAPSED_CONFIG_TYPE);
configLabels.removeLabel(DefaultTreeLayerConfiguration.TREE_LEAF_CONFIG_TYPE);
}
}
// mark cells of empty level objects
if (this.handleNoObjectsInLevel && !hasLevelObject(columnPosition, rowPosition)) {
configLabels.addLabelOnTop(NO_OBJECT_IN_LEVEL);
}
return configLabels;
}
@Override
public ICellPainter getCellPainter(
int columnPosition,
int rowPosition,
ILayerCell cell,
IConfigRegistry configRegistry) {
ICellPainter cellPainter = super.getCellPainter(
columnPosition, rowPosition, cell, configRegistry);
// if the cell is a tree column and is not marked to contain no object,
// the registered painter is wrapped by the tree painter
if (cell.getConfigLabels().hasLabel(TreeLayer.TREE_COLUMN_CELL)
&& !cell.getConfigLabels().hasLabel(NO_OBJECT_IN_LEVEL)) {
ICellPainter treeCellPainter = configRegistry.getConfigAttribute(
TreeConfigAttributes.TREE_STRUCTURE_PAINTER,
cell.getDisplayMode(),
cell.getConfigLabels());
if (treeCellPainter != null) {
IndentedTreeImagePainter treePainter = findIndentedTreeImagePainter(treeCellPainter);
if (treePainter != null) {
treePainter.setBaseCellPainter(cellPainter);
cellPainter = treeCellPainter;
} else {
LOG.warn("There is no IndentedTreeImagePainter found for TREE_STRUCTURE_PAINTER"); //$NON-NLS-1$
}
} else {
LOG.warn("There is no IndentedTreeImagePainter found for TREE_STRUCTURE_PAINTER"); //$NON-NLS-1$
}
}
return cellPainter;
}
private IndentedTreeImagePainter findIndentedTreeImagePainter(ICellPainter painter) {
IndentedTreeImagePainter result = null;
if (painter instanceof IndentedTreeImagePainter) {
result = (IndentedTreeImagePainter) painter;
} else if (painter instanceof CellPainterWrapper
&& ((CellPainterWrapper) painter).getWrappedPainter() != null) {
result = findIndentedTreeImagePainter(((CellPainterWrapper) painter).getWrappedPainter());
} else if (painter instanceof CellPainterDecorator) {
result = findIndentedTreeImagePainter(((CellPainterDecorator) painter).getBaseCellPainter());
if (result == null) {
result = findIndentedTreeImagePainter(((CellPainterDecorator) painter).getDecoratorCellPainter());
}
}
return result;
}
@Override
public ILayerCell getCellByPosition(int columnPosition, int rowPosition) {
if (isLevelHeaderColumn(columnPosition)) {
ILayerCell right = getCellByPosition(columnPosition + 1, rowPosition);
if (right != null) {
return new LayerCell(this, columnPosition, right.getOriginRowPosition(), columnPosition, rowPosition, 1, right.getRowSpan());
}
return null;
}
int underlyingColumnPosition = localToUnderlyingColumnPosition(columnPosition);
int underlyingRowPosition = localToUnderlyingRowPosition(rowPosition);
ILayerCell cell = this.underlyingLayer.getCellByPosition(underlyingColumnPosition, underlyingRowPosition);
if (cell != null) {
ILayerCell localCell = new TranslatedLayerCell(cell, this,
underlyingToLocalColumnPosition(this.underlyingLayer, cell.getOriginColumnPosition()),
underlyingToLocalRowPosition(this.underlyingLayer, cell.getOriginRowPosition()),
underlyingToLocalColumnPosition(this.underlyingLayer, cell.getColumnPosition()),
underlyingToLocalRowPosition(this.underlyingLayer, cell.getRowPosition()));
if (cell.isSpannedCell()) {
// in case a deeper level is collapsed, rows are hidden via row
// hide/show mechanism
// therefore the spanning needs to be updated to reflect the
// hiding accordingly
boolean rowSpanUpdated = false;
int rowSpan = cell.getRowSpan();
int rowIndex = this.underlyingLayer.getRowIndexByPosition(cell.getOriginRowPosition());
for (int row = 0; row < cell.getRowSpan(); row++) {
if (isRowIndexHidden(rowIndex) && !isHiddenInUnderlyingLayer(rowIndex)) {
rowSpan--;
rowSpanUpdated = true;
}
rowIndex++;
}
if (rowSpanUpdated) {
cell = new SpanningLayerCell(localCell, localCell.getColumnSpan(), rowSpan);
} else {
cell = localCell;
}
} else {
cell = localCell;
}
}
return cell;
}
@Override
public Object getDataValueByPosition(int columnPosition, int rowPosition) {
if (isLevelHeaderColumn(columnPosition)) {
return null;
}
return super.getDataValueByPosition(columnPosition, rowPosition);
}
@Override
public String getDisplayModeByPosition(int columnPosition, int rowPosition) {
if (isLevelHeaderColumn(columnPosition)) {
// there is no support for hover styling of the tree level header
// the HoverLayer does not see the level header cells and therefore
// can not add the HOVER DisplayMode
if (isRowPositionInLevelSelected(columnPosition, rowPosition)) {
return DisplayMode.SELECT;
}
return DisplayMode.NORMAL;
}
return super.getDisplayModeByPosition(columnPosition, rowPosition);
}
/**
* Test if a cell in the given row and a column belonging to the level of
* the given level header position is selected.
*
* @param levelHeaderColumnPosition
* The column position of the level header column.
* @param rowPosition
* The row position.
* @return <code>true</code> if a cell in the given row is selected in the
* level of the level header position, <code>false</code> if not.
*/
protected boolean isRowPositionInLevelSelected(int levelHeaderColumnPosition, int rowPosition) {
// first perform a check if any position in the row is selected
// better performance in case of no selection
if (this.selectionLayer != null) {
ILayerCell headerCell = getCellByPosition(levelHeaderColumnPosition, rowPosition);
int selectionLayerRowPosition = LayerUtil.convertRowPosition(this, headerCell.getOriginRowPosition(), this.selectionLayer);
if (this.selectionLayer.isRowPositionSelected(selectionLayerRowPosition)) {
int level = 0;
for (int i = 0; i < this.levelHeaderPositions.length; i++) {
int levelHeaderPos = this.levelHeaderPositions[i];
if (levelHeaderPos == levelHeaderColumnPosition) {
level = i;
break;
}
}
List<Integer> levelColumns = getColumnIndexesForLevel(level);
for (int columnIndex : levelColumns) {
int column = this.selectionLayer.getColumnPositionByIndex(columnIndex);
if (column >= 0 && this.selectionLayer.isCellPositionSelected(column, selectionLayerRowPosition)) {
return true;
}
}
}
}
return false;
}
/**
* Calculates the number of header columns to the right of a given level
* header column position.
*
* @param levelHeaderPosition
* The column position of a level header column.
* @return The number of columns to select when a level header column is
* selected.
*/
protected int getNumberOfColumnsToSelect(int levelHeaderPosition) {
int columnsToSelect = 0;
if (isSelectSubLevels()) {
int levelHeaderCount = 0;
for (int i = this.levelHeaderPositions.length - 1; i >= 0; i--) {
if (this.levelHeaderPositions[i] >= levelHeaderPosition) {
levelHeaderCount++;
}
}
columnsToSelect = getColumnCount() - levelHeaderCount - (levelHeaderPosition);
} else {
for (int i = 0; i < this.levelHeaderPositions.length; i++) {
int pos = this.levelHeaderPositions[i];
if (pos == levelHeaderPosition) {
columnsToSelect = getLevelIndexMapping().get(i).size();
}
}
}
return columnsToSelect;
}
/**
* Test if the column at the given position is a tree column.
*
* @param columnPosition
* The column position to check.
* @return <code>true</code> if the given position is a tree column,
* <code>false</code> if it is a content column.
*/
protected boolean isTreeColumn(int columnPosition) {
int col = localToUnderlyingColumnPosition(columnPosition);
if (isUseTreeColumnIndex()) {
col = getColumnIndexByPosition(columnPosition);
}
return this.nodeColumnMapping.containsValue(col) && this.leafLevelColumnIndex != col;
}
/**
* Test if the column at the given position is a level header column.
*
* @param columnPosition
* The column position to check.
* @return <code>true</code> if the given position is a level header column,
* <code>false</code> if it is a content column.
*/
public boolean isLevelHeaderColumn(int columnPosition) {
for (int pos : this.levelHeaderPositions) {
if (pos == columnPosition) {
return true;
}
}
return false;
}
/**
* Test if the cell at the given coordinates belongs to a cell with an
* object for the corresponding level or not.
*
* @param columnPosition
* The column position to check.
* @param rowPosition
* The row position to check.
* @return <code>true</code> if there is a level object for the
* corresponding level, <code>false</code> if there is no level
* object.
*/
protected boolean hasLevelObject(int columnPosition, int rowPosition) {
int columnIndex = getColumnIndexByPosition(columnPosition);
int level = -1;
if (columnIndex >= 0) {
level = getLevelByColumnIndex(columnIndex);
} else {
int[] positions = this.levelHeaderPositions;
for (int i = 0; i < positions.length; i++) {
int pos = positions[i];
if (pos == columnPosition) {
level = i;
}
}
}
if (level >= 0) {
int rowIndex = getRowIndexByPosition(rowPosition);
if (rowIndex >= 0 && rowIndex < this.underlyingList.size()) {
HierarchicalWrapper rowObject = this.underlyingList.get(rowIndex);
return rowObject.getObject(level) != null;
}
}
return false;
}
/**
* Returns the level to which a given column index belongs to.
*
* @param columnIndex
* The column index for which the level is requested.
* @return The level to which the given column index belongs to or -1 if the
* columnIndex is invalid.
*/
public int getLevelByColumnIndex(int columnIndex) {
if (columnIndex >= 0 && columnIndex < this.propertyNames.length) {
String propertyName = this.propertyNames[columnIndex];
String[] split = propertyName.split(HierarchicalHelper.PROPERTY_SEPARATOR_REGEX);
return split.length - 1;
}
return -1;
}
/**
* Returns all column indexes for a given level.
*
* @param level
* The level for which the column indexes are requested.
* @return The column indexes of the columns that belong to the given level.
*/
public List<Integer> getColumnIndexesForLevel(int level) {
return this.levelIndexMapping.get(level);
}
/**
*
* @return Mapping of the level to the list of the columns belonging to the
* level.
*/
public Map<Integer, List<Integer>> getLevelIndexMapping() {
return this.levelIndexMapping;
}
@Override
public boolean isRowIndexHidden(int rowIndex) {
return this.hiddenRowIndexes.contains(rowIndex) || isHiddenInUnderlyingLayer(rowIndex);
}
@Override
public Collection<Integer> getHiddenRowIndexes() {
return ArrayUtil.asIntegerList(this.hiddenRowIndexes.toSortedArray());
}
@Override
public int[] getHiddenRowIndexesArray() {
return this.hiddenRowIndexes.toSortedArray();
}
@Override
public boolean hasHiddenRows() {
return !this.hiddenRowIndexes.isEmpty();
}
/**
* Checks the underlying layer if the row is hidden by another layer.
*
* @param rowIndex
* The index of the row whose hidden state should be checked
* @return <code>true</code> if the row at the given index is hidden in the
* underlying layer <code>false</code> if not.
*/
private boolean isHiddenInUnderlyingLayer(int rowIndex) {
IUniqueIndexLayer underlyingLayer = getUnderlyingLayer();
return (underlyingLayer.getRowPositionByIndex(rowIndex) == -1);
}
// expand / collapse
/**
* Expands or collapses the node at the given index coordinates according to
* its current state.
*
* @param columnIndex
* The column index of the node to handle.
* @param rowIndex
* The row index of the node to handle.
*/
public void expandOrCollapse(int columnIndex, int rowIndex) {
expandOrCollapse(columnIndex, rowIndex, -1);
}
/**
* Expands or collapses the node at the given index coordinates according to
* its current state. Expands to the given level, e.g. if toLevel 0 is
* given, only the first level of a row is expanded, given toLevel is 1 and
* a node in the first level should be expanded, that node will be expanded
* as well as all collapsed nodes in the second level for this object.
*
* @param columnIndex
* The column index of the node to handle.
* @param rowIndex
* The row index of the node to handle.
* @param toLevel
* 0 based hierarchy level to expand to. Will be ignored on
* collapse or if value is -1.
* <p>
* <b>Note:</b> This is the level to expand to, not the number of
* levels to expand from the expanded level.
* </p>
*/
public void expandOrCollapse(int columnIndex, int rowIndex, int toLevel) {
MutableIntList toProcess = IntLists.mutable.of(getChildIndexes(columnIndex, rowIndex)).sortThis();
HierarchicalTreeNode coord = new HierarchicalTreeNode(columnIndex, rowIndex, null);
if (this.collapsedNodes.contains(coord)) {
this.collapsedNodes.remove(coord);
// ensure that deeper level collapsed rows are not shown again
Range children = new Range(rowIndex, toProcess.get(toProcess.size() - 1));
int toLevelColumnIndex = (toLevel >= 0) ? this.nodeColumnMapping.get(toLevel) : -1;
for (Iterator<HierarchicalTreeNode> it = this.collapsedNodes.iterator(); it.hasNext();) {
HierarchicalTreeNode p = it.next();
// only handle if coord row is in range of toProcess
// and coord column is bigger than the toLevel
if (children.contains(p.rowIndex)) {
if (p.columnIndex > toLevelColumnIndex) {
toProcess.removeAll(getChildIndexes(p.columnIndex, p.rowIndex));
} else {
// we also remove the coord in case it will be expanded
it.remove();
}
}
}
this.hiddenRowIndexes.removeAll(toProcess);
invalidateCache();
fireLayerEvent(new ShowRowPositionsEvent(
this,
toProcess.primitiveStream()
.map(this::getRowPositionByIndex)
.filter(r -> r >= 0)
.sorted()
.toArray()));
} else {
// if the rowPos is negative, it is not visible because of hidden
// state in an underlying layer
int[] rowPositions = toProcess.primitiveStream()
.map(this::getRowPositionByIndex)
.filter(rowPos -> rowPos >= 0)
.sorted()
.toArray();
// collect the indexes that where really hidden in this layer
int[] hiddenIndexes = Arrays.stream(rowPositions)
.map(this::getRowIndexByPosition)
.sorted()
.toArray();
coord.rowObject = this.underlyingList.get(coord.rowIndex);
this.collapsedNodes.add(coord);
this.hiddenRowIndexes.addAll(toProcess);
invalidateCache();
fireLayerEvent(new HideRowPositionsEvent(this, rowPositions, hiddenIndexes));
}
}
/**
* Collapses all tree nodes.
*/
public void collapseAll() {
MutableIntList rowsToHide = IntLists.mutable.empty();
int[] nodeColumns = this.nodeColumnMapping.values().stream()
.sorted()
.mapToInt(Integer::intValue)
.toArray();
int columnIndex = nodeColumns[0];
int columnPosition = getColumnPositionByIndex(columnIndex);
ILayerCell cell = null;
for (int row = 0; row < getRowCount(); row++) {
cell = getCellByPosition(columnPosition, row);
if (cell.getRowSpan() > 1) {
int rowIndex = getRowIndexByPosition(row);
this.collapsedNodes.add(new HierarchicalTreeNode(columnIndex, rowIndex, this.underlyingList.get(rowIndex)));
rowsToHide.addAll(getChildIndexes(columnIndex, rowIndex));
row += (cell.getRowSpan()) - 1;
}
}
// search for node coords in deeper levels, except the leaf
for (int col = 1; col < (nodeColumns.length - 1); col++) {
columnIndex = nodeColumns[col];
columnPosition = getColumnPositionByIndex(columnIndex);
for (int row = 0; row < getRowCount(); row++) {
cell = getCellByPosition(columnPosition, row);
if (cell.getRowSpan() > 1) {
// only collect the coordinates, the rows are already hidden
// by the first level
int rowIndex = getRowIndexByPosition(row);
this.collapsedNodes.add(new HierarchicalTreeNode(columnIndex, rowIndex, this.underlyingList.get(rowIndex)));
row += (cell.getRowSpan()) - 1;
}
}
}
// if the rowPos is negative, it is not visible because of hidden
// state in an underlying layer
int[] rowPositions = rowsToHide.primitiveStream()
.map(this::getRowPositionByIndex)
.filter(rowPos -> rowPos >= 0)
.sorted()
.toArray();
// collect the indexes that where really hidden in this layer
int[] hiddenIndexes = Arrays.stream(rowPositions)
.map(this::getRowIndexByPosition)
.sorted()
.toArray();
this.hiddenRowIndexes.addAll(rowsToHide);
invalidateCache();
fireLayerEvent(new HideRowPositionsEvent(this, rowPositions, hiddenIndexes));
}
/**
* Expands all tree nodes.
*/
public void expandAll() {
MutableIntList rowsToShow = IntLists.mutable.ofAll(this.hiddenRowIndexes);
this.hiddenRowIndexes = IntSets.mutable.empty();
this.collapsedNodes.clear();
invalidateCache();
fireLayerEvent(new ShowRowPositionsEvent(this, rowsToShow.primitiveStream()
.map(this::getRowPositionByIndex)
.filter(r -> r >= 0)
.sorted()
.toArray()));
}
/**
* Expands all tree nodes starting from the first level to the specified
* level.
*
* @param toLevel
* 0 based hierarchy level to expand to. A negative value will be
* defaulted to 0 to at least expand the first level.
*/
public void expandAllToLevel(int toLevel) {
// at least the first level will always be expanded
int toLevelColumnIndex = (toLevel >= 0) ? this.nodeColumnMapping.get(toLevel) : 0;
// first remove all node coords that should be expanded
for (Iterator<HierarchicalTreeNode> it = this.collapsedNodes.iterator(); it.hasNext();) {
HierarchicalTreeNode coord = it.next();
// coords in the first level aswell as level matching
if (coord.columnIndex <= toLevelColumnIndex) {
it.remove();
}
}
// collect all rows of coords that are still collapsed
int[] remain = this.collapsedNodes.stream()
.map(node -> getChildIndexes(node.columnIndex, node.rowIndex))
.flatMapToInt(Arrays::stream)
.toArray();
// calculate the indexes that get visible afterwards
MutableIntSet toProcess = IntSets.mutable.ofAll(this.hiddenRowIndexes);
toProcess.removeAll(remain);
// set still collapsed as hidden row indexes
this.hiddenRowIndexes = IntSets.mutable.of(remain);
// invalidate cache and fire event for rows that got visible
invalidateCache();
fireLayerEvent(new ShowRowPositionsEvent(this, toProcess.toSortedArray()));
}
/**
* Calculates the child row indexes for the node at the given coordinates.
*
* @param columnIndex
* The column index of the node whose children are requested.
* @param rowIndex
* The row index of the node whose children are requested.
* @return The row indexes for the children of the node at the given
* coordinates.
*
* @since 2.0
*/
protected int[] getChildIndexes(int columnIndex, int rowIndex) {
if (rowIndex >= 0) {
HierarchicalWrapper rowObject = this.underlyingList.get(rowIndex);
// find children with same parents and same level object
int level = getLevelByColumnIndex(columnIndex);
Object levelObject = rowObject.getObject(level);
MutableIntList children = IntLists.mutable.empty();
for (int i = rowIndex + 1; i < this.underlyingList.size(); i++) {
HierarchicalWrapper child = this.underlyingList.get(i);
// as long as the level objects are the same, we have found a
// child
if (levelObject == child.getObject(level)) {
children.add(i);
} else {
// once the level object is not the same anymore, we can
// stop checking as another node is in the list
break;
}
}
return children.toArray();
}
return new int[0];
}
/**
* Find the top row index for the given row object and the given column
* index. Used to determine the row index of the top row so the node
* coordinates can be correctly calculated.
*
* @param columnIndex
* The column index to determine the level for the necessary
* level object checks.
* @param rowObject
* The row object that builds a node.
* @return The row index of the top most row of a spanned cell that
* determines a node.
*/
public int findTopRowIndex(int columnIndex, HierarchicalWrapper rowObject) {
int rowIndex = this.underlyingList.indexOf(rowObject);
int level = getLevelByColumnIndex(columnIndex);
Object levelObject = rowObject.getObject(level);
int topRowIndex = rowIndex - 1;
for (; topRowIndex >= 0; topRowIndex--) {
HierarchicalWrapper child = this.underlyingList.get(topRowIndex);
// as long as the level objects are the same, we have found a child
if (levelObject != child.getObject(level)) {
break;
}
}
return topRowIndex + 1;
}
/**
* Returns whether the cell at the given position is a collapsed node.
*
* @param columnPosition
* The column position of the cell to check.
* @param rowPosition
* The row position of the cell to check.
* @return <code>true</code> if the cell at the given coordinates is a
* collapsed node, <code>false</code> if not.
*/
public boolean isCollapsed(int columnPosition, int rowPosition) {
return this.collapsedNodes.contains(
new HierarchicalTreeNode(
getColumnIndexByPosition(columnPosition),
getRowIndexByPosition(rowPosition),
null));
}
/**
*
* @return The set of tree node coordinates based on indexes that are
* collapsed.
*/
public Set<HierarchicalTreeNode> getCollapsedNodes() {
return this.collapsedNodes;
}
// layer configuration
/**
* @return <code>true</code> if the column index is used to determine the
* tree column, <code>false</code> if the column position is used.
* Default is <code>false</code>.
*/
public boolean isUseTreeColumnIndex() {
return this.useTreeColumnIndex;
}
/**
* Configure whether (column index == 0) or (column position == 0) should be
* performed to identify the tree column.
*
* @param useTreeColumnIndex
* <code>true</code> if the column index should be used to
* determine the tree column, <code>false</code> if the column
* position should be used.
*/
public void setUseTreeColumnIndex(boolean useTreeColumnIndex) {
this.useTreeColumnIndex = useTreeColumnIndex;
}
/**
*
* @return <code>true</code> if the tree level header is shown,
* <code>false</code> if not.
*/
public boolean isShowTreeLevelHeader() {
return this.showTreeLevelHeader;
}
/**
* Configure whether the tree level header should be shown or not.
*
* @param show
* <code>true</code> if the tree level header should be shown,
* <code>false</code> if not.
*/
public void setShowTreeLevelHeader(boolean show) {
this.showTreeLevelHeader = show;
calculateLevelColumnHeaderPositions();
}
/**
*
* @return <code>true</code> if {@link #getConfigLabelsByPosition(int, int)}
* adds the {@link #COLLAPSED_CHILD} label to the
* {@link LabelStack}, <code>false</code> if that processing is not
* performed.
*/
public boolean isHandleCollapsedChildren() {
return this.handleCollapsedChildren;
}
/**
* Configure whether {@link #getConfigLabelsByPosition(int, int)} should add
* the {@link #COLLAPSED_CHILD} label to the {@link LabelStack}. Enabling
* this configuration allows a different configuration for child cells of
* collapsed rows, e.g. different styles like no content painter or
* different background.
*
* @param handleCollapsedChildren
* <code>true</code> if
* {@link #getConfigLabelsByPosition(int, int)} should add the
* {@link #COLLAPSED_CHILD} label to the {@link LabelStack},
* <code>false</code> if that processing should not be performed.
*/
public void setHandleCollapsedChildren(boolean handleCollapsedChildren) {
this.handleCollapsedChildren = handleCollapsedChildren;
}
/**
*
* @return <code>true</code> if {@link #getConfigLabelsByPosition(int, int)}
* adds the {@link #NO_OBJECT_IN_LEVEL} label to the
* {@link LabelStack}, <code>false</code> if that processing is not
* performed.
*/
public boolean isHandleNoObjectsInLevel() {
return this.handleNoObjectsInLevel;
}
/**
* Configure whether {@link #getConfigLabelsByPosition(int, int)} should add
* the {@link #NO_OBJECT_IN_LEVEL} label to the {@link LabelStack}. Enabling
* this configuration allows a different configuration for child cells of
* row objects that have no object for a child level, e.g. making those
* cells not editable and different styles like no content painter or
* different background.
* <p>
* <b>Note:</b> To identify level cells without a level object a deep
* inspection needs to be performed, which might cause a negative effect on
* the rendering performance. The handling is enabled by default, but if the
* underlying data model does not support empty level objects or the table
* configuration supports editing of such cells by automatically adding
* level objects on edit, it is recommended to disable this feature.
* </p>
*
* @param handleNoObjectsInLevel
* <code>true</code> if
* {@link #getConfigLabelsByPosition(int, int)} should add the
* {@link #NO_OBJECT_IN_LEVEL} label to the {@link LabelStack},
* <code>false</code> if that processing should not be performed.
*/
public void setHandleNoObjectsInLevel(boolean handleNoObjectsInLevel) {
this.handleNoObjectsInLevel = handleNoObjectsInLevel;
}
/**
*
* @return <code>true</code> if collapsed nodes are retained even if the
* corresponding row object is removed from the underlying list.
* <code>false</code> if the collapsed nodes are removed if the
* referenced row object is not contained anymore. Default is
* <code>true</code>.
*/
public boolean isRetainRemovedRowObjectNodes() {
return this.retainRemovedRowObjectNodes;
}
/**
* Configure whether collapsed nodes should be retained in the
* {@link #collapsedNodes} even if the row object is not contained in the
* underlying list anymore. This can for example happen when using a
* FilterList, as filtering will remove the row objects from that list.
* Without using a FilterList or supporting deleting rows, it is suggested
* to set this flag to <code>false</code> to avoid memory leaks on deleting
* an object.
*
* @param retainRemovedRowObjectNodes
* <code>true</code> to keep collapse nodes even if the
* corresponding row object is removed from the underlying list.
* <code>false</code> if the collapsed nodes should not contain
* references to removed row objects. Default is
* <code>true</code>.
*/
public void setRetainRemovedRowObjectNodes(boolean retainRemovedRowObjectNodes) {
this.retainRemovedRowObjectNodes = retainRemovedRowObjectNodes;
}
/**
* If {@link #retainRemovedRowObjectNodes} is set to <code>true</code>, the
* {@link #collapsedNodes} could contain references to row objects that were
* deleted meanwhile. To deal with this and avoid memory leaks, this method
* can be called to remove any collapsed node that has a reference to a non
* existing row object.
*/
public void cleanupRetainedCollapsedNodes() {
for (Iterator<HierarchicalTreeNode> it = this.collapsedNodes.iterator(); it.hasNext();) {
HierarchicalTreeNode node = it.next();
if (node.rowIndex < 0) {
it.remove();
}
}
}
/**
* If {@link #retainRemovedRowObjectNodes} is set to <code>true</code>, the
* {@link #collapsedNodes} could contain references to row objects that were
* deleted meanwhile. To deal with this and avoid memory leaks, this method
* can be called to remove a collapsed node that references the given row
* object which for example is removed from the underlying collection.
*
* @param rowObject
* The row object that was removed from the underlying list, to
* be able to cleanup a collapsed node reference.
*/
public void cleanupRetainedCollapsedNodes(HierarchicalWrapper rowObject) {
for (Iterator<HierarchicalTreeNode> it = this.collapsedNodes.iterator(); it.hasNext();) {
HierarchicalTreeNode node = it.next();
if (node.rowObject == rowObject) {
it.remove();
break;
}
}
}
/**
*
* @return <code>true</code> if collapsed nodes are expanded if they contain
* rows that are found on search. <code>false</code> if only the
* found row is made visible by still keeping the nodes collapsed.
* Default is <code>true</code>.
*/
public boolean isExpandOnSearch() {
return this.expandOnSearch;
}
/**
* Configure whether collapsed nodes should be expanded if they contain rows
* that are found on search or only the found row should be made visible by
* still keeping the nodes collapsed.
*
* @param expandOnSearch
* <code>true</code> if collapsed nodes should be expanded if
* they contain rows that are found on search. <code>false</code>
* if only the found row should be made visible by still keeping
* the nodes collapsed.
*/
public void setExpandOnSearch(boolean expandOnSearch) {
this.expandOnSearch = expandOnSearch;
}
/**
* Return whether columns in sub levels should be selected when selecting a
* level header cell or if only the cells in the same level should be
* selected. Default is <code>false</code>.
*
* @return <code>true</code> if columns in sub levels are selected when
* selecting a level header cell, <code>false</code> if only the
* cells in the same level are selected.
*/
public boolean isSelectSubLevels() {
return this.selectSubLevels;
}
/**
* Configure whether columns in sub levels should be selected when selecting
* a level header cell or if only the cells in the same level should be
* selected. Default is <code>false</code>.
*
* @param selectSubLevels
* <code>true</code> if columns in sub levels should be selected
* when selecting a level header cell, <code>false</code> if only
* the cells in the same level should be selected.
*/
public void setSelectSubLevels(boolean selectSubLevels) {
this.selectSubLevels = selectSubLevels;
}
private void calculateLevelColumnHeaderPositions() {
if (isShowTreeLevelHeader()) {
this.levelHeaderPositions = new int[this.nodeColumnMapping.size()];
int pos = 0;
for (Map.Entry<Integer, Integer> entry : this.nodeColumnMapping.entrySet()) {
int hiddenColumns = 0;
for (int i = (entry.getValue() - 1); i >= 0; i--) {
if (getUnderlyingLayer().getColumnPositionByIndex(i) < 0) {
hiddenColumns++;
}
}
this.levelHeaderPositions[pos++] = entry.getValue() + entry.getKey() - hiddenColumns;
}
} else {
this.levelHeaderPositions = new int[0];
}
}
@Override
protected IUniqueIndexLayer getUnderlyingLayer() {
return (IUniqueIndexLayer) this.underlyingLayer;
}
// Columns
@Override
public int getColumnCount() {
return super.getColumnCount() + this.levelHeaderPositions.length;
}
@Override
public int getColumnIndexByPosition(int columnPosition) {
if (columnPosition < 0
|| columnPosition >= getColumnCount()) {
return -1;
} else if (isLevelHeaderColumn(columnPosition)) {
// return a special negative number for a level header column which
// can then be interpreted by LayerUtil and
// getColumnPositionByIndex()
int level = 0;
for (int pos : this.levelHeaderPositions) {
level++;
if (pos == columnPosition) {
break;
}
}
return level * LayerUtil.ADDITIONAL_POSITION_MODIFIER;
}
return super.getColumnIndexByPosition(columnPosition);
}
@Override
public int getColumnPositionByIndex(int columnIndex) {
if (columnIndex < 0 && columnIndex % LayerUtil.ADDITIONAL_POSITION_MODIFIER == 0) {
return this.levelHeaderPositions[(columnIndex / LayerUtil.ADDITIONAL_POSITION_MODIFIER) - 1];
}
int columnPosition = super.getColumnPositionByIndex(columnIndex);
if (isShowTreeLevelHeader()) {
for (int pos : this.levelHeaderPositions) {
if (columnPosition >= pos) {
columnPosition++;
} else {
break;
}
}
}
return columnPosition;
}
@Override
public int localToUnderlyingColumnPosition(int localColumnPosition) {
if (isShowTreeLevelHeader()) {
int i = 0;
for (; i < this.levelHeaderPositions.length; i++) {
if (localColumnPosition < this.levelHeaderPositions[i]) {
break;
}
}
return super.localToUnderlyingColumnPosition(localColumnPosition - i);
}
return super.localToUnderlyingColumnPosition(localColumnPosition);
}
@Override
public int underlyingToLocalColumnPosition(ILayer sourceUnderlyingLayer, int underlyingColumnPosition) {
if (isShowTreeLevelHeader()) {
int pos = sourceUnderlyingLayer.getColumnIndexByPosition(underlyingColumnPosition);
return getColumnPositionByIndex(pos);
}
return super.underlyingToLocalColumnPosition(sourceUnderlyingLayer, underlyingColumnPosition);
}
@Override
public Collection<Range> underlyingToLocalColumnPositions(ILayer sourceUnderlyingLayer, Collection<Range> underlyingColumnPositionRanges) {
if (isShowTreeLevelHeader()) {
Collection<Range> localColumnPositionRanges = new ArrayList<>();
for (Range underlyingColumnPositionRange : underlyingColumnPositionRanges) {
int start = underlyingToLocalColumnPosition(sourceUnderlyingLayer, underlyingColumnPositionRange.start);
// test end with 1 offset to avoid -1 result from asking out of
// range
int end = underlyingToLocalColumnPosition(sourceUnderlyingLayer, underlyingColumnPositionRange.end - 1);
end++;
// if end - 1 (exclusive position) is a level header column,
// reduce the end
for (int pos : this.levelHeaderPositions) {
if (pos == (end - 1)) {
end--;
}
}
localColumnPositionRanges.add(new Range(start, end));
}
return localColumnPositionRanges;
}
return super.underlyingToLocalColumnPositions(sourceUnderlyingLayer, underlyingColumnPositionRanges);
}
/**
* Checks if the column at the given from position can be reordered to the
* given to position. Mainly performs a check if the column at the given
* from position is a level header (which can not be reordered) or if the
* reordering would mean to move a column into a different level, which is
* also forbidden.
*
* @param fromColumnPosition
* The position of the column to reorder.
* @param toColumnPosition
* The position to move the column to.
* @return <code>true</code> if reordering would be valid,
* <code>false</code> in case the column at the from position is a
* level header column or the move would be a level change.
*/
public boolean isValidTargetColumnPosition(int fromColumnPosition, int toColumnPosition) {
int fromIndex = getColumnIndexByPosition(fromColumnPosition);
int toIndex = getColumnIndexByPosition(toColumnPosition);
// it is not allowed to drag a level header column and the index of a
// level header is -1
if (fromIndex < 0) {
return false;
}
if (toIndex < 0 && fromColumnPosition < toColumnPosition) {
// get the position to the left of the level header
toIndex = getColumnIndexByPosition(toColumnPosition - 1);
} else if (toIndex < 0 && fromColumnPosition > toColumnPosition) {
return false;
}
int fromLevel = getLevelByColumnIndex(fromIndex);
int toLevel = getLevelByColumnIndex(toIndex);
if (fromLevel != toLevel && isShowTreeLevelHeader()) {
return false;
} else if (fromLevel != toLevel && !isShowTreeLevelHeader()) {
// if no tree level headers are shown, test for the column to the
// left to check for the right level border
toLevel = getLevelByColumnIndex(toIndex - 1);
if (fromLevel != toLevel) {
return false;
}
}
return true;
}
// Width
/**
*
* @return The column width of the level header columns in pixel.
*/
public int getLevelHeaderWidth() {
return this.levelHeaderWidth;
}
/**
* Set the column width for the level header columns.
*
* @param width
* The column width in pixels that should be used for the level
* header columns.
*/
public void setLevelHeaderWidth(int width) {
this.levelHeaderWidth = width;
}
/**
*
* @return The level header width in DPI.
*/
protected int getScaledLevelHeaderWidth() {
if (this.dpiConverter != null) {
return this.dpiConverter.convertPixelToDpi(this.levelHeaderWidth);
}
return this.levelHeaderWidth;
}
@Override
public int getWidth() {
return super.getWidth() + (this.levelHeaderPositions.length * getScaledLevelHeaderWidth());
}
@Override
public int getPreferredWidth() {
return super.getPreferredWidth() + (this.levelHeaderPositions.length * getScaledLevelHeaderWidth());
}
@Override
public int getColumnWidthByPosition(int columnPosition) {
if (isShowTreeLevelHeader() && isLevelHeaderColumn(columnPosition)) {
return getScaledLevelHeaderWidth();
}
return super.getColumnWidthByPosition(columnPosition);
}
@Override
public int getStartXOfColumnPosition(int columnPosition) {
if (isShowTreeLevelHeader()) {
if (isLevelHeaderColumn(columnPosition)) {
// get the underlying start of the next column and reduce the
// header width afterwards
if (columnPosition + 1 < getColumnCount()) {
int start = getStartXOfColumnPosition(columnPosition + 1);
return start - getScaledLevelHeaderWidth();
} else {
int start = getStartXOfColumnPosition(columnPosition - 1);
return start + getColumnWidthByPosition(columnPosition - 1);
}
}
int start = super.getStartXOfColumnPosition(columnPosition);
for (int pos : this.levelHeaderPositions) {
if (columnPosition >= pos) {
start += getScaledLevelHeaderWidth();
} else {
break;
}
}
return start;
}
return super.getStartXOfColumnPosition(columnPosition);
}
@Override
public int getColumnPositionByX(int x) {
return LayerUtil.getColumnPositionByX(this, x);
}
@Override
public Collection<String> getProvidedLabels() {
Collection<String> result = super.getProvidedLabels();
result.add(TreeLayer.TREE_COLUMN_CELL);
result.add(DefaultTreeLayerConfiguration.TREE_LEAF_CONFIG_TYPE);
result.add(DefaultTreeLayerConfiguration.TREE_COLLAPSED_CONFIG_TYPE);
result.add(DefaultTreeLayerConfiguration.TREE_EXPANDED_CONFIG_TYPE);
result.add(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + "0"); //$NON-NLS-1$
result.add(LEVEL_HEADER_CELL);
result.add(COLLAPSED_CHILD);
result.add(NO_OBJECT_IN_LEVEL);
return result;
}
/**
* Simple node for remembering collapsed nodes. Carries also the row object
* for being able to update the index based coordinates in case of
* structural changes. Otherwise only the column and row index are of
* interest.
*/
public static class HierarchicalTreeNode {
public final int columnIndex;
public final int rowIndex;
/**
* The row object reference needed to recalculate the rowIndex on
* structural changes.
*/
public HierarchicalWrapper rowObject;
public HierarchicalTreeNode(int columnIndex, int rowIndex, HierarchicalWrapper rowObject) {
this.columnIndex = columnIndex;
this.rowIndex = rowIndex;
this.rowObject = rowObject;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + this.columnIndex;
result = prime * result + this.rowIndex;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
HierarchicalTreeNode other = (HierarchicalTreeNode) obj;
if (this.columnIndex != other.columnIndex)
return false;
if (this.rowIndex != other.rowIndex)
return false;
return true;
}
@Override
public String toString() {
return "HierarchicalTreeNode [columnIndex=" + this.columnIndex + ", rowIndex=" + this.rowIndex + "]"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
}
}
}