/* * Copyright 2000-2012 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.options.newEditor; import com.intellij.ide.util.treeView.NodeDescriptor; import com.intellij.openapi.Disposable; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.SearchableConfigurable; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Weighted; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.treeStructure.*; import com.intellij.ui.treeStructure.filtered.FilteringTreeBuilder; import com.intellij.ui.treeStructure.filtered.FilteringTreeStructure; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.util.*; import java.util.List; public class OptionsTree extends JPanel implements Disposable, OptionsEditorColleague { Project myProject; final SimpleTree myTree; Configurable[] myConfigurables; FilteringTreeBuilder myBuilder; Root myRoot; OptionsEditorContext myContext; Map<Configurable, EditorNode> myConfigurable2Node = new HashMap<Configurable, EditorNode>(); MergingUpdateQueue mySelection; public OptionsTree(Project project, Configurable[] configurables, OptionsEditorContext context) { super(new BorderLayout()); myProject = project; myConfigurables = configurables; myContext = context; myRoot = new Root(); final SimpleTreeStructure structure = new SimpleTreeStructure() { @Override public Object getRootElement() { return myRoot; } }; myTree = new SimpleTree() { @Override protected boolean paintNodes() { return false; } }; TreeUtil.installActions(myTree); myTree.setBorder(JBUI.Borders.empty()); myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); myTree.setRootVisible(false); myTree.setShowsRootHandles(true); myTree.setFont(UIUtil.getLabelFont(UIUtil.FontSize.BIGGER)); myBuilder = new MyBuilder(structure); myBuilder.setFilteringMerge(300, null); Disposer.register(this, myBuilder); myTree.addComponentListener(new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { myBuilder.revalidateTree(); } @Override public void componentMoved(final ComponentEvent e) { myBuilder.revalidateTree(); } @Override public void componentShown(final ComponentEvent e) { myBuilder.revalidateTree(); } }); final JScrollPane scrolls = ScrollPaneFactory.createScrollPane(myTree, true); scrolls.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); add(scrolls, BorderLayout.CENTER); mySelection = new MergingUpdateQueue("OptionsTree", 150, false, this, this, this).setRestartTimerOnAdd(true); myTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { @Override public void valueChanged(final TreeSelectionEvent e) { final TreePath path = e.getNewLeadSelectionPath(); if (path == null) { queueSelection(null); } else { final Base base = extractNode(path.getLastPathComponent()); queueSelection(base != null ? base.getConfigurable() : null); } } }); myTree.addKeyListener(new KeyListener() { @Override public void keyTyped(final KeyEvent e) { _onTreeKeyEvent(e); } @Override public void keyPressed(final KeyEvent e) { _onTreeKeyEvent(e); } @Override public void keyReleased(final KeyEvent e) { _onTreeKeyEvent(e); } }); } protected void _onTreeKeyEvent(KeyEvent e) { final KeyStroke stroke = KeyStroke.getKeyStrokeForEvent(e); final Object action = myTree.getInputMap().get(stroke); if (action == null) { onTreeKeyEvent(e); } } protected void onTreeKeyEvent(KeyEvent e) { } ActionCallback select(@Nullable Configurable configurable) { return queueSelection(configurable); } public void selectFirst() { if (myConfigurables.length > 0) { queueSelection(myConfigurables[0]); } } private Configurable myQueuedConfigurable; ActionCallback queueSelection(final Configurable configurable) { if (myBuilder.isSelectionBeingAdjusted()) { return new ActionCallback.Rejected(); } final ActionCallback callback = new ActionCallback(); myQueuedConfigurable = configurable; final Update update = new Update(this) { @Override public void run() { if (configurable != myQueuedConfigurable) return; if (configurable == null) { myTree.getSelectionModel().clearSelection(); myContext.fireSelected(null, OptionsTree.this); } else { myBuilder.getReady(this).doWhenDone(new Runnable() { @Override public void run() { if (configurable != myQueuedConfigurable) return; final EditorNode editorNode = myConfigurable2Node.get(configurable); FilteringTreeStructure.FilteringNode editorUiNode = myBuilder.getVisibleNodeFor(editorNode); if (editorUiNode == null) return; if (!myBuilder.getSelectedElements().contains(editorUiNode)) { myBuilder.select(editorUiNode, new Runnable() { @Override public void run() { fireSelected(configurable, callback); } }); } else { myBuilder.scrollSelectionToVisible(new Runnable() { @Override public void run() { fireSelected(configurable, callback); } }, false); } } }); } } @Override public void setRejected() { super.setRejected(); callback.setRejected(); } }; mySelection.queue(update); return callback; } private void fireSelected(Configurable configurable, final ActionCallback callback) { myContext.fireSelected(configurable, this).doWhenProcessed(callback.createSetDoneRunnable()); } public JTree getTree() { return myTree; } public List<Configurable> getPathToRoot(final Configurable configurable) { final ArrayList<Configurable> path = new ArrayList<Configurable>(); EditorNode eachNode = myConfigurable2Node.get(configurable); if (eachNode == null) return path; while (eachNode != null) { path.add(eachNode.getConfigurable()); final SimpleNode parent = eachNode.getParent(); if (parent instanceof EditorNode) { eachNode = (EditorNode)parent; } else { break; } } return path; } public SimpleNode findNodeFor(final Configurable toSelect) { return myConfigurable2Node.get(toSelect); } @Nullable public <T extends Configurable> T findConfigurable(Class<T> configurableClass) { for (Configurable configurable : myConfigurable2Node.keySet()) { if (configurableClass.isInstance(configurable)) { return configurableClass.cast(configurable); } } return null; } @Nullable public SearchableConfigurable findConfigurableById(@NotNull String configurableId) { for (Configurable configurable : myConfigurable2Node.keySet()) { if (configurable instanceof SearchableConfigurable) { SearchableConfigurable searchableConfigurable = (SearchableConfigurable)configurable; if (configurableId.equals(searchableConfigurable.getId())) { return searchableConfigurable; } } } return null; } @Nullable private Base extractNode(Object object) { if (object instanceof DefaultMutableTreeNode) { final DefaultMutableTreeNode uiNode = (DefaultMutableTreeNode)object; final Object o = uiNode.getUserObject(); if (o instanceof FilteringTreeStructure.FilteringNode) { return (Base)((FilteringTreeStructure.FilteringNode)o).getDelegate(); } } return null; } abstract static class Base extends CachingSimpleNode { protected Base(final SimpleNode aParent) { super(aParent); } String getText() { return null; } boolean isModified() { return false; } boolean isError() { return false; } Configurable getConfigurable() { return null; } } class Root extends Base { Root() { super(null); } @Override protected SimpleNode[] buildChildren() { return ContainerUtil.toArray(map(myConfigurables), EMPTY_EN_ARRAY); } @NotNull private List<EditorNode> map(final Configurable[] configurables) { List<EditorNode> result = new ArrayList<EditorNode>(); for (Configurable eachKid : configurables) { if (isInvisibleNode(eachKid)) { result.addAll(OptionsTree.this.buildChildren(eachKid, this)); } else { result.add(new EditorNode(this, eachKid)); } } return sort(result); } } private static boolean isInvisibleNode(final Configurable child) { return child instanceof SearchableConfigurable.Parent && !((SearchableConfigurable.Parent)child).isVisible(); } private static List<EditorNode> sort(List<EditorNode> c) { List<EditorNode> cc = new ArrayList<EditorNode>(c); Collections.sort(cc, new Comparator<EditorNode>() { @Override public int compare(final EditorNode o1, final EditorNode o2) { double weight1 = getWeight(o1); double weight2 = getWeight(o2); if(weight1 != weight2) { return (int)(weight2 - weight1); } return getConfigurableDisplayName(o1.getConfigurable()).compareToIgnoreCase(getConfigurableDisplayName(o2.getConfigurable())); } }); return cc; } private static double getWeight(EditorNode node) { Configurable configurable = node.getConfigurable(); if (configurable instanceof Weighted) { return ((Weighted)configurable).getWeight(); } return 0; } private static String getConfigurableDisplayName(final Configurable c) { final String name = c.getDisplayName(); return name != null ? name : "{ Unnamed Page:" + c.getClass().getSimpleName() + " }"; } private List<EditorNode> buildChildren(final Configurable configurable, SimpleNode parent) { if (configurable instanceof Configurable.Composite) { final Configurable[] kids = ((Configurable.Composite)configurable).getConfigurables(); final List<EditorNode> result = new ArrayList<EditorNode>(kids.length); for (Configurable child : kids) { if (isInvisibleNode(child)) { result.addAll(buildChildren(child, parent)); } result.add(new EditorNode(parent, child)); myContext.registerKid(configurable, child); } return sort(result); } else { return Collections.emptyList(); } } private static final EditorNode[] EMPTY_EN_ARRAY = new EditorNode[0]; class EditorNode extends Base { Configurable myConfigurable; EditorNode(SimpleNode parent, Configurable configurable) { super(parent); myConfigurable = configurable; myConfigurable2Node.put(configurable, this); addPlainText(getConfigurableDisplayName(configurable)); } @Override protected EditorNode[] buildChildren() { List<EditorNode> list = OptionsTree.this.buildChildren(myConfigurable, this); return list.isEmpty() ? EMPTY_EN_ARRAY : list.toArray(new EditorNode[list.size()]); } @Override public boolean isAlwaysLeaf() { return !(myConfigurable instanceof Configurable.Composite); } @Override public boolean isContentHighlighted() { return getParent() == myRoot; } @Override Configurable getConfigurable() { return myConfigurable; } @Override public int getWeight() { return WeightBasedComparator.UNDEFINED_WEIGHT; } @Override String getText() { return getConfigurableDisplayName(myConfigurable).replace("\n", " "); } @Override boolean isModified() { return myContext.getModified().contains(myConfigurable); } @Override boolean isError() { return myContext.getErrors().containsKey(myConfigurable); } } @Override public void dispose() { myQueuedConfigurable = null; } @Override public ActionCallback onSelected(final Configurable configurable, final Configurable oldConfigurable) { return queueSelection(configurable); } @Override public ActionCallback onModifiedAdded(final Configurable colleague) { myTree.repaint(); return new ActionCallback.Done(); } @Override public ActionCallback onModifiedRemoved(final Configurable configurable) { myTree.repaint(); return new ActionCallback.Done(); } @Override public ActionCallback onErrorsChanged() { return new ActionCallback.Done(); } public void processTextEvent(KeyEvent e) { myTree.processKeyEvent(e); } private class MyBuilder extends FilteringTreeBuilder { List<Object> myToExpandOnResetFilter; boolean myRefilteringNow; boolean myWasHoldingFilter; public MyBuilder(SimpleTreeStructure structure) { super(OptionsTree.this.myTree, myContext.getFilter(), structure, new WeightBasedComparator(false)); myTree.addTreeExpansionListener(new TreeExpansionListener() { @Override public void treeExpanded(TreeExpansionEvent event) { invalidateExpansions(); } @Override public void treeCollapsed(TreeExpansionEvent event) { invalidateExpansions(); } }); } private void invalidateExpansions() { if (!myRefilteringNow) { myToExpandOnResetFilter = null; } } @Override protected boolean isSelectable(final Object nodeObject) { return nodeObject instanceof EditorNode; } @Override public boolean isAutoExpandNode(final NodeDescriptor nodeDescriptor) { return myContext.isHoldingFilter(); } @Override public boolean isToEnsureSelectionOnFocusGained() { return false; } @Override protected ActionCallback refilterNow(Object preferredSelection, boolean adjustSelection) { final List<Object> toRestore = new ArrayList<Object>(); if (myContext.isHoldingFilter() && !myWasHoldingFilter && myToExpandOnResetFilter == null) { myToExpandOnResetFilter = myBuilder.getUi().getExpandedElements(); } else if (!myContext.isHoldingFilter() && myWasHoldingFilter && myToExpandOnResetFilter != null) { toRestore.addAll(myToExpandOnResetFilter); myToExpandOnResetFilter = null; } myWasHoldingFilter = myContext.isHoldingFilter(); ActionCallback result = super.refilterNow(preferredSelection, adjustSelection); myRefilteringNow = true; return result.doWhenDone(new Runnable() { @Override public void run() { myRefilteringNow = false; if (!myContext.isHoldingFilter() && getSelectedElements().isEmpty()) { restoreExpandedState(toRestore); } } }); } private void restoreExpandedState(List<Object> toRestore) { TreePath[] selected = myTree.getSelectionPaths(); if (selected == null) { selected = new TreePath[0]; } List<TreePath> toCollapse = new ArrayList<TreePath>(); for (int eachRow = 0; eachRow < myTree.getRowCount(); eachRow++) { if (!myTree.isExpanded(eachRow)) continue; TreePath eachVisiblePath = myTree.getPathForRow(eachRow); if (eachVisiblePath == null) continue; Object eachElement = myBuilder.getElementFor(eachVisiblePath.getLastPathComponent()); if (toRestore.contains(eachElement)) continue; for (TreePath eachSelected : selected) { if (!eachVisiblePath.isDescendant(eachSelected)) { toCollapse.add(eachVisiblePath); } } } for (TreePath each : toCollapse) { myTree.collapsePath(each); } } } }