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