/* * Copyright 2000-2015 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.Comparing; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.FileStatus; 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.ContainerUtil; import com.intellij.util.containers.Convertor; import com.intellij.util.containers.FactoryMap; 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.*; @SuppressWarnings("UnusedReturnValue") public class TreeModelBuilder { @NonNls private static final String ROOT_NODE_VALUE = "root"; private static final int UNVERSIONED_MAX_SIZE = 50; @NotNull protected final Project myProject; protected final boolean myShowFlatten; @NotNull protected final DefaultTreeModel myModel; @NotNull protected final ChangesBrowserNode myRoot; @NotNull private final Map<ChangesBrowserNode, ChangesGroupingPolicy> myGroupingPoliciesCache; @NotNull private final Map<ChangesBrowserNode, Map<String, ChangesBrowserNode>> myFoldersCache; @SuppressWarnings("unchecked") private static final Comparator<ChangesBrowserNode> BROWSER_NODE_COMPARATOR = (node1, node2) -> { int sortWeightDiff = Comparing.compare(node1.getSortWeight(), node2.getSortWeight()); if (sortWeightDiff != 0) return sortWeightDiff; if (node1 instanceof Comparable && node1.getClass().equals(node2.getClass())) { return ((Comparable)node1).compareTo(node2); } return node1.compareUserObjects(node2.getUserObject()); }; protected final static Comparator<Change> PATH_LENGTH_COMPARATOR = (o1, o2) -> { FilePath fp1 = ChangesUtil.getFilePath(o1); FilePath fp2 = ChangesUtil.getFilePath(o2); return Comparing.compare(fp1.getPath().length(), fp2.getPath().length()); }; public TreeModelBuilder(@NotNull Project project, boolean showFlatten) { myProject = project; myShowFlatten = showFlatten; myRoot = ChangesBrowserNode.create(myProject, ROOT_NODE_VALUE); myModel = new DefaultTreeModel(myRoot); myGroupingPoliciesCache = new MyGroupingPolicyFactoryMap(myProject, myModel); myFoldersCache = new HashMap<>(); } @NotNull public static DefaultTreeModel buildEmpty(@NotNull Project project) { return new DefaultTreeModel(ChangesBrowserNode.create(project, ROOT_NODE_VALUE)); } @NotNull public static DefaultTreeModel buildFromChanges(@NotNull Project project, boolean showFlatten, @NotNull Collection<? extends Change> changes, @Nullable ChangeNodeDecorator changeNodeDecorator) { return new TreeModelBuilder(project, showFlatten) .setChanges(changes, changeNodeDecorator) .build(); } @NotNull public static DefaultTreeModel buildFromFilePaths(@NotNull Project project, boolean showFlatten, @NotNull Collection<FilePath> filePaths) { return new TreeModelBuilder(project, showFlatten) .setFilePaths(filePaths) .build(); } @NotNull public static DefaultTreeModel buildFromChangeLists(@NotNull Project project, boolean showFlatten, @NotNull Collection<? extends ChangeList> changeLists) { return new TreeModelBuilder(project, showFlatten) .setChangeLists(changeLists) .build(); } @NotNull public static DefaultTreeModel buildFromVirtualFiles(@NotNull Project project, boolean showFlatten, @NotNull Collection<VirtualFile> virtualFiles) { return new TreeModelBuilder(project, showFlatten) .setVirtualFiles(virtualFiles, null) .build(); } @NotNull public TreeModelBuilder setChanges(@NotNull Collection<? extends Change> changes, @Nullable ChangeNodeDecorator changeNodeDecorator) { List<? extends Change> sortedChanges = ContainerUtil.sorted(changes, PATH_LENGTH_COMPARATOR); for (Change change : sortedChanges) { insertChangeNode(change, myRoot, createChangeNode(change, changeNodeDecorator)); } return this; } @NotNull public TreeModelBuilder setUnversioned(@Nullable List<VirtualFile> unversionedFiles) { if (ContainerUtil.isEmpty(unversionedFiles)) return this; int dirsCount = ContainerUtil.count(unversionedFiles, it -> it.isDirectory()); int filesCount = unversionedFiles.size() - dirsCount; boolean manyFiles = unversionedFiles.size() > UNVERSIONED_MAX_SIZE; ChangesBrowserUnversionedFilesNode node = new ChangesBrowserUnversionedFilesNode(myProject, filesCount, dirsCount, manyFiles); return insertSpecificNodeToModel(unversionedFiles, node); } @NotNull public TreeModelBuilder setIgnored(@Nullable List<VirtualFile> ignoredFiles, boolean updatingMode) { if (ContainerUtil.isEmpty(ignoredFiles)) return this; int dirsCount = ContainerUtil.count(ignoredFiles, it -> it.isDirectory()); int filesCount = ignoredFiles.size() - dirsCount; boolean manyFiles = ignoredFiles.size() > UNVERSIONED_MAX_SIZE; ChangesBrowserIgnoredFilesNode node = new ChangesBrowserIgnoredFilesNode(myProject, filesCount, dirsCount, manyFiles, updatingMode); return insertSpecificNodeToModel(ignoredFiles, node); } @NotNull private TreeModelBuilder insertSpecificNodeToModel(@NotNull List<VirtualFile> specificFiles, @NotNull ChangesBrowserSpecificFilesNode node) { myModel.insertNodeInto(node, myRoot, myRoot.getChildCount()); if (!node.isManyFiles()) { insertFilesIntoNode(specificFiles, node); } return this; } @NotNull public TreeModelBuilder setChangeLists(@NotNull Collection<? extends ChangeList> changeLists) { final RemoteRevisionsCache revisionsCache = RemoteRevisionsCache.getInstance(myProject); for (ChangeList list : changeLists) { List<Change> changes = ContainerUtil.sorted(list.getChanges(), PATH_LENGTH_COMPARATOR); ChangeListRemoteState listRemoteState = new ChangeListRemoteState(changes.size()); ChangesBrowserChangeListNode listNode = new ChangesBrowserChangeListNode(myProject, list, listRemoteState); myModel.insertNodeInto(listNode, myRoot, 0); for (int i = 0; i < changes.size(); i++) { Change change = changes.get(i); RemoteStatusChangeNodeDecorator decorator = new RemoteStatusChangeNodeDecorator(revisionsCache, listRemoteState, i); insertChangeNode(change, listNode, createChangeNode(change, decorator)); } } return this; } protected ChangesBrowserNode createChangeNode(Change change, ChangeNodeDecorator decorator) { return new ChangesBrowserChangeNode(myProject, change, decorator); } @NotNull public TreeModelBuilder setLockedFolders(@Nullable List<VirtualFile> lockedFolders) { return setVirtualFiles(lockedFolders, ChangesBrowserNode.LOCKED_FOLDERS_TAG); } @NotNull public TreeModelBuilder setModifiedWithoutEditing(@NotNull List<VirtualFile> modifiedWithoutEditing) { return setVirtualFiles(modifiedWithoutEditing, ChangesBrowserNode.MODIFIED_WITHOUT_EDITING_TAG); } @NotNull private TreeModelBuilder setVirtualFiles(@Nullable Collection<VirtualFile> files, @Nullable Object tag) { if (ContainerUtil.isEmpty(files)) return this; insertFilesIntoNode(files, createTagNode(tag)); return this; } @NotNull private ChangesBrowserNode createTagNode(@Nullable Object tag) { if (tag == null) return myRoot; ChangesBrowserNode subtreeRoot = ChangesBrowserNode.create(myProject, tag); myModel.insertNodeInto(subtreeRoot, myRoot, myRoot.getChildCount()); return subtreeRoot; } private void insertFilesIntoNode(@NotNull Collection<VirtualFile> files, @NotNull ChangesBrowserNode subtreeRoot) { List<VirtualFile> sortedFiles = ContainerUtil.sorted(files, VirtualFileHierarchicalComparator.getInstance()); for (VirtualFile file : sortedFiles) { insertChangeNode(file, subtreeRoot, ChangesBrowserNode.create(myProject, file)); } } @NotNull public TreeModelBuilder setLocallyDeletedPaths(@Nullable Collection<LocallyDeletedChange> locallyDeletedChanges) { if (ContainerUtil.isEmpty(locallyDeletedChanges)) return this; ChangesBrowserNode subtreeRoot = createTagNode(ChangesBrowserNode.LOCALLY_DELETED_NODE_TAG); for (LocallyDeletedChange change : locallyDeletedChanges) { // whether a folder does not matter final StaticFilePath key = new StaticFilePath(false, change.getPresentableUrl(), change.getPath().getVirtualFile()); ChangesBrowserNode oldNode = getFolderCache(subtreeRoot).get(key.getKey()); if (oldNode == null) { ChangesBrowserNode node = ChangesBrowserNode.create(change); ChangesBrowserNode parent = getParentNodeFor(key, subtreeRoot); myModel.insertNodeInto(node, parent, parent.getChildCount()); getFolderCache(subtreeRoot).put(key.getKey(), node); } } return this; } @NotNull public TreeModelBuilder setFilePaths(@NotNull Collection<FilePath> filePaths) { return setFilePaths(filePaths, myRoot); } @NotNull private TreeModelBuilder setFilePaths(@NotNull Collection<FilePath> filePaths, @NotNull ChangesBrowserNode subtreeRoot) { 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 = getFolderCache(subtreeRoot).get(pathKey.getKey()); if (oldNode == null) { final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, file); final ChangesBrowserNode parentNode = getParentNodeFor(pathKey, subtreeRoot); myModel.insertNodeInto(node, parentNode, 0); // we could also ask whether a file or directory, though for deleted files not a good idea getFolderCache(subtreeRoot).put(pathKey.getKey(), node); } } return this; } @NotNull public TreeModelBuilder setSwitchedRoots(@Nullable Map<VirtualFile, String> switchedRoots) { if (ContainerUtil.isEmpty(switchedRoots)) return this; final ChangesBrowserNode rootsHeadNode = createTagNode(ChangesBrowserNode.SWITCHED_ROOTS_TAG); rootsHeadNode.setAttributes(SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES); List<VirtualFile> files = ContainerUtil.sorted(switchedRoots.keySet(), VirtualFileHierarchicalComparator.getInstance()); for (VirtualFile vf : files) { final ContentRevision cr = new CurrentContentRevision(VcsUtil.getFilePath(vf)); final Change change = new Change(cr, cr, FileStatus.NOT_CHANGED); final String branchName = switchedRoots.get(vf); insertChangeNode(vf, rootsHeadNode, createChangeNode(change, new ChangeNodeDecorator() { @Override public void decorate(Change change1, SimpleColoredComponent component, boolean isShowFlatten) { } @Override public void preDecorate(Change change1, ChangesBrowserNodeRenderer renderer, boolean showFlatten) { renderer.append("[" + branchName + "] ", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES); } })); } return this; } @NotNull public TreeModelBuilder setSwitchedFiles(@NotNull MultiMap<String, VirtualFile> switchedFiles) { if (switchedFiles.isEmpty()) return this; ChangesBrowserNode subtreeRoot = createTagNode(ChangesBrowserNode.SWITCHED_FILES_TAG); for(String branchName: switchedFiles.keySet()) { List<VirtualFile> switchedFileList = ContainerUtil.sorted(switchedFiles.get(branchName), VirtualFileHierarchicalComparator.getInstance()); if (switchedFileList.size() > 0) { ChangesBrowserNode branchNode = ChangesBrowserNode.create(myProject, branchName); myModel.insertNodeInto(branchNode, subtreeRoot, subtreeRoot.getChildCount()); for (VirtualFile file : switchedFileList) { insertChangeNode(file, branchNode, ChangesBrowserNode.create(myProject, file)); } } } return this; } @NotNull public TreeModelBuilder setLogicallyLockedFiles(@Nullable Map<VirtualFile, LogicalLock> logicallyLockedFiles) { if (ContainerUtil.isEmpty(logicallyLockedFiles)) return this; final ChangesBrowserNode subtreeRoot = createTagNode(ChangesBrowserNode.LOGICALLY_LOCKED_TAG); List<VirtualFile> keys = ContainerUtil.sorted(logicallyLockedFiles.keySet(), VirtualFileHierarchicalComparator.getInstance()); for (VirtualFile file : keys) { final LogicalLock lock = logicallyLockedFiles.get(file); final ChangesBrowserLogicallyLockedFile obj = new ChangesBrowserLogicallyLockedFile(myProject, file, lock); insertChangeNode(obj, subtreeRoot, ChangesBrowserNode.create(myProject, obj)); } return this; } protected void insertChangeNode(@NotNull Object change, @NotNull ChangesBrowserNode subtreeRoot, @NotNull ChangesBrowserNode node) { insertChangeNode(change, subtreeRoot, node, this::createPathNode); } protected void insertChangeNode(@NotNull Object change, @NotNull ChangesBrowserNode subtreeRoot, @NotNull ChangesBrowserNode node, @NotNull Convertor<StaticFilePath, ChangesBrowserNode> nodeBuilder) { final StaticFilePath pathKey = getKey(change); ChangesBrowserNode parentNode = getParentNodeFor(pathKey, subtreeRoot, nodeBuilder); myModel.insertNodeInto(node, parentNode, myModel.getChildCount(parentNode)); if (pathKey.isDirectory()) { getFolderCache(subtreeRoot).put(pathKey.getKey(), node); } } @NotNull public DefaultTreeModel build() { collapseDirectories(myModel, myRoot); sortNodes(); return myModel; } private void sortNodes() { TreeUtil.sort(myModel, BROWSER_NODE_COMPARATOR); myModel.nodeStructureChanged((TreeNode)myModel.getRoot()); } private static void collapseDirectories(@NotNull DefaultTreeModel model, @NotNull 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); } } } @NotNull private static StaticFilePath getKey(@NotNull 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()); } throw new IllegalArgumentException("Unknown type - " + o.getClass()); } @NotNull private static StaticFilePath staticFrom(@NotNull 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()); } @NotNull private static StaticFilePath staticFrom(@NotNull VirtualFile vf) { return new StaticFilePath(vf.isDirectory(), vf.getPath(), vf); } @NotNull public static FilePath getPathForObject(@NotNull Object o) { if (o instanceof Change) { return ChangesUtil.getFilePath((Change)o); } else if (o instanceof VirtualFile) { return VcsUtil.getFilePath((VirtualFile)o); } else if (o instanceof FilePath) { return (FilePath)o; } else if (o instanceof ChangesBrowserLogicallyLockedFile) { return VcsUtil.getFilePath(((ChangesBrowserLogicallyLockedFile)o).getUserObject()); } else if (o instanceof LocallyDeletedChange) { return ((LocallyDeletedChange)o).getPath(); } throw new IllegalArgumentException("Unknown type - " + o.getClass()); } @NotNull protected ChangesBrowserNode getParentNodeFor(@NotNull StaticFilePath nodePath, @NotNull ChangesBrowserNode subtreeRoot) { return getParentNodeFor(nodePath, subtreeRoot, this::createPathNode); } @NotNull protected ChangesBrowserNode getParentNodeFor(@NotNull StaticFilePath nodePath, @NotNull ChangesBrowserNode subtreeRoot, @NotNull Convertor<StaticFilePath, ChangesBrowserNode> nodeBuilder) { if (myShowFlatten) { return subtreeRoot; } ChangesGroupingPolicy policy = myGroupingPoliciesCache.get(subtreeRoot); if (policy != null) { ChangesBrowserNode nodeFromPolicy = policy.getParentNodeFor(nodePath, subtreeRoot); if (nodeFromPolicy != null) { return nodeFromPolicy; } } StaticFilePath parentPath = nodePath.getParent(); while (parentPath != null) { ChangesBrowserNode oldParentNode = getFolderCache(subtreeRoot).get(parentPath.getKey()); if (oldParentNode != null) return oldParentNode; ChangesBrowserNode parentNode = nodeBuilder.convert(parentPath); if (parentNode != null) { ChangesBrowserNode grandPa = getParentNodeFor(parentPath, subtreeRoot, nodeBuilder); myModel.insertNodeInto(parentNode, grandPa, grandPa.getChildCount()); getFolderCache(subtreeRoot).put(parentPath.getKey(), parentNode); return parentNode; } parentPath = parentPath.getParent(); } return subtreeRoot; } @Nullable private ChangesBrowserNode createPathNode(@NotNull StaticFilePath path) { FilePath filePath = path.getVf() == null ? VcsUtil.getFilePath(path.getPath(), true) : VcsUtil.getFilePath(path.getVf()); return ChangesBrowserNode.create(myProject, filePath); } @NotNull private Map<String, ChangesBrowserNode> getFolderCache(@NotNull ChangesBrowserNode subtreeRoot) { return myFoldersCache.computeIfAbsent(subtreeRoot, (key) -> new HashMap<>()); } public boolean isEmpty() { return myModel.getChildCount(myRoot) == 0; } @NotNull @Deprecated public DefaultTreeModel buildModel(@NotNull List<Change> changes, @Nullable ChangeNodeDecorator changeNodeDecorator) { return setChanges(changes, changeNodeDecorator).build(); } private static class MyGroupingPolicyFactoryMap extends FactoryMap<ChangesBrowserNode, ChangesGroupingPolicy> { @NotNull private final Project myProject; @NotNull private final DefaultTreeModel myModel; public MyGroupingPolicyFactoryMap(@NotNull Project project, @NotNull DefaultTreeModel model) { myProject = project; myModel = model; } @Nullable @Override protected ChangesGroupingPolicy create(ChangesBrowserNode key) { ChangesGroupingPolicyFactory factory = ChangesGroupingPolicyFactory.getInstance(myProject); return factory != null ? factory.createGroupingPolicy(myModel) : null; } } }