| /* |
| * Copyright (C) 2013 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.ui; |
| |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Lists; |
| import com.intellij.openapi.util.io.FileUtil; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import javax.swing.*; |
| import javax.swing.event.TreeModelListener; |
| import javax.swing.tree.TreeModel; |
| import javax.swing.tree.TreePath; |
| import java.io.File; |
| import java.util.List; |
| |
| /** |
| * In-memory tree representation of a file tree. Must be created with a file tree, |
| * and then additional files (which may or may not exist) may be added to the representation. |
| * Can be rendered to a {@link JTree} using the {@link FileTreeCellRenderer}. |
| */ |
| public class FileTreeModel implements TreeModel { |
| |
| /** |
| * Root file that this model was created with. |
| */ |
| private File myRoot; |
| |
| /** |
| * Root of the data structure representation. |
| */ |
| private Node myRootNode; |
| |
| private boolean myHideIrrelevantFiles; |
| |
| public FileTreeModel(@NotNull File root, boolean hideIrrelevantFiles) { |
| this(root); |
| myHideIrrelevantFiles = hideIrrelevantFiles; |
| } |
| |
| public FileTreeModel(@NotNull File root) { |
| myRoot = root; |
| myRootNode = makeTree(root); |
| } |
| |
| /** |
| * Return the root {@link Node} of this representation. |
| */ |
| @Override |
| public Object getRoot() { |
| if (myHideIrrelevantFiles && !myRootNode.isProposedFile) { |
| return null; |
| } |
| return myRootNode; |
| } |
| |
| /** |
| * Get the Nth child {@link Node} of the given parent. |
| */ |
| @Override |
| public Object getChild(Object parent, int index) { |
| Node n = (Node)parent; |
| if (!myHideIrrelevantFiles) { |
| return n.children.get(index); |
| } |
| |
| for (int i = 0; i < n.children.size(); i++) { |
| Node child = n.children.get(i); |
| if (child.isProposedFile && index == 0) { |
| return child; |
| } else if (child.isProposedFile) { |
| index--; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Get the number of children that the given parent {@link Node} has. |
| */ |
| @Override |
| public int getChildCount(Object parent) { |
| if (!myHideIrrelevantFiles) { |
| return ((Node)parent).children.size(); |
| } |
| int count = 0; |
| for (Node n : ((Node)parent).children) { |
| if (n.isProposedFile) { |
| count++; |
| } |
| } |
| return count; |
| } |
| |
| /** |
| * Returns true iff the given {@link Node} has no children (is a leaf) |
| */ |
| @Override |
| public boolean isLeaf(Object node) { |
| if (!myHideIrrelevantFiles) { |
| return ((Node)node).children.isEmpty(); |
| } |
| for (Node n : ((Node)node).children) { |
| if (n.isProposedFile) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @Override |
| public void valueForPathChanged(TreePath path, Object newValue) { |
| // Not implemented |
| } |
| |
| /** |
| * Returns the index of the given child inside the given parent or -1 if given node is not a child of the parent. |
| */ |
| @Override |
| public int getIndexOfChild(Object parent, Object child) { |
| if (!myHideIrrelevantFiles) { |
| //noinspection SuspiciousMethodCalls |
| return ((Node)parent).children.indexOf(child); |
| } |
| Node n = (Node)parent; |
| int index = 0; |
| for (int i = 0; i < n.children.size(); i++) { |
| Node candidate = n.children.get(i); |
| if (candidate.equals(child)) { |
| return index; |
| } |
| if (candidate.isProposedFile) { |
| index++; |
| } |
| } |
| return -1; |
| } |
| |
| @Override |
| public void addTreeModelListener(TreeModelListener l) { |
| // Not implemented |
| } |
| |
| @Override |
| public void removeTreeModelListener(TreeModelListener l) { |
| // Not implemented |
| } |
| |
| /** |
| * Check to see if there are any conflicts (multiple files added to the same location) in the tree. |
| */ |
| public boolean hasConflicts() { |
| if (myRootNode == null) { |
| return false; |
| } |
| return treeHasConflicts(myRootNode); |
| } |
| |
| /** |
| * DFS through the tree looking for conflicted nodes. |
| */ |
| private static boolean treeHasConflicts(Node root) { |
| if (root.isConflicted) { |
| return true; |
| } |
| |
| for (Node n : root.children) { |
| if (treeHasConflicts(n)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Add the given file to the representation. |
| * This is a no-op if the given path already exists within the tree. |
| */ |
| public void addFile(@NotNull File f) { |
| addFile(f, null); |
| } |
| |
| /** |
| * Add the given file to the representation and mark it with the given icon. |
| * This is a no-op if the given path already exists within the tree. |
| */ |
| public void addFile(@NotNull File f, @Nullable Icon ic) { |
| String s = f.isAbsolute() ? FileUtil.getRelativePath(myRoot, f) : f.getPath(); |
| if (s != null) { |
| List<String> parts = Lists.newLinkedList(Splitter.on(File.separatorChar).split(s)); |
| makeNode(myRootNode, parts, ic, false); |
| } |
| } |
| |
| /** |
| * Add the given file to the representation and mark it with the given icon. |
| * If the path already exists within the tree it will be marked as a conflicting path. |
| */ |
| public void forceAddFile(@NotNull File f, @Nullable Icon ic) { |
| String s = f.isAbsolute() ? FileUtil.getRelativePath(myRoot, f) : f.getPath(); |
| if (s != null) { |
| List<String> parts = Lists.newLinkedList(Splitter.on(File.separatorChar).split(s)); |
| makeNode(myRootNode, parts, ic, true); |
| } |
| } |
| |
| /** |
| * Representation of a node within the tree |
| */ |
| protected static class Node { |
| public String name; |
| public List<Node> children = Lists.newLinkedList(); |
| public boolean existsOnDisk; |
| public boolean isConflicted; |
| public boolean isProposedFile; |
| public Icon icon; |
| |
| @Override |
| public String toString() { |
| return name; |
| } |
| |
| /** |
| * Returns true iff this node has a child with the given name. |
| */ |
| public boolean hasChild(String name) { |
| for (Node child : children) { |
| if (child.name.equals(name)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Returns the child with the given name or null |
| */ |
| @Nullable |
| public Node getChild(String name) { |
| for (Node child : children) { |
| if (child.name.equals(name)) { |
| return child; |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Recursively build the node(s) specified in the given path hierarchy starting at the given root. |
| * Mark the last node in the path with the given icon. If markConflict is set, mark the final node |
| * as conflicted if it already exists. |
| */ |
| private static void makeNode(@NotNull Node root, @NotNull List<String> path, @Nullable Icon ic, boolean markConflict) { |
| root.isProposedFile = true; |
| if (path.isEmpty()) { |
| return; |
| } |
| |
| String name = path.get(0); |
| |
| if (markConflict) { |
| if (path.size() == 1 && root.name.equals(name)) { |
| root.isConflicted = true; |
| return; |
| } |
| } |
| if (root.name.equals(name)) { |
| // Continue down along already-created paths |
| makeNode(root, rest(path), ic, markConflict); |
| } else if (root.hasChild(name)) { |
| // Allow paths relative to root (rather than including root explicitly) |
| if (markConflict && path.size() == 1) { |
| Node targetNode = root.getChild(name); |
| targetNode.isConflicted = true; |
| targetNode.icon = ic; |
| targetNode.isProposedFile = true; |
| return; |
| } |
| //noinspection ConstantConditions |
| makeNode(root.getChild(name), rest(path), ic, markConflict); |
| } else { |
| // If this node in the path doesn't exist, then create it. |
| Node n = new Node(); |
| n.name = name; |
| root.children.add(n); |
| if (path.size() == 1) { |
| // If this is the end of the path, mark with the given icon |
| n.icon = ic; |
| n.isProposedFile = true; |
| } else { |
| // Continue down to create the rest of the path |
| makeNode(n, rest(path), ic, markConflict); |
| } |
| } |
| } |
| |
| /** |
| * Populate a tree from the file hierarchy rooted at the given file. |
| */ |
| private static Node makeTree(@NotNull File root) { |
| Node n = new Node(); |
| n.name = root.getName(); |
| n.existsOnDisk = root.exists(); |
| if (root.isDirectory()) { |
| File[] children = root.listFiles(); |
| if (children != null) { |
| for (File f : children) { |
| if (!f.isHidden()) { |
| n.children.add(makeTree(f)); |
| } |
| } |
| } |
| } |
| return n; |
| } |
| |
| /** |
| * Convenience function. Operates on a list and returns a list containing all elements but the first. |
| */ |
| private static <T> List<T> rest(List<T> list) { |
| return list.subList(1, list.size()); |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| toString(sb, myRootNode); |
| return sb.toString(); |
| } |
| |
| /** |
| * DFS over the tree to build a string representation e.g. (root (child (grandchild) (grandchild)) (child)) |
| */ |
| private void toString(StringBuilder sb, Node root) { |
| sb.append('('); |
| sb.append(root.name); |
| if (!isLeaf(root)) { |
| sb.append(' '); |
| } |
| for (Node child : root.children) { |
| toString(sb, child); |
| } |
| sb.append(')'); |
| } |
| } |