VCS: changes view, for VirtualFiles in tree, don't wrap them into Filepath just to...
[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 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 = new HashMap<String, ChangesBrowserNode>();
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       final String key = FilePathsHelper.convertPath(change.getPresentableUrl());
239       ChangesBrowserNode oldNode = myFoldersCache.get(key);
240       if (oldNode == null) {
241         final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, change);
242         final ChangesBrowserNode parent = getParentNodeFor(node, policy, baseNode);
243         model.insertNodeInto(node, parent, parent.getChildCount());
244         myFoldersCache.put(key, node);
245       }
246     }
247   }
248
249   private void buildFilePaths(final Collection<FilePath> filePaths, final ChangesBrowserNode baseNode) {
250     final ChangesGroupingPolicy policy = createGroupingPolicy();
251     for (FilePath file : filePaths) {
252       assert file != null;
253       // todo: or path from filepath?
254       final String pathKey = FilePathsHelper.convertPath(file.getIOFile().getAbsolutePath());
255       ChangesBrowserNode oldNode = myFoldersCache.get(pathKey);
256       if (oldNode == null) {
257         final ChangesBrowserNode node = ChangesBrowserNode.create(myProject, file);
258         final ChangesBrowserNode parentNode = getParentNodeFor(node, policy, baseNode);
259         model.insertNodeInto(node, parentNode, 0);
260         myFoldersCache.put(pathKey, node);
261       }
262     }
263   }
264
265   private void buildSwitchedRoots(final Map<VirtualFile, String> switchedRoots) {
266     final ChangesBrowserNode rootsHeadNode = ChangesBrowserNode.create(myProject, ChangesBrowserNode.SWITCHED_ROOTS_TAG);
267     rootsHeadNode.setAttributes(SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
268     model.insertNodeInto(rootsHeadNode, root, root.getChildCount());
269
270     for (VirtualFile vf : switchedRoots.keySet()) {
271       final ChangesGroupingPolicy policy = createGroupingPolicy();
272       final ContentRevision cr = new CurrentContentRevision(new FilePathImpl(vf));
273       final Change change = new Change(cr, cr, FileStatus.NOT_CHANGED);
274       final String branchName = switchedRoots.get(vf);
275       insertChangeNode(vf, policy, rootsHeadNode, new Computable<ChangesBrowserNode>() {
276         public ChangesBrowserNode compute() {
277           return new ChangesBrowserChangeNode(myProject, change, new ChangeNodeDecorator() {
278             public void decorate(Change change, SimpleColoredComponent component, boolean isShowFlatten) {
279             }
280             public List<Pair<String, Stress>> stressPartsOfFileName(Change change, String parentPath) {
281               return null;
282             }
283             public void preDecorate(Change change, ChangesBrowserNodeRenderer renderer, boolean showFlatten) {
284               renderer.append("[" + branchName + "] ", SimpleTextAttributes.GRAYED_BOLD_ATTRIBUTES);
285             }
286           });
287         }
288       });
289     }
290   }
291
292   private void buildSwitchedFiles(final MultiMap<String, VirtualFile> switchedFiles) {
293     ChangesBrowserNode baseNode = ChangesBrowserNode.create(myProject, ChangesBrowserNode.SWITCHED_FILES_TAG);
294     model.insertNodeInto(baseNode, root, root.getChildCount());
295     for(String branchName: switchedFiles.keySet()) {
296       final Collection<VirtualFile> switchedFileList = switchedFiles.get(branchName);
297       if (switchedFileList.size() > 0) {
298         ChangesBrowserNode branchNode = ChangesBrowserNode.create(myProject, branchName);
299         model.insertNodeInto(branchNode, baseNode, baseNode.getChildCount());
300
301         final ChangesGroupingPolicy policy = createGroupingPolicy();
302         for (VirtualFile file : switchedFileList) {
303           insertChangeNode(file, policy, branchNode, defaultNodeCreator(file));
304         }
305       }
306     }
307   }
308
309   private void buildLogicallyLockedFiles(final Map<VirtualFile, LogicalLock> logicallyLockedFiles) {
310     final ChangesBrowserNode baseNode = createNode(ChangesBrowserNode.LOGICALLY_LOCKED_TAG);
311
312     final ChangesGroupingPolicy policy = createGroupingPolicy();
313     for (Map.Entry<VirtualFile, LogicalLock> entry : logicallyLockedFiles.entrySet()) {
314       final VirtualFile file = entry.getKey();
315       final LogicalLock lock = entry.getValue();
316       final ChangesBrowserLogicallyLockedFile obj = new ChangesBrowserLogicallyLockedFile(myProject, file, lock);
317       insertChangeNode(obj, policy, baseNode,
318                        defaultNodeCreator(obj));
319     }
320   }
321   
322   private Computable<ChangesBrowserNode> defaultNodeCreator(final Object change) {
323     return new Computable<ChangesBrowserNode>() {
324       public ChangesBrowserNode compute() {
325         return ChangesBrowserNode.create(myProject, change);
326       }
327     };
328   }
329
330   private void insertChangeNode(final Object change, final ChangesGroupingPolicy policy,
331                                 final ChangesBrowserNode listNode, final Computable<ChangesBrowserNode> nodeCreator) {
332     final String pathKey = getKey(change);
333     ChangesBrowserNode oldNode = (pathKey == null) ? null : myFoldersCache.get(pathKey);
334     final ChangesBrowserNode node = nodeCreator.compute();
335     if (oldNode != null) {
336       for(int i=oldNode.getChildCount()-1; i >= 0; i--) {
337         MutableTreeNode child = (MutableTreeNode) model.getChild(oldNode, i);
338         model.removeNodeFromParent(child);
339         model.insertNodeInto(child, node, model.getChildCount(node));
340       }
341       final MutableTreeNode parent = (MutableTreeNode)oldNode.getParent();
342       int index = model.getIndexOfChild(parent, oldNode);
343       model.removeNodeFromParent(oldNode);
344       model.insertNodeInto(node, parent, index);
345     }
346     else {
347       ChangesBrowserNode parentNode = getParentNodeFor(node, policy, listNode);
348       model.insertNodeInto(node, parentNode, model.getChildCount(parentNode));
349     }
350
351     if (pathKey != null) {
352       myFoldersCache.put(pathKey, node);
353     }
354   }
355
356   private void sortNodes() {
357     // todo static instance + think
358     TreeUtil.sort(model, MyChangesBrowserNodeComparator.getInstance());
359
360     model.nodeStructureChanged((TreeNode)model.getRoot());
361   }
362
363   private static class MyChangesBrowserNodeComparator implements Comparator<ChangesBrowserNode> {
364     private static final MyChangesBrowserNodeComparator ourInstance = new MyChangesBrowserNodeComparator();
365
366     public static MyChangesBrowserNodeComparator getInstance() {
367       return ourInstance;
368     }
369
370     public int compare(ChangesBrowserNode node1, ChangesBrowserNode node2) {
371       final int classdiff = node1.getSortWeight() - node2.getSortWeight();
372       if (classdiff != 0) return classdiff;
373       return node1.compareUserObjects(node2.getUserObject());
374     }
375   }
376
377   private static void collapseDirectories(DefaultTreeModel model, ChangesBrowserNode node) {
378     if (node.getUserObject() instanceof FilePath && node.getChildCount() == 1) {
379       final ChangesBrowserNode child = (ChangesBrowserNode)node.getChildAt(0);
380       if (child.getUserObject() instanceof FilePath && !child.isLeaf()) {
381         ChangesBrowserNode parent = (ChangesBrowserNode)node.getParent();
382         final int idx = parent.getIndex(node);
383         model.removeNodeFromParent(node);
384         model.removeNodeFromParent(child);
385         model.insertNodeInto(child, parent, idx);
386         collapseDirectories(model, parent);
387       }
388     }
389     else {
390       final Enumeration children = node.children();
391       while (children.hasMoreElements()) {
392         ChangesBrowserNode child = (ChangesBrowserNode)children.nextElement();
393         collapseDirectories(model, child);
394       }
395     }
396   }
397
398   // todo: temporal
399   private static String getKey(final Object o) {
400     if (o instanceof Change) {
401       return FilePathsHelper.convertPath(ChangesUtil.getFilePath((Change) o));
402     }
403     else if (o instanceof VirtualFile) {
404       return FilePathsHelper.convertPath((VirtualFile) o);
405     }
406     else if (o instanceof FilePath) {
407       // todo ? or path from filepath
408       return FilePathsHelper.convertPath(((FilePath) o).getIOFile().getAbsolutePath());
409     } else if (o instanceof ChangesBrowserLogicallyLockedFile) {
410       return FilePathsHelper.convertPath(((ChangesBrowserLogicallyLockedFile) o).getUserObject());
411     } else if (o instanceof LocallyDeletedChange) {
412       return FilePathsHelper.convertPath(((LocallyDeletedChange) o).getPath());
413     }
414
415     return null;
416   }
417
418   public static FilePath getPathForObject(Object o) {
419     if (o instanceof Change) {
420       return ChangesUtil.getFilePath((Change)o);
421     }
422     else if (o instanceof VirtualFile) {
423       return new FilePathImpl((VirtualFile) o);
424     }
425     else if (o instanceof FilePath) {
426       return (FilePath)o;
427     } else if (o instanceof ChangesBrowserLogicallyLockedFile) {
428       return new FilePathImpl(((ChangesBrowserLogicallyLockedFile) o).getUserObject());
429     } else if (o instanceof LocallyDeletedChange) {
430       return ((LocallyDeletedChange) o).getPath();
431     }
432
433     return null;
434   }
435
436   private ChangesBrowserNode getParentNodeFor(ChangesBrowserNode node, @Nullable ChangesGroupingPolicy policy, ChangesBrowserNode rootNode) {
437     if (showFlatten) {
438       return rootNode;
439     }
440
441     final FilePath path = getPathForObject(node.getUserObject());
442
443     if (policy != null) {
444       ChangesBrowserNode nodeFromPolicy = policy.getParentNodeFor(node, rootNode);
445       if (nodeFromPolicy != null) {
446         return nodeFromPolicy;
447       }
448     }
449
450     FilePath parentPath = path.getParentPath();
451     if (parentPath == null) {
452       return rootNode;
453     }
454
455     final String parentKey = FilePathsHelper.convertPath(parentPath);
456     ChangesBrowserNode parentNode = myFoldersCache.get(parentKey);
457     if (parentNode == null) {
458       parentNode = ChangesBrowserNode.create(myProject, parentPath);
459       ChangesBrowserNode grandPa = getParentNodeFor(parentNode, policy, rootNode);
460       model.insertNodeInto(parentNode, grandPa, grandPa.getChildCount());
461       myFoldersCache.put(parentKey, parentNode);
462     }
463
464     return parentNode;
465   }
466
467   public DefaultTreeModel clearAndGetModel() {
468     root.removeAllChildren();
469     model = new DefaultTreeModel(root);
470     myFoldersCache = new HashMap<String, ChangesBrowserNode>();
471     myPolicyInitialized = false;
472     return model;
473   }
474
475   public boolean isEmpty() {
476     return model.getChildCount(root) == 0;
477   }
478 }