4ff479b52113d82cba2f8d610253b8b33378c28a
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / ui / TreeModelBuilder.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package com.intellij.openapi.vcs.changes.ui;
17
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;
33
34 import javax.swing.tree.DefaultTreeModel;
35 import javax.swing.tree.MutableTreeNode;
36 import javax.swing.tree.TreeNode;
37 import java.util.*;
38
39 /**
40  * @author max
41  */
42 public class TreeModelBuilder {
43   @NonNls public static final String ROOT_NODE_VALUE = "root";
44
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;
52
53   public TreeModelBuilder(final Project project, final boolean showFlatten) {
54     myProject = project;
55     this.showFlatten = showFlatten;
56     root = ChangesBrowserNode.create(myProject, ROOT_NODE_VALUE);
57     model = new DefaultTreeModel(root);
58     myFoldersCache = new HashMap<String, ChangesBrowserNode>();
59   }
60
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);
67         }
68       });
69     }
70
71     collapseDirectories(model, root);
72     sortNodes();
73
74     return model;
75   }
76
77   @Nullable
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);
84       }
85     }
86     return myPolicy;
87   }
88
89   public DefaultTreeModel buildModelFromFiles(final List<VirtualFile> files) {
90     buildVirtualFiles(files, null);
91     collapseDirectories(model, root);
92     sortNodes();
93     return model;
94   }
95
96   public DefaultTreeModel buildModelFromFilePaths(final Collection<FilePath> files) {
97     buildFilePaths(files, root);
98     collapseDirectories(model, root);
99     sortNodes();
100     return model;
101   }
102
103   private static class MyChangeNodeUnderChangeListDecorator extends RemoteStatusChangeNodeDecorator {
104     private final ChangeListRemoteState.Reporter myReporter;
105
106     private MyChangeNodeUnderChangeListDecorator(final RemoteRevisionsCache remoteRevisionsCache, final ChangeListRemoteState.Reporter reporter) {
107       super(remoteRevisionsCache);
108       myReporter = reporter;
109     }
110
111     @Override
112     protected void reportState(boolean state) {
113       myReporter.report(state);
114     }
115
116     public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
117     }
118   }
119
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) {
128     resetGrouping();
129     buildModel(changeLists);
130
131     if (!modifiedWithoutEditing.isEmpty()) {
132       resetGrouping();
133       buildVirtualFiles(modifiedWithoutEditing, ChangesBrowserNode.MODIFIED_WITHOUT_EDITING_TAG);
134     }
135     if (!unversionedFiles.isEmpty()) {
136       resetGrouping();
137       buildVirtualFiles(unversionedFiles, ChangesBrowserNode.UNVERSIONED_FILES_TAG);
138     }
139     if (switchedRoots != null && (! switchedRoots.isEmpty())) {
140       resetGrouping();
141       buildSwitchedRoots(switchedRoots);
142     }
143     if (!switchedFiles.isEmpty()) {
144       resetGrouping();
145       buildSwitchedFiles(switchedFiles);
146     }
147     if (ignoredFiles != null && !ignoredFiles.isEmpty()) {
148       resetGrouping();
149       buildVirtualFiles(ignoredFiles, ChangesBrowserNode.IGNORED_FILES_TAG);
150     }
151     if (lockedFolders != null && !lockedFolders.isEmpty()) {
152       resetGrouping();
153       buildVirtualFiles(lockedFolders, ChangesBrowserNode.LOCKED_FOLDERS_TAG);
154     }
155     if (logicallyLockedFiles != null && (! logicallyLockedFiles.isEmpty())) {
156       resetGrouping();
157       buildLogicallyLockedFiles(logicallyLockedFiles);
158     }
159
160     if (!locallyDeletedFiles.isEmpty()) {
161       resetGrouping();
162       ChangesBrowserNode locallyDeletedNode = ChangesBrowserNode.create(myProject, VcsBundle.message("changes.nodetitle.locally.deleted.files"));
163       model.insertNodeInto(locallyDeletedNode, root, root.getChildCount());
164       buildLocallyDeletedPaths(locallyDeletedFiles, locallyDeletedNode);
165     }
166
167     collapseDirectories(model, root);
168     sortNodes();
169
170     return model;
171   }
172
173   private void resetGrouping() {
174     myFoldersCache.clear();
175     myPolicyInitialized = false;
176   }
177
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);
185       resetGrouping();
186       final ChangesGroupingPolicy policy = createGroupingPolicy();
187       int i = 0;
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);
194           }
195         });
196         ++ i;
197       }
198     }
199     return model;
200   }
201
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()));
208     }
209   }
210
211   private void buildVirtualFiles(final List<VirtualFile> files, @Nullable final Object tag) {
212     final ChangesBrowserNode baseNode = createNode(tag);
213     insertFilesIntoNode(files, baseNode);
214   }
215
216   private ChangesBrowserNode createNode(Object tag) {
217     ChangesBrowserNode baseNode;
218     if (tag != null) {
219       baseNode = ChangesBrowserNode.create(myProject, tag);
220       model.insertNodeInto(baseNode, root, root.getChildCount());
221     }
222     else {
223       baseNode = root;
224     }
225     return baseNode;
226   }
227
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));
232     }
233   }
234
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);
244       }
245     }
246   }
247
248   public void buildFilePaths(final Collection<FilePath> filePaths, final ChangesBrowserNode baseNode) {
249     final ChangesGroupingPolicy policy = createGroupingPolicy();
250     for (FilePath file : filePaths) {
251       assert file != null;
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);
258       }
259     }
260   }
261
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());
266
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) {
276             }
277             public List<Pair<String, Stress>> stressPartsOfFileName(Change change, String parentPath) {
278               return null;
279             }
280             public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
281               renderer.append("[" + branchName + "] ", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
282             }
283           });
284         }
285       });
286     }
287   }
288
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());
297
298         final ChangesGroupingPolicy policy = createGroupingPolicy();
299         for (VirtualFile file : switchedFileList) {
300           insertChangeNode(file, policy, branchNode, defaultNodeCreator(file));
301         }
302       }
303     }
304   }
305
306   private void buildLogicallyLockedFiles(final Map<VirtualFile, LogicalLock> logicallyLockedFiles) {
307     final ChangesBrowserNode baseNode = createNode(ChangesBrowserNode.LOGICALLY_LOCKED_TAG);
308
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));
316     }
317   }
318   
319   private Computable<ChangesBrowserNode> defaultNodeCreator(final Object change) {
320     return new Computable<ChangesBrowserNode>() {
321       public ChangesBrowserNode compute() {
322         return ChangesBrowserNode.create(myProject, change);
323       }
324     };
325   }
326
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));
337       }
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);
343     }
344     else {
345       ChangesBrowserNode parentNode = getParentNodeFor(node, policy, listNode);
346       model.insertNodeInto(node, parentNode, model.getChildCount(parentNode));
347       // ?
348       if (nodePath != null) {
349         myFoldersCache.put(nodePath.getIOFile().getAbsolutePath(), node);
350       }
351     }
352   }
353
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;
359
360         final int classdiff = node1.getSortWeight() - node2.getSortWeight();
361         if (classdiff != 0) return classdiff;
362
363         return node1.compareUserObjects(node2.getUserObject());
364       }
365    });
366
367     model.nodeStructureChanged((TreeNode)model.getRoot());
368   }
369
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);
380       }
381     }
382     else {
383       final Enumeration children = node.children();
384       while (children.hasMoreElements()) {
385         ChangesBrowserNode child = (ChangesBrowserNode)children.nextElement();
386         collapseDirectories(model, child);
387       }
388     }
389   }
390
391   public static FilePath getPathForObject(Object o) {
392     if (o instanceof Change) {
393       return ChangesUtil.getFilePath((Change)o);
394     }
395     else if (o instanceof VirtualFile) {
396       return new FilePathImpl((VirtualFile) o);
397     }
398     else if (o instanceof FilePath) {
399       return (FilePath)o;
400     } else if (o instanceof ChangesBrowserLogicallyLockedFile) {
401       return new FilePathImpl(((ChangesBrowserLogicallyLockedFile) o).getUserObject());
402     } else if (o instanceof LocallyDeletedChange) {
403       return ((LocallyDeletedChange) o).getPath();
404     }
405
406     return null;
407   }
408
409   private ChangesBrowserNode getParentNodeFor(ChangesBrowserNode node, @Nullable ChangesGroupingPolicy policy, ChangesBrowserNode rootNode) {
410     if (showFlatten) {
411       return rootNode;
412     }
413
414     final FilePath path = getPathForObject(node.getUserObject());
415
416     if (policy != null) {
417       ChangesBrowserNode nodeFromPolicy = policy.getParentNodeFor(node, rootNode);
418       if (nodeFromPolicy != null) {
419         return nodeFromPolicy;
420       }
421     }
422
423     FilePath parentPath = path.getParentPath();
424     if (parentPath == null) {
425       return rootNode;
426     }
427
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);
435     }
436
437     return parentNode;
438   }
439
440   public DefaultTreeModel clearAndGetModel() {
441     root.removeAllChildren();
442     model = new DefaultTreeModel(root);
443     myFoldersCache.clear();
444     myPolicyInitialized = false;
445     return model;
446   }
447
448   public boolean isEmpty() {
449     return model.getChildCount(root) == 0;
450   }
451 }