Code cleanup
[idea/community.git] / platform / lang-impl / src / com / intellij / execution / ui / RunContentManagerImpl.java
1 /*
2  * Copyright 2000-2014 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 package com.intellij.execution.ui;
17
18 import com.intellij.execution.*;
19 import com.intellij.execution.process.ProcessAdapter;
20 import com.intellij.execution.process.ProcessEvent;
21 import com.intellij.execution.process.ProcessHandler;
22 import com.intellij.execution.runners.ExecutionEnvironment;
23 import com.intellij.execution.runners.GenericProgramRunner;
24 import com.intellij.execution.ui.layout.impl.DockableGridContainerFactory;
25 import com.intellij.ide.DataManager;
26 import com.intellij.ide.impl.ContentManagerWatcher;
27 import com.intellij.openapi.Disposable;
28 import com.intellij.openapi.actionSystem.DataContext;
29 import com.intellij.openapi.actionSystem.DataProvider;
30 import com.intellij.openapi.actionSystem.PlatformDataKeys;
31 import com.intellij.openapi.application.ApplicationManager;
32 import com.intellij.openapi.diagnostic.Logger;
33 import com.intellij.openapi.progress.ProgressIndicator;
34 import com.intellij.openapi.progress.ProgressManager;
35 import com.intellij.openapi.progress.Task;
36 import com.intellij.openapi.project.Project;
37 import com.intellij.openapi.project.ProjectManager;
38 import com.intellij.openapi.project.ProjectManagerListener;
39 import com.intellij.openapi.ui.DialogWrapper;
40 import com.intellij.openapi.util.Comparing;
41 import com.intellij.openapi.util.Disposer;
42 import com.intellij.openapi.util.IconLoader;
43 import com.intellij.openapi.util.Key;
44 import com.intellij.openapi.wm.ToolWindow;
45 import com.intellij.openapi.wm.ToolWindowAnchor;
46 import com.intellij.openapi.wm.ToolWindowManager;
47 import com.intellij.openapi.wm.ex.ToolWindowManagerAdapter;
48 import com.intellij.openapi.wm.ex.ToolWindowManagerEx;
49 import com.intellij.ui.AppUIUtil;
50 import com.intellij.ui.ColorUtil;
51 import com.intellij.ui.LayeredIcon;
52 import com.intellij.ui.content.*;
53 import com.intellij.ui.docking.DockManager;
54 import com.intellij.util.SmartList;
55 import com.intellij.util.concurrency.Semaphore;
56 import com.intellij.util.containers.ContainerUtil;
57 import com.intellij.util.ui.GraphicsUtil;
58 import com.intellij.util.ui.UIUtil;
59 import gnu.trove.THashMap;
60 import gnu.trove.THashSet;
61 import org.jetbrains.annotations.NotNull;
62 import org.jetbrains.annotations.Nullable;
63
64 import javax.swing.*;
65 import java.awt.*;
66 import java.awt.geom.Ellipse2D;
67 import java.util.*;
68 import java.util.List;
69
70 public class RunContentManagerImpl implements RunContentManager, Disposable {
71   public static final Key<Boolean> ALWAYS_USE_DEFAULT_STOPPING_BEHAVIOUR_KEY = Key.create("ALWAYS_USE_DEFAULT_STOPPING_BEHAVIOUR_KEY");
72   private static final Logger LOG = Logger.getInstance(RunContentManagerImpl.class);
73   private static final Key<RunContentDescriptor> DESCRIPTOR_KEY = Key.create("Descriptor");
74
75   private final Project myProject;
76   private final Map<String, ContentManager> myToolwindowIdToContentManagerMap = new THashMap<String, ContentManager>();
77   private final Map<String, Icon> myToolwindowIdToBaseIconMap = new THashMap<String, Icon>();
78
79   private final Map<RunContentListener, Disposable> myListeners = new THashMap<RunContentListener, Disposable>();
80   private final LinkedList<String> myToolwindowIdZBuffer = new LinkedList<String>();
81
82   public RunContentManagerImpl(@NotNull Project project, @NotNull DockManager dockManager) {
83     myProject = project;
84     DockableGridContainerFactory containerFactory = new DockableGridContainerFactory();
85     dockManager.register(DockableGridContainerFactory.TYPE, containerFactory);
86     Disposer.register(myProject, containerFactory);
87
88     AppUIUtil.invokeOnEdt(new Runnable() {
89       @Override
90       public void run() {
91         init();
92       }
93     }, myProject.getDisposed());
94   }
95
96   // must be called on EDT
97   private void init() {
98     ToolWindowManagerEx toolWindowManager = ToolWindowManagerEx.getInstanceEx(myProject);
99     if (toolWindowManager == null) {
100       return;
101     }
102
103     for (Executor executor : ExecutorRegistry.getInstance().getRegisteredExecutors()) {
104       registerToolwindow(executor, toolWindowManager);
105     }
106
107     toolWindowManager.addToolWindowManagerListener(new ToolWindowManagerAdapter() {
108       @Override
109       public void stateChanged() {
110         if (myProject.isDisposed()) {
111           return;
112         }
113
114         ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(myProject);
115         Set<String> currentWindows = new THashSet<String>();
116         ContainerUtil.addAll(currentWindows, toolWindowManager.getToolWindowIds());
117         myToolwindowIdZBuffer.retainAll(currentWindows);
118
119         final String activeToolWindowId = toolWindowManager.getActiveToolWindowId();
120         if (activeToolWindowId != null) {
121           if (myToolwindowIdZBuffer.remove(activeToolWindowId)) {
122             myToolwindowIdZBuffer.addFirst(activeToolWindowId);
123           }
124         }
125       }
126     });
127   }
128
129   @Override
130   public void dispose() {
131   }
132
133   private void registerToolwindow(@NotNull final Executor executor, @NotNull ToolWindowManagerEx toolWindowManager) {
134     final String toolWindowId = executor.getToolWindowId();
135     if (toolWindowManager.getToolWindow(toolWindowId) != null) {
136       return;
137     }
138
139     final ToolWindow toolWindow = toolWindowManager.registerToolWindow(toolWindowId, true, ToolWindowAnchor.BOTTOM, this, true);
140     final ContentManager contentManager = toolWindow.getContentManager();
141     contentManager.addDataProvider(new DataProvider() {
142       private int myInsideGetData = 0;
143
144       @Override
145       public Object getData(String dataId) {
146         myInsideGetData++;
147         try {
148           if (PlatformDataKeys.HELP_ID.is(dataId)) {
149             return executor.getHelpId();
150           }
151           else {
152             return myInsideGetData == 1 ? DataManager.getInstance().getDataContext(contentManager.getComponent()).getData(dataId) : null;
153           }
154         }
155         finally {
156           myInsideGetData--;
157         }
158       }
159     });
160
161     toolWindow.setIcon(executor.getToolWindowIcon());
162     myToolwindowIdToBaseIconMap.put(toolWindowId, executor.getToolWindowIcon());
163     new ContentManagerWatcher(toolWindow, contentManager);
164     contentManager.addContentManagerListener(new ContentManagerAdapter() {
165       @Override
166       public void selectionChanged(final ContentManagerEvent event) {
167         Content content = event.getContent();
168         getSyncPublisher().contentSelected(content == null ? null : getRunContentDescriptorByContent(content), executor);
169       }
170     });
171     myToolwindowIdToContentManagerMap.put(toolWindowId, contentManager);
172     Disposer.register(contentManager, new Disposable() {
173       @Override
174       public void dispose() {
175         myToolwindowIdToContentManagerMap.remove(toolWindowId).removeAllContents(true);
176         myToolwindowIdZBuffer.remove(toolWindowId);
177         myToolwindowIdToBaseIconMap.remove(toolWindowId);
178       }
179     });
180     myToolwindowIdZBuffer.addLast(toolWindowId);
181   }
182
183   private RunContentWithExecutorListener getSyncPublisher() {
184     return myProject.getMessageBus().syncPublisher(TOPIC);
185   }
186
187   @Override
188   public void toFrontRunContent(final Executor requestor, final ProcessHandler handler) {
189     final RunContentDescriptor descriptor = getDescriptorBy(handler, requestor);
190     if (descriptor == null) {
191       return;
192     }
193     toFrontRunContent(requestor, descriptor);
194   }
195
196   @Override
197   public void toFrontRunContent(final Executor requestor, final RunContentDescriptor descriptor) {
198     ApplicationManager.getApplication().invokeLater(new Runnable() {
199       @Override
200       public void run() {
201         ContentManager contentManager = getContentManagerForRunner(requestor);
202         Content content = getRunContentByDescriptor(contentManager, descriptor);
203         if (content != null) {
204           contentManager.setSelectedContent(content);
205           ToolWindowManager.getInstance(myProject).getToolWindow(requestor.getToolWindowId()).show(null);
206         }
207       }
208     }, myProject.getDisposed());
209   }
210
211   @Override
212   public void hideRunContent(@NotNull final Executor executor, final RunContentDescriptor descriptor) {
213     ApplicationManager.getApplication().invokeLater(new Runnable() {
214       @Override
215       public void run() {
216         ToolWindow toolWindow = ToolWindowManager.getInstance(myProject).getToolWindow(executor.getToolWindowId());
217         if (toolWindow != null) {
218           toolWindow.hide(null);
219         }
220       }
221     }, myProject.getDisposed());
222   }
223
224   @Override
225   @Nullable
226   public RunContentDescriptor getSelectedContent(final Executor executor) {
227     final Content selectedContent = getContentManagerForRunner(executor).getSelectedContent();
228     return selectedContent != null ? getRunContentDescriptorByContent(selectedContent) : null;
229   }
230
231   @Override
232   @Nullable
233   public RunContentDescriptor getSelectedContent() {
234     for (String activeWindow : myToolwindowIdZBuffer) {
235       final ContentManager contentManager = myToolwindowIdToContentManagerMap.get(activeWindow);
236       if (contentManager == null) {
237         continue;
238       }
239
240       final Content selectedContent = contentManager.getSelectedContent();
241       if (selectedContent == null) {
242         if (contentManager.getContentCount() == 0) {
243           // continue to the next window if the content manager is empty
244           continue;
245         }
246         else {
247           // stop iteration over windows because there is some content in the window and the window is the last used one
248           break;
249         }
250       }
251       // here we have selected content
252       return getRunContentDescriptorByContent(selectedContent);
253     }
254
255     return null;
256   }
257
258   @Override
259   public boolean removeRunContent(@NotNull final Executor executor, final RunContentDescriptor descriptor) {
260     final ContentManager contentManager = getContentManagerForRunner(executor);
261     final Content content = getRunContentByDescriptor(contentManager, descriptor);
262     return content != null && contentManager.removeContent(content, true);
263   }
264
265   @Override
266   public void showRunContent(@NotNull Executor executor, @NotNull RunContentDescriptor descriptor) {
267     showRunContent(executor, descriptor, descriptor.getExecutionId());
268   }
269
270   public void showRunContent(@NotNull final Executor executor, @NotNull final RunContentDescriptor descriptor, final long executionId) {
271     if (ApplicationManager.getApplication().isUnitTestMode()) {
272       return;
273     }
274
275     final ContentManager contentManager = getContentManagerForRunner(executor);
276     RunContentDescriptor oldDescriptor = chooseReuseContentForDescriptor(contentManager, descriptor, executionId, descriptor.getDisplayName());
277     final Content content;
278     if (oldDescriptor == null) {
279       content = createNewContent(contentManager, descriptor, executor);
280       Icon icon = descriptor.getIcon();
281       content.setIcon(icon == null ? executor.getToolWindowIcon() : icon);
282     }
283     else {
284       content = oldDescriptor.getAttachedContent();
285       LOG.assertTrue(content != null);
286       getSyncPublisher().contentRemoved(oldDescriptor, executor);
287       Disposer.dispose(oldDescriptor); // is of the same category, can be reused
288     }
289
290     content.setExecutionId(executionId);
291     content.setComponent(descriptor.getComponent());
292     content.setPreferredFocusedComponent(descriptor.getPreferredFocusComputable());
293     content.putUserData(DESCRIPTOR_KEY, descriptor);
294     final ToolWindow toolWindow = ToolWindowManager.getInstance(myProject).getToolWindow(executor.getToolWindowId());
295     final ProcessHandler processHandler = descriptor.getProcessHandler();
296     if (processHandler != null) {
297       final ProcessAdapter processAdapter = new ProcessAdapter() {
298         @Override
299         public void startNotified(final ProcessEvent event) {
300           UIUtil.invokeLaterIfNeeded(new Runnable() {
301             @Override
302             public void run() {
303               toolWindow.setIcon(getLiveIndicator(myToolwindowIdToBaseIconMap.get(executor.getToolWindowId())));
304             }
305           });
306         }
307
308         @Override
309         public void processTerminated(final ProcessEvent event) {
310           ApplicationManager.getApplication().invokeLater(new Runnable() {
311             @Override
312             public void run() {
313               boolean alive = false;
314               String toolWindowId = executor.getToolWindowId();
315               ContentManager manager = myToolwindowIdToContentManagerMap.get(toolWindowId);
316               for (Content content : manager.getContents()) {
317                 RunContentDescriptor descriptor = getRunContentDescriptorByContent(content);
318                 if (descriptor != null) {
319                   ProcessHandler handler = descriptor.getProcessHandler();
320                   if (handler != null && !handler.isProcessTerminated()) {
321                     alive = true;
322                     break;
323                   }
324                 }
325               }
326               Icon base = myToolwindowIdToBaseIconMap.get(toolWindowId);
327               toolWindow.setIcon(alive ? getLiveIndicator(base) : base);
328
329               Icon icon = descriptor.getIcon();
330               content.setIcon(icon == null ? executor.getDisabledIcon() : IconLoader.getTransparentIcon(icon));
331             }
332           });
333         }
334       };
335       processHandler.addProcessListener(processAdapter);
336       final Disposable disposer = content.getDisposer();
337       if (disposer != null) {
338         Disposer.register(disposer, new Disposable() {
339           @Override
340           public void dispose() {
341             processHandler.removeProcessListener(processAdapter);
342           }
343         });
344       }
345     }
346     content.setDisplayName(descriptor.getDisplayName());
347     descriptor.setAttachedContent(content);
348     content.getManager().setSelectedContent(content);
349
350     if (!descriptor.isActivateToolWindowWhenAdded()) {
351       return;
352     }
353
354     ApplicationManager.getApplication().invokeLater(new Runnable() {
355       @Override
356       public void run() {
357         ToolWindow window = ToolWindowManager.getInstance(myProject).getToolWindow(executor.getToolWindowId());
358         // let's activate tool window, but don't move focus
359         //
360         // window.show() isn't valid here, because it will not
361         // mark the window as "last activated" windows and thus
362         // some action like navigation up/down in stacktrace wont
363         // work correctly
364         descriptor.getPreferredFocusComputable();
365         window.activate(descriptor.getActivationCallback(), descriptor.isAutoFocusContent(), descriptor.isAutoFocusContent());
366       }
367     }, myProject.getDisposed());
368   }
369
370   private final static int INDICATOR_SIZE = 4;
371   private static Icon getLiveIndicator(final Icon base) {
372     return new LayeredIcon(base, new Icon() {
373       @Override
374       public void paintIcon(Component c, Graphics g, int x, int y) {
375         Graphics2D g2d = (Graphics2D)g.create();
376         try {
377           GraphicsUtil.setupAAPainting(g2d);
378           g2d.setColor(Color.GREEN);
379           Ellipse2D.Double shape =
380             new Ellipse2D.Double(x + getIconWidth() - INDICATOR_SIZE, y + getIconHeight() - INDICATOR_SIZE, INDICATOR_SIZE, INDICATOR_SIZE);
381           g2d.fill(shape);
382           g2d.setColor(ColorUtil.withAlpha(Color.BLACK, .40));
383           g2d.draw(shape);
384         }
385         finally {
386           g2d.dispose();
387         }
388       }
389
390       @Override
391       public int getIconWidth() {
392         return base != null ? base.getIconWidth() : 13;
393       }
394
395       @Override
396       public int getIconHeight() {
397         return base != null ? base.getIconHeight() : 13;
398       }
399     });
400   }
401
402   @Override
403   @Nullable
404   @Deprecated
405   public RunContentDescriptor getReuseContent(final Executor requestor, DataContext dataContext) {
406     if (ApplicationManager.getApplication().isUnitTestMode()) {
407       return null;
408     }
409     //noinspection deprecation
410     return getReuseContent(requestor, GenericProgramRunner.CONTENT_TO_REUSE_DATA_KEY.getData(dataContext));
411   }
412
413   @Override
414   @Nullable
415   @Deprecated
416   public RunContentDescriptor getReuseContent(Executor requestor, @Nullable RunContentDescriptor contentToReuse) {
417     if (ApplicationManager.getApplication().isUnitTestMode()) {
418       return null;
419     }
420     if (contentToReuse != null) {
421       return contentToReuse;
422     }
423     return chooseReuseContentForDescriptor(getContentManagerForRunner(requestor), null, 0L, null);
424   }
425
426   @Nullable
427   @Override
428   public RunContentDescriptor getReuseContent(Executor requestor, @NotNull ExecutionEnvironment executionEnvironment) {
429     return getReuseContent(executionEnvironment);
430   }
431
432   @Nullable
433   @Override
434   public RunContentDescriptor getReuseContent(@NotNull ExecutionEnvironment executionEnvironment) {
435     if (ApplicationManager.getApplication().isUnitTestMode()) return null;
436     RunContentDescriptor contentToReuse = executionEnvironment.getContentToReuse();
437     if (contentToReuse != null) {
438       return contentToReuse;
439     }
440
441     final ContentManager contentManager = getContentManagerForRunner(executionEnvironment.getExecutor());
442     return chooseReuseContentForDescriptor(contentManager, null, executionEnvironment.getExecutionId(),
443                                            executionEnvironment.toString());
444   }
445
446   @Override
447   public RunContentDescriptor findContentDescriptor(final Executor requestor, final ProcessHandler handler) {
448     return getDescriptorBy(handler, requestor);
449   }
450
451   @Override
452   public void showRunContent(@NotNull Executor info, @NotNull RunContentDescriptor descriptor, @Nullable RunContentDescriptor contentToReuse) {
453     copyContentAndBehavior(descriptor, contentToReuse);
454     showRunContent(info, descriptor, descriptor.getExecutionId());
455   }
456
457   public static void copyContentAndBehavior(@NotNull RunContentDescriptor descriptor, @Nullable RunContentDescriptor contentToReuse) {
458     if (contentToReuse != null) {
459       Content attachedContent = contentToReuse.getAttachedContent();
460       if (attachedContent != null && attachedContent.isValid()) {
461         descriptor.setAttachedContent(attachedContent);
462       }
463       if (contentToReuse.isReuseToolWindowActivation()) {
464         descriptor.setActivateToolWindowWhenAdded(contentToReuse.isActivateToolWindowWhenAdded());
465       }
466     }
467   }
468
469   @Nullable
470   private static RunContentDescriptor chooseReuseContentForDescriptor(@NotNull ContentManager contentManager,
471                                                                       @Nullable RunContentDescriptor descriptor,
472                                                                       long executionId,
473                                                                       @Nullable String preferredName) {
474     Content content = null;
475     if (descriptor != null) {
476       //Stage one: some specific descriptors (like AnalyzeStacktrace) cannot be reused at all
477       if (descriptor.isContentReuseProhibited()) {
478         return null;
479       }
480       //Stage two: try to get content from descriptor itself
481       final Content attachedContent = descriptor.getAttachedContent();
482
483       if (attachedContent != null
484           && attachedContent.isValid()
485           && contentManager.getIndexOfContent(attachedContent) != -1
486           && (Comparing.equal(descriptor.getDisplayName(), attachedContent.getDisplayName()) || !attachedContent.isPinned())) {
487         content = attachedContent;
488       }
489     }
490     //Stage three: choose the content with name we prefer
491     if (content == null) {
492       content = getContentFromManager(contentManager, preferredName, executionId);
493     }
494     if (content == null || !isTerminated(content) || (content.getExecutionId() == executionId && executionId != 0)) {
495       return null;
496     }
497     final RunContentDescriptor oldDescriptor = getRunContentDescriptorByContent(content);
498     if (oldDescriptor != null && !oldDescriptor.isContentReuseProhibited() ) {
499       //content.setExecutionId(executionId);
500       return oldDescriptor;
501     }
502
503     return null;
504   }
505
506   @Nullable
507   private static Content getContentFromManager(ContentManager contentManager, @Nullable String preferredName, long executionId) {
508     ArrayList<Content> contents = new ArrayList<Content>(Arrays.asList(contentManager.getContents()));
509     Content first = contentManager.getSelectedContent();
510     if (first != null && contents.remove(first)) {//selected content should be checked first
511       contents.add(0, first);
512     }
513     if (preferredName != null) {//try to match content with specified preferred name
514       for (Content c : contents) {
515         if (canReuseContent(c, executionId) && preferredName.equals(c.getDisplayName())) {
516           return c;
517         }
518       }
519     }
520     for (Content c : contents) {//return first "good" content
521       if (canReuseContent(c, executionId)) {
522         return c;
523       }
524     }
525     return null;
526   }
527
528   private static boolean canReuseContent(Content c, long executionId) {
529     return c != null && !c.isPinned() && isTerminated(c) && !(c.getExecutionId() == executionId && executionId != 0);
530   }
531
532   @NotNull
533   private ContentManager getContentManagerForRunner(final Executor executor) {
534     final ContentManager contentManager = myToolwindowIdToContentManagerMap.get(executor.getToolWindowId());
535     if (contentManager == null) {
536       LOG.error("Runner " + executor.getId() + " is not registered");
537     }
538     //noinspection ConstantConditions
539     return contentManager;
540   }
541
542   private Content createNewContent(final ContentManager contentManager, final RunContentDescriptor descriptor, Executor executor) {
543     final String processDisplayName = descriptor.getDisplayName();
544     final Content content = ContentFactory.SERVICE.getInstance().createContent(descriptor.getComponent(), processDisplayName, true);
545     content.putUserData(DESCRIPTOR_KEY, descriptor);
546     content.putUserData(ToolWindow.SHOW_CONTENT_ICON, Boolean.TRUE);
547     contentManager.addContent(content);
548     new CloseListener(content, executor);
549     return content;
550   }
551
552   private static boolean isTerminated(@NotNull Content content) {
553     RunContentDescriptor descriptor = getRunContentDescriptorByContent(content);
554     ProcessHandler processHandler = descriptor == null ? null : descriptor.getProcessHandler();
555     return processHandler == null || processHandler.isProcessTerminated();
556   }
557
558   @Nullable
559   private static RunContentDescriptor getRunContentDescriptorByContent(@NotNull Content content) {
560     return content.getUserData(DESCRIPTOR_KEY);
561   }
562
563   @Override
564   @Nullable
565   public ToolWindow getToolWindowByDescriptor(@NotNull RunContentDescriptor descriptor) {
566     for (Map.Entry<String, ContentManager> entry : myToolwindowIdToContentManagerMap.entrySet()) {
567       if (getRunContentByDescriptor(entry.getValue(), descriptor) != null) {
568         return ToolWindowManager.getInstance(myProject).getToolWindow(entry.getKey());
569       }
570     }
571     return null;
572   }
573
574   @Nullable
575   private static Content getRunContentByDescriptor(@NotNull ContentManager contentManager, @NotNull RunContentDescriptor descriptor) {
576     for (Content content : contentManager.getContents()) {
577       if (descriptor.equals(content.getUserData(DESCRIPTOR_KEY))) {
578         return content;
579       }
580     }
581     return null;
582   }
583
584   @Override
585   public void addRunContentListener(@NotNull final RunContentListener listener, final Executor executor) {
586     final Disposable disposable = Disposer.newDisposable();
587     myProject.getMessageBus().connect(disposable).subscribe(TOPIC, new RunContentWithExecutorListener() {
588       @Override
589       public void contentSelected(@Nullable RunContentDescriptor descriptor, @NotNull Executor executor2) {
590         if (executor2.equals(executor)) {
591           listener.contentSelected(descriptor);
592         }
593       }
594
595       @Override
596       public void contentRemoved(@Nullable RunContentDescriptor descriptor, @NotNull Executor executor2) {
597         if (executor2.equals(executor)) {
598           listener.contentRemoved(descriptor);
599         }
600       }
601     });
602     myListeners.put(listener, disposable);
603   }
604
605   @Override
606   public void addRunContentListener(@NotNull final RunContentListener listener) {
607     final Disposable disposable = Disposer.newDisposable();
608     myProject.getMessageBus().connect(disposable).subscribe(TOPIC, new RunContentWithExecutorListener() {
609       @Override
610       public void contentSelected(@Nullable RunContentDescriptor descriptor, @NotNull Executor executor) {
611         listener.contentSelected(descriptor);
612       }
613
614       @Override
615       public void contentRemoved(@Nullable RunContentDescriptor descriptor, @NotNull Executor executor) {
616         listener.contentRemoved(descriptor);
617       }
618     });
619     myListeners.put(listener, disposable);
620   }
621
622   @Override
623   @NotNull
624   public List<RunContentDescriptor> getAllDescriptors() {
625     if (myToolwindowIdToContentManagerMap.isEmpty()) {
626       return Collections.emptyList();
627     }
628
629     List<RunContentDescriptor> descriptors = new SmartList<RunContentDescriptor>();
630     for (String id : myToolwindowIdToContentManagerMap.keySet()) {
631       for (Content content : myToolwindowIdToContentManagerMap.get(id).getContents()) {
632         RunContentDescriptor descriptor = getRunContentDescriptorByContent(content);
633         if (descriptor != null) {
634           descriptors.add(descriptor);
635         }
636       }
637     }
638     return descriptors;
639   }
640
641   @Override
642   public void removeRunContentListener(final RunContentListener listener) {
643     Disposable disposable = myListeners.remove(listener);
644     if (disposable != null) {
645       Disposer.dispose(disposable);
646     }
647   }
648
649   @Nullable
650   private RunContentDescriptor getDescriptorBy(ProcessHandler handler, Executor runnerInfo) {
651     for (Content content : getContentManagerForRunner(runnerInfo).getContents()) {
652       RunContentDescriptor runContentDescriptor = getRunContentDescriptorByContent(content);
653       assert runContentDescriptor != null;
654       if (runContentDescriptor.getProcessHandler() == handler) {
655         return runContentDescriptor;
656       }
657     }
658     return null;
659   }
660
661   private class CloseListener extends ContentManagerAdapter implements ProjectManagerListener {
662     private Content myContent;
663     private final Executor myExecutor;
664
665     private CloseListener(@NotNull final Content content, @NotNull Executor executor) {
666       myContent = content;
667       content.getManager().addContentManagerListener(this);
668       ProjectManager.getInstance().addProjectManagerListener(this);
669       myExecutor = executor;
670     }
671
672     @Override
673     public void contentRemoved(final ContentManagerEvent event) {
674       final Content content = event.getContent();
675       if (content == myContent) {
676         dispose();
677       }
678     }
679
680     private void dispose() {
681       if (myContent == null) return;
682
683       final Content content = myContent;
684       try {
685         RunContentDescriptor descriptor = getRunContentDescriptorByContent(content);
686         getSyncPublisher().contentRemoved(descriptor, myExecutor);
687         if (descriptor != null) {
688           Disposer.dispose(descriptor);
689         }
690       }
691       finally {
692         content.getManager().removeContentManagerListener(this);
693         ProjectManager.getInstance().removeProjectManagerListener(this);
694         content.release(); // don't invoke myContent.release() because myContent becomes null after destroyProcess()
695         myContent = null;
696       }
697     }
698
699     @Override
700     public void contentRemoveQuery(final ContentManagerEvent event) {
701       if (event.getContent() == myContent) {
702         final boolean canClose = closeQuery(false);
703         if (!canClose) {
704           event.consume();
705         }
706       }
707     }
708
709     @Override
710     public void projectOpened(final Project project) {
711     }
712
713     @Override
714     public void projectClosed(final Project project) {
715       if (myContent != null && project == myProject) {
716         myContent.getManager().removeContent(myContent, true);
717         dispose(); // Dispose content even if content manager refused to.
718       }
719     }
720
721     @Override
722     public boolean canCloseProject(final Project project) {
723       if (project != myProject) return true;
724
725       if (myContent == null) return true;
726
727       final boolean canClose = closeQuery(true);
728       if (canClose) {
729         myContent.getManager().removeContent(myContent, true);
730         myContent = null;
731       }
732       return canClose;
733     }
734
735     @Override
736     public void projectClosing(final Project project) {
737     }
738
739     private boolean closeQuery(boolean modal) {
740       final RunContentDescriptor descriptor = getRunContentDescriptorByContent(myContent);
741       if (descriptor == null) {
742         return true;
743       }
744
745       final ProcessHandler processHandler = descriptor.getProcessHandler();
746       if (processHandler == null || processHandler.isProcessTerminated() || processHandler.isProcessTerminating()) {
747         return true;
748       }
749       final boolean destroyProcess;
750       //noinspection deprecation
751       if (processHandler.isSilentlyDestroyOnClose() || Boolean.TRUE.equals(processHandler.getUserData(ProcessHandler.SILENTLY_DESTROY_ON_CLOSE))) {
752         destroyProcess = true;
753       }
754       else {
755         //todo[nik] this is a temporary solution for the following problem: some configurations should not allow user to choose between 'terminating' and 'detaching'
756         final boolean useDefault = Boolean.TRUE.equals(processHandler.getUserData(ALWAYS_USE_DEFAULT_STOPPING_BEHAVIOUR_KEY));
757         final TerminateRemoteProcessDialog.TerminateOption option = new TerminateRemoteProcessDialog.TerminateOption(processHandler.detachIsDefault(), useDefault);
758         final int rc = TerminateRemoteProcessDialog.show(myProject, descriptor.getDisplayName(), option);
759         if (rc != DialogWrapper.OK_EXIT_CODE) return false;
760         destroyProcess = !option.isToBeShown();
761       }
762       if (destroyProcess) {
763         processHandler.destroyProcess();
764       }
765       else {
766         processHandler.detachProcess();
767       }
768       waitForProcess(descriptor, modal);
769       return true;
770     }
771   }
772
773   private void waitForProcess(final RunContentDescriptor descriptor, final boolean modal) {
774     final ProcessHandler processHandler = descriptor.getProcessHandler();
775     final boolean killable = !modal && (processHandler instanceof KillableProcess) && ((KillableProcess)processHandler).canKillProcess();
776
777     String title = ExecutionBundle.message("terminating.process.progress.title", descriptor.getDisplayName());
778     ProgressManager.getInstance().run(new Task.Backgroundable(myProject, title, true) {
779
780       {
781         if (killable) {
782           String cancelText= ExecutionBundle.message("terminating.process.progress.kill");
783           setCancelText(cancelText);
784           setCancelTooltipText(cancelText);
785         }
786       }
787
788       @Override
789       public boolean isConditionalModal() {
790         return modal;
791       }
792
793       @Override
794       public boolean shouldStartInBackground() {
795         return !modal;
796       }
797
798       @Override
799       public void run(@NotNull final ProgressIndicator progressIndicator) {
800         final Semaphore semaphore = new Semaphore();
801         semaphore.down();
802
803         ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
804           @Override
805           public void run() {
806             final ProcessHandler processHandler = descriptor.getProcessHandler();
807             try {
808               if (processHandler != null) {
809                 processHandler.waitFor();
810               }
811             }
812             finally {
813               semaphore.up();
814             }
815           }
816         });
817
818         progressIndicator.setText(ExecutionBundle.message("waiting.for.vm.detach.progress.text"));
819         ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
820           @Override
821           public void run() {
822             while (true) {
823               if (progressIndicator.isCanceled() || !progressIndicator.isRunning()) {
824                 semaphore.up();
825                 break;
826               }
827               try {
828                 //noinspection SynchronizeOnThis
829                 synchronized (this) {
830                   //noinspection SynchronizeOnThis
831                   wait(2000L);
832                 }
833               }
834               catch (InterruptedException ignore) {
835               }
836             }
837           }
838         });
839
840         semaphore.waitFor();
841       }
842
843       @Override
844       public void onCancel() {
845         if (killable && !processHandler.isProcessTerminated()) {
846           ((KillableProcess)processHandler).killProcess();
847         }
848       }
849     });
850   }
851 }