e9bd51dfb5276e280c3de0a902b1a5483b34a048
[idea/community.git] / platform / platform-impl / src / com / intellij / notification / EventLog.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
17 package com.intellij.notification;
18
19 import com.intellij.execution.filters.HyperlinkInfo;
20 import com.intellij.ide.actions.ContextHelpAction;
21 import com.intellij.notification.impl.NotificationsConfigurable;
22 import com.intellij.notification.impl.NotificationsConfigurationImpl;
23 import com.intellij.notification.impl.NotificationsManagerImpl;
24 import com.intellij.openapi.actionSystem.*;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.components.AbstractProjectComponent;
27 import com.intellij.openapi.editor.Editor;
28 import com.intellij.openapi.editor.actions.ScrollToTheEndToolbarAction;
29 import com.intellij.openapi.editor.actions.ToggleUseSoftWrapsToolbarAction;
30 import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
31 import com.intellij.openapi.options.ShowSettingsUtil;
32 import com.intellij.openapi.project.DumbAware;
33 import com.intellij.openapi.project.DumbAwareAction;
34 import com.intellij.openapi.project.Project;
35 import com.intellij.openapi.project.ProjectManager;
36 import com.intellij.openapi.ui.SimpleToolWindowPanel;
37 import com.intellij.openapi.ui.popup.Balloon;
38 import com.intellij.openapi.util.*;
39 import com.intellij.openapi.util.text.StringUtil;
40 import com.intellij.openapi.wm.ToolWindow;
41 import com.intellij.openapi.wm.ToolWindowFactory;
42 import com.intellij.openapi.wm.ToolWindowManager;
43 import com.intellij.ui.awt.RelativePoint;
44 import com.intellij.ui.content.Content;
45 import com.intellij.ui.content.ContentFactory;
46 import org.jetbrains.annotations.NonNls;
47 import org.jetbrains.annotations.NotNull;
48 import org.jetbrains.annotations.Nullable;
49
50 import javax.swing.event.HyperlinkEvent;
51 import java.net.MalformedURLException;
52 import java.net.URL;
53 import java.util.ArrayList;
54 import java.util.List;
55 import java.util.TreeSet;
56 import java.util.concurrent.CopyOnWriteArrayList;
57 import java.util.regex.Matcher;
58 import java.util.regex.Pattern;
59
60 /**
61  * @author peter
62  */
63 public class EventLog implements Notifications {
64   public static final String LOG_REQUESTOR = "Internal log requestor";
65   public static final String LOG_TOOL_WINDOW_ID = "Event Log";
66   public static final String HELP_ID = "reference.toolwindows.event.log";
67   private final LogModel myModel = new LogModel(null, ApplicationManager.getApplication());
68   private static final String A_CLOSING = "</a>";
69   private static final Pattern TAG_PATTERN = Pattern.compile("<[^>]*>");
70   private static final Pattern A_PATTERN = Pattern.compile("<a ([^>]* )?href=[\"\']([^>]*)[\"\'][^>]*>");
71
72   public EventLog() {
73     ApplicationManager.getApplication().getMessageBus().connect().subscribe(Notifications.TOPIC, this);
74   }
75
76   @Override
77   public void notify(@NotNull Notification notification) {
78     final Project[] openProjects = ProjectManager.getInstance().getOpenProjects();
79     if (openProjects.length == 0) {
80       myModel.addNotification(notification);
81     }
82     for (Project p : openProjects) {
83       getProjectComponent(p).printNotification(notification);
84     }
85   }
86
87   public static void expire(@NotNull Notification notification) {
88     getApplicationComponent().myModel.removeNotification(notification);
89     for (Project p : ProjectManager.getInstance().getOpenProjects()) {
90       getProjectComponent(p).myProjectModel.removeNotification(notification);
91     }
92   }
93
94   private static EventLog getApplicationComponent() {
95     return ApplicationManager.getApplication().getComponent(EventLog.class);
96   }
97
98   @Override
99   public void register(@NotNull String groupDisplayName, @NotNull NotificationDisplayType defaultDisplayType) {
100   }
101
102   @Override
103   public void register(@NotNull String groupDisplayName,
104                        @NotNull NotificationDisplayType defaultDisplayType,
105                        boolean shouldLog) {
106   }
107
108   @NotNull
109   public static LogModel getLogModel(@Nullable Project project) {
110     return project != null ? getProjectComponent(project).myProjectModel : getApplicationComponent().myModel;
111   }
112
113   @Nullable
114   public static Pair<Notification, Long> getStatusMessage(@Nullable Project project) {
115     return getLogModel(project).getStatusMessage();
116   }
117
118   public static LogEntry formatForLog(@NotNull final Notification notification) {
119     String content = notification.getContent();
120     String mainText = notification.getTitle();
121     boolean showMore = false;
122     if (StringUtil.isNotEmpty(content)) {
123       if (content.startsWith("<p>")) {
124         content = content.substring("<p>".length());
125       }
126
127       if (content.startsWith("<") && !content.startsWith("<a ")) {
128         showMore = true;
129       }
130       if (StringUtil.isNotEmpty(mainText)) {
131         mainText += ": ";
132       }
133       mainText += content;
134     }
135
136     int nlIndex = eolIndex(mainText);
137     if (nlIndex >= 0) {
138       mainText = mainText.substring(0, nlIndex);
139       showMore = true;
140     }
141
142     List<Pair<TextRange, HyperlinkInfo>> links = new ArrayList<Pair<TextRange, HyperlinkInfo>>();
143
144     String message = "";
145     while (true) {
146       Matcher tagMatcher = TAG_PATTERN.matcher(mainText);
147       if (!tagMatcher.find()) {
148         message += mainText;
149         break;
150       }
151       message += mainText.substring(0, tagMatcher.start());
152       Matcher aMatcher = A_PATTERN.matcher(tagMatcher.group());
153       if (aMatcher.matches()) {
154         final String href = aMatcher.group(2);
155         int linkEnd = mainText.indexOf(A_CLOSING, tagMatcher.end());
156         if (linkEnd > 0) {
157           String linkText = mainText.substring(tagMatcher.end(), linkEnd).replaceAll(TAG_PATTERN.pattern(), "");
158
159           links.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(message.length(), linkText.length()), new NotificationHyperlinkInfo(notification, href)));
160
161           message += linkText;
162           mainText = mainText.substring(linkEnd + A_CLOSING.length());
163           continue;
164         }
165       }
166       mainText = mainText.substring(tagMatcher.end());
167     }
168
169     message = StringUtil.unescapeXml(StringUtil.convertLineSeparators(message));
170
171     String status = message;
172
173     if (showMore) {
174       message += " more ";
175       links.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(message.length() - 5, 4), new ShowBalloon(notification)));
176     }
177
178     return new LogEntry(message, status, links);
179   }
180
181   public static class LogEntry {
182     public final String message;
183     public final String status;
184     public final List<Pair<TextRange, HyperlinkInfo>> links;
185
186     public LogEntry(String message, String status, List<Pair<TextRange, HyperlinkInfo>> links) {
187       this.message = message;
188       this.status = status;
189       this.links = links;
190     }
191   }
192
193   private static int eolIndex(String mainText) {
194     TreeSet<Integer> indices = new TreeSet<Integer>();
195     indices.add(mainText.indexOf("<br>", 1));
196     indices.add(mainText.indexOf("<br/>", 1));
197     indices.add(mainText.indexOf("<p/>", 1));
198     indices.add(mainText.indexOf("<p>", 1));
199     indices.add(mainText.indexOf("\n"));
200     indices.remove(-1);
201     return indices.isEmpty() ? -1 : indices.iterator().next();
202   }
203
204   public static boolean isEventLogVisible(Project project) {
205     final ToolWindow window = getEventLog(project);
206     return window != null && window.isVisible();
207   }
208
209   @Nullable
210   public static ToolWindow getEventLog(Project project) {
211     return project == null ? null : ToolWindowManager.getInstance(project).getToolWindow(LOG_TOOL_WINDOW_ID);
212   }
213
214   public static void toggleLog(final Project project) {
215     final ToolWindow eventLog = getEventLog(project);
216     if (eventLog != null) {
217       if (!eventLog.isVisible()) {
218         eventLog.activate(null, true);
219         getLogModel(project).logShown();
220       } else {
221         eventLog.hide(null);
222       }
223     }
224   }
225
226   public static class ProjectTracker extends AbstractProjectComponent {
227     private volatile EventLogConsole myConsole;
228     private final List<Notification> myInitial = new CopyOnWriteArrayList<Notification>();
229     private final LogModel myProjectModel;
230
231     public ProjectTracker(@NotNull final Project project) {
232       super(project);
233
234       myProjectModel = new LogModel(project, project);
235
236       for (Notification notification : getApplicationComponent().myModel.takeNotifications()) {
237         printNotification(notification);
238       }
239
240       project.getMessageBus().connect(project).subscribe(Notifications.TOPIC, new Notifications() {
241         @Override
242         public void notify(@NotNull Notification notification) {
243           printNotification(notification);
244         }
245
246         @Override
247         public void register(@NotNull String groupDisplayName, @NotNull NotificationDisplayType defaultDisplayType) {
248         }
249
250         @Override
251         public void register(@NotNull String groupDisplayName,
252                              @NotNull NotificationDisplayType defaultDisplayType,
253                              boolean shouldLog) {
254         }
255       });
256
257     }
258
259     @Override
260     public void projectOpened() {
261       myConsole = new EventLogConsole(myProjectModel);
262
263       for (Notification notification : myInitial) {
264         printNotification(notification);
265       }
266       myInitial.clear();
267     }
268
269     @Override
270     public void projectClosed() {
271       getApplicationComponent().myModel.setStatusMessage(null, 0);
272     }
273
274     private void printNotification(final Notification notification) {
275       final EventLogConsole console = myConsole;
276       if (console == null) {
277         myInitial.add(notification);
278         return;
279       }
280
281       if (!NotificationsConfigurationImpl.getSettings(notification.getGroupId()).isShouldLog()) {
282         return;
283       }
284
285       myProjectModel.addNotification(notification);
286
287       ApplicationManager.getApplication().invokeLater(new Runnable() {
288         @Override
289         public void run() {
290           if (!ShutDownTracker.isShutdownHookRunning() && !myProject.isDisposed()) {
291             console.doPrintNotification(notification);
292           }
293         }
294       });
295     }
296
297   }
298
299   private static ProjectTracker getProjectComponent(Project project) {
300     return project.getComponent(ProjectTracker.class);
301   }
302   public static class FactoryItself implements ToolWindowFactory, DumbAware {
303     public void createToolWindowContent(final Project project, ToolWindow toolWindow) {
304       final Editor editor = getProjectComponent(project).myConsole.getConsoleEditor();
305
306       SimpleToolWindowPanel panel = new SimpleToolWindowPanel(false, true) {
307         @Override
308         public Object getData(@NonNls String dataId) {
309           return PlatformDataKeys.HELP_ID.is(dataId) ? HELP_ID : super.getData(dataId);
310         }
311       };
312       panel.setContent(editor.getComponent());
313
314       DefaultActionGroup group = new DefaultActionGroup();
315       group.add(new DumbAwareAction("Settings", "Edit notification settings", IconLoader.getIcon("/actions/showSettings.png")) {
316         @Override
317         public void actionPerformed(AnActionEvent e) {
318           ShowSettingsUtil.getInstance().editConfigurable(project, new NotificationsConfigurable());
319         }
320       });
321       group.add(new DisplayBalloons());
322       group.add(new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
323         @Override
324         protected Editor getEditor(AnActionEvent e) {
325           return editor;
326         }
327       });
328       group.add(new ScrollToTheEndToolbarAction(editor));
329       group.add(new DumbAwareAction("Mark all as read", "Mark all unread notifications as read", IconLoader.getIcon("/general/reset.png")) {
330         @Override
331         public void update(AnActionEvent e) {
332           if (project.isDisposed()) return;
333           e.getPresentation().setEnabled(!getProjectComponent(project).myProjectModel.getNotifications().isEmpty());
334         }
335
336         @Override
337         public void actionPerformed(AnActionEvent e) {
338           LogModel model = getProjectComponent(project).myProjectModel;
339           for (Notification notification : model.getNotifications()) {
340             model.removeNotification(notification);
341             notification.expire();
342           }
343         }
344       });
345       group.add(new ContextHelpAction(HELP_ID));
346
347       ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, false);
348       toolbar.setTargetComponent(panel);
349       panel.setToolbar(toolbar.getComponent());
350
351       final Content content = ContentFactory.SERVICE.getInstance().createContent(panel, "", false);
352       toolWindow.getContentManager().addContent(content);
353     }
354
355     private static class DisplayBalloons extends ToggleAction implements DumbAware {
356       public DisplayBalloons() {
357         super("Show balloons", "Enable or suppress notification balloons", IconLoader.getIcon("/general/balloon.png"));
358       }
359
360       @Override
361       public boolean isSelected(AnActionEvent e) {
362         return NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS;
363       }
364
365       @Override
366       public void setSelected(AnActionEvent e, boolean state) {
367         NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS = state;
368       }
369     }
370   }
371
372   private static class NotificationHyperlinkInfo implements HyperlinkInfo {
373     private final Notification myNotification;
374     private final String myHref;
375
376     public NotificationHyperlinkInfo(Notification notification, String href) {
377       myNotification = notification;
378       myHref = href;
379     }
380
381     @Override
382     public void navigate(Project project) {
383       NotificationListener listener = myNotification.getListener();
384       if (listener != null) {
385         EventLogConsole console = EventLog.getProjectComponent(project).myConsole;
386         URL url = null;
387         try {
388           url = new URL(null, myHref);
389         }
390         catch (MalformedURLException ignored) {
391         }
392         listener.hyperlinkUpdate(myNotification, new HyperlinkEvent(console.getConsoleEditor().getContentComponent(), HyperlinkEvent.EventType.ACTIVATED, url, myHref));
393       }
394     }
395   }
396
397   private static class ShowBalloon implements HyperlinkInfo {
398     private final Notification myNotification;
399
400     public ShowBalloon(Notification notification) {
401       myNotification = notification;
402     }
403
404     @Override
405     public void navigate(Project project) {
406       hideBalloon(myNotification);
407
408       for (Notification notification : getLogModel(project).getNotifications()) {
409         hideBalloon(notification);
410       }
411
412       RelativePoint target = EventLog.getProjectComponent(project).myConsole.getHyperlinkLocation(this);
413       if (target != null) {
414         Balloon balloon = NotificationsManagerImpl.createBalloon(myNotification, true, true);
415         Disposer.register(project, balloon);
416         balloon.show(target, Balloon.Position.above);
417       }
418     }
419
420     private static void hideBalloon(Notification notification1) {
421       Balloon balloon = notification1.getBalloon();
422       if (balloon != null) {
423         balloon.hide();
424       }
425     }
426   }
427 }