Settings tree: selecting & filtering fixes
[fedora-idea.git] / platform / platform-impl / src / com / intellij / openapi / options / newEditor / OptionsTree.java
blob23d64a0c0f9dbb2425ed2eb9467b03f37cd37417
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;
25 import javax.swing.*;
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;
37 import java.awt.*;
38 import java.awt.event.*;
39 import java.util.*;
40 import java.util.List;
42 public class OptionsTree extends JPanel implements Disposable, OptionsEditorColleague {
43 Project myProject;
44 final SimpleTree myTree;
45 List<ConfigurableGroup> myGroups;
46 FilteringTreeBuilder myBuilder;
47 Root myRoot;
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) {
56 myProject = project;
57 myGroups = Arrays.asList(groups);
58 myContext = context;
61 myRoot = new Root();
62 final SimpleTreeStructure structure = new SimpleTreeStructure() {
63 public Object getRootElement() {
64 return myRoot;
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() {
86 @Override
87 public void componentResized(final ComponentEvent e) {
88 revalidateTree();
91 @Override
92 public void componentMoved(final ComponentEvent e) {
93 revalidateTree();
96 @Override
97 public void componentShown(final ComponentEvent e) {
98 revalidateTree();
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();
111 if (path == null) {
112 queueSelection(null);
114 else {
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) {
122 _onTreeKeyEvent(e);
125 public void keyPressed(final KeyEvent e) {
126 _onTreeKeyEvent(e);
129 public void keyReleased(final KeyEvent e) {
130 _onTreeKeyEvent(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) {
140 onTreeKeyEvent(e);
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]);
158 return;
163 ActionCallback queueSelection(final Configurable configurable) {
164 final ActionCallback callback = new ActionCallback();
165 final Update update = new Update(this) {
166 public void run() {
167 if (configurable == null) {
168 myTree.getSelectionModel().clearSelection();
169 myContext.fireSelected(null, OptionsTree.this);
171 else {
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() {
176 public void run() {
177 fireSelected(configurable, callback);
180 } else {
181 myBuilder.scrollSelectionToVisible(new Runnable() {
182 public void run() {
183 fireSelected(configurable, callback);
185 }, false);
190 @Override
191 public void setRejected() {
192 super.setRejected();
193 callback.setRejected();
196 mySelection.queue(update);
197 return callback;
200 private void fireSelected(Configurable configurable, final ActionCallback callback) {
201 myContext.fireSelected(configurable, this).doWhenProcessed(new Runnable() {
202 public void run() {
203 callback.setDone();
208 void revalidateTree() {
209 myTree.invalidate();
210 myTree.setRowHeight(myTree.getRowHeight() == -1 ? -2 : -1);
211 myTree.revalidate();
212 myTree.repaint();
215 public JTree getTree() {
216 return myTree;
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;
231 else {
232 break;
236 return path;
239 @Nullable
240 public Configurable getParentFor(final Configurable configurable) {
241 final List<Configurable> path = getPathToRoot(configurable);
242 if (path.size() > 1) {
243 return path.get(1);
245 else {
246 return null;
250 public SimpleNode findNodeFor(final Configurable toSelect) {
251 return myConfigurable2Node.get(toSelect);
254 class Renderer extends GroupedElementsRenderer.Tree implements TreeCellRenderer {
257 private JLabel myHandle;
259 @Override
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,
277 final Object value,
278 final boolean selected,
279 final boolean expanded,
280 final boolean leaf,
281 final int row,
282 final boolean hasFocus) {
285 JComponent result;
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();
299 else {
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);
312 if (path == null) {
313 if (value instanceof DefaultMutableTreeNode) {
314 path = new TreePath(((DefaultMutableTreeNode)value).getPath());
318 final boolean toStretch = tree.isVisible() && path != null;
320 if (toStretch) {
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()) {
340 fg = Color.red;
342 else if (base.isModified()) {
343 fg = Color.blue;
347 else {
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));
355 } else {
356 myHandle.setIcon(null);
360 myTextLabel.setForeground(selected ? UIUtil.getTreeSelectionForeground() : fg);
362 myTextLabel.setOpaque(selected);
364 return result;
367 protected JComponent createItemComponent() {
368 myTextLabel = new ErrorLabel();
369 return myTextLabel;
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;
379 @Nullable
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();
389 return null;
392 abstract class Base extends CachingSimpleNode {
394 protected Base(final SimpleNode aParent) {
395 super(aParent);
398 String getText() {
399 return null;
402 boolean isModified() {
403 return false;
406 boolean isError() {
407 return false;
410 Configurable getConfigurable() {
411 return null;
415 class Root extends Base {
417 Root() {
418 super(null);
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));
439 else {
440 result.add(new EditorNode(this, eachKid, eachGroup));
445 return sort(result);
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()));
460 return cc;
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);
481 else {
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) {
492 super(parent);
493 myConfigurable = configurable;
494 myGroup = group;
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()]);
504 @Override
505 public boolean isContentHighlighted() {
506 return getParent() == myRoot;
509 @Override
510 Configurable getConfigurable() {
511 return myConfigurable;
514 @Override
515 public int getWeight() {
516 if (getParent() == myRoot) {
517 return Integer.MAX_VALUE - myGroups.indexOf(myGroup);
519 else {
520 return WeightBasedComparator.UNDEFINED_WEIGHT;
524 public ConfigurableGroup getGroup() {
525 return myGroup;
528 @Override
529 String getText() {
530 return getConfigurableDisplayName(myConfigurable).replace("\n", " ");
533 @Override
534 boolean isModified() {
535 return myContext.getModified().contains(myConfigurable);
538 @Override
539 boolean isError() {
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) {
552 myTree.repaint();
553 return new ActionCallback.Done();
556 public ActionCallback onModifiedRemoved(final Configurable configurable) {
557 myTree.repaint();
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 {
571 private MyTree() {
572 getInputMap().clear();
575 @Override
576 protected boolean highlightSingleNode() {
577 return false;
580 @Override
581 public void setUI(final TreeUI ui) {
582 TreeUI actualUI = ui;
583 if (!(ui instanceof MyTreeUi)) {
584 actualUI = new MyTreeUi();
586 super.setUI(actualUI);
589 @Override
590 protected void configureUiHelper(final TreeUIHelper helper) {
591 helper.installToolTipHandler(this);
594 @Override
595 public boolean getScrollableTracksViewportWidth() {
596 return true;
600 @Override
601 public void processKeyEvent(final KeyEvent e) {
602 TreePath path = myTree.getSelectionPath();
603 if (path != null) {
604 if (e.getKeyCode() == KeyEvent.VK_LEFT) {
605 if (isExpanded(path)) {
606 collapsePath(path);
607 return;
609 } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
610 if (isCollapsed(path)) {
611 expandPath(path);
612 return;
617 super.processKeyEvent(e);
620 @Override
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());
625 if (path != null) {
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);
635 comp.validate();
637 Point point = new Point(e.getX() - bounds.x, e.getY() - bounds.y);
638 if (myRendrer.isUnderHandle(point)) {
639 ui.toggleExpandState(path);
640 e.consume();
641 return;
647 super.processMouseEvent(e);
650 private class MyTreeUi extends BasicTreeUI {
652 @Override
653 public void toggleExpandState(final TreePath path) {
654 super.toggleExpandState(path);
657 @Override
658 public boolean isToggleEvent(final MouseEvent event) {
659 return super.isToggleEvent(event);
662 @Override
663 protected boolean shouldPaintExpandControl(final TreePath path,
664 final int row,
665 final boolean isExpanded,
666 final boolean hasBeenExpanded,
667 final boolean isLeaf) {
668 return false;
671 @Override
672 protected void paintHorizontalPartOfLeg(final Graphics g,
673 final Rectangle clipBounds,
674 final Insets insets,
675 final Rectangle bounds,
676 final TreePath path,
677 final int row,
678 final boolean isExpanded,
679 final boolean hasBeenExpanded,
680 final boolean isLeaf) {
684 @Override
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;
715 @Override
716 protected boolean isSelectable(final Object nodeObject) {
717 return nodeObject instanceof EditorNode;
720 @Override
721 public boolean isAutoExpandNode(final NodeDescriptor nodeDescriptor) {
722 return myContext.isHoldingFilter();
725 @Override
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() {
740 public void run() {
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);