1/****************************************************************************** 2 * Copyright (c) 2006-2012 Transmission authors and contributors 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a 5 * copy of this software and associated documentation files (the "Software"), 6 * to deal in the Software without restriction, including without limitation 7 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 * and/or sell copies of the Software, and to permit persons to whom the 9 * Software is furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 * DEALINGS IN THE SOFTWARE. 21 *****************************************************************************/ 22 23#include <libtransmission/transmission.h> 24#include <libtransmission/log.h> 25 26#import "MessageWindowController.h" 27#import "Controller.h" 28#import "NSApplicationAdditions.h" 29#import "NSMutableArrayAdditions.h" 30#import "NSStringAdditions.h" 31 32#define LEVEL_ERROR 0 33#define LEVEL_INFO 1 34#define LEVEL_DEBUG 2 35 36#define UPDATE_SECONDS 0.75 37 38@interface MessageWindowController (Private) 39 40- (void) resizeColumn; 41- (BOOL) shouldIncludeMessageForFilter: (NSString *) filterString message: (NSDictionary *) message; 42- (void) updateListForFilter; 43- (NSString *) stringForMessage: (NSDictionary *) message; 44 45@end 46 47@implementation MessageWindowController 48 49- (id) init 50{ 51 return [super initWithWindowNibName: @"MessageWindow"]; 52} 53 54- (void) awakeFromNib 55{ 56 NSWindow * window = [self window]; 57 [window setFrameAutosaveName: @"MessageWindowFrame"]; 58 [window setFrameUsingName: @"MessageWindowFrame"]; 59 [window setRestorationClass: [self class]]; 60 61 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(resizeColumn) 62 name: NSTableViewColumnDidResizeNotification object: fMessageTable]; 63 64 [window setContentBorderThickness: NSMinY([[fMessageTable enclosingScrollView] frame]) forEdge: NSMinYEdge]; 65 66 [[self window] setTitle: NSLocalizedString(@"Message Log", "Message window -> title")]; 67 68 //set images and text for popup button items 69 [[fLevelButton itemAtIndex: LEVEL_ERROR] setTitle: NSLocalizedString(@"Error", "Message window -> level string")]; 70 [[fLevelButton itemAtIndex: LEVEL_INFO] setTitle: NSLocalizedString(@"Info", "Message window -> level string")]; 71 [[fLevelButton itemAtIndex: LEVEL_DEBUG] setTitle: NSLocalizedString(@"Debug", "Message window -> level string")]; 72 if (![NSApp isOnYosemiteOrBetter]) 73 { 74 [[fLevelButton itemAtIndex: LEVEL_ERROR] setImage: [NSImage imageNamed: @"RedDotGlossy"]]; 75 [[fLevelButton itemAtIndex: LEVEL_INFO] setImage: [NSImage imageNamed: @"YellowDotGlossy"]]; 76 [[fLevelButton itemAtIndex: LEVEL_DEBUG] setImage: [NSImage imageNamed: @"PurpleDotGlossy"]]; 77 } 78 79 const CGFloat levelButtonOldWidth = NSWidth([fLevelButton frame]); 80 [fLevelButton sizeToFit]; 81 82 //set table column text 83 [[[fMessageTable tableColumnWithIdentifier: @"Date"] headerCell] setTitle: NSLocalizedString(@"Date", 84 "Message window -> table column")]; 85 [[[fMessageTable tableColumnWithIdentifier: @"Name"] headerCell] setTitle: NSLocalizedString(@"Process", 86 "Message window -> table column")]; 87 [[[fMessageTable tableColumnWithIdentifier: @"Message"] headerCell] setTitle: NSLocalizedString(@"Message", 88 "Message window -> table column")]; 89 90 //set and size buttons 91 [fSaveButton setTitle: [NSLocalizedString(@"Save", "Message window -> save button") stringByAppendingEllipsis]]; 92 [fSaveButton sizeToFit]; 93 94 NSRect saveButtonFrame = [fSaveButton frame]; 95 saveButtonFrame.size.width += 10.0; 96 saveButtonFrame.origin.x += NSWidth([fLevelButton frame]) - levelButtonOldWidth; 97 [fSaveButton setFrame: saveButtonFrame]; 98 99 const CGFloat oldClearButtonWidth = [fClearButton frame].size.width; 100 101 [fClearButton setTitle: NSLocalizedString(@"Clear", "Message window -> save button")]; 102 [fClearButton sizeToFit]; 103 104 NSRect clearButtonFrame = [fClearButton frame]; 105 clearButtonFrame.size.width = MAX(clearButtonFrame.size.width + 10.0, saveButtonFrame.size.width); 106 clearButtonFrame.origin.x -= NSWidth(clearButtonFrame) - oldClearButtonWidth; 107 [fClearButton setFrame: clearButtonFrame]; 108 109 [[fFilterField cell] setPlaceholderString: NSLocalizedString(@"Filter", "Message window -> filter field")]; 110 NSRect filterButtonFrame = [fFilterField frame]; 111 filterButtonFrame.origin.x -= NSWidth(clearButtonFrame) - oldClearButtonWidth; 112 [fFilterField setFrame: filterButtonFrame]; 113 114 fAttributes = [[[[fMessageTable tableColumnWithIdentifier: @"Message"] dataCell] attributedStringValue] 115 attributesAtIndex: 0 effectiveRange: NULL]; 116 117 //select proper level in popup button 118 switch ([[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"]) 119 { 120 case TR_LOG_ERROR: 121 [fLevelButton selectItemAtIndex: LEVEL_ERROR]; 122 break; 123 case TR_LOG_INFO: 124 [fLevelButton selectItemAtIndex: LEVEL_INFO]; 125 break; 126 case TR_LOG_DEBUG: 127 [fLevelButton selectItemAtIndex: LEVEL_DEBUG]; 128 break; 129 default: //safety 130 [[NSUserDefaults standardUserDefaults] setInteger: TR_LOG_ERROR forKey: @"MessageLevel"]; 131 [fLevelButton selectItemAtIndex: LEVEL_ERROR]; 132 } 133 134 fMessages = [[NSMutableArray alloc] init]; 135 fDisplayedMessages = [[NSMutableArray alloc] init]; 136 137 fLock = [[NSLock alloc] init]; 138} 139 140- (void) dealloc 141{ 142 [[NSNotificationCenter defaultCenter] removeObserver: self]; 143 [fTimer invalidate]; 144} 145 146- (void) windowDidBecomeKey: (NSNotification *) notification 147{ 148 if (!fTimer) 149 { 150 fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_SECONDS target: self selector: @selector(updateLog:) userInfo: nil repeats: YES]; 151 [self updateLog: nil]; 152 } 153} 154 155- (void) windowWillClose: (id)sender 156{ 157 [fTimer invalidate]; 158 fTimer = nil; 159} 160 161+ (void) restoreWindowWithIdentifier: (NSString *) identifier state: (NSCoder *) state completionHandler: (void (^)(NSWindow *, NSError *)) completionHandler 162{ 163 NSAssert1([identifier isEqualToString: @"MessageWindow"], @"Trying to restore unexpected identifier %@", identifier); 164 165 NSWindow * window = [[(Controller *)[NSApp delegate] messageWindowController] window]; 166 completionHandler(window, nil); 167} 168 169- (void) window: (NSWindow *) window didDecodeRestorableState: (NSCoder *) coder 170{ 171 [fTimer invalidate]; 172 fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_SECONDS target: self selector: @selector(updateLog:) userInfo: nil repeats: YES]; 173 [self updateLog: nil]; 174} 175 176- (void) updateLog: (NSTimer *) timer 177{ 178 tr_log_message * messages; 179 if ((messages = tr_logGetQueue()) == NULL) 180 return; 181 182 [fLock lock]; 183 184 static NSUInteger currentIndex = 0; 185 186 NSScroller * scroller = [[fMessageTable enclosingScrollView] verticalScroller]; 187 const BOOL shouldScroll = currentIndex == 0 || [scroller floatValue] == 1.0 || [scroller isHidden] 188 || [scroller knobProportion] == 1.0; 189 190 const NSInteger maxLevel = [[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"]; 191 NSString * filterString = [fFilterField stringValue]; 192 193 BOOL changed = NO; 194 195 for (tr_log_message * currentMessage = messages; currentMessage != NULL; currentMessage = currentMessage->next) 196 { 197 NSString * name = currentMessage->name != NULL ? @(currentMessage->name) 198 : [[NSProcessInfo processInfo] processName]; 199 200 NSString * file = [[@(currentMessage->file) lastPathComponent] stringByAppendingFormat: @":%d", 201 currentMessage->line]; 202 203 NSDictionary * message = @{ 204 @"Message": @(currentMessage->message), 205 @"Date": [NSDate dateWithTimeIntervalSince1970: currentMessage->when], 206 @"Index": @(currentIndex++), //more accurate when sorting by date 207 @"Level": @(currentMessage->level), 208 @"Name": name, 209 @"File": file}; 210 [fMessages addObject: message]; 211 212 if (currentMessage->level <= maxLevel && [self shouldIncludeMessageForFilter: filterString message: message]) 213 { 214 [fDisplayedMessages addObject: message]; 215 changed = YES; 216 } 217 } 218 219 if ([fMessages count] > TR_LOG_MAX_QUEUE_LENGTH) 220 { 221 const NSUInteger oldCount = [fDisplayedMessages count]; 222 223 NSIndexSet * removeIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fMessages count]-TR_LOG_MAX_QUEUE_LENGTH)]; 224 NSArray * itemsToRemove = [fMessages objectsAtIndexes: removeIndexes]; 225 226 [fMessages removeObjectsAtIndexes: removeIndexes]; 227 [fDisplayedMessages removeObjectsInArray: itemsToRemove]; 228 229 changed |= oldCount > [fDisplayedMessages count]; 230 } 231 232 if (changed) 233 { 234 [fDisplayedMessages sortUsingDescriptors: [fMessageTable sortDescriptors]]; 235 236 [fMessageTable reloadData]; 237 if (shouldScroll) 238 [fMessageTable scrollRowToVisible: [fMessageTable numberOfRows]-1]; 239 } 240 241 [fLock unlock]; 242 243 tr_logFreeQueue (messages); 244} 245 246- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView 247{ 248 return [fDisplayedMessages count]; 249} 250 251- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row 252{ 253 NSString * ident = [column identifier]; 254 NSDictionary * message = fDisplayedMessages[row]; 255 256 if ([ident isEqualToString: @"Date"]) 257 return message[@"Date"]; 258 else if ([ident isEqualToString: @"Level"]) 259 { 260 const NSInteger level = [message[@"Level"] integerValue]; 261 switch (level) 262 { 263 case TR_LOG_ERROR: 264 return [NSImage imageNamed: ([NSApp isOnYosemiteOrBetter] ? @"RedDotFlat" : @"RedDotGlossy")]; 265 case TR_LOG_INFO: 266 return [NSImage imageNamed: ([NSApp isOnYosemiteOrBetter] ? @"YellowDotFlat" : @"YellowDotGlossy")]; 267 case TR_LOG_DEBUG: 268 return [NSImage imageNamed: ([NSApp isOnYosemiteOrBetter] ? @"PurpleDotFlat" : @"PurpleDotGlossy")]; 269 default: 270 NSAssert1(NO, @"Unknown message log level: %ld", level); 271 return nil; 272 } 273 } 274 else if ([ident isEqualToString: @"Name"]) 275 return message[@"Name"]; 276 else 277 return message[@"Message"]; 278} 279 280#warning don't cut off end 281- (CGFloat) tableView: (NSTableView *) tableView heightOfRow: (NSInteger) row 282{ 283 NSString * message = fDisplayedMessages[row][@"Message"]; 284 285 NSTableColumn * column = [tableView tableColumnWithIdentifier: @"Message"]; 286 const CGFloat count = floorf([message sizeWithAttributes: fAttributes].width / [column width]); 287 288 return [tableView rowHeight] * (count + 1.0); 289} 290 291- (void) tableView: (NSTableView *) tableView sortDescriptorsDidChange: (NSArray *) oldDescriptors 292{ 293 [fDisplayedMessages sortUsingDescriptors: [fMessageTable sortDescriptors]]; 294 [fMessageTable reloadData]; 295} 296 297- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect 298 tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation 299{ 300 NSDictionary * message = fDisplayedMessages[row]; 301 return message[@"File"]; 302} 303 304- (void) copy: (id) sender 305{ 306 NSIndexSet * indexes = [fMessageTable selectedRowIndexes]; 307 NSMutableArray * messageStrings = [NSMutableArray arrayWithCapacity: [indexes count]]; 308 309 for (NSDictionary * message in [fDisplayedMessages objectsAtIndexes: indexes]) 310 [messageStrings addObject: [self stringForMessage: message]]; 311 312 NSString * messageString = [messageStrings componentsJoinedByString: @"\n"]; 313 314 NSPasteboard * pb = [NSPasteboard generalPasteboard]; 315 [pb clearContents]; 316 [pb writeObjects: @[messageString]]; 317} 318 319- (BOOL) validateMenuItem: (NSMenuItem *) menuItem 320{ 321 SEL action = [menuItem action]; 322 323 if (action == @selector(copy:)) 324 return [fMessageTable numberOfSelectedRows] > 0; 325 326 return YES; 327} 328 329- (void) changeLevel: (id) sender 330{ 331 NSInteger level; 332 switch ([fLevelButton indexOfSelectedItem]) 333 { 334 case LEVEL_ERROR: 335 level = TR_LOG_ERROR; 336 break; 337 case LEVEL_INFO: 338 level = TR_LOG_INFO; 339 break; 340 case LEVEL_DEBUG: 341 level = TR_LOG_DEBUG; 342 break; 343 default: 344 NSAssert1(NO, @"Unknown message log level: %ld", [fLevelButton indexOfSelectedItem]); 345 level = TR_LOG_INFO; 346 } 347 348 if ([[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"] == level) 349 return; 350 351 [[NSUserDefaults standardUserDefaults] setInteger: level forKey: @"MessageLevel"]; 352 353 [fLock lock]; 354 355 [self updateListForFilter]; 356 357 [fLock unlock]; 358} 359 360- (void) changeFilter: (id) sender 361{ 362 [fLock lock]; 363 364 [self updateListForFilter]; 365 366 [fLock unlock]; 367} 368 369- (void) clearLog: (id) sender 370{ 371 [fLock lock]; 372 373 [fMessages removeAllObjects]; 374 375 [fMessageTable beginUpdates]; 376 [fMessageTable removeRowsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fDisplayedMessages count])] withAnimation: NSTableViewAnimationSlideLeft]; 377 378 [fDisplayedMessages removeAllObjects]; 379 380 [fMessageTable endUpdates]; 381 382 [fLock unlock]; 383} 384 385- (void) writeToFile: (id) sender 386{ 387 NSSavePanel * panel = [NSSavePanel savePanel]; 388 [panel setAllowedFileTypes: @[@"txt"]]; 389 [panel setCanSelectHiddenExtension: YES]; 390 391 [panel setNameFieldStringValue: NSLocalizedString(@"untitled", "Save log panel -> default file name")]; 392 393 [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) { 394 if (result == NSFileHandlingPanelOKButton) 395 { 396 //make the array sorted by date 397 NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"Index" ascending: YES]; 398 NSArray * descriptors = [[NSArray alloc] initWithObjects: descriptor, nil]; 399 NSArray * sortedMessages = [fDisplayedMessages sortedArrayUsingDescriptors: descriptors]; 400 401 //create the text to output 402 NSMutableArray * messageStrings = [NSMutableArray arrayWithCapacity: [sortedMessages count]]; 403 for (NSDictionary * message in sortedMessages) 404 [messageStrings addObject: [self stringForMessage: message]]; 405 406 NSString * fileString = [messageStrings componentsJoinedByString: @"\n"]; 407 408 if (![fileString writeToFile: [[panel URL] path] atomically: YES encoding: NSUTF8StringEncoding error: nil]) 409 { 410 NSAlert * alert = [[NSAlert alloc] init]; 411 [alert addButtonWithTitle: NSLocalizedString(@"OK", "Save log alert panel -> button")]; 412 [alert setMessageText: NSLocalizedString(@"Log Could Not Be Saved", "Save log alert panel -> title")]; 413 [alert setInformativeText: [NSString stringWithFormat: 414 NSLocalizedString(@"There was a problem creating the file \"%@\".", 415 "Save log alert panel -> message"), [[[panel URL] path] lastPathComponent]]]; 416 [alert setAlertStyle: NSWarningAlertStyle]; 417 418 [alert runModal]; 419 } 420 } 421 }]; 422} 423 424@end 425 426@implementation MessageWindowController (Private) 427 428- (void) resizeColumn 429{ 430 [fMessageTable noteHeightOfRowsWithIndexesChanged: [NSIndexSet indexSetWithIndexesInRange: 431 NSMakeRange(0, [fMessageTable numberOfRows])]]; 432} 433 434- (BOOL) shouldIncludeMessageForFilter: (NSString *) filterString message: (NSDictionary *) message 435{ 436 if ([filterString isEqualToString: @""]) 437 return YES; 438 439 const NSStringCompareOptions searchOptions = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch; 440 return [message[@"Name"] rangeOfString: filterString options: searchOptions].location != NSNotFound 441 || [message[@"Message"] rangeOfString: filterString options: searchOptions].location != NSNotFound; 442} 443 444- (void) updateListForFilter 445{ 446 const NSInteger level = [[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"]; 447 NSString * filterString = [fFilterField stringValue]; 448 449 NSIndexSet * indexes = [fMessages indexesOfObjectsWithOptions: NSEnumerationConcurrent passingTest: ^BOOL(id message, NSUInteger idx, BOOL * stop) { 450 return [((NSDictionary *)message)[@"Level"] integerValue] <= level && [self shouldIncludeMessageForFilter: filterString message: message]; 451 }]; 452 453 NSArray * tempMessages = [[fMessages objectsAtIndexes: indexes] sortedArrayUsingDescriptors: [fMessageTable sortDescriptors]]; 454 455 [fMessageTable beginUpdates]; 456 457 //figure out which rows were added/moved 458 NSUInteger currentIndex = 0, totalCount = 0; 459 NSMutableArray * itemsToAdd = [NSMutableArray array]; 460 NSMutableIndexSet * itemsToAddIndexes = [NSMutableIndexSet indexSet]; 461 462 for (NSDictionary * message in tempMessages) 463 { 464 const NSUInteger previousIndex = [fDisplayedMessages indexOfObject: message inRange: NSMakeRange(currentIndex, [fDisplayedMessages count]-currentIndex)]; 465 if (previousIndex == NSNotFound) 466 { 467 [itemsToAdd addObject: message]; 468 [itemsToAddIndexes addIndex: totalCount]; 469 } 470 else 471 { 472 if (previousIndex != currentIndex) 473 { 474 [fDisplayedMessages moveObjectAtIndex: previousIndex toIndex: currentIndex]; 475 [fMessageTable moveRowAtIndex: previousIndex toIndex: currentIndex]; 476 } 477 ++currentIndex; 478 } 479 480 ++totalCount; 481 } 482 483 //remove trailing items - those are the unused 484 if (currentIndex < [fDisplayedMessages count]) 485 { 486 const NSRange removeRange = NSMakeRange(currentIndex, [fDisplayedMessages count]-currentIndex); 487 [fDisplayedMessages removeObjectsInRange: removeRange]; 488 [fMessageTable removeRowsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: removeRange] withAnimation: NSTableViewAnimationSlideDown]; 489 } 490 491 //add new items 492 [fDisplayedMessages insertObjects: itemsToAdd atIndexes: itemsToAddIndexes]; 493 [fMessageTable insertRowsAtIndexes: itemsToAddIndexes withAnimation: NSTableViewAnimationSlideUp]; 494 495 [fMessageTable endUpdates]; 496 497 NSAssert2([fDisplayedMessages isEqualToArray: tempMessages], @"Inconsistency between message arrays! %@ %@", fDisplayedMessages, tempMessages); 498} 499 500- (NSString *) stringForMessage: (NSDictionary *) message 501{ 502 NSString * levelString; 503 const NSInteger level = [message[@"Level"] integerValue]; 504 switch (level) 505 { 506 case TR_LOG_ERROR: 507 levelString = NSLocalizedString(@"Error", "Message window -> level"); 508 break; 509 case TR_LOG_INFO: 510 levelString = NSLocalizedString(@"Info", "Message window -> level"); 511 break; 512 case TR_LOG_DEBUG: 513 levelString = NSLocalizedString(@"Debug", "Message window -> level"); 514 break; 515 default: 516 NSAssert1(NO, @"Unknown message log level: %ld", level); 517 levelString = @"?"; 518 } 519 520 return [NSString stringWithFormat: @"%@ %@ [%@] %@: %@", message[@"Date"], 521 message[@"File"], levelString, 522 message[@"Name"], message[@"Message"], nil]; 523} 524 525@end 526