package com.intellij.openapi.vcs.changes.committed; import com.intellij.ide.CopyProvider; import com.intellij.ide.DefaultTreeExpander; import com.intellij.ide.TreeExpander; import com.intellij.ide.actions.ContextHelpAction; import com.intellij.ide.ui.SplitterProportionsDataImpl; import com.intellij.ide.util.treeView.TreeState; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Splitter; import com.intellij.openapi.ui.SplitterProportionsData; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.vcs.VcsDataKeys; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangesUtil; import com.intellij.openapi.vcs.changes.ContentRevision; import com.intellij.openapi.vcs.changes.issueLinks.TreeLinkMouseListener; import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList; import com.intellij.pom.Navigatable; import com.intellij.ui.*; import com.intellij.ui.treeStructure.Tree; import com.intellij.ui.treeStructure.actions.CollapseAllAction; import com.intellij.ui.treeStructure.actions.ExpandAllAction; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.messages.Topic; import com.intellij.util.ui.StatusText; import com.intellij.util.ui.tree.TreeUtil; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.border.Border; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import java.awt.*; import java.util.*; import java.util.List; /** * @author yole */ public class CommittedChangesTreeBrowser extends JPanel implements TypeSafeDataProvider, Disposable, DecoratorManager { private static final Border RIGHT_BORDER = IdeBorderFactory.createBorder(SideBorder.TOP | SideBorder.LEFT); private final Project myProject; private final Tree myChangesTree; private final RepositoryChangesBrowser myDetailsView; private List<CommittedChangeList> myChangeLists; private List<CommittedChangeList> mySelectedChangeLists; private ChangeListGroupingStrategy myGroupingStrategy = new DateChangeListGroupingStrategy(); private final CompositeChangeListFilteringStrategy myFilteringStrategy = new CompositeChangeListFilteringStrategy(); private final JPanel myLeftPanel; private final FilterChangeListener myFilterChangeListener = new FilterChangeListener(); private final SplitterProportionsData mySplitterProportionsData = new SplitterProportionsDataImpl(); private final CopyProvider myCopyProvider; private final TreeExpander myTreeExpander; private String myHelpId; public static final Topic<CommittedChangesReloadListener> ITEMS_RELOADED = new Topic<CommittedChangesReloadListener>("ITEMS_RELOADED", CommittedChangesReloadListener.class); private final List<CommittedChangeListDecorator> myDecorators; @NonNls public static final String ourHelpId = "reference.changesToolWindow.incoming"; private WiseSplitter myInnerSplitter; private final MessageBusConnection myConnection; private TreeState myState; public CommittedChangesTreeBrowser(final Project project, final List<CommittedChangeList> changeLists) { super(new BorderLayout()); myProject = project; myDecorators = new LinkedList<CommittedChangeListDecorator>(); myChangeLists = changeLists; myChangesTree = new ChangesBrowserTree(); myChangesTree.setRootVisible(false); myChangesTree.setShowsRootHandles(true); myChangesTree.setCellRenderer(new CommittedChangeListRenderer(project, myDecorators)); TreeUtil.expandAll(myChangesTree); myChangesTree.getExpandableItemsHandler().setEnabled(false); myDetailsView = new RepositoryChangesBrowser(project, Collections.<CommittedChangeList>emptyList()); myDetailsView.getViewer().setScrollPaneBorder(RIGHT_BORDER); myChangesTree.getSelectionModel().addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { updateBySelectionChange(); } }); final TreeLinkMouseListener linkMouseListener = new TreeLinkMouseListener(new CommittedChangeListRenderer(project, myDecorators)); linkMouseListener.installOn(myChangesTree); myLeftPanel = new JPanel(new BorderLayout()); initSplitters(); updateBySelectionChange(); ActionManager.getInstance().getAction("CommittedChanges.Details") .registerCustomShortcutSet(new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_QUICK_JAVADOC)), this); myCopyProvider = new TreeCopyProvider(myChangesTree); myTreeExpander = new DefaultTreeExpander(myChangesTree); myDetailsView.addToolbarAction(ActionManager.getInstance().getAction("Vcs.ShowTabbedFileHistory")); myHelpId = ourHelpId; myDetailsView.getDiffAction().registerCustomShortcutSet(CommonShortcuts.getDiff(), myChangesTree); myConnection = myProject.getMessageBus().connect(); myConnection.subscribe(ITEMS_RELOADED, new CommittedChangesReloadListener() { public void itemsReloaded() { } public void emptyRefresh() { updateGrouping(); } }); } private void initSplitters() { final Splitter filterSplitter = new Splitter(false, 0.5f); filterSplitter.setSecondComponent(ScrollPaneFactory.createScrollPane(myChangesTree)); myLeftPanel.add(filterSplitter, BorderLayout.CENTER); final Splitter mainSplitter = new Splitter(false, 0.7f); mainSplitter.setFirstComponent(myLeftPanel); mainSplitter.setSecondComponent(myDetailsView); add(mainSplitter, BorderLayout.CENTER); myInnerSplitter = new WiseSplitter(new Runnable() { public void run() { filterSplitter.doLayout(); updateModel(); } }, filterSplitter); Disposer.register(this, myInnerSplitter); mySplitterProportionsData.externalizeFromDimensionService("CommittedChanges.SplitterProportions"); mySplitterProportionsData.restoreSplitterProportions(this); } public void addFilter(final ChangeListFilteringStrategy strategy) { myFilteringStrategy.addStrategy(strategy.getKey(), strategy); strategy.addChangeListener(myFilterChangeListener); } private void updateGrouping() { if (myGroupingStrategy.changedSinceApply()) { ApplicationManager.getApplication().invokeLater(new Runnable() { public void run() { updateModel(); } }, ModalityState.NON_MODAL); } } private TreeModel buildTreeModel(final List<CommittedChangeList> filteredChangeLists) { DefaultMutableTreeNode root = new DefaultMutableTreeNode(); DefaultTreeModel model = new DefaultTreeModel(root); Collections.sort(filteredChangeLists, myGroupingStrategy.getComparator()); myGroupingStrategy.beforeStart(); DefaultMutableTreeNode lastGroupNode = null; String lastGroupName = null; for (CommittedChangeList list : filteredChangeLists) { String groupName = myGroupingStrategy.getGroupName(list); if (!Comparing.equal(groupName, lastGroupName)) { lastGroupName = groupName; lastGroupNode = new DefaultMutableTreeNode(lastGroupName); root.add(lastGroupNode); } assert lastGroupNode != null; lastGroupNode.add(new DefaultMutableTreeNode(list)); } return model; } public void setHelpId(final String helpId) { myHelpId = helpId; } public StatusText getEmptyText() { return myChangesTree.getEmptyText(); } public void setToolBar(JComponent toolBar) { myLeftPanel.add(toolBar, BorderLayout.NORTH); Dimension prefSize = myDetailsView.getHeaderPanel().getPreferredSize(); if (prefSize.height < toolBar.getPreferredSize().height) { prefSize.height = toolBar.getPreferredSize().height; myDetailsView.getHeaderPanel().setPreferredSize(prefSize); } } public void dispose() { myConnection.disconnect(); mySplitterProportionsData.saveSplitterProportions(this); mySplitterProportionsData.externalizeToDimensionService("CommittedChanges.SplitterProportions"); myDetailsView.dispose(); } public void setItems(@NotNull List<CommittedChangeList> items, final CommittedChangesBrowserUseCase useCase) { myDetailsView.setUseCase(useCase); myChangeLists = items; myFilteringStrategy.setFilterBase(items); myProject.getMessageBus().syncPublisher(ITEMS_RELOADED).itemsReloaded(); updateModel(); } private void updateModel() { final List<CommittedChangeList> filteredChangeLists = myFilteringStrategy.filterChangeLists(myChangeLists); final TreePath[] paths = myChangesTree.getSelectionPaths(); myChangesTree.setModel(buildTreeModel(filteredChangeLists)); TreeUtil.expandAll(myChangesTree); myChangesTree.setSelectionPaths(paths); } public void setGroupingStrategy(ChangeListGroupingStrategy strategy) { myGroupingStrategy = strategy; updateModel(); } private void updateBySelectionChange() { List<CommittedChangeList> selection = new ArrayList<CommittedChangeList>(); final TreePath[] selectionPaths = myChangesTree.getSelectionPaths(); if (selectionPaths != null) { for (TreePath path : selectionPaths) { DefaultMutableTreeNode node = (DefaultMutableTreeNode)path.getLastPathComponent(); if (node.getUserObject() instanceof CommittedChangeList) { selection.add((CommittedChangeList)node.getUserObject()); } } } if (!selection.equals(mySelectedChangeLists)) { mySelectedChangeLists = selection; myDetailsView.setChangesToDisplay(collectChanges(mySelectedChangeLists, false)); } } public static List<Change> collectChanges(final List<? extends CommittedChangeList> selectedChangeLists, final boolean withMovedTrees) { List<Change> result = new ArrayList<>(); Collections.sort(selectedChangeLists, new Comparator<CommittedChangeList>() { public int compare(final CommittedChangeList o1, final CommittedChangeList o2) { return o1.getCommitDate().compareTo(o2.getCommitDate()); } }); for (CommittedChangeList cl : selectedChangeLists) { final Collection<Change> changes = withMovedTrees ? cl.getChangesWithMovedTrees() : cl.getChanges(); for (Change c : changes) { addOrReplaceChange(result, c); } } return result; } /** * Zips changes by removing duplicates (changes in the same file) and compounding the diff. * <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. */ @NotNull public static List<Change> zipChanges(@NotNull List<Change> changes) { final List<Change> result = new ArrayList<Change>(); for (Change change : changes) { addOrReplaceChange(result, change); } return result; } private static void addOrReplaceChange(final List<Change> changes, final Change c) { final ContentRevision beforeRev = c.getBeforeRevision(); // todo!!! further improvements needed if (beforeRev != null) { final String beforeName = beforeRev.getFile().getName(); final String beforeAbsolutePath = beforeRev.getFile().getIOFile().getAbsolutePath(); for (Change oldChange : changes) { ContentRevision rev = oldChange.getAfterRevision(); // first compare name, which is many times faster - to remove 99% not matching if (rev != null && (rev.getFile().getName().equals(beforeName)) && rev.getFile().getIOFile().getAbsolutePath().equals(beforeAbsolutePath)) { changes.remove(oldChange); if (oldChange.getBeforeRevision() != null || c.getAfterRevision() != null) { changes.add(new Change(oldChange.getBeforeRevision(), c.getAfterRevision())); } return; } } } changes.add(c); } private List<CommittedChangeList> getSelectedChangeLists() { return TreeUtil.collectSelectedObjectsOfType(myChangesTree, CommittedChangeList.class); } public void setTableContextMenu(final ActionGroup group, final List<AnAction> auxiliaryActions) { DefaultActionGroup menuGroup = new DefaultActionGroup(); menuGroup.add(group); for (AnAction action : auxiliaryActions) { menuGroup.add(action); } menuGroup.add(ActionManager.getInstance().getAction(IdeActions.ACTION_COPY)); PopupHandler.installPopupHandler(myChangesTree, menuGroup, ActionPlaces.UNKNOWN, ActionManager.getInstance()); } public void removeFilteringStrategy(final CommittedChangesFilterKey key) { final ChangeListFilteringStrategy strategy = myFilteringStrategy.removeStrategy(key); if (strategy != null) { strategy.removeChangeListener(myFilterChangeListener); } myInnerSplitter.remove(key); } public boolean setFilteringStrategy(final ChangeListFilteringStrategy filteringStrategy) { if (myInnerSplitter.canAdd()) { filteringStrategy.addChangeListener(myFilterChangeListener); final CommittedChangesFilterKey key = filteringStrategy.getKey(); myFilteringStrategy.addStrategy(key, filteringStrategy); myFilteringStrategy.setFilterBase(myChangeLists); final JComponent filterUI = filteringStrategy.getFilterUI(); if (filterUI != null) { myInnerSplitter.add(key, filterUI); } return true; } return false; } public ActionToolbar createGroupFilterToolbar(final Project project, final ActionGroup leadGroup, @Nullable final ActionGroup tailGroup, final List<AnAction> extra) { DefaultActionGroup toolbarGroup = new DefaultActionGroup(); toolbarGroup.add(leadGroup); toolbarGroup.addSeparator(); toolbarGroup.add(new SelectFilteringAction(project, this)); toolbarGroup.add(new SelectGroupingAction(project, this)); final ExpandAllAction expandAllAction = new ExpandAllAction(myChangesTree); final CollapseAllAction collapseAllAction = new CollapseAllAction(myChangesTree); expandAllAction.registerCustomShortcutSet(new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_EXPAND_ALL)), myChangesTree); collapseAllAction .registerCustomShortcutSet(new CustomShortcutSet(KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_COLLAPSE_ALL)), myChangesTree); toolbarGroup.add(expandAllAction); toolbarGroup.add(collapseAllAction); toolbarGroup.add(ActionManager.getInstance().getAction(IdeActions.ACTION_COPY)); toolbarGroup.add(new ContextHelpAction(myHelpId)); if (tailGroup != null) { toolbarGroup.add(tailGroup); } for (AnAction anAction : extra) { toolbarGroup.add(anAction); } return ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, toolbarGroup, true); } public void calcData(DataKey key, DataSink sink) { if (key.equals(VcsDataKeys.CHANGES)) { final Collection<Change> changes = collectChanges(getSelectedChangeLists(), false); sink.put(VcsDataKeys.CHANGES, changes.toArray(new Change[changes.size()])); } else if (key.equals(VcsDataKeys.HAVE_SELECTED_CHANGES)) { final int count = myChangesTree.getSelectionCount(); sink.put(VcsDataKeys.HAVE_SELECTED_CHANGES, count > 0 ? Boolean.TRUE : Boolean.FALSE); } else if (key.equals(VcsDataKeys.CHANGES_WITH_MOVED_CHILDREN)) { final Collection<Change> changes = collectChanges(getSelectedChangeLists(), true); sink.put(VcsDataKeys.CHANGES_WITH_MOVED_CHILDREN, changes.toArray(new Change[changes.size()])); } else if (key.equals(VcsDataKeys.CHANGE_LISTS)) { final List<CommittedChangeList> lists = getSelectedChangeLists(); if (!lists.isEmpty()) { sink.put(VcsDataKeys.CHANGE_LISTS, lists.toArray(new CommittedChangeList[lists.size()])); } } else if (key.equals(CommonDataKeys.NAVIGATABLE_ARRAY)) { final Collection<Change> changes = collectChanges(getSelectedChangeLists(), false); Navigatable[] result = ChangesUtil.getNavigatableArray(myProject, ChangesUtil.getFilesFromChanges(changes)); sink.put(CommonDataKeys.NAVIGATABLE_ARRAY, result); } else if (key.equals(PlatformDataKeys.HELP_ID)) { sink.put(PlatformDataKeys.HELP_ID, myHelpId); } else if (VcsDataKeys.SELECTED_CHANGES_IN_DETAILS.equals(key)) { final List<Change> selectedChanges = myDetailsView.getSelectedChanges(); sink.put(VcsDataKeys.SELECTED_CHANGES_IN_DETAILS, selectedChanges.toArray(new Change[selectedChanges.size()])); } } public TreeExpander getTreeExpander() { return myTreeExpander; } public void repaintTree() { myChangesTree.revalidate(); myChangesTree.repaint(); } public void install(final CommittedChangeListDecorator decorator) { myDecorators.add(decorator); repaintTree(); } public void remove(final CommittedChangeListDecorator decorator) { myDecorators.remove(decorator); repaintTree(); } public void reportLoadedLists(final CommittedChangeListsListener listener) { ApplicationManager.getApplication().executeOnPooledThread(new Runnable() { public void run() { listener.onBeforeStartReport(); for (CommittedChangeList list : myChangeLists) { listener.report(list); } listener.onAfterEndReport(); } }); } // for appendable view public void reset() { myChangeLists.clear(); myFilteringStrategy.resetFilterBase(); myState = TreeState.createOn(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot()); updateModel(); } public void append(final List<CommittedChangeList> list) { final TreeState state = myChangeLists.isEmpty() && myState != null ? myState : TreeState.createOn(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot()); state.setScrollToSelection(false); myChangeLists.addAll(list); myFilteringStrategy.appendFilterBase(list); myChangesTree.setModel(buildTreeModel(myFilteringStrategy.filterChangeLists(myChangeLists))); state.applyTo(myChangesTree, (DefaultMutableTreeNode)myChangesTree.getModel().getRoot()); TreeUtil.expandAll(myChangesTree); myProject.getMessageBus().syncPublisher(ITEMS_RELOADED).itemsReloaded(); } public static class MoreLauncher implements Runnable { private final Project myProject; private final CommittedChangeList myList; MoreLauncher(final Project project, final CommittedChangeList list) { myProject = project; myList = list; } public void run() { ChangeListDetailsAction.showDetailsPopup(myProject, myList); } } private class FilterChangeListener implements ChangeListener { public void stateChanged(ChangeEvent e) { if (ApplicationManager.getApplication().isDispatchThread()) { updateModel(); } else { ApplicationManager.getApplication().invokeLater(new Runnable() { public void run() { updateModel(); } }); } } } private class ChangesBrowserTree extends Tree implements TypeSafeDataProvider { public ChangesBrowserTree() { super(buildTreeModel(myFilteringStrategy.filterChangeLists(myChangeLists))); } @Override public boolean getScrollableTracksViewportWidth() { return true; } public void calcData(final DataKey key, final DataSink sink) { if (key.equals(PlatformDataKeys.COPY_PROVIDER)) { sink.put(PlatformDataKeys.COPY_PROVIDER, myCopyProvider); } else if (key.equals(PlatformDataKeys.TREE_EXPANDER)) { sink.put(PlatformDataKeys.TREE_EXPANDER, myTreeExpander); } else { final String name = key.getName(); if (VcsDataKeys.SELECTED_CHANGES.is(name) || VcsDataKeys.CHANGE_LEAD_SELECTION.is(name) || CommittedChangesBrowserUseCase.DATA_KEY.is(name)) { final Object data = myDetailsView.getData(name); if (data != null) { sink.put(key, data); } } } } } public interface CommittedChangesReloadListener { void itemsReloaded(); void emptyRefresh(); } public void setLoading(final boolean value) { new AbstractCalledLater(myProject, ModalityState.NON_MODAL) { public void run() { myChangesTree.setPaintBusy(value); } }.callMe(); } }