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