/* * Copyright 2000-2017 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.ui; import com.intellij.CommonBundle; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.ConfigurationException; import com.intellij.openapi.options.MasterDetails; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.ui.popup.JBPopupFactory; import com.intellij.openapi.ui.popup.ListPopup; import com.intellij.openapi.ui.popup.ListPopupStep; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.Conditions; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.wm.IdeFocusManager; import com.intellij.ui.*; import com.intellij.ui.navigation.History; import com.intellij.ui.navigation.Place; import com.intellij.ui.treeStructure.Tree; import com.intellij.util.PlatformIcons; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.HashSet; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import com.intellij.util.xmlb.XmlSerializerUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.tree.*; import java.awt.*; import java.util.*; import java.util.List; /** * @author anna * @since 29-May-2006 */ public abstract class MasterDetailsComponent implements Configurable, DetailsComponent.Facade, MasterDetails { protected static final Logger LOG = Logger.getInstance("#com.intellij.openapi.ui.MasterDetailsComponent"); protected static final Icon COPY_ICON = PlatformIcons.COPY_ICON; protected NamedConfigurable myCurrentConfigurable; private final JBSplitter mySplitter; @NonNls public static final String TREE_OBJECT = "treeObject"; @NonNls public static final String TREE_NAME = "treeName"; protected History myHistory = new History(new Place.Navigator() { @Override public void setHistory(final History history) { myHistory = history; } @Override @Nullable public ActionCallback navigateTo(@Nullable final Place place, final boolean requestFocus) { return null; } @Override public void queryPlace(@NotNull final Place place) { } }); private JComponent myMaster; public void setHistory(final History history) { myHistory = history; } protected final MasterDetailsState myState; protected final Runnable TREE_UPDATER = new Runnable() { @Override public void run() { final TreePath selectionPath = myTree.getSelectionPath(); if (selectionPath == null) return; MyNode node = (MyNode)selectionPath.getLastPathComponent(); if (node == null) return; myState.setLastEditedConfigurable(getNodePathString(node)); //survive after rename; myDetails.setText(node.getConfigurable().getBannerSlogan()); node.reloadNode((DefaultTreeModel)myTree.getModel()); fireItemsChangedExternally(); } }; protected MyNode myRoot = new MyRootNode(); protected Tree myTree = new Tree(); private final DetailsComponent myDetails = new DetailsComponent(false, false); protected JPanel myWholePanel; public JPanel myNorthPanel = new JPanel(new BorderLayout()); private final List<ItemsChangeListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList(); private final Set<NamedConfigurable> myInitializedConfigurables = new HashSet<>(); private boolean myHasDeletedItems; protected AutoScrollToSourceHandler myAutoScrollHandler; protected boolean myToReInitWholePanel = true; protected MasterDetailsComponent() { this(new MasterDetailsState()); } protected MasterDetailsComponent(MasterDetailsState state) { myState = state; mySplitter = new OnePixelSplitter(false, .2f); mySplitter.setSplitterProportionKey("ProjectStructure.SecondLevelElements"); mySplitter.setHonorComponentsMinimumSize(true); installAutoScroll(); reInitWholePanelIfNeeded(); } protected void reInitWholePanelIfNeeded() { if (!myToReInitWholePanel) return; myWholePanel = new JPanel(new BorderLayout()) { @Override public void addNotify() { super.addNotify(); MasterDetailsComponent.this.addNotify(); TreeModel m = myTree.getModel(); if (m instanceof DefaultTreeModel) { DefaultTreeModel model = (DefaultTreeModel)m; for (int eachRow = 0; eachRow < myTree.getRowCount(); eachRow++) { TreePath eachPath = myTree.getPathForRow(eachRow); Object component = eachPath.getLastPathComponent(); if (component instanceof TreeNode) { model.nodeChanged((TreeNode)component); } } } } }; mySplitter.setHonorComponentsMinimumSize(true); myWholePanel.add(mySplitter, BorderLayout.CENTER); JPanel left = new JPanel(new BorderLayout()) { @Override public Dimension getMinimumSize() { final Dimension original = super.getMinimumSize(); return new Dimension(Math.max(original.width, 100), original.height); } }; ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTree); DefaultActionGroup group = createToolbarActionGroup(); if (group != null) { decorator.setActionGroup(group); } //left.add(myNorthPanel, BorderLayout.NORTH); myMaster = decorator.setAsUsualTopToolbar().setPanelBorder(JBUI.Borders.empty()).createPanel(); myNorthPanel.setVisible(false); left.add(myMaster, BorderLayout.CENTER); mySplitter.setFirstComponent(left); final JPanel right = new JPanel(new BorderLayout()); right.add(myDetails.getComponent(), BorderLayout.CENTER); mySplitter.setSecondComponent(right); GuiUtils.replaceJSplitPaneWithIDEASplitter(myWholePanel); myToReInitWholePanel = false; } private void installAutoScroll() { myAutoScrollHandler = new AutoScrollToSourceHandler() { @Override protected boolean isAutoScrollMode() { return isAutoScrollEnabled(); } @Override protected void setAutoScrollMode(boolean state) { //do nothing } @Override protected void scrollToSource(Component tree) { updateSelectionFromTree(); } @Override protected boolean needToCheckFocus() { return false; } }; myAutoScrollHandler.install(myTree); } protected void addNotify() { updateSelectionFromTree(); } private void updateSelectionFromTree() { TreePath[] treePaths = myTree.getSelectionPaths(); if (treePaths != null) { List<NamedConfigurable> selectedConfigurables = new ArrayList<>(); for (TreePath path : treePaths) { Object lastPathComponent = path.getLastPathComponent(); if (lastPathComponent instanceof MyNode) { selectedConfigurables.add(((MyNode)lastPathComponent).getConfigurable()); } } if (selectedConfigurables.size() > 1 && updateMultiSelection(selectedConfigurables)) { return; } } final TreePath path = myTree.getSelectionPath(); if (path != null) { final Object lastPathComp = path.getLastPathComponent(); if (!(lastPathComp instanceof MyNode)) return; final MyNode node = (MyNode)lastPathComp; setSelectedNode(node); } else { setSelectedNode(null); } } protected boolean updateMultiSelection(final List<NamedConfigurable> selectedConfigurables) { return false; } @Override public DetailsComponent getDetailsComponent() { return myDetails; } public Splitter getSplitter() { return mySplitter; } protected boolean isAutoScrollEnabled() { return myHistory == null || !myHistory.isNavigatingNow(); } protected DefaultActionGroup createToolbarActionGroup() { final ArrayList<AnAction> actions = createActions(false); if (actions != null) { final DefaultActionGroup group = new DefaultActionGroup(); for (AnAction action : actions) { if (action instanceof ActionGroupWithPreselection) { group.add(new MyActionGroupWrapper((ActionGroupWithPreselection)action)); } else { group.add(action); } } return group; } return null; } public void addItemsChangeListener(ItemsChangeListener l) { myListeners.add(l); } protected Dimension getPanelPreferredSize() { return JBUI.size(800, 600); } @Override @NotNull public JComponent createComponent() { myTree.updateUI(); reInitWholePanelIfNeeded(); updateSelectionFromTree(); final JPanel panel = new JPanel(new BorderLayout()) { @Override public Dimension getPreferredSize() { return getPanelPreferredSize(); } }; panel.add(myWholePanel, BorderLayout.CENTER); return panel; } @Override public boolean isModified() { if (myHasDeletedItems) return true; final boolean[] modified = new boolean[1]; TreeUtil.traverseDepth(myRoot, node -> { if (node instanceof MyNode) { final NamedConfigurable configurable = ((MyNode)node).getConfigurable(); if (isInitialized(configurable) && configurable.isModified()) { modified[0] = true; return false; } } return true; }); return modified[0]; } protected boolean isInitialized(final NamedConfigurable configurable) { return myInitializedConfigurables.contains(configurable); } @Override public void apply() throws ConfigurationException { processRemovedItems(); final ConfigurationException[] ex = new ConfigurationException[1]; TreeUtil.traverse(myRoot, node -> { if (node instanceof MyNode) { try { final NamedConfigurable configurable = ((MyNode)node).getConfigurable(); if (isInitialized(configurable) && configurable.isModified()) { configurable.apply(); } } catch (ConfigurationException e) { ex[0] = e; return false; } } return true; }); if (ex[0] != null) { throw ex[0]; } myHasDeletedItems = false; } protected abstract void processRemovedItems(); protected abstract boolean wasObjectStored(Object editableObject); @Override public void reset() { loadComponentState(); myHasDeletedItems = false; ((DefaultTreeModel)myTree.getModel()).reload(); //myTree.requestFocus(); myState.getProportions().restoreSplitterProportions(myWholePanel); final Enumeration enumeration = myRoot.breadthFirstEnumeration(); boolean selected = false; while (enumeration.hasMoreElements()) { final MyNode node = (MyNode)enumeration.nextElement(); if (node instanceof MyRootNode) continue; final String path = getNodePathString(node); if (!selected && Comparing.strEqual(path, myState.getLastEditedConfigurable())) { TreeUtil.selectInTree(node, false, myTree); selected = true; } } if (!selected) { TreeUtil.selectFirstNode(myTree); } updateSelectionFromTree(); } protected void loadComponentState() { final String key = getComponentStateKey(); final MasterDetailsStateService stateService = getStateService(); if (key != null && stateService != null) { final MasterDetailsState state = stateService.getComponentState(key, myState.getClass()); if (state != null) { loadState(state); } } } private static String getNodePathString(final MyNode node) { StringBuilder path = new StringBuilder(); MyNode current = node; while (current != null) { final Object userObject = current.getUserObject(); if (!(userObject instanceof NamedConfigurable)) break; final String displayName = current.getDisplayName(); if (StringUtil.isEmptyOrSpaces(displayName)) break; if (path.length() > 0) { path.append('|'); } path.append(displayName); final TreeNode parent = current.getParent(); if (!(parent instanceof MyNode)) break; current = (MyNode)parent; } return path.toString(); } @Nullable @NonNls protected String getComponentStateKey() { return null; } @Nullable protected MasterDetailsStateService getStateService() { return null; } protected MasterDetailsState getState() { return myState; } protected void loadState(final MasterDetailsState object) { XmlSerializerUtil.copyBean(object, myState); } @Override public void disposeUIResources() { myState.getProportions().saveSplitterProportions(myWholePanel); myAutoScrollHandler.cancelAllRequests(); myDetails.disposeUIResources(); myInitializedConfigurables.clear(); clearChildren(); final String key = getComponentStateKey(); final MasterDetailsStateService stateService = getStateService(); if (key != null && stateService != null) { stateService.setComponentState(key, getState()); } myCurrentConfigurable = null; } protected void clearChildren() { TreeUtil.traverseDepth(myRoot, node -> { if (node instanceof MyNode) { final MyNode treeNode = (MyNode)node; treeNode.getConfigurable().disposeUIResources(); if (!(treeNode instanceof MyRootNode)) { treeNode.setUserObject(null); } } return true; }); myRoot.removeAllChildren(); } @Nullable protected ArrayList<AnAction> createActions(final boolean fromPopup) { return null; } protected void initTree() { ((DefaultTreeModel)myTree.getModel()).setRoot(myRoot); myTree.setRootVisible(false); myTree.setShowsRootHandles(true); UIUtil.setLineStyleAngled(myTree); TreeUtil.installActions(myTree); myTree.setCellRenderer(new ColoredTreeCellRenderer() { @Override public void customizeCellRenderer(@NotNull JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (value instanceof MyNode) { final MyNode node = (MyNode)value; setIcon(node.getIcon(expanded)); final Font font = UIUtil.getTreeFont(); if (node.isDisplayInBold()) { setFont(font.deriveFont(Font.BOLD)); } else { setFont(font.deriveFont(Font.PLAIN)); } append(node.getDisplayName(), node.isDisplayInBold() ? SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES : SimpleTextAttributes.REGULAR_ATTRIBUTES); } } }); ArrayList<AnAction> actions = createActions(true); if (actions != null) { final DefaultActionGroup group = new DefaultActionGroup(); for (AnAction action : actions) { group.add(action); } actions = getAdditionalActions(); if (actions != null) { group.addSeparator(); for (AnAction action : actions) { group.add(action); } } PopupHandler .installPopupHandler(myTree, group, ActionPlaces.UNKNOWN, ActionManager.getInstance()); //popup should follow the selection } } @Nullable protected ArrayList<AnAction> getAdditionalActions() { return null; } private void fireItemsChangeListener(final Object editableObject) { for (ItemsChangeListener listener : myListeners) { listener.itemChanged(editableObject); } } private void fireItemsChangedExternally() { for (ItemsChangeListener listener : myListeners) { listener.itemsExternallyChanged(); } } private void createUIComponents() { myTree = new Tree() { @Override public Dimension getPreferredScrollableViewportSize() { Dimension size = super.getPreferredScrollableViewportSize(); size = new Dimension(size.width + 20, size.height); return size; } @Override @SuppressWarnings("NonStaticInitializer") public JToolTip createToolTip() { final JToolTip toolTip = new JToolTip() { { setUI(new MultiLineTooltipUI()); } }; toolTip.setComponent(this); return toolTip; } }; } protected void addNode(MyNode nodeToAdd, MyNode parent) { int i = TreeUtil.indexedBinarySearch(parent, nodeToAdd, getNodeComparator()); int insertionPoint = i >= 0 ? i : -i - 1; ((DefaultTreeModel)myTree.getModel()).insertNodeInto(nodeToAdd, parent, insertionPoint); } protected void sortDescendants(MyNode root) { TreeUtil.sortRecursively(root, getNodeComparator()); ((DefaultTreeModel)myTree.getModel()).reload(root); } protected Comparator<MyNode> getNodeComparator() { return (o1, o2) -> StringUtil.naturalCompare(o1.getDisplayName(), o2.getDisplayName()); } public ActionCallback selectNodeInTree(final DefaultMutableTreeNode nodeToSelect) { return selectNodeInTree(nodeToSelect, true, false); } public ActionCallback selectNodeInTree(final DefaultMutableTreeNode nodeToSelect, boolean requestFocus) { return selectNodeInTree(nodeToSelect, true, requestFocus); } public ActionCallback selectNodeInTree(final DefaultMutableTreeNode nodeToSelect, boolean center, final boolean requestFocus) { if (requestFocus) { IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myTree, true)); } if (nodeToSelect != null) { return TreeUtil.selectInTree(nodeToSelect, requestFocus, myTree, center); } return TreeUtil.selectFirstNode(myTree); } @Nullable public Object getSelectedObject() { final TreePath selectionPath = myTree.getSelectionPath(); if (selectionPath != null && selectionPath.getLastPathComponent() instanceof MyNode) { MyNode node = (MyNode)selectionPath.getLastPathComponent(); final NamedConfigurable configurable = node.getConfigurable(); LOG.assertTrue(configurable != null, "already disposed"); return configurable.getEditableObject(); } return null; } @Nullable public NamedConfigurable getSelectedConfigurable() { final TreePath selectionPath = myTree.getSelectionPath(); if (selectionPath != null) { MyNode node = (MyNode)selectionPath.getLastPathComponent(); final NamedConfigurable configurable = node.getConfigurable(); LOG.assertTrue(configurable != null, "already disposed"); return configurable; } return null; } public void selectNodeInTree(String displayName) { final MyNode nodeByName = findNodeByName(myRoot, displayName); selectNodeInTree(nodeByName, true); } public void selectNodeInTree(final Object object) { selectNodeInTree(findNodeByObject(myRoot, object), true); } @Nullable protected static MyNode findNodeByName(final TreeNode root, final String profileName) { if (profileName == null) return null; //do not suggest root node return findNodeByCondition(root, configurable -> Comparing.strEqual(profileName, configurable.getDisplayName())); } @Nullable public static MyNode findNodeByObject(final TreeNode root, final Object editableObject) { if (editableObject == null) return null; //do not suggest root node return findNodeByCondition(root, configurable -> Comparing.equal(editableObject, configurable.getEditableObject())); } protected static MyNode findNodeByCondition(final TreeNode root, final Condition<NamedConfigurable> condition) { final MyNode[] nodeToSelect = new MyNode[1]; TreeUtil.traverseDepth(root, node -> { if (condition.value(((MyNode)node).getConfigurable())) { nodeToSelect[0] = (MyNode)node; return false; } return true; }); return nodeToSelect[0]; } protected void setSelectedNode(@Nullable MyNode node) { if (node != null) { myState.setLastEditedConfigurable(getNodePathString(node)); } updateSelection(node != null ? node.getConfigurable() : null); } protected void updateSelection(@Nullable NamedConfigurable configurable) { myDetails.setText(configurable != null ? configurable.getBannerSlogan() : null); myCurrentConfigurable = configurable; if (configurable != null) { final JComponent comp = configurable.createComponent(); if (comp == null) { setEmpty(); LOG.error("createComponent() returned null. configurable=" + configurable); } else { myDetails.setContent(comp); ensureInitialized(configurable); myHistory.pushPlaceForElement(TREE_OBJECT, configurable.getEditableObject()); } } else { setEmpty(); } } public void ensureInitialized(NamedConfigurable configurable) { if (!isInitialized(configurable)) { configurable.reset(); initializeConfigurable(configurable); } } private void setEmpty() { myDetails.setContent(null); myDetails.setEmptyContentText(getEmptySelectionString()); } @Override public String getHelpTopic() { if (myCurrentConfigurable != null) { return myCurrentConfigurable.getHelpTopic(); } return null; } @Nullable protected String getEmptySelectionString() { return null; } protected void initializeConfigurable(final NamedConfigurable configurable) { myInitializedConfigurables.add(configurable); } /** * @deprecated use {@link #checkForEmptyAndDuplicatedNames(String, String, Class)} instead */ protected void checkApply(Set<MyNode> rootNodes, String prefix, String title) throws ConfigurationException { for (MyNode rootNode : rootNodes) { checkForEmptyAndDuplicatedNames(rootNode, prefix, title, NamedConfigurable.class, false); } } protected final void checkForEmptyAndDuplicatedNames(String prefix, String title, Class<? extends NamedConfigurable> configurableClass) throws ConfigurationException { checkForEmptyAndDuplicatedNames(myRoot, prefix, title, configurableClass, true); } private void checkForEmptyAndDuplicatedNames(MyNode rootNode, String prefix, String title, Class<? extends NamedConfigurable> configurableClass, boolean recursively) throws ConfigurationException { final Set<String> names = new HashSet<>(); for (int i = 0; i < rootNode.getChildCount(); i++) { final MyNode node = (MyNode)rootNode.getChildAt(i); final NamedConfigurable scopeConfigurable = node.getConfigurable(); if (configurableClass.isInstance(scopeConfigurable)) { final String name = scopeConfigurable.getDisplayName(); if (name.trim().isEmpty()) { selectNodeInTree(node); throw new ConfigurationException("Name should contain non-space characters"); } if (names.contains(name)) { final NamedConfigurable selectedConfigurable = getSelectedConfigurable(); if (selectedConfigurable == null || !Comparing.strEqual(selectedConfigurable.getDisplayName(), name)) { selectNodeInTree(node); } throw new ConfigurationException(CommonBundle.message("smth.already.exist.error.message", prefix, name), title); } names.add(name); } if (recursively) { checkForEmptyAndDuplicatedNames(node, prefix, title, configurableClass, true); } } } public Tree getTree() { return myTree; } protected void removePaths(final TreePath... paths) { List<MyNode> nodes = new ArrayList<>(); for (TreePath path : paths) { nodes.add((MyNode)path.getLastPathComponent()); } removeNodes(nodes); } protected void removeNodes(final List<MyNode> nodes) { MyNode parentNode = null; int idx = -1; for (MyNode node : nodes) { final NamedConfigurable namedConfigurable = node.getConfigurable(); final Object editableObject = namedConfigurable.getEditableObject(); parentNode = (MyNode)node.getParent(); if (parentNode == null) continue; idx = parentNode.getIndex(node); ((DefaultTreeModel)myTree.getModel()).removeNodeFromParent(node); myHasDeletedItems |= wasObjectStored(editableObject); fireItemsChangeListener(editableObject); onItemDeleted(editableObject); namedConfigurable.disposeUIResources(); } if (!nodes.isEmpty()) { if (parentNode != null && idx != -1) { DefaultMutableTreeNode toSelect = null; if (idx < parentNode.getChildCount()) { toSelect = (DefaultMutableTreeNode) parentNode.getChildAt(idx); } else { if (idx > 0 && parentNode.getChildCount() > 0) { if (idx - 1 < parentNode.getChildCount()) { toSelect = (DefaultMutableTreeNode) parentNode.getChildAt(idx - 1); } else { toSelect = (DefaultMutableTreeNode) parentNode.getFirstChild(); } } else { if (parentNode.isRoot() && myTree.isRootVisible()) { toSelect = parentNode; } else if (parentNode.getChildCount() > 0) { toSelect = (DefaultMutableTreeNode) parentNode.getFirstChild(); } } } if (toSelect != null) { TreeUtil.selectInTree(toSelect, true, myTree); } } else { TreeUtil.selectFirstNode(myTree); } } } protected void onItemDeleted(Object item) { } protected class MyDeleteAction extends AnAction implements DumbAware { private final Condition<Object[]> myCondition; public MyDeleteAction() { this(Conditions.alwaysTrue()); } public MyDeleteAction(Condition<Object[]> availableCondition) { super(CommonBundle.message("button.delete"), CommonBundle.message("button.delete"), PlatformIcons.DELETE_ICON); registerCustomShortcutSet(CommonShortcuts.getDelete(), myTree); myCondition = availableCondition; } @Override public void update(AnActionEvent e) { final Presentation presentation = e.getPresentation(); presentation.setEnabled(false); final TreePath[] selectionPath = myTree.getSelectionPaths(); if (selectionPath != null) { Object[] nodes = ContainerUtil.map2Array(selectionPath, TreePath::getLastPathComponent); if (!myCondition.value(nodes)) return; presentation.setEnabled(true); } } @Override public void actionPerformed(AnActionEvent e) { removePaths(myTree.getSelectionPaths()); } } protected static Condition<Object[]> forAll(final Condition<Object> condition) { return objects -> { for (Object object : objects) { if (!condition.value(object)) return false; } return true; }; } public static class MyNode extends DefaultMutableTreeNode { private boolean myDisplayInBold; public MyNode(@NotNull NamedConfigurable userObject) { super(userObject); } public MyNode(@NotNull NamedConfigurable userObject, boolean displayInBold) { super(userObject); myDisplayInBold = displayInBold; } @NotNull public String getDisplayName() { final NamedConfigurable configurable = (NamedConfigurable)getUserObject(); LOG.assertTrue(configurable != null, "Tree was already disposed"); return configurable.getDisplayName(); } public NamedConfigurable getConfigurable() { return (NamedConfigurable)getUserObject(); } public boolean isDisplayInBold() { return myDisplayInBold; } public void setDisplayInBold(boolean displayInBold) { myDisplayInBold = displayInBold; } @Nullable public Icon getIcon(boolean expanded) { // thanks to invokeLater() in TreeUtil.showAndSelect(), we can get calls to getIcon() after the tree has been disposed final NamedConfigurable configurable = getConfigurable(); if (configurable != null) { return configurable.getIcon(expanded); } return null; } protected void reloadNode(DefaultTreeModel treeModel) { treeModel.reload(this); } } @SuppressWarnings("ConstantConditions") protected static class MyRootNode extends MyNode { public MyRootNode() { super(new NamedConfigurable(false, null) { @Override public void setDisplayName(String name) { } @Override public Object getEditableObject() { return null; } @Override public String getBannerSlogan() { return null; } @Override public String getDisplayName() { return ""; } @Override public JComponent createOptionsPanel() { return null; } @Override public boolean isModified() { return false; } @Override public void apply() { } }, false); } } protected interface ItemsChangeListener { void itemChanged(@Nullable Object deletedItem); void itemsExternallyChanged(); } public interface ActionGroupWithPreselection { ActionGroup getActionGroup(); int getDefaultIndex(); } protected class MyActionGroupWrapper extends AnAction implements DumbAware { private final ActionGroup myActionGroup; private ActionGroupWithPreselection myPreselection; public MyActionGroupWrapper(final ActionGroupWithPreselection actionGroup) { this(actionGroup.getActionGroup()); myPreselection = actionGroup; } public MyActionGroupWrapper(final ActionGroup actionGroup) { super(actionGroup.getTemplatePresentation().getText(), actionGroup.getTemplatePresentation().getDescription(), actionGroup.getTemplatePresentation().getIcon()); myActionGroup = actionGroup; registerCustomShortcutSet(actionGroup.getShortcutSet(), myTree); } @Override public void actionPerformed(AnActionEvent e) { final JBPopupFactory popupFactory = JBPopupFactory.getInstance(); final DataContext dataContext = e.getDataContext(); final ListPopupStep step = popupFactory.createActionsStep(myActionGroup, dataContext, false, false, myActionGroup.getTemplatePresentation().getText(), myTree, true, myPreselection != null ? myPreselection.getDefaultIndex() : 0, true); final ListPopup listPopup = popupFactory.createListPopup(step); listPopup.setHandleAutoSelectionBeforeShow(true); if (e instanceof AnActionButton.AnActionEventWrapper) { ((AnActionButton.AnActionEventWrapper)e).showPopup(listPopup); } else { listPopup.showInBestPositionFor(dataContext); } } } @Override public JComponent getToolbar() { myToReInitWholePanel = true; return myNorthPanel; } @Override public JComponent getMaster() { myToReInitWholePanel = true; return myMaster; } @Override public DetailsComponent getDetails() { myToReInitWholePanel = true; return myDetails; } @Override public void initUi() { createComponent(); } }