2327b0e2a56e99d92ddcc4a13e1138d1203418d5
[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("/general/secondaryGroup.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           e.getPresentation().setEnabled(!getProjectComponent(project).myProjectModel.getNotifications().isEmpty());
333         }
334
335         @Override
336         public void actionPerformed(AnActionEvent e) {
337           LogModel model = getProjectComponent(project).myProjectModel;
338           for (Notification notification : model.getNotifications()) {
339             model.removeNotification(notification);
340             notification.expire();
341           }
342         }
343       });
344       group.add(new ContextHelpAction(HELP_ID));
345
346       ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, false);
347       toolbar.setTargetComponent(panel);
348       panel.setToolbar(toolbar.getComponent());
349
350       final Content content = ContentFactory.SERVICE.getInstance().createContent(panel, "", false);
351       toolWindow.getContentManager().addContent(content);
352     }
353
354     private static class DisplayBalloons extends ToggleAction implements DumbAware {
355       public DisplayBalloons() {
356         super("Show balloons", "Enable or suppress notification balloons", IconLoader.getIcon("/general/balloon.png"));
357       }
358
359       @Override
360       public boolean isSelected(AnActionEvent e) {
361         return NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS;
362       }
363
364       @Override
365       public void setSelected(AnActionEvent e, boolean state) {
366         NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS = state;
367       }
368     }
369   }
370
371   private static class NotificationHyperlinkInfo implements HyperlinkInfo {
372     private final Notification myNotification;
373     private final String myHref;
374
375     public NotificationHyperlinkInfo(Notification notification, String href) {
376       myNotification = notification;
377       myHref = href;
378     }
379
380     @Override
381     public void navigate(Project project) {
382       NotificationListener listener = myNotification.getListener();
383       if (listener != null) {
384         EventLogConsole console = EventLog.getProjectComponent(project).myConsole;
385         URL url = null;
386         try {
387           url = new URL(null, myHref);
388         }
389         catch (MalformedURLException ignored) {
390         }
391         listener.hyperlinkUpdate(myNotification, new HyperlinkEvent(console.getConsoleEditor().getContentComponent(), HyperlinkEvent.EventType.ACTIVATED, url, myHref));
392       }
393     }
394   }
395
396   private static class ShowBalloon implements HyperlinkInfo {
397     private final Notification myNotification;
398
399     public ShowBalloon(Notification notification) {
400       myNotification = notification;
401     }
402
403     @Override
404     public void navigate(Project project) {
405       hideBalloon(myNotification);
406
407       for (Notification notification : getLogModel(project).getNotifications()) {
408         hideBalloon(notification);
409       }
410
411       RelativePoint target = EventLog.getProjectComponent(project).myConsole.getHyperlinkLocation(this);
412       if (target != null) {
413         Balloon balloon = NotificationsManagerImpl.createBalloon(myNotification, true, true);
414         Disposer.register(project, balloon);
415         balloon.show(target, Balloon.Position.above);
416       }
417     }
418
419     private static void hideBalloon(Notification notification1) {
420       Balloon balloon = notification1.getBalloon();
421       if (balloon != null) {
422         balloon.hide();
423       }
424     }
425   }
426 }