macosx: Clarify VLCLogWindowController description
[vlc.git] / modules / gui / macosx / VLCLogWindowController.m
1 /*****************************************************************************
2  * VLCLogWindowController.m: Log message window controller
3  *****************************************************************************
4  * Copyright (C) 2004-2013 VLC authors and VideoLAN
5  * $Id$
6  *
7  * Authors: Felix Paul Kühne <fkuehne at videolan dot org>
8  *          Pierre d'Herbemont <pdherbemont # videolan org>
9  *          Derk-Jan Hartman <hartman at videolan.org>
10  *
11  * This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation; either version 2 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public License
22  * along with this program; if not, write to the Free Software
23  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24  *****************************************************************************/
25
26 #import "VLCLogWindowController.h"
27 #import "VLCMain.h"
28 #import <vlc_common.h>
29
30 @interface VLCLogWindowController () <NSWindowDelegate>
31
32 /* This array stores messages that are managed by the arrayController */
33 @property (retain) NSMutableArray *messagesArray;
34
35 /* This array stores messages before they are added to the messagesArray on refresh */
36 @property (retain) NSMutableArray *messageBuffer;
37
38 /* We do not want to refresh the table for every message, as that would be very frequent when
39  * there are a lot of messages, therefore we use a timer to refresh the table with new data
40  * from the messageBuffer every now and then, which is much more efficient and still fast
41  * enough for a good user experience
42  */
43 @property (retain) NSTimer        *refreshTimer;
44
45 - (void)addMessage:(NSDictionary *)message;
46
47 @end
48
49 /*
50  * MsgCallback: Callback triggered by the core once a new debug message is
51  * ready to be displayed. We store everything in a NSArray in our Cocoa part
52  * of this file.
53  */
54 static void MsgCallback(void *data, int type, const vlc_log_t *item, const char *format, va_list ap)
55 {
56     @autoreleasepool {
57         int state;
58         char *msg;
59         VLCLogWindowController *controller = (__bridge VLCLogWindowController*)data;
60         static NSString *types[4] = { @"info", @"error", @"warning", @"debug" };
61
62         if (vasprintf(&msg, format, ap) == -1) {
63             return;
64         }
65
66         if (!item->psz_module || !msg) {
67             free(msg);
68             return;
69         }
70
71         NSString *position = [NSString stringWithFormat:@"%s:%i", item->file, item->line];
72
73         NSDictionary *messageDict = @{
74                                       @"type"       : types[type],
75                                       @"message"    : toNSStr(msg),
76                                       @"component"  : toNSStr(item->psz_module),
77                                       @"position"   : position,
78                                       @"func"       : toNSStr(item->func)
79                                       };
80         [controller addMessage:messageDict];
81         free(msg);
82     }
83 }
84
85 @implementation VLCLogWindowController
86
87 - (id)init
88 {
89     self = [super initWithWindowNibName:@"LogMessageWindow"];
90     if (self) {
91         _messagesArray = [[NSMutableArray alloc] initWithCapacity:500];
92         _messageBuffer = [[NSMutableArray alloc] initWithCapacity:100];
93     }
94     return self;
95 }
96
97 - (void)dealloc
98 {
99     if (getIntf())
100         vlc_LogSet( getIntf()->obj.libvlc, NULL, NULL );
101 }
102
103 - (void)windowDidLoad
104 {
105     [self.window setExcludedFromWindowsMenu:YES];
106     [self.window setDelegate:self];
107     [self.window setTitle:_NS("Messages")];
108     [self.window setLevel:NSModalPanelWindowLevel];
109
110 #define setupButton(target, title, desc)                                              \
111     [target accessibilitySetOverrideValue:title                                       \
112                              forAttribute:NSAccessibilityTitleAttribute];             \
113     [target accessibilitySetOverrideValue:desc                                        \
114                              forAttribute:NSAccessibilityDescriptionAttribute];       \
115     [target setToolTip:desc];
116
117     setupButton(_saveButton,
118                 _NS("Save log"),
119                 _NS("Click to save the debug log to a file."));
120     setupButton(_refreshButton,
121                 _NS("Refresh log"),
122                 _NS("Click to frefresh the log output."));
123     setupButton(_clearButton,
124                 _NS("Clear log"),
125                 _NS("Click to clear the log output."));
126     setupButton(_toggleDetailsButton,
127                 _NS("Toggle details"),
128                 _NS("Click to show/hide details about a log message."));
129
130 #undef setupButton
131 }
132
133 - (void)showWindow:(id)sender
134 {
135     // Do nothing if window is already visible
136     if ([self.window isVisible]) {
137         return [super showWindow:sender];
138     }
139
140     // Subscribe to LibVLCCore's messages
141     vlc_LogSet(getIntf()->obj.libvlc, MsgCallback, (__bridge void*)self);
142     _refreshTimer = [NSTimer scheduledTimerWithTimeInterval:0.3
143                                                      target:self
144                                                    selector:@selector(appendMessageBuffer)
145                                                    userInfo:nil
146                                                     repeats:YES];
147     return [super showWindow:sender];
148 }
149
150 - (void)windowWillClose:(NSNotification *)notification
151 {
152     // Unsubscribe from LibVLCCore's messages
153     vlc_LogSet( getIntf()->obj.libvlc, NULL, NULL );
154
155     // Remove all messages
156     [self clearMessageBuffer];
157     [self clearMessageTable];
158
159     // Invalidate timer
160     [_refreshTimer invalidate];
161     _refreshTimer = nil;
162 }
163
164 #pragma mark -
165 #pragma mark Delegate methods
166
167 /*
168  * Called when a row is added to the table
169  * We use this to set the correct background color for the row, depending on the
170  * message type.
171  */
172 - (void)tableView:(NSTableView *)tableView didAddRowView:(NSTableRowView *)rowView forRow:(NSInteger)row
173 {
174     // Initialize background colors
175     static NSDictionary *colors = nil;
176     static dispatch_once_t onceToken;
177     dispatch_once(&onceToken, ^{
178         colors = @{
179                    @"info"     : [NSColor colorWithCalibratedRed:0.65 green:0.91 blue:1.0 alpha:0.7],
180                    @"error"    : [NSColor colorWithCalibratedRed:1.0 green:0.49 blue:0.45 alpha:0.5],
181                    @"warning"  : [NSColor colorWithCalibratedRed:1.0 green:0.88 blue:0.45 alpha:0.7],
182                    @"debug"    : [NSColor colorWithCalibratedRed:0.96 green:0.96 blue:0.96 alpha:0.5]
183                    };
184     });
185
186     // Lookup color for message type
187     NSDictionary *message = [[_arrayController arrangedObjects] objectAtIndex:row];
188     rowView.backgroundColor = [colors objectForKey:[message objectForKey:@"type"]];
189 }
190
191 - (void)splitViewDidResizeSubviews:(NSNotification *)notification
192 {
193     if ([_splitView isSubviewCollapsed:_detailView]) {
194         [_toggleDetailsButton setState:NSOffState];
195     } else {
196         [_toggleDetailsButton setState:NSOnState];
197     }
198 }
199
200 #pragma mark -
201 #pragma mark UI actions
202
203 /* Save debug log to file action
204  */
205 - (IBAction)saveDebugLog:(id)sender
206 {
207     NSSavePanel * saveFolderPanel = [[NSSavePanel alloc] init];
208
209     [saveFolderPanel setCanSelectHiddenExtension: NO];
210     [saveFolderPanel setCanCreateDirectories: YES];
211     [saveFolderPanel setAllowedFileTypes: [NSArray arrayWithObject:@"txt"]];
212     [saveFolderPanel setNameFieldStringValue:[NSString stringWithFormat: _NS("VLC Debug Log (%s).txt"), VERSION_MESSAGE]];
213     [saveFolderPanel beginSheetModalForWindow: self.window completionHandler:^(NSInteger returnCode) {
214         if (returnCode != NSOKButton) {
215             return;
216         }
217         NSMutableString *string = [[NSMutableString alloc] init];
218
219         for (NSDictionary *line in _messagesArray) {
220             NSString *message = [NSString stringWithFormat:@"%@ %@ %@\n",
221                                  [line objectForKey:@"component"],
222                                  [line objectForKey:@"type"],
223                                  [line objectForKey:@"message"]];
224             [string appendString:message];
225         }
226         NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding];
227         if ([data writeToFile: [[saveFolderPanel URL] path] atomically: YES] == NO)
228             msg_Warn(getIntf(), "Error while saving the debug log");
229     }];
230 }
231
232 /* Clear log action
233  */
234 - (IBAction)clearLog:(id)sender
235 {
236     // Unregister handler
237     vlc_LogSet(getIntf()->obj.libvlc, NULL, NULL);
238
239     // Remove all messages
240     [self clearMessageBuffer];
241     [self clearMessageTable];
242
243     // Reregister handler, to write new header to log
244     vlc_LogSet(getIntf()->obj.libvlc, MsgCallback, (__bridge void*)self);
245 }
246
247 /* Refresh log action
248  */
249 - (IBAction)refreshLog:(id)sender
250 {
251     [self appendMessageBuffer];
252     [_messageTable scrollToEndOfDocument:self];
253 }
254
255 /* Show/Hide details action
256  */
257 - (IBAction)toggleDetails:(id)sender
258 {
259     if ([_splitView isSubviewCollapsed:_detailView]) {
260         [_detailView setHidden:NO];
261     } else {
262         [_detailView setHidden:YES];
263     }
264 }
265
266 /* Called when the user hits CMD + C or copy is clicked in the edit menu
267  */
268 - (void) copy:(id)sender {
269     NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
270     [pasteBoard clearContents];
271     for (NSDictionary *line in [_arrayController selectedObjects]) {
272         NSString *message = [NSString stringWithFormat:@"%@ %@ %@",
273                              [line objectForKey:@"component"],
274                              [line objectForKey:@"type"],
275                              [line objectForKey:@"message"]];
276         [pasteBoard writeObjects:@[message]];
277     }
278 }
279
280 #pragma mark -
281 #pragma mark UI validation
282
283 /* Validate the copy menu item
284  */
285 - (BOOL)validateUserInterfaceItem:(id <NSValidatedUserInterfaceItem>)anItem
286 {
287     SEL theAction = [anItem action];
288
289     if (theAction == @selector(copy:)) {
290         if ([[_arrayController selectedObjects] count] > 0) {
291             return YES;
292         }
293         return NO;
294     }
295     /* Indicate that we handle the validation method,
296      * even if we don’t implement the action
297      */
298     return YES;
299 }
300
301 #pragma mark -
302 #pragma mark Data handling
303
304 /** 
305  Adds a message to the messageBuffer, it does not has to be called from the main thread, as
306  items are only added to the messageArray on refresh.
307  */
308 - (void)addMessage:(NSDictionary *)messageDict
309 {
310     @synchronized (_messageBuffer) {
311         [_messageBuffer addObject:messageDict];
312     }
313 }
314
315 /**
316  Clears the message buffer
317  */
318 - (void)clearMessageBuffer
319 {
320     @synchronized (_messageBuffer) {
321         [_messageBuffer removeAllObjects];
322     }
323 }
324
325 /**
326  Clears all messages in the message table by removing all items from the arrayController
327  */
328 - (void)clearMessageTable
329 {
330     NSRange range = NSMakeRange(0, [[_arrayController arrangedObjects] count]);
331     [_arrayController removeObjectsAtArrangedObjectIndexes:[NSIndexSet indexSetWithIndexesInRange:range]];
332 }
333
334 /**
335  Appends all messages from the buffer to the arrayController and clears the buffer
336  */
337 - (void)appendMessageBuffer
338 {
339     if ([_messagesArray count] > 1000000) {
340         [_messagesArray removeObjectsInRange:NSMakeRange(0, 2)];
341     }
342     @synchronized (_messageBuffer) {
343         [_arrayController addObjects:_messageBuffer];
344         [_messageBuffer removeAllObjects];
345     }
346 }
347
348 @end