diff --git a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/IdeaProjectSettingsService.java b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/IdeaProjectSettingsService.java index 8da7f775f8b8..c2a781cb622a 100644 --- a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/IdeaProjectSettingsService.java +++ b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/IdeaProjectSettingsService.java @@ -119,7 +119,7 @@ public class IdeaProjectSettingsService extends ProjectSettingsService implement @Override public boolean processModulesMoved(final Module[] modules, @Nullable final ModuleGroup targetGroup) { final ModuleStructureConfigurable rootConfigurable = ModuleStructureConfigurable.getInstance(myProject); - if (rootConfigurable.updateProjectTree(modules, targetGroup)) { //inside project root editor + if (rootConfigurable.updateProjectTree(modules)) { //inside project root editor if (targetGroup != null) { rootConfigurable.selectNodeInTree(targetGroup.toString()); } diff --git a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/projectRoot/ModuleStructureConfigurable.java b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/projectRoot/ModuleStructureConfigurable.java index 04d88c17d565..5db37d1b6135 100644 --- a/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/projectRoot/ModuleStructureConfigurable.java +++ b/java/idea-ui/src/com/intellij/openapi/roots/ui/configuration/projectRoot/ModuleStructureConfigurable.java @@ -24,8 +24,7 @@ import com.intellij.icons.AllIcons; import com.intellij.ide.IdeBundle; import com.intellij.ide.highlighter.ModuleFileType; import com.intellij.ide.projectView.impl.ModuleGroup; -import com.intellij.ide.projectView.impl.ModuleGroupUtil; -import com.intellij.openapi.module.ModuleGrouper; +import com.intellij.ide.projectView.impl.ModuleGroupingTreeHelper; import com.intellij.ide.util.projectWizard.ModuleBuilder; import com.intellij.ide.util.projectWizard.NamePathComponent; import com.intellij.ide.util.projectWizard.ProjectWizardUtil; @@ -54,6 +53,7 @@ import com.intellij.openapi.ui.*; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.NullableComputable; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.LocalFileSystem; @@ -94,6 +94,9 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple if (editableObject2 instanceof Module && editableObject1 instanceof String) return 1; if (editableObject1 instanceof Module && editableObject2 instanceof String) return -1; + if (editableObject2 instanceof Module && editableObject1 instanceof Facet) return 1; + if (editableObject1 instanceof Module && editableObject2 instanceof Facet) return -1; + if (editableObject2 instanceof ModuleGroup && editableObject1 instanceof String) return 1; if (editableObject1 instanceof ModuleGroup && editableObject2 instanceof String) return -1; @@ -166,7 +169,7 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple protected void loadTree() { createProjectNodes(); - ((DefaultTreeModel)myTree.getModel()).reload(); + getTreeModel().reload(); myUiDisposed = false; } @@ -217,27 +220,12 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple private void createProjectNodes() { - final Map moduleGroup2NodeMap = new HashMap<>(); - final Module[] modules = myModuleManager.getModules(); - for (final Module module : modules) { - ModuleGrouper moduleGrouper = getModuleGrouper(); - MyNode moduleNode = new ModuleNode(new ModuleConfigurable(myContext.myModulesConfigurator, module, TREE_UPDATER, moduleGrouper)); - boolean nodesAdded = myFacetEditorFacade.addFacetsNodes(module, moduleNode); - nodesAdded |= addNodesFromExtensions(module, moduleNode); - if (nodesAdded) { - myTree.setShowsRootHandles(true); - } - final List groupPath = myHideModuleGroups || myFlattenModules ? Collections.emptyList() : moduleGrouper.getGroupPath(module); - if (groupPath.isEmpty()) { - myRoot.add(moduleNode); - } - else { - final MyNode moduleGroupNode = ModuleGroupUtil - .buildModuleGroupPath(new ModuleGroup(groupPath), myRoot, moduleGroup2NodeMap, - parentChildRelation -> parentChildRelation.getParent().add(parentChildRelation.getChild()), - ModuleStructureConfigurable::createModuleGroupNode); - moduleGroupNode.add(moduleNode); - } + ModuleGrouper moduleGrouper = getModuleGrouper(); + ModuleGroupingTreeHelper helper = ModuleGroupingTreeHelper.forEmptyTree(!myHideModuleGroups && !myFlattenModules, + moduleGrouper, ModuleStructureConfigurable::createModuleGroupNode, this::createModuleNode, getNodeComparator()); + helper.createModuleNodes(Arrays.asList(myModuleManager.getModules()), myRoot, getTreeModel()); + if (containsSecondLevelNodes(myRoot)) { + myTree.setShowsRootHandles(true); } sortDescendants(myRoot); if (myProject.isDefault()) { //do not add modules node in case of template project @@ -252,13 +240,34 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple //myProjectNode.add(myLevel2Nodes.get(LibraryTablesRegistrar.PROJECT_LEVEL)); } + private static boolean containsSecondLevelNodes(TreeNode rootNode) { + int count = rootNode.getChildCount(); + for (int i = 0; i < count; i++) { + TreeNode child = rootNode.getChildAt(i); + if (child.getChildCount() > 0) { + return true; + } + } + return false; + } + + @NotNull + private ModuleNode createModuleNode(Module module, ModuleGrouper moduleGrouper) { + ModuleConfigurable configurable = new ModuleConfigurable(myContext.myModulesConfigurator, module, TREE_UPDATER, moduleGrouper); + List groupPath = moduleGrouper.getModuleAsGroupPath(module); + ModuleNode node = new ModuleNode(configurable, groupPath != null ? new ModuleGroup(groupPath) : null); + myFacetEditorFacade.addFacetsNodes(module, node); + addNodesFromExtensions(module, node); + return node; + } + @NotNull private static MyNode createModuleGroupNode(ModuleGroup moduleGroup) { final NamedConfigurable moduleGroupConfigurable = new TextConfigurable<>(moduleGroup, moduleGroup.toString(), ProjectBundle.message("module.group.banner.text", moduleGroup.toString()), ProjectBundle.message("project.roots.module.groups.text"), PlatformIcons.CLOSED_MODULE_GROUP_ICON); - return new ModuleGroupNode(moduleGroupConfigurable, moduleGroup); + return new ModuleGroupNodeImpl(moduleGroupConfigurable, moduleGroup); } private void addRootNodesFromExtensions(final MyNode root, final Project project) { @@ -267,53 +276,35 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple } } - private boolean addNodesFromExtensions(final Module module, final MyNode moduleNode) { - boolean nodesAdded = false; + private void addNodesFromExtensions(final Module module, final MyNode moduleNode) { for (final ModuleStructureExtension extension : ModuleStructureExtension.EP_NAME.getExtensions()) { - nodesAdded |= extension.addModuleNodeChildren(module, moduleNode, TREE_UPDATER); + extension.addModuleNodeChildren(module, moduleNode, TREE_UPDATER); } - return nodesAdded; } - public boolean updateProjectTree(final Module[] modules, final ModuleGroup group) { + public boolean updateProjectTree(final Module[] modules) { if (myRoot.getChildCount() == 0) return false; //isn't visible - final MyNode[] nodes = new MyNode[modules.length]; - int i = 0; + List> nodes = new ArrayList<>(modules.length); for (Module module : modules) { MyNode node = findModuleNode(module); LOG.assertTrue(node != null, "Module " + module.getName() + " is not in project."); - node.removeFromParent(); - nodes[i++] = node; + nodes.add(Pair.create(node, module)); } - for (final MyNode moduleNode : nodes) { - List groupPath = myHideModuleGroups || myFlattenModules - ? Collections.emptyList() - : group != null ? group.getGroupPathList() : Collections.emptyList(); - if (groupPath.isEmpty()) { - myRoot.add(moduleNode); - } - else { - final MyNode moduleGroupNode = getOrCreateModuleGroupNode(groupPath, null); - moduleGroupNode.add(moduleNode); - } - Module module = (Module)moduleNode.getConfigurable().getEditableObject(); - myFacetEditorFacade.addFacetsNodes(module, moduleNode); - addNodesFromExtensions(module, moduleNode); - } - sortDescendants(myRoot); + ModuleGroupingTreeHelper helper = createGroupingHelper(); + helper.moveModuleNodesToProperGroup(nodes, myRoot, getTreeModel(), myTree); return true; } - private MyNode getOrCreateModuleGroupNode(List groupPath, @Nullable DefaultTreeModel treeModel) { - return ModuleGroupUtil.updateModuleGroupPath(new ModuleGroup(groupPath), myRoot, group1 -> findNodeByObject(myRoot, group1), - parentChildRelation -> { - MyNode parent = parentChildRelation.getParent(); - parent.add(parentChildRelation.getChild()); - if (treeModel != null) { - treeModel.nodesWereInserted(parent, new int[]{parent.getChildCount() - 1}); - } - }, - ModuleStructureConfigurable::createModuleGroupNode); + private DefaultTreeModel getTreeModel() { + return (DefaultTreeModel)myTree.getModel(); + } + + @NotNull + private ModuleGroupingTreeHelper createGroupingHelper() { + return ModuleGroupingTreeHelper.forTree(myRoot, (node) -> node instanceof ModuleGroupNode ? ((ModuleGroupNode)node).getModuleGroup() : null, + (node) -> node instanceof ModuleNode ? ((ModuleNode)node).getModule() : null, + !myHideModuleGroups && !myFlattenModules, getModuleGrouper(), ModuleStructureConfigurable::createModuleGroupNode, + this::createModuleNode, getNodeComparator()); } @Override @@ -336,7 +327,7 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple if (node != null) { final TreeNode parent = node.getParent(); node.removeFromParent(); - ((DefaultTreeModel)myTree.getModel()).reload(parent); + getTreeModel().reload(parent); } myContext.getDaemonAnalyzer().removeElement(new LibraryProjectStructureElement(myContext, library)); } @@ -553,7 +544,6 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple } private void addModuleNode(final Module module) { - MyNode node = new ModuleNode(new ModuleConfigurable(myContext.myModulesConfigurator, module, TREE_UPDATER, getModuleGrouper())); final TreePath selectionPath = myTree.getSelectionPath(); MyNode parent = null; if (selectionPath != null) { @@ -573,10 +563,8 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple } } if (parent == null) parent = myRoot; - addNode(node, parent); - myFacetEditorFacade.addFacetsNodes(module, node); - addNodesFromExtensions(module, node); - ((DefaultTreeModel)myTree.getModel()).reload(parent); + MyNode node = createModuleNode(module, getModuleGrouper()); + TreeUtil.insertNode(parent, node, getTreeModel(), getNodeComparator()); selectNodeInTree(node); final ProjectStructureDaemonAnalyzer daemonAnalyzer = myContext.getDaemonAnalyzer(); daemonAnalyzer.queueUpdate(new ModuleProjectStructureElement(myContext, module)); @@ -632,9 +620,16 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple } } - private class ModuleNode extends MyNode { - public ModuleNode(@NotNull ModuleConfigurable configurable) { + private class ModuleNode extends MyNode implements ModuleGroupNode { + private final ModuleGroup myModuleAsGroup; + + public ModuleNode(@NotNull ModuleConfigurable configurable, @Nullable ModuleGroup moduleAsGroup) { super(configurable); + myModuleAsGroup = moduleAsGroup; + } + + public ModuleGroup getModuleGroup() { + return myModuleAsGroup; } @NotNull @@ -658,45 +653,36 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple @Override protected void reloadNode(DefaultTreeModel treeModel) { - TreeNode parent = getParent(); - List actualPath = parent instanceof ModuleGroupNode ? ((ModuleGroupNode)parent).myModuleGroup.getGroupPathList() : Collections.emptyList(); - List expectedPath = getModuleGrouper().getGroupPath(getModule()); - if (!actualPath.equals(expectedPath)) { - TreePath path = myTree.getSelectionPath(); - boolean wasSelected = path != null && this.equals(path.getLastPathComponent()); - boolean autoScrollWasEnabled = myAutoScrollEnabled; - try { - myAutoScrollEnabled = false; - MyNode newParent = getOrCreateModuleGroupNode(expectedPath, treeModel); - treeModel.removeNodeFromParent(this); - while (parent instanceof ModuleGroupNode && parent.getChildCount() == 0) { - TreeNode grandParent = parent.getParent(); - treeModel.removeNodeFromParent((MutableTreeNode)parent); - parent = grandParent; - } - - newParent.add(this); - treeModel.nodesWereInserted(newParent, new int[]{newParent.getChildCount() - 1}); - if (wasSelected) { - myTree.expandPath(TreeUtil.getPath(myRoot, newParent)); - myTree.setSelectionPath(TreeUtil.getPath(myRoot, this)); - } - } - finally { - myAutoScrollEnabled = autoScrollWasEnabled; - } + boolean autoScrollWasEnabled = myAutoScrollEnabled; + try { + myAutoScrollEnabled = false; + ModuleGroupingTreeHelper helper = createGroupingHelper(); + MyNode newNode = helper.moveModuleNodeToProperGroup(this, getModule(), myRoot, treeModel, myTree); + treeModel.reload(newNode); + } + finally { + myAutoScrollEnabled = autoScrollWasEnabled; } - super.reloadNode(treeModel); } } - private static class ModuleGroupNode extends MyNode { + private interface ModuleGroupNode extends MutableTreeNode { + @Nullable + ModuleGroup getModuleGroup(); + } + + private static class ModuleGroupNodeImpl extends MyNode implements ModuleGroupNode { private final ModuleGroup myModuleGroup; - public ModuleGroupNode(@NotNull NamedConfigurable configurable, ModuleGroup moduleGroup) { + public ModuleGroupNodeImpl(@NotNull NamedConfigurable configurable, @NotNull ModuleGroup moduleGroup) { super(configurable, true); myModuleGroup = moduleGroup; } + + @Override + public ModuleGroup getModuleGroup() { + return myModuleGroup; + } } private class FacetInModuleRemoveHandler extends RemoveConfigurableHandler { @@ -796,7 +782,7 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple @Override public void setSelected(AnActionEvent e, boolean state) { myFlattenModules = state; - regroupModules(myFlattenModules); + regroupModules(); } } @@ -830,39 +816,22 @@ public class ModuleStructureConfigurable extends BaseStructureConfigurable imple @Override public void setSelected(AnActionEvent e, boolean state) { myHideModuleGroups = state; - regroupModules(state); + regroupModules(); } } - private void regroupModules(boolean removeGroupNodes) { + private void regroupModules() { DefaultMutableTreeNode selection = null; final TreePath selectionPath = myTree.getSelectionPath(); if (selectionPath != null) { selection = (DefaultMutableTreeNode)selectionPath.getLastPathComponent(); } - ModuleGrouper grouper = getModuleGrouper(); - for (Module module : grouper.getAllModules()) { - final List groupPath = grouper.getGroupPath(module); - updateProjectTree(new Module[]{module}, !groupPath.isEmpty() ? new ModuleGroup(groupPath) : null); - } - if (removeGroupNodes) { - removeModuleGroups(); - } + createGroupingHelper().moveAllModuleNodesToProperGroups(myRoot, getTreeModel()); if (selection != null) { TreeUtil.selectInTree(selection, true, myTree); } } - private void removeModuleGroups() { - for (int i = myRoot.getChildCount() - 1; i >= 0; i--) { - final MyNode node = (MyNode)myRoot.getChildAt(i); - if (node.getConfigurable().getEditableObject() instanceof ModuleGroup) { - node.removeFromParent(); - } - } - ((DefaultTreeModel)myTree.getModel()).reload(myRoot); - } - @Override protected AbstractAddGroup createAddAction() { return new AbstractAddGroup(ProjectBundle.message("add.new.header.text")) { diff --git a/platform/lang-impl/src/com/intellij/ide/projectView/impl/ModuleGroupingTreeHelper.kt b/platform/lang-impl/src/com/intellij/ide/projectView/impl/ModuleGroupingTreeHelper.kt new file mode 100644 index 000000000000..552892c4b961 --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/projectView/impl/ModuleGroupingTreeHelper.kt @@ -0,0 +1,242 @@ +/* + * 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. + */ +/* + * 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.ide.projectView.impl + +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleGrouper +import com.intellij.openapi.util.Pair +import com.intellij.util.ui.tree.TreeUtil +import org.jetbrains.annotations.TestOnly +import java.util.* +import javax.swing.JTree +import javax.swing.tree.DefaultTreeModel +import javax.swing.tree.MutableTreeNode +import javax.swing.tree.TreeNode + +/** + * Provides methods to build trees where nodes are grouped by modules (and optionally by module groups). + * + * @author nik + */ +class ModuleGroupingTreeHelper private constructor( + private val groupingEnabled: Boolean, + private val grouper: ModuleGrouper, + private val moduleGroupNodeFactory: (ModuleGroup) -> N, + private val moduleNodeFactory: (Module, ModuleGrouper) -> N, + private val nodeComparator: Comparator +) { + private val nodeForGroup = HashMap() + private val nodeData = HashMap() + + companion object { + @JvmStatic + fun forEmptyTree(groupingEnabled: Boolean, grouper: ModuleGrouper, + moduleGroupNodeFactory: (ModuleGroup) -> N, moduleNodeFactory: (Module, ModuleGrouper) -> N, + nodeComparator: Comparator) = + ModuleGroupingTreeHelper(groupingEnabled, grouper, moduleGroupNodeFactory, moduleNodeFactory, nodeComparator) + + @JvmStatic + fun forTree(rootNode: N, moduleGroupByNode: (N) -> ModuleGroup?, moduleByNode: (N) -> Module?, + groupingEnabled: Boolean, grouper: ModuleGrouper, + moduleGroupNodeFactory: (ModuleGroup) -> N, moduleNodeFactory: (Module, ModuleGrouper) -> N, + nodeComparator: Comparator): ModuleGroupingTreeHelper { + val helper = ModuleGroupingTreeHelper(groupingEnabled, grouper, moduleGroupNodeFactory, moduleNodeFactory, nodeComparator) + TreeUtil.traverse(rootNode) { node -> + @Suppress("UNCHECKED_CAST") + val group = moduleGroupByNode(node as N) + val module = moduleByNode(node) + if (group != null) { + helper.nodeForGroup[group] = node + } + if (group != null || module != null) { + helper.nodeData[node] = ModuleTreeNodeData(module, group) + } + true + } + return helper + } + } + + fun createModuleNodes(modules: Collection, rootNode: N, model: DefaultTreeModel): List { + val nodes = modules.map { createModuleNode(it, rootNode, model) } + TreeUtil.sortRecursively(rootNode, nodeComparator) + model.nodeStructureChanged(rootNode) + return nodes + } + + private fun createModuleNode(module: Module, rootNode: N, model: DefaultTreeModel): N { + val group = ModuleGroup(grouper.getGroupPath(module)) + val parentNode = getOrCreateNodeForModuleGroup(group, rootNode, model, true) + val moduleNode = moduleNodeFactory(module, grouper) + insertModuleNode(moduleNode, parentNode, module, model, true) + return moduleNode + } + + /** + * If [bulkOperation] is true, no events will be fired and new node will be added into arbitrary place in the children list + */ + private fun insertModuleNode(moduleNode: N, parentNode: N, module: Module, model: DefaultTreeModel, bulkOperation: Boolean) { + val moduleAsGroup = grouper.getModuleAsGroupPath(module) + if (moduleAsGroup != null) { + val moduleGroup = ModuleGroup(moduleAsGroup) + val oldModuleGroupNode = nodeForGroup[moduleGroup] + if (oldModuleGroupNode != null) { + moveChildren(oldModuleGroupNode, moduleNode, model) + model.removeNodeFromParent(oldModuleGroupNode) + removeNode(oldModuleGroupNode) + } + nodeForGroup[moduleGroup] = moduleNode + nodeData[moduleNode] = ModuleTreeNodeData(module, moduleGroup) + } + else { + nodeData[moduleNode] = ModuleTreeNodeData(module, null) + } + + insertNode(moduleNode, parentNode, model, bulkOperation) + } + + private fun moveChildren(fromNode: N, toNode: N, model: DefaultTreeModel) { + val children = TreeUtil.childrenToArray(fromNode) + moveChildren(children, toNode, model) + } + + private fun moveChildren(children: List, toNode: N, model: DefaultTreeModel) { + TreeUtil.addChildrenTo(toNode, children) + TreeUtil.sortChildren(toNode, nodeComparator) + model.nodeStructureChanged(toNode) + } + + /** + * If [bulkOperation] is true, no events will be fired and new node will be added into arbitrary place in the children list + */ + private fun getOrCreateNodeForModuleGroup(group: ModuleGroup, rootNode: N, model: DefaultTreeModel, bulkOperation: Boolean): N { + if (!groupingEnabled) return rootNode + + var parentNode = rootNode + val path = group.groupPathList + for (i in path.indices) { + val current = ModuleGroup(path.subList(0, i+1)) + var node = nodeForGroup[current] + if (node == null) { + node = moduleGroupNodeFactory(current) + insertNode(node, parentNode, model, bulkOperation) + nodeForGroup[current] = node + nodeData[node] = ModuleTreeNodeData(null,group) + } + parentNode = node + } + return parentNode + } + + private fun insertNode(node: N, parentNode: N, model: DefaultTreeModel, bulkOperation: Boolean) { + if (bulkOperation) { + parentNode.insert(node, parentNode.childCount) + } + else { + TreeUtil.insertNode(node, parentNode, model, nodeComparator) + } + } + + fun moveAllModuleNodesToProperGroups(rootNode: N, model: DefaultTreeModel) { + val modules = nodeData.values.map { it.module }.filterNotNull() + nodeData.keys.forEach { it.removeFromParent() } + nodeData.clear() + nodeForGroup.clear() + createModuleNodes(modules, rootNode, model) + } + + fun moveModuleNodesToProperGroup(nodes: List>, rootNode: N, model: DefaultTreeModel, tree: JTree) { + nodes.forEach { + moveModuleNodeToProperGroup(it.first, it.second, rootNode, model, tree) + } + } + + fun moveModuleNodeToProperGroup(node: N, module: Module, rootNode: N, model: DefaultTreeModel, tree: JTree): N { + val actualGroup = ModuleGroup(grouper.getGroupPath(module)) + val parent = node.parent + val expectedParent = if (groupingEnabled) nodeForGroup[actualGroup] else rootNode + if (expectedParent == parent) return node + + val selectionPath = tree.selectionPath + val wasSelected = selectionPath?.lastPathComponent == node + model.removeNodeFromParent(node) + + val nodeAsGroup = nodeData[node]?.group + removeNode(node) + if (nodeAsGroup != null) { + val childrenToKeep = TreeUtil.childrenToArray(node).filter { it in nodeData } + if (childrenToKeep.isNotEmpty()) { + val newGroupNode = getOrCreateNodeForModuleGroup(nodeAsGroup, rootNode, model, false) + moveChildren(childrenToKeep, newGroupNode, model) + } + } + + removeEmptySyntheticModuleGroupNodes(parent, model) + + val newParent = getOrCreateNodeForModuleGroup(actualGroup, rootNode, model, false) + val newNode = moduleNodeFactory(module, grouper) + insertModuleNode(newNode, newParent, module, model, false) + + if (wasSelected) { + tree.expandPath(TreeUtil.getPath(rootNode, newParent)) + tree.selectionPath = TreeUtil.getPath(rootNode, newNode) + } + return newNode + } + + private fun removeEmptySyntheticModuleGroupNodes(parentNode: TreeNode?, model: DefaultTreeModel) { + var parent = parentNode + while (parent is MutableTreeNode && parent in nodeData && nodeData[parent]?.module == null && parent.childCount == 0) { + val grandParent = parent.parent + model.removeNodeFromParent(parent) + removeNode(parent as N) + parent = grandParent + } + } + + private fun removeNode(node: N) { + val group = nodeData.remove(node)?.group + if (group != null) { + nodeForGroup.remove(group) + } + } + + @TestOnly + fun getNodeForGroupMap() = Collections.unmodifiableMap(nodeForGroup) + + @TestOnly + fun getModuleByNodeMap() = nodeData.mapValues { it.value.module }.filterValues { it != null } + + @TestOnly + fun getGroupByNodeMap() = nodeData.mapValues { it.value.group }.filterValues { it != null } +} + +private class ModuleTreeNodeData(val module: Module?, val group: ModuleGroup?) \ No newline at end of file diff --git a/platform/lang-impl/testSources/com/intellij/ide/projectView/ModuleGroupingTreeHelperTest.kt b/platform/lang-impl/testSources/com/intellij/ide/projectView/ModuleGroupingTreeHelperTest.kt new file mode 100644 index 000000000000..795ba8783fd6 --- /dev/null +++ b/platform/lang-impl/testSources/com/intellij/ide/projectView/ModuleGroupingTreeHelperTest.kt @@ -0,0 +1,258 @@ +/* + * 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.ide.projectView + +import com.intellij.ide.projectView.impl.ModuleGroup +import com.intellij.ide.projectView.impl.ModuleGroupingTreeHelper +import com.intellij.module.getQualifiedNameModuleGrouper +import com.intellij.module.renameModule +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleGrouper +import com.intellij.openapi.util.Pair +import com.intellij.testFramework.PlatformTestCase +import com.intellij.testFramework.PlatformTestUtil +import com.intellij.ui.treeStructure.Tree +import com.intellij.util.ui.tree.TreeUtil +import junit.framework.TestCase +import java.util.* +import javax.swing.tree.DefaultMutableTreeNode +import javax.swing.tree.DefaultTreeModel + +/** + * @author nik + */ +class ModuleGroupingTreeHelperTest: PlatformTestCase() { + private lateinit var tree: Tree + private lateinit var root: MockModuleTreeNode + private lateinit var model: DefaultTreeModel + + override fun setUp() { + super.setUp() + root = MockModuleTreeNode("root") + model = DefaultTreeModel(root) + tree = Tree(model) + } + + fun `test disabled grouping`() { + createHelper(false).createModuleNodes("a.main") + assertTreeEqual(""" + -root + a.main""") + createHelperFromTree(true).moveAllModuleNodesAndCheckResult(""" + -root + -a + a.main""") + } + + fun `test single module`() { + createHelper().createModuleNodes("a.main") + assertTreeEqual(""" + -root + -a + a.main""") + + createHelperFromTree(false).moveAllModuleNodesAndCheckResult(""" + -root + a.main""") + } + + fun `test two modules`() { + createHelper().createModuleNodes("a.main", "a.util") + assertTreeEqual(""" + -root + -a + a.main + a.util +""") + + createHelperFromTree(false).moveAllModuleNodesAndCheckResult(""" + -root + a.main + a.util""") + } + + fun `test two groups`() { + createHelper().createModuleNodes("a.main", "b.util", "a.util", "b.main") + assertTreeEqual(""" + -root + -a + a.main + a.util + -b + b.main + b.util +""") + + createHelperFromTree(false).moveAllModuleNodesAndCheckResult(""" + -root + a.main + a.util + b.main + b.util +""") + } + + fun `test module as a group`() { + createHelper().createModuleNodes("a.impl", "a", "a.tests") + assertTreeEqual(""" + -root + -a + a.impl + a.tests +""") + + createHelperFromTree(false).moveAllModuleNodesAndCheckResult(""" + -root + a + a.impl + a.tests""") + } + + fun `test move module node to new group`() { + val nodes = createHelper().createModuleNodes("main", "util") + assertTreeEqual(""" + -root + main + util""") + val node = nodes.find { it.second.name == "main" }!! + renameModule(node.second, "a.main") + moveModuleNodeToProperGroupAndCheckResult(node, """ + -root + -a + a.main + util""") + } + + fun `test move module node from parent module`() { + val nodes = createHelper().createModuleNodes("a", "a.main") + assertTreeEqual(""" + -root + -a + a.main""") + val node = nodes.find { it.second.name == "a.main" }!! + renameModule(node.second, "main") + moveModuleNodeToProperGroupAndCheckResult(node, """ + -root + a + main""") + } + + fun `test move module node to parent module`() { + val nodes = createHelper().createModuleNodes("a", "main") + val node = nodes.find { it.second.name == "main" }!! + renameModule(node.second, "a.main") + moveModuleNodeToProperGroupAndCheckResult(node, """ + -root + -a + a.main""") + } + + fun `test module node become parent module`() { + val nodes = createHelper().createModuleNodes("b", "a.main") + val node = nodes.find { it.second.name == "b" }!! + renameModule(node.second, "a") + moveModuleNodeToProperGroupAndCheckResult(node, """ + -root + -a + a.main""") + } + + fun `test parent module become ordinary module`() { + val nodes = createHelper().createModuleNodes("a", "a.main") + val node = nodes.find { it.second.name == "a" }!! + renameModule(node.second, "b") + moveModuleNodeToProperGroupAndCheckResult(node, """ + -root + -a + a.main + b""") + } + + private fun moveModuleNodeToProperGroupAndCheckResult(node: Pair, + expected: String) { + val helper = createHelperFromTree() + helper.checkConsistency() + helper.moveModuleNodeToProperGroup(node.first, node.second, root, model, tree) + assertTreeEqual(expected) + helper.checkConsistency() + } + + private fun ModuleGroupingTreeHelper.moveAllModuleNodesAndCheckResult(expected: String) { + checkConsistency() + moveAllModuleNodesToProperGroups(root, model) + assertTreeEqual(expected) + checkConsistency() + } + + private fun ModuleGroupingTreeHelper.createModuleNodes(vararg names: String): List> { + val modules = createModules(*names) + val nodes = createModuleNodes(modules, root, model) + return nodes.map { Pair(it, (it as MockModuleNode).module)} + } + + private fun assertTreeEqual(expected: String) { + TreeUtil.expandAll(tree) + PlatformTestUtil.assertTreeEqual(tree, expected.trimIndent() + "\n") + } + + private fun createHelper(enableGrouping: Boolean = true): ModuleGroupingTreeHelper { + val grouper = getQualifiedNameModuleGrouper(myProject) + return ModuleGroupingTreeHelper.forEmptyTree(enableGrouping, grouper, ::MockModuleGroupNode, ::MockModuleNode, nodeComparator) + } + + private fun createHelperFromTree(enableGrouping: Boolean = true): ModuleGroupingTreeHelper { + val grouper = getQualifiedNameModuleGrouper(myProject) + return ModuleGroupingTreeHelper.forTree(root, { it.moduleGroup }, { (it as? MockModuleNode)?.module }, + enableGrouping, grouper, ::MockModuleGroupNode, ::MockModuleNode, nodeComparator) + } + + private fun createModules(vararg names: String) = names.map { createModule(it) } + + private fun ModuleGroupingTreeHelper.checkConsistency() { + val expectedNodeForGroup = HashMap(getNodeForGroupMap()) + val expectedGroupByNode = HashMap(getGroupByNodeMap()) + val expectedModuleByNode = HashMap(getModuleByNodeMap()) + TreeUtil.traverse(root, { o -> + val node = o as MockModuleTreeNode + if (node == root) return@traverse true + TestCase.assertSame(node, expectedNodeForGroup[node.moduleGroup]) + expectedNodeForGroup.remove(node.moduleGroup) + TestCase.assertEquals(node.moduleGroup, expectedGroupByNode[node]) + expectedGroupByNode.remove(node) + if (node is MockModuleNode) { + TestCase.assertEquals(node.module, expectedModuleByNode[node]) + expectedModuleByNode.remove(node) + } + true + }) + assertEmpty("Unexpected nodes in helper", expectedNodeForGroup.entries) + assertEmpty("Unexpected groups in helper", expectedGroupByNode.entries) + assertEmpty("Unexpected modules in helper", expectedModuleByNode.entries) + } +} + +private val nodeComparator = Comparator.comparing { node: MockModuleTreeNode -> node.text } + +private open class MockModuleTreeNode(userObject: Any, val text: String = userObject.toString()): DefaultMutableTreeNode(text) { + open val moduleGroup: ModuleGroup? = null + override fun toString() = text +} + +private class MockModuleNode(val module: Module, grouper: ModuleGrouper): MockModuleTreeNode(module, module.name) { + override val moduleGroup = grouper.getModuleAsGroupPath(module)?.let(::ModuleGroup) +} + +private class MockModuleGroupNode(override val moduleGroup: ModuleGroup): MockModuleTreeNode(moduleGroup) \ No newline at end of file diff --git a/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java b/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java index 2cc83a834798..4586527144e8 100644 --- a/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java +++ b/platform/platform-api/src/com/intellij/util/ui/tree/TreeUtil.java @@ -332,16 +332,28 @@ public final class TreeUtil { } public static void sort(@NotNull final DefaultMutableTreeNode node, @Nullable Comparator comparator) { - final List children = childrenToArray(node); - Collections.sort(children, comparator); - node.removeAllChildren(); - addChildrenTo(node, children); + sortRecursively(node, comparator); + } + + public static void sortRecursively(@NotNull T node, @Nullable Comparator comparator) { + sortChildren(node, comparator); for (int i = 0; i < node.getChildCount(); i++) { - sort((DefaultMutableTreeNode) node.getChildAt(i), comparator); + //noinspection unchecked + sortRecursively((T) node.getChildAt(i), comparator); } } - public static void addChildrenTo(@NotNull final MutableTreeNode node, @NotNull final List children) { + public static void sortChildren(@NotNull T node, @Nullable Comparator comparator) { + //noinspection unchecked + final List children = (List)childrenToArray(node); + Collections.sort(children, comparator); + for (int i = node.getChildCount() - 1; i >= 0; i--) { + node.remove(i); + } + addChildrenTo(node, children); + } + + public static void addChildrenTo(@NotNull final MutableTreeNode node, @NotNull final List children) { for (final Object aChildren : children) { final MutableTreeNode child = (MutableTreeNode)aChildren; node.insert(child, node.getChildCount()); diff --git a/platform/platform-tests/testSrc/com/intellij/module/GroupModulesByQualifiedNamesTest.kt b/platform/platform-tests/testSrc/com/intellij/module/GroupModulesByQualifiedNamesTest.kt index 7a899121906a..127d1bbc8231 100644 --- a/platform/platform-tests/testSrc/com/intellij/module/GroupModulesByQualifiedNamesTest.kt +++ b/platform/platform-tests/testSrc/com/intellij/module/GroupModulesByQualifiedNamesTest.kt @@ -17,7 +17,6 @@ package com.intellij.module import com.intellij.ide.projectView.impl.ModuleGroup import com.intellij.openapi.module.ModuleGrouper -import com.intellij.openapi.util.registry.Registry import com.intellij.testFramework.PlatformTestCase import org.junit.Assert import org.junit.Test @@ -59,14 +58,5 @@ class GroupModulesByQualifiedNamesTest : PlatformTestCase() { } private val grouper: ModuleGrouper - get() { - val property = Registry.get("project.qualified.module.names") - return try { - property.setValue(true) - ModuleGrouper.instanceFor(myProject) - } - finally { - property.setValue(false) - } - } + get() = getQualifiedNameModuleGrouper(myProject) } diff --git a/platform/projectModel-api/src/com/intellij/openapi/module/ModuleGrouper.kt b/platform/projectModel-api/src/com/intellij/openapi/module/ModuleGrouper.kt index 1d4c856f6c9a..a06a1280391e 100644 --- a/platform/projectModel-api/src/com/intellij/openapi/module/ModuleGrouper.kt +++ b/platform/projectModel-api/src/com/intellij/openapi/module/ModuleGrouper.kt @@ -43,6 +43,11 @@ abstract class ModuleGrouper { abstract fun getGroupPathByModuleName(name: String): List + /** + * If [module] itself can be considered as a group, returns its groups. Otherwise returns null. + */ + abstract fun getModuleAsGroupPath(module: Module): List? + abstract fun getAllModules(): Array companion object { @@ -76,6 +81,8 @@ private class QualifiedNameGrouper(project: Project, model: ModifiableModuleMode override fun getShortenedNameByFullModuleName(name: String) = StringUtil.getShortName(name) override fun getGroupPathByModuleName(name: String) = name.split('.').dropLast(1) + + override fun getModuleAsGroupPath(module: Module) = getModuleName(module).split('.') } private class ExplicitModuleGrouper(project: Project, model: ModifiableModuleModel?): ModuleGrouperBase(project, model) { @@ -87,4 +94,6 @@ private class ExplicitModuleGrouper(project: Project, model: ModifiableModuleMod override fun getShortenedNameByFullModuleName(name: String) = name override fun getGroupPathByModuleName(name: String): List = emptyList() + + override fun getModuleAsGroupPath(module: Module) = null } diff --git a/platform/testFramework/src/com/intellij/module/moduleGroupTests.kt b/platform/testFramework/src/com/intellij/module/moduleGroupTests.kt new file mode 100644 index 000000000000..792db4c6ab4d --- /dev/null +++ b/platform/testFramework/src/com/intellij/module/moduleGroupTests.kt @@ -0,0 +1,44 @@ +/* + * 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.module + +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleGrouper +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.registry.Registry + +/** + * @author nik + */ +fun getQualifiedNameModuleGrouper(project: Project): ModuleGrouper { + val property = Registry.get("project.qualified.module.names") + return try { + property.setValue(true) + ModuleGrouper.instanceFor(project) + } + finally { + property.setValue(false) + } + +} + +fun renameModule(module: Module, newName: String) { + val model = ModuleManager.getInstance(module.project).modifiableModel + model.renameModule(module, newName) + runWriteAction { model.commit() } +} \ No newline at end of file