blob: 71d114401127f06e386f4bb4138161e262733766 [file] [log] [blame]
/*
* 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(')');
}
}