1 package com
.intellij
.openapi
.options
.newEditor
;
3 import com
.intellij
.ide
.util
.treeView
.NodeDescriptor
;
4 import com
.intellij
.openapi
.Disposable
;
5 import com
.intellij
.openapi
.options
.Configurable
;
6 import com
.intellij
.openapi
.options
.ConfigurableGroup
;
7 import com
.intellij
.openapi
.options
.SearchableConfigurable
;
8 import com
.intellij
.openapi
.project
.Project
;
9 import com
.intellij
.openapi
.util
.ActionCallback
;
10 import com
.intellij
.openapi
.util
.Disposer
;
11 import com
.intellij
.openapi
.util
.SystemInfo
;
12 import com
.intellij
.ui
.ErrorLabel
;
13 import com
.intellij
.ui
.GroupedElementsRenderer
;
14 import com
.intellij
.ui
.LoadingNode
;
15 import com
.intellij
.ui
.TreeUIHelper
;
16 import com
.intellij
.ui
.components
.panels
.NonOpaquePanel
;
17 import com
.intellij
.ui
.treeStructure
.*;
18 import com
.intellij
.ui
.treeStructure
.filtered
.FilteringTreeBuilder
;
19 import com
.intellij
.ui
.treeStructure
.filtered
.FilteringTreeStructure
;
20 import com
.intellij
.util
.ui
.UIUtil
;
21 import com
.intellij
.util
.ui
.update
.MergingUpdateQueue
;
22 import com
.intellij
.util
.ui
.update
.Update
;
23 import org
.jetbrains
.annotations
.Nullable
;
26 import javax
.swing
.border
.EmptyBorder
;
27 import javax
.swing
.event
.TreeExpansionEvent
;
28 import javax
.swing
.event
.TreeExpansionListener
;
29 import javax
.swing
.event
.TreeSelectionEvent
;
30 import javax
.swing
.event
.TreeSelectionListener
;
31 import javax
.swing
.plaf
.TreeUI
;
32 import javax
.swing
.plaf
.basic
.BasicTreeUI
;
33 import javax
.swing
.tree
.DefaultMutableTreeNode
;
34 import javax
.swing
.tree
.TreeCellRenderer
;
35 import javax
.swing
.tree
.TreePath
;
36 import javax
.swing
.tree
.TreeSelectionModel
;
38 import java
.awt
.event
.*;
40 import java
.util
.List
;
42 public class OptionsTree
extends JPanel
implements Disposable
, OptionsEditorColleague
{
44 final SimpleTree myTree
;
45 List
<ConfigurableGroup
> myGroups
;
46 FilteringTreeBuilder myBuilder
;
48 OptionsEditorContext myContext
;
50 Map
<Configurable
, EditorNode
> myConfigurable2Node
= new HashMap
<Configurable
, EditorNode
>();
52 MergingUpdateQueue mySelection
;
53 private final OptionsTree
.Renderer myRendrer
;
55 public OptionsTree(Project project
, ConfigurableGroup
[] groups
, OptionsEditorContext context
) {
57 myGroups
= Arrays
.asList(groups
);
62 final SimpleTreeStructure structure
= new SimpleTreeStructure() {
63 public Object
getRootElement() {
68 myTree
= new MyTree();
69 myTree
.setBorder(new EmptyBorder(0, 1, 0, 0));
71 myTree
.setRowHeight(-1);
72 myTree
.getSelectionModel().setSelectionMode(TreeSelectionModel
.SINGLE_TREE_SELECTION
);
73 myRendrer
= new Renderer();
74 myTree
.setCellRenderer(myRendrer
);
75 myTree
.setRootVisible(false);
76 myTree
.setShowsRootHandles(false);
77 myBuilder
= new MyBuilder(structure
);
78 myBuilder
.setFilteringMerge(300, null);
79 Disposer
.register(this, myBuilder
);
81 myBuilder
.updateFromRoot();
83 setLayout(new BorderLayout());
85 myTree
.addComponentListener(new ComponentAdapter() {
87 public void componentResized(final ComponentEvent e
) {
92 public void componentMoved(final ComponentEvent e
) {
97 public void componentShown(final ComponentEvent e
) {
102 final JScrollPane scrolls
= new JScrollPane(myTree
);
103 scrolls
.setVerticalScrollBarPolicy(ScrollPaneConstants
.VERTICAL_SCROLLBAR_ALWAYS
);
105 add(scrolls
, BorderLayout
.CENTER
);
107 mySelection
= new MergingUpdateQueue("OptionsTree", 150, false, this, this, this).setRestartTimerOnAdd(true);
108 myTree
.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
109 public void valueChanged(final TreeSelectionEvent e
) {
110 final TreePath path
= e
.getNewLeadSelectionPath();
112 queueSelection(null);
115 final Base base
= extractNode(path
.getLastPathComponent());
116 queueSelection(base
!= null ? base
.getConfigurable() : null);
120 myTree
.addKeyListener(new KeyListener() {
121 public void keyTyped(final KeyEvent e
) {
125 public void keyPressed(final KeyEvent e
) {
129 public void keyReleased(final KeyEvent e
) {
135 protected void _onTreeKeyEvent(KeyEvent e
) {
136 final KeyStroke stroke
= KeyStroke
.getKeyStrokeForEvent(e
);
138 final Object action
= myTree
.getInputMap().get(stroke
);
139 if (action
== null) {
144 protected void onTreeKeyEvent(KeyEvent e
) {
149 ActionCallback
select(@Nullable Configurable configurable
) {
150 return queueSelection(configurable
);
153 public void selectFirst() {
154 for (ConfigurableGroup eachGroup
: myGroups
) {
155 final Configurable
[] kids
= eachGroup
.getConfigurables();
156 if (kids
.length
> 0) {
157 queueSelection(kids
[0]);
163 ActionCallback
queueSelection(final Configurable configurable
) {
164 final ActionCallback callback
= new ActionCallback();
165 final Update update
= new Update(this) {
167 if (configurable
== null) {
168 myTree
.getSelectionModel().clearSelection();
169 myContext
.fireSelected(null, OptionsTree
.this);
172 final EditorNode editorNode
= myConfigurable2Node
.get(configurable
);
173 FilteringTreeStructure
.Node editorUiNode
= myBuilder
.getVisibleNodeFor(editorNode
);
174 if (!myBuilder
.getSelectedElements().contains(editorUiNode
)) {
175 myBuilder
.select(editorUiNode
, new Runnable() {
177 fireSelected(configurable
, callback
);
181 myBuilder
.scrollSelectionToVisible(new Runnable() {
183 fireSelected(configurable
, callback
);
191 public void setRejected() {
193 callback
.setRejected();
196 mySelection
.queue(update
);
200 private void fireSelected(Configurable configurable
, final ActionCallback callback
) {
201 myContext
.fireSelected(configurable
, this).doWhenProcessed(new Runnable() {
208 void revalidateTree() {
210 myTree
.setRowHeight(myTree
.getRowHeight() == -1 ?
-2 : -1);
215 public JTree
getTree() {
219 public List
<Configurable
> getPathToRoot(final Configurable configurable
) {
220 final ArrayList
<Configurable
> path
= new ArrayList
<Configurable
>();
222 EditorNode eachNode
= myConfigurable2Node
.get(configurable
);
223 if (eachNode
== null) return path
;
225 while (eachNode
!= null) {
226 path
.add(eachNode
.getConfigurable());
227 final SimpleNode parent
= eachNode
.getParent();
228 if (parent
instanceof EditorNode
) {
229 eachNode
= (EditorNode
)parent
;
240 public Configurable
getParentFor(final Configurable configurable
) {
241 final List
<Configurable
> path
= getPathToRoot(configurable
);
242 if (path
.size() > 1) {
250 public SimpleNode
findNodeFor(final Configurable toSelect
) {
251 return myConfigurable2Node
.get(toSelect
);
254 class Renderer
extends GroupedElementsRenderer
.Tree
implements TreeCellRenderer
{
257 private JLabel myHandle
;
260 protected void layout() {
261 myRendererComponent
.setOpaqueActive(false);
263 myRendererComponent
.add(mySeparatorComponent
, BorderLayout
.NORTH
);
265 final NonOpaquePanel content
= new NonOpaquePanel(new BorderLayout());
266 myHandle
= new JLabel("", JLabel
.CENTER
);
267 if (!SystemInfo
.isMac
) {
268 myHandle
.setBorder(new EmptyBorder(0, 2, 0, 2));
270 myHandle
.setOpaque(false);
271 content
.add(myHandle
, BorderLayout
.WEST
);
272 content
.add(myComponent
, BorderLayout
.CENTER
);
273 myRendererComponent
.add(content
, BorderLayout
.CENTER
);
276 public Component
getTreeCellRendererComponent(final JTree tree
,
278 final boolean selected
,
279 final boolean expanded
,
282 final boolean hasFocus
) {
286 Color fg
= UIUtil
.getTreeTextForeground();
288 final Base base
= extractNode(value
);
289 if (base
instanceof EditorNode
) {
290 DefaultMutableTreeNode node
= (DefaultMutableTreeNode
)value
;
292 final EditorNode editor
= (EditorNode
)base
;
293 ConfigurableGroup group
= null;
294 if (editor
.getParent() == myRoot
) {
295 final DefaultMutableTreeNode prevValue
= ((DefaultMutableTreeNode
)value
).getPreviousSibling();
296 if (prevValue
== null || prevValue
instanceof LoadingNode
) {
297 group
= editor
.getGroup();
300 final Base prevBase
= extractNode(prevValue
);
301 if (prevBase
instanceof EditorNode
) {
302 final EditorNode prevEditor
= (EditorNode
)prevBase
;
303 if (prevEditor
.getGroup() != editor
.getGroup()) {
304 group
= editor
.getGroup();
310 int forcedWidth
= 2000;
311 TreePath path
= tree
.getPathForRow(row
);
313 if (value
instanceof DefaultMutableTreeNode
) {
314 path
= new TreePath(((DefaultMutableTreeNode
)value
).getPath());
318 final boolean toStretch
= tree
.isVisible() && path
!= null;
321 final Rectangle visibleRect
= tree
.getVisibleRect();
323 int nestingLevel
= tree
.isRootVisible() ? path
.getPathCount() - 1 : path
.getPathCount() - 2;
325 final int left
= UIManager
.getInt("Tree.leftChildIndent");
326 final int right
= UIManager
.getInt("Tree.rightChildIndent");
328 final Insets treeInsets
= tree
.getInsets();
330 int indent
= (left
+ right
) * nestingLevel
+ (treeInsets
!= null ? treeInsets
.left
+ treeInsets
.right
: 0);
332 forcedWidth
= visibleRect
.width
> 0 ? visibleRect
.width
- indent
: forcedWidth
;
335 result
= configureComponent(base
.getText(), base
.getText(), null, null, row
== -1 ?
true : selected
, group
!= null,
336 group
!= null ? group
.getDisplayName() : null, forcedWidth
- 4);
339 if (base
.isError()) {
342 else if (base
.isModified()) {
348 result
= configureComponent(value
.toString(), null, null, null, selected
, false, null, -1);
351 if (value
instanceof DefaultMutableTreeNode
) {
352 DefaultMutableTreeNode node
= (DefaultMutableTreeNode
)value
;
353 TreePath nodePath
= new TreePath(node
.getPath());
354 myHandle
.setIcon(((SimpleTree
)tree
).getHandleIcon(node
, nodePath
));
356 myHandle
.setIcon(null);
360 myTextLabel
.setForeground(selected ? UIUtil
.getTreeSelectionForeground() : fg
);
362 myTextLabel
.setOpaque(selected
);
367 protected JComponent
createItemComponent() {
368 myTextLabel
= new ErrorLabel();
372 public boolean isUnderHandle(final Point point
) {
373 final Point handlePoint
= SwingUtilities
.convertPoint(myRendererComponent
, point
, myHandle
);
374 final Rectangle bounds
= myHandle
.getBounds();
375 return bounds
.x
< handlePoint
.x
&& bounds
.getMaxX() >= handlePoint
.x
;
380 private Base
extractNode(Object object
) {
381 if (object
instanceof DefaultMutableTreeNode
) {
382 final DefaultMutableTreeNode uiNode
= (DefaultMutableTreeNode
)object
;
383 final Object o
= uiNode
.getUserObject();
384 if (o
instanceof FilteringTreeStructure
.Node
) {
385 return (Base
)((FilteringTreeStructure
.Node
)o
).getDelegate();
392 abstract class Base
extends CachingSimpleNode
{
394 protected Base(final SimpleNode aParent
) {
402 boolean isModified() {
410 Configurable
getConfigurable() {
415 class Root
extends Base
{
421 protected SimpleNode
[] buildChildren() {
422 ArrayList
<SimpleNode
> result
= new ArrayList
<SimpleNode
>();
423 for (int i
= 0; i
< myGroups
.size(); i
++) {
424 ConfigurableGroup eachGroup
= myGroups
.get(i
);
425 result
.addAll(buildGroup(eachGroup
));
428 return result
.toArray(new SimpleNode
[result
.size()]);
431 private List
<EditorNode
> buildGroup(final ConfigurableGroup eachGroup
) {
432 List
<EditorNode
> result
= new ArrayList
<EditorNode
>();
433 final Configurable
[] kids
= eachGroup
.getConfigurables();
434 if (kids
.length
> 0) {
435 for (Configurable eachKid
: kids
) {
436 if (isInvisibleNode(eachKid
)) {
437 result
.addAll(OptionsTree
.this.buildChildren(eachKid
, this, eachGroup
));
440 result
.add(new EditorNode(this, eachKid
, eachGroup
));
449 private boolean isInvisibleNode(final Configurable child
) {
450 return child
instanceof SearchableConfigurable
.Parent
&& !((SearchableConfigurable
.Parent
)child
).isVisible();
453 private static List
<EditorNode
> sort(List
<EditorNode
> c
) {
454 List
<EditorNode
> cc
= new ArrayList
<EditorNode
>(c
);
455 Collections
.sort(cc
, new Comparator
<EditorNode
>() {
456 public int compare(final EditorNode o1
, final EditorNode o2
) {
457 return getConfigurableDisplayName(o1
.getConfigurable()).compareToIgnoreCase(getConfigurableDisplayName(o2
.getConfigurable()));
463 private static String
getConfigurableDisplayName(final Configurable c
) {
464 final String name
= c
.getDisplayName();
465 return name
!= null ? name
: "{ Unnamed Page:" + c
.getClass().getSimpleName() + " }";
468 private List
<EditorNode
> buildChildren(final Configurable configurable
, SimpleNode parent
, final ConfigurableGroup group
) {
469 if (configurable
instanceof Configurable
.Composite
) {
470 final Configurable
[] kids
= ((Configurable
.Composite
)configurable
).getConfigurables();
471 final List
<EditorNode
> result
= new ArrayList
<EditorNode
>(kids
.length
);
472 for (Configurable child
: kids
) {
473 if (isInvisibleNode(child
)) {
474 result
.addAll(buildChildren(child
, parent
, group
));
476 result
.add(new EditorNode(parent
, child
, group
));
477 myContext
.registerKid(configurable
, child
);
479 return result
; // TODO: DECIDE IF INNERS SHOULD BE SORTED: sort(result);
482 return Collections
.EMPTY_LIST
;
486 class EditorNode
extends Base
{
488 Configurable myConfigurable
;
489 ConfigurableGroup myGroup
;
491 EditorNode(SimpleNode parent
, Configurable configurable
, @Nullable ConfigurableGroup group
) {
493 myConfigurable
= configurable
;
495 myConfigurable2Node
.put(configurable
, this);
496 addPlainText(getConfigurableDisplayName(configurable
));
499 protected EditorNode
[] buildChildren() {
500 List
<EditorNode
> list
= OptionsTree
.this.buildChildren(myConfigurable
, this, null);
501 return list
.toArray(new EditorNode
[list
.size()]);
505 public boolean isContentHighlighted() {
506 return getParent() == myRoot
;
510 Configurable
getConfigurable() {
511 return myConfigurable
;
515 public int getWeight() {
516 if (getParent() == myRoot
) {
517 return Integer
.MAX_VALUE
- myGroups
.indexOf(myGroup
);
520 return WeightBasedComparator
.UNDEFINED_WEIGHT
;
524 public ConfigurableGroup
getGroup() {
530 return getConfigurableDisplayName(myConfigurable
).replace("\n", " ");
534 boolean isModified() {
535 return myContext
.getModified().contains(myConfigurable
);
540 return myContext
.getErrors().containsKey(myConfigurable
);
544 public void dispose() {
547 public ActionCallback
onSelected(final Configurable configurable
, final Configurable oldConfigurable
) {
548 return queueSelection(configurable
);
551 public ActionCallback
onModifiedAdded(final Configurable colleague
) {
553 return new ActionCallback
.Done();
556 public ActionCallback
onModifiedRemoved(final Configurable configurable
) {
558 return new ActionCallback
.Done();
561 public ActionCallback
onErrorsChanged() {
562 return new ActionCallback
.Done();
565 public void processTextEvent(KeyEvent e
) {
566 myTree
.processKeyEvent(e
);
569 private class MyTree
extends SimpleTree
{
572 getInputMap().clear();
576 protected boolean highlightSingleNode() {
581 public void setUI(final TreeUI ui
) {
582 TreeUI actualUI
= ui
;
583 if (!(ui
instanceof MyTreeUi
)) {
584 actualUI
= new MyTreeUi();
586 super.setUI(actualUI
);
590 protected void configureUiHelper(final TreeUIHelper helper
) {
591 helper
.installToolTipHandler(this);
595 public boolean getScrollableTracksViewportWidth() {
601 public void processKeyEvent(final KeyEvent e
) {
602 TreePath path
= myTree
.getSelectionPath();
604 if (e
.getKeyCode() == KeyEvent
.VK_LEFT
) {
605 if (isExpanded(path
)) {
609 } else if (e
.getKeyCode() == KeyEvent
.VK_RIGHT
) {
610 if (isCollapsed(path
)) {
617 super.processKeyEvent(e
);
621 protected void processMouseEvent(final MouseEvent e
) {
622 final MyTreeUi ui
= (MyTreeUi
)myTree
.getUI();
623 if (e
.getID() == MouseEvent
.MOUSE_RELEASED
&& UIUtil
.isActionClick(e
, MouseEvent
.MOUSE_RELEASED
) && !ui
.isToggleEvent(e
)) {
624 final TreePath path
= getPathForLocation(e
.getX(), e
.getY());
626 final Rectangle bounds
= getPathBounds(path
);
627 if (bounds
!= null && path
.getLastPathComponent() instanceof DefaultMutableTreeNode
) {
628 DefaultMutableTreeNode node
= (DefaultMutableTreeNode
)path
.getLastPathComponent();
629 final boolean selected
= isPathSelected(path
);
630 final boolean expanded
= isExpanded(path
);
631 final Component comp
=
632 myRendrer
.getTreeCellRendererComponent(this, node
, selected
, expanded
, node
.isLeaf(), getRowForPath(path
), isFocusOwner());
634 comp
.setBounds(bounds
);
637 Point point
= new Point(e
.getX() - bounds
.x
, e
.getY() - bounds
.y
);
638 if (myRendrer
.isUnderHandle(point
)) {
639 ui
.toggleExpandState(path
);
647 super.processMouseEvent(e
);
650 private class MyTreeUi
extends BasicTreeUI
{
653 public void toggleExpandState(final TreePath path
) {
654 super.toggleExpandState(path
);
658 public boolean isToggleEvent(final MouseEvent event
) {
659 return super.isToggleEvent(event
);
663 protected boolean shouldPaintExpandControl(final TreePath path
,
665 final boolean isExpanded
,
666 final boolean hasBeenExpanded
,
667 final boolean isLeaf
) {
672 protected void paintHorizontalPartOfLeg(final Graphics g
,
673 final Rectangle clipBounds
,
675 final Rectangle bounds
,
678 final boolean isExpanded
,
679 final boolean hasBeenExpanded
,
680 final boolean isLeaf
) {
685 protected void paintVerticalPartOfLeg(final Graphics g
, final Rectangle clipBounds
, final Insets insets
, final TreePath path
) {
690 private class MyBuilder
extends FilteringTreeBuilder
{
692 List
<Object
> myToExpandOnResetFilter
;
693 boolean myRefilteringNow
;
694 boolean myWasHoldingFilter
;
696 public MyBuilder(SimpleTreeStructure structure
) {
697 super(OptionsTree
.this.myProject
, OptionsTree
.this.myTree
, OptionsTree
.this.myContext
.getFilter(), structure
, new WeightBasedComparator(false));
698 myTree
.addTreeExpansionListener(new TreeExpansionListener() {
699 public void treeExpanded(TreeExpansionEvent event
) {
700 invalidateExpansions();
703 public void treeCollapsed(TreeExpansionEvent event
) {
704 invalidateExpansions();
709 private void invalidateExpansions() {
710 if (!myRefilteringNow
) {
711 myToExpandOnResetFilter
= null;
716 protected boolean isSelectable(final Object nodeObject
) {
717 return nodeObject
instanceof EditorNode
;
721 public boolean isAutoExpandNode(final NodeDescriptor nodeDescriptor
) {
722 return myContext
.isHoldingFilter();
726 protected ActionCallback
refilterNow(Object preferredSelection
, boolean adjustSelection
) {
727 final List
<Object
> toRestore
= new ArrayList
<Object
>();
728 if (myContext
.isHoldingFilter() && !myWasHoldingFilter
&& myToExpandOnResetFilter
== null) {
729 myToExpandOnResetFilter
= myBuilder
.getUi().getExpandedElements();
730 } else if (!myContext
.isHoldingFilter() && myWasHoldingFilter
&& myToExpandOnResetFilter
!= null) {
731 toRestore
.addAll(myToExpandOnResetFilter
);
732 myToExpandOnResetFilter
= null;
735 myWasHoldingFilter
= myContext
.isHoldingFilter();
737 ActionCallback result
= super.refilterNow(preferredSelection
, adjustSelection
);
738 myRefilteringNow
= true;
739 return result
.doWhenDone(new Runnable() {
741 myRefilteringNow
= false;
742 if (!myContext
.isHoldingFilter()) {
743 restoreExpandedState(toRestore
);
749 private void restoreExpandedState(List
<Object
> toRestore
) {
750 TreePath
[] selected
= myTree
.getSelectionPaths();
751 if (selected
== null) {
752 selected
= new TreePath
[0];
755 List
<TreePath
> toCollapse
= new ArrayList
<TreePath
>();
757 for (int eachRow
= 0; eachRow
< myTree
.getRowCount(); eachRow
++) {
758 if (!myTree
.isExpanded(eachRow
)) continue;
760 TreePath eachVisiblePath
= myTree
.getPathForRow(eachRow
);
761 if (eachVisiblePath
== null) continue;
763 Object eachElement
= myBuilder
.getElementFor(eachVisiblePath
.getLastPathComponent());
764 if (toRestore
.contains(eachElement
)) continue;
767 for (TreePath eachSelected
: selected
) {
768 if (!eachVisiblePath
.isDescendant(eachSelected
)) {
769 toCollapse
.add(eachVisiblePath
);
774 for (TreePath each
: toCollapse
) {
775 myTree
.collapsePath(each
);