]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.browsing.ui.nattable/src/org/simantics/browsing/ui/nattable/override/TreeLayer2.java
Performance optimizations for NatTableGraphExplorer
[simantics/platform.git] / bundles / org.simantics.browsing.ui.nattable / src / org / simantics / browsing / ui / nattable / override / TreeLayer2.java
1 /*******************************************************************************
2  * Copyright (c) 2012 Original authors and others.
3  * All rights reserved. This program and the accompanying materials
4  * are made available under the terms of the Eclipse Public License v1.0
5  * which accompanies this distribution, and is available at
6  * http://www.eclipse.org/legal/epl-v10.html
7  * 
8  * Contributors:
9  *     Original authors and others - initial API and implementation
10  ******************************************************************************/
11 package org.simantics.browsing.ui.nattable.override;
12
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.List;
16
17 import org.eclipse.nebula.widgets.nattable.command.ILayerCommand;
18 import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
19 import org.eclipse.nebula.widgets.nattable.hideshow.command.MultiRowHideCommand;
20 import org.eclipse.nebula.widgets.nattable.hideshow.command.RowHideCommand;
21 import org.eclipse.nebula.widgets.nattable.hideshow.event.HideRowPositionsEvent;
22 import org.eclipse.nebula.widgets.nattable.hideshow.event.ShowRowPositionsEvent;
23 import org.eclipse.nebula.widgets.nattable.layer.IUniqueIndexLayer;
24 import org.eclipse.nebula.widgets.nattable.layer.LabelStack;
25 import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
26 import org.eclipse.nebula.widgets.nattable.painter.cell.BackgroundPainter;
27 import org.eclipse.nebula.widgets.nattable.painter.cell.CellPainterWrapper;
28 import org.eclipse.nebula.widgets.nattable.painter.cell.ICellPainter;
29 import org.eclipse.nebula.widgets.nattable.tree.ITreeRowModel;
30 import org.eclipse.nebula.widgets.nattable.tree.TreeLayer;
31 import org.eclipse.nebula.widgets.nattable.tree.config.DefaultTreeLayerConfiguration;
32 import org.eclipse.nebula.widgets.nattable.tree.config.TreeConfigAttributes;
33 import org.eclipse.nebula.widgets.nattable.tree.painter.IndentedTreeImagePainter;
34
35 import it.unimi.dsi.fastutil.ints.IntRBTreeSet;
36
37 public class TreeLayer2 extends AbstractRowHideShowLayer2 {
38
39     //private static final Log log = LogFactory.getLog(TreeLayer.class);
40
41     public static final String TREE_COLUMN_CELL = "TREE_COLUMN_CELL"; //$NON-NLS-1$
42
43     public static final int TREE_COLUMN_NUMBER = 0;
44
45     /**
46      * Flag to configure whether the tree column should be identified by
47      * position or by index. Default is position.
48      */
49     private boolean useTreeColumnIndex = false;
50
51     /**
52      * The ITreeRowModelListener that is used to get information about the tree
53      * structure.
54      */
55     private final ITreeRowModel<?> treeRowModel;
56
57     /**
58      * Collection of all row indexes that are hidden if tree nodes are
59      * collapsed.
60      * <p>
61      * Note: This collection is only in use if the used {@link ITreeRowModel}
62      * implementation is returning the row indexes of affected rows on
63      * expand/collapse. There are also implementations that use another approach
64      * where the hide/show approach is not used (e.g. GlazedListTreeRowModel)
65      * </p>
66      */
67         private final IntRBTreeSet hiddenRowIndexes = new IntRBTreeSet();
68
69     /**
70      * The IndentedTreeImagePainter that paints indentation to the left of the
71      * configured base painter and icons for expand/collapse if possible, to
72      * render tree structure accordingly.
73      */
74     private IndentedTreeImagePainter indentedTreeImagePainter;
75
76     /**
77      * Creates a TreeLayer instance based on the given information. Will use a
78      * default IndentedTreeImagePainter that uses 10 pixels for indentation and
79      * simple + and - icons for expand/collapse icons. It also uses the
80      * DefaultTreeLayerConfiguration.
81      *
82      * @param underlyingLayer
83      *            The underlying layer on whose top this layer will be set.
84      * @param treeRowModel
85      *            The ITreeRowModelListener that is used to get information
86      *            about the tree structure.
87      */
88     public TreeLayer2(IUniqueIndexLayer underlyingLayer, ITreeRowModel<?> treeRowModel) {
89         this(underlyingLayer, treeRowModel, new IndentedTreeImagePainter());
90     }
91
92     /**
93      * Creates a TreeLayer instance based on the given information. Allows to
94      * specify the IndentedTreeImagePainter while using the
95      * DefaultTreeLayerConfiguration.
96      *
97      * @param underlyingLayer
98      *            The underlying layer on whose top this layer will be set.
99      * @param treeRowModel
100      *            The ITreeRowModelListener that is used to get information
101      *            about the tree structure.
102      * @param indentedTreeImagePainter
103      *            The IndentedTreeImagePainter that paints indentation to the
104      *            left of the configured base painter and icons for
105      *            expand/collapse if possible, to render tree structure
106      *            accordingly.
107      */
108     public TreeLayer2(
109             IUniqueIndexLayer underlyingLayer,
110             ITreeRowModel<?> treeRowModel,
111             IndentedTreeImagePainter indentedTreeImagePainter) {
112         this(underlyingLayer, treeRowModel, indentedTreeImagePainter, true);
113     }
114
115     /**
116      * Creates a TreeLayer instance based on the given information. Will use a
117      * default IndentedTreeImagePainter that uses 10 pixels for indentation and
118      * simple + and - icons for expand/collapse icons.
119      *
120      * @param underlyingLayer
121      *            The underlying layer on whose top this layer will be set.
122      * @param treeRowModel
123      *            The ITreeRowModelListener that is used to get information
124      *            about the tree structure.
125      * @param useDefaultConfiguration
126      *            <code>true</code> to use the DefaultTreeLayerConfiguration,
127      *            <code>false</code> if you want to specify your own
128      *            configuration.
129      */
130     public TreeLayer2(
131             IUniqueIndexLayer underlyingLayer,
132             ITreeRowModel<?> treeRowModel,
133             boolean useDefaultConfiguration) {
134         this(underlyingLayer,
135                 treeRowModel,
136                 new IndentedTreeImagePainter(),
137                 useDefaultConfiguration);
138     }
139
140     /**
141      * Creates a TreeLayer instance based on the given information.
142      *
143      * @param underlyingLayer
144      *            The underlying layer on whose top this layer will be set.
145      * @param treeRowModel
146      *            The ITreeRowModelListener that is used to get information
147      *            about the tree structure.
148      * @param indentedTreeImagePainter
149      *            The IndentedTreeImagePainter that paints indentation to the
150      *            left of the configured base painter and icons for
151      *            expand/collapse if possible, to render tree structure
152      *            accordingly.
153      * @param useDefaultConfiguration
154      *            <code>true</code> to use the DefaultTreeLayerConfiguration,
155      *            <code>false</code> if you want to specify your own
156      *            configuration.
157      */
158     public TreeLayer2(
159             IUniqueIndexLayer underlyingLayer,
160             ITreeRowModel<?> treeRowModel,
161             IndentedTreeImagePainter indentedTreeImagePainter,
162             boolean useDefaultConfiguration) {
163
164         super(underlyingLayer);
165         this.treeRowModel = treeRowModel;
166
167         if (useDefaultConfiguration) {
168             addConfiguration(new DefaultTreeLayerConfiguration2(this));
169         }
170
171         this.indentedTreeImagePainter = indentedTreeImagePainter;
172
173         registerCommandHandler(new TreeExpandCollapseCommandHandler(this));
174         registerCommandHandler(new TreeCollapseAllCommandHandler(this));
175         registerCommandHandler(new TreeExpandAllCommandHandler(this));
176         registerCommandHandler(new TreeExpandToLevelCommandHandler(this));
177     }
178
179     @Override
180     public LabelStack getConfigLabelsByPosition(int columnPosition, int rowPosition) {
181         LabelStack configLabels = super.getConfigLabelsByPosition(columnPosition, rowPosition);
182
183         if (isTreeColumn(columnPosition)) {
184             configLabels.addLabelOnTop(TREE_COLUMN_CELL);
185
186             int rowIndex = getRowIndexByPosition(rowPosition);
187             configLabels.addLabelOnTop(
188                     DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + this.treeRowModel.depth(rowIndex));
189             if (!this.treeRowModel.hasChildren(rowIndex)) {
190                 configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_LEAF_CONFIG_TYPE);
191             } else {
192                 if (this.treeRowModel.isCollapsed(rowIndex)) {
193                     configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_COLLAPSED_CONFIG_TYPE);
194                 } else {
195                     configLabels.addLabelOnTop(DefaultTreeLayerConfiguration.TREE_EXPANDED_CONFIG_TYPE);
196                 }
197             }
198         }
199         return configLabels;
200     }
201
202     /**
203      * @return The ITreeRowModelListener that is used to get information about
204      *         the tree structure.
205      */
206     public ITreeRowModel<?> getModel() {
207         return this.treeRowModel;
208     }
209
210     /**
211      * @return The IndentedTreeImagePainter that paints indentation to the left
212      *         of the configured base painter and icons for expand/collapse if
213      *         possible, to render tree structure accordingly.
214      *
215      * @deprecated since 1.1 the configured TreeImagePainter should be used
216      *             instead of the hard referenced one
217      */
218     @Deprecated
219     public IndentedTreeImagePainter getIndentedTreeImagePainter() {
220         return this.indentedTreeImagePainter;
221     }
222
223     /**
224      * @return The ICellPainter that is used to paint the images in the tree by
225      *         the IndentedTreeImagePainter. Usually it is some type of
226      *         TreeImagePainter that paints expand/collapse/leaf icons regarding
227      *         the node state.<br>
228      *         Can be <code>null</code> if set explicitly to the
229      *         IndentedTreeImagePainter!
230      *
231      * @deprecated since 1.1 the configured TreeImagePainter should be used
232      *             instead of the hard referenced one
233      */
234     @Deprecated
235     public ICellPainter getTreeImagePainter() {
236         return this.indentedTreeImagePainter != null ? this.indentedTreeImagePainter
237                 .getTreeImagePainter() : null;
238     }
239
240     /**
241      * @param columnPosition
242      *            The column position to check.
243      * @return <code>true</code> if the given column position is the tree
244      *         column, <code>false</code> if not.
245      */
246     private boolean isTreeColumn(int columnPosition) {
247         if (this.useTreeColumnIndex)
248             return getColumnIndexByPosition(columnPosition) == TREE_COLUMN_NUMBER;
249
250         return columnPosition == TREE_COLUMN_NUMBER;
251     }
252
253     @Override
254     public ICellPainter getCellPainter(
255             int columnPosition, int rowPosition,
256             ILayerCell cell, IConfigRegistry configRegistry) {
257         ICellPainter cellPainter = super.getCellPainter(
258                 columnPosition, rowPosition, cell, configRegistry);
259
260         if (cell.getConfigLabels().hasLabel(TREE_COLUMN_CELL)) {
261
262             ICellPainter treeCellPainter = configRegistry.getConfigAttribute(
263                     TreeConfigAttributes.TREE_STRUCTURE_PAINTER,
264                     cell.getDisplayMode(),
265                     cell.getConfigLabels().getLabels());
266
267             if (treeCellPainter != null) {
268                 ICellPainter innerWrapper = treeCellPainter;
269                 IndentedTreeImagePainter treePainter = null;
270                 if (innerWrapper instanceof IndentedTreeImagePainter) {
271                     treePainter = (IndentedTreeImagePainter) innerWrapper;
272                 } else {
273                     while (treePainter == null
274                             && innerWrapper != null
275                             && innerWrapper instanceof CellPainterWrapper
276                             && ((CellPainterWrapper) innerWrapper).getWrappedPainter() != null) {
277
278                         innerWrapper = ((CellPainterWrapper) innerWrapper).getWrappedPainter();
279                         if (innerWrapper instanceof IndentedTreeImagePainter) {
280                             treePainter = (IndentedTreeImagePainter) innerWrapper;
281                         }
282                     }
283                 }
284
285                 if (treePainter != null) {
286                     treePainter.setBaseCellPainter(cellPainter);
287                     cellPainter = treeCellPainter;
288                 } else {
289                     // log error
290 //                    log.warn("There is no IndentedTreeImagePainter found for TREE_STRUCTURE_PAINTER, " //$NON-NLS-1$
291 //                            + "using local configured IndentedTreeImagePainter as fallback"); //$NON-NLS-1$
292                     // fallback
293                     this.indentedTreeImagePainter.setBaseCellPainter(cellPainter);
294                     cellPainter = new BackgroundPainter(this.indentedTreeImagePainter);
295                 }
296             } else {
297                 // backwards compatibility fallback
298                 this.indentedTreeImagePainter.setBaseCellPainter(cellPainter);
299                 cellPainter = new BackgroundPainter(this.indentedTreeImagePainter);
300             }
301         }
302
303         return cellPainter;
304     }
305
306     @Override
307     public boolean isRowIndexHidden(int rowIndex) {
308         return this.hiddenRowIndexes.contains(rowIndex)
309                 || isHiddenInUnderlyingLayer(rowIndex);
310     }
311
312     @Override
313     public Collection<Integer> getHiddenRowIndexes() {
314         return this.hiddenRowIndexes;
315     }
316
317     /**
318      * Performs an expand/collapse action dependent on the current state of the
319      * tree node at the given row index.
320      *
321      * @param parentIndex
322      *            The index of the row that shows the tree node for which the
323      *            expand/collapse action should be performed.
324      */
325     public void expandOrCollapseIndex(int parentIndex) {
326         if (this.treeRowModel.isCollapsed(parentIndex)) {
327             expandTreeRow(parentIndex);
328         } else {
329             collapseTreeRow(parentIndex);
330         }
331     }
332
333     /**
334      * Collapses the tree node for the given row index.
335      *
336      * @param parentIndex
337      *            The index of the row that shows the node that should be
338      *            collapsed
339      */
340     public void collapseTreeRow(int parentIndex) {
341         List<Integer> rowIndexes = this.treeRowModel.collapse(parentIndex);
342         List<Integer> rowPositions = new ArrayList<Integer>();
343         for (Integer rowIndex : rowIndexes) {
344             int rowPos = getRowPositionByIndex(rowIndex);
345             // if the rowPos is negative, it is not visible because of hidden
346             // state in an underlying layer
347             if (rowPos >= 0) {
348                 rowPositions.add(rowPos);
349             }
350         }
351         this.hiddenRowIndexes.addAll(rowIndexes);
352         invalidateCache();
353         fireLayerEvent(new HideRowPositionsEvent(this, rowPositions));
354     }
355
356     /**
357      * Collapses all tree nodes in the tree.
358      */
359     public void collapseAll() {
360         List<Integer> rowIndexes = this.treeRowModel.collapseAll();
361         List<Integer> rowPositions = new ArrayList<Integer>();
362         for (Integer rowIndex : rowIndexes) {
363             int rowPos = getRowPositionByIndex(rowIndex);
364             // if the rowPos is negative, it is not visible because of hidden
365             // state in an underlying layer
366             if (rowPos >= 0) {
367                 rowPositions.add(rowPos);
368             }
369         }
370         this.hiddenRowIndexes.addAll(rowIndexes);
371         invalidateCache();
372         fireLayerEvent(new HideRowPositionsEvent(this, rowPositions));
373     }
374
375     /**
376      * Expands the tree node for the given row index.
377      *
378      * @param parentIndex
379      *            The index of the row that shows the node that should be
380      *            expanded
381      */
382     public void expandTreeRow(int parentIndex) {
383         List<Integer> rowIndexes = this.treeRowModel.expand(parentIndex);
384         // Bug 432865: iterating and removing every single item is faster than
385         // removeAll()
386         if (rowIndexes.isEmpty())
387                 return;
388         for (final Integer expandedChildRowIndex : rowIndexes) {
389             this.hiddenRowIndexes.remove(expandedChildRowIndex);
390         }
391         invalidateCache();
392         fireLayerEvent(new ShowRowPositionsEvent(this, rowIndexes));
393     }
394
395     /**
396      * Expands the tree node for the given row index in the tree to a certain
397      * level.
398      *
399      * @param parentIndex
400      *            The index of the row that shows the node that should be
401      *            expanded
402      * @param level
403      *            The level to which the tree node should be expanded.
404      */
405     public void expandTreeRowToLevel(int parentIndex, int level) {
406         List<Integer> rowIndexes = this.treeRowModel.expandToLevel(parentIndex, level);
407         // Bug 432865: iterating and removing every single item is faster than
408         // removeAll()
409         for (final Integer expandedChildRowIndex : rowIndexes) {
410             this.hiddenRowIndexes.remove(expandedChildRowIndex);
411         }
412         invalidateCache();
413         fireLayerEvent(new ShowRowPositionsEvent(this, rowIndexes));
414     }
415
416     /**
417      * Expands all tree nodes in the tree.
418      */
419     public void expandAll() {
420         List<Integer> rowIndexes = this.treeRowModel.expandAll();
421         this.hiddenRowIndexes.clear();
422         invalidateCache();
423         fireLayerEvent(new ShowRowPositionsEvent(this, rowIndexes));
424     }
425
426     /**
427      * Expands all tree nodes in the tree to a certain level.
428      *
429      * @param level
430      *            The level to which the tree node should be expanded.
431      */
432     public void expandAllToLevel(int level) {
433         List<Integer> rowIndexes = this.treeRowModel.expandToLevel(level);
434         // Bug 432865: iterating and removing every single item is faster than
435         // removeAll()
436 //        for (final Integer expandedChildRowIndex : rowIndexes) {
437 //            this.hiddenRowIndexes.remove(expandedChildRowIndex);
438 //        }
439         if (rowIndexes == null)
440                 return;
441         for (int i = rowIndexes.size()-1; i>=0; i--) {
442                 this.hiddenRowIndexes.remove(rowIndexes.get(i));
443         }
444         invalidateCache();
445         fireLayerEvent(new ShowRowPositionsEvent(this, rowIndexes));
446     }
447
448     /**
449      * Checks the underlying layer if the row is hidden by another layer.
450      *
451      * @param rowIndex
452      *            The index of the row whose hidden state should be checked
453      * @return <code>true</code> if the row at the given index is hidden in the
454      *         underlying layer <code>false</code> if not.
455      */
456     private boolean isHiddenInUnderlyingLayer(int rowIndex) {
457         IUniqueIndexLayer underlyingLayer = (IUniqueIndexLayer) getUnderlyingLayer();
458         return (underlyingLayer.getRowPositionByIndex(rowIndex) == -1);
459     }
460
461     @Override
462     public boolean doCommand(ILayerCommand command) {
463         // special command transformations are needed to hide also child nodes
464         if (command instanceof RowHideCommand) {
465             return handleRowHideCommand((RowHideCommand) command);
466         } else if (command instanceof MultiRowHideCommand) {
467             return handleMultiRowHideCommand((MultiRowHideCommand) command);
468         }
469         return super.doCommand(command);
470     }
471
472     /**
473      * Checks if the given command tries to hide a row that is a node that is
474      * not collapsed and has children. In that case also the child rows need to
475      * be hidden.
476      *
477      * @param command
478      *            The {@link RowHideCommand} to process
479      * @return <code>true</code> if the command has been handled,
480      *         <code>false</code> otherwise
481      */
482     protected boolean handleRowHideCommand(RowHideCommand command) {
483         // transform position to index
484         if (command.convertToTargetLayer(this)) {
485             int rowIndex = getRowIndexByPosition(command.getRowPosition());
486             if (this.treeRowModel.hasChildren(rowIndex)
487                     && !this.treeRowModel.isCollapsed(rowIndex)) {
488                 List<Integer> childIndexes = this.treeRowModel.getChildIndexes(rowIndex);
489                 int[] childPositions = new int[childIndexes.size() + 1];
490                 childPositions[0] = command.getRowPosition();
491                 for (int i = 1; i < childIndexes.size() + 1; i++) {
492                     int childPos = getRowPositionByIndex(childIndexes.get(i - 1));
493                     childPositions[i] = childPos;
494                 }
495                 return super.doCommand(new MultiRowHideCommand(this, childPositions));
496             }
497         }
498         return super.doCommand(command);
499     }
500
501     /**
502      * Checks if the given command tries to hide rows that are nodes that are
503      * not collapsed and have children. In that case also the child rows need to
504      * be hidden.
505      *
506      * @param command
507      *            The {@link MultiRowHideCommand} to process
508      * @return <code>true</code> if the command has been handled,
509      *         <code>false</code> otherwise
510      */
511     protected boolean handleMultiRowHideCommand(MultiRowHideCommand command) {
512         // transform position to index
513         if (command.convertToTargetLayer(this)) {
514             List<Integer> rowPositionsToHide = new ArrayList<Integer>();
515             for (Integer rowPos : command.getRowPositions()) {
516                 rowPositionsToHide.add(rowPos);
517                 int rowIndex = getRowIndexByPosition(rowPos);
518                 if (this.treeRowModel.hasChildren(rowIndex)
519                         && !this.treeRowModel.isCollapsed(rowIndex)) {
520                     List<Integer> childIndexes = this.treeRowModel.getChildIndexes(rowIndex);
521                     for (Integer childIndex : childIndexes) {
522                         rowPositionsToHide.add(getRowPositionByIndex(childIndex));
523                     }
524                 }
525             }
526
527             int[] childPositions = new int[rowPositionsToHide.size()];
528             for (int i = 0; i < rowPositionsToHide.size(); i++) {
529                 childPositions[i] = rowPositionsToHide.get(i);
530             }
531             return super.doCommand(new MultiRowHideCommand(this, childPositions));
532         }
533         return super.doCommand(command);
534     }
535
536     /**
537      * @return <code>true</code> if the column index is used to determine the
538      *         tree column, <code>false</code> if the column position is used.
539      *         Default is <code>false</code>.
540      */
541     public boolean isUseTreeColumnIndex() {
542         return this.useTreeColumnIndex;
543     }
544
545     /**
546      * Configure whether (column index == 0) or (column position == 0) should be
547      * performed to identify the tree column.
548      *
549      * @param useTreeColumnIndex
550      *            <code>true</code> if the column index should be used to
551      *            determine the tree column, <code>false</code> if the column
552      *            position should be used.
553      */
554     public void setUseTreeColumnIndex(boolean useTreeColumnIndex) {
555         this.useTreeColumnIndex = useTreeColumnIndex;
556     }
557
558     /**
559      * @since 1.4
560      */
561     @Override
562     public Collection<String> getProvidedLabels() {
563         Collection<String> result = super.getProvidedLabels();
564
565         result.add(TreeLayer.TREE_COLUMN_CELL);
566         result.add(DefaultTreeLayerConfiguration.TREE_LEAF_CONFIG_TYPE);
567         result.add(DefaultTreeLayerConfiguration.TREE_COLLAPSED_CONFIG_TYPE);
568         result.add(DefaultTreeLayerConfiguration.TREE_EXPANDED_CONFIG_TYPE);
569         // configure 5 levels to be configurable via CSS
570         // if you need more you need to override this method
571         result.add(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + "0"); //$NON-NLS-1$
572         result.add(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + "1"); //$NON-NLS-1$
573         result.add(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + "2"); //$NON-NLS-1$
574         result.add(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + "3"); //$NON-NLS-1$
575         result.add(DefaultTreeLayerConfiguration.TREE_DEPTH_CONFIG_TYPE + "4"); //$NON-NLS-1$
576
577         return result;
578     }
579 }