replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-api / src / com / intellij / ui / treeStructure / Tree.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;
17
18 import com.intellij.ide.util.treeView.*;
19 import com.intellij.openapi.ui.GraphicsConfig;
20 import com.intellij.openapi.ui.Queryable;
21 import com.intellij.openapi.util.Condition;
22 import com.intellij.openapi.util.Conditions;
23 import com.intellij.openapi.util.Disposer;
24 import com.intellij.openapi.util.SystemInfo;
25 import com.intellij.ui.*;
26 import com.intellij.util.ReflectionUtil;
27 import com.intellij.util.ui.*;
28 import com.intellij.util.ui.tree.TreeUtil;
29 import com.intellij.util.ui.tree.WideSelectionTreeUI;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import javax.swing.*;
34 import javax.swing.event.TreeSelectionEvent;
35 import javax.swing.plaf.TreeUI;
36 import javax.swing.plaf.basic.BasicTreeUI;
37 import javax.swing.text.Position;
38 import javax.swing.tree.*;
39 import java.awt.*;
40 import java.awt.dnd.Autoscroll;
41 import java.awt.event.*;
42 import java.lang.reflect.Array;
43 import java.lang.reflect.Method;
44 import java.util.ArrayList;
45 import java.util.Map;
46
47 public class Tree extends JTree implements ComponentWithEmptyText, ComponentWithExpandableItems<Integer>, Autoscroll, Queryable,
48                                            ComponentWithFileColors {
49   private final StatusText myEmptyText;
50   private final ExpandableItemsHandler<Integer> myExpandableItemsHandler;
51
52   private AsyncProcessIcon myBusyIcon;
53   private boolean myBusy;
54   private Rectangle myLastVisibleRec;
55
56   private Dimension myHoldSize;
57   private final MySelectionModel mySelectionModel = new MySelectionModel();
58   private boolean myHorizontalAutoScrolling = true;
59
60   private TreePath rollOverPath;
61
62   public Tree() {
63     this(getDefaultTreeModel());
64   }
65
66   public Tree(TreeNode root) {
67     this(new DefaultTreeModel(root, false));
68   }
69
70   public Tree(TreeModel treemodel) {
71     super(treemodel);
72     myEmptyText = new StatusText(this) {
73       @Override
74       protected boolean isStatusVisible() {
75         return Tree.this.isEmpty();
76       }
77     };
78
79     myExpandableItemsHandler = ExpandableItemsHandlerFactory.install(this);
80
81     if (UIUtil.isUnderWin10LookAndFeel()) {
82       addMouseMotionListener(new MouseMotionAdapter() {
83         @Override
84         public void mouseMoved(MouseEvent e) {
85           Point p = e.getPoint();
86           TreePath newPath = getPathForLocation(p.x, p.y);
87           if (newPath != null && !newPath.equals(rollOverPath)) {
88             TreeCellRenderer renderer = getCellRenderer();
89             if (newPath.getLastPathComponent() instanceof TreeNode) {
90               TreeNode node = (TreeNode)newPath.getLastPathComponent();
91               JComponent c = (JComponent)renderer.getTreeCellRendererComponent(
92                 Tree.this, node,
93                 isPathSelected(newPath),
94                 isExpanded(newPath),
95                 getModel().isLeaf(node),
96                 getRowForPath(newPath), hasFocus());
97
98               c.putClientProperty(UIUtil.CHECKBOX_ROLLOVER_PROPERTY, c instanceof JCheckBox ? getPathBounds(newPath) : node);
99               rollOverPath = newPath;
100               UIUtil.repaintViewport(Tree.this);
101             }
102           }
103         }
104       });
105     }
106
107     addMouseListener(new MyMouseListener());
108     addFocusListener(new MyFocusListener());
109
110     setCellRenderer(new NodeRenderer());
111
112     setSelectionModel(mySelectionModel);
113     setOpaque(false);
114   }
115
116   @Override
117   public void setUI(TreeUI ui) {
118     TreeUI actualUI = ui;
119     if (!isCustomUI()) {
120       if (!(ui instanceof WideSelectionTreeUI) && isWideSelection() && !UIUtil.isUnderGTKLookAndFeel()) {
121         actualUI = new WideSelectionTreeUI(isWideSelection(), getWideSelectionBackgroundCondition());
122       }
123     }
124     super.setUI(actualUI);
125   }
126
127   @Override
128   protected Graphics getComponentGraphics(Graphics graphics) {
129     return JBSwingUtilities.runGlobalCGTransform(this, super.getComponentGraphics(graphics));
130   }
131
132   public boolean isEmpty() {
133     TreeModel model = getModel();
134     if (model == null) return true;
135     if (model.getRoot() == null) return true;
136     if (!isRootVisible()) {
137       int childCount = model.getChildCount(model.getRoot());
138       if (childCount == 0) {
139         return true;
140       }
141       if (childCount == 1) {
142         Object node = model.getChild(model.getRoot(), 0);
143         if (node instanceof LoadingNode) {
144           return true;
145         }
146       }
147     }
148     return false;
149   }
150
151   protected boolean isCustomUI() {
152     return false;
153   }
154
155   protected boolean isWideSelection() {
156     return true;
157   }
158
159   /**
160    * @return a strategy which determines if a wide selection should be drawn for a target row (it's number is
161    * {@link Condition#value(Object) given} as an argument to the strategy)
162    */
163   @SuppressWarnings("unchecked")
164   @NotNull
165   protected Condition<Integer> getWideSelectionBackgroundCondition() {
166     return Conditions.alwaysTrue();
167   }
168
169   @Override
170   public boolean isFileColorsEnabled() {
171     return false;
172   }
173
174   @NotNull
175   @Override
176   public StatusText getEmptyText() {
177     return myEmptyText;
178   }
179
180   @Override
181   @NotNull
182   public ExpandableItemsHandler<Integer> getExpandableItemsHandler() {
183     return myExpandableItemsHandler;
184   }
185
186   @Override
187   public void setExpandableItemsEnabled(boolean enabled) {
188     myExpandableItemsHandler.setEnabled(enabled);
189   }
190
191   @Override
192   public Color getBackground() {
193     return isBackgroundSet() ? super.getBackground() : UIUtil.getTreeTextBackground();
194   }
195
196   @Override
197   public Color getForeground() {
198     return isForegroundSet() ? super.getForeground() : UIUtil.getTreeForeground();
199   }
200
201   @Override
202   public void addNotify() {
203     super.addNotify();
204
205     updateBusy();
206   }
207
208   @Override
209   public void removeNotify() {
210     super.removeNotify();
211
212     if (myBusyIcon != null) {
213       remove(myBusyIcon);
214       Disposer.dispose(myBusyIcon);
215       myBusyIcon = null;
216     }
217   }
218
219   @Override
220   public void doLayout() {
221     super.doLayout();
222
223     updateBusyIconLocation();
224   }
225
226   private void updateBusyIconLocation() {
227     if (myBusyIcon != null) {
228       myBusyIcon.updateLocation(this);
229     }
230   }
231
232   @Override
233   public void paint(Graphics g) {
234     Rectangle visible = getVisibleRect();
235
236     boolean canHoldSelection = false;
237     TreePath[] paths = getSelectionModel().getSelectionPaths();
238     if (paths != null) {
239       for (TreePath each : paths) {
240         Rectangle selection = getPathBounds(each);
241         if (selection != null && (g.getClipBounds().intersects(selection) || g.getClipBounds().contains(selection))) {
242           if (myBusy && myBusyIcon != null) {
243             Rectangle busyIconBounds = myBusyIcon.getBounds();
244             if (selection.contains(busyIconBounds) || selection.intersects(busyIconBounds)) {
245               canHoldSelection = false;
246               break;
247             }
248             else {
249               canHoldSelection = true;
250             }
251           }
252           else {
253             canHoldSelection = true;
254           }
255         }
256       }
257     }
258
259     if (canHoldSelection) {
260       if (!AbstractTreeBuilder.isToPaintSelection(this)) {
261         mySelectionModel.holdSelection();
262       }
263     }
264
265     try {
266       super.paint(g);
267
268       if (!visible.equals(myLastVisibleRec)) {
269         updateBusyIconLocation();
270       }
271
272       myLastVisibleRec = visible;
273     }
274     finally {
275       mySelectionModel.unholdSelection();
276     }
277   }
278
279   public void setPaintBusy(boolean paintBusy) {
280     if (myBusy == paintBusy) return;
281
282     myBusy = paintBusy;
283     updateBusy();
284   }
285
286   private void updateBusy() {
287     if (myBusy) {
288       if (myBusyIcon == null) {
289         myBusyIcon = new AsyncProcessIcon(toString()).setUseMask(false);
290         myBusyIcon.setOpaque(false);
291         myBusyIcon.setPaintPassiveIcon(false);
292         add(myBusyIcon);
293         myBusyIcon.addMouseListener(new MouseAdapter() {
294           @Override
295           public void mousePressed(MouseEvent e) {
296             if (!UIUtil.isActionClick(e)) return;
297             AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(Tree.this);
298             if (builder != null) {
299               builder.cancelUpdate();
300             }
301           }
302         });
303       }
304     }
305
306     if (myBusyIcon != null) {
307       if (myBusy) {
308         if (shouldShowBusyIconIfNeeded()) {
309           myBusyIcon.resume();
310           myBusyIcon.setToolTipText("Update is in progress. Click to cancel");
311         }
312       }
313       else {
314         myBusyIcon.suspend();
315         myBusyIcon.setToolTipText(null);
316         //noinspection SSBasedInspection
317         SwingUtilities.invokeLater(() -> {
318           if (myBusyIcon != null) {
319             repaint();
320           }
321         });
322       }
323       updateBusyIconLocation();
324     }
325   }
326
327   protected boolean shouldShowBusyIconIfNeeded() {
328     // https://youtrack.jetbrains.com/issue/IDEA-101422 "Rotating wait symbol in Project list whenever typing"
329     return hasFocus();
330   }
331
332   protected boolean paintNodes() {
333     return false;
334   }
335
336   @Override
337   protected void paintComponent(Graphics g) {
338     if (paintNodes()) {
339       g.setColor(getBackground());
340       g.fillRect(0, 0, getWidth(), getHeight());
341
342       paintNodeContent(g);
343     }
344
345     if (isFileColorsEnabled()) {
346       g.setColor(getBackground());
347       g.fillRect(0, 0, getWidth(), getHeight());
348
349       paintFileColorGutter(g);
350     }
351
352     super.paintComponent(g);
353     myEmptyText.paint(this, g);
354   }
355
356   protected void paintFileColorGutter(Graphics g) {
357     GraphicsConfig config = new GraphicsConfig(g);
358     Rectangle rect = getVisibleRect();
359     int firstVisibleRow = getClosestRowForLocation(rect.x, rect.y);
360     int lastVisibleRow = getClosestRowForLocation(rect.x, rect.y + rect.height);
361
362     for (int row = firstVisibleRow; row <= lastVisibleRow; row++) {
363       TreePath path = getPathForRow(row);
364       Color color = path == null ? null : getFileColorForPath(path);
365       if (color != null) {
366         Rectangle bounds = getRowBounds(row);
367         g.setColor(color);
368         g.fillRect(0, bounds.y, getWidth(), bounds.height);
369       }
370     }
371     config.restore();
372   }
373
374   @Nullable
375   public Color getFileColorForPath(@NotNull TreePath path) {
376     Object component = path.getLastPathComponent();
377     if (component instanceof LoadingNode) {
378       Object[] pathObjects = path.getPath();
379       if (pathObjects.length > 1) {
380         component = pathObjects[pathObjects.length - 2];
381       }
382     }
383     return getFileColorFor(TreeUtil.getUserObject(component));
384   }
385
386   @Nullable
387   public Color getFileColorFor(Object object) {
388     return null;
389   }
390
391   @Override
392   protected void processKeyEvent(KeyEvent e) {
393     super.processKeyEvent(e);
394   }
395
396   /**
397    * Hack to prevent loosing multiple selection on Mac when clicking Ctrl+Left Mouse Button.
398    * See faulty code at BasicTreeUI.selectPathForEvent():2245
399    * <p>
400    * Another hack to match selection UI (wide) and selection behavior (narrow) in Nimbus/GTK+.
401    */
402   @Override
403   protected void processMouseEvent(MouseEvent e) {
404     MouseEvent e2 = e;
405
406     if (SystemInfo.isMac) {
407       if (SwingUtilities.isLeftMouseButton(e) && e.isControlDown() && e.getID() == MouseEvent.MOUSE_PRESSED) {
408         int modifiers = e.getModifiers() & ~(InputEvent.CTRL_MASK | InputEvent.BUTTON1_MASK) | InputEvent.BUTTON3_MASK;
409         e2 = new MouseEvent(e.getComponent(), e.getID(), e.getWhen(), modifiers, e.getX(), e.getY(), e.getClickCount(),
410                             true, MouseEvent.BUTTON3);
411       }
412     }
413     else if (UIUtil.isUnderNimbusLookAndFeel() || UIUtil.isUnderGTKLookAndFeel()) {
414       if (SwingUtilities.isLeftMouseButton(e) && (e.getID() == MouseEvent.MOUSE_PRESSED || e.getID() == MouseEvent.MOUSE_CLICKED)) {
415         TreePath path = getClosestPathForLocation(e.getX(), e.getY());
416         if (path != null) {
417           Rectangle bounds = getPathBounds(path);
418           if (bounds != null &&
419               e.getY() > bounds.y && e.getY() < bounds.y + bounds.height &&
420               (e.getX() >= bounds.x + bounds.width ||
421                e.getX() < bounds.x && !isLocationInExpandControl(path, e.getX(), e.getY()))) {
422             int newX = bounds.x + bounds.width - 2;
423             e2 = MouseEventAdapter.convert(e, e.getComponent(), newX, e.getY());
424           }
425         }
426       }
427     }
428
429     super.processMouseEvent(e2);
430   }
431
432   /**
433    * Returns true if {@code mouseX} falls
434    * in the area of row that is used to expand/collapse the node and
435    * the node at {@code row} does not represent a leaf.
436    */
437   protected boolean isLocationInExpandControl(@Nullable TreePath path, int mouseX) {
438     if (path == null) return false;
439     Rectangle bounds = getRowBounds(getRowForPath(path));
440     return isLocationInExpandControl(path, mouseX, bounds.y + bounds.height / 2);
441   }
442
443
444   private boolean isLocationInExpandControl(TreePath path, int x, int y) {
445     TreeUI ui = getUI();
446     if (!(ui instanceof BasicTreeUI)) return false;
447
448     try {
449       Class aClass = ui.getClass();
450       while (BasicTreeUI.class.isAssignableFrom(aClass) && !BasicTreeUI.class.equals(aClass)) {
451         aClass = aClass.getSuperclass();
452       }
453       Method method = ReflectionUtil.getDeclaredMethod(aClass, "isLocationInExpandControl", TreePath.class, int.class, int.class);
454       if (method != null) {
455         return (Boolean)method.invoke(ui, path, x, y);
456       }
457     }
458     catch (Throwable ignore) {
459     }
460
461     return false;
462   }
463
464   /**
465    * Disable Sun's speed search
466    */
467   @Override
468   public TreePath getNextMatch(String prefix, int startingRow, Position.Bias bias) {
469     return null;
470   }
471
472   private static final int AUTOSCROLL_MARGIN = 10;
473
474   @Override
475   public Insets getAutoscrollInsets() {
476     return new Insets(getLocation().y + AUTOSCROLL_MARGIN, 0, getParent().getHeight() - AUTOSCROLL_MARGIN, getWidth() - 1);
477   }
478
479   @Override
480   public void autoscroll(Point p) {
481     int realRow = getClosestRowForLocation(p.x, p.y);
482     if (getLocation().y + p.y <= AUTOSCROLL_MARGIN) {
483       if (realRow >= 1) realRow--;
484     }
485     else {
486       if (realRow < getRowCount() - 1) realRow++;
487     }
488     scrollRowToVisible(realRow);
489   }
490
491   protected boolean highlightSingleNode() {
492     return true;
493   }
494
495   private void paintNodeContent(Graphics g) {
496     if (!(getUI() instanceof BasicTreeUI)) return;
497
498     AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
499     if (builder == null || builder.isDisposed()) return;
500
501     GraphicsConfig config = new GraphicsConfig(g);
502     config.setAntialiasing(true);
503
504     AbstractTreeStructure structure = builder.getTreeStructure();
505
506     for (int eachRow = 0; eachRow < getRowCount(); eachRow++) {
507       TreePath path = getPathForRow(eachRow);
508       PresentableNodeDescriptor node = toPresentableNode(path.getLastPathComponent());
509       if (node == null) continue;
510
511       if (!node.isContentHighlighted()) continue;
512
513       if (highlightSingleNode()) {
514         if (node.isContentHighlighted()) {
515           TreePath nodePath = getPath(node);
516
517           Rectangle rect;
518
519           Rectangle parentRect = getPathBounds(nodePath);
520           if (isExpanded(nodePath)) {
521             int[] max = getMax(node, structure);
522             rect = new Rectangle(parentRect.x,
523                                  parentRect.y,
524                                  Math.max((int)parentRect.getMaxX(), max[1]) - parentRect.x - 1,
525                                  Math.max((int)parentRect.getMaxY(), max[0]) - parentRect.y - 1);
526           }
527           else {
528             rect = parentRect;
529           }
530
531           if (rect != null) {
532             Color highlightColor = node.getHighlightColor();
533             g.setColor(highlightColor);
534             g.fillRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
535             g.setColor(highlightColor.darker());
536             g.drawRoundRect(rect.x, rect.y, rect.width, rect.height, 4, 4);
537           }
538         }
539       }
540       else {
541         //todo: to investigate why it might happen under 1.6: http://www.productiveme.net:8080/browse/PM-217
542         if (node.getParentDescriptor() == null) continue;
543
544         Object[] kids = structure.getChildElements(node);
545         if (kids.length == 0) continue;
546
547         PresentableNodeDescriptor first = null;
548         PresentableNodeDescriptor last = null;
549         int lastIndex = -1;
550         for (int i = 0; i < kids.length; i++) {
551           Object kid = kids[i];
552           if (kid instanceof PresentableNodeDescriptor) {
553             PresentableNodeDescriptor eachKid = (PresentableNodeDescriptor)kid;
554             if (!node.isHighlightableContentNode(eachKid)) continue;
555             if (first == null) {
556               first = eachKid;
557             }
558             last = eachKid;
559             lastIndex = i;
560           }
561         }
562
563         if (first == null || last == null) continue;
564         Rectangle firstBounds = getPathBounds(getPath(first));
565
566         if (isExpanded(getPath(last))) {
567           if (lastIndex + 1 < kids.length) {
568             Object child = kids[lastIndex + 1];
569             if (child instanceof PresentableNodeDescriptor) {
570               PresentableNodeDescriptor nextKid = (PresentableNodeDescriptor)child;
571               int nextRow = getRowForPath(getPath(nextKid));
572               last = toPresentableNode(getPathForRow(nextRow - 1).getLastPathComponent());
573             }
574           }
575           else {
576             NodeDescriptor parentNode = node.getParentDescriptor();
577             if (parentNode instanceof PresentableNodeDescriptor) {
578               PresentableNodeDescriptor ppd = (PresentableNodeDescriptor)parentNode;
579               int nodeIndex = node.getIndex();
580               if (nodeIndex + 1 < structure.getChildElements(ppd).length) {
581                 PresentableNodeDescriptor nextChild = ppd.getChildToHighlightAt(nodeIndex + 1);
582                 int nextRow = getRowForPath(getPath(nextChild));
583                 TreePath prevPath = getPathForRow(nextRow - 1);
584                 if (prevPath != null) {
585                   last = toPresentableNode(prevPath.getLastPathComponent());
586                 }
587               }
588               else {
589                 int lastRow = getRowForPath(getPath(last));
590                 PresentableNodeDescriptor lastParent = last;
591                 boolean lastWasFound = false;
592                 for (int i = lastRow + 1; i < getRowCount(); i++) {
593                   PresentableNodeDescriptor eachNode = toPresentableNode(getPathForRow(i).getLastPathComponent());
594                   if (!node.isParentOf(eachNode)) {
595                     last = lastParent;
596                     lastWasFound = true;
597                     break;
598                   }
599                   lastParent = eachNode;
600                 }
601                 if (!lastWasFound) {
602                   last = toPresentableNode(getPathForRow(getRowCount() - 1).getLastPathComponent());
603                 }
604               }
605             }
606           }
607         }
608
609         if (last == null) continue;
610         Rectangle lastBounds = getPathBounds(getPath(last));
611
612         if (firstBounds == null || lastBounds == null) continue;
613
614         Rectangle toPaint = new Rectangle(firstBounds.x, firstBounds.y, 0, (int)lastBounds.getMaxY() - firstBounds.y - 1);
615
616         toPaint.width = getWidth() - toPaint.x - 4;
617
618         Color highlightColor = first.getHighlightColor();
619         g.setColor(highlightColor);
620         g.fillRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
621         g.setColor(highlightColor.darker());
622         g.drawRoundRect(toPaint.x, toPaint.y, toPaint.width, toPaint.height, 4, 4);
623       }
624     }
625
626     config.restore();
627   }
628
629   private int[] getMax(PresentableNodeDescriptor node, AbstractTreeStructure structure) {
630     int x = 0;
631     int y = 0;
632     Object[] children = structure.getChildElements(node);
633     for (Object child : children) {
634       if (child instanceof PresentableNodeDescriptor) {
635         TreePath childPath = getPath((PresentableNodeDescriptor)child);
636         if (childPath != null) {
637           if (isExpanded(childPath)) {
638             int[] tmp = getMax((PresentableNodeDescriptor)child, structure);
639             y = Math.max(y, tmp[0]);
640             x = Math.max(x, tmp[1]);
641           }
642
643           Rectangle r = getPathBounds(childPath);
644           if (r != null) {
645             y = Math.max(y, (int)r.getMaxY());
646             x = Math.max(x, (int)r.getMaxX());
647           }
648         }
649       }
650     }
651
652     return new int[]{y, x};
653   }
654
655   @Nullable
656   private static PresentableNodeDescriptor toPresentableNode(Object pathComponent) {
657     if (!(pathComponent instanceof DefaultMutableTreeNode)) return null;
658     Object userObject = ((DefaultMutableTreeNode)pathComponent).getUserObject();
659     if (!(userObject instanceof PresentableNodeDescriptor)) return null;
660     return (PresentableNodeDescriptor)userObject;
661   }
662
663   public TreePath getPath(PresentableNodeDescriptor node) {
664     AbstractTreeBuilder builder = AbstractTreeBuilder.getBuilderFor(this);
665     DefaultMutableTreeNode treeNode = builder.getNodeForElement(node);
666
667     return treeNode != null ? new TreePath(treeNode.getPath()) : new TreePath(node);
668   }
669
670   private static class MySelectionModel extends DefaultTreeSelectionModel {
671
672     private TreePath[] myHeldSelection;
673
674     @Override
675     protected void fireValueChanged(TreeSelectionEvent e) {
676       if (myHeldSelection == null) {
677         super.fireValueChanged(e);
678       }
679     }
680
681     public void holdSelection() {
682       myHeldSelection = getSelectionPaths();
683     }
684
685     public void unholdSelection() {
686       if (myHeldSelection != null) {
687         setSelectionPaths(myHeldSelection);
688         myHeldSelection = null;
689       }
690     }
691   }
692
693   private class MyMouseListener extends MouseAdapter {
694     @Override
695     public void mousePressed(MouseEvent event) {
696       setPressed(event, true);
697
698       if (!JBSwingUtilities.isLeftMouseButton(event) &&
699           (JBSwingUtilities.isRightMouseButton(event) || JBSwingUtilities.isMiddleMouseButton(event))) {
700         TreePath path = getClosestPathForLocation(event.getX(), event.getY());
701         if (path == null) return;
702
703         Rectangle bounds = getPathBounds(path);
704         if (bounds != null && bounds.y + bounds.height < event.getY()) return;
705
706         if (getSelectionModel().getSelectionMode() != TreeSelectionModel.SINGLE_TREE_SELECTION) {
707           TreePath[] selectionPaths = getSelectionModel().getSelectionPaths();
708           if (selectionPaths != null) {
709             for (TreePath selectionPath : selectionPaths) {
710               if (selectionPath != null && selectionPath.equals(path)) return;
711             }
712           }
713         }
714         getSelectionModel().setSelectionPath(path);
715       }
716     }
717
718     @Override
719     public void mouseReleased(MouseEvent event) {
720       setPressed(event, false);
721       if (event.getButton() == MouseEvent.BUTTON1 &&
722           event.getClickCount() == 2 &&
723           isLocationInExpandControl(getClosestPathForLocation(event.getX(), event.getY()), event.getX())) {
724         event.consume();
725       }
726     }
727
728     @Override
729     public void mouseExited(MouseEvent e) {
730       if (UIUtil.isUnderWin10LookAndFeel() && rollOverPath != null) {
731         TreeCellRenderer renderer = getCellRenderer();
732         if (rollOverPath.getLastPathComponent() instanceof TreeNode) {
733           TreeNode node = (TreeNode)rollOverPath.getLastPathComponent();
734           JComponent c = (JComponent)renderer.getTreeCellRendererComponent(
735             Tree.this, node,
736             isPathSelected(rollOverPath),
737             isExpanded(rollOverPath),
738             getModel().isLeaf(node),
739             getRowForPath(rollOverPath), hasFocus());
740
741           c.putClientProperty(UIUtil.CHECKBOX_ROLLOVER_PROPERTY, null);
742           rollOverPath = null;
743           UIUtil.repaintViewport(Tree.this);
744         }
745       }
746     }
747
748     private void setPressed(MouseEvent e, boolean pressed) {
749       if (UIUtil.isUnderWin10LookAndFeel()) {
750         Point p = e.getPoint();
751         TreePath path = getPathForLocation(p.x, p.y);
752         if (path != null) {
753           if (path.getLastPathComponent() instanceof TreeNode) {
754             TreeNode node = (TreeNode)path.getLastPathComponent();
755             JComponent c = (JComponent)getCellRenderer().getTreeCellRendererComponent(
756               Tree.this, node,
757               isPathSelected(path), isExpanded(path),
758               getModel().isLeaf(node),
759               getRowForPath(path), hasFocus());
760             if (pressed) {
761               c.putClientProperty(UIUtil.CHECKBOX_PRESSED_PROPERTY, c instanceof JCheckBox ? getPathBounds(path) : node);
762             }
763             else {
764               c.putClientProperty(UIUtil.CHECKBOX_PRESSED_PROPERTY, null);
765             }
766             UIUtil.repaintViewport(Tree.this);
767           }
768         }
769       }
770     }
771   }
772
773   /**
774    * This is patch for 4893787 SUN bug. The problem is that the BasicTreeUI.FocusHandler repaints
775    * only lead selection index on focus changes. It's a problem with multiple selected nodes.
776    */
777   private class MyFocusListener extends FocusAdapter {
778     private void focusChanges() {
779       TreePath[] paths = getSelectionPaths();
780       if (paths != null) {
781         TreeUI ui = getUI();
782         for (int i = paths.length - 1; i >= 0; i--) {
783           Rectangle bounds = ui.getPathBounds(Tree.this, paths[i]);
784           if (bounds != null) {
785             repaint(bounds);
786           }
787         }
788       }
789     }
790
791     @Override
792     public void focusGained(FocusEvent e) {
793       focusChanges();
794     }
795
796     @Override
797     public void focusLost(FocusEvent e) {
798       focusChanges();
799     }
800   }
801
802   public final void setLineStyleAngled() {
803     UIUtil.setLineStyleAngled(this);
804   }
805
806   @NotNull
807   public <T> T[] getSelectedNodes(Class<T> nodeType, @Nullable NodeFilter<T> filter) {
808     TreePath[] paths = getSelectionPaths();
809     if (paths == null) return (T[])Array.newInstance(nodeType, 0);
810
811     ArrayList<T> nodes = new ArrayList<>();
812     for (TreePath path : paths) {
813       Object last = path.getLastPathComponent();
814       if (nodeType.isAssignableFrom(last.getClass())) {
815         if (filter != null && !filter.accept((T)last)) continue;
816         nodes.add((T)last);
817       }
818     }
819     T[] result = (T[])Array.newInstance(nodeType, nodes.size());
820     nodes.toArray(result);
821     return result;
822   }
823
824   public interface NodeFilter<T> {
825     boolean accept(T node);
826   }
827
828   @Override
829   public void putInfo(@NotNull Map<String, String> info) {
830     TreePath[] selection = getSelectionPaths();
831     if (selection == null) return;
832
833     StringBuilder nodesText = new StringBuilder();
834
835     for (TreePath eachPath : selection) {
836       Object eachNode = eachPath.getLastPathComponent();
837       Component c =
838         getCellRenderer().getTreeCellRendererComponent(this, eachNode, false, false, false, getRowForPath(eachPath), false);
839
840       if (c != null) {
841         if (nodesText.length() > 0) {
842           nodesText.append(";");
843         }
844         nodesText.append(c);
845       }
846     }
847
848     if (nodesText.length() > 0) {
849       info.put("selectedNodes", nodesText.toString());
850     }
851   }
852
853   public void setHoldSize(boolean hold) {
854     if (hold && myHoldSize == null) {
855       myHoldSize = getPreferredSize();
856     }
857     else if (!hold && myHoldSize != null) {
858       myHoldSize = null;
859       revalidate();
860     }
861   }
862
863   @Override
864   public Dimension getPreferredSize() {
865     Dimension size = super.getPreferredSize();
866
867     if (myHoldSize != null) {
868       size.width = Math.max(size.width, myHoldSize.width);
869       size.height = Math.max(size.height, myHoldSize.height);
870     }
871
872     return size;
873   }
874
875   public boolean isHorizontalAutoScrollingEnabled() {
876     return myHorizontalAutoScrolling;
877   }
878
879   public void setHorizontalAutoScrollingEnabled(boolean enabled) {
880     myHorizontalAutoScrolling = enabled;
881   }
882
883   /**
884    * Returns the deepest visible component
885    * that will be rendered at the specified location.
886    *
887    * @param x horizontal location in the tree
888    * @param y vertical location in the tree
889    * @return the deepest visible component of the renderer
890    */
891   @Nullable
892   public Component getDeepestRendererComponentAt(int x, int y) {
893     int row = getRowForLocation(x, y);
894     if (row >= 0) {
895       TreeCellRenderer renderer = getCellRenderer();
896       if (renderer != null) {
897         TreePath path = getPathForRow(row);
898         Object node = path.getLastPathComponent();
899         Component component = renderer.getTreeCellRendererComponent(
900           this, node,
901           isRowSelected(row),
902           isExpanded(row),
903           getModel().isLeaf(node),
904           row, true);
905         Rectangle bounds = getPathBounds(path);
906         if (bounds != null) {
907           component.setBounds(bounds); // initialize size to layout complex renderer
908           return SwingUtilities.getDeepestComponentAt(component, x - bounds.x, y - bounds.y);
909         }
910       }
911     }
912     return null;
913   }
914 }