9269da7ab967841855ea49b243aa8a0ea178765c
[idea/community.git] / platform / platform-impl / src / com / intellij / openapi / options / newEditor / SettingsTreeView.java
1 /*
2  * Copyright 2000-2014 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.openapi.options.newEditor;
17
18 import com.intellij.icons.AllIcons;
19 import com.intellij.ide.ui.search.ConfigurableHit;
20 import com.intellij.ide.util.treeView.NodeDescriptor;
21 import com.intellij.openapi.Disposable;
22 import com.intellij.openapi.options.*;
23 import com.intellij.openapi.options.ex.ConfigurableWrapper;
24 import com.intellij.openapi.options.ex.NodeConfigurable;
25 import com.intellij.openapi.project.Project;
26 import com.intellij.openapi.util.ActionCallback;
27 import com.intellij.openapi.util.Disposer;
28 import com.intellij.ui.*;
29 import com.intellij.ui.treeStructure.*;
30 import com.intellij.ui.treeStructure.filtered.FilteringTreeBuilder;
31 import com.intellij.ui.treeStructure.filtered.FilteringTreeStructure;
32 import com.intellij.util.ArrayUtil;
33 import com.intellij.util.ui.GraphicsUtil;
34 import com.intellij.util.ui.UIUtil;
35 import com.intellij.util.ui.tree.TreeUtil;
36 import com.intellij.util.ui.update.MergingUpdateQueue;
37 import com.intellij.util.ui.update.Update;
38 import org.jetbrains.annotations.NotNull;
39 import org.jetbrains.annotations.Nullable;
40
41 import java.awt.*;
42 import java.awt.event.*;
43 import java.util.*;
44 import java.util.List;
45 import javax.swing.*;
46 import javax.swing.event.TreeExpansionEvent;
47 import javax.swing.event.TreeExpansionListener;
48 import javax.swing.event.TreeSelectionEvent;
49 import javax.swing.event.TreeSelectionListener;
50 import javax.swing.plaf.TreeUI;
51 import javax.swing.plaf.basic.BasicTreeUI;
52 import javax.swing.tree.DefaultMutableTreeNode;
53 import javax.swing.tree.TreePath;
54 import javax.swing.tree.TreeSelectionModel;
55
56 /**
57  * @author Sergey.Malenkov
58  */
59 final class SettingsTreeView extends JComponent implements Disposable, OptionsEditorColleague {
60   final SimpleTree myTree;
61   final FilteringTreeBuilder myBuilder;
62
63   private final OptionsEditorContext myContext;
64   private final MyRoot myRoot;
65   private final JScrollPane myScroller;
66   private JLabel mySeparator;
67   private final MyRenderer myRenderer = new MyRenderer();
68   private final IdentityHashMap<Configurable, MyNode> myConfigurableToNodeMap = new IdentityHashMap<Configurable, MyNode>();
69   private final MergingUpdateQueue myQueue = new MergingUpdateQueue("OptionsTree", 150, false, this, this, this).setRestartTimerOnAdd(true);
70
71   private Configurable myQueuedConfigurable;
72
73   SettingsTreeView(final KeyListener listener, OptionsEditorContext context, ConfigurableGroup... groups) {
74     myContext = context;
75     myRoot = new MyRoot(groups);
76
77     myTree = new MyTree();
78     myTree.getInputMap().clear();
79     TreeUtil.installActions(myTree);
80
81     myTree.setOpaque(true);
82     myTree.setBorder(BorderFactory.createEmptyBorder(0, 1, 0, 0));
83
84     myTree.setRowHeight(-1);
85     myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
86
87     myTree.setCellRenderer(myRenderer);
88     myTree.setRootVisible(false);
89     myTree.setShowsRootHandles(false);
90
91     myScroller = ScrollPaneFactory.createScrollPane(myTree);
92     myScroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
93     add(myScroller);
94
95     myTree.addComponentListener(new ComponentAdapter() {
96       @Override
97       public void componentResized(ComponentEvent e) {
98         myBuilder.revalidateTree();
99       }
100
101       @Override
102       public void componentMoved(ComponentEvent e) {
103         myBuilder.revalidateTree();
104       }
105
106       @Override
107       public void componentShown(ComponentEvent e) {
108         myBuilder.revalidateTree();
109       }
110     });
111
112     myTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
113       public void valueChanged(TreeSelectionEvent event) {
114         MyNode node = extractNode(event.getNewLeadSelectionPath());
115         select(node == null ? null : node.myConfigurable);
116       }
117     });
118
119     myTree.addKeyListener(new KeyListener() {
120       public void keyTyped(KeyEvent event) {
121         if (listener != null && isValid(event)) {
122           listener.keyTyped(event);
123         }
124       }
125
126       public void keyPressed(KeyEvent event) {
127         if (listener != null && isValid(event)) {
128           listener.keyPressed(event);
129         }
130       }
131
132       public void keyReleased(KeyEvent event) {
133         if (listener != null && isValid(event)) {
134           listener.keyReleased(event);
135         }
136       }
137
138       private boolean isValid(KeyEvent event) {
139         return null == myTree.getInputMap().get(KeyStroke.getKeyStrokeForEvent(event));
140       }
141     });
142     myBuilder = new MyBuilder(new SimpleTreeStructure.Impl(myRoot));
143     myBuilder.setFilteringMerge(300, null);
144     Disposer.register(this, myBuilder);
145   }
146
147   @NotNull
148   String[] getPathNames(Configurable configurable) {
149     ArrayDeque<String> path = new ArrayDeque<String>();
150     MyNode node = myConfigurableToNodeMap.get(configurable);
151     while (node != null) {
152       path.push(node.myDisplayName);
153       SimpleNode parent = node.getParent();
154       node = parent instanceof MyNode
155              ? (MyNode)parent
156              : null;
157     }
158     return ArrayUtil.toStringArray(path);
159   }
160
161   @Nullable
162   SimpleNode findNode(Configurable toSelect) {
163     return myConfigurableToNodeMap.get(toSelect);
164   }
165
166   @Nullable
167   SearchableConfigurable findConfigurableById(@NotNull String id) {
168     for (Configurable configurable : myConfigurableToNodeMap.keySet()) {
169       if (configurable instanceof SearchableConfigurable) {
170         SearchableConfigurable searchable = (SearchableConfigurable)configurable;
171         if (id.equals(searchable.getId())) {
172           return searchable;
173         }
174       }
175     }
176     return null;
177   }
178
179   @Nullable
180   <T extends UnnamedConfigurable> T findConfigurable(@NotNull Class<T> type) {
181     for (UnnamedConfigurable configurable : myConfigurableToNodeMap.keySet()) {
182       if (configurable instanceof ConfigurableWrapper) {
183         ConfigurableWrapper wrapper = (ConfigurableWrapper)configurable;
184         configurable = wrapper.getConfigurable();
185       }
186       if (type.isInstance(configurable)) {
187         return type.cast(configurable);
188       }
189     }
190     return null;
191   }
192
193   @Nullable
194   Project findConfigurableProject(@Nullable Configurable configurable) {
195     if (configurable instanceof ConfigurableWrapper) {
196       ConfigurableWrapper wrapper = (ConfigurableWrapper)configurable;
197       return wrapper.getExtensionPoint().getProject();
198     }
199     return findConfigurableProject(myConfigurableToNodeMap.get(configurable));
200   }
201
202   @Nullable
203   private static Project findConfigurableProject(@Nullable MyNode node) {
204     if (node != null) {
205       Configurable configurable = node.myConfigurable;
206       if (configurable instanceof ConfigurableWrapper) {
207         ConfigurableWrapper wrapper = (ConfigurableWrapper)configurable;
208         return wrapper.getExtensionPoint().getProject();
209       }
210       SimpleNode parent = node.getParent();
211       if (parent instanceof MyNode) {
212         return findConfigurableProject((MyNode)parent);
213       }
214     }
215     return null;
216   }
217
218   @Nullable
219   private ConfigurableGroup findConfigurableGroupAt(int x, int y) {
220     TreePath path = myTree.getClosestPathForLocation(x - myTree.getX(), y - myTree.getY());
221     while (path != null) {
222       MyNode node = extractNode(path);
223       if (node == null) {
224         return null;
225       }
226       if (node.myComposite instanceof ConfigurableGroup) {
227         return (ConfigurableGroup)node.myComposite;
228       }
229       path = path.getParentPath();
230     }
231     return null;
232   }
233
234   @Nullable
235   private static MyNode extractNode(@Nullable Object object) {
236     if (object instanceof TreePath) {
237       TreePath path = (TreePath)object;
238       object = path.getLastPathComponent();
239     }
240     if (object instanceof DefaultMutableTreeNode) {
241       DefaultMutableTreeNode node = (DefaultMutableTreeNode)object;
242       object = node.getUserObject();
243     }
244     if (object instanceof FilteringTreeStructure.FilteringNode) {
245       FilteringTreeStructure.FilteringNode node = (FilteringTreeStructure.FilteringNode)object;
246       object = node.getDelegate();
247     }
248     return object instanceof MyNode
249            ? (MyNode)object
250            : null;
251   }
252
253   static boolean isFiltered(Set<Configurable> configurables, ConfigurableHit hits, SimpleNode value) {
254     if (value instanceof MyNode && !configurables.contains(((MyNode)value).myConfigurable)) {
255       if (hits != null) {
256         configurables = hits.getNameFullHits();
257         while (value != null) {
258           if (value instanceof MyNode) {
259             if (configurables.contains(((MyNode)value).myConfigurable)) {
260               return true;
261             }
262           }
263           value = value.getParent();
264         }
265       }
266       return false;
267     }
268     return true;
269   }
270
271   @Override
272   public void doLayout() {
273     myScroller.setBounds(0, 0, getWidth(), getHeight());
274   }
275
276   @Override
277   public void paint(Graphics g) {
278     super.paint(g);
279
280     if (mySeparator == null) {
281       mySeparator = new JLabel();
282       mySeparator.setFont(UIUtil.getLabelFont());
283       mySeparator.setFont(getFont().deriveFont(Font.BOLD));
284     }
285     ConfigurableGroup group = findConfigurableGroupAt(0, 5 + mySeparator.getFont().getSize());
286     if (group != null && group == findConfigurableGroupAt(0, -5)) {
287       int offset = UIUtil.isUnderNativeMacLookAndFeel() ? 1 : 3;
288       mySeparator.setBorder(BorderFactory.createEmptyBorder(offset, 18, offset, 3));
289       mySeparator.setText(group.getDisplayName());
290
291       Rectangle bounds = myScroller.getViewport().getBounds();
292       int height = mySeparator.getPreferredSize().height;
293       if (bounds.height > height) {
294         bounds.height = height;
295       }
296       g.setColor(myTree.getBackground());
297       if (g instanceof Graphics2D) {
298         int h = bounds.height / 4;
299         int y = bounds.y + bounds.height - h;
300         g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height - h);
301         ((Graphics2D)g).setPaint(UIUtil.getGradientPaint(
302           0, y, g.getColor(),
303           0, y + h, ColorUtil.toAlpha(g.getColor(), 0)));
304         g.fillRect(bounds.x, y, bounds.width, h + h);
305       }
306       else {
307         g.fillRect(bounds.x, bounds.y, bounds.width, bounds.height);
308       }
309       mySeparator.setSize(bounds.width - 1, bounds.height);
310       mySeparator.paint(g.create(bounds.x + 1, bounds.y, bounds.width - 1, bounds.height));
311     }
312   }
313
314   void selectFirst() {
315     for (ConfigurableGroup eachGroup : myRoot.myGroups) {
316       Configurable[] kids = eachGroup.getConfigurables();
317       if (kids.length > 0) {
318         select(kids[0]);
319         return;
320       }
321     }
322   }
323
324   ActionCallback select(@Nullable final Configurable configurable) {
325     if (myBuilder.isSelectionBeingAdjusted()) {
326       return new ActionCallback.Rejected();
327     }
328     final ActionCallback callback = new ActionCallback();
329     myQueuedConfigurable = configurable;
330     myQueue.queue(new Update(this) {
331       public void run() {
332         if (configurable == myQueuedConfigurable) {
333           if (configurable == null) {
334             fireSelected(null, callback);
335           }
336           else {
337             myBuilder.getReady(this).doWhenDone(new Runnable() {
338               @Override
339               public void run() {
340                 if (configurable != myQueuedConfigurable) return;
341
342                 MyNode editorNode = myConfigurableToNodeMap.get(configurable);
343                 FilteringTreeStructure.FilteringNode editorUiNode = myBuilder.getVisibleNodeFor(editorNode);
344                 if (editorUiNode == null) return;
345
346                 if (!myBuilder.getSelectedElements().contains(editorUiNode)) {
347                   myBuilder.select(editorUiNode, new Runnable() {
348                     public void run() {
349                       fireSelected(configurable, callback);
350                     }
351                   });
352                 }
353                 else {
354                   myBuilder.scrollSelectionToVisible(new Runnable() {
355                     public void run() {
356                       fireSelected(configurable, callback);
357                     }
358                   }, false);
359                 }
360               }
361             });
362           }
363         }
364       }
365
366       @Override
367       public void setRejected() {
368         super.setRejected();
369         callback.setRejected();
370       }
371     });
372     return callback;
373   }
374
375   private void fireSelected(Configurable configurable, ActionCallback callback) {
376     myContext.fireSelected(configurable, this).doWhenProcessed(callback.createSetDoneRunnable());
377   }
378
379   @Override
380   public void dispose() {
381     myQueuedConfigurable = null;
382   }
383
384   @Override
385   public ActionCallback onSelected(@Nullable Configurable configurable, Configurable oldConfigurable) {
386     return select(configurable);
387   }
388
389   @Override
390   public ActionCallback onModifiedAdded(Configurable configurable) {
391     myTree.repaint();
392     return new ActionCallback.Done();
393   }
394
395   @Override
396   public ActionCallback onModifiedRemoved(Configurable configurable) {
397     myTree.repaint();
398     return new ActionCallback.Done();
399   }
400
401   @Override
402   public ActionCallback onErrorsChanged() {
403     return new ActionCallback.Done();
404   }
405
406   private final class MyRoot extends CachingSimpleNode {
407     private final ConfigurableGroup[] myGroups;
408
409     private MyRoot(ConfigurableGroup[] groups) {
410       super(null);
411       myGroups = groups;
412     }
413
414     @Override
415     protected SimpleNode[] buildChildren() {
416       if (myGroups == null || myGroups.length == 0) {
417         return NO_CHILDREN;
418       }
419       SimpleNode[] result = new SimpleNode[myGroups.length];
420       for (int i = 0; i < myGroups.length; i++) {
421         result[i] = new MyNode(this, myGroups[i]);
422       }
423       return result;
424     }
425   }
426
427   private final class MyNode extends CachingSimpleNode {
428     private final Configurable.Composite myComposite;
429     private final Configurable myConfigurable;
430     private final String myDisplayName;
431
432     private MyNode(CachingSimpleNode parent, Configurable configurable) {
433       super(parent);
434       myComposite = configurable instanceof Configurable.Composite ? (Configurable.Composite)configurable : null;
435       myConfigurable = configurable;
436       String name = configurable.getDisplayName();
437       myDisplayName = name != null ? name.replace("\n", " ") : "{ " + configurable.getClass().getSimpleName() + " }";
438
439       myConfigurableToNodeMap.put(configurable, this);
440     }
441
442     private MyNode(CachingSimpleNode parent, ConfigurableGroup group) {
443       super(parent);
444       myComposite = group;
445       myConfigurable = null;
446       String name = group.getDisplayName();
447       myDisplayName = name != null ? name.replace("\n", " ") : "{ " + group.getClass().getSimpleName() + " }";
448     }
449
450     @Override
451     protected SimpleNode[] buildChildren() {
452       if (myComposite == null) {
453         return NO_CHILDREN;
454       }
455       Configurable[] configurables = myComposite.getConfigurables();
456       if (configurables == null || configurables.length == 0) {
457         return NO_CHILDREN;
458       }
459       SimpleNode[] result = new SimpleNode[configurables.length];
460       for (int i = 0; i < configurables.length; i++) {
461         result[i] = new MyNode(this, configurables[i]);
462         if (myConfigurable != null) {
463           myContext.registerKid(myConfigurable, configurables[i]);
464         }
465       }
466       return result;
467     }
468
469     @Override
470     public boolean isAlwaysLeaf() {
471       return myComposite == null;
472     }
473
474     @Override
475     public int getWeight() {
476       return WeightBasedComparator.UNDEFINED_WEIGHT;
477     }
478   }
479
480   private final class MyRenderer extends GroupedElementsRenderer.Tree {
481     private JLabel myNodeIcon;
482     private JLabel myProjectIcon;
483
484     protected JComponent createItemComponent() {
485       myTextLabel = new ErrorLabel();
486       return myTextLabel;
487     }
488
489     @Override
490     protected void layout() {
491       myNodeIcon = new JLabel(" ", SwingConstants.RIGHT);
492       myProjectIcon = new JLabel(" ", SwingConstants.LEFT);
493       myProjectIcon.setOpaque(true);
494       myRendererComponent.add(BorderLayout.NORTH, mySeparatorComponent);
495       myRendererComponent.add(BorderLayout.CENTER, myComponent);
496       myRendererComponent.add(BorderLayout.WEST, myNodeIcon);
497       myRendererComponent.add(BorderLayout.EAST, myProjectIcon);
498     }
499
500     public Component getTreeCellRendererComponent(JTree tree,
501                                                   Object value,
502                                                   boolean selected,
503                                                   boolean expanded,
504                                                   boolean leaf,
505                                                   int row,
506                                                   boolean focused) {
507       myTextLabel.setOpaque(selected);
508       myTextLabel.setFont(UIUtil.getLabelFont());
509
510       String text;
511       boolean hasSeparatorAbove = false;
512       int preferredForcedWidth = -1;
513
514       MyNode node = extractNode(value);
515       if (node == null) {
516         text = value.toString();
517       }
518       else {
519         text = node.myDisplayName;
520         // show groups in bold
521         if (myRoot == node.getParent()) {
522           hasSeparatorAbove = node != myRoot.getChildAt(0);
523           myTextLabel.setFont(myTextLabel.getFont().deriveFont(Font.BOLD));
524         }
525         TreePath path = tree.getPathForRow(row);
526         if (path == null) {
527           if (value instanceof DefaultMutableTreeNode) {
528             path = new TreePath(((DefaultMutableTreeNode)value).getPath());
529           }
530         }
531         int forcedWidth = 2000;
532         if (path != null && tree.isVisible()) {
533           Rectangle visibleRect = tree.getVisibleRect();
534
535           int nestingLevel = tree.isRootVisible() ? path.getPathCount() - 1 : path.getPathCount() - 2;
536
537           int left = UIUtil.getTreeLeftChildIndent();
538           int right = UIUtil.getTreeRightChildIndent();
539
540           Insets treeInsets = tree.getInsets();
541
542           int indent = (left + right) * nestingLevel + (treeInsets != null ? treeInsets.left + treeInsets.right : 0);
543
544           forcedWidth = visibleRect.width > 0 ? visibleRect.width - indent : forcedWidth;
545         }
546         preferredForcedWidth = forcedWidth - 4;
547       }
548       Component result = configureComponent(text, null, null, null, selected, hasSeparatorAbove, null, preferredForcedWidth);
549       // update font color for modified configurables
550       if (!selected && node != null) {
551         Configurable configurable = node.myConfigurable;
552         if (configurable != null) {
553           if (myContext.getErrors().containsKey(configurable)) {
554             myTextLabel.setForeground(JBColor.RED);
555           }
556           else if (myContext.getModified().contains(configurable)) {
557             myTextLabel.setForeground(JBColor.BLUE);
558           }
559         }
560       }
561       // configure project icon
562       Project project = null;
563       if (node != null) {
564         SimpleNode parent = node.getParent();
565         if (parent instanceof MyNode) {
566           if (myRoot == parent.getParent()) {
567             project = findConfigurableProject(node); // show icon for top-level nodes
568             if (node.myConfigurable instanceof NodeConfigurable) { // special case for custom subgroups (build.tools)
569               Configurable[] configurables = ((NodeConfigurable)node.myConfigurable).getConfigurables();
570               if (configurables != null) { // assume that all configurables have the same project
571                 project = findConfigurableProject(configurables[0]);
572               }
573             }
574           }
575           else if (((MyNode)parent).myConfigurable instanceof NodeConfigurable) {
576             if (((MyNode)node.getParent()).myConfigurable instanceof NodeConfigurable) {
577               project = findConfigurableProject(node); // special case for custom subgroups
578             }
579           }
580         }
581       }
582       if (project != null) {
583         myProjectIcon.setIcon(selected
584                               ? AllIcons.General.ProjectConfigurableSelected
585                               : AllIcons.General.ProjectConfigurable);
586         myProjectIcon.setToolTipText(OptionsBundle.message(project.isDefault()
587                                                            ? "configurable.default.project.tooltip"
588                                                            : "configurable.current.project.tooltip"));
589         myProjectIcon.setBackground(myTextLabel.getBackground());
590         myProjectIcon.setVisible(true);
591       }
592       else {
593         myProjectIcon.setVisible(false);
594       }
595       // configure node icon
596       if (value instanceof DefaultMutableTreeNode) {
597         DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode)value;
598         TreePath treePath = new TreePath(treeNode.getPath());
599         myNodeIcon.setIcon(myTree.getHandleIcon(treeNode, treePath));
600       }
601       else {
602         myNodeIcon.setIcon(null);
603       }
604       return result;
605     }
606
607
608     public boolean isUnderHandle(Point point) {
609       Point handlePoint = SwingUtilities.convertPoint(myRendererComponent, point, myNodeIcon);
610       Rectangle bounds = myNodeIcon.getBounds();
611       return bounds.x < handlePoint.x && bounds.getMaxX() >= handlePoint.x;
612     }
613   }
614
615   private final class MyTree extends SimpleTree {
616     @Override
617     public String getToolTipText(MouseEvent event) {
618       if (event != null) {
619         Component component = getDeepestRendererComponentAt(event.getX(), event.getY());
620         if (component instanceof JLabel) {
621           JLabel label = (JLabel)component;
622           if (label.getIcon() != null) {
623             String text = label.getToolTipText();
624             if (text != null) {
625               return text;
626             }
627           }
628         }
629       }
630       return super.getToolTipText(event);
631     }
632
633     @Override
634     protected boolean paintNodes() {
635       return false;
636     }
637
638     @Override
639     protected boolean highlightSingleNode() {
640       return false;
641     }
642
643     @Override
644     public void setUI(TreeUI ui) {
645       TreeUI actualUI = ui;
646       if (!(ui instanceof MyTreeUi)) {
647         actualUI = new MyTreeUi();
648       }
649       super.setUI(actualUI);
650     }
651
652     @Override
653     protected boolean isCustomUI() {
654       return true;
655     }
656
657     @Override
658     protected void configureUiHelper(TreeUIHelper helper) {
659     }
660
661     @Override
662     public boolean getScrollableTracksViewportWidth() {
663       return true;
664     }
665
666
667     @Override
668     public void processKeyEvent(KeyEvent e) {
669       TreePath path = myTree.getSelectionPath();
670       if (path != null) {
671         if (e.getKeyCode() == KeyEvent.VK_LEFT) {
672           if (isExpanded(path)) {
673             collapsePath(path);
674             return;
675           }
676         }
677         else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
678           if (isCollapsed(path)) {
679             expandPath(path);
680             return;
681           }
682         }
683       }
684       super.processKeyEvent(e);
685     }
686
687     @Override
688     protected void processMouseEvent(MouseEvent e) {
689       MyTreeUi ui = (MyTreeUi)myTree.getUI();
690       boolean toggleNow = MouseEvent.MOUSE_RELEASED == e.getID()
691                           && UIUtil.isActionClick(e, MouseEvent.MOUSE_RELEASED)
692                           && !ui.isToggleEvent(e);
693
694       if (toggleNow || MouseEvent.MOUSE_PRESSED == e.getID()) {
695         TreePath path = getPathForLocation(e.getX(), e.getY());
696         if (path != null) {
697           Rectangle bounds = getPathBounds(path);
698           if (bounds != null && path.getLastPathComponent() instanceof DefaultMutableTreeNode) {
699             DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent();
700             boolean selected = isPathSelected(path);
701             boolean expanded = isExpanded(path);
702             Component comp =
703               myRenderer.getTreeCellRendererComponent(this, node, selected, expanded, node.isLeaf(), getRowForPath(path), isFocusOwner());
704
705             comp.setBounds(bounds);
706             comp.validate();
707
708             Point point = new Point(e.getX() - bounds.x, e.getY() - bounds.y);
709             if (myRenderer.isUnderHandle(point)) {
710               if (toggleNow) {
711                 ui.toggleExpandState(path);
712               }
713               e.consume();
714               return;
715             }
716           }
717         }
718       }
719
720       super.processMouseEvent(e);
721     }
722
723     private final class MyTreeUi extends BasicTreeUI {
724
725       @Override
726       public void toggleExpandState(TreePath path) {
727         super.toggleExpandState(path);
728       }
729
730       @Override
731       public boolean isToggleEvent(MouseEvent event) {
732         return super.isToggleEvent(event);
733       }
734
735       @Override
736       protected boolean shouldPaintExpandControl(TreePath path,
737                                                  int row,
738                                                  boolean isExpanded,
739                                                  boolean hasBeenExpanded,
740                                                  boolean isLeaf) {
741         return false;
742       }
743
744       @Override
745       protected void paintHorizontalPartOfLeg(Graphics g,
746                                               Rectangle clipBounds,
747                                               Insets insets,
748                                               Rectangle bounds,
749                                               TreePath path,
750                                               int row,
751                                               boolean isExpanded,
752                                               boolean hasBeenExpanded,
753                                               boolean isLeaf) {
754
755       }
756
757       @Override
758       protected void paintVerticalPartOfLeg(Graphics g, Rectangle clipBounds, Insets insets, TreePath path) {
759       }
760
761       @Override
762       public void paint(Graphics g, JComponent c) {
763         GraphicsUtil.setupAntialiasing(g);
764         super.paint(g, c);
765       }
766     }
767   }
768
769   private final class MyBuilder extends FilteringTreeBuilder {
770
771     List<Object> myToExpandOnResetFilter;
772     boolean myRefilteringNow;
773     boolean myWasHoldingFilter;
774
775     public MyBuilder(SimpleTreeStructure structure) {
776       super(myTree, myContext.getFilter(), structure, new WeightBasedComparator(false));
777       myTree.addTreeExpansionListener(new TreeExpansionListener() {
778         public void treeExpanded(TreeExpansionEvent event) {
779           invalidateExpansions();
780         }
781
782         public void treeCollapsed(TreeExpansionEvent event) {
783           invalidateExpansions();
784         }
785       });
786     }
787
788     private void invalidateExpansions() {
789       if (!myRefilteringNow) {
790         myToExpandOnResetFilter = null;
791       }
792     }
793
794     @Override
795     protected boolean isSelectable(Object object) {
796       return object instanceof MyNode;
797     }
798
799     @Override
800     public boolean isAutoExpandNode(NodeDescriptor nodeDescriptor) {
801       return myContext.isHoldingFilter();
802     }
803
804     @Override
805     public boolean isToEnsureSelectionOnFocusGained() {
806       return false;
807     }
808
809     @Override
810     protected ActionCallback refilterNow(Object preferredSelection, boolean adjustSelection) {
811       final List<Object> toRestore = new ArrayList<Object>();
812       if (myContext.isHoldingFilter() && !myWasHoldingFilter && myToExpandOnResetFilter == null) {
813         myToExpandOnResetFilter = myBuilder.getUi().getExpandedElements();
814       }
815       else if (!myContext.isHoldingFilter() && myWasHoldingFilter && myToExpandOnResetFilter != null) {
816         toRestore.addAll(myToExpandOnResetFilter);
817         myToExpandOnResetFilter = null;
818       }
819
820       myWasHoldingFilter = myContext.isHoldingFilter();
821
822       ActionCallback result = super.refilterNow(preferredSelection, adjustSelection);
823       myRefilteringNow = true;
824       return result.doWhenDone(new Runnable() {
825         public void run() {
826           myRefilteringNow = false;
827           if (!myContext.isHoldingFilter() && getSelectedElements().isEmpty()) {
828             restoreExpandedState(toRestore);
829           }
830         }
831       });
832     }
833
834     private void restoreExpandedState(List<Object> toRestore) {
835       TreePath[] selected = myTree.getSelectionPaths();
836       if (selected == null) {
837         selected = new TreePath[0];
838       }
839
840       List<TreePath> toCollapse = new ArrayList<TreePath>();
841
842       for (int eachRow = 0; eachRow < myTree.getRowCount(); eachRow++) {
843         if (!myTree.isExpanded(eachRow)) continue;
844
845         TreePath eachVisiblePath = myTree.getPathForRow(eachRow);
846         if (eachVisiblePath == null) continue;
847
848         Object eachElement = myBuilder.getElementFor(eachVisiblePath.getLastPathComponent());
849         if (toRestore.contains(eachElement)) continue;
850
851
852         for (TreePath eachSelected : selected) {
853           if (!eachVisiblePath.isDescendant(eachSelected)) {
854             toCollapse.add(eachVisiblePath);
855           }
856         }
857       }
858
859       for (TreePath each : toCollapse) {
860         myTree.collapsePath(each);
861       }
862     }
863   }
864 }