2d1e660c2c6d189cf4587d5c2bf8bc4b7de7631b
[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     mainText = StringUtil.replace(mainText, "&nbsp;", " ");
137     int nlIndex = eolIndex(mainText);
138     if (nlIndex >= 0) {
139       mainText = mainText.substring(0, nlIndex);
140       showMore = true;
141     }
142
143     List<Pair<TextRange, HyperlinkInfo>> links = new ArrayList<Pair<TextRange, HyperlinkInfo>>();
144
145     String message = "";
146     while (true) {
147       Matcher tagMatcher = TAG_PATTERN.matcher(mainText);
148       if (!tagMatcher.find()) {
149         message += mainText;
150         break;
151       }
152       message += mainText.substring(0, tagMatcher.start());
153       Matcher aMatcher = A_PATTERN.matcher(tagMatcher.group());
154       if (aMatcher.matches()) {
155         final String href = aMatcher.group(2);
156         int linkEnd = mainText.indexOf(A_CLOSING, tagMatcher.end());
157         if (linkEnd > 0) {
158           String linkText = mainText.substring(tagMatcher.end(), linkEnd).replaceAll(TAG_PATTERN.pattern(), "");
159
160           links.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(message.length(), linkText.length()), new NotificationHyperlinkInfo(notification, href)));
161
162           message += linkText;
163           mainText = mainText.substring(linkEnd + A_CLOSING.length());
164           continue;
165         }
166       }
167       mainText = mainText.substring(tagMatcher.end());
168     }
169
170     message = StringUtil.unescapeXml(StringUtil.convertLineSeparators(message));
171
172     String status = message;
173
174     if (showMore) {
175       message += " more ";
176       links.add(new Pair<TextRange, HyperlinkInfo>(TextRange.from(message.length() - 5, 4), new ShowBalloon(notification)));
177     }
178
179     return new LogEntry(message, status, links);
180   }
181
182   public static class LogEntry {
183     public final String message;
184     public final String status;
185     public final List<Pair<TextRange, HyperlinkInfo>> links;
186
187     public LogEntry(String message, String status, List<Pair<TextRange, HyperlinkInfo>> links) {
188       this.message = message;
189       this.status = status;
190       this.links = links;
191     }
192   }
193
194   private static int eolIndex(String mainText) {
195     TreeSet<Integer> indices = new TreeSet<Integer>();
196     indices.add(mainText.indexOf("<br>", 1));
197     indices.add(mainText.indexOf("<br/>", 1));
198     indices.add(mainText.indexOf("<p/>", 1));
199     indices.add(mainText.indexOf("<p>", 1));
200     indices.add(mainText.indexOf("\n"));
201     indices.remove(-1);
202     return indices.isEmpty() ? -1 : indices.iterator().next();
203   }
204
205   public static boolean isEventLogVisible(Project project) {
206     final ToolWindow window = getEventLog(project);
207     return window != null && window.isVisible();
208   }
209
210   @Nullable
211   public static ToolWindow getEventLog(Project project) {
212     return project == null ? null : ToolWindowManager.getInstance(project).getToolWindow(LOG_TOOL_WINDOW_ID);
213   }
214
215   public static void toggleLog(final Project project) {
216     final ToolWindow eventLog = getEventLog(project);
217     if (eventLog != null) {
218       if (!eventLog.isVisible()) {
219         eventLog.activate(null, true);
220         getLogModel(project).logShown();
221       } else {
222         eventLog.hide(null);
223       }
224     }
225   }
226
227   public static class ProjectTracker extends AbstractProjectComponent {
228     private volatile EventLogConsole myConsole;
229     private final List<Notification> myInitial = new CopyOnWriteArrayList<Notification>();
230     private final LogModel myProjectModel;
231
232     public ProjectTracker(@NotNull final Project project) {
233       super(project);
234
235       myProjectModel = new LogModel(project, project);
236
237       for (Notification notification : getApplicationComponent().myModel.takeNotifications()) {
238         printNotification(notification);
239       }
240
241       project.getMessageBus().connect(project).subscribe(Notifications.TOPIC, new Notifications() {
242         @Override
243         public void notify(@NotNull Notification notification) {
244           printNotification(notification);
245         }
246
247         @Override
248         public void register(@NotNull String groupDisplayName, @NotNull NotificationDisplayType defaultDisplayType) {
249         }
250
251         @Override
252         public void register(@NotNull String groupDisplayName,
253                              @NotNull NotificationDisplayType defaultDisplayType,
254                              boolean shouldLog) {
255         }
256       });
257
258     }
259
260     @Override
261     public void projectOpened() {
262       myConsole = new EventLogConsole(myProjectModel);
263
264       for (Notification notification : myInitial) {
265         printNotification(notification);
266       }
267       myInitial.clear();
268     }
269
270     @Override
271     public void projectClosed() {
272       getApplicationComponent().myModel.setStatusMessage(null, 0);
273     }
274
275     private void printNotification(final Notification notification) {
276       final EventLogConsole console = myConsole;
277       if (console == null) {
278         myInitial.add(notification);
279         return;
280       }
281
282       if (!NotificationsConfigurationImpl.getSettings(notification.getGroupId()).isShouldLog()) {
283         return;
284       }
285
286       myProjectModel.addNotification(notification);
287
288       ApplicationManager.getApplication().invokeLater(new Runnable() {
289         @Override
290         public void run() {
291           if (!ShutDownTracker.isShutdownHookRunning() && !myProject.isDisposed()) {
292             console.doPrintNotification(notification);
293           }
294         }
295       });
296     }
297
298   }
299
300   private static ProjectTracker getProjectComponent(Project project) {
301     return project.getComponent(ProjectTracker.class);
302   }
303   public static class FactoryItself implements ToolWindowFactory, DumbAware {
304     public void createToolWindowContent(final Project project, ToolWindow toolWindow) {
305       final Editor editor = getProjectComponent(project).myConsole.getConsoleEditor();
306
307       SimpleToolWindowPanel panel = new SimpleToolWindowPanel(false, true) {
308         @Override
309         public Object getData(@NonNls String dataId) {
310           return PlatformDataKeys.HELP_ID.is(dataId) ? HELP_ID : super.getData(dataId);
311         }
312       };
313       panel.setContent(editor.getComponent());
314
315       DefaultActionGroup group = new DefaultActionGroup();
316       group.add(new DumbAwareAction("Settings", "Edit notification settings", IconLoader.getIcon("/actions/showSettings.png")) {
317         @Override
318         public void actionPerformed(AnActionEvent e) {
319           ShowSettingsUtil.getInstance().editConfigurable(project, new NotificationsConfigurable());
320         }
321       });
322       group.add(new DisplayBalloons());
323       group.add(new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
324         @Override
325         protected Editor getEditor(AnActionEvent e) {
326           return editor;
327         }
328       });
329       group.add(new ScrollToTheEndToolbarAction(editor));
330       group.add(new DumbAwareAction("Mark all as read", "Mark all unread notifications as read", IconLoader.getIcon("/general/reset.png")) {
331         @Override
332         public void update(AnActionEvent e) {
333           if (project.isDisposed()) return;
334           e.getPresentation().setEnabled(!getProjectComponent(project).myProjectModel.getNotifications().isEmpty());
335         }
336
337         @Override
338         public void actionPerformed(AnActionEvent e) {
339           LogModel model = getProjectComponent(project).myProjectModel;
340           for (Notification notification : model.getNotifications()) {
341             model.removeNotification(notification);
342             notification.expire();
343           }
344         }
345       });
346       group.add(new ContextHelpAction(HELP_ID));
347
348       ActionToolbar toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, group, false);
349       toolbar.setTargetComponent(panel);
350       panel.setToolbar(toolbar.getComponent());
351
352       final Content content = ContentFactory.SERVICE.getInstance().createContent(panel, "", false);
353       toolWindow.getContentManager().addContent(content);
354     }
355
356     private static class DisplayBalloons extends ToggleAction implements DumbAware {
357       public DisplayBalloons() {
358         super("Show balloons", "Enable or suppress notification balloons", IconLoader.getIcon("/general/balloon.png"));
359       }
360
361       @Override
362       public boolean isSelected(AnActionEvent e) {
363         return NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS;
364       }
365
366       @Override
367       public void setSelected(AnActionEvent e, boolean state) {
368         NotificationsConfigurationImpl.getNotificationsConfigurationImpl().SHOW_BALLOONS = state;
369       }
370     }
371   }
372
373   private static class NotificationHyperlinkInfo implements HyperlinkInfo {
374     private final Notification myNotification;
375     private final String myHref;
376
377     public NotificationHyperlinkInfo(Notification notification, String href) {
378       myNotification = notification;
379       myHref = href;
380     }
381
382     @Override
383     public void navigate(Project project) {
384       NotificationListener listener = myNotification.getListener();
385       if (listener != null) {
386         EventLogConsole console = EventLog.getProjectComponent(project).myConsole;
387         URL url = null;
388         try {
389           url = new URL(null, myHref);
390         }
391         catch (MalformedURLException ignored) {
392         }
393         listener.hyperlinkUpdate(myNotification, new HyperlinkEvent(console.getConsoleEditor().getContentComponent(), HyperlinkEvent.EventType.ACTIVATED, url, myHref));
394       }
395     }
396   }
397
398   private static class ShowBalloon implements HyperlinkInfo {
399     private final Notification myNotification;
400
401     public ShowBalloon(Notification notification) {
402       myNotification = notification;
403     }
404
405     @Override
406     public void navigate(Project project) {
407       hideBalloon(myNotification);
408
409       for (Notification notification : getLogModel(project).getNotifications()) {
410         hideBalloon(notification);
411       }
412
413       RelativePoint target = EventLog.getProjectComponent(project).myConsole.getHyperlinkLocation(this);
414       if (target != null) {
415         Balloon balloon = NotificationsManagerImpl.createBalloon(myNotification, true, true);
416         Disposer.register(project, balloon);
417         balloon.show(target, Balloon.Position.above);
418       }
419     }
420
421     private static void hideBalloon(Notification notification1) {
422       Balloon balloon = notification1.getBalloon();
423       if (balloon != null) {
424         balloon.hide();
425       }
426     }
427   }
428 }