1 package com.intellij.openapi.vcs.changes.committed;
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;
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;
52 import java.util.List;
57 public class CommittedChangesTreeBrowser extends JPanel implements TypeSafeDataProvider, Disposable, DecoratorManager {
58 private static final Border RIGHT_BORDER = IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.LEFT);
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;
74 public static final Topic<CommittedChangesReloadListener> ITEMS_RELOADED = new Topic<CommittedChangesReloadListener>("ITEMS_RELOADED", CommittedChangesReloadListener.class);
76 private final List<CommittedChangeListDecorator> myDecorators;
78 @NonNls public static final String ourHelpId = "reference.changesToolWindow.incoming";
80 private WiseSplitter myInnerSplitter;
81 private final MessageBusConnection myConnection;
82 private TreeState myState;
84 public CommittedChangesTreeBrowser(final Project project, final List<CommittedChangeList> changeLists) {
85 super(new BorderLayout());
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);
97 myDetailsView = new RepositoryChangesBrowser(project, Collections.<CommittedChangeList>emptyList());
98 myDetailsView.getViewer().setScrollPaneBorder(RIGHT_BORDER);
100 myChangesTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() {
101 public void valueChanged(TreeSelectionEvent e) {
102 updateBySelectionChange();
106 final TreeLinkMouseListener linkMouseListener = new TreeLinkMouseListener(new CommittedChangeListRenderer(project, myDecorators));
107 linkMouseListener.installOn(myChangesTree);
109 myLeftPanel = new JPanel(new BorderLayout());
113 updateBySelectionChange();
115 Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
116 CustomShortcutSet quickdocShortcuts = new CustomShortcutSet(keymap.getShortcuts(IdeActions.ACTION_QUICK_JAVADOC));
117 EmptyAction.registerWithShortcutSet("CommittedChanges.Details", quickdocShortcuts, this);
119 myCopyProvider = new TreeCopyProvider(myChangesTree);
120 myTreeExpander = new DefaultTreeExpander(myChangesTree);
121 myDetailsView.addToolbarAction(ActionManager.getInstance().getAction("Vcs.ShowTabbedFileHistory"));
123 myHelpId = ourHelpId;
125 myDetailsView.getDiffAction().registerCustomShortcutSet(myDetailsView.getDiffAction().getShortcutSet(), myChangesTree);
127 myConnection = myProject.getMessageBus().connect();
128 myConnection.subscribe(ITEMS_RELOADED, new CommittedChangesReloadListener() {
129 public void itemsReloaded() {
131 public void emptyRefresh() {
137 private void initSplitters() {
138 final Splitter filterSplitter = new Splitter(false, 0.5f);
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);
146 add(mainSplitter, BorderLayout.CENTER);
148 myInnerSplitter = new WiseSplitter(new Runnable() {
150 filterSplitter.doLayout();
154 Disposer.register(this, myInnerSplitter);
156 mySplitterProportionsData.externalizeFromDimensionService("CommittedChanges.SplitterProportions");
157 mySplitterProportionsData.restoreSplitterProportions(this);
160 public void addFilter(final ChangeListFilteringStrategy strategy) {
161 myFilteringStrategy.addStrategy(strategy.getKey(), strategy);
162 strategy.addChangeListener(myFilterChangeListener);
165 private void updateGrouping() {
166 if (myGroupingStrategy.changedSinceApply()) {
167 ApplicationManager.getApplication().invokeLater(new Runnable() {
171 }, ModalityState.NON_MODAL);
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);
189 assert lastGroupNode != null;
190 lastGroupNode.add(new DefaultMutableTreeNode(list));
195 public void setHelpId(final String helpId) {
199 public StatusText getEmptyText() {
200 return myChangesTree.getEmptyText();
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);
212 public void dispose() {
213 myConnection.disconnect();
214 mySplitterProportionsData.saveSplitterProportions(this);
215 mySplitterProportionsData.externalizeToDimensionService("CommittedChanges.SplitterProportions");
216 myDetailsView.dispose();
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();
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);
235 public void setGroupingStrategy(ChangeListGroupingStrategy strategy) {
236 myGroupingStrategy = strategy;
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());
252 if (!selection.equals(mySelectedChangeLists)) {
253 mySelectedChangeLists = selection;
254 myDetailsView.setChangesToDisplay(collectChanges(mySelectedChangeLists, false));
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);
263 for(CommittedChangeList cl: selectedChangeLists) {
264 final Collection<Change> changes = withMovedTrees ? cl.getChangesWithMovedTrees() : cl.getChanges();
265 for(Change c: changes) {
266 addOrReplaceChange(result, c);
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.
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);
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()));
306 private List<CommittedChangeList> getSelectedChangeLists() {
307 return TreeUtil.collectSelectedObjectsOfType(myChangesTree, CommittedChangeList.class);
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);
316 menuGroup.add(ActionManager.getInstance().getAction(VcsActions.ACTION_COPY_REVISION_NUMBER));
317 PopupHandler.installPopupHandler(myChangesTree, menuGroup, ActionPlaces.UNKNOWN, ActionManager.getInstance());
320 public void removeFilteringStrategy(final CommittedChangesFilterKey key) {
321 final ChangeListFilteringStrategy strategy = myFilteringStrategy.removeStrategy(key);
322 if (strategy != null) {
323 strategy.removeChangeListener(myFilterChangeListener);
325 myInnerSplitter.remove(key);
328 public boolean setFilteringStrategy(final ChangeListFilteringStrategy filteringStrategy) {
329 if (myInnerSplitter.canAdd()) {
330 filteringStrategy.addChangeListener(myFilterChangeListener);
332 final CommittedChangesFilterKey key = filteringStrategy.getKey();
333 myFilteringStrategy.addStrategy(key, filteringStrategy);
334 myFilteringStrategy.setFilterBase(myChangeLists);
336 final JComponent filterUI = filteringStrategy.getFilterUI();
337 if (filterUI != null) {
338 myInnerSplitter.add(key, filterUI);
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)),
357 collapseAllAction.registerCustomShortcutSet(
358 new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_COLLAPSE_ALL)),
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);
367 for (AnAction anAction : extra) {
368 toolbarGroup.add(anAction);
370 return ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, toolbarGroup, true);
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);
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()]));
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()]));
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);
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()]));
404 public TreeExpander getTreeExpander() {
405 return myTreeExpander;
408 public void repaintTree() {
409 myChangesTree.revalidate();
410 myChangesTree.repaint();
413 public void install(final CommittedChangeListDecorator decorator) {
414 myDecorators.add(decorator);
418 public void remove(final CommittedChangeListDecorator decorator) {
419 myDecorators.remove(decorator);
423 public void reportLoadedLists(final CommittedChangeListsListener listener) {
424 ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
426 listener.onBeforeStartReport();
427 for (CommittedChangeList list : myChangeLists) {
428 listener.report(list);
430 listener.onAfterEndReport();
435 // for appendable view
436 public void reset() {
437 myChangeLists.clear();
438 myFilteringStrategy.resetFilterBase();
440 myState = TreeState.createOn(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot());
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);
450 myFilteringStrategy.appendFilterBase(list);
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();
458 public static class MoreLauncher implements Runnable {
459 private final Project myProject;
460 private final CommittedChangeList myList;
462 MoreLauncher(final Project project, final CommittedChangeList list) {
468 ChangeListDetailsAction.showDetailsPopup(myProject, myList);
472 private class FilterChangeListener implements ChangeListener {
473 public void stateChanged(ChangeEvent e) {
474 if (ApplicationManager.getApplication().isDispatchThread()) {
477 ApplicationManager.getApplication().invokeLater(new Runnable() {
486 private class ChangesBrowserTree extends Tree implements TypeSafeDataProvider {
487 public ChangesBrowserTree() {
488 super(buildTreeModel(myFilteringStrategy.filterChangeLists(myChangeLists)));
492 public boolean getScrollableTracksViewportWidth() {
496 public void calcData(final DataKey key, final DataSink sink) {
497 if (key.equals(PlatformDataKeys.COPY_PROVIDER)) {
498 sink.put(PlatformDataKeys.COPY_PROVIDER, myCopyProvider);
500 else if (key.equals(PlatformDataKeys.TREE_EXPANDER)) {
501 sink.put(PlatformDataKeys.TREE_EXPANDER, myTreeExpander);
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);
515 public interface CommittedChangesReloadListener {
516 void itemsReloaded();
520 public void setLoading(final boolean value) {
521 new AbstractCalledLater(myProject, ModalityState.NON_MODAL) {
523 myChangesTree.setPaintBusy(value);