| /***************************************************************************** |
| * 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$ |
| } |
| |
| } |
| } |