209fb51d5dbcc29c1954453bbb277e16e1118725
[idea/community.git] / platform / platform-api / src / com / intellij / ui / treeStructure / treetable / TreeTable.java
1 /*
2  * Copyright 2000-2016 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ui.treeStructure.treetable;
17
18 import com.intellij.ui.TableUtil;
19 import com.intellij.ui.table.JBTable;
20 import com.intellij.util.ObjectUtils;
21 import com.intellij.util.ui.JBUI;
22 import com.intellij.util.ui.accessibility.ScreenReader;
23
24 import javax.swing.*;
25 import javax.swing.event.ListSelectionEvent;
26 import javax.swing.event.ListSelectionListener;
27 import javax.swing.tree.DefaultTreeSelectionModel;
28 import javax.swing.tree.TreeCellRenderer;
29 import javax.swing.tree.TreePath;
30 import java.awt.*;
31 import java.awt.event.KeyEvent;
32 import java.awt.event.MouseEvent;
33 import java.beans.PropertyChangeEvent;
34 import java.beans.PropertyChangeListener;
35 import java.util.*;
36 import java.util.List;
37
38 /**
39  * This example shows how to create a simple JTreeTable component,
40  * by using a JTree as a renderer (and editor) for the cells in a
41  * particular column in the JTable.
42  *
43  * @version 1.2 10/27/98
44  *
45  * @author Philip Milne
46  * @author Scott Violet
47  */
48 public class TreeTable extends JBTable {
49   /** A subclass of JTree. */
50   private TreeTableTree myTree;
51   private TreeTableModel myTableModel;
52   private PropertyChangeListener myTreeRowHeightPropertyListener;
53   // If a screen reader is present, it is better to let the left/right cursor keys
54   // be routed to the JTable, as opposed to expand/collapse tree nodes.
55   private boolean myProcessCursorKeys = !ScreenReader.isActive();
56
57   public TreeTable(TreeTableModel treeTableModel) {
58     super();
59     setModel(treeTableModel);
60   }
61
62   @SuppressWarnings({"MethodOverloadsMethodOfSuperclass"})
63   public void setModel(TreeTableModel treeTableModel) {// Create the tree. It will be used as a renderer and editor.
64     if (myTree != null) {
65       myTree.removePropertyChangeListener(JTree.ROW_HEIGHT_PROPERTY, myTreeRowHeightPropertyListener);
66     }
67     myTree = new TreeTableTree(treeTableModel, this);
68     setRowHeight(myTree.getRowHeight());
69     myTreeRowHeightPropertyListener = new PropertyChangeListener() {
70       public void propertyChange(PropertyChangeEvent evt) {
71         int treeRowHeight = myTree.getRowHeight();
72         if (treeRowHeight == getRowHeight()) return;
73         setRowHeight(treeRowHeight);
74       }
75     };
76     myTree.addPropertyChangeListener(JTree.ROW_HEIGHT_PROPERTY, myTreeRowHeightPropertyListener);
77
78     // Install a tableModel representing the visible rows in the tree.
79     setTableModel(treeTableModel);
80     // Force the JTable and JTree to share their row selection models.
81     ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
82     myTree.setSelectionModel(selectionWrapper);
83     setSelectionModel(selectionWrapper.getListSelectionModel());
84
85     // Install the tree editor renderer and editor.
86     TreeTableCellRenderer treeTableCellRenderer = createTableRenderer(treeTableModel);
87     setDefaultRenderer(TreeTableModel.class, treeTableCellRenderer);
88     setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor(treeTableCellRenderer));
89
90     // No grid.
91     setShowGrid(false);
92
93     // No intercell spacing
94     setIntercellSpacing(new Dimension(0, 0));
95
96     // And update the height of the trees row to match that of the table.
97     if (myTree.getRowHeight() < 1) {
98       setRowHeight(JBUI.scale(18));  // Metal looks better like this.
99     }
100     else {
101       setRowHeight(getRowHeight());
102     }
103   }
104
105   public TreeTableModel getTableModel() {
106     return myTableModel;
107   }
108
109   public void setTableModel(TreeTableModel treeTableModel) {
110     myTableModel = treeTableModel;
111     super.setModel(adapt(treeTableModel));
112   }
113
114   protected TreeTableModelAdapter adapt(TreeTableModel treeTableModel) {
115     return new TreeTableModelAdapter(treeTableModel, myTree, this);
116   }
117
118   public void setRootVisible(boolean visible){
119     myTree.setRootVisible(visible);
120   }
121
122   public void putTreeClientProperty(Object key, Object value){
123     myTree.putClientProperty(key, value);
124   }
125
126   public void setTreeCellRenderer(TreeCellRenderer renderer){
127     myTree.setCellRenderer(renderer);
128   }
129
130   /**
131    * Overridden to message super and forward the method to the tree.
132    * Since the tree is not actually in the component hierarchy it will
133    * never receive this unless we forward it in this manner.
134    */
135   public void updateUI() {
136     super.updateUI();
137     if (myTree!= null) {
138       myTree.updateUI();
139     }
140     // Use the tree's default foreground and background colors in the
141     // table.
142     //noinspection HardCodedStringLiteral
143     LookAndFeel.installColorsAndFont(this, "Tree.background", "Tree.foreground", "Tree.font");
144   }
145
146   /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
147    * paint the editor. The UI currently uses different techniques to
148    * paint the renderers and editors and overriding setBounds() below
149    * is not the right thing to do for an editor. Returning -1 for the
150    * editing row in this case, ensures the editor is never painted.
151    */
152   public int getEditingRow() {
153     return editingColumn == -1 || isTreeColumn(editingColumn) ? -1 : editingRow;
154   }
155
156   /**
157    * Overridden to pass the new rowHeight to the tree.
158    */
159   public void setRowHeight(int rowHeight) {
160     super.setRowHeight(rowHeight);
161     if (myTree != null && myTree.getRowHeight() < rowHeight) {
162       myTree.setRowHeight(getRowHeight());
163     }
164   }
165
166   /**
167    * @return the tree that is being shared between the model.
168    */
169   public TreeTableTree getTree() {
170     return myTree;
171   }
172
173   protected void processKeyEvent(KeyEvent e){
174     if (!myProcessCursorKeys) {
175       super.processKeyEvent(e);
176       return;
177     }
178
179     int keyCode = e.getKeyCode();
180     final int selColumn = columnModel.getSelectionModel().getAnchorSelectionIndex();
181     boolean treeHasFocus = selColumn == -1 || selColumn >= 0 && isTreeColumn(selColumn);
182     boolean oneRowSelected = getSelectedRowCount() == 1;
183     if(treeHasFocus && oneRowSelected && ((keyCode == KeyEvent.VK_LEFT) || (keyCode == KeyEvent.VK_RIGHT))){
184       myTree._processKeyEvent(e);
185       int rowToSelect = ObjectUtils.notNull(myTree.getSelectionRows())[0];
186       getSelectionModel().setSelectionInterval(rowToSelect, rowToSelect);
187       TableUtil.scrollSelectionToVisible(this);
188     }
189     else{
190       super.processKeyEvent(e);
191     }
192   }
193
194   /**
195    * Enable or disable processing of left/right cursor keys to expand/collapse
196    * nodes in the tree column. Disabling these keys can be useful to improve
197    * accessibility support when the left/right cursor keys are better suited to
198    * navigate to the previous/next cell of a given row.
199    */
200   public void setProcessCursorKeys(boolean processCursorKeys) {
201     myProcessCursorKeys = processCursorKeys;
202   }
203
204   /**
205    * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
206    * to listen for changes in the ListSelectionModel it maintains. Once
207    * a change in the ListSelectionModel happens, the paths are updated
208    * in the DefaultTreeSelectionModel.
209    */
210   private class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
211     /** Set to true when we are updating the ListSelectionModel. */
212     protected boolean updatingListSelectionModel;
213
214     public ListToTreeSelectionModelWrapper() {
215       super();
216       getListSelectionModel().addListSelectionListener(createListSelectionListener());
217     }
218
219     /**
220      * @return the list selection model. ListToTreeSelectionModelWrapper
221      * listens for changes to this model and updates the selected paths
222      * accordingly.
223      */
224     ListSelectionModel getListSelectionModel() {
225       return listSelectionModel;
226     }
227
228     /**
229      * This is overriden to set <code>updatingListSelectionModel</code>
230      * and message super. This is the only place DefaultTreeSelectionModel
231      * alters the ListSelectionModel.
232      */
233     public void resetRowSelection() {
234       if (!updatingListSelectionModel) {
235         updatingListSelectionModel = true;
236         try {
237           Set<Integer> selectedRows = new HashSet<>();
238           int min = listSelectionModel.getMinSelectionIndex();
239           int max = listSelectionModel.getMaxSelectionIndex();
240
241           if (min != -1 && max != -1) {
242             for (int counter = min; counter <= max; counter++) {
243               if (listSelectionModel.isSelectedIndex(counter)) {
244                 selectedRows.add(new Integer(counter));
245               }
246             }
247           }
248
249           super.resetRowSelection();
250
251           listSelectionModel.clearSelection();
252           for (final Object selectedRow : selectedRows) {
253             Integer row = (Integer)selectedRow;
254             listSelectionModel.addSelectionInterval(row.intValue(), row.intValue());
255           }
256         }
257         finally {
258           updatingListSelectionModel = false;
259         }
260       }
261       // Notice how we don't message super if
262       // updatingListSelectionModel is true. If
263       // updatingListSelectionModel is true, it implies the
264       // ListSelectionModel has already been updated and the
265       // paths are the only thing that needs to be updated.
266     }
267
268     /**
269      * @return a newly created instance of ListSelectionHandler.
270      */
271     protected ListSelectionListener createListSelectionListener() {
272       return new ListSelectionHandler();
273     }
274
275     /**
276      * If <code>updatingListSelectionModel</code> is false, this will
277      * reset the selected paths from the selected rows in the list
278      * selection model.
279      */
280     protected void updateSelectedPathsFromSelectedRows() {
281       if (!updatingListSelectionModel) {
282         updatingListSelectionModel = true;
283         try {
284           // This is way expensive, ListSelectionModel needs an
285           // enumerator for iterating.
286           int min = listSelectionModel.getMinSelectionIndex();
287           int max = listSelectionModel.getMaxSelectionIndex();
288
289           clearSelection();
290           if (min != -1 && max != -1) {
291             List<TreePath> selectionPaths = new ArrayList<>();
292             for (int counter = min; counter <= max; counter++) {
293               if (listSelectionModel.isSelectedIndex(counter)) {
294                 TreePath selPath = myTree.getPathForRow(counter);
295
296                 if (selPath != null) {
297                   selectionPaths.add(selPath);
298                 }
299               }
300             }
301             if (!selectionPaths.isEmpty()) {
302               addSelectionPaths(selectionPaths.toArray(new TreePath[selectionPaths.size()]));
303             }
304           }
305         }
306         finally {
307           updatingListSelectionModel = false;
308         }
309       }
310     }
311
312     /**
313      * Class responsible for calling updateSelectedPathsFromSelectedRows
314      * when the selection of the list changse.
315      */
316     class ListSelectionHandler implements ListSelectionListener {
317       public void valueChanged(ListSelectionEvent e) {
318         updateSelectedPathsFromSelectedRows();
319       }
320     }
321   }
322
323   public boolean editCellAt(int row, int column, EventObject e) {
324     boolean editResult = super.editCellAt(row, column, e);
325     if (e instanceof MouseEvent && isTreeColumn(column)){
326       MouseEvent me = (MouseEvent)e;
327       int y = me.getY();
328
329       if (getRowHeight() != myTree.getRowHeight()) {
330         // fix y if row heights are not equal
331         // [todo]: review setRowHeight to synchronize heights correctly!
332         final Rectangle tableCellRect = getCellRect(row, column, true);
333         y = Math.min(y - tableCellRect.y, myTree.getRowHeight() - 1) + row * myTree.getRowHeight();
334       }
335
336       MouseEvent newEvent = new MouseEvent(myTree, me.getID(),
337         me.getWhen(), me.getModifiers(),
338         me.getX() - getCellRect(0, column, true).x,
339         y, me.getClickCount(),
340         me.isPopupTrigger()
341       );
342       myTree.dispatchEvent(newEvent);
343
344       // Some LAFs, for example, Aqua under MAC OS X
345       // expand tree node by MOUSE_RELEASED event. Unfortunately,
346       // it's not possible to find easy way to wedge in table's
347       // event sequense. Therefore we send "synthetic" release event.
348       if (newEvent.getID()==MouseEvent.MOUSE_PRESSED) {
349         MouseEvent newME2 = new MouseEvent(
350           myTree,
351           MouseEvent.MOUSE_RELEASED,
352           me.getWhen(), me.getModifiers(),
353           me.getX() - getCellRect(0, column, true).x,
354           y - getCellRect(0, column, true).y, me.getClickCount(),
355           me.isPopupTrigger()
356         );
357         myTree.dispatchEvent(newME2);
358       }
359     }    
360     return editResult;
361   }
362
363   protected boolean isTreeColumn(int column) {
364     return TreeTableModel.class.isAssignableFrom(getColumnClass(column));
365   }
366
367   public void addSelectedPath(TreePath path) {
368     int row = getTree().getRowForPath(path);
369     getTree().addSelectionPath(path);
370     getSelectionModel().addSelectionInterval(row, row);
371   }
372
373   public void removeSelectedPath(TreePath path) {
374     int row = getTree().getRowForPath(path);
375     getTree().removeSelectionPath(path);
376     getSelectionModel().removeSelectionInterval(row, row);
377   }
378
379   public TreeTableCellRenderer createTableRenderer(TreeTableModel treeTableModel) {
380     return new TreeTableCellRenderer(this, myTree);
381   }
382
383   public void setMinRowHeight(int i) {
384     setRowHeight(Math.max(getRowHeight(), i));
385   }
386
387 }
388