/* * Copyright 2000-2017 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.openapi.vcs.changes; import com.intellij.diff.util.DiffPlaces; import com.intellij.diff.util.DiffUtil; import com.intellij.icons.AllIcons; import com.intellij.ide.CommonActionsManager; import com.intellij.ide.TreeExpander; import com.intellij.ide.actions.ContextHelpAction; import com.intellij.ide.dnd.DnDEvent; import com.intellij.lifecycle.PeriodicalTasksCloser; 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.components.*; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.SimpleToolWindowPanel; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Factory; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.ProjectLevelVcsManager; import com.intellij.openapi.vcs.VcsBundle; import com.intellij.openapi.vcs.VcsConfiguration; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.actions.IgnoredSettingsAction; import com.intellij.openapi.vcs.changes.actions.ShowDiffPreviewAction; import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager; import com.intellij.openapi.vcs.changes.ui.*; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.impl.DebugUtil; import com.intellij.ui.JBColor; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.content.Content; import com.intellij.util.Alarm; import com.intellij.util.FunctionUtil; import com.intellij.util.ui.JBUI; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.tree.TreeUtil; import com.intellij.util.xmlb.annotations.Attribute; import org.intellij.lang.annotations.JdkConstants; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.InputEvent; import java.awt.event.KeyEvent; import java.util.Collection; import java.util.List; import static com.intellij.util.ObjectUtils.assertNotNull; import static java.util.stream.Collectors.toList; @State( name = "ChangesViewManager", storages = @Storage(file = StoragePathMacros.WORKSPACE_FILE) ) public class ChangesViewManager implements ChangesViewI, ProjectComponent, PersistentStateComponent<ChangesViewManager.State> { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.ChangesViewManager"); public static final String CHANGES_VIEW_PREVIEW_SPLITTER_PROPORTION = "ChangesViewManager.DETAILS_SPLITTER_PROPORTION"; @NotNull private final ChangesListView myView; private JPanel myProgressLabel; private final Alarm myRepaintAlarm; private boolean myDisposed = false; @NotNull private final ChangeListListener myListener = new MyChangeListListener(); @NotNull private final Project myProject; @NotNull private final ChangesViewContentManager myContentManager; @NotNull private ChangesViewManager.State myState = new ChangesViewManager.State(); private PreviewDiffSplitterComponent mySplitterComponent; @NotNull private final TreeSelectionListener myTsl; private Content myContent; @NotNull public static ChangesViewI getInstance(@NotNull Project project) { return PeriodicalTasksCloser.getInstance().safeGetComponent(project, ChangesViewI.class); } public ChangesViewManager(@NotNull Project project, @NotNull ChangesViewContentManager contentManager) { myProject = project; myContentManager = contentManager; myView = new ChangesListView(project); myRepaintAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, project); myTsl = new TreeSelectionListener() { @Override public void valueChanged(TreeSelectionEvent e) { if (LOG.isDebugEnabled()) { TreePath[] paths = myView.getSelectionPaths(); String joinedPaths = paths != null ? StringUtil.join(paths, FunctionUtil.string(), ", ") : null; String message = "selection changed. selected: " + joinedPaths; if (LOG.isTraceEnabled()) { LOG.trace(message + " from: " + DebugUtil.currentStackTrace()); } else { LOG.debug(message); } } ApplicationManager.getApplication().invokeLater(() -> updatePreview()); } }; } @Override public void projectOpened() { final ChangeListManager changeListManager = ChangeListManager.getInstance(myProject); changeListManager.addChangeListListener(myListener); Disposer.register(myProject, new Disposable() { @Override public void dispose() { changeListManager.removeChangeListListener(myListener); } }); if (ApplicationManager.getApplication().isHeadlessEnvironment()) return; myContent = new MyChangeViewContent(createChangeViewComponent(), ChangesViewContentManager.LOCAL_CHANGES, false); myContent.setCloseable(false); myContentManager.addContent(myContent); scheduleRefresh(); myProject.getMessageBus().connect().subscribe(RemoteRevisionsCache.REMOTE_VERSION_CHANGED, () -> ApplicationManager.getApplication().invokeLater(() -> refreshView(), ModalityState.NON_MODAL, myProject.getDisposed())); updatePreview(); } @Override public void projectClosed() { myView.removeTreeSelectionListener(myTsl); myDisposed = true; myRepaintAlarm.cancelAllRequests(); } @Override @NonNls @NotNull public String getComponentName() { return "ChangesViewManager"; } private JComponent createChangeViewComponent() { SimpleToolWindowPanel panel = new SimpleToolWindowPanel(false, true); EmptyAction.registerWithShortcutSet("ChangesView.Refresh", CommonShortcuts.getRerun(), panel); EmptyAction.registerWithShortcutSet("ChangesView.NewChangeList", CommonShortcuts.getNew(), panel); EmptyAction.registerWithShortcutSet("ChangesView.RemoveChangeList", CommonShortcuts.getDelete(), panel); EmptyAction.registerWithShortcutSet(IdeActions.MOVE_TO_ANOTHER_CHANGE_LIST, CommonShortcuts.getMove(), panel); EmptyAction.registerWithShortcutSet("ChangesView.Rename",CommonShortcuts.getRename() , panel); EmptyAction.registerWithShortcutSet("ChangesView.SetDefault", new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_U, InputEvent.ALT_DOWN_MASK | ctrlMask())), panel); EmptyAction.registerWithShortcutSet("ChangesView.Diff", CommonShortcuts.getDiff(), panel); DefaultActionGroup group = (DefaultActionGroup)ActionManager.getInstance().getAction("ChangesViewToolbar"); ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR, group, false); toolbar.setTargetComponent(myView); JComponent toolbarComponent = toolbar.getComponent(); JPanel toolbarPanel = new JPanel(new BorderLayout()); toolbarPanel.add(toolbarComponent, BorderLayout.WEST); DefaultActionGroup visualActionsGroup = new DefaultActionGroup(); final Expander expander = new Expander(); visualActionsGroup.add(CommonActionsManager.getInstance().createExpandAllAction(expander, panel)); visualActionsGroup.add(CommonActionsManager.getInstance().createCollapseAllAction(expander, panel)); ToggleShowFlattenAction showFlattenAction = new ToggleShowFlattenAction(); showFlattenAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_P, ctrlMask())), panel); visualActionsGroup.add(showFlattenAction); visualActionsGroup.add(ActionManager.getInstance().getAction(IdeActions.ACTION_COPY)); visualActionsGroup.add(new ToggleShowIgnoredAction()); visualActionsGroup.add(new IgnoredSettingsAction()); visualActionsGroup.add(new ToggleDetailsAction()); visualActionsGroup.add(new ContextHelpAction(ChangesListView.HELP_ID)); toolbarPanel.add( ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR, visualActionsGroup, false).getComponent(), BorderLayout.CENTER); myView.setMenuActions((DefaultActionGroup)ActionManager.getInstance().getAction("ChangesViewPopupMenu")); myView.setShowFlatten(myState.myShowFlatten); myProgressLabel = new JPanel(new BorderLayout()); panel.setToolbar(toolbarPanel); final JPanel content = new JPanel(new BorderLayout()); final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myView); final JPanel wrapper = new JPanel(new BorderLayout()); wrapper.add(scrollPane, BorderLayout.CENTER); MyChangeProcessor changeProcessor = new MyChangeProcessor(myProject); mySplitterComponent = new PreviewDiffSplitterComponent(wrapper, changeProcessor, CHANGES_VIEW_PREVIEW_SPLITTER_PROPORTION, VcsConfiguration.getInstance(myProject).LOCAL_CHANGES_DETAILS_PREVIEW_SHOWN); content.add(mySplitterComponent, BorderLayout.CENTER); content.add(myProgressLabel, BorderLayout.SOUTH); panel.setContent(content); ChangesDnDSupport.install(myProject, myView); myView.addTreeSelectionListener(myTsl); return panel; } @JdkConstants.InputEventMask private static int ctrlMask() { return SystemInfo.isMac ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK; } private void updateProgressComponent(@NotNull final Factory<JComponent> progress) { //noinspection SSBasedInspection SwingUtilities.invokeLater(() -> { if (myProgressLabel != null) { myProgressLabel.removeAll(); myProgressLabel.add(progress.create()); myProgressLabel.setMinimumSize(JBUI.emptySize()); } }); } @Override public void updateProgressText(String text, boolean isError) { updateProgressComponent(createTextStatusFactory(text, isError)); } @Override public void setBusy(final boolean b) { UIUtil.invokeLaterIfNeeded(() -> myView.setPaintBusy(b)); } @NotNull public static Factory<JComponent> createTextStatusFactory(final String text, final boolean isError) { return () -> { JLabel label = new JLabel(text); label.setForeground(isError ? JBColor.RED : UIUtil.getLabelForeground()); return label; }; } @Override public void scheduleRefresh() { if (ApplicationManager.getApplication().isHeadlessEnvironment()) return; if (myProject.isDisposed()) return; int was = myRepaintAlarm.cancelAllRequests(); if (LOG.isDebugEnabled()) { LOG.debug("schedule refresh, was " + was); } if (!myRepaintAlarm.isDisposed()) { myRepaintAlarm.addRequest(() -> refreshView(), 100, ModalityState.NON_MODAL); } } private void refreshView() { if (myDisposed || !myProject.isInitialized() || ApplicationManager.getApplication().isUnitTestMode()) return; if (!ProjectLevelVcsManager.getInstance(myProject).hasActiveVcss()) return; ChangeListManagerImpl changeListManager = ChangeListManagerImpl.getInstanceImpl(myProject); TreeModelBuilder treeModelBuilder = new TreeModelBuilder(myProject, myView.isShowFlatten()) .setChangeLists(changeListManager.getChangeListsCopy()) .setLocallyDeletedPaths(changeListManager.getDeletedFiles()) .setModifiedWithoutEditing(changeListManager.getModifiedWithoutEditing()) .setSwitchedFiles(changeListManager.getSwitchedFilesMap()) .setSwitchedRoots(changeListManager.getSwitchedRoots()) .setLockedFolders(changeListManager.getLockedFolders()) .setLogicallyLockedFiles(changeListManager.getLogicallyLockedFolders()) .setUnversioned(changeListManager.getUnversionedFiles()); if (myState.myShowIgnored) { treeModelBuilder.setIgnored(changeListManager.getIgnoredFiles(), changeListManager.isIgnoredInUpdateMode()); } myView.updateModel( treeModelBuilder.build() ); updatePreview(); } private void updatePreview() { if (mySplitterComponent != null) { mySplitterComponent.updatePreview(); } } @NotNull @Override public ChangesViewManager.State getState() { return myState; } @Override public void loadState(@NotNull ChangesViewManager.State state) { myState = state; } @Override public void setShowFlattenMode(boolean state) { myState.myShowFlatten = state; myView.setShowFlatten(state); refreshView(); } @Override public void selectFile(@Nullable VirtualFile vFile) { if (vFile == null) return; Change change = ChangeListManager.getInstance(myProject).getChange(vFile); Object objectToFind = change != null ? change : vFile; DefaultMutableTreeNode root = (DefaultMutableTreeNode)myView.getModel().getRoot(); DefaultMutableTreeNode node = TreeUtil.findNodeWithObject(root, objectToFind); if (node != null) { TreeUtil.selectNode(myView, node); } } @Override public void refreshChangesViewNodeAsync(@NotNull final VirtualFile file) { ApplicationManager.getApplication().invokeLater(() -> refreshChangesViewNode(file), myProject.getDisposed()); } private void refreshChangesViewNode(@NotNull VirtualFile file) { ChangeListManager changeListManager = ChangeListManager.getInstance(myProject); Object userObject = changeListManager.isUnversioned(file) ? file : changeListManager.getChange(file); if (userObject != null) { DefaultMutableTreeNode root = (DefaultMutableTreeNode)myView.getModel().getRoot(); DefaultMutableTreeNode node = TreeUtil.findNodeWithObject(root, userObject); if (node != null) { myView.getModel().nodeChanged(node); } } } public static class State { @Attribute("flattened_view") public boolean myShowFlatten = true; @Attribute("show_ignored") public boolean myShowIgnored; } private class MyChangeListListener extends ChangeListAdapter { @Override public void changeListAdded(ChangeList list) { scheduleRefresh(); } @Override public void changeListRemoved(ChangeList list) { scheduleRefresh(); } @Override public void changeListRenamed(ChangeList list, String oldName) { scheduleRefresh(); } @Override public void changesMoved(Collection<Change> changes, ChangeList fromList, ChangeList toList) { scheduleRefresh(); } @Override public void defaultListChanged(final ChangeList oldDefaultList, ChangeList newDefaultList) { scheduleRefresh(); } @Override public void changeListUpdateDone() { scheduleRefresh(); ChangeListManagerImpl changeListManager = ChangeListManagerImpl.getInstanceImpl(myProject); VcsException updateException = changeListManager.getUpdateException(); setBusy(false); if (updateException == null) { Factory<JComponent> additionalUpdateInfo = changeListManager.getAdditionalUpdateInfo(); if (additionalUpdateInfo != null) { updateProgressComponent(additionalUpdateInfo); } else { updateProgressText("", false); } } else { updateProgressText(VcsBundle.message("error.updating.changes", updateException.getMessage()), true); } } } private class Expander implements TreeExpander { @Override public void expandAll() { TreeUtil.expandAll(myView); } @Override public boolean canExpand() { return true; } @Override public void collapseAll() { TreeUtil.collapseAll(myView, 2); TreeUtil.expand(myView, 1); } @Override public boolean canCollapse() { return true; } } private class ToggleShowFlattenAction extends ToggleAction implements DumbAware { public ToggleShowFlattenAction() { super(VcsBundle.message("changes.action.show.directories.text"), VcsBundle.message("changes.action.show.directories.description"), AllIcons.Actions.GroupByPackage); } @Override public boolean isSelected(AnActionEvent e) { return !myState.myShowFlatten; } @Override public void setSelected(AnActionEvent e, boolean state) { setShowFlattenMode(!state); } } private class ToggleShowIgnoredAction extends ToggleAction implements DumbAware { public ToggleShowIgnoredAction() { super(VcsBundle.message("changes.action.show.ignored.text"), VcsBundle.message("changes.action.show.ignored.description"), AllIcons.Actions.ShowHiddens); } @Override public boolean isSelected(AnActionEvent e) { return myState.myShowIgnored; } @Override public void setSelected(AnActionEvent e, boolean state) { myState.myShowIgnored = state; refreshView(); } } private class ToggleDetailsAction extends ShowDiffPreviewAction { @Override public void setSelected(AnActionEvent e, boolean state) { super.setSelected(e, state); assertNotNull(mySplitterComponent).setDetailsOn(state); VcsConfiguration.getInstance(myProject).LOCAL_CHANGES_DETAILS_PREVIEW_SHOWN = state; } } private class MyChangeProcessor extends ChangeViewDiffRequestProcessor { public MyChangeProcessor(@NotNull Project project) { super(project, DiffPlaces.CHANGES_VIEW); Disposer.register(project, this); } @Override public boolean isWindowFocused() { return DiffUtil.isFocusedComponent(myProject, myContent.getComponent()); } @NotNull @Override protected List<Change> getSelectedChanges() { List<Change> result = myView.getSelectedChanges().collect(toList()); if (result.isEmpty()) result = myView.getChanges().collect(toList()); return result; } @NotNull @Override protected List<Change> getAllChanges() { return myView.getChanges().collect(toList()); } @Override protected void selectChange(@NotNull Change change) { DefaultMutableTreeNode root = (DefaultMutableTreeNode)myView.getModel().getRoot(); DefaultMutableTreeNode node = TreeUtil.findNodeWithObject(root, change); if (node != null) { TreePath path = TreeUtil.getPathFromRoot(node); TreeUtil.selectPath(myView, path, false); } } } private class MyChangeViewContent extends DnDTargetContentAdapter { private MyChangeViewContent(JComponent component, String displayName, boolean isLockable) { super(component, displayName, isLockable); } @Override public void drop(DnDEvent event) { Object attachedObject = event.getAttachedObject(); if (attachedObject instanceof ShelvedChangeListDragBean) { FileDocumentManager.getInstance().saveAllDocuments(); ShelvedChangeListDragBean shelvedBean = (ShelvedChangeListDragBean)attachedObject; ShelveChangesManager.getInstance(myProject) .unshelveSilentlyAsynchronously(myProject, shelvedBean.getShelvedChangelists(), shelvedBean.getChanges(), shelvedBean.getBinaryFiles(), null); } } @Override public boolean update(DnDEvent event) { Object attachedObject = event.getAttachedObject(); if (attachedObject instanceof ShelvedChangeListDragBean) { ShelvedChangeListDragBean shelveBean = (ShelvedChangeListDragBean)attachedObject; event.setDropPossible(!shelveBean.getShelvedChangelists().isEmpty()); return false; } return true; } } }