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