Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / committed / CommittedChangesTreeBrowser.java
1 package com.intellij.openapi.vcs.changes.committed;
2
3 import com.intellij.ide.CopyProvider;
4 import com.intellij.ide.DefaultTreeExpander;
5 import com.intellij.ide.TreeExpander;
6 import com.intellij.ide.actions.ContextHelpAction;
7 import com.intellij.ide.ui.SplitterProportionsDataImpl;
8 import com.intellij.ide.util.treeView.TreeState;
9 import com.intellij.openapi.Disposable;
10 import com.intellij.openapi.actionSystem.*;
11 import com.intellij.openapi.application.ApplicationManager;
12 import com.intellij.openapi.application.ModalityState;
13 import com.intellij.openapi.keymap.Keymap;
14 import com.intellij.openapi.keymap.KeymapManager;
15 import com.intellij.openapi.project.Project;
16 import com.intellij.openapi.ui.Splitter;
17 import com.intellij.openapi.ui.SplitterProportionsData;
18 import com.intellij.openapi.util.Comparing;
19 import com.intellij.openapi.util.Disposer;
20 import com.intellij.openapi.vcs.VcsActions;
21 import com.intellij.openapi.vcs.VcsDataKeys;
22 import com.intellij.openapi.vcs.changes.Change;
23 import com.intellij.openapi.vcs.changes.ChangesUtil;
24 import com.intellij.openapi.vcs.changes.ContentRevision;
25 import com.intellij.openapi.vcs.changes.issueLinks.TreeLinkMouseListener;
26 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
27 import com.intellij.pom.Navigatable;
28 import com.intellij.ui.*;
29 import com.intellij.ui.treeStructure.Tree;
30 import com.intellij.ui.treeStructure.actions.CollapseAllAction;
31 import com.intellij.ui.treeStructure.actions.ExpandAllAction;
32 import com.intellij.util.messages.MessageBusConnection;
33 import com.intellij.util.messages.Topic;
34 import com.intellij.util.ui.StatusText;
35 import com.intellij.util.ui.tree.TreeUtil;
36 import org.jetbrains.annotations.NonNls;
37 import org.jetbrains.annotations.NotNull;
38 import org.jetbrains.annotations.Nullable;
39
40 import javax.swing.*;
41 import javax.swing.border.Border;
42 import javax.swing.event.ChangeEvent;
43 import javax.swing.event.ChangeListener;
44 import javax.swing.event.TreeSelectionEvent;
45 import javax.swing.event.TreeSelectionListener;
46 import javax.swing.tree.DefaultMutableTreeNode;
47 import javax.swing.tree.DefaultTreeModel;
48 import javax.swing.tree.TreeModel;
49 import javax.swing.tree.TreePath;
50 import java.awt.*;
51 import java.util.*;
52 import java.util.List;
53
54 /**
55  * @author yole
56  */
57 public class CommittedChangesTreeBrowser extends JPanel implements TypeSafeDataProvider, Disposable, DecoratorManager {
58   private static final Border RIGHT_BORDER = IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.LEFT);
59
60   private final Project myProject;
61   private final Tree myChangesTree;
62   private final RepositoryChangesBrowser myDetailsView;
63   private List<CommittedChangeList> myChangeLists;
64   private List<CommittedChangeList> mySelectedChangeLists;
65   private ChangeListGroupingStrategy myGroupingStrategy = new DateChangeListGroupingStrategy();
66   private final CompositeChangeListFilteringStrategy myFilteringStrategy = new CompositeChangeListFilteringStrategy();
67   private final JPanel myLeftPanel;
68   private final FilterChangeListener myFilterChangeListener = new FilterChangeListener();
69   private final SplitterProportionsData mySplitterProportionsData = new SplitterProportionsDataImpl();
70   private final CopyProvider myCopyProvider;
71   private final TreeExpander myTreeExpander;
72   private String myHelpId;
73
74   public static final Topic<CommittedChangesReloadListener> ITEMS_RELOADED = new Topic<CommittedChangesReloadListener>("ITEMS_RELOADED", CommittedChangesReloadListener.class);
75
76   private final List<CommittedChangeListDecorator> myDecorators;
77
78   @NonNls public static final String ourHelpId = "reference.changesToolWindow.incoming";
79
80   private WiseSplitter myInnerSplitter;
81   private final MessageBusConnection myConnection;
82   private TreeState myState;
83
84   public CommittedChangesTreeBrowser(final Project project, final List<CommittedChangeList> changeLists) {
85     super(new BorderLayout());
86
87     myProject = project;
88     myDecorators = new LinkedList<CommittedChangeListDecorator>();
89     myChangeLists = changeLists;
90     myChangesTree = new ChangesBrowserTree();
91     myChangesTree.setRootVisible(false);
92     myChangesTree.setShowsRootHandles(true);
93     myChangesTree.setCellRenderer(new CommittedChangeListRenderer(project, myDecorators));
94     TreeUtil.expandAll(myChangesTree);
95     myChangesTree.getExpandableItemsHandler().setEnabled(false);
96
97     myDetailsView = new RepositoryChangesBrowser(project, Collections.<CommittedChangeList>emptyList());
98     myDetailsView.getViewer().setScrollPaneBorder(RIGHT_BORDER);
99
100     myChangesTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
101       public void valueChanged(TreeSelectionEvent e) {
102         updateBySelectionChange();
103       }
104     });
105
106     final TreeLinkMouseListener linkMouseListener = new TreeLinkMouseListener(new CommittedChangeListRenderer(project, myDecorators));
107     linkMouseListener.installOn(myChangesTree);
108
109     myLeftPanel = new JPanel(new BorderLayout());
110
111     initSplitters();
112
113     updateBySelectionChange();
114
115     Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
116     CustomShortcutSet quickdocShortcuts = new CustomShortcutSet(keymap.getShortcuts(IdeActions.ACTION_QUICK_JAVADOC));
117     EmptyAction.registerWithShortcutSet("CommittedChanges.Details", quickdocShortcuts, this);
118
119     myCopyProvider = new TreeCopyProvider(myChangesTree);
120     myTreeExpander = new DefaultTreeExpander(myChangesTree);
121     myDetailsView.addToolbarAction(ActionManager.getInstance().getAction("Vcs.ShowTabbedFileHistory"));
122
123     myHelpId = ourHelpId;
124
125     myDetailsView.getDiffAction().registerCustomShortcutSet(myDetailsView.getDiffAction().getShortcutSet(), myChangesTree);
126
127     myConnection = myProject.getMessageBus().connect();
128     myConnection.subscribe(ITEMS_RELOADED, new CommittedChangesReloadListener() {
129       public void itemsReloaded() {
130       }
131       public void emptyRefresh() {
132         updateGrouping();
133       }
134     });
135   }
136
137   private void initSplitters() {
138     final Splitter filterSplitter = new Splitter(false, 0.5f);
139
140     filterSplitter.setSecondComponent(ScrollPaneFactory.createScrollPane(myChangesTree));
141     myLeftPanel.add(filterSplitter, BorderLayout.CENTER);
142     final Splitter mainSplitter = new Splitter(false, 0.7f);
143     mainSplitter.setFirstComponent(myLeftPanel);
144     mainSplitter.setSecondComponent(myDetailsView);
145
146     add(mainSplitter, BorderLayout.CENTER);
147
148     myInnerSplitter = new WiseSplitter(new Runnable() {
149       public void run() {
150         filterSplitter.doLayout();
151         updateModel();
152       }
153     }, filterSplitter);
154     Disposer.register(this, myInnerSplitter);
155
156     mySplitterProportionsData.externalizeFromDimensionService("CommittedChanges.SplitterProportions");
157     mySplitterProportionsData.restoreSplitterProportions(this);
158   }
159
160   public void addFilter(final ChangeListFilteringStrategy strategy) {
161     myFilteringStrategy.addStrategy(strategy.getKey(), strategy);
162     strategy.addChangeListener(myFilterChangeListener);
163   }
164
165   private void updateGrouping() {
166     if (myGroupingStrategy.changedSinceApply()) {
167       ApplicationManager.getApplication().invokeLater(new Runnable() {
168         public void run() {
169           updateModel();
170         }
171       }, ModalityState.NON_MODAL);
172     }
173   }
174
175   private TreeModel buildTreeModel(final List<CommittedChangeList> filteredChangeLists) {
176     DefaultMutableTreeNode root = new DefaultMutableTreeNode();
177     DefaultTreeModel model = new DefaultTreeModel(root);
178     Collections.sort(filteredChangeLists, myGroupingStrategy.getComparator());
179     myGroupingStrategy.beforeStart();
180     DefaultMutableTreeNode lastGroupNode = null;
181     String lastGroupName = null;
182     for(CommittedChangeList list: filteredChangeLists) {
183       String groupName = myGroupingStrategy.getGroupName(list);
184       if (!Comparing.equal(groupName, lastGroupName)) {
185         lastGroupName = groupName;
186         lastGroupNode = new DefaultMutableTreeNode(lastGroupName);
187         root.add(lastGroupNode);
188       }
189       assert lastGroupNode != null;
190       lastGroupNode.add(new DefaultMutableTreeNode(list));
191     }
192     return model;
193   }
194
195   public void setHelpId(final String helpId) {
196     myHelpId = helpId;
197   }
198
199   public StatusText getEmptyText() {
200     return myChangesTree.getEmptyText();
201   }
202
203   public void setToolBar(JComponent toolBar) {
204     myLeftPanel.add(toolBar, BorderLayout.NORTH);
205     Dimension prefSize = myDetailsView.getHeaderPanel().getPreferredSize();
206     if (prefSize.height < toolBar.getPreferredSize().height) {
207       prefSize.height = toolBar.getPreferredSize().height;
208       myDetailsView.getHeaderPanel().setPreferredSize(prefSize);
209     }
210   }
211
212   public void dispose() {
213     myConnection.disconnect();
214     mySplitterProportionsData.saveSplitterProportions(this);
215     mySplitterProportionsData.externalizeToDimensionService("CommittedChanges.SplitterProportions");
216     myDetailsView.dispose();
217   }
218
219   public void setItems(@NotNull List<CommittedChangeList> items, final CommittedChangesBrowserUseCase useCase) {
220     myDetailsView.setUseCase(useCase);
221     myChangeLists = items;
222     myFilteringStrategy.setFilterBase(items);
223     myProject.getMessageBus().syncPublisher(ITEMS_RELOADED).itemsReloaded();
224     updateModel();
225   }
226
227   private void updateModel() {
228     final List<CommittedChangeList> filteredChangeLists = myFilteringStrategy.filterChangeLists(myChangeLists);
229     final TreePath[] paths = myChangesTree.getSelectionPaths();
230     myChangesTree.setModel(buildTreeModel(filteredChangeLists));
231     TreeUtil.expandAll(myChangesTree);
232     myChangesTree.setSelectionPaths(paths);
233   }
234
235   public void setGroupingStrategy(ChangeListGroupingStrategy strategy) {
236     myGroupingStrategy = strategy;
237     updateModel();
238   }
239
240   private void updateBySelectionChange() {
241     List<CommittedChangeList> selection = new ArrayList<CommittedChangeList>();
242     final TreePath[] selectionPaths = myChangesTree.getSelectionPaths();
243     if (selectionPaths != null) {
244       for(TreePath path: selectionPaths) {
245         DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
246         if (node.getUserObject() instanceof CommittedChangeList) {
247           selection.add((CommittedChangeList) node.getUserObject());
248         }
249       }
250     }
251
252     if (!selection.equals(mySelectedChangeLists)) {
253       mySelectedChangeLists = selection;
254       myDetailsView.setChangesToDisplay(collectChanges(mySelectedChangeLists, false));
255     }
256   }
257
258   @NotNull
259   public static List<Change> collectChanges(final List<? extends CommittedChangeList> selectedChangeLists, final boolean withMovedTrees) {
260     List<Change> result = new ArrayList<Change>();
261     Collections.sort(selectedChangeLists, CommittedChangeListByDateComparator.ASCENDING);
262
263     for(CommittedChangeList cl: selectedChangeLists) {
264       final Collection<Change> changes = withMovedTrees ? cl.getChangesWithMovedTrees() : cl.getChanges();
265       for(Change c: changes) {
266         addOrReplaceChange(result, c);
267       }
268     }
269     return result;
270   }
271
272   /**
273    * Zips changes by removing duplicates (changes in the same file) and compounding the diff.
274    * <b>NB:</b> changes must be given in the time-ascending order, i.e the first change in the list should be the oldest one.
275    */
276   @NotNull
277   public static List<Change> zipChanges(@NotNull List<Change> changes) {
278     final List<Change> result = new ArrayList<Change>();
279     for (Change change : changes) {
280       addOrReplaceChange(result, change);
281     }
282     return result;
283   }
284
285   private static void addOrReplaceChange(final List<Change> changes, final Change c) {
286     final ContentRevision beforeRev = c.getBeforeRevision();
287     // todo!!! further improvements needed
288     if (beforeRev != null) {
289       final String beforeName = beforeRev.getFile().getName();
290       final String beforeAbsolutePath = beforeRev.getFile().getIOFile().getAbsolutePath();
291       for(Change oldChange: changes) {
292         ContentRevision rev = oldChange.getAfterRevision();
293         // first compare name, which is many times faster - to remove 99% not matching
294         if (rev != null && (rev.getFile().getName().equals(beforeName)) && rev.getFile().getIOFile().getAbsolutePath().equals(beforeAbsolutePath)) {
295           changes.remove(oldChange);
296           if (oldChange.getBeforeRevision() != null || c.getAfterRevision() != null) {
297             changes.add(new Change(oldChange.getBeforeRevision(), c.getAfterRevision()));
298           }
299           return;
300         }
301       }
302     }
303     changes.add(c);
304   }
305
306   private List<CommittedChangeList> getSelectedChangeLists() {
307     return TreeUtil.collectSelectedObjectsOfType(myChangesTree, CommittedChangeList.class);
308   }
309
310   public void setTableContextMenu(final ActionGroup group, final List<AnAction> auxiliaryActions) {
311     DefaultActionGroup menuGroup = new DefaultActionGroup();
312     menuGroup.add(group);
313     for (AnAction action : auxiliaryActions) {
314       menuGroup.add(action);
315     }
316     menuGroup.add(ActionManager.getInstance().getAction(VcsActions.ACTION_COPY_REVISION_NUMBER));
317     PopupHandler.installPopupHandler(myChangesTree, menuGroup, ActionPlaces.UNKNOWN, ActionManager.getInstance());
318   }
319
320   public void removeFilteringStrategy(final CommittedChangesFilterKey key) {
321     final ChangeListFilteringStrategy strategy = myFilteringStrategy.removeStrategy(key);
322     if (strategy != null) {
323       strategy.removeChangeListener(myFilterChangeListener);
324     }
325     myInnerSplitter.remove(key);
326   }
327
328   public boolean setFilteringStrategy(final ChangeListFilteringStrategy filteringStrategy) {
329     if (myInnerSplitter.canAdd()) {
330       filteringStrategy.addChangeListener(myFilterChangeListener);
331
332       final CommittedChangesFilterKey key = filteringStrategy.getKey();
333       myFilteringStrategy.addStrategy(key, filteringStrategy);
334       myFilteringStrategy.setFilterBase(myChangeLists);
335
336       final JComponent filterUI = filteringStrategy.getFilterUI();
337       if (filterUI != null) {
338         myInnerSplitter.add(key, filterUI);
339       }
340       return true;
341     }
342     return false;
343   }
344
345   public ActionToolbar createGroupFilterToolbar(final Project project, final ActionGroup leadGroup, @Nullable final ActionGroup tailGroup,
346                                                 final List<AnAction> extra) {
347     DefaultActionGroup toolbarGroup = new DefaultActionGroup();
348     toolbarGroup.add(leadGroup);
349     toolbarGroup.addSeparator();
350     toolbarGroup.add(new SelectFilteringAction(project, this));
351     toolbarGroup.add(new SelectGroupingAction(project, this));
352     final ExpandAllAction expandAllAction = new ExpandAllAction(myChangesTree);
353     final CollapseAllAction collapseAllAction = new CollapseAllAction(myChangesTree);
354     expandAllAction.registerCustomShortcutSet(
355       new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_EXPAND_ALL)),
356       myChangesTree);
357     collapseAllAction.registerCustomShortcutSet(
358       new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_COLLAPSE_ALL)),
359       myChangesTree);
360     toolbarGroup.add(expandAllAction);
361     toolbarGroup.add(collapseAllAction);
362     toolbarGroup.add(ActionManager.getInstance().getAction(VcsActions.ACTION_COPY_REVISION_NUMBER));
363     toolbarGroup.add(new ContextHelpAction(myHelpId));
364     if (tailGroup != null) {
365       toolbarGroup.add(tailGroup);
366     }
367     for (AnAction anAction : extra) {
368       toolbarGroup.add(anAction);
369     }
370     return ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, toolbarGroup, true);
371   }
372
373   public void calcData(DataKey key, DataSink sink) {
374     if (key.equals(VcsDataKeys.CHANGES)) {
375       final Collection<Change> changes = collectChanges(getSelectedChangeLists(), false);
376       sink.put(VcsDataKeys.CHANGES, changes.toArray(new Change[changes.size()]));
377     } else if (key.equals(VcsDataKeys.HAVE_SELECTED_CHANGES)) {
378       final int count = myChangesTree.getSelectionCount();
379       sink.put(VcsDataKeys.HAVE_SELECTED_CHANGES, count > 0 ? Boolean.TRUE : Boolean.FALSE);
380     }
381     else if (key.equals(VcsDataKeys.CHANGES_WITH_MOVED_CHILDREN)) {
382       final Collection<Change> changes = collectChanges(getSelectedChangeLists(), true);
383       sink.put(VcsDataKeys.CHANGES_WITH_MOVED_CHILDREN, changes.toArray(new Change[changes.size()]));
384     }
385     else if (key.equals(VcsDataKeys.CHANGE_LISTS)) {
386       final List<CommittedChangeList> lists = getSelectedChangeLists();
387       if (!lists.isEmpty()) {
388         sink.put(VcsDataKeys.CHANGE_LISTS, lists.toArray(new CommittedChangeList[lists.size()]));
389       }
390     }
391     else if (key.equals(CommonDataKeys.NAVIGATABLE_ARRAY)) {
392       final Collection<Change> changes = collectChanges(getSelectedChangeLists(), false);
393       Navigatable[] result = ChangesUtil.getNavigatableArray(myProject, ChangesUtil.getFilesFromChanges(changes));
394       sink.put(CommonDataKeys.NAVIGATABLE_ARRAY, result);
395     }
396     else if (key.equals(PlatformDataKeys.HELP_ID)) {
397       sink.put(PlatformDataKeys.HELP_ID, myHelpId);
398     } else if (VcsDataKeys.SELECTED_CHANGES_IN_DETAILS.equals(key)) {
399       final List<Change> selectedChanges = myDetailsView.getSelectedChanges();
400       sink.put(VcsDataKeys.SELECTED_CHANGES_IN_DETAILS, selectedChanges.toArray(new Change[selectedChanges.size()]));
401     }
402   }
403
404   public TreeExpander getTreeExpander() {
405     return myTreeExpander;
406   }
407
408   public void repaintTree() {
409     myChangesTree.revalidate();
410     myChangesTree.repaint();
411   }
412
413   public void install(final CommittedChangeListDecorator decorator) {
414     myDecorators.add(decorator);
415     repaintTree();
416   }
417
418   public void remove(final CommittedChangeListDecorator decorator) {
419     myDecorators.remove(decorator);
420     repaintTree();
421   }
422
423   public void reportLoadedLists(final CommittedChangeListsListener listener) {
424     ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
425       public void run() {
426         listener.onBeforeStartReport();
427         for (CommittedChangeList list : myChangeLists) {
428           listener.report(list);
429         }
430         listener.onAfterEndReport();
431       }
432     });
433   }
434
435   // for appendable view
436   public void reset() {
437     myChangeLists.clear();
438     myFilteringStrategy.resetFilterBase();
439
440     myState = TreeState.createOn(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot());
441     updateModel();
442   }
443
444   public void append(final List<CommittedChangeList> list) {
445     final TreeState state = myChangeLists.isEmpty() && myState != null ? myState :
446       TreeState.createOn(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot());
447     state.setScrollToSelection(false);
448     myChangeLists.addAll(list);
449
450     myFilteringStrategy.appendFilterBase(list);
451
452     myChangesTree.setModel(buildTreeModel(myFilteringStrategy.filterChangeLists(myChangeLists)));
453     state.applyTo(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot());
454     TreeUtil.expandAll(myChangesTree);
455     myProject.getMessageBus().syncPublisher(ITEMS_RELOADED).itemsReloaded();
456   }
457
458   public static class MoreLauncher implements Runnable {
459     private final Project myProject;
460     private final CommittedChangeList myList;
461
462     MoreLauncher(final Project project, final CommittedChangeList list) {
463       myProject = project;
464       myList = list;
465     }
466
467     public void run() {
468       ChangeListDetailsAction.showDetailsPopup(myProject, myList);
469     }
470   }
471
472   private class FilterChangeListener implements ChangeListener {
473     public void stateChanged(ChangeEvent e) {
474       if (ApplicationManager.getApplication().isDispatchThread()) {
475         updateModel();
476       } else {
477         ApplicationManager.getApplication().invokeLater(new Runnable() {
478           public void run() {
479             updateModel();
480           }
481         });
482       }
483     }
484   }
485
486   private class ChangesBrowserTree extends Tree implements TypeSafeDataProvider {
487     public ChangesBrowserTree() {
488       super(buildTreeModel(myFilteringStrategy.filterChangeLists(myChangeLists)));
489     }
490
491     @Override
492     public boolean getScrollableTracksViewportWidth() {
493       return true;
494     }
495
496     public void calcData(final DataKey key, final DataSink sink) {
497       if (key.equals(PlatformDataKeys.COPY_PROVIDER)) {
498         sink.put(PlatformDataKeys.COPY_PROVIDER, myCopyProvider);
499       }
500       else if (key.equals(PlatformDataKeys.TREE_EXPANDER)) {
501         sink.put(PlatformDataKeys.TREE_EXPANDER, myTreeExpander);
502       } else {
503         final String name = key.getName();
504         if (VcsDataKeys.SELECTED_CHANGES.is(name) || VcsDataKeys.CHANGE_LEAD_SELECTION.is(name) ||
505             CommittedChangesBrowserUseCase.DATA_KEY.is(name)) {
506           final Object data = myDetailsView.getData(name);
507           if (data != null) {
508             sink.put(key, data);
509           }
510         }
511       }
512     }
513   }
514
515   public interface CommittedChangesReloadListener {
516     void itemsReloaded();
517     void emptyRefresh();
518   }
519
520   public void setLoading(final boolean value) {
521     new AbstractCalledLater(myProject, ModalityState.NON_MODAL) {
522       public void run() {
523         myChangesTree.setPaintBusy(value);
524       }
525     }.callMe();
526   }
527 }