blob: 48a17aeaa31a4a319f244d15a4d2ad8a2b1c43d1 [file] [log] [blame]
/*
* Copyright 2000-2009 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.vcs.changes.ui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FilePathImpl;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.VcsBundle;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.SimpleColoredComponent;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.tree.TreeUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import java.io.File;
import java.util.*;
/**
* @author max
*/
public class TreeModelBuilder {
@NonNls public static final String ROOT_NODE_VALUE = "root";
public static final String LOCALLY_DELETED_NODE = VcsBundle.message("changes.nodetitle.locally.deleted.files");
private final Project myProject;
private final boolean showFlatten;
private DefaultTreeModel model;
private final ChangesBrowserNode root;
private boolean myPolicyInitialized;
private ChangesGroupingPolicy myPolicy;
private HashMap<String, ChangesBrowserNode> myFoldersCache;
public TreeModelBuilder(final Project project, final boolean showFlatten) {
myProject = project;
this.showFlatten = showFlatten;
root = ChangesBrowserNode.create(myProject, ROOT_NODE_VALUE);
model = new DefaultTreeModel(root);
myFoldersCache = new HashMap<String, ChangesBrowserNode>();
}
public DefaultTreeModel buildModel(final List<Change> changes, final ChangeNodeDecorator changeNodeDecorator) {
Collections.sort(changes, MyChangePathLengthComparator.getInstance());
final ChangesGroupingPolicy policy = createGroupingPolicy();
for (final Change change : changes) {
insertChangeNode(change, policy, root, new Computable<ChangesBrowserNode>() {
@Override
public ChangesBrowserNode compute() {
return new ChangesBrowserChangeNode(myProject, change, changeNodeDecorator);
}
});
}
collapseDirectories(model, root);
sortNodes();
return model;
}
@Nullable
private ChangesGroupingPolicy createGroupingPolicy() {
if (! myPolicyInitialized) {
myPolicyInitialized = true;
final ChangesGroupingPolicyFactory factory = ChangesGroupingPolicyFactory.getInstance(myProject);
if (factory != null) {
myPolicy = factory.createGroupingPolicy(model);
}
}
return myPolicy;
}
public DefaultTreeModel buildModelFromFiles(@NotNull List<VirtualFile> files) {
buildVirtualFiles(files, null);
collapseDirectories(model, root);
sortNodes();
return model;
}
public DefaultTreeModel buildModelFromFilePaths(final Collection<FilePath> files) {
buildFilePaths(files, root);
collapseDirectories(model, root);
sortNodes();
return model;
}
private static class MyChangeNodeUnderChangeListDecorator extends RemoteStatusChangeNodeDecorator {
private final ChangeListRemoteState.Reporter myReporter;
private MyChangeNodeUnderChangeListDecorator(final RemoteRevisionsCache remoteRevisionsCache, final ChangeListRemoteState.Reporter reporter) {
super(remoteRevisionsCache);
myReporter = reporter;
}
@Override
protected void reportState(boolean state) {
myReporter.report(state);
}
@Override
public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
}
}
public DefaultTreeModel buildModel(final List<? extends ChangeList> changeLists,
final Trinity<List<VirtualFile>, Integer, Integer> unversionedFiles,
final List<LocallyDeletedChange> locallyDeletedFiles,
final List<VirtualFile> modifiedWithoutEditing,
final MultiMap<String, VirtualFile> switchedFiles,
@Nullable Map<VirtualFile, String> switchedRoots,
@Nullable final List<VirtualFile> ignoredFiles,
@Nullable final List<VirtualFile> lockedFolders,
@Nullable final Map<VirtualFile, LogicalLock> logicallyLockedFiles) {
resetGrouping();
buildModel(changeLists);
if (!modifiedWithoutEditing.isEmpty()) {
resetGrouping();
buildVirtualFiles(modifiedWithoutEditing, ChangesBrowserNode.MODIFIED_WITHOUT_EDITING_TAG);
}
final boolean manyUnversioned = unversionedFiles.getSecond() > unversionedFiles.getFirst().size();
if (manyUnversioned || ! unversionedFiles.getFirst().isEmpty()) {
resetGrouping();
if (manyUnversioned) {
final ChangesBrowserNode baseNode = new ChangesBrowserManyUnversionedFilesNode(myProject, unversionedFiles.getSecond(), unversionedFiles.getThird());
model.insertNodeInto(baseNode, root, root.getChildCount());
} else {
buildVirtualFiles(unversionedFiles.getFirst(), ChangesBrowserNode.UNVERSIONED_FILES_TAG);
}
}
if (switchedRoots != null && ! switchedRoots.isEmpty()) {
resetGrouping();
buildSwitchedRoots(switchedRoots);
}
if (!switchedFiles.isEmpty()) {
resetGrouping();
buildSwitchedFiles(switchedFiles);
}
if (ignoredFiles != null && !ignoredFiles.isEmpty()) {
resetGrouping();
buildVirtualFiles(ignoredFiles, ChangesBrowserNode.IGNORED_FILES_TAG);
}
if (lockedFolders != null && !lockedFolders.isEmpty()) {
resetGrouping();
buildVirtualFiles(lockedFolders, ChangesBrowserNode.LOCKED_FOLDERS_TAG);
}
if (logicallyLockedFiles != null && ! logicallyLockedFiles.isEmpty()) {
resetGrouping();
buildLogicallyLockedFiles(logicallyLockedFiles);
}
if (!locallyDeletedFiles.isEmpty()) {
resetGrouping();
ChangesBrowserNode locallyDeletedNode = ChangesBrowserNode.create(myProject, LOCALLY_DELETED_NODE);
model.insertNodeInto(locallyDeletedNode, root, root.getChildCount());
buildLocallyDeletedPaths(locallyDeletedFiles, locallyDeletedNode);
}
collapseDirectories(model, root);
sortNodes();
return model;
}
private void resetGrouping() {
myFoldersCache = new HashMap<String, ChangesBrowserNode>();
myPolicyInitialized = false;
}
public DefaultTreeModel buildModel(@NotNull List<? extends ChangeList> changeLists) {
final RemoteRevisionsCache revisionsCache = RemoteRevisionsCache.getInstance(myProject);
for (ChangeList list : changeLists) {
final List<Change> changes = new ArrayList<Change>(list.getChanges());
final ChangeListRemoteState listRemoteState = new ChangeListRemoteState(changes.size());
ChangesBrowserNode listNode = new ChangesBrowserChangeListNode(myProject, list, listRemoteState);
model.insertNodeInto(listNode, root, 0);
resetGrouping();
final ChangesGroupingPolicy policy = createGroupingPolicy();
int i = 0;
Collections.sort(changes, MyChangePathLengthComparator.getInstance());
for (final Change change : changes) {
final MyChangeNodeUnderChangeListDecorator decorator =
new MyChangeNodeUnderChangeListDecorator(revisionsCache, new ChangeListRemoteState.Reporter(i, listRemoteState));
insertChangeNode(change, policy, listNode, new Computable<ChangesBrowserNode>() {
@Override
public ChangesBrowserNode compute() {
return new ChangesBrowserChangeNode(myProject, change, decorator);
}
});
++ i;
}
}
return model;
}
private static class MyChangePathLengthComparator implements Comparator<Change> {
private final static MyChangePathLengthComparator ourInstance = new MyChangePathLengthComparator();
public static MyChangePathLengthComparator getInstance() {
return ourInstance;
}
@Override
public int compare(Change o1, Change o2) {
final FilePath fp1 = ChangesUtil.getFilePath(o1);
final FilePath fp2 = ChangesUtil.getFilePath(o2);
final int diff = fp1.getIOFile().getPath().length() - fp2.getIOFile().getPath().length();
return diff == 0 ? 0 : diff < 0 ? -1 : 1;
}
}
/*private void buildVirtualFiles(final Iterator<FilePath> iterator, @Nullable final Object tag) {
final ChangesBrowserNode baseNode = createNode(tag);
final ChangesGroupingPolicy policy = createGroupingPolicy();
for (; ; iterator.hasNext()) {
final FilePath path = iterator.next();
insertChangeNode(path.getVirtualFile(), policy, baseNode, defaultNodeCreator(path.getVirtualFile()));
}
} */
private void buildVirtualFiles(@NotNull List<VirtualFile> files, @Nullable final Object tag) {
final ChangesBrowserNode baseNode = createNode(tag);
insertFilesIntoNode(files, baseNode);
}
private ChangesBrowserNode createNode(Object tag) {
ChangesBrowserNode baseNode;
if (tag != null) {
baseNode = ChangesBrowserNode.create(myProject, tag);
model.insertNodeInto(baseNode, root, root.getChildCount());
}
else {
baseNode = root;
}
return baseNode;
}
private void insertFilesIntoNode(@NotNull List<VirtualFile> files, ChangesBrowserNode baseNode) {
final ChangesGroupingPolicy policy = createGroupingPolicy();
Collections.sort(files, VirtualFileHierarchicalComparator.getInstance());
for (VirtualFile file : files) {
insertChangeNode(file, policy, baseNode, defaultNodeCreator(file));
}
}
private void buildLocallyDeletedPaths(final Collection<LocallyDeletedChange> locallyDeletedChanges, final ChangesBrowserNode baseNode) {
final ChangesGroupingPolicy policy = createGroupingPolicy();
for (LocallyDeletedChange change : locallyDeletedChanges) {
// whether a folder does not matter
final StaticFilePath key = new StaticFilePath(false, change.getPresentableUrl(), change.getPath().getVirtualFile());
ChangesBrowserNode oldNode = myFoldersCache.get(key.getKey());
if (oldNode == null) {
final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, change);
final ChangesBrowserNode parent = getParentNodeFor(key, policy, baseNode);
model.insertNodeInto(node, parent, parent.getChildCount());
myFoldersCache.put(key.getKey(), node);
}
}
}
private void buildFilePaths(final Collection<FilePath> filePaths, final ChangesBrowserNode baseNode) {
final ChangesGroupingPolicy policy = createGroupingPolicy();
for (FilePath file : filePaths) {
assert file != null;
// whether a folder does not matter
final String path = file.getPath();
final StaticFilePath pathKey = ! FileUtil.isAbsolute(path) || VcsUtil.isPathRemote(path) ?
new StaticFilePath(false, path, null) :
new StaticFilePath(false, new File(file.getIOFile().getPath().replace('\\', '/')).getAbsolutePath(), file.getVirtualFile());
ChangesBrowserNode oldNode = myFoldersCache.get(pathKey.getKey());
if (oldNode == null) {
final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, file);
final ChangesBrowserNode parentNode = getParentNodeFor(pathKey, policy, baseNode);
model.insertNodeInto(node, parentNode, 0);
// we could also ask whether a file or directory, though for deleted files not a good idea
myFoldersCache.put(pathKey.getKey(), node);
}
}
}
private void buildSwitchedRoots(final Map<VirtualFile, String> switchedRoots) {
final ChangesBrowserNode rootsHeadNode = ChangesBrowserNode.create(myProject, ChangesBrowserNode.SWITCHED_ROOTS_TAG);
rootsHeadNode.setAttributes(SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
model.insertNodeInto(rootsHeadNode, root, root.getChildCount());
final List<VirtualFile> files = new ArrayList<VirtualFile>(switchedRoots.keySet());
Collections.sort(files, VirtualFileHierarchicalComparator.getInstance());
for (VirtualFile vf : files) {
final ChangesGroupingPolicy policy = createGroupingPolicy();
final ContentRevision cr = new CurrentContentRevision(new FilePathImpl(vf));
final Change change = new Change(cr, cr, FileStatus.NOT_CHANGED);
final String branchName = switchedRoots.get(vf);
insertChangeNode(vf, policy, rootsHeadNode, new Computable<ChangesBrowserNode>() {
@Override
public ChangesBrowserNode compute() {
return new ChangesBrowserChangeNode(myProject, change, new ChangeNodeDecorator() {
@Override
public void decorate(Change change, SimpleColoredComponent component, boolean isShowFlatten) {
}
@Override
public List<Pair<String, Stress>> stressPartsOfFileName(Change change, String parentPath) {
return null;
}
@Override
public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
renderer.append("[" + branchName + "] ", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
}
});
}
});
}
}
private void buildSwitchedFiles(final MultiMap<String, VirtualFile> switchedFiles) {
ChangesBrowserNode baseNode = ChangesBrowserNode.create(myProject, ChangesBrowserNode.SWITCHED_FILES_TAG);
model.insertNodeInto(baseNode, root, root.getChildCount());
for(String branchName: switchedFiles.keySet()) {
final List<VirtualFile> switchedFileList = new ArrayList<VirtualFile>(switchedFiles.get(branchName));
if (switchedFileList.size() > 0) {
ChangesBrowserNode branchNode = ChangesBrowserNode.create(myProject, branchName);
model.insertNodeInto(branchNode, baseNode, baseNode.getChildCount());
final ChangesGroupingPolicy policy = createGroupingPolicy();
Collections.sort(switchedFileList, VirtualFileHierarchicalComparator.getInstance());
for (VirtualFile file : switchedFileList) {
insertChangeNode(file, policy, branchNode, defaultNodeCreator(file));
}
}
}
}
private void buildLogicallyLockedFiles(final Map<VirtualFile, LogicalLock> logicallyLockedFiles) {
final ChangesBrowserNode baseNode = createNode(ChangesBrowserNode.LOGICALLY_LOCKED_TAG);
final ChangesGroupingPolicy policy = createGroupingPolicy();
final List<VirtualFile> keys = new ArrayList<VirtualFile>(logicallyLockedFiles.keySet());
Collections.sort(keys, VirtualFileHierarchicalComparator.getInstance());
for (final VirtualFile file : keys) {
final LogicalLock lock = logicallyLockedFiles.get(file);
final ChangesBrowserLogicallyLockedFile obj = new ChangesBrowserLogicallyLockedFile(myProject, file, lock);
insertChangeNode(obj, policy, baseNode, defaultNodeCreator(obj));
}
}
private Computable<ChangesBrowserNode> defaultNodeCreator(final Object change) {
return new Computable<ChangesBrowserNode>() {
@Override
public ChangesBrowserNode compute() {
return ChangesBrowserNode.create(myProject, change);
}
};
}
private void insertChangeNode(final Object change, final ChangesGroupingPolicy policy,
final ChangesBrowserNode listNode, final Computable<ChangesBrowserNode> nodeCreator) {
final StaticFilePath pathKey = getKey(change);
final ChangesBrowserNode node = nodeCreator.compute();
ChangesBrowserNode parentNode = getParentNodeFor(pathKey, policy, listNode);
model.insertNodeInto(node, parentNode, model.getChildCount(parentNode));
if (pathKey != null && pathKey.isDirectory()) {
myFoldersCache.put(pathKey.getKey(), node);
}
}
private void sortNodes() {
TreeUtil.sort(model, MyChangesBrowserNodeComparator.getInstance());
model.nodeStructureChanged((TreeNode)model.getRoot());
}
private static class MyChangesBrowserNodeComparator implements Comparator<ChangesBrowserNode> {
private static final MyChangesBrowserNodeComparator ourInstance = new MyChangesBrowserNodeComparator();
public static MyChangesBrowserNodeComparator getInstance() {
return ourInstance;
}
@Override
public int compare(ChangesBrowserNode node1, ChangesBrowserNode node2) {
final int classdiff = node1.getSortWeight() - node2.getSortWeight();
if (classdiff != 0) return classdiff;
if (node1 instanceof Comparable && node1.getClass().equals(node2.getClass())) {
return ((Comparable)node1).compareTo(node2);
}
return node1.compareUserObjects(node2.getUserObject());
}
}
private static void collapseDirectories(DefaultTreeModel model, ChangesBrowserNode node) {
if (node.getUserObject() instanceof FilePath && node.getChildCount() == 1) {
final ChangesBrowserNode child = (ChangesBrowserNode)node.getChildAt(0);
if (child.getUserObject() instanceof FilePath && !child.isLeaf()) {
ChangesBrowserNode parent = (ChangesBrowserNode)node.getParent();
final int idx = parent.getIndex(node);
model.removeNodeFromParent(node);
model.removeNodeFromParent(child);
model.insertNodeInto(child, parent, idx);
collapseDirectories(model, parent);
}
}
else {
final Enumeration children = node.children();
while (children.hasMoreElements()) {
ChangesBrowserNode child = (ChangesBrowserNode)children.nextElement();
collapseDirectories(model, child);
}
}
}
private static StaticFilePath getKey(final Object o) {
if (o instanceof Change) {
return staticFrom(ChangesUtil.getFilePath((Change) o));
}
else if (o instanceof VirtualFile) {
return staticFrom((VirtualFile) o);
}
else if (o instanceof FilePath) {
return staticFrom((FilePath) o);
} else if (o instanceof ChangesBrowserLogicallyLockedFile) {
return staticFrom(((ChangesBrowserLogicallyLockedFile) o).getUserObject());
} else if (o instanceof LocallyDeletedChange) {
return staticFrom(((LocallyDeletedChange) o).getPath());
}
return null;
}
private static StaticFilePath staticFrom(final FilePath fp) {
final String path = fp.getPath();
if (fp.isNonLocal() && (! FileUtil.isAbsolute(path) || VcsUtil.isPathRemote(path))) {
return new StaticFilePath(fp.isDirectory(), fp.getIOFile().getPath().replace('\\', '/'), fp.getVirtualFile());
}
return new StaticFilePath(fp.isDirectory(), new File(fp.getIOFile().getPath().replace('\\', '/')).getAbsolutePath(), fp.getVirtualFile());
}
private static StaticFilePath staticFrom(final VirtualFile vf) {
return new StaticFilePath(vf.isDirectory(), vf.getPath(), vf);
}
public static FilePath getPathForObject(Object o) {
if (o instanceof Change) {
return ChangesUtil.getFilePath((Change)o);
}
else if (o instanceof VirtualFile) {
return new FilePathImpl((VirtualFile) o);
}
else if (o instanceof FilePath) {
return (FilePath)o;
} else if (o instanceof ChangesBrowserLogicallyLockedFile) {
return new FilePathImpl(((ChangesBrowserLogicallyLockedFile) o).getUserObject());
} else if (o instanceof LocallyDeletedChange) {
return ((LocallyDeletedChange) o).getPath();
}
return null;
}
private ChangesBrowserNode getParentNodeFor(final StaticFilePath nodePath, @Nullable ChangesGroupingPolicy policy, ChangesBrowserNode rootNode) {
if (showFlatten) {
return rootNode;
}
if (policy != null) {
ChangesBrowserNode nodeFromPolicy = policy.getParentNodeFor(nodePath, rootNode);
if (nodeFromPolicy != null) {
return nodeFromPolicy;
}
}
final StaticFilePath parentPath = nodePath.getParent();
if (parentPath == null) {
return rootNode;
}
ChangesBrowserNode parentNode = myFoldersCache.get(parentPath.getKey());
if (parentNode == null) {
FilePathImpl filePath = parentPath.getVf() == null ? new FilePathImpl(new File(parentPath.getPath()), true) : new FilePathImpl(parentPath.getVf());
parentNode = ChangesBrowserNode.create(myProject, filePath);
ChangesBrowserNode grandPa = getParentNodeFor(parentPath, policy, rootNode);
model.insertNodeInto(parentNode, grandPa, grandPa.getChildCount());
myFoldersCache.put(parentPath.getKey(), parentNode);
}
return parentNode;
}
public DefaultTreeModel clearAndGetModel() {
root.removeAllChildren();
model = new DefaultTreeModel(root);
myFoldersCache = new HashMap<String, ChangesBrowserNode>();
myPolicyInitialized = false;
return model;
}
public boolean isEmpty() {
return model.getChildCount(root) == 0;
}
}