IDEA-80382 Notifications: when balloon is shown in Event Log and after clicking...
[idea/community.git] / platform / platform-impl / src / com / intellij / notification / impl / NotificationsManagerImpl.java
1 /*
2  * Copyright 2000-2011 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.notification.impl;
17
18 import com.intellij.ide.FrameStateManager;
19 import com.intellij.notification.*;
20 import com.intellij.notification.impl.ui.NotificationsUtil;
21 import com.intellij.openapi.application.ApplicationManager;
22 import com.intellij.openapi.components.ApplicationComponent;
23 import com.intellij.openapi.project.Project;
24 import com.intellij.openapi.project.ProjectManager;
25 import com.intellij.openapi.project.ProjectManagerAdapter;
26 import com.intellij.openapi.startup.StartupManager;
27 import com.intellij.openapi.ui.MessageType;
28 import com.intellij.openapi.ui.popup.*;
29 import com.intellij.openapi.util.Disposer;
30 import com.intellij.openapi.util.text.StringUtil;
31 import com.intellij.openapi.wm.ToolWindowManager;
32 import com.intellij.openapi.wm.WindowManager;
33 import com.intellij.openapi.wm.impl.IdeFrameImpl;
34 import com.intellij.ui.BalloonImpl;
35 import com.intellij.ui.components.panels.NonOpaquePanel;
36 import com.intellij.util.ArrayUtil;
37 import com.intellij.util.PairFunction;
38 import com.intellij.util.ui.UIUtil;
39 import org.jetbrains.annotations.NotNull;
40 import org.jetbrains.annotations.Nullable;
41
42 import javax.swing.*;
43 import javax.swing.border.EmptyBorder;
44 import javax.swing.event.HyperlinkEvent;
45 import javax.swing.event.HyperlinkListener;
46 import java.awt.*;
47 import java.util.ArrayList;
48 import java.util.Arrays;
49 import java.util.Collection;
50 import java.util.List;
51
52 /**
53  * @author spleaner
54  */
55 public class NotificationsManagerImpl extends NotificationsManager implements Notifications, ApplicationComponent {
56
57   private final NotificationModel myModel = new NotificationModel();
58
59   @NotNull
60   public String getComponentName() {
61     return "NotificationsManager";
62   }
63
64   public void initComponent() {
65     ApplicationManager.getApplication().getMessageBus().connect().subscribe(TOPIC, this);
66   }
67
68   public static NotificationsManagerImpl getNotificationsManagerImpl() {
69     return (NotificationsManagerImpl) ApplicationManager.getApplication().getComponent(NotificationsManager.class);
70   }
71
72   public void notify(@NotNull Notification notification) {
73     doNotify(notification, NotificationDisplayType.BALLOON);
74   }
75
76   @Override
77   public void register(@NotNull String groupDisplayName, @NotNull NotificationDisplayType defaultDisplayType) {
78   }
79
80   @Override
81   public void register(@NotNull String groupDisplayName,
82                        @NotNull NotificationDisplayType defaultDisplayType,
83                        boolean shouldLog) {
84   }
85
86   @Override
87   public void expire(@NotNull Notification notification) {
88     EventLog.expire(notification);
89     remove(notification);
90   }
91
92   @Override
93   public <T extends Notification> T[] getNotificationsOfType(Class<T> klass, @Nullable final Project project) {
94     final List<Notification> notifications = myModel.getByType(null, createFilter(project, false));
95     final List<T> result = new ArrayList<T>();
96     for (final Notification notification : notifications) {
97       if (klass.isInstance(notification)) {
98         //noinspection unchecked
99         result.add((T) notification);
100       }
101     }
102     
103     return ArrayUtil.toObjectArray(result, klass);
104   }
105
106   private static final PairFunction<Notification, Project, Boolean> ALL = new PairFunction<Notification, Project, Boolean>() {
107     @NotNull
108     public Boolean fun(final Notification notification, final Project project) {
109       return true;
110     }
111   };
112
113   private static final PairFunction<Notification, Project, Boolean> APPLICATION = new PairFunction<Notification, Project, Boolean>() {
114     @NotNull
115     public Boolean fun(final Notification notification, final Project project) {
116       return project == null;
117     }
118   };
119
120   public void clear(@Nullable Project project) {
121     myModel.clear(createFilter(project, true));
122   }
123
124   public void disposeComponent() {
125     myModel.clear(ALL);
126   }
127
128   protected void doNotify(@NotNull Notification notification, @Nullable final NotificationDisplayType displayType) {
129     doNotify(notification, displayType, null);
130   }
131
132   public void doNotify(@NotNull final Notification notification, @Nullable NotificationDisplayType displayType, @Nullable final Project project) {
133     final NotificationsConfigurationImpl configuration = NotificationsConfigurationImpl.getNotificationsConfigurationImpl();
134     if (!configuration.isRegistered(notification.getGroupId())) {
135       configuration.register(notification.getGroupId(), displayType == null ? NotificationDisplayType.BALLOON : displayType);
136     }
137
138     final NotificationSettings settings = NotificationsConfigurationImpl.getSettings(notification.getGroupId());
139     boolean shouldLog = settings.isShouldLog();
140     boolean displayable = settings.getDisplayType() != NotificationDisplayType.NONE;
141     if (shouldLog && displayable) {
142       myModel.add(notification, project);
143     }
144
145     boolean willBeShown = displayable && NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS;
146     if (!shouldLog && !willBeShown) {
147       notification.expire();
148     }
149
150     if (NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS) {
151       final Runnable runnable = new Runnable() {
152         @Override
153         public void run() {
154           showNotification(notification, project);
155         }
156       };
157       if (project == null) {
158         runnable.run();
159       } else if (!project.isDisposed()) {
160         StartupManager.getInstance(project).runWhenProjectIsInitialized(runnable);
161       }
162     }
163   }
164
165   private static void showNotification(final Notification notification, @Nullable final Project project) {
166     String groupId = notification.getGroupId();
167     final NotificationSettings settings = NotificationsConfigurationImpl.getSettings(groupId);
168
169     NotificationDisplayType type = settings.getDisplayType();
170     String toolWindowId = NotificationsConfigurationImpl.getNotificationsConfigurationImpl().getToolWindowId(groupId);
171     if (type == NotificationDisplayType.TOOL_WINDOW &&
172         (toolWindowId == null || project == null || !Arrays.asList(ToolWindowManager.getInstance(project).getToolWindowIds()).contains(toolWindowId))) {
173       type = NotificationDisplayType.BALLOON;
174     }
175
176     switch (type) {
177       case NONE:
178         return;
179       //case EXTERNAL:
180       //  notifyByExternal(notification);
181       //  break;
182       case STICKY_BALLOON:
183       case BALLOON:
184       default:
185         Balloon balloon = notifyByBalloon(notification, type, project);
186         if (!settings.isShouldLog()) {
187           if (balloon == null) {
188             notification.expire();
189           } else {
190             balloon.addListener(new JBPopupAdapter() {
191               @Override
192               public void onClosed(LightweightWindowEvent event) {
193                 notification.expire();
194               }
195             });
196           }
197         }
198         break;
199       case TOOL_WINDOW:
200         MessageType messageType = notification.getType() == NotificationType.ERROR
201                             ? MessageType.ERROR
202                             : notification.getType() == NotificationType.WARNING ? MessageType.WARNING : MessageType.INFO;
203         final NotificationListener notificationListener = notification.getListener();
204         HyperlinkListener listener = notificationListener == null ? null : new HyperlinkListener() {
205           @Override
206           public void hyperlinkUpdate(HyperlinkEvent e) {
207             notificationListener.hyperlinkUpdate(notification, e);
208           }
209         };
210         assert toolWindowId != null;
211         String msg = notification.getTitle();
212         if (StringUtil.isNotEmpty(notification.getContent())) {
213           if (StringUtil.isNotEmpty(msg)) {
214             msg += "<br>";
215           }
216           msg += notification.getContent();
217         }
218
219         //noinspection SSBasedInspection
220         ToolWindowManager.getInstance(project).notifyByBalloon(toolWindowId, messageType, msg, notification.getIcon(), listener);
221     }
222   }
223
224   @Nullable
225   private static Balloon notifyByBalloon(final Notification notification,
226                                       final NotificationDisplayType displayType,
227                                       @Nullable final Project project) {
228     if (ApplicationManager.getApplication().isUnitTestMode()) return null;
229
230     Window window = findWindowForBalloon(project);
231     if (window instanceof IdeFrameImpl) {
232       final ProjectManager projectManager = ProjectManager.getInstance();
233       final boolean noProjects = projectManager.getOpenProjects().length == 0;
234       final boolean sticky = NotificationDisplayType.STICKY_BALLOON == displayType || noProjects;
235       final Balloon balloon = createBalloon(notification, false, false);
236       Disposer.register(project != null ? project : ApplicationManager.getApplication(), balloon);
237       ((IdeFrameImpl)window).getBalloonLayout().add(balloon);
238       if (NotificationDisplayType.BALLOON == displayType) {
239         FrameStateManager.getInstance().getApplicationActive().doWhenDone(new Runnable() {
240           @Override
241           public void run() {
242             if (balloon.isDisposed()) {
243               return;
244             }
245
246             if (!sticky) {
247               ((BalloonImpl)balloon).startFadeoutTimer(3000);
248             }
249             else //noinspection ConstantConditions
250               if (noProjects) {
251               projectManager.addProjectManagerListener(new ProjectManagerAdapter() {
252                 @Override
253                 public void projectOpened(Project project) {
254                   projectManager.removeProjectManagerListener(this);
255                   if (!balloon.isDisposed()) {
256                     ((BalloonImpl)balloon).startFadeoutTimer(300);
257                   }
258                 }
259               });
260             }
261           }
262         });
263       }
264       return balloon;
265     }
266     return null;
267   }
268
269   @Nullable
270   public static Window findWindowForBalloon(Project project) {
271     final JFrame frame = WindowManager.getInstance().getFrame(project);
272     if (frame == null && project == null) {
273       return KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow();
274     }
275     return frame;
276   }
277
278   public static Balloon createBalloon(final Notification notification, final boolean showCallout, final boolean hideOnClickOutside) {
279     final JEditorPane text = new JEditorPane();
280     text.setEditorKit(UIUtil.getHTMLEditorKit());
281
282     final HyperlinkListener listener = NotificationsUtil.wrapListener(notification);
283     if (listener != null) {
284       text.addHyperlinkListener(listener);
285     }
286
287     final JLabel label = new JLabel(NotificationsUtil.buildHtml(notification, null));
288     text.setText(NotificationsUtil.buildHtml(notification, "width:" + Math.min(400, label.getPreferredSize().width) + "px;"));
289     text.setEditable(false);
290     text.setOpaque(false);
291
292     if (UIUtil.isUnderNimbusLookAndFeel()) {
293       text.setBackground(UIUtil.TRANSPARENT_COLOR);
294     }
295
296     text.setBorder(null);
297
298     final JPanel content = new NonOpaquePanel(new BorderLayout((int)(label.getIconTextGap() * 1.5), (int)(label.getIconTextGap() * 1.5)));
299
300     final NonOpaquePanel textWrapper = new NonOpaquePanel(new GridBagLayout());
301     textWrapper.add(text);
302     content.add(textWrapper, BorderLayout.CENTER);
303
304     final NonOpaquePanel north = new NonOpaquePanel(new BorderLayout());
305     north.add(new JLabel(NotificationsUtil.getIcon(notification)), BorderLayout.NORTH);
306     content.add(north, BorderLayout.WEST);
307
308     content.setBorder(new EmptyBorder(2, 4, 2, 4));
309
310     final BalloonBuilder builder = JBPopupFactory.getInstance().createBalloonBuilder(content);
311     builder.setFillColor(NotificationsUtil.getBackground(notification)).setCloseButtonEnabled(true).setShowCallout(showCallout)
312       .setHideOnClickOutside(hideOnClickOutside)
313       .setHideOnAction(hideOnClickOutside)
314       .setHideOnKeyOutside(hideOnClickOutside).setHideOnFrameResize(false);
315
316     final Balloon balloon = builder.createBalloon();
317     notification.setBalloon(balloon);
318     return balloon;
319   }
320
321   private static PairFunction<Notification, Project, Boolean> createFilter(@Nullable final Project project, final boolean strict) {
322     return project == null ? APPLICATION : new ProjectFilter(project, strict);
323   }
324
325   @Nullable
326   public Notification remove(Notification notification) {
327     return myModel.remove(notification);
328   }
329
330   public Collection<Notification> getByType(@Nullable final NotificationType type, @Nullable final Project project) {
331     return myModel.getByType(type, createFilter(project, false));
332   }
333
334   private static class ProjectFilter implements PairFunction<Notification, Project, Boolean> {
335     private final Project myProject;
336     private final boolean myStrict;
337
338     private ProjectFilter(@NotNull final Project project, final boolean strict) {
339       myProject = project;
340       myStrict = strict;
341     }
342
343     @NotNull
344     public Boolean fun(final Notification notification, final Project project) {
345       return myStrict ? project == myProject : project == null || project == myProject;
346     }
347   }
348 }