source: trunk/macosx/MessageWindowController.m @ 13469

Last change on this file since 13469 was 13469, checked in by livings124, 10 years ago

Revert r13468 for now.

  • Property svn:keywords set to Date Rev Author Id
File size: 21.5 KB
Line 
1/******************************************************************************
2 * $Id: MessageWindowController.m 13469 2012-09-06 03:21:03Z 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    [fLock release];
143   
144    [fMessages release];
145    [fDisplayedMessages release];
146   
147    [fAttributes release];
148   
149    [super dealloc];
150}
151
152- (void) windowDidBecomeKey: (NSNotification *) notification
153{
154    if (!fTimer)
155    {
156        fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_SECONDS target: self selector: @selector(updateLog:) userInfo: nil repeats: YES];
157        [self updateLog: nil];
158    }
159}
160
161- (void) windowWillClose: (id)sender
162{
163    [fTimer invalidate];
164    fTimer = nil;
165}
166
167+ (void) restoreWindowWithIdentifier: (NSString *) identifier state: (NSCoder *) state completionHandler: (void (^)(NSWindow *, NSError *)) completionHandler
168{
169    NSAssert1([identifier isEqualToString: @"MessageWindow"], @"Trying to restore unexpected identifier %@", identifier);
170   
171    NSWindow * window = [[(Controller *)[NSApp delegate] messageWindowController] window];
172    completionHandler(window, nil);
173}
174
175- (void) window: (NSWindow *) window didDecodeRestorableState: (NSCoder *) coder
176{
177    [fTimer invalidate];
178    fTimer = [NSTimer scheduledTimerWithTimeInterval: UPDATE_SECONDS target: self selector: @selector(updateLog:) userInfo: nil repeats: YES];
179    [self updateLog: nil];
180}
181
182- (void) updateLog: (NSTimer *) timer
183{
184    tr_msg_list * messages;
185    if ((messages = tr_getQueuedMessages()) == NULL)
186        return;
187   
188    [fLock lock];
189   
190    static NSUInteger currentIndex = 0;
191   
192    NSScroller * scroller = [[fMessageTable enclosingScrollView] verticalScroller];
193    const BOOL shouldScroll = currentIndex == 0 || [scroller floatValue] == 1.0 || [scroller isHidden]
194                                || [scroller knobProportion] == 1.0;
195   
196    const NSInteger maxLevel = [[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"];
197    NSString * filterString = [fFilterField stringValue];
198   
199    BOOL changed = NO;
200   
201    for (tr_msg_list * currentMessage = messages; currentMessage != NULL; currentMessage = currentMessage->next)
202    {
203        NSString * name = currentMessage->name != NULL ? [NSString stringWithUTF8String: currentMessage->name]
204                            : [[NSProcessInfo processInfo] processName];
205       
206        NSString * file = [[[NSString stringWithUTF8String: currentMessage->file] lastPathComponent] stringByAppendingFormat: @":%d",
207                            currentMessage->line];
208       
209        NSDictionary * message  = [NSDictionary dictionaryWithObjectsAndKeys:
210                                    [NSString stringWithUTF8String: currentMessage->message], @"Message",
211                                    [NSDate dateWithTimeIntervalSince1970: currentMessage->when], @"Date",
212                                    [NSNumber numberWithUnsignedInteger: currentIndex++], @"Index", //more accurate when sorting by date
213                                    [NSNumber numberWithInteger: currentMessage->level], @"Level",
214                                    name, @"Name",
215                                    file, @"File", nil];
216       
217        [fMessages addObject: message];
218       
219        if (currentMessage->level <= maxLevel && [self shouldIncludeMessageForFilter: filterString message: message])
220        {
221            [fDisplayedMessages addObject: message];
222            changed = YES;
223        }
224    }
225   
226    if ([fMessages count] > TR_MAX_MSG_LOG)
227    {
228        const NSUInteger oldCount = [fDisplayedMessages count];
229       
230        NSIndexSet * removeIndexes = [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fMessages count]-TR_MAX_MSG_LOG)];
231        NSArray * itemsToRemove = [fMessages objectsAtIndexes: removeIndexes];
232       
233        [fMessages removeObjectsAtIndexes: removeIndexes];
234        [fDisplayedMessages removeObjectsInArray: itemsToRemove];
235       
236        changed |= oldCount > [fDisplayedMessages count];
237    }
238   
239    if (changed)
240    {
241        [fDisplayedMessages sortUsingDescriptors: [fMessageTable sortDescriptors]];
242       
243        [fMessageTable reloadData];
244        if (shouldScroll)
245            [fMessageTable scrollRowToVisible: [fMessageTable numberOfRows]-1];
246    }
247   
248    [fLock unlock];
249   
250    tr_freeMessageList(messages);
251}
252
253- (NSInteger) numberOfRowsInTableView: (NSTableView *) tableView
254{
255    return [fDisplayedMessages count];
256}
257
258- (id) tableView: (NSTableView *) tableView objectValueForTableColumn: (NSTableColumn *) column row: (NSInteger) row
259{
260    NSString * ident = [column identifier];
261    NSDictionary * message = [fDisplayedMessages objectAtIndex: row];
262
263    if ([ident isEqualToString: @"Date"])
264        return [message objectForKey: @"Date"];
265    else if ([ident isEqualToString: @"Level"])
266    {
267        const NSInteger level = [[message objectForKey: @"Level"] integerValue];
268        switch (level)
269        {
270            case TR_MSG_ERR:
271                return [NSImage imageNamed: @"RedDot"];
272            case TR_MSG_INF:
273                return [NSImage imageNamed: @"YellowDot"];
274            case TR_MSG_DBG:
275                return [NSImage imageNamed: @"PurpleDot"];
276            default:
277                NSAssert1(NO, @"Unknown message log level: %ld", level);
278                return nil;
279        }
280    }
281    else if ([ident isEqualToString: @"Name"])
282        return [message objectForKey: @"Name"];
283    else
284        return [message objectForKey: @"Message"];
285}
286
287#warning don't cut off end
288- (CGFloat) tableView: (NSTableView *) tableView heightOfRow: (NSInteger) row
289{
290    NSString * message = [[fDisplayedMessages objectAtIndex: row] objectForKey: @"Message"];
291   
292    NSTableColumn * column = [tableView tableColumnWithIdentifier: @"Message"];
293    const CGFloat count = floorf([message sizeWithAttributes: fAttributes].width / [column width]);
294   
295    const CGFloat oldHeight = [tableView rowHeight] * (count + 1.0);
296    NSLog(@"oldHeight: %f", oldHeight);
297   
298    NSAttributedString * attributedMessage = [[NSAttributedString alloc] initWithString: message attributes: fAttributes];
299    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributedMessage);
300    [attributedMessage release];
301   
302    const CGSize size = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake([column width], CGFLOAT_MAX), NULL);
303    CFRelease(framesetter);
304   
305    NSLog(@"new %@", NSStringFromSize(size));
306    //NSLog(@"%@", fAttributes);
307   
308    CGFloat newHeight = size.height;
309   
310    //not sure why this is needed
311    const CGFloat numRows = newHeight / 13.0;
312    newHeight += numRows * 1.0;
313   
314    return newHeight;
315}
316
317- (void) tableView: (NSTableView *) tableView sortDescriptorsDidChange: (NSArray *) oldDescriptors
318{
319    [fDisplayedMessages sortUsingDescriptors: [fMessageTable sortDescriptors]];
320    [fMessageTable reloadData];
321}
322
323- (NSString *) tableView: (NSTableView *) tableView toolTipForCell: (NSCell *) cell rect: (NSRectPointer) rect
324                tableColumn: (NSTableColumn *) column row: (NSInteger) row mouseLocation: (NSPoint) mouseLocation
325{
326    NSDictionary * message = [fDisplayedMessages objectAtIndex: row];
327    return [message objectForKey: @"File"];
328}
329
330- (void) copy: (id) sender
331{
332    NSIndexSet * indexes = [fMessageTable selectedRowIndexes];
333    NSMutableArray * messageStrings = [NSMutableArray arrayWithCapacity: [indexes count]];
334   
335    for (NSDictionary * message in [fDisplayedMessages objectsAtIndexes: indexes])
336        [messageStrings addObject: [self stringForMessage: message]];
337   
338    NSString * messageString = [messageStrings componentsJoinedByString: @"\n"];
339   
340    NSPasteboard * pb = [NSPasteboard generalPasteboard];
341    [pb clearContents];
342    [pb writeObjects: [NSArray arrayWithObject: messageString]];
343}
344
345- (BOOL) validateMenuItem: (NSMenuItem *) menuItem
346{
347    SEL action = [menuItem action];
348   
349    if (action == @selector(copy:))
350        return [fMessageTable numberOfSelectedRows] > 0;
351   
352    return YES;
353}
354
355- (void) changeLevel: (id) sender
356{
357    NSInteger level;
358    switch ([fLevelButton indexOfSelectedItem])
359    {
360        case LEVEL_ERROR:
361            level = TR_MSG_ERR;
362            break;
363        case LEVEL_INFO:
364            level = TR_MSG_INF;
365            break;
366        case LEVEL_DEBUG:
367            level = TR_MSG_DBG;
368            break;
369        default:
370            NSAssert1(NO, @"Unknown message log level: %ld", [fLevelButton indexOfSelectedItem]);
371    }
372   
373    if ([[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"] == level)
374        return;
375   
376    [[NSUserDefaults standardUserDefaults] setInteger: level forKey: @"MessageLevel"];
377   
378    [fLock lock];
379   
380    [self updateListForFilter];
381   
382    [fLock unlock];
383}
384
385- (void) changeFilter: (id) sender
386{
387    [fLock lock];
388   
389    [self updateListForFilter];
390   
391    [fLock unlock];
392}
393
394- (void) clearLog: (id) sender
395{
396    [fLock lock];
397   
398    [fMessages removeAllObjects];
399   
400    const BOOL onLion = [NSApp isOnLionOrBetter];
401   
402    if (onLion)
403        [fMessageTable beginUpdates];
404   
405    if (onLion)
406        [fMessageTable removeRowsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fDisplayedMessages count])] withAnimation: NSTableViewAnimationSlideLeft];
407    [fDisplayedMessages removeAllObjects];
408   
409    if (onLion)
410        [fMessageTable endUpdates];
411    else
412        [fMessageTable reloadData];
413   
414    [fLock unlock];
415}
416
417- (void) writeToFile: (id) sender
418{
419    NSSavePanel * panel = [NSSavePanel savePanel];
420    [panel setAllowedFileTypes: [NSArray arrayWithObject: @"txt"]];
421    [panel setCanSelectHiddenExtension: YES];
422   
423    [panel setNameFieldStringValue: NSLocalizedString(@"untitled", "Save log panel -> default file name")];
424   
425    [panel beginSheetModalForWindow: [self window] completionHandler: ^(NSInteger result) {
426        if (result == NSFileHandlingPanelOKButton)
427        {
428            //make the array sorted by date
429            NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey: @"Index" ascending: YES];
430            NSArray * descriptors = [[NSArray alloc] initWithObjects: descriptor, nil];
431            NSArray * sortedMessages = [fDisplayedMessages sortedArrayUsingDescriptors: descriptors];
432            [descriptors release];
433           
434            //create the text to output
435            NSMutableArray * messageStrings = [NSMutableArray arrayWithCapacity: [sortedMessages count]];
436            for (NSDictionary * message in sortedMessages)
437                [messageStrings addObject: [self stringForMessage: message]];
438           
439            NSString * fileString = [messageStrings componentsJoinedByString: @"\n"];
440           
441            if (![fileString writeToFile: [[panel URL] path] atomically: YES encoding: NSUTF8StringEncoding error: nil])
442            {
443                NSAlert * alert = [[NSAlert alloc] init];
444                [alert addButtonWithTitle: NSLocalizedString(@"OK", "Save log alert panel -> button")];
445                [alert setMessageText: NSLocalizedString(@"Log Could Not Be Saved", "Save log alert panel -> title")];
446                [alert setInformativeText: [NSString stringWithFormat:
447                                            NSLocalizedString(@"There was a problem creating the file \"%@\".",
448                                                              "Save log alert panel -> message"), [[[panel URL] path] lastPathComponent]]];
449                [alert setAlertStyle: NSWarningAlertStyle];
450               
451                [alert runModal];
452                [alert release];
453            }
454        }
455    }];
456}
457
458@end
459
460@implementation MessageWindowController (Private)
461
462- (void) resizeColumn
463{
464    [fMessageTable noteHeightOfRowsWithIndexesChanged: [NSIndexSet indexSetWithIndexesInRange:
465                    NSMakeRange(0, [fMessageTable numberOfRows])]];
466}
467
468- (BOOL) shouldIncludeMessageForFilter: (NSString *) filterString message: (NSDictionary *) message
469{
470    if ([filterString isEqualToString: @""])
471        return YES;
472   
473    const NSStringCompareOptions searchOptions = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch;
474    return [[message objectForKey: @"Name"] rangeOfString: filterString options: searchOptions].location != NSNotFound
475            || [[message objectForKey: @"Message"] rangeOfString: filterString options: searchOptions].location != NSNotFound;
476}
477
478- (void) updateListForFilter
479{
480    const NSInteger level = [[NSUserDefaults standardUserDefaults] integerForKey: @"MessageLevel"];
481    NSString * filterString = [fFilterField stringValue];
482   
483    NSIndexSet * indexes = [fMessages indexesOfObjectsWithOptions: NSEnumerationConcurrent passingTest: ^BOOL(id message, NSUInteger idx, BOOL * stop) {
484        return [[(NSDictionary *)message objectForKey: @"Level"] integerValue] <= level && [self shouldIncludeMessageForFilter: filterString message: message];
485    }];
486   
487    NSArray * tempMessages = [[fMessages objectsAtIndexes: indexes] sortedArrayUsingDescriptors: [fMessageTable sortDescriptors]];
488   
489    const BOOL onLion = [NSApp isOnLionOrBetter];
490   
491    if (onLion)
492        [fMessageTable beginUpdates];
493   
494    //figure out which rows were added/moved
495    NSUInteger currentIndex = 0, totalCount = 0;
496    NSMutableArray * itemsToAdd = [NSMutableArray array];
497    NSMutableIndexSet * itemsToAddIndexes = [NSMutableIndexSet indexSet];
498   
499    for (NSDictionary * message in tempMessages)
500    {
501        const NSUInteger previousIndex = [fDisplayedMessages indexOfObject: message inRange: NSMakeRange(currentIndex, [fDisplayedMessages count]-currentIndex)];
502        if (previousIndex == NSNotFound)
503        {
504            [itemsToAdd addObject: message];
505            [itemsToAddIndexes addIndex: totalCount];
506        }
507        else
508        {
509            if (previousIndex != currentIndex)
510            {
511                [fDisplayedMessages moveObjectAtIndex: previousIndex toIndex: currentIndex];
512                if (onLion)
513                    [fMessageTable moveRowAtIndex: previousIndex toIndex: currentIndex];
514            }
515            ++currentIndex;
516        }
517       
518        ++totalCount;
519    }
520   
521    //remove trailing items - those are the unused
522    if (currentIndex < [fDisplayedMessages count])
523    {
524        const NSRange removeRange = NSMakeRange(currentIndex, [fDisplayedMessages count]-currentIndex);
525        [fDisplayedMessages removeObjectsInRange: removeRange];
526        if (onLion)
527            [fMessageTable removeRowsAtIndexes: [NSIndexSet indexSetWithIndexesInRange: removeRange] withAnimation: NSTableViewAnimationSlideDown];
528    }
529   
530    //add new items
531    [fDisplayedMessages insertObjects: itemsToAdd atIndexes: itemsToAddIndexes];
532    if (onLion)
533        [fMessageTable insertRowsAtIndexes: itemsToAddIndexes withAnimation: NSTableViewAnimationSlideUp];
534   
535    if (onLion)
536        [fMessageTable endUpdates];
537    else
538    {
539        [fMessageTable reloadData];
540       
541        if ([fDisplayedMessages count] > 0)
542            [fMessageTable deselectAll: self];
543    }
544   
545    NSAssert2([fDisplayedMessages isEqualToArray: tempMessages], @"Inconsistency between message arrays! %@ %@", fDisplayedMessages, tempMessages);
546}
547
548- (NSString *) stringForMessage: (NSDictionary *) message
549{
550    NSString * levelString;
551    const NSInteger level = [[message objectForKey: @"Level"] integerValue];
552    switch (level)
553    {
554        case TR_MSG_ERR:
555            levelString = NSLocalizedString(@"Error", "Message window -> level");
556            break;
557        case TR_MSG_INF:
558            levelString = NSLocalizedString(@"Info", "Message window -> level");
559            break;
560        case TR_MSG_DBG:
561            levelString = NSLocalizedString(@"Debug", "Message window -> level");
562            break;
563        default:
564            NSAssert1(NO, @"Unknown message log level: %ld", level);
565    }
566   
567    return [NSString stringWithFormat: @"%@ %@ [%@] %@: %@", [message objectForKey: @"Date"],
568            [message objectForKey: @"File"], levelString,
569            [message objectForKey: @"Name"], [message objectForKey: @"Message"], nil];
570}
571
572@end
Note: See TracBrowser for help on using the repository browser.