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