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