/* * Copyright (C) 2014 The Android Open Source Project * * 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.android.tools.idea.navigator; import com.android.tools.idea.navigator.nodes.DirectoryGroupNode; import com.google.common.collect.Maps; import com.intellij.ide.projectView.impl.ProjectAbstractTreeStructureBase; import com.intellij.ide.projectView.impl.ProjectTreeBuilder; import com.intellij.ide.util.treeView.AbstractTreeNode; import com.intellij.ide.util.treeView.AbstractTreeUpdater; import com.intellij.ide.util.treeView.NodeDescriptor; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.openapi.vfs.newvfs.BulkFileListener; import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent; import com.intellij.openapi.vfs.newvfs.events.VFileEvent; import com.intellij.psi.PsiDirectory; import com.intellij.psi.PsiFile; import com.intellij.util.messages.MessageBusConnection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import java.util.Comparator; import java.util.Enumeration; import java.util.List; import java.util.Map; public class AndroidProjectTreeBuilder extends ProjectTreeBuilder { private Map<VirtualFile,AbstractTreeNode> myFileToNodeMap = Maps.newHashMap(); public AndroidProjectTreeBuilder(@NotNull Project project, @NotNull JTree tree, @NotNull DefaultTreeModel treeModel, @Nullable Comparator<NodeDescriptor> comparator, @NotNull ProjectAbstractTreeStructureBase treeStructure) { super(project, tree, treeModel, comparator, treeStructure); MessageBusConnection connection = project.getMessageBus().connect(project); connection.subscribe(VirtualFileManager.VFS_CHANGES, new BulkFileListener.Adapter() { @Override public void after(@NotNull List<? extends VFileEvent> events) { for (VFileEvent e : events) { if (e instanceof VFileDeleteEvent) { removeMapping(e.getFile()); } } } }); } @Nullable @Override protected AbstractTreeUpdater createUpdater() { return new AndroidTreeUpdater(getTreeStructure(), this); } @NotNull public Project getProject() { return myProject; } /** * Returns the tree node corresponding to a model element. * e.g. from a PsiDirectory -> tree node corresponding to that PsiDirectory * * When {@link com.intellij.ide.util.treeView.AbstractTreeUi} creates a {@link javax.swing.tree.DefaultMutableTreeNode} for a given * {@link com.intellij.ide.util.treeView.AbstractTreeNode}, it maintains a mapping between. This mapping between the model element to * the tree node is necessary when locating items by their model element (PsiFile or PsiDirectory). * * In the Android view, we have virtual nodes that don't correspond to Psi Elements, or a single virtual node corresponding to * multiple files/directories. Since such mappings aren't saved by the tree UI, these are handled in this method. * * The way this works is that every time a virtual node is created, it calls back to {@link #createMapping()} to save that mapping * between the virtual file and the node that represents it. When we need to map a virtual file to its tree node, we look at the * saved mapping to see if that virtual file corresponds to any node. If so, we obtain the tree node corresponding to the node's parent, * then iterate through its children to locate the tree node corresponding to the element. */ @Nullable @Override protected Object findNodeByElement(@Nullable Object element) { if (element == null) { return null; } Object node = super.findNodeByElement(element); if (node != null) { return node; } VirtualFile virtualFile = null; if (element instanceof PsiDirectory) { virtualFile = ((PsiDirectory)element).getVirtualFile(); } else if (element instanceof PsiFile) { virtualFile = ((PsiFile)element).getVirtualFile(); } if (virtualFile == null) { return null; } AbstractTreeNode treeNode = getNodeForFile(virtualFile); if (treeNode == null) { return null; } // recurse and find the tree node corresponding to the parent Object parentNode = findNodeByElement(treeNode.getParent()); if (!(parentNode instanceof DefaultMutableTreeNode)) { return null; } // examine all the children of the parent tree node and return the one that maps to this virtual file. Enumeration children = ((DefaultMutableTreeNode)parentNode).children(); while (children.hasMoreElements()) { DefaultMutableTreeNode child = (DefaultMutableTreeNode)children.nextElement(); if (child.getUserObject() instanceof DirectoryGroupNode) { for (PsiDirectory folder : ((DirectoryGroupNode)child.getUserObject()).getDirectories()) { if (folder.getVirtualFile().equals(virtualFile)) { return child; } } } } return null; } public void createMapping(@NotNull VirtualFile file, @NotNull AbstractTreeNode node) { myFileToNodeMap.put(file, node); } private void removeMapping(@Nullable VirtualFile file) { myFileToNodeMap.remove(file); } @Nullable private AbstractTreeNode getNodeForFile(@NotNull VirtualFile file) { return myFileToNodeMap.get(file); } }