source: trunk/macosx/MessageWindowController.m @ 13318

Last change on this file since 13318 was 13318, checked in by livings124, 9 years ago

#4920 On Lion, use window restoration on the message log and stats window

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