Merge remote-tracking branch 'origin/master'
[idea/community.git] / platform / vcs-impl / src / com / intellij / openapi / vcs / changes / ChangesViewManager.java
1 /*
2  * Copyright 2000-2015 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.openapi.vcs.changes;
18
19 import com.intellij.diff.util.DiffPlaces;
20 import com.intellij.diff.util.DiffUtil;
21 import com.intellij.icons.AllIcons;
22 import com.intellij.ide.CommonActionsManager;
23 import com.intellij.ide.TreeExpander;
24 import com.intellij.ide.actions.ContextHelpAction;
25 import com.intellij.ide.dnd.DnDEvent;
26 import com.intellij.lifecycle.PeriodicalTasksCloser;
27 import com.intellij.openapi.Disposable;
28 import com.intellij.openapi.actionSystem.*;
29 import com.intellij.openapi.application.ApplicationManager;
30 import com.intellij.openapi.application.ModalityState;
31 import com.intellij.openapi.components.*;
32 import com.intellij.openapi.diagnostic.Logger;
33 import com.intellij.openapi.fileEditor.FileDocumentManager;
34 import com.intellij.openapi.project.DumbAware;
35 import com.intellij.openapi.project.Project;
36 import com.intellij.openapi.ui.SimpleToolWindowPanel;
37 import com.intellij.openapi.util.*;
38 import com.intellij.openapi.util.text.StringUtil;
39 import com.intellij.openapi.vcs.ProjectLevelVcsManager;
40 import com.intellij.openapi.vcs.VcsBundle;
41 import com.intellij.openapi.vcs.VcsConfiguration;
42 import com.intellij.openapi.vcs.VcsException;
43 import com.intellij.openapi.vcs.changes.actions.IgnoredSettingsAction;
44 import com.intellij.openapi.vcs.changes.shelf.ShelveChangesManager;
45 import com.intellij.openapi.vcs.changes.ui.*;
46 import com.intellij.openapi.vfs.VirtualFile;
47 import com.intellij.psi.impl.DebugUtil;
48 import com.intellij.ui.*;
49 import com.intellij.ui.content.Content;
50 import com.intellij.util.Alarm;
51 import com.intellij.util.FunctionUtil;
52 import com.intellij.util.ui.JBUI;
53 import com.intellij.util.ui.UIUtil;
54 import com.intellij.util.ui.tree.TreeUtil;
55 import com.intellij.util.xmlb.annotations.Attribute;
56 import org.intellij.lang.annotations.JdkConstants;
57 import org.jetbrains.annotations.NonNls;
58 import org.jetbrains.annotations.NotNull;
59 import org.jetbrains.annotations.Nullable;
60
61 import javax.swing.*;
62 import javax.swing.event.TreeSelectionEvent;
63 import javax.swing.event.TreeSelectionListener;
64 import javax.swing.tree.DefaultMutableTreeNode;
65 import javax.swing.tree.TreePath;
66 import java.awt.*;
67 import java.awt.event.InputEvent;
68 import java.awt.event.KeyEvent;
69 import java.util.Collection;
70 import java.util.List;
71
72 import static java.util.stream.Collectors.toList;
73
74 @State(
75   name = "ChangesViewManager",
76   storages = @Storage(file = StoragePathMacros.WORKSPACE_FILE)
77 )
78 public class ChangesViewManager implements ChangesViewI, ProjectComponent, PersistentStateComponent<ChangesViewManager.State> {
79
80   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.changes.ChangesViewManager");
81
82   @NotNull private final ChangesListView myView;
83   private JPanel myProgressLabel;
84
85   private final Alarm myRepaintAlarm;
86
87   private boolean myDisposed = false;
88
89   @NotNull private final ChangeListListener myListener = new MyChangeListListener();
90   @NotNull private final Project myProject;
91   @NotNull private final ChangesViewContentManager myContentManager;
92
93   @NotNull private ChangesViewManager.State myState = new ChangesViewManager.State();
94
95   private JBSplitter mySplitter;
96
97   private boolean myDetailsOn;
98   @NotNull private final NotNullLazyValue<MyChangeProcessor> myDiffDetails = new NotNullLazyValue<MyChangeProcessor>() {
99     @NotNull
100     @Override
101     protected MyChangeProcessor compute() {
102       return new MyChangeProcessor(myProject);
103     }
104   };
105
106   @NotNull private final TreeSelectionListener myTsl;
107   private Content myContent;
108
109   @NotNull
110   public static ChangesViewI getInstance(@NotNull Project project) {
111     return PeriodicalTasksCloser.getInstance().safeGetComponent(project, ChangesViewI.class);
112   }
113
114   public ChangesViewManager(@NotNull Project project, @NotNull ChangesViewContentManager contentManager) {
115     myProject = project;
116     myContentManager = contentManager;
117     myView = new ChangesListView(project);
118     myRepaintAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, project);
119     myTsl = new TreeSelectionListener() {
120       @Override
121       public void valueChanged(TreeSelectionEvent e) {
122         if (LOG.isDebugEnabled()) {
123           TreePath[] paths = myView.getSelectionPaths();
124           String joinedPaths = paths != null ? StringUtil.join(paths, FunctionUtil.string(), ", ") : null;
125           String message = "selection changed. selected:  " + joinedPaths;
126
127           if (LOG.isTraceEnabled()) {
128             LOG.trace(message + " from: " + DebugUtil.currentStackTrace());
129           }
130           else {
131             LOG.debug(message);
132           }
133         }
134         SwingUtilities.invokeLater(new Runnable() {
135           @Override
136           public void run() {
137             changeDetails();
138           }
139         });
140       }
141     };
142   }
143
144   public void projectOpened() {
145     final ChangeListManager changeListManager = ChangeListManager.getInstance(myProject);
146     changeListManager.addChangeListListener(myListener);
147     Disposer.register(myProject, new Disposable() {
148       public void dispose() {
149         changeListManager.removeChangeListListener(myListener);
150       }
151     });
152     if (ApplicationManager.getApplication().isHeadlessEnvironment()) return;
153     myContent = new MyChangeViewContent(createChangeViewComponent(), ChangesViewContentManager.LOCAL_CHANGES, false);
154     myContent.setCloseable(false);
155     myContentManager.addContent(myContent);
156
157     scheduleRefresh();
158     myProject.getMessageBus().connect().subscribe(RemoteRevisionsCache.REMOTE_VERSION_CHANGED, new Runnable() {
159       public void run() {
160         ApplicationManager.getApplication().invokeLater(new Runnable() {
161           public void run() {
162             refreshView();
163           }
164         }, ModalityState.NON_MODAL, myProject.getDisposed());
165       }
166     });
167
168     myDetailsOn = VcsConfiguration.getInstance(myProject).LOCAL_CHANGES_DETAILS_PREVIEW_SHOWN;
169     changeDetails();
170   }
171
172   public void projectClosed() {
173     myView.removeTreeSelectionListener(myTsl);
174     myDisposed = true;
175     myRepaintAlarm.cancelAllRequests();
176   }
177
178   @NonNls @NotNull
179   public String getComponentName() {
180     return "ChangesViewManager";
181   }
182
183   private JComponent createChangeViewComponent() {
184     SimpleToolWindowPanel panel = new SimpleToolWindowPanel(false, true);
185
186     EmptyAction.registerWithShortcutSet("ChangesView.Refresh", CommonShortcuts.getRerun(), panel);
187     EmptyAction.registerWithShortcutSet("ChangesView.NewChangeList", CommonShortcuts.getNew(), panel);
188     EmptyAction.registerWithShortcutSet("ChangesView.RemoveChangeList", CommonShortcuts.getDelete(), panel);
189     EmptyAction.registerWithShortcutSet(IdeActions.MOVE_TO_ANOTHER_CHANGE_LIST, CommonShortcuts.getMove(), panel);
190     EmptyAction.registerWithShortcutSet("ChangesView.Rename",CommonShortcuts.getRename() , panel);
191     EmptyAction.registerWithShortcutSet("ChangesView.SetDefault", new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_U, InputEvent.ALT_DOWN_MASK | ctrlMask())), panel);
192     EmptyAction.registerWithShortcutSet("ChangesView.Diff", CommonShortcuts.getDiff(), panel);
193
194     DefaultActionGroup group = (DefaultActionGroup)ActionManager.getInstance().getAction("ChangesViewToolbar");
195     ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR, group, false);
196     toolbar.setTargetComponent(myView);
197     JComponent toolbarComponent = toolbar.getComponent();
198     JPanel toolbarPanel = new JPanel(new BorderLayout());
199     toolbarPanel.add(toolbarComponent, BorderLayout.WEST);
200
201     DefaultActionGroup visualActionsGroup = new DefaultActionGroup();
202     final Expander expander = new Expander();
203     visualActionsGroup.add(CommonActionsManager.getInstance().createExpandAllAction(expander, panel));
204     visualActionsGroup.add(CommonActionsManager.getInstance().createCollapseAllAction(expander, panel));
205
206     ToggleShowFlattenAction showFlattenAction = new ToggleShowFlattenAction();
207     showFlattenAction.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_P, ctrlMask())), panel);
208     visualActionsGroup.add(showFlattenAction);
209     visualActionsGroup.add(ActionManager.getInstance().getAction(IdeActions.ACTION_COPY));
210     visualActionsGroup.add(new ToggleShowIgnoredAction());
211     visualActionsGroup.add(new IgnoredSettingsAction());
212     visualActionsGroup.add(new ToggleDetailsAction());
213     visualActionsGroup.add(new ContextHelpAction(ChangesListView.ourHelpId));
214     toolbarPanel.add(
215       ActionManager.getInstance().createActionToolbar(ActionPlaces.CHANGES_VIEW_TOOLBAR, visualActionsGroup, false).getComponent(), BorderLayout.CENTER);
216
217
218     myView.setMenuActions((DefaultActionGroup)ActionManager.getInstance().getAction("ChangesViewPopupMenu"));
219
220     myView.setShowFlatten(myState.myShowFlatten);
221
222     myProgressLabel = new JPanel(new BorderLayout());
223
224     panel.setToolbar(toolbarPanel);
225
226     final JPanel content = new JPanel(new BorderLayout());
227     mySplitter = new JBSplitter(false, "ChangesViewManager.DETAILS_SPLITTER_PROPORTION", 0.5f);
228     mySplitter.setHonorComponentsMinimumSize(false);
229     final JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myView);
230     final JPanel wrapper = new JPanel(new BorderLayout());
231     wrapper.add(scrollPane, BorderLayout.CENTER);
232     mySplitter.setFirstComponent(wrapper);
233     content.add(mySplitter, BorderLayout.CENTER);
234     content.add(myProgressLabel, BorderLayout.SOUTH);
235     panel.setContent(content);
236
237     ChangesDnDSupport.install(myProject, myView);
238     myView.addTreeSelectionListener(myTsl);
239     return panel;
240   }
241
242   private void changeDetails() {
243     if (!myDetailsOn) {
244       if (myDiffDetails.isComputed()) {
245         myDiffDetails.getValue().clear();
246
247         if (mySplitter.getSecondComponent() != null) {
248           setChangeDetailsPanel(null);
249         }
250       }
251     }
252     else {
253       myDiffDetails.getValue().refresh();
254
255       if (mySplitter.getSecondComponent() == null) {
256         setChangeDetailsPanel(myDiffDetails.getValue().getComponent());
257       }
258     }
259   }
260
261   private void setChangeDetailsPanel(@Nullable JComponent component) {
262     mySplitter.setSecondComponent(component);
263     mySplitter.getFirstComponent().setBorder(component == null ? null : IdeBorderFactory.createBorder(SideBorder.RIGHT));
264     mySplitter.revalidate();
265     mySplitter.repaint();
266   }
267
268   @JdkConstants.InputEventMask
269   private static int ctrlMask() {
270     return SystemInfo.isMac ? InputEvent.META_DOWN_MASK : InputEvent.CTRL_DOWN_MASK;
271   }
272
273   private void updateProgressComponent(@NotNull final Factory<JComponent> progress) {
274     //noinspection SSBasedInspection
275     SwingUtilities.invokeLater(new Runnable() {
276       public void run() {
277         if (myProgressLabel != null) {
278           myProgressLabel.removeAll();
279           myProgressLabel.add(progress.create());
280           myProgressLabel.setMinimumSize(JBUI.emptySize());
281         }
282       }
283     });
284   }
285
286   public void updateProgressText(String text, boolean isError) {
287     updateProgressComponent(createTextStatusFactory(text, isError));
288   }
289
290   @Override
291   public void setBusy(final boolean b) {
292     UIUtil.invokeLaterIfNeeded(new Runnable() {
293       @Override
294       public void run() {
295         myView.setPaintBusy(b);
296       }
297     });
298   }
299
300   @NotNull
301   public static Factory<JComponent> createTextStatusFactory(final String text, final boolean isError) {
302     return new Factory<JComponent>() {
303       @Override
304       public JComponent create() {
305         JLabel label = new JLabel(text);
306         label.setForeground(isError ? JBColor.RED : UIUtil.getLabelForeground());
307         return label;
308       }
309     };
310   }
311
312   @Override
313   public void scheduleRefresh() {
314     if (ApplicationManager.getApplication().isHeadlessEnvironment()) return;
315     if (myProject.isDisposed()) return;
316     int was = myRepaintAlarm.cancelAllRequests();
317     if (LOG.isDebugEnabled()) {
318       LOG.debug("schedule refresh, was " + was);
319     }
320     if (!myRepaintAlarm.isDisposed()) {
321       myRepaintAlarm.addRequest(new Runnable() {
322         public void run() {
323           refreshView();
324         }
325       }, 100, ModalityState.NON_MODAL);
326     }
327   }
328
329   private void refreshView() {
330     if (myDisposed || !myProject.isInitialized() || ApplicationManager.getApplication().isUnitTestMode()) return;
331     if (!ProjectLevelVcsManager.getInstance(myProject).hasActiveVcss()) return;
332
333     ChangeListManagerImpl changeListManager = ChangeListManagerImpl.getInstanceImpl(myProject);
334
335     myView.updateModel(
336       new TreeModelBuilder(myProject, myView.isShowFlatten())
337         .set(changeListManager.getChangeListsCopy(), changeListManager.getDeletedFiles(), changeListManager.getModifiedWithoutEditing(),
338              changeListManager.getSwitchedFilesMap(), changeListManager.getSwitchedRoots(),
339              myState.myShowIgnored ? changeListManager.getIgnoredFiles() : null, changeListManager.getLockedFolders(),
340              changeListManager.getLogicallyLockedFolders())
341         .setUnversioned(changeListManager.getUnversionedFiles(), changeListManager.getUnversionedFilesSize())
342         .build()
343     );
344
345     changeDetails();
346   }
347
348   @NotNull
349   @Override
350   public ChangesViewManager.State getState() {
351     return myState;
352   }
353
354   @Override
355   public void loadState(@NotNull ChangesViewManager.State state) {
356     myState = state;
357   }
358
359   @Override
360   public void setShowFlattenMode(boolean state) {
361     myState.myShowFlatten = state;
362     myView.setShowFlatten(state);
363     refreshView();
364   }
365
366   @Override
367   public void selectFile(@Nullable VirtualFile vFile) {
368     if (vFile == null) return;
369     Change change = ChangeListManager.getInstance(myProject).getChange(vFile);
370     Object objectToFind = change != null ? change : vFile;
371
372     DefaultMutableTreeNode root = (DefaultMutableTreeNode)myView.getModel().getRoot();
373     DefaultMutableTreeNode node = TreeUtil.findNodeWithObject(root, objectToFind);
374     if (node != null) {
375       TreeUtil.selectNode(myView, node);
376     }
377   }
378
379   @Override
380   public void refreshChangesViewNodeAsync(@NotNull final VirtualFile file) {
381     ApplicationManager.getApplication().invokeLater(new Runnable() {
382       public void run() {
383         refreshChangesViewNode(file);
384       }
385     }, myProject.getDisposed());
386   }
387
388   private void refreshChangesViewNode(@NotNull VirtualFile file) {
389     ChangeListManager changeListManager = ChangeListManager.getInstance(myProject);
390     Object userObject = changeListManager.isUnversioned(file) ? file : changeListManager.getChange(file);
391
392     if (userObject != null) {
393       DefaultMutableTreeNode root = (DefaultMutableTreeNode)myView.getModel().getRoot();
394       DefaultMutableTreeNode node = TreeUtil.findNodeWithObject(root, userObject);
395
396       if (node != null) {
397         myView.getModel().nodeChanged(node);
398       }
399     }
400   }
401
402   public static class State {
403
404     @Attribute("flattened_view")
405     public boolean myShowFlatten = true;
406
407     @Attribute("show_ignored")
408     public boolean myShowIgnored;
409   }
410
411   private class MyChangeListListener extends ChangeListAdapter {
412
413     public void changeListAdded(ChangeList list) {
414       scheduleRefresh();
415     }
416
417     public void changeListRemoved(ChangeList list) {
418       scheduleRefresh();
419     }
420
421     public void changeListRenamed(ChangeList list, String oldName) {
422       scheduleRefresh();
423     }
424
425     public void changesMoved(Collection<Change> changes, ChangeList fromList, ChangeList toList) {
426       scheduleRefresh();
427     }
428
429     public void defaultListChanged(final ChangeList oldDefaultList, ChangeList newDefaultList) {
430       scheduleRefresh();
431     }
432
433     public void changeListUpdateDone() {
434       scheduleRefresh();
435       ChangeListManagerImpl changeListManager = ChangeListManagerImpl.getInstanceImpl(myProject);
436       VcsException updateException = changeListManager.getUpdateException();
437       setBusy(false);
438       if (updateException == null) {
439         Factory<JComponent> additionalUpdateInfo = changeListManager.getAdditionalUpdateInfo();
440
441         if (additionalUpdateInfo != null) {
442           updateProgressComponent(additionalUpdateInfo);
443         }
444         else {
445           updateProgressText("", false);
446         }
447       }
448       else {
449         updateProgressText(VcsBundle.message("error.updating.changes", updateException.getMessage()), true);
450       }
451     }
452   }
453
454   private class Expander implements TreeExpander {
455     public void expandAll() {
456       TreeUtil.expandAll(myView);
457     }
458
459     public boolean canExpand() {
460       return true;
461     }
462
463     public void collapseAll() {
464       TreeUtil.collapseAll(myView, 2);
465       TreeUtil.expand(myView, 1);
466     }
467
468     public boolean canCollapse() {
469       return true;
470     }
471   }
472
473   private class ToggleShowFlattenAction extends ToggleAction implements DumbAware {
474     public ToggleShowFlattenAction() {
475       super(VcsBundle.message("changes.action.show.directories.text"),
476             VcsBundle.message("changes.action.show.directories.description"),
477             AllIcons.Actions.GroupByPackage);
478     }
479
480     public boolean isSelected(AnActionEvent e) {
481       return !myState.myShowFlatten;
482     }
483
484     public void setSelected(AnActionEvent e, boolean state) {
485       setShowFlattenMode(!state);
486     }
487   }
488
489   private class ToggleShowIgnoredAction extends ToggleAction implements DumbAware {
490     public ToggleShowIgnoredAction() {
491       super(VcsBundle.message("changes.action.show.ignored.text"),
492             VcsBundle.message("changes.action.show.ignored.description"),
493             AllIcons.Actions.ShowHiddens);
494     }
495
496     public boolean isSelected(AnActionEvent e) {
497       return myState.myShowIgnored;
498     }
499
500     public void setSelected(AnActionEvent e, boolean state) {
501       myState.myShowIgnored = state;
502       refreshView();
503     }
504   }
505
506   @Override
507   public void disposeComponent() {
508   }
509
510   @Override
511   public void initComponent() {
512   }
513
514   private class ToggleDetailsAction extends ToggleAction implements DumbAware {
515     private ToggleDetailsAction() {
516       super("Preview Diff", null, AllIcons.Actions.PreviewDetails);
517     }
518
519     @Override
520     public boolean isSelected(AnActionEvent e) {
521       return myDetailsOn;
522     }
523
524     @Override
525     public void setSelected(AnActionEvent e, boolean state) {
526       myDetailsOn = state;
527       VcsConfiguration.getInstance(myProject).LOCAL_CHANGES_DETAILS_PREVIEW_SHOWN = myDetailsOn;
528       changeDetails();
529     }
530   }
531
532   private class MyChangeProcessor extends CacheChangeProcessor {
533     public MyChangeProcessor(@NotNull Project project) {
534       super(project, DiffPlaces.CHANGES_VIEW);
535       Disposer.register(project, this);
536     }
537
538     @Override
539     public boolean isWindowFocused() {
540       return DiffUtil.isFocusedComponent(myProject, myContent.getComponent());
541     }
542
543     @NotNull
544     @Override
545     protected List<Change> getSelectedChanges() {
546       List<Change> result = myView.getSelectedChanges().collect(toList());
547       if (result.isEmpty()) result = myView.getChanges().collect(toList());
548       return result;
549     }
550
551     @NotNull
552     @Override
553     protected List<Change> getAllChanges() {
554       return myView.getChanges().collect(toList());
555     }
556
557     @Override
558     protected void selectChange(@NotNull Change change) {
559       DefaultMutableTreeNode root = (DefaultMutableTreeNode)myView.getModel().getRoot();
560       DefaultMutableTreeNode node = TreeUtil.findNodeWithObject(root, change);
561       if (node != null) {
562         TreePath path = TreeUtil.getPathFromRoot(node);
563         TreeUtil.selectPath(myView, path, false);
564       }
565     }
566   }
567
568   private class MyChangeViewContent extends DnDTargetContentAdapter {
569     private MyChangeViewContent(JComponent component, String displayName, boolean isLockable) {
570       super(component, displayName, isLockable);
571     }
572
573     @Override
574     public void drop(DnDEvent event) {
575       Object attachedObject = event.getAttachedObject();
576       if (attachedObject instanceof ShelvedChangeListDragBean) {
577         FileDocumentManager.getInstance().saveAllDocuments();
578         ShelvedChangeListDragBean shelvedBean = (ShelvedChangeListDragBean)attachedObject;
579         ShelveChangesManager.getInstance(myProject)
580           .unshelveSilentlyAsynchronously(myProject, shelvedBean.getShelvedChangelists(), shelvedBean.getChanges(),
581                                           shelvedBean.getBinaryFiles(), null);
582       }
583     }
584
585     @Override
586     public boolean update(DnDEvent event) {
587       Object attachedObject = event.getAttachedObject();
588       if (attachedObject instanceof ShelvedChangeListDragBean) {
589         ShelvedChangeListDragBean shelveBean = (ShelvedChangeListDragBean)attachedObject;
590         event.setDropPossible(!shelveBean.getShelvedChangelists().isEmpty());
591         return false;
592       }
593       return true;
594     }
595   }
596 }