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