2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package com.intellij.openapi.vcs.changes.ui;
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.util.Computable;
20 import com.intellij.openapi.util.Pair;
21 import com.intellij.openapi.vcs.FilePath;
22 import com.intellij.openapi.vcs.FilePathImpl;
23 import com.intellij.openapi.vcs.FileStatus;
24 import com.intellij.openapi.vcs.VcsBundle;
25 import com.intellij.openapi.vcs.changes.*;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.ui.SimpleColoredComponent;
28 import com.intellij.ui.SimpleTextAttributes;
29 import com.intellij.util.containers.MultiMap;
30 import com.intellij.util.ui.tree.TreeUtil;
31 import org.jetbrains.annotations.NonNls;
32 import org.jetbrains.annotations.Nullable;
34 import javax.swing.tree.DefaultTreeModel;
35 import javax.swing.tree.MutableTreeNode;
36 import javax.swing.tree.TreeNode;
42 public class TreeModelBuilder {
43 @NonNls public static final String ROOT_NODE_VALUE = "root";
45 private final Project myProject;
46 private final boolean showFlatten;
47 private DefaultTreeModel model;
48 private final ChangesBrowserNode root;
49 private boolean myPolicyInitialized;
50 private ChangesGroupingPolicy myPolicy;
51 private final HashMap<String, ChangesBrowserNode> myFoldersCache;
53 public TreeModelBuilder(final Project project, final boolean showFlatten) {
55 this.showFlatten = showFlatten;
56 root = ChangesBrowserNode.create(myProject, ROOT_NODE_VALUE);
57 model = new DefaultTreeModel(root);
58 myFoldersCache = new HashMap<String, ChangesBrowserNode>();
61 public DefaultTreeModel buildModel(final List<Change> changes, final ChangeNodeDecorator changeNodeDecorator) {
62 final ChangesGroupingPolicy policy = createGroupingPolicy();
63 for (final Change change : changes) {
64 insertChangeNode(change, policy, root, new Computable<ChangesBrowserNode>() {
65 public ChangesBrowserNode compute() {
66 return new ChangesBrowserChangeNode(myProject, change, changeNodeDecorator);
71 collapseDirectories(model, root);
78 private ChangesGroupingPolicy createGroupingPolicy() {
79 if (! myPolicyInitialized) {
80 myPolicyInitialized = true;
81 final ChangesGroupingPolicyFactory factory = ChangesGroupingPolicyFactory.getInstance(myProject);
82 if (factory != null) {
83 myPolicy = factory.createGroupingPolicy(model);
89 public DefaultTreeModel buildModelFromFiles(final List<VirtualFile> files) {
90 buildVirtualFiles(files, null);
91 collapseDirectories(model, root);
96 public DefaultTreeModel buildModelFromFilePaths(final Collection<FilePath> files) {
97 buildFilePaths(files, root);
98 collapseDirectories(model, root);
103 private static class MyChangeNodeUnderChangeListDecorator extends RemoteStatusChangeNodeDecorator {
104 private final ChangeListRemoteState.Reporter myReporter;
106 private MyChangeNodeUnderChangeListDecorator(final RemoteRevisionsCache remoteRevisionsCache, final ChangeListRemoteState.Reporter reporter) {
107 super(remoteRevisionsCache);
108 myReporter = reporter;
112 protected void reportState(boolean state) {
113 myReporter.report(state);
116 public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
120 public DefaultTreeModel buildModel(final List<? extends ChangeList> changeLists,
121 final List<VirtualFile> unversionedFiles,
122 final List<LocallyDeletedChange> locallyDeletedFiles,
123 final List<VirtualFile> modifiedWithoutEditing,
124 final MultiMap<String, VirtualFile> switchedFiles,
125 @Nullable Map<VirtualFile, String> switchedRoots,
126 @Nullable final List<VirtualFile> ignoredFiles, @Nullable final List<VirtualFile> lockedFolders,
127 @Nullable final Map<VirtualFile, LogicalLock> logicallyLockedFiles) {
129 buildModel(changeLists);
131 if (!modifiedWithoutEditing.isEmpty()) {
133 buildVirtualFiles(modifiedWithoutEditing, ChangesBrowserNode.MODIFIED_WITHOUT_EDITING_TAG);
135 if (!unversionedFiles.isEmpty()) {
137 buildVirtualFiles(unversionedFiles, ChangesBrowserNode.UNVERSIONED_FILES_TAG);
139 if (switchedRoots != null && (! switchedRoots.isEmpty())) {
141 buildSwitchedRoots(switchedRoots);
143 if (!switchedFiles.isEmpty()) {
145 buildSwitchedFiles(switchedFiles);
147 if (ignoredFiles != null && !ignoredFiles.isEmpty()) {
149 buildVirtualFiles(ignoredFiles, ChangesBrowserNode.IGNORED_FILES_TAG);
151 if (lockedFolders != null && !lockedFolders.isEmpty()) {
153 buildVirtualFiles(lockedFolders, ChangesBrowserNode.LOCKED_FOLDERS_TAG);
155 if (logicallyLockedFiles != null && (! logicallyLockedFiles.isEmpty())) {
157 buildLogicallyLockedFiles(logicallyLockedFiles);
160 if (!locallyDeletedFiles.isEmpty()) {
162 ChangesBrowserNode locallyDeletedNode = ChangesBrowserNode.create(myProject, VcsBundle.message("changes.nodetitle.locally.deleted.files"));
163 model.insertNodeInto(locallyDeletedNode, root, root.getChildCount());
164 buildLocallyDeletedPaths(locallyDeletedFiles, locallyDeletedNode);
167 collapseDirectories(model, root);
173 private void resetGrouping() {
174 myFoldersCache.clear();
175 myPolicyInitialized = false;
178 public DefaultTreeModel buildModel(List<? extends ChangeList> changeLists) {
179 final RemoteRevisionsCache revisionsCache = RemoteRevisionsCache.getInstance(myProject);
180 for (ChangeList list : changeLists) {
181 final Collection<Change> changes = list.getChanges();
182 final ChangeListRemoteState listRemoteState = new ChangeListRemoteState(changes.size());
183 ChangesBrowserNode listNode = new ChangesBrowserChangeListNode(myProject, list, listRemoteState);
184 model.insertNodeInto(listNode, root, 0);
186 final ChangesGroupingPolicy policy = createGroupingPolicy();
188 for (final Change change : changes) {
189 final MyChangeNodeUnderChangeListDecorator decorator =
190 new MyChangeNodeUnderChangeListDecorator(revisionsCache, new ChangeListRemoteState.Reporter(i, listRemoteState));
191 insertChangeNode(change, policy, listNode, new Computable<ChangesBrowserNode>() {
192 public ChangesBrowserNode compute() {
193 return new ChangesBrowserChangeNode(myProject, change, decorator);
202 private void buildVirtualFiles(final Iterator<FilePath> iterator, @Nullable final Object tag) {
203 final ChangesBrowserNode baseNode = createNode(tag);
204 final ChangesGroupingPolicy policy = createGroupingPolicy();
205 for (; ; iterator.hasNext()) {
206 final FilePath path = iterator.next();
207 insertChangeNode(path.getVirtualFile(), policy, baseNode, defaultNodeCreator(path.getVirtualFile()));
211 private void buildVirtualFiles(final List<VirtualFile> files, @Nullable final Object tag) {
212 final ChangesBrowserNode baseNode = createNode(tag);
213 insertFilesIntoNode(files, baseNode);
216 private ChangesBrowserNode createNode(Object tag) {
217 ChangesBrowserNode baseNode;
219 baseNode = ChangesBrowserNode.create(myProject, tag);
220 model.insertNodeInto(baseNode, root, root.getChildCount());
228 private void insertFilesIntoNode(List<VirtualFile> files, ChangesBrowserNode baseNode) {
229 final ChangesGroupingPolicy policy = createGroupingPolicy();
230 for (VirtualFile file : files) {
231 insertChangeNode(file, policy, baseNode, defaultNodeCreator(file));
235 private void buildLocallyDeletedPaths(final Collection<LocallyDeletedChange> locallyDeletedChanges, final ChangesBrowserNode baseNode) {
236 final ChangesGroupingPolicy policy = createGroupingPolicy();
237 for (LocallyDeletedChange change : locallyDeletedChanges) {
238 ChangesBrowserNode oldNode = myFoldersCache.get(change.getPresentableUrl());
239 if (oldNode == null) {
240 final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, change);
241 final ChangesBrowserNode parent = getParentNodeFor(node, policy, baseNode);
242 model.insertNodeInto(node, parent, parent.getChildCount());
243 myFoldersCache.put(change.getPresentableUrl(), node);
248 public void buildFilePaths(final Collection<FilePath> filePaths, final ChangesBrowserNode baseNode) {
249 final ChangesGroupingPolicy policy = createGroupingPolicy();
250 for (FilePath file : filePaths) {
252 ChangesBrowserNode oldNode = myFoldersCache.get(file.getIOFile().getAbsolutePath());
253 if (oldNode == null) {
254 final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, file);
255 final ChangesBrowserNode parentNode = getParentNodeFor(node, policy, baseNode);
256 model.insertNodeInto(node, parentNode, 0);
257 myFoldersCache.put(file.getIOFile().getAbsolutePath(), node);
262 private void buildSwitchedRoots(final Map<VirtualFile, String> switchedRoots) {
263 final ChangesBrowserNode rootsHeadNode = ChangesBrowserNode.create(myProject, ChangesBrowserNode.SWITCHED_ROOTS_TAG);
264 rootsHeadNode.setAttributes(SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
265 model.insertNodeInto(rootsHeadNode, root, root.getChildCount());
267 for (VirtualFile vf : switchedRoots.keySet()) {
268 final ChangesGroupingPolicy policy = createGroupingPolicy();
269 final ContentRevision cr = new CurrentContentRevision(new FilePathImpl(vf));
270 final Change change = new Change(cr, cr, FileStatus.NOT_CHANGED);
271 final String branchName = switchedRoots.get(vf);
272 insertChangeNode(vf, policy, rootsHeadNode, new Computable<ChangesBrowserNode>() {
273 public ChangesBrowserNode compute() {
274 return new ChangesBrowserChangeNode(myProject, change, new ChangeNodeDecorator() {
275 public void decorate(Change change, SimpleColoredComponent component, boolean isShowFlatten) {
277 public List<Pair<String, Stress>> stressPartsOfFileName(Change change, String parentPath) {
280 public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
281 renderer.append("[" + branchName + "] ", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
289 private void buildSwitchedFiles(final MultiMap<String, VirtualFile> switchedFiles) {
290 ChangesBrowserNode baseNode = ChangesBrowserNode.create(myProject, ChangesBrowserNode.SWITCHED_FILES_TAG);
291 model.insertNodeInto(baseNode, root, root.getChildCount());
292 for(String branchName: switchedFiles.keySet()) {
293 final Collection<VirtualFile> switchedFileList = switchedFiles.get(branchName);
294 if (switchedFileList.size() > 0) {
295 ChangesBrowserNode branchNode = ChangesBrowserNode.create(myProject, branchName);
296 model.insertNodeInto(branchNode, baseNode, baseNode.getChildCount());
298 final ChangesGroupingPolicy policy = createGroupingPolicy();
299 for (VirtualFile file : switchedFileList) {
300 insertChangeNode(file, policy, branchNode, defaultNodeCreator(file));
306 private void buildLogicallyLockedFiles(final Map<VirtualFile, LogicalLock> logicallyLockedFiles) {
307 final ChangesBrowserNode baseNode = createNode(ChangesBrowserNode.LOGICALLY_LOCKED_TAG);
309 final ChangesGroupingPolicy policy = createGroupingPolicy();
310 for (Map.Entry<VirtualFile, LogicalLock> entry : logicallyLockedFiles.entrySet()) {
311 final VirtualFile file = entry.getKey();
312 final LogicalLock lock = entry.getValue();
313 final ChangesBrowserLogicallyLockedFile obj = new ChangesBrowserLogicallyLockedFile(myProject, file, lock);
314 insertChangeNode(obj, policy, baseNode,
315 defaultNodeCreator(obj));
319 private Computable<ChangesBrowserNode> defaultNodeCreator(final Object change) {
320 return new Computable<ChangesBrowserNode>() {
321 public ChangesBrowserNode compute() {
322 return ChangesBrowserNode.create(myProject, change);
327 private void insertChangeNode(final Object change, final ChangesGroupingPolicy policy,
328 final ChangesBrowserNode listNode, final Computable<ChangesBrowserNode> nodeCreator) {
329 final FilePath nodePath = getPathForObject(change);
330 ChangesBrowserNode oldNode = (nodePath == null) ? null : myFoldersCache.get(nodePath.getIOFile().getAbsolutePath());
331 ChangesBrowserNode node = nodeCreator.compute();
332 if (oldNode != null) {
333 for(int i=oldNode.getChildCount()-1; i >= 0; i--) {
334 MutableTreeNode child = (MutableTreeNode) model.getChild(oldNode, i);
335 model.removeNodeFromParent(child);
336 model.insertNodeInto(child, node, model.getChildCount(node));
338 final MutableTreeNode parent = (MutableTreeNode)oldNode.getParent();
339 int index = model.getIndexOfChild(parent, oldNode);
340 model.removeNodeFromParent(oldNode);
341 model.insertNodeInto(node, parent, index);
342 myFoldersCache.put(nodePath.getIOFile().getAbsolutePath(), node);
345 ChangesBrowserNode parentNode = getParentNodeFor(node, policy, listNode);
346 model.insertNodeInto(node, parentNode, model.getChildCount(parentNode));
348 if (nodePath != null) {
349 myFoldersCache.put(nodePath.getIOFile().getAbsolutePath(), node);
354 private void sortNodes() {
355 TreeUtil.sort(model, new Comparator() {
356 public int compare(final Object n1, final Object n2) {
357 final ChangesBrowserNode node1 = (ChangesBrowserNode)n1;
358 final ChangesBrowserNode node2 = (ChangesBrowserNode)n2;
360 final int classdiff = node1.getSortWeight() - node2.getSortWeight();
361 if (classdiff != 0) return classdiff;
363 return node1.compareUserObjects(node2.getUserObject());
367 model.nodeStructureChanged((TreeNode)model.getRoot());
370 private static void collapseDirectories(DefaultTreeModel model, ChangesBrowserNode node) {
371 if (node.getUserObject() instanceof FilePath && node.getChildCount() == 1) {
372 final ChangesBrowserNode child = (ChangesBrowserNode)node.getChildAt(0);
373 if (child.getUserObject() instanceof FilePath && !child.isLeaf()) {
374 ChangesBrowserNode parent = (ChangesBrowserNode)node.getParent();
375 final int idx = parent.getIndex(node);
376 model.removeNodeFromParent(node);
377 model.removeNodeFromParent(child);
378 model.insertNodeInto(child, parent, idx);
379 collapseDirectories(model, parent);
383 final Enumeration children = node.children();
384 while (children.hasMoreElements()) {
385 ChangesBrowserNode child = (ChangesBrowserNode)children.nextElement();
386 collapseDirectories(model, child);
391 public static FilePath getPathForObject(Object o) {
392 if (o instanceof Change) {
393 return ChangesUtil.getFilePath((Change)o);
395 else if (o instanceof VirtualFile) {
396 return new FilePathImpl((VirtualFile) o);
398 else if (o instanceof FilePath) {
400 } else if (o instanceof ChangesBrowserLogicallyLockedFile) {
401 return new FilePathImpl(((ChangesBrowserLogicallyLockedFile) o).getUserObject());
402 } else if (o instanceof LocallyDeletedChange) {
403 return ((LocallyDeletedChange) o).getPath();
409 private ChangesBrowserNode getParentNodeFor(ChangesBrowserNode node, @Nullable ChangesGroupingPolicy policy, ChangesBrowserNode rootNode) {
414 final FilePath path = getPathForObject(node.getUserObject());
416 if (policy != null) {
417 ChangesBrowserNode nodeFromPolicy = policy.getParentNodeFor(node, rootNode);
418 if (nodeFromPolicy != null) {
419 return nodeFromPolicy;
423 FilePath parentPath = path.getParentPath();
424 if (parentPath == null) {
428 final String parentKey = parentPath.getIOFile().getAbsolutePath();
429 ChangesBrowserNode parentNode = myFoldersCache.get(parentKey);
430 if (parentNode == null) {
431 parentNode = ChangesBrowserNode.create(myProject, parentPath);
432 ChangesBrowserNode grandPa = getParentNodeFor(parentNode, policy, rootNode);
433 model.insertNodeInto(parentNode, grandPa, grandPa.getChildCount());
434 myFoldersCache.put(parentKey, parentNode);
440 public DefaultTreeModel clearAndGetModel() {
441 root.removeAllChildren();
442 model = new DefaultTreeModel(root);
443 myFoldersCache.clear();
444 myPolicyInitialized = false;
448 public boolean isEmpty() {
449 return model.getChildCount(root) == 0;