cc20898b9ef956fd6b37b8dcf225a8ebb8841933
[idea/community.git] / platform / platform-api / src / com / intellij / ui / treeStructure / Tree.java
1 /*
2  * Copyright 2000-2009 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;
17
18 import com.intellij.Patches;
19 import com.intellij.ide.util.treeView.*;
20 import com.intellij.openapi.ui.Queryable;
21 import com.intellij.openapi.util.Disposer;
22 import com.intellij.openapi.util.SystemInfo;
23 import com.intellij.openapi.wm.impl.content.GraphicsConfig;
24 import com.intellij.ui.SimpleTextAttributes;
25 import com.intellij.util.ui.AsyncProcessIcon;
26 import com.intellij.util.ui.ComponentWithEmptyText;
27 import com.intellij.util.ui.EmptyTextHelper;
28 import com.intellij.util.ui.UIUtil;
29 import org.jetbrains.annotations.Nullable;
30
31 import javax.swing.*;
32 import javax.swing.plaf.TreeUI;
33 import javax.swing.plaf.basic.BasicTreeUI;
34 import javax.swing.text.Position;
35 import javax.swing.tree.*;
36 import java.awt.*;
37 import java.awt.dnd.Autoscroll;
38 import java.awt.event.*;
39 import java.lang.reflect.Array;
40 import java.util.ArrayList;
41 import java.util.Map;
42
43 public class Tree extends JTree implements ComponentWithEmptyText, Autoscroll, Queryable {
44   private EmptyTextHelper myEmptyTextHelper;
45
46   private AsyncProcessIcon myBusyIcon;
47   private boolean myBusy;
48   private Rectangle myLastVisibleRec;
49
50   public Tree() {
51     initTree_();
52   }
53
54   public Tree(TreeModel treemodel) {
55     super(treemodel);
56     initTree_();
57   }
58
59   public Tree(TreeNode root) {
60     super(root);
61     initTree_();
62   }
63
64   private void initTree_() {
65     myEmptyTextHelper = new EmptyTextHelper(this) {
66       @Override
67       protected boolean isEmpty() {
68         TreeModel model = getModel();
69         if (model == null) return true;
70         if (model.getRoot() == null) return true;
71         return !isRootVisible() && model.getChildCount(model.getRoot()) == 0;
72       }
73     };
74
75     addMouseListener(new MyMouseListener());
76     if (Patches.SUN_BUG_ID_4893787) {
77       addFocusListener(new MyFocusListener());
78     }
79
80     setCellRenderer(new NodeRenderer());
81   }
82
83   public String getEmptyText() {
84     return myEmptyTextHelper.getEmptyText();
85   }
86
87   public void setEmptyText(String emptyText) {
88     myEmptyTextHelper.setEmptyText(emptyText);
89   }
90
91   public void setEmptyText(String emptyText, SimpleTextAttributes attrs) {
92     myEmptyTextHelper.setEmptyText(emptyText, attrs);
93   }
94
95   public void clearEmptyText() {
96     myEmptyTextHelper.clearEmptyText();
97   }
98
99   public void appendEmptyText(String text, SimpleTextAttributes attrs) {
100     myEmptyTextHelper.appendEmptyText(text, attrs);
101   }
102
103   public void appendEmptyText(String text, SimpleTextAttributes attrs, ActionListener listener) {
104     myEmptyTextHelper.appendEmptyText(text, attrs, listener);
105   }
106
107   @Override
108   public void addNotify() {
109     super.addNotify();
110
111     updateBusy();
112   }
113
114   @Override
115   public void removeNotify() {
116     super.removeNotify();
117
118     if (myBusyIcon != null) {
119       remove(myBusyIcon);
120       Disposer.dispose(myBusyIcon);
121       myBusyIcon = null;
122     }
123   }
124
125   @Override
126   public void doLayout() {
127     super.doLayout();
128
129     updateBusyIconLocation();
130   }
131
132   private void updateBusyIconLocation() {
133     if (myBusyIcon != null) {
134       final Rectangle rec = getVisibleRect();
135
136       final Dimension iconSize = myBusyIcon.getPreferredSize();
137
138       final Rectangle newBounds = new Rectangle(rec.x + rec.width - iconSize.width, rec.y, iconSize.width, iconSize.height);
139       if (!newBounds.equals(myBusyIcon.getBounds())) {
140         myBusyIcon.setBounds(newBounds);
141         repaint();
142       }
143     }
144   }
145
146   @Override
147   public void paint(Graphics g) {
148     super.paint(g);
149
150     final Rectangle visible = getVisibleRect();
151
152     if (!visible.equals(myLastVisibleRec)) {
153       updateBusyIconLocation();
154     }
155
156     myLastVisibleRec = visible;
157   }
158
159   public void setPaintBusy(boolean paintBusy) {
160     if (myBusy == paintBusy) return;
161
162     myBusy = paintBusy;
163     updateBusy();
164   }
165
166   private void updateBusy() {
167     if (myBusy) {
168       if (myBusyIcon == null) {
169         myBusyIcon = new AsyncProcessIcon(toString());
170         myBusyIcon.setPaintPassiveIcon(false);
171         add(myBusyIcon);
172       }
173     }
174
175     if (myBusyIcon != null) {
176       if (myBusy) {
177         myBusyIcon.resume();
178       } else {
179         myBusyIcon.suspend();
180         SwingUtilities.invokeLater(new Runnable() {
181           public void run() {
182             if (myBusyIcon != null) {
183               repaint();
184             }
185           }
186         });
187       }
188       updateBusyIconLocation();
189     }
190   }
191
192   protected boolean paintNodes() {
193     return false;
194   }
195
196   @Override
197   protected void paintComponent(Graphics g) {
198     if (paintNodes()) {
199       g.setColor(getBackground());
200       g.fillRect(0, 0, getWidth(), getHeight());
201
202       paintNodeContent(g);
203     }
204
205     super.paintComponent(g);
206     myEmptyTextHelper.paint(g);
207   }
208
209   /**
210    * Hack to prevent loosing multiple selection on Mac when clicking Ctrl+Left Mouse Button.
211    * See faulty code at BasicTreeUI.selectPathForEvent():2245
212    *
213    * @param e
214    */
215   protected void processMouseEvent(MouseEvent e) {
216     if (SystemInfo.isMac) {
217       if (SwingUtilities.isLeftMouseButton(e) && e.isControlDown() && e.getID() == MouseEvent.MOUSE_PRESSED) {
218         int modifiers = (e.getModifiers() & ~(MouseEvent.CTRL_MASK | MouseEvent.BUTTON1_MASK)) | MouseEvent.BUTTON3_MASK;
219         e = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), modifiers, e.getX(), e.getY(), e.getClickCount(), true,
220                            MouseEvent.BUTTON3);
221       }
222     }
223     super.processMouseEvent(e);
224   }
225
226   /**
227    * Disable Sun's speedsearch
228    */
229   public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) {
230     return null;
231   }
232
233   private static final int AUTOSCROLL_MARGIN = 10;
234
235   public Insets getAutoscrollInsets() {
236     return new Insets(getLocation().y + AUTOSCROLL_MARGIN, 0, getParent().getHeight() - AUTOSCROLL_MARGIN, getWidth() - 1);
237   }
238
239   public void autoscroll(Point p) {
240     int realrow = getClosestRowForLocation(p.x, p.y);
241     if (getLocation().y + p.y <= AUTOSCROLL_MARGIN) {
242       if (realrow >= 1) realrow--;
243     }
244     else {
245       if (realrow < getRowCount() - 1) realrow++;
246     }
247     scrollRowToVisible(realrow);
248   }
249
250   protected boolean highlightSingleNode() {
251     return true;
252   }
253
254   private void paintNodeContent(Graphics g) {
255     if (!(getUI() instanceof BasicTreeUI)) return;
256
257     final AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
258     if (builder == null || builder.isDisposed()) return;
259
260     GraphicsConfig config = new GraphicsConfig(g);
261     config.setAntialiasing(true);
262
263     final AbstractTreeStructure structure = builder.getTreeStructure();
264
265     for (int eachRow = 0; eachRow < getRowCount(); eachRow++) {
266       final TreePath path = getPathForRow(eachRow);
267       PresentableNodeDescriptor node = toPresentableNode(path.getLastPathComponent());
268       if (node == null) continue;
269
270       if (!node.isContentHighlighted()) continue;
271
272       if (highlightSingleNode()) {
273         if (node.isContentHighlighted()) {
274           final TreePath nodePath = getPath(node);
275
276           Rectangle rect;
277
278           final Rectangle parentRect = getPathBounds(nodePath);
279           if (isExpanded(nodePath)) {
280             final int[] max = getMax(node, structure);
281             rect = new Rectangle(parentRect.x, parentRect.y, Math.max((int) parentRect.getMaxX(), max[1]) - parentRect.x - 1,
282                                  Math.max((int) parentRect.getMaxY(), max[0]) - parentRect.y - 1);
283           }
284           else {
285             rect = parentRect;
286           }
287
288           if (rect != null) {
289             final Color highlightColor = node.getHighlightColor();
290             g.setColor(highlightColor);
291             g.fillRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
292             g.setColor(highlightColor.darker());
293             g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
294           }
295         }
296       }
297       else {
298 //todo: to investigate why it might happen under 1.6: http://www.productiveme.net:8080/browse/PM-217
299         if (node.getParentDescriptor() == null) continue;
300
301         final Object[] kids = structure.getChildElements(node);
302         if (kids.length == 0) continue;
303
304         PresentableNodeDescriptor first = null;
305         PresentableNodeDescriptor last = null;
306         int lastIndex = -1;
307         for (int i = 0; i < kids.length; i++) {
308           final Object kid = kids[i];
309           if (kid instanceof PresentableNodeDescriptor) {
310           PresentableNodeDescriptor eachKid = (PresentableNodeDescriptor) kid;
311           if (!node.isHighlightableContentNode(eachKid)) continue;
312           if (first == null) {
313             first = eachKid;
314           }
315           last = eachKid;
316           lastIndex = i;
317           }
318         }
319
320         if (first == null || last == null) continue;
321         Rectangle firstBounds = getPathBounds(getPath(first));
322
323         if (isExpanded(getPath(last))) {
324           if (lastIndex + 1 < kids.length) {
325             final Object child = kids[lastIndex + 1];
326             if (child instanceof PresentableNodeDescriptor) {
327               PresentableNodeDescriptor nextKid = (PresentableNodeDescriptor) child;
328               int nextRow = getRowForPath(getPath(nextKid));
329               last = toPresentableNode(getPathForRow(nextRow - 1).getLastPathComponent());
330             }
331           }
332           else {
333             NodeDescriptor parentNode = node.getParentDescriptor();
334             if (parentNode instanceof PresentableNodeDescriptor) {
335               final PresentableNodeDescriptor ppd = (PresentableNodeDescriptor)parentNode;
336               int nodeIndex = node.getIndex();
337               if (nodeIndex + 1 < structure.getChildElements(ppd).length) {
338                 PresentableNodeDescriptor nextChild = ppd.getChildToHighlightAt(nodeIndex + 1);
339                 int nextRow = getRowForPath(getPath(nextChild));
340                 TreePath prevPath = getPathForRow(nextRow - 1);
341                 if (prevPath != null) {
342                   last = toPresentableNode(prevPath.getLastPathComponent());
343                 }
344               }
345               else {
346                 int lastRow = getRowForPath(getPath(last));
347                 PresentableNodeDescriptor lastParent = last;
348                 boolean lastWasFound = false;
349                 for (int i = lastRow + 1; i < getRowCount(); i++) {
350                   PresentableNodeDescriptor eachNode = toPresentableNode(getPathForRow(i).getLastPathComponent());
351                   if (!node.isParentOf(eachNode)) {
352                     last = lastParent;
353                     lastWasFound = true;
354                     break;
355                   }
356                   lastParent = eachNode;
357                 }
358                 if (!lastWasFound) {
359                   last = toPresentableNode(getPathForRow(getRowCount() - 1).getLastPathComponent());
360                 }
361               }
362             }
363           }
364         }
365
366         if (last == null) continue;
367         Rectangle lastBounds = getPathBounds(getPath(last));
368
369         if (firstBounds == null || lastBounds == null) continue;
370
371         Rectangle toPaint = new Rectangle(firstBounds.x, firstBounds.y, 0, (int)lastBounds.getMaxY() - firstBounds.y - 1);
372
373         toPaint.width = getWidth() - toPaint.x - 4;
374
375         final Color highlightColor = first.getHighlightColor();
376         g.setColor(highlightColor);
377         g.fillRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
378         g.setColor(highlightColor.darker());
379         g.drawRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
380       }
381     }
382
383     config.restore();
384   }
385
386   private int[] getMax(final PresentableNodeDescriptor node, final AbstractTreeStructure structure) {
387     int x = 0;
388     int y = 0;
389     final Object[] children = structure.getChildElements(node);
390     for (final Object child : children) {
391       if (child instanceof PresentableNodeDescriptor) {
392         final TreePath childPath = getPath((PresentableNodeDescriptor)child);
393         if (childPath != null) {
394           if (isExpanded(childPath)) {
395             final int[] tmp = getMax((PresentableNodeDescriptor)child, structure);
396             y = Math.max(y, tmp[0]);
397             x = Math.max(x, tmp[1]);
398           }
399
400           final Rectangle r = getPathBounds(childPath);
401           if (r != null) {
402             y = Math.max(y, (int)r.getMaxY());
403             x = Math.max(x, (int)r.getMaxX());
404           }
405         }
406       }
407     }
408
409     return new int[]{y, x};
410   }
411
412   @Nullable
413   private static PresentableNodeDescriptor toPresentableNode(final Object pathComponent) {
414     if (!(pathComponent instanceof DefaultMutableTreeNode)) return null;
415     final Object userObject = ((DefaultMutableTreeNode)pathComponent).getUserObject();
416     if (!(userObject instanceof PresentableNodeDescriptor)) return null;
417     return (PresentableNodeDescriptor)userObject;
418   }
419
420   public TreePath getPath(PresentableNodeDescriptor node) {
421     final AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
422     final DefaultMutableTreeNode treeNode = builder.getNodeForElement(node);
423
424     return treeNode != null ? new TreePath(treeNode.getPath()) : new TreePath(node);
425   }
426
427   private class MyMouseListener extends MouseAdapter {
428     public void mousePressed(MouseEvent mouseevent) {
429       if (!SwingUtilities.isLeftMouseButton(mouseevent) &&
430           (SwingUtilities.isRightMouseButton(mouseevent) || SwingUtilities.isMiddleMouseButton(mouseevent))) {
431         TreePath treepath = getPathForLocation(mouseevent.getX(), mouseevent.getY());
432         if (treepath != null) {
433           if (getSelectionModel().getSelectionMode() != TreeSelectionModel.SINGLE_TREE_SELECTION) {
434             TreePath[] selectionPaths = getSelectionModel().getSelectionPaths();
435             if (selectionPaths != null) {
436               for (TreePath selectionPath : selectionPaths) {
437                 if (selectionPath == treepath) return;
438               }
439             }
440           }
441           getSelectionModel().setSelectionPath(treepath);
442         }
443       }
444     }
445   }
446
447   /**
448    * This is patch for 4893787 SUN bug. The problem is that the BasicTreeUI.FocusHandler repaints
449    * only lead selection index on focus changes. It's a problem with multiple selected nodes.
450    */
451   private class MyFocusListener extends FocusAdapter {
452     private void focusChanges() {
453       TreePath[] paths = getSelectionPaths();
454       if (paths != null) {
455         TreeUI ui = getUI();
456         for (int i = paths.length - 1; i >= 0; i--) {
457           Rectangle bounds = ui.getPathBounds(Tree.this, paths[i]);
458           if (bounds != null) {
459             repaint(bounds);
460           }
461         }
462       }
463     }
464
465     public void focusGained(FocusEvent e) {
466       focusChanges();
467     }
468
469     public void focusLost(FocusEvent e) {
470       focusChanges();
471     }
472   }
473
474   public final void setLineStyleAngled() {
475     UIUtil.setLineStyleAngled(this);
476   }
477
478   public <T> T[] getSelectedNodes(Class<T> nodeType, @Nullable NodeFilter<T> filter) {
479     TreePath[] paths = getSelectionPaths();
480     if (paths == null) return (T[])Array.newInstance(nodeType, 0);
481
482     ArrayList<T> nodes = new ArrayList<T>();
483     for (int i = 0; i < paths.length; i++) {
484       Object last = paths[i].getLastPathComponent();
485       if (nodeType.isAssignableFrom(last.getClass())) {
486         if (filter != null && !filter.accept((T)last)) continue;
487         nodes.add((T)last);
488       }
489     }
490     T[] result = (T[])Array.newInstance(nodeType, nodes.size());
491     nodes.toArray(result);
492     return result;
493   }
494
495   public interface NodeFilter<T> {
496     boolean accept(T node);
497   }
498
499   public void putInfo(Map<String, String> info) {
500     final TreePath[] selection = getSelectionPaths();
501     if (selection == null) return;
502
503     StringBuffer nodesText = new StringBuffer();
504
505     for (TreePath eachPath : selection) {
506       final Object eachNode = eachPath.getLastPathComponent();
507       final Component c =
508         getCellRenderer().getTreeCellRendererComponent(this, eachNode, false, false, false, getRowForPath(eachPath), false);
509
510       if (c != null) {
511         if (nodesText.length() > 0) {
512           nodesText.append(";");
513         }
514         nodesText.append(c.toString());
515       }
516     }
517
518     if (nodesText.length() > 0) {
519       info.put("selectedNodes", nodesText.toString());
520     }
521   }
522 }