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