Put NSAutoreleasePool usage around other distributed notification observer methods
[adiumx.git] / Source / AIAbstractLogViewerWindowController.m
blob24d4ac27a533fd737524b12dd707420a72698a32
1 //
2 //  AIAbstractLogViewerWindowController.m
3 //  Adium
4 //
5 //  Created by Evan Schoenberg on 3/24/06.
6 //
8 #import "AIAbstractLogViewerWindowController.h"
9 #import "AIChatLog.h"
10 #import "AILogFromGroup.h"
11 #import "AILogToGroup.h"
12 #import "AILoggerPlugin.h"
13 #import "ESRankingCell.h" 
14 #import "GBChatlogHTMLConverter.h"
15 #import "AILogDateFormatter.h"
17 #import <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIPreferenceControllerProtocol.h>
19 #import <Adium/AIChatControllerProtocol.h>
20 #import <Adium/AIContactControllerProtocol.h>
21 #import <Adium/AIContentControllerProtocol.h>
22 #import <Adium/AIMenuControllerProtocol.h>
23 #import <Adium/AIHTMLDecoder.h>
24 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIMetaContact.h>
27 #import <Adium/AIServiceIcons.h>
28 #import <Adium/AIUserIcons.h>
29 #import <Adium/KFTypeSelectTableView.h>
30 #import <Adium/KNShelfSplitView.h>
31 #import <AIUtilities/AIArrayAdditions.h>
32 #import <AIUtilities/AIAttributedStringAdditions.h>
33 #import <AIUtilities/AIDateFormatterAdditions.h>
34 #import <AIUtilities/AIFileManagerAdditions.h>
35 #import <AIUtilities/AIImageAdditions.h>
36 #import <AIUtilities/AIImageTextCell.h>
37 #import <AIUtilities/AIOutlineViewAdditions.h>
38 #import <AIUtilities/AISplitView.h>
39 #import <AIUtilities/AIStringAdditions.h>
40 #import <AIUtilities/AITableViewAdditions.h>
41 #import <AIUtilities/AITextAttributes.h>
42 #import <AIUtilities/AIToolbarUtilities.h>
43 #import <AIUtilities/AIApplicationAdditions.h>
44 #import <AIUtilities/AIDividedAlternatingRowOutlineView.h>
46 #define KEY_LOG_VIEWER_WINDOW_FRAME             @"Log Viewer Frame"
47 #define PREF_GROUP_CONTACT_LIST                 @"Contact List"
48 #define KEY_LOG_VIEWER_GROUP_STATE              @"Log Viewer Group State"       //Expand/Collapse state of groups
49 #define TOOLBAR_LOG_VIEWER                              @"Log Viewer Toolbar"
51 #define MAX_LOGS_TO_SORT_WHILE_SEARCHING        10000   //Max number of logs we will live sort while searching
52 #define LOG_SEARCH_STATUS_INTERVAL                      20      //1/60ths of a second to wait before refreshing search status
54 #define SEARCH_MENU                                             AILocalizedString(@"Search Menu",nil)
55 #define FROM                                                    AILocalizedString(@"From",nil)
56 #define TO                                                              AILocalizedString(@"To",nil)
57 #define DATE                                                    AILocalizedString(@"Date",nil)
58 #define CONTENT                                                 AILocalizedString(@"Content",nil)
59 #define DELETE                                                  AILocalizedString(@"Delete",nil)
60 #define DELETEALL                                               AILocalizedString(@"Delete All",nil)
61 #define SEARCH                                                  AILocalizedString(@"Search",nil)
63 #define HIDE_EMOTICONS                                  AILocalizedString(@"Hide Emoticons",nil)
64 #define SHOW_EMOTICONS                                  AILocalizedString(@"Show Emoticons",nil)
66 #define IMAGE_EMOTICONS_OFF                             @"emoticon32"
67 #define IMAGE_EMOTICONS_ON                              @"emoticon32_transparent"
69 #define REFRESH_RESULTS_INTERVAL                1.0 //Interval between results refreshes while searching
71 @interface AIAbstractLogViewerWindowController (PRIVATE)
72 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
73 - (void)initLogFiltering;
74 - (void)displayLog:(AIChatLog *)log;
75 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
76 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
77 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
78 - (void)buildSearchMenu;
79 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
80 - (void)_logContentFilter:(NSString *)searchString searchID:(int)searchID onSearchIndex:(SKIndexRef)logSearchIndex;
81 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode;
82 - (void)installToolbar;
83 - (void)updateRankColumnVisibility;
84 - (void)openLogAtPath:(NSString *)inPath;
85 - (void)rebuildContactsList;
86 - (void)filterForContact:(AIListContact *)inContact;
87 - (void)selectCachedIndex;
89 - (void)_willOpenForContact;
90 - (void)_didOpenForContact;
92 - (void)deleteSelection:(id)sender;
93 @end
95 @implementation AIAbstractLogViewerWindowController
97 static AIAbstractLogViewerWindowController      *sharedLogViewerInstance = nil;
98 static int toArraySort(id itemA, id itemB, void *context);
100 + (NSString *)nibName
102         return @"LogViewer";    
105 + (id)openForPlugin:(id)inPlugin
107     if (!sharedLogViewerInstance) {
108                 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
109         }
111     [sharedLogViewerInstance showWindow:nil];
112     
113         return sharedLogViewerInstance;
116 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
118         [self openForPlugin:inPlugin];
119         
120         [sharedLogViewerInstance openLogAtPath:inPath];
121         
122         return sharedLogViewerInstance;
125 //Open the log viewer window to a specific contact's logs
126 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
128     if (!sharedLogViewerInstance) {
129                 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
130         }
132         [sharedLogViewerInstance _willOpenForContact];
133         [sharedLogViewerInstance showWindow:nil];
134         [sharedLogViewerInstance filterForContact:inContact];
135         [sharedLogViewerInstance _didOpenForContact];
137     return sharedLogViewerInstance;
140 //Returns the window controller if one exists
141 + (id)existingWindowController
143     return sharedLogViewerInstance;
146 //Close the log viewer window
147 + (void)closeSharedInstance
149     if (sharedLogViewerInstance) {
150         [sharedLogViewerInstance closeWindow:nil];
151     }
154 //init
155 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
157     //init
158     plugin = inPlugin;
159     selectedColumn = nil;
160     activeSearchID = 0;
161     searching = NO;
162     automaticSearch = YES;
163     showEmoticons = NO;
164     activeSearchString = nil;
165     displayedLogArray = nil;
166     windowIsClosing = NO;
167         desiredContactsSourceListDeltaX = 0;
169     blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
171     sortDirection = YES;
172     searchMode = LOG_SEARCH_CONTENT;
173     headerDateFormatter = [[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString] 
174                                                                                                  allowNaturalLanguage:NO];
175     currentSearchResults = [[NSMutableArray alloc] init];
176     fromArray = [[NSMutableArray alloc] init];
177     fromServiceArray = [[NSMutableArray alloc] init];
178     logFromGroupDict = [[NSMutableDictionary alloc] init];
179     toArray = [[NSMutableArray alloc] init];
180     toServiceArray = [[NSMutableArray alloc] init];
181     logToGroupDict = [[NSMutableDictionary alloc] init];
182     resultsLock = [[NSRecursiveLock alloc] init];
183     searchingLock = [[NSLock alloc] init];
184         contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
186         allContactsIdentifier = [[NSNumber alloc] initWithInt:-1];
188         undoManager = [[NSUndoManager alloc] init];
190     [super initWithWindowNibName:windowNibName];
191         
192     return self;
195 //dealloc
196 - (void)dealloc
198     [resultsLock release];
199     [searchingLock release];
200     [fromArray release];
201     [fromServiceArray release];
202     [toArray release];
203     [toServiceArray release];
204     [currentSearchResults release];
205     [selectedColumn release];
206     [headerDateFormatter release];
207     [displayedLogArray release];
208     [blankImage release];
209     [activeSearchString release];
210         [contactIDsToFilter release];
212         [logFromGroupDict release]; logFromGroupDict = nil;
213         [logToGroupDict release]; logToGroupDict = nil;
215     [filterForAccountName release]; filterForAccountName = nil;
217         [horizontalRule release]; horizontalRule = nil;
219         [adiumIcon release]; adiumIcon = nil;
220         [adiumIconHighlighted release]; adiumIconHighlighted = nil;
221         
222         //We loaded     view_DatePicker from a nib manually, so we must release it
223         [view_DatePicker release]; view_DatePicker = nil;
225         [allContactsIdentifier release];
226     [undoManager release]; undoManager = nil;
228     [super dealloc];
231 //Init our log filtering tree
232 - (void)initLogFiltering
234     NSEnumerator                        *enumerator;
235     NSString                            *folderName;
236     NSMutableDictionary         *toDict = [NSMutableDictionary dictionary];
237     NSString                            *basePath = [AILoggerPlugin logBasePath];
238     NSString                            *fromUID, *serviceClass;
240     //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
241         //first by service, then by account name.
242         enumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:basePath] sortedArrayUsingSelector:@selector(compare:)] objectEnumerator];
243     while ((folderName = [enumerator nextObject])) {
244                 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
245                         NSEnumerator    *toEnum;
246                         AILogToGroup    *currentToGroup;                        
247                         AILogFromGroup  *logFromGroup;
248                         NSMutableSet    *toSetForThisService;
249                         NSArray         *serviceAndFromUIDArray;
250                         
251                         /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
252                          * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
253                          */
254                         serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
256                         if ([serviceAndFromUIDArray count] >= 2) {
257                                 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
259                                 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
260                                 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
261                         } else {
262                                 //Fallback: blank non-nil serviceClass; folderName as the fromUID
263                                 serviceClass = @"";
264                                 fromUID = folderName;
265                         }
267                         logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
269                         //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
270                         [logFromGroupDict setObject:logFromGroup forKey:folderName];
272                         //To processing
273                         if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
274                                 toSetForThisService = [NSMutableSet set];
275                                 [toDict setObject:toSetForThisService
276                                                    forKey:serviceClass];
277                         }
279                         //Add the 'to' for each grouping on this account
280                         toEnum = [[logFromGroup toGroupArray] objectEnumerator];
281                         while ((currentToGroup = [toEnum nextObject])) {
282                                 NSString        *currentTo;
284                                 if ((currentTo = [currentToGroup to])) {
285                                         //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
286                                         [logToGroupDict setObject:currentToGroup forKey:[currentToGroup path]];
287                                 }
288                         }
290                         [logFromGroup release];
291                 }
292         }
294         [self rebuildContactsList];
297 - (void)rebuildContactsList
299         NSEnumerator    *enumerator = [logFromGroupDict objectEnumerator];
300         AILogFromGroup  *logFromGroup;
301         
302         int     oldCount = [toArray count];
303         [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
305         while ((logFromGroup = [enumerator nextObject])) {
306                 NSEnumerator    *toEnum;
307                 AILogToGroup    *currentToGroup;
308                 NSString                *serviceClass = [logFromGroup serviceClass];
310                 //Add the 'to' for each grouping on this account
311                 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
312                 while ((currentToGroup = [toEnum nextObject])) {
313                         NSString        *currentTo;
314                         
315                         if ((currentTo = [currentToGroup to])) {
316                                 AIListObject *listObject = ((serviceClass && currentTo) ?
317                                                                                         [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
318                                                                                                                                                                                                                                                                                          UID:currentTo]] :
319                                                                                         nil);
320                                 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
321                                         AIListContact *parentContact = [(AIListContact *)listObject parentContact];
322                                         if (![toArray containsObjectIdenticalTo:parentContact]) {
323                                                 [toArray addObject:parentContact];
324                                         }
325                                         
326                                 } else {
327                                         if (![toArray containsObject:currentToGroup]) {
328                                                 [toArray addObject:currentToGroup];
329                                         }
330                                 }
331                         }
332                 }               
333         }
334         
335         [toArray sortUsingFunction:toArraySort context:NULL];
336         [outlineView_contacts reloadData];
338         if (!isOpeningForContact) {
339                 //If we're opening for a contact, the outline view selection will be changed in a moment anyways
340                 [self outlineViewSelectionDidChange:nil];
341         }
345 - (NSString *)adiumFrameAutosaveName
347         return KEY_LOG_VIEWER_WINDOW_FRAME;
350 //Setup the window before it is displayed
351 - (void)windowDidLoad
353         suppressSearchRequests = YES;
355         [super windowDidLoad];
357         [plugin pauseIndexing];
359         [[self window] setTitle:AILocalizedString(@"Chat Transcript Viewer",nil)];
360     [textField_progress setStringValue:@""];
362         //Autosave doesn't do anything yet
363         [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
364         [shelf_splitView setFrame:[[[self window] contentView] frame]];
366         // Pull our main article/display split view out of the nib and position it in the shelf view
367         [containingView_results retain];
368         [containingView_results removeFromSuperview];
369         [shelf_splitView setContentView:containingView_results];
370         [containingView_results release];
371         [tableView_results accessibilitySetOverrideValue:AILocalizedString(@"Transcripts", nil)
372                                                                                 forAttribute:NSAccessibilityRoleDescriptionAttribute];
373         
374         // Pull our source view out of the nib and position it in the shelf view
375         [containingView_contactsSourceList retain];
376         [containingView_contactsSourceList removeFromSuperview];
377         [shelf_splitView setShelfView:containingView_contactsSourceList];
378         [outlineView_contacts accessibilitySetOverrideValue:AILocalizedString(@"Contacts", nil)
379                                                                                 forAttribute:NSAccessibilityRoleDescriptionAttribute];
380         [containingView_contactsSourceList release];
382     //Set emoticon filtering
383     showEmoticons = [[[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
384                                                               group:PREF_GROUP_LOGGING] boolValue];
385     [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
386     [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
388         //Toolbar
389         [self installToolbar];  
391         //This is the Mail.app source list background color... which differs from the iTunes one.
392         [outlineView_contacts setBackgroundColor:[NSColor colorWithCalibratedRed:.9059
393                                                                                                                                            green:.9294
394                                                                                                                                                 blue:.9647
395                                                                                                                                            alpha:1.0]];
397         AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
398         NSTableColumn   *tableColumn = [[outlineView_contacts tableColumns] objectAtIndex:0];
399         [tableColumn setDataCell:dataCell];
400         [tableColumn setEditable:NO];
401         [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
402         [dataCell release];
404         [outlineView_contacts setDrawsGradientSelection:YES];
405         // Set the selector for doubleAction
406         [outlineView_contacts setDoubleAction:@selector(openChatOnDoubleAction:)];
407         
408         //Localize tableView_results column headers
409         [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
410         [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
411         [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
412         [self tableViewColumnDidResize:nil];
414         [tableView_results sizeLastColumnToFit];
416     //Prepare the search controls
417     [self buildSearchMenu];
418     if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
419                 [textView_content setUsesFindPanel:YES];
420     }
422     //Sort by preference, defaulting to sorting by date
423         NSString        *selectedTableColumnPref;
424         if ((selectedTableColumnPref = [[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
425                                                                                                                                                    group:PREF_GROUP_LOGGING])) {
426                 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
427         }
428         if (!selectedColumn) {
429                 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
430         }
431         [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
433     //Prepare indexing and filter searching
434         [plugin prepareLogContentSearching];
435     [self initLogFiltering];
437     //Begin our initial search
438         [self setSearchMode:LOG_SEARCH_TO];
440     [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
441         suppressSearchRequests = NO;
443         if (!isOpeningForContact) {
444                 //If we're opening for a contact, we'll select it and then begin searching
445                 [self startSearchingClearingCurrentResults:YES];
446         }
448         [plugin resumeIndexing];
451 -(void)rebuildIndices
453     //Rebuild the 'global' log indexes
454     [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
455     [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
456     [toServiceArray removeAllObjects];
457     [fromArray removeAllObjects];
458     [fromServiceArray removeAllObjects];
459     
460     [self initLogFiltering];
461     
462     [tableView_results reloadData];
463     [self selectDisplayedLog];
466 //Called as the window closes
467 - (void)windowWillClose:(id)sender
469         [super windowWillClose:sender];
471         //Set preference for emoticon filtering
472         [[adium preferenceController] setPreference:[NSNumber numberWithBool:showEmoticons]
473                                                                                  forKey:KEY_LOG_VIEWER_EMOTICONS
474                                                                                   group:PREF_GROUP_LOGGING];
475         
476         //Set preference for selected column
477         [[adium preferenceController] setPreference:[selectedColumn identifier]
478                                                                                  forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
479                                                                                   group:PREF_GROUP_LOGGING];
481     /* Disable the search field.  If we don't disable the search field, it will often try to call its target action
482      * after the window has closed (and we are gone).  I'm not sure why this happens, but disabling the field
483      * before we close the window down seems to prevent the crash.
484          */
485     [searchField_logs setEnabled:NO];
486         
487         /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
488          * it was gone, like responding to a logIndexUpdated message
489          */
490         windowIsClosing = YES;
492     //Abort any in-progress searching and indexing, and wait for their completion
493     [self stopSearching];
494     [plugin cleanUpLogContentSearching];
496         //Reset our column widths if needed
497         [activeSearchString release]; activeSearchString = nil;
498         [self updateRankColumnVisibility];
499         
500         [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
501         [toolbarItems autorelease]; toolbarItems = nil;
504 //Display --------------------------------------------------------------------------------------------------------------
505 #pragma mark Display
506 //Update log viewer progress string to reflect current status
507 - (void)updateProgressDisplay
509     NSMutableString     *progress = nil;
510     int                                 indexNumber, indexTotal;
511     BOOL                                indexing;
513     //We always convey the number of logs being displayed
514     [resultsLock lock];
515         unsigned count = [currentSearchResults count];
516     if (activeSearchString && [activeSearchString length]) {
517                 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
518                                                                                                                                                            AILocalizedString(@"%i matching transcripts",nil) :
519                                                                                                                                                            AILocalizedString(@"1 matching transcript",nil)),count]];
520     } else {
521                 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
522                                                                                                                                                            AILocalizedString(@"%i transcripts",nil) :
523                                                                                                                                                            AILocalizedString(@"1 transcript",nil)),count]];
524                 
525                 //We are searching, but there is no active search  string. This indicates we're still opening logs.
526                 if (searching) {
527                         progress = [[AILocalizedString(@"Opening transcripts",nil) mutableCopy] autorelease];                   
528                 }
529     }
530     [resultsLock unlock];
532         indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
534     //Append search progress
535     if (activeSearchString && [activeSearchString length]) {
536                 if (progress) {
537                         [progress appendString:@" - "];
538                 } else {
539                         progress = [NSMutableString string];
540                 }
542                 if (searching || indexing) {
543                         [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
544                 } else {
545                         [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];                     
546                 }
547         }
549     //Append indexing progress
550     if (indexing) {
551                 if (progress) {
552                         [progress appendString:@" - "];
553                 } else {
554                         progress = [NSMutableString string];
555                 }
556                 
557                 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %i of %i transcripts",nil), indexNumber, indexTotal]];
558     }
559         
560         if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
561                 [progress appendString:[NSString ellipsis]];    
562         }
564     //Enable/disable the searching animation
565     if (searching || indexing) {
566                 [progressIndicator startAnimation:nil];
567     } else {
568                 [progressIndicator stopAnimation:nil];
569     }
570     
571     [textField_progress setStringValue:(progress ? progress : @"")];
574 //The plugin is informing us that the log indexing changed
575 - (void)logIndexingProgressUpdate
577         //Don't do anything if the window is already closing
578         if (!windowIsClosing) {
579                 [self updateProgressDisplay];
580                 
581                 //If we are searching by content, we should re-search without clearing our current results so the
582                 //the newly-indexed logs can be added without blanking the current table contents.
583                 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
584                         if (searching) {
585                                 //We're already searching; reattempt when done
586                                 searchIDToReattemptWhenComplete = activeSearchID;
587                         } else {
588                                 //We're not searching - restart the search immediately every 10 updates to utilize the newly indexed logs
589                                 indexingUpdatesReceivedWhileSearching++;
590                                 if ((indexingUpdatesReceivedWhileSearching % 10) == 0)
591                                         [self startSearchingClearingCurrentResults:NO];
592                         }
593                 }
594         }
597 //Refresh the results table
598 - (void)refreshResults
600         [self updateProgressDisplay];
602         [self refreshResultsSearchIsComplete:NO];
605 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
607     [resultsLock lock];
608     int count = [currentSearchResults count];
609     [resultsLock unlock];
610         AILog(@"refreshResultsSearchIsComplete: %i (count is %i)",searchIsComplete,count);
611     if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
612                 //Sort the logs correctly which will also reload the table
613                 [self resortLogs];
614                 
615                 if (searchIsComplete && automaticSearch) {
616                         //If search is complete, select the first log if requested and possible
617                         [self selectFirstLog];
618                         
619                 } else {
620                         BOOL oldAutomaticSearch = automaticSearch;
622                         //We don't want the above re-selection to change our automaticSearch tracking
623                         //(The only reason automaticSearch should change is in response to user action)
624                         automaticSearch = oldAutomaticSearch;
625                 }
626     }
627         
628         if (searchIsComplete &&
629                 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
630                 searchIDToReattemptWhenComplete = -1;
631                 [self startSearchingClearingCurrentResults:NO];
632         }
633         
634         if(deleteOccurred)
635                 [self selectCachedIndex];
637     //Update status
638     [self updateProgressDisplay];
641 - (void)searchComplete
643         [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
644         [self refreshResultsSearchIsComplete:YES];
647 // Called on doubleAction to open a chat
648 -(void)openChatOnDoubleAction:(id)sender
650         id item = [outlineView_contacts firstSelectedItem];
651         if ([item isKindOfClass:[AIListContact class]]) {
652                 //Open a new message with the contact
653                 [[adium interfaceController] setActiveChat:[[adium chatController] openChatWithContact:(AIListContact *)item onPreferredAccount:YES]];
654         }
657 //Displays the contents of the specified log in our window
658 - (void)displayLogs:(NSArray *)logArray;
659 {       
660     NSMutableAttributedString   *displayText = nil;
661         NSAttributedString                      *finalDisplayText = nil;
662         NSRange                                         scrollRange = NSMakeRange(0,0);
663         BOOL                                            appendedFirstLog = NO;
665     if (![logArray isEqualToArray:displayedLogArray]) {
666                 [displayedLogArray release];
667                 displayedLogArray = [logArray copy];
668         }
670         if ([logArray count] > 1) {
671                 displayText = [[NSMutableAttributedString alloc] init];
672         }
674         NSEnumerator *enumerator = [logArray objectEnumerator];
675         AIChatLog        *theLog;
676         NSString         *logBasePath = [AILoggerPlugin logBasePath];
677         AILog(@"Displaying %@",logArray);
678         while ((theLog = [enumerator nextObject])) {
679                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
680                 
681                 if (displayText) {
682                         if (!horizontalRule) {
683                                 #define HORIZONTAL_BAR                  0x2013
684                                 #define HORIZONTAL_RULE_LENGTH  18
685                                 
686                                 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
687                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
688                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
689                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
690                                 };
691                                 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
692                         }       
693                         
694                         [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
695                                 (appendedFirstLog ? @"\n" : @""),
696                                 horizontalRule,
697                                 [headerDateFormatter stringFromDate:[theLog date]],
698                                 [theLog to],
699                                 horizontalRule]
700                                            withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
701                 }
702                 
703                 if ([[theLog path] hasSuffix:@".AdiumHTMLLog"] || [[theLog path] hasSuffix:@".html"] || [[theLog path] hasSuffix:@".html.bak"]) {
704                         //HTML log
705                         NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
706                         
707                         if (displayText) {
708                                 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
709                         } else {
710                                 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
711                         }
713                 } else if ([[theLog path] hasSuffix:@".chatlog"]){
714                         //XML log
715                         NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog path]];
716                         
717                         //If this log begins with a malformed UTF-8 BOM (which was written out by Adium for a brief time between 1.0b7 and 1.0b8), fix it before trying to read it in.
718                         enum {
719                                 failedUtf8BomLength = 6
720                         };
721                         NSData *data = [NSData dataWithContentsOfMappedFile:logFullPath];
722                         const unsigned char *ptr = [data bytes];
723                         unsigned len = [data length];
724                         if ((len >= failedUtf8BomLength)
725                                 &&  (ptr[0] == 0xC3)
726                                 &&  (ptr[1] == 0x94)
727                                 &&  (ptr[2] == 0xC2)
728                                 &&  (ptr[3] == 0xAA)
729                                 &&  (ptr[4] == 0xC3)
730                                 &&  (ptr[5] == 0xB8)
731                                 ) {
732                                 //Yup. Back up the old file, then strip it off.
733                                 NSLog(@"Transcript file at %@ has unwanted bytes at the front of it. (This is a bug in a previous version of Adium, not this version.) Attempting recovery.", logFullPath);
734                                 NSString *backupPath = [logFullPath stringByAppendingPathExtension:@"bak"];
735                                 if(![[NSFileManager defaultManager] movePath:logFullPath toPath:backupPath handler:nil])
736                                         NSLog(@"Could not back up file; recovery failed. This transcript will probably appear blank in the transcript viewer.");
737                                 else {
738                                         NSRange range = { failedUtf8BomLength, len - failedUtf8BomLength };
739                                         NSData *theRestOfIt = [data subdataWithRange:range];
740                                         if([theRestOfIt writeToFile:logFullPath atomically:YES])
741                                                 NSLog(@"Wrote fixed version to same file. The corrupted version was renamed to %@; you may remove this file at your leisure after you are satisfied that the recovery succeeded. You can test this by viewing the transcript (%@) in the transcript viewer.", backupPath, [logFullPath lastPathComponent]);
742                                         else
743                                                 NSLog(@"Could not write fix!");
744                                 }
745                         }
746                         NSString *logFileText = [GBChatlogHTMLConverter readFile:logFullPath];
748                         if (logFileText) {
749                                 if (displayText)
750                                         [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
751                                 else
752                                         displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
753                         }
755                 } else {
756                         //Fallback: Plain text log
757                         NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
758                         if (logFileText) {
759                                 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
760                                 
761                                 if (displayText) {
762                                         [displayText appendAttributedString:[[[NSAttributedString alloc] initWithString:logFileText 
763                                                                                                                                                                                  attributes:[textAttributes dictionary]] autorelease]];
764                                 } else {
765                                         displayText = [[NSMutableAttributedString alloc] initWithString:logFileText attributes:[textAttributes dictionary]];
766                                 }
767                         }
768                 }
769                 
770                 appendedFirstLog = YES;
771                 
772                 [pool release];
773         }
774         
775         if (displayText && [displayText length]) {
776                 //Add pretty formatting to links
777                 [displayText addFormattingForLinks];
779                 //If we are searching by content, highlight the search results
780                 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
781                         NSEnumerator                            *enumerator;
782                         NSString                                        *searchWord;
783                         NSMutableArray                          *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
784                         NSScanner                                       *scanner = [NSScanner scannerWithString:activeSearchString];
785                         
786                         //Look for an initial quote
787                         while (![scanner isAtEnd]) {
788                                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
789                                 
790                                 [scanner scanUpToString:@"\"" intoString:NULL];
791                                 
792                                 //Scan past the quote
793                                 if (![scanner scanString:@"\"" intoString:NULL]) continue;
794                                 
795                                 NSString *quotedString;
796                                 //And a closing one
797                                 if (![scanner isAtEnd] &&
798                                         [scanner scanUpToString:@"\"" intoString:&quotedString]) {
799                                         //Scan past the quote
800                                         [scanner scanString:@"\"" intoString:NULL];
801                                         /* If a string within quotes is found, remove the words from the quoted string and add the full string
802                                          * to what we'll be highlighting.
803                                          *
804                                          * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
805                                          * "killer attack ninja kittens" OR ninja
806                                          * wouldn't highlight the word ninja by itself.
807                                          */
808                                         NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
809                                         int quotedWordsCount = [quotedWords count];
810                                         
811                                         for (int i = 0; i < quotedWordsCount; i++) {
812                                                 NSString        *quotedWord = [quotedWords objectAtIndex:i];
813                                                 if (i == 0) {
814                                                         //Originally started with a quote, so put it back on
815                                                         quotedWord = [@"\"" stringByAppendingString:quotedWord];
816                                                 }
817                                                 if (i == quotedWordsCount - 1) {
818                                                         //Originally ended with a quote, so put it back on
819                                                         quotedWord = [quotedWord stringByAppendingString:@"\""];
820                                                 }
821                                                 int searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
822                                                 if (searchWordsIndex != NSNotFound) {
823                                                         [searchWordsArray removeObjectAtIndex:searchWordsIndex];
824                                                 } else {
825                                                         NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
826                                                 }
827                                         }
828                                         
829                                         //Add the full quoted string
830                                         [searchWordsArray addObject:quotedString];
831                                 }
832                                 [pool release];
833                         }
835                         BOOL shouldScrollToWord = NO;
836                         scrollRange = NSMakeRange([displayText length],0);
838                         enumerator = [searchWordsArray objectEnumerator];
839                         while ((searchWord = [enumerator nextObject])) {
840                                 NSRange     occurrence;
841                                 
842                                 //Check against and/or.  We don't just remove it from the array because then we couldn't check case insensitively.
843                                 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
844                                         ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
845                                         [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
846                                         
847                                         //We'll want to scroll to the first occurrance of any matching word or words
848                                         if (occurrence.location < scrollRange.location) {
849                                                 scrollRange = occurrence;
850                                                 shouldScrollToWord = YES;
851                                         }
852                                 }
853                         }
854                         
855                         //If we shouldn't be scrolling to a new range, we want to scroll to the top
856                         if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
857                         
858                         [searchWordsArray release];
859                 }
860                 
861                 //Filter emoticons
862                 if (showEmoticons) {
863                         finalDisplayText = [[adium contentController] filterAttributedString:displayText
864                                                                                                                                  usingFilterType:AIFilterMessageDisplay
865                                                                                                                                            direction:AIFilterOutgoing
866                                                                                                                                                  context:nil];
867                 } else {
868                         finalDisplayText = displayText;
869                 }
870         }
872         if (finalDisplayText) {
873                 [[textView_content textStorage] setAttributedString:finalDisplayText];
875                 //Set this string and scroll to the top/bottom/occurrence
876                 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
877                         [textView_content scrollRangeToVisible:scrollRange];
878                 } else {
879                         [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
880                 }
882         } else {
883                 //No log selected, empty the view
884                 [textView_content setString:@""];
885         }
887         [displayText release];
890 - (void)displayLog:(AIChatLog *)theLog
892         [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
895 //Reselect the displayed log (Or another log if not possible)
896 - (void)selectDisplayedLog
898     int     firstIndex = NSNotFound;
899     
900     /* Is the log we had selected still in the table?
901          * (When performing an automatic search, we ignore the previous selection.  This ensures that we always
902      * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
903          */
904         if (!automaticSearch) {
905                 [resultsLock lock];
906                 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
907                 [resultsLock unlock];
908                 
909                 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
910         }
912         if (firstIndex != NSNotFound) {
913                 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
914     } else {
915         if (useSame == YES && sameSelection > 0) {
916             [tableView_results selectRow:sameSelection byExtendingSelection:NO];
917         } else {
918             [self selectFirstLog];
919         }
920     }
922     useSame = NO;
925 - (void)selectFirstLog
927         AIChatLog   *theLog = nil;
928         
929         //If our selected log is no more, select the first one in the list
930         [resultsLock lock];
931         if ([currentSearchResults count] != 0) {
932                 theLog = [currentSearchResults objectAtIndex:0];
933         }
934         [resultsLock unlock];
935         
936         //Change the table selection to this new log
937         //We need a little trickery here.  When we change the row, the table view will call our tableViewSelectionDidChange: method.
938         //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
939         //search.  As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
940         //that we instantiated this selection change, and not the user.
941         ignoreSelectionChange = YES;
942         [tableView_results selectRow:0 byExtendingSelection:NO];
943         [tableView_results scrollRowToVisible:0];
944         ignoreSelectionChange = NO;
946         [self displayLog:theLog];  //Manually update the displayed log
949 //Highlight the occurences of a search string within a displayed log
950 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
952     int                                 location = 0;
953     NSRange                             searchRange, foundRange;
954     NSString                    *plainBigString = [bigString string];
955         unsigned                        plainBigStringLength = [plainBigString length];
956         NSMutableDictionary *attributeDictionary = nil;
958     outRange->location = NSNotFound;
960     //Search for the little string in the big string
961     while (location != NSNotFound && location < plainBigStringLength) {
962         searchRange = NSMakeRange(location, plainBigStringLength-location);
963         foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
964                 
965                 //Bold and color this match
966         if (foundRange.location != NSNotFound) {
967                         if (outRange->location == NSNotFound) *outRange = foundRange;
969                         if (!attributeDictionary) {
970                                 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
971                                         [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
972                                         [NSColor yellowColor], NSBackgroundColorAttributeName,
973                                         nil];
974                         }
975                         [bigString addAttributes:attributeDictionary
976                                                            range:foundRange];
977         }
979         location = NSMaxRange(foundRange);
980     }
984 //Sorting --------------------------------------------------------------------------------------------------------------
985 #pragma mark Sorting
986 - (void)resortLogs
988         NSString *identifier = [selectedColumn identifier];
990     //Resort the data
991         [resultsLock lock];
992     if ([identifier isEqualToString:@"To"]) {
993                 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
994                 
995     } else if ([identifier isEqualToString:@"From"]) {
996         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
997                 
998     } else if ([identifier isEqualToString:@"Date"]) {
999         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
1000                 
1001     } else if ([identifier isEqualToString:@"Rank"]) {
1002             [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
1003         }
1004         
1005     [resultsLock unlock];
1007     //Reload the data
1008     [tableView_results reloadData];
1010     //Reapply the selection
1011     [self selectDisplayedLog];  
1014 //Sorts the selected log array and adjusts the selected column
1015 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
1017     //If there already was a sorted column, remove the indicator image from it.
1018     if (selectedColumn && selectedColumn != tableColumn) {
1019         [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
1020     }
1021     
1022     //Set the indicator image in the newly selected column
1023     [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
1024                            inTableColumn:tableColumn];
1025     
1026     //Set the highlighted table column.
1027     [tableView_results setHighlightedTableColumn:tableColumn];
1028     [selectedColumn release]; selectedColumn = [tableColumn retain];
1029     sortDirection = direction;
1030         
1031         [self resortLogs];
1034 //Searching ------------------------------------------------------------------------------------------------------------
1035 #pragma mark Searching
1036 //(Jag)Change search string
1037 - (void)controlTextDidChange:(NSNotification *)notification
1039     if (searchMode != LOG_SEARCH_CONTENT) {
1040                 [self updateSearch:nil];
1041     }
1044 //Change search string (Called by searchfield)
1045 - (IBAction)updateSearch:(id)sender
1047     automaticSearch = NO;
1048     [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1049         AILog(@"updateSearch calling startSearching");
1050     [self startSearchingClearingCurrentResults:YES];
1053 //Change search mode (Called by mode menu)
1054 - (IBAction)selectSearchType:(id)sender
1056     automaticSearch = NO;
1058         //First, update the search mode to the newly selected type
1059     [self setSearchMode:[sender tag]]; 
1060         
1061         //Then, ensure we are ready to search using the current string
1062         [self setSearchString:activeSearchString];
1064         //Now we are ready to start searching
1065         AILog(@"selectSearchType calling startSearching");
1066     [self startSearchingClearingCurrentResults:YES];
1069 //Begin a specific search
1070 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1072     automaticSearch = YES;
1073         //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1074     [self setSearchMode:inMode];
1075     [self setSearchString:inString];
1077         AILog(@"setSearchString:mode: calling startSearching");
1078     [self startSearchingClearingCurrentResults:YES];
1081 //Begin the current search
1082 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1084     NSDictionary    *searchDict;
1086         if (suppressSearchRequests) return;
1087         AILog(@"Starting a search for %@",activeSearchString);
1089     //Once all searches have exited, we can start a new one
1090         if (clearCurrentResults) {
1091                 [resultsLock lock];
1092                 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1093                 [self stopSearching];
1095                 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1096                 [resultsLock unlock];
1097         } else {
1098             //Stop any existing searches
1099                 [self stopSearching];   
1100         }
1102         searching = YES;
1103         indexingUpdatesReceivedWhileSearching = 0;
1104     searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1105                 [NSNumber numberWithInt:activeSearchID], @"ID",
1106                 [NSNumber numberWithInt:searchMode], @"Mode",
1107                 activeSearchString, @"String",
1108                 [plugin logContentIndex], @"SearchIndex",
1109                 nil];
1110     [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1111     
1112         //Update the table periodically while the logs load.
1113         [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1114         refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1115                                                                                                                         target:self
1116                                                                                                                   selector:@selector(refreshResults)
1117                                                                                                                   userInfo:nil
1118                                                                                                                    repeats:YES] retain];
1121 //Abort any active searches
1122 - (void)stopSearching
1124         [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
1126     //Increase the active search ID so any existing searches stop, and then
1127     //wait for any active searches to finish and release the lock
1128     activeSearchID++;
1131 //Set the active search mode (Does not invoke a search)
1132 - (void)setSearchMode:(LogSearchMode)inMode
1134         NSTextFieldCell *cell = [searchField_logs cell];
1135         
1136     searchMode = inMode;
1137         
1138         //Clear any filter from the table if it's the current mode, as well
1139         switch (searchMode) {
1140                 case LOG_SEARCH_FROM:
1141                         [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1142                         break;
1144                 case LOG_SEARCH_TO:
1145                         [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1146                         break;
1147                         
1148                 case LOG_SEARCH_DATE:
1149                         [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1150                         break;
1152                 case LOG_SEARCH_CONTENT:
1153                         [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1154                         break;
1155         }
1157         [self updateRankColumnVisibility];
1158     [self buildSearchMenu];
1161 - (void)updateRankColumnVisibility
1163         NSTableColumn   *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1164         
1165         if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1166                 //Add the resultsColumn and resize if it should be shown but is not at present
1167                 if (!resultsColumn) {   
1168                         NSArray                 *tableColumns;
1170                         //Set up the results column
1171                         resultsColumn = [[NSTableColumn alloc] initWithIdentifier:@"Rank"];
1172                         [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1173                         [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1174                         
1175                         //Add it to the table
1176                         [tableView_results addTableColumn:resultsColumn];
1178                         //Make it half again as large as the desired width from the @"Rank" header title
1179                         [resultsColumn sizeToFit];
1180                         [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1181                         
1182                         tableColumns = [tableView_results tableColumns];
1183                         if ([tableColumns indexOfObject:resultsColumn] > 0) {
1184                                 NSTableColumn   *nextDoorNeighbor;
1186                                 //Adjust the column to the results column's left so results is now visible
1187                                 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1188                                 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1189                         }
1190                 }
1191         } else {
1192                 //Remove the resultsColumn and resize if it should not be shown but is at present
1193                 if (resultsColumn) {
1194                         NSArray                 *tableColumns;
1196                         tableColumns = [tableView_results tableColumns];
1197                         if ([tableColumns indexOfObject:resultsColumn] > 0) {
1198                                 NSTableColumn   *nextDoorNeighbor;
1200                                 //Adjust the column to the results column's left to take up the space again
1201                                 tableColumns = [tableView_results tableColumns];
1202                                 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1203                                 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1204                         }
1206                         //Remove it
1207                         [tableView_results removeTableColumn:resultsColumn];
1208                 }
1209         }
1212 //Set the active search string (Does not invoke a search)
1213 - (void)setSearchString:(NSString *)inString
1215     if (![[searchField_logs stringValue] isEqualToString:inString]) {
1216                 [searchField_logs setStringValue:(inString ? inString : @"")];
1217     }
1218         
1219         //Use autorelease so activeSearchString can be passed back to here
1220         if (activeSearchString != inString) {
1221                 [activeSearchString release];
1222                 activeSearchString = [inString retain];
1223         }
1225         [self updateRankColumnVisibility];
1228 //Build the search mode menu
1229 - (void)buildSearchMenu
1231     NSMenu  *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1232     [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1233     [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1234     [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1235     [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1237         [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1240 - (void)_willOpenForContact
1242         isOpeningForContact = YES;
1245 - (void)_didOpenForContact
1247         isOpeningForContact = NO;
1251  * @brief Focus the log viewer on a particular contact
1253  * If the contact is within a metacontact, the metacontact will be focused.
1254  */
1255 - (void)filterForContact:(AIListContact *)inContact
1257         AIListContact *parentContact = [inContact parentContact];
1259         if (!isOpeningForContact) {
1260                 /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1261                 * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1262                 * with regard to inContact since the log viewer opened.
1263                 *
1264                 * If we're opening initially, the list is guaranteed fresh.
1265                 */
1266                 [self rebuildContactsList];
1267         }
1269         //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1270         if (searchMode == LOG_SEARCH_TO) {
1271                 [self setSearchMode:LOG_SEARCH_CONTENT];
1272                 
1273                 //Update our search string to ensure we're configured for content searching
1274                 [self setSearchString:activeSearchString];
1275         }
1277         //Changing the selection will start a new search
1278         [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1279         unsigned int selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1280         if (selectedRow != NSNotFound) {
1281                 [outlineView_contacts scrollRowToVisible:selectedRow];
1282         }
1286  * @brief Returns a menu item for the search mode menu
1287  */
1288 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1290     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title 
1291                                                                                                                                                                  action:@selector(selectSearchType:) 
1292                                                                                                                                                   keyEquivalent:@""];
1293     [menuItem setTag:mode];
1294     [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1295     
1296     return [menuItem autorelease];
1299 #pragma mark Filtering search results
1301 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1303         BOOL matchesDateFilter;
1305         switch (filterDateType) {
1306                 case AIDateTypeAfter:
1307                         matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1308                         break;
1309                 case AIDateTypeBefore:
1310                         matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1311                         break;
1312                 case AIDateTypeExactly:
1313                         matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1314                         break;
1315                 default:
1316                         matchesDateFilter = YES;
1317                         break;
1318         }
1320         return matchesDateFilter;
1324 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1326         CFURLRef        url = SKDocumentCopyURL(inDocument);
1327         CFStringRef logPath = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
1328         NSArray         *pathComponents = [(NSString *)logPath pathComponents];
1329         
1330         CFRelease(url);
1331         CFRelease(logPath);
1332         
1333         return pathComponents;
1337  * @brief Should a search display a document with the given information?
1338  */
1339 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1341         BOOL shouldDisplayDocument = YES;
1343         if ([contactIDsToFilter count]) {
1344                 //Determine the path components if we weren't supplied them
1345                 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1347                 unsigned int numPathComponents = [pathComponents count];
1348                 
1349                 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1350                 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1352                 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1354                 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1355         } 
1356         
1357         if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1358                 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1360                 unsigned int    numPathComponents = [pathComponents count];
1361                 NSString                *toPath = [NSString stringWithFormat:@"%@/%@",
1362                         [pathComponents objectAtIndex:numPathComponents-3],
1363                         [pathComponents objectAtIndex:numPathComponents-2]];
1364                 NSString                *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1365                 AIChatLog               *theLog;
1366                 
1367                 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1368                 
1369                 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1370         }
1372         return shouldDisplayDocument;
1375 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1376 #pragma mark Threaded filter/search methods
1377 //Search the logs, filtering out any matching logs into the currentSearchResults
1378 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1380     NSAutoreleasePool       *pool = [[NSAutoreleasePool alloc] init];
1381     int                     mode = [[searchInfoDict objectForKey:@"Mode"] intValue];
1382     int                     searchID = [[searchInfoDict objectForKey:@"ID"] intValue];
1383     NSString                *searchString = [searchInfoDict objectForKey:@"String"];
1385     if (searchID == activeSearchID) { //If we're still supposed to go
1386                 searching = YES;
1387                 AILog(@"filterLogsWithSearch (search ID %i): %@",searchID,searchInfoDict);
1388                 //Search
1389                 [plugin pauseIndexing];
1390                 if (searchString && [searchString length]) {
1391                         switch (mode) {
1392                                 case LOG_SEARCH_FROM:
1393                                 case LOG_SEARCH_TO:
1394                                 case LOG_SEARCH_DATE:
1395                                         [self _logFilter:searchString
1396                                                         searchID:searchID
1397                                                                 mode:mode];
1398                                         break;
1399                                 case LOG_SEARCH_CONTENT:
1400                                         [self _logContentFilter:searchString
1401                                                                    searchID:searchID
1402                                                           onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1403                                         break;
1404                         }
1405                 } else {
1406                         [self _logFilter:nil
1407                                         searchID:searchID
1408                                                 mode:mode];
1409                 }
1410                 
1411                 //Refresh
1412                 searching = NO;
1413                 [plugin resumeIndexing];
1414                 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1415                 AILog(@"filterLogsWithSearch (search ID %i): finished",searchID);
1416     }
1417         
1418     //Cleanup
1419     [pool release];
1422 //Perform a filter search based on source name, destination name, or date
1423 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode
1425     NSEnumerator        *fromEnumerator, *toEnumerator, *logEnumerator;
1426     AILogToGroup        *toGroup;
1427     AILogFromGroup      *fromGroup;
1428     AIChatLog                   *theLog;
1429     UInt32              lastUpdate = TickCount();
1430     
1431     NSCalendarDate      *searchStringDate = nil;
1432         
1433         if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1434                 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString]  dateWithCalendarFormat:nil timeZone:nil];
1435         }
1436         
1437     //Walk through every 'from' group
1438     fromEnumerator = [logFromGroupDict objectEnumerator];
1439     while ((fromGroup = [fromEnumerator nextObject]) && (searchID == activeSearchID)) {
1440                 
1441                 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1442                 if ((mode != LOG_SEARCH_FROM) ||
1443                         (!searchString) || 
1444                         ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1446                         //Walk through every 'to' group
1447                         toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
1448                         while ((toGroup = [toEnumerator nextObject]) && (searchID == activeSearchID)) {
1449                                 
1450                                 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1451                                  * For all other search modes, we always proceed here so long as either:
1452                                  *      a) We are not filtering for specific contact names or
1453                                  *      b) The contact name matches one of the names in contactIDsToFilter
1454                                  */
1455                                 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1456                                    ((mode != LOG_SEARCH_TO) ||
1457                                    (!searchString) || 
1458                                    ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1459                                         
1460                                         //Walk through every log
1462                                         logEnumerator = [toGroup logEnumerator];
1463                                         while ((theLog = [logEnumerator nextObject]) && (searchID == activeSearchID)) {
1464                                                 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1465                                                  * For all other search modes, we always proceed here
1466                                                  */
1467                                                 if ((mode != LOG_SEARCH_DATE) ||
1468                                                    (!searchString) ||
1469                                                    (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1471                                                         if ([self chatLogMatchesDateFilter:theLog]) {
1472                                                                 //Add the log
1473                                                                 [resultsLock lock];
1474                                                                 [currentSearchResults addObject:theLog];
1475                                                                 [resultsLock unlock];                                                   
1476                                                                 
1477                                                                 //Update our status
1478                                                                 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1479                                                                         [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1480                                                                                                                    withObject:nil
1481                                                                                                                 waitUntilDone:NO];
1482                                                                         lastUpdate = TickCount();
1483                                                                 }
1484                                                         }
1485                                                 }
1486                                         }
1487                                 }
1488                         }           
1489                 }
1490     }
1493 //Search results table view --------------------------------------------------------------------------------------------
1494 #pragma mark Search results table view
1495 //Since this table view's source data will be accessed from within other threads, we need to lock before
1496 //accessing it.  We also must be very sure that an incorrect row request is handled silently, since this
1497 //can occur if the array size is changed during the reload.
1498 - (int)numberOfRowsInTableView:(NSTableView *)tableView
1500     int count;
1501     
1502     [resultsLock lock];
1503     count = [currentSearchResults count];
1504     [resultsLock unlock];
1505     
1506     return count;
1510 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
1512     NSString    *identifier = [tableColumn identifier];
1514         if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1515                 AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
1516                 
1517                 [aCell setPercentage:[theLog rankingPercentage]];
1518         }
1522 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
1524     NSString    *identifier = [tableColumn identifier];
1525     id          value = nil;
1526     
1527     [resultsLock lock];
1528     if (row < 0 || row >= [currentSearchResults count]) {
1529                 if ([identifier isEqualToString:@"Service"]) {
1530                         value = blankImage;
1531                 } else {
1532                         value = @"";
1533                 }
1534                 
1535         } else {
1536                 AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
1538                 if ([identifier isEqualToString:@"To"]) {
1539                         // Get ListObject for to-UID
1540                         AIListObject *listObject = [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:[theLog serviceClass]
1541                                                                                                                                                                                                                                                                                 UID:[theLog to]]];
1542                         if (listObject) {
1543                                 //Use the longDisplayName, following the user's contact list preferences as this is presumably how she wants to view contacts' names.
1544                                 value = [listObject longDisplayName];
1546                         } else {
1547                                 //No username available
1548                                 value = [theLog to];
1549                         }
1550                         
1551                 } else if ([identifier isEqualToString:@"From"]) {
1552                         value = [theLog from];
1553                         
1554                 } else if ([identifier isEqualToString:@"Date"]) {
1555                         value = [theLog date];
1556                         
1557                 } else if ([identifier isEqualToString:@"Service"]) {
1558                         NSString        *serviceClass;
1559                         NSImage         *image;
1560                         
1561                         serviceClass = [theLog serviceClass];
1562                         image = [AIServiceIcons serviceIconForService:[[adium accountController] firstServiceWithServiceID:serviceClass]
1563                                                                                                          type:AIServiceIconSmall
1564                                                                                                 direction:AIIconNormal];
1565                         value = (image ? image : blankImage);
1566                 }
1567     }
1568     [resultsLock unlock];
1569     
1570     return value;
1574 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1576     if (!ignoreSelectionChange) {
1577                 NSArray         *selectedLogs;
1578                 
1579                 //Update the displayed log
1580                 automaticSearch = NO;
1581                 
1582                 [resultsLock lock];
1583                 selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
1584                 [resultsLock unlock];
1585                 
1586                 [self displayLogs:selectedLogs];
1587     }
1590 //Sort the log array & reflect the new column
1591 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1592 {    
1593     [self sortCurrentSearchResultsForTableColumn:tableColumn
1594                                    direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1597 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1599     [self deleteSelection:nil];
1602 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
1604         if (tableView == tableView_results) {
1605                 [tableView setSearchColumnIdentifiers:[NSSet setWithObjects:@"To", @"From", nil]];
1606                 [tableView setSearchWraps:YES];
1608         } else if (tableView == (KFTypeSelectTableView *)outlineView_contacts) {
1609                 [tableView setSearchWraps:YES];
1610         }
1613 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1615         NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1617         if (!aNotification ||
1618                 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1619                 NSDateFormatter *dateFormatter;
1620                 NSCell                  *cell = [dateTableColumn dataCell];
1622                 [cell setObjectValue:[NSDate date]];
1624                 float width = [dateTableColumn width];
1626 #define NUMBER_TIME_STYLES      2
1627 #define NUMBER_DATE_STYLES      4
1628                 NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1629                 NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1630                 float requiredWidth;
1632                 dateFormatter = [cell formatter];
1633                 if (!dateFormatter) {
1634                         dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1635                         [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1636                         [cell setFormatter:dateFormatter];
1637                 }
1638                 
1639                 requiredWidth = width + 1;
1640                 for (int i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1641                         [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1643                         for (int j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1644                                 [dateFormatter setDateStyle:formatterStyles[j]];
1645                                 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1646                                 //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1647                                 requiredWidth += 3;                                     
1648                         }
1649                 }
1650         }
1653 - (IBAction)toggleEmoticonFiltering:(id)sender
1655         showEmoticons = !showEmoticons;
1656         [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1657         [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1659         [self displayLogs:displayedLogArray];
1662 #pragma mark Outline View Data source
1663 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
1665         if (!item) {
1666                 if (index == 0) {
1667                         return allContactsIdentifier;
1669                 } else {
1670                         return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1671                 }
1673         } else {
1674                 if ([item isKindOfClass:[AIMetaContact class]]) {
1675                         return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1676                 }
1677         }
1678         
1679         return nil;
1682 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1684         return (!item || 
1685                         ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1686                         [item isKindOfClass:[NSArray class]]);
1689 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1691         if (!item) {
1692                 return [toArray count] + 1; //+1 for the All item
1694         } else if ([item isKindOfClass:[AIMetaContact class]]) {
1695                 unsigned count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1696                 if (count > 1)
1697                         return count;
1698                 else
1699                         return 0;
1701         } else {
1702                 return 0;
1703         }
1706 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1708         Class itemClass = [item class];
1710         if (itemClass == [AIMetaContact class]) {
1711                 return [(AIMetaContact *)item longDisplayName];
1712                 
1713         } else if (itemClass == [AIListContact class]) {
1714                 if ([(AIListContact *)item parentContact] != item) {
1715                         //This contact is within a metacontact - always show its UID
1716                         return [(AIListContact *)item formattedUID];
1717                 } else {
1718                         return [(AIListContact *)item longDisplayName];
1719                 } 
1720                 
1721         } else if (itemClass == [AILogToGroup class]) {
1722                 return [(AILogToGroup *)item to];
1723                 
1724         } else if (itemClass == [allContactsIdentifier class]) {
1725                 int contactCount = [toArray count];
1726                 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1727                         ((contactCount == 1) ?
1728                          AILocalizedString(@"1 Contact", nil) :
1729                          [NSString stringWithFormat:AILocalizedString(@"%i Contacts", nil), contactCount])]; 
1731         } else if (itemClass == [NSString class]) {
1732                 return item;
1734         } else {
1735                 NSLog(@"%@: no idea",item);
1736                 return nil;
1737         }
1740 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1742         if ([item isKindOfClass:[AIMetaContact class]] &&
1743                 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1744                 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1745                  * If it has multiple contacts, use no icon unless a user icon is present.
1746                  */
1747                 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1748                                                                                                                 size:NSMakeSize(16,16)];
1749                 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1751                 [cell setImage:image];
1753         } else if ([item isKindOfClass:[AIListContact class]]) {
1754                 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1755                                                                                                                 size:NSMakeSize(16,16)];
1756                 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1757                                                                                                                         type:AIServiceIconSmall
1758                                                                                                            direction:AIIconFlipped];
1759                 [cell setImage:image];
1761         } else if ([item isKindOfClass:[AILogToGroup class]]) {
1762                 [cell setImage:[AIServiceIcons serviceIconForServiceID:[(AILogToGroup *)item serviceClass]
1763                                                                                                            type:AIServiceIconSmall
1764                                                                                                   direction:AIIconFlipped]];
1765                 
1766         } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1767                 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1768                         ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1769                         if (!adiumIconHighlighted) {
1770                                 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
1771                                                                                                    forClass:[self class]] retain];
1772                         }
1774                         [cell setImage:adiumIconHighlighted];
1776                 } else {
1777                         if (!adiumIcon) {
1778                                 adiumIcon = [[NSImage imageNamed:@"adium"
1779                                                                                 forClass:[self class]] retain];
1780                         }
1782                         [cell setImage:adiumIcon];
1783                 }
1785         } else if ([item isKindOfClass:[NSString class]]) {
1786                 [cell setImage:nil];
1787                 
1788         } else {
1789                 NSLog(@"%@: no idea",item);
1790                 [cell setImage:nil];
1791         }       
1795  * @brief Is item supposed to have a divider below?
1797  */
1798 - (AIDividerPosition)outlineView:(NSOutlineView*)outlineView dividerPositionForItem:(id)item
1800         if ([item isKindOfClass:[allContactsIdentifier class]]) {
1801                 return AIDividerPositionBelow;
1802         } else {
1803                 return AIDividerPositionNone;
1804         }
1807 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
1809         [self deleteSelection:nil];
1812 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1814         NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
1816         [contactIDsToFilter removeAllObjects];
1818         if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
1819                 id              item;
1820                 NSEnumerator *enumerator;
1822                 enumerator = [selectedItems objectEnumerator];
1823                 while ((item = [enumerator nextObject])) {
1824                         if ([item isKindOfClass:[AIMetaContact class]]) {
1825                                 NSEnumerator    *metaEnumerator;
1826                                 AIListContact   *contact;
1828                                 metaEnumerator = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectEnumerator];
1829                                 while ((contact = [metaEnumerator nextObject])) {
1830                                         [contactIDsToFilter addObject:
1831                                                 [[[NSString stringWithFormat:@"%@.%@",[contact serviceID],[contact UID]] compactedString] safeFilenameString]];
1832                                 }
1833                                 
1834                         } else if ([item isKindOfClass:[AIListContact class]]) {
1835                                 [contactIDsToFilter addObject:
1836                                         [[[NSString stringWithFormat:@"%@.%@",[(AIListContact *)item serviceID],[(AIListContact *)item UID]] compactedString] safeFilenameString]];
1837                                 
1838                         } else if ([item isKindOfClass:[AILogToGroup class]]) {
1839                                 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]]; 
1840                         }
1841                 }
1842         }
1843         
1844         [self startSearchingClearingCurrentResults:YES];
1847 - (NSMenu *)outlineView:(NSOutlineView *)outlineView menuForEvent:(NSEvent *)theEvent;
1849         if (outlineView == outlineView_contacts) {
1850                 int clickedRow = [outlineView_contacts rowAtPoint:[outlineView_contacts convertPoint:[theEvent locationInWindow]
1851                                                                                                                                                                         fromView:nil]];
1852                 id item = [outlineView_contacts itemAtRow:clickedRow];
1854                 //If we have a To group, see if we can make a contact out of it
1855                 if ([item isKindOfClass:[AILogToGroup class]]) {
1856                         if ([(AILogToGroup *)item to] && [(AILogToGroup *)item serviceClass]) {
1857                                 //We need a service with ther right service ID
1858                                 AIService *service = [[adium accountController] firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]];
1859                                 if (service) {
1860                                         NSEnumerator *enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1861                                         AIAccount        *account;
1863                                         //Next, we want an online account
1864                                         while ((account = [enumerator nextObject])) {
1865                                                 if ([account online]) break;
1866                                         }
1867                                         
1868                                         if (account) {
1869                                                 //Finally, make a contact
1870                                                 item = [[adium contactController] contactWithService:service
1871                                                                                                                                          account:account
1872                                                                                                                                                  UID:[(AILogToGroup *)item to]];
1873                                         }
1874                                         
1875                                 }
1876                         }
1877                 }
1879                 if ([item isKindOfClass:[AIListContact class]]) {
1880                         NSArray                 *locationsArray = [NSArray arrayWithObjects:
1881                                 [NSNumber numberWithInt:Context_Contact_Message],
1882                                 [NSNumber numberWithInt:Context_Contact_Manage],
1883                                 [NSNumber numberWithInt:Context_Contact_Action],
1884                                 [NSNumber numberWithInt:Context_Contact_ListAction],
1885                                 [NSNumber numberWithInt:Context_Contact_NegativeAction],
1886                                 [NSNumber numberWithInt:Context_Contact_Additions], nil];
1888                         return [[adium menuController] contextualMenuWithLocations:locationsArray
1889                                                                                                                  forListObject:(AIListContact *)item];
1890                 }
1891         }
1892         
1893         return nil;
1896 static int toArraySort(id itemA, id itemB, void *context)
1898         NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
1899         NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
1900         NSComparisonResult result = [nameA caseInsensitiveCompare:nameB];
1901         if (result == NSOrderedSame) result = [nameA compare:nameB];
1903         return result;
1906 - (void)draggedDividerRightBy:(float)deltaX
1907 {       
1908         desiredContactsSourceListDeltaX = deltaX;
1909         [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
1910         desiredContactsSourceListDeltaX = 0;
1914 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
1916         if ((sender == splitView_contacts_results) &&
1917                 desiredContactsSourceListDeltaX != 0) {
1918                 float dividerThickness = [sender dividerThickness];
1920                 NSRect newFrame = [sender frame];               
1921                 NSRect leftFrame = [containingView_contactsSourceList frame]; 
1922                 NSRect rightFrame = [containingView_results frame];
1924                 leftFrame.size.width += desiredContactsSourceListDeltaX; 
1925                 leftFrame.size.height = newFrame.size.height;
1926                 leftFrame.origin = NSMakePoint(0,0);
1928                 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
1929                 rightFrame.size.height = newFrame.size.height;
1930                 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
1932                 [containingView_contactsSourceList setFrame:leftFrame];
1933                 [containingView_contactsSourceList setNeedsDisplay:YES];
1934                 [containingView_results setFrame:rightFrame];
1935                 [containingView_results setNeedsDisplay:YES];
1937         } else {
1938                 //Perform the default implementation
1939                 [sender adjustSubviews];
1940         }
1944 //Window Toolbar -------------------------------------------------------------------------------------------------------
1945 #pragma mark Window Toolbar
1946 - (NSString *)dateItemNibName
1948         return nil;
1951 - (void)installToolbar
1952 {       
1953         [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
1955     NSToolbar           *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
1956     NSToolbarItem       *toolbarItem;
1957         
1958     [toolbar setDelegate:self];
1959     [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
1960     [toolbar setSizeMode:NSToolbarSizeModeRegular];
1961     [toolbar setVisible:YES];
1962     [toolbar setAllowsUserCustomization:YES];
1963     [toolbar setAutosavesConfiguration:YES];
1964     toolbarItems = [[NSMutableDictionary alloc] init];
1966         //Delete Logs
1967         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1968                                         withIdentifier:@"delete"
1969                                                  label:DELETE
1970                                           paletteLabel:DELETE
1971                                                toolTip:AILocalizedString(@"Delete the selection",nil)
1972                                                 target:self
1973                                        settingSelector:@selector(setImage:)
1974                                            itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
1975                                                 action:@selector(deleteSelection:)
1976                                                   menu:nil];
1977         
1978         //Search
1979         [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
1980         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
1981                                                                                                                   label:SEARCH
1982                                                                                                    paletteLabel:SEARCH
1983                                                                                                                 toolTip:AILocalizedString(@"Search or filter logs",nil)
1984                                                                                                                  target:self
1985                                                                                                 settingSelector:@selector(setView:)
1986                                                                                                         itemContent:view_SearchField
1987                                                                                                                  action:@selector(updateSearch:)
1988                                                                                                                    menu:nil];
1989         if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1990                 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
1991         }
1992         [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
1993         [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
1994         [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1996         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
1997                                                                                                                   label:AILocalizedString(@"Date", nil)
1998                                                                                                    paletteLabel:AILocalizedString(@"Date", nil)
1999                                                                                                                 toolTip:AILocalizedString(@"Filter logs by date",nil)
2000                                                                                                                  target:self
2001                                                                                                 settingSelector:@selector(setView:)
2002                                                                                                         itemContent:view_DatePicker
2003                                                                                                                  action:nil
2004                                                                                                                    menu:nil];
2005         if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
2006                 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
2007         }
2008         [toolbarItem setMinSize:[view_DatePicker frame].size];
2009         [toolbarItem setMaxSize:[view_DatePicker frame].size];
2010         [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
2012         //Toggle Emoticons
2013         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2014                                                                         withIdentifier:@"toggleemoticons"
2015                                                                                          label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
2016                                                                           paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
2017                                                                                    toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
2018                                                                                         target:self
2019                                                                    settingSelector:@selector(setImage:)
2020                                                                            itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
2021                                                                                         action:@selector(toggleEmoticonFiltering:)
2022                                                                                           menu:nil];
2024         [[self window] setToolbar:toolbar];
2026         [self configureDateFilter];
2029 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
2031     return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
2034 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
2036     return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
2037                 @"delete", @"toggleemoticons", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
2038                 @"search", nil];
2041 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
2043     return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
2044                 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
2045                         NSToolbarSpaceItemIdentifier,
2046                         NSToolbarFlexibleSpaceItemIdentifier,
2047                         NSToolbarCustomizeToolbarItemIdentifier, 
2048                         NSToolbarPrintItemIdentifier, nil]];
2051 - (void)toolbarWillAddItem:(NSNotification *)notification
2053         NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
2054         if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2055                 [item setTarget:self];
2056                 [item setAction:@selector(adiumPrint:)];
2057         }
2060 #pragma mark Date filter
2063  * @brief Returns a menu item for the date type filter menu
2064  */
2065 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
2067     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInt:dateType]] 
2068                                                                                                                                                                  action:@selector(selectDateType:) 
2069                                                                                                                                                   keyEquivalent:@""];
2070     [menuItem setTag:dateType];
2071     
2072     return [menuItem autorelease];
2075 - (NSMenu *)dateTypeMenu
2077         NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2078                 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInt:AIDateTypeAnyDate],
2079                 AILocalizedString(@"Today", nil), [NSNumber numberWithInt:AIDateTypeToday],
2080                 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInt:AIDateTypeSinceYesterday],
2081                 AILocalizedString(@"This Week", nil), [NSNumber numberWithInt:AIDateTypeThisWeek],
2082                 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoWeeks],
2083                 AILocalizedString(@"This Month", nil), [NSNumber numberWithInt:AIDateTypeThisMonth],
2084                 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoMonths],
2085                 nil];
2086         NSMenu  *dateTypeMenu = [[NSMenu alloc] init];
2087         AIDateType dateType;
2088         
2089         [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
2090         [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2092         for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
2093                 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2094         }
2095         
2096         return [dateTypeMenu autorelease];
2099 - (int)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
2101         int todayDayOfWeek = [today dayOfWeek];
2103         //Try to look at the iCal preferences if possible
2104         if (!iCalFirstDayOfWeekDetermined) {
2105                 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
2106                 if (iCalFirstDayOfWeek) {
2107                         //This should return a CFNumberRef... we're using another app's prefs, so make sure.
2108                         if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
2109                                 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek intValue];
2110                         }
2112                         CFRelease(iCalFirstDayOfWeek);
2113                 }
2115                 //Don't check again
2116                 iCalFirstDayOfWeekDetermined = YES;
2117         }
2119         return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2123  * @brief A new date type was selected
2125  * This does not start a search
2126  */
2127 - (void)selectedDateType:(AIDateType)dateType
2129         NSCalendarDate  *today = [NSCalendarDate date];
2130         
2131         [filterDate release]; filterDate = nil;
2132         
2133         switch (dateType) {
2134                 case AIDateTypeAnyDate:
2135                         filterDateType = AIDateTypeAnyDate;
2136                         break;
2137                         
2138                 case AIDateTypeToday:
2139                         filterDateType = AIDateTypeExactly;
2140                         filterDate = [today retain];
2141                         break;
2142                         
2143                 case AIDateTypeSinceYesterday:
2144                         filterDateType = AIDateTypeAfter;
2145                         filterDate = [[today dateByAddingYears:0
2146                                                                                         months:0
2147                                                                                           days:-1
2148                                                                                          hours:-[today hourOfDay]
2149                                                                                    minutes:-[today minuteOfHour]
2150                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2151                         break;
2152                         
2153                 case AIDateTypeThisWeek:
2154                         filterDateType = AIDateTypeAfter;
2155                         filterDate = [[today dateByAddingYears:0
2156                                                                                         months:0
2157                                                                                           days:-[self daysSinceStartOfWeekGivenToday:today]
2158                                                                                          hours:-[today hourOfDay]
2159                                                                                    minutes:-[today minuteOfHour]
2160                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2161                         break;
2162                         
2163                 case AIDateTypeWithinLastTwoWeeks:
2164                         filterDateType = AIDateTypeAfter;
2165                         filterDate = [[today dateByAddingYears:0
2166                                                                                         months:0
2167                                                                                           days:-14
2168                                                                                          hours:-[today hourOfDay]
2169                                                                                    minutes:-[today minuteOfHour]
2170                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2171                         break;
2172                         
2173                 case AIDateTypeThisMonth:
2174                         filterDateType = AIDateTypeAfter;
2175                         filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2176                                                                                                                         months:0
2177                                                                                                                           days:-[today dayOfMonth]
2178                                                                                                                          hours:0
2179                                                                                                                    minutes:0
2180                                                                                                                    seconds:-1] retain];
2181                         break;
2182                         
2183                 case AIDateTypeWithinLastTwoMonths:
2184                         filterDateType = AIDateTypeAfter;
2185                         filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2186                                                                                                                         months:-1
2187                                                                                                                           days:-[today dayOfMonth]
2188                                                                                                                          hours:0
2189                                                                                                                    minutes:0
2190                                                                                                                    seconds:-1] retain];                 
2191                         break;
2192                         
2193                 default:
2194                         break;
2195         }       
2199  * @brief Select the date type
2200  */
2201 - (void)selectDateType:(id)sender
2203         [self selectedDateType:[sender tag]];
2204         [self startSearchingClearingCurrentResults:YES];
2207 - (void)configureDateFilter
2209         firstDayOfWeek = 0; /* Sunday */
2210         iCalFirstDayOfWeekDetermined = NO;
2212         [popUp_dateFilter setMenu:[self dateTypeMenu]];
2213         int index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2214         if(index != NSNotFound)
2215                 [popUp_dateFilter selectItemAtIndex:index];
2216         [self selectedDateType:AIDateTypeAnyDate];
2219 #pragma mark Open Log
2221 - (void)openLogAtPath:(NSString *)inPath
2223         AIChatLog   *chatLog = nil;
2224         NSString        *basePath = [AILoggerPlugin logBasePath];
2226         //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2227         NSArray         *pathComponents = [inPath pathComponents];
2228         int                     lastIndex = [pathComponents count];
2229         NSString        *logName = [pathComponents objectAtIndex:--lastIndex];
2230         NSString        *contactName = [pathComponents objectAtIndex:--lastIndex];
2231         NSString        *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];    
2232         NSString                *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2234         NSString        *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2235         //Filter for logs from the contact associated with the log we're loading
2236         [self filterForContact:[[adium contactController] contactWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
2237                                                                                                                                  account:nil
2238                                                                                                                                          UID:contactName]];
2239         
2240         NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2241         NSString *canonicalInPath = [inPath stringByStandardizingPath];
2243         if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2244                 AILogToGroup    *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2245                 
2246                 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2247                 
2248         } else {
2249                 /* Different Adium user... this sucks. We're given a path like this:
2250                  *      /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2251                  * and we want to make it relative to our current user's logs folder, which might be
2252                  *  /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2253                  *
2254                  * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2255                  */
2256                 NSString        *fakeRelativePath = @"";
2257                 
2258                 //Use .. to get back to the root from the base path
2259                 int componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2260                 for (int i = 0; i < componentsOfBasePath; i++) {
2261                         fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2262                 }
2263                 
2264                 //Now add the path from the root to the actual log
2265                 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2266                 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2267                                                                                           from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2268                                                                                                 to:contactName
2269                                                                           serviceClass:serviceID] autorelease];
2270         }
2272         //Now display the requested log
2273         if (chatLog) {
2274                 [self displayLog:chatLog];
2275         }
2278 #pragma mark Printing
2280 - (void)adiumPrint:(id)sender
2282         NSTextView                      *printView;
2283     NSPrintOperation    *printOperation;
2284     NSPrintInfo                 *printInfo = [NSPrintInfo sharedPrintInfo];
2286     [printInfo setHorizontalPagination:NSFitPagination];
2287     [printInfo setHorizontallyCentered:NO];
2288     [printInfo setVerticallyCentered:NO];
2289     
2290         printView = [[NSTextView alloc] initWithFrame:[[NSPrintInfo sharedPrintInfo] imageablePageBounds]];
2291     [printView setVerticallyResizable:YES];
2292     [printView setHorizontallyResizable:NO];
2293         
2294     [[printView textStorage] setAttributedString:[textView_content textStorage]];
2295         
2296     printOperation = [NSPrintOperation printOperationWithView:printView printInfo:printInfo];
2297     [printOperation runOperationModalForWindow:[self window] delegate:nil
2298                                                                 didRunSelector:NULL contextInfo:NULL];
2299         [printView release];
2302 - (BOOL)validatePrintMenuItem:(NSMenuItem *)menuItem
2304         return ([displayedLogArray count] > 0);
2307 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2309         if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2310                 return [self validatePrintMenuItem:nil];
2312         } else {
2313                 return YES;
2314         }
2317 - (void)selectCachedIndex
2319         int numberOfRows = [tableView_results numberOfRows];
2320         
2321         if (cachedSelectionIndex <  numberOfRows) {
2322                 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2323                                            byExtendingSelection:NO];
2324         } else {
2325                 if (numberOfRows)
2326                         [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2327                                                    byExtendingSelection:NO];                    
2328         }
2330         if (numberOfRows) {
2331                 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2332         }
2334         deleteOccurred = NO;
2337 #pragma mark Deletion
2340  * @brief Get an NSAlert to request deletion of multiple logs
2341  */
2342 - (NSAlert *)alertForDeletionOfLogCount:(int)logCount
2344         NSAlert *alert = [[NSAlert alloc] init];
2345         [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2346         [alert setInformativeText:[NSString stringWithFormat:
2347                 AILocalizedString(@"Are you sure you want to send %i logs to the Trash?",nil), logCount]];
2348         [alert addButtonWithTitle:DELETE]; 
2349         [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2350         
2351         return [alert autorelease];
2355  * @brief Undo the deletion of one or more AIChatLogs
2357  * The logs will be marked for readdition to the index
2358  */
2359 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2361         NSEnumerator    *enumerator;
2362         AIChatLog               *aLog;
2363         NSFileManager   *fileManager = [NSFileManager defaultManager];
2364         NSString                *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2366         enumerator = [deletedLogs objectEnumerator];
2367         while ((aLog = [enumerator nextObject])) {
2368                 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2369                 
2370                 [fileManager createDirectoriesForPath:[logPath stringByDeletingLastPathComponent]];
2371                 
2372                 [fileManager movePath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2373                                            toPath:logPath 
2374                                           handler:NULL];
2375                 
2376                 [plugin markLogDirtyAtPath:logPath];
2377         }
2378         
2379         [self rebuildIndices];
2382 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode  contextInfo:(void *)contextInfo;
2384         NSArray *selectedLogs = (NSArray *)contextInfo;
2385         if (returnCode == NSAlertFirstButtonReturn) {
2386                 [resultsLock lock];
2387                 
2388                 AIChatLog               *aLog;
2389                 NSEnumerator    *enumerator;
2390                 NSMutableSet    *logPaths = [NSMutableSet set];
2391                 
2392                 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2393                 
2394                 enumerator = [selectedLogs objectEnumerator];
2395                 while ((aLog = [enumerator nextObject])) {
2396                         NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2397                         
2398                         [[adium notificationCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2399                         AILogToGroup    *logToGroup = [logToGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@/%@",[aLog serviceClass],[aLog from],[aLog to]]];
2400                         BOOL success = [logToGroup trashLog:aLog];
2401                         AILog(@"Trashing %@: %i",[aLog path], success);
2402                         //Clear the to group out if it no longer has anything of interest
2403                         if ([logToGroup logCount] == 0) {
2404                                 AILogFromGroup  *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[aLog serviceClass],[aLog from]]];
2405                                 [logFromGroup removeToGroup:logToGroup];
2406                         }
2407                         
2408                         [logPaths addObject:logPath];
2409                         [currentSearchResults removeObjectIdenticalTo:aLog];
2410                 }
2411                 
2412                 [plugin removePathsFromIndex:logPaths];
2413                 
2414                 [undoManager registerUndoWithTarget:self
2415                                                                    selector:@selector(restoreDeletedLogs:)
2416                                                                          object:selectedLogs];
2417                 [undoManager setActionName:DELETE];
2418                 
2419                 [resultsLock unlock];
2420                 [tableView_results reloadData];
2421                 
2422                 deleteOccurred = YES;
2423                 
2424                 [self rebuildContactsList];
2425                 [self updateProgressDisplay];
2426         }
2427         [selectedLogs release];
2431  * @brief Delete logs
2433  * If two or more logs are passed, confirmation will be requested.
2434  * This operation registers with the window controller's undo manager.
2436  * @param selectedLogs An NSArray of logs to delete
2437  */
2438 - (void)deleteLogs:(NSArray *)selectedLogs
2439 {       
2440         if ([selectedLogs count] > 1) {
2441                 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2442                 [alert beginSheetModalForWindow:[self window]
2443                                                   modalDelegate:self
2444                                                  didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2445                                                         contextInfo:[selectedLogs retain]];
2446         } else {
2447                 [self deleteLogsAlertDidEnd:nil
2448                                                  returnCode:NSAlertFirstButtonReturn
2449                                                 contextInfo:[selectedLogs retain]];
2450         }
2454  * @brief Returns a set of all selected to groups on all accounts
2456  * @param totalLogCount If non-NULL, will be set to the total number of logs on return
2457  */
2458 - (NSArray *)allSelectedToGroups:(int *)totalLogCount
2460     NSEnumerator        *fromEnumerator;
2461     AILogFromGroup      *fromGroup;
2462         NSMutableArray          *allToGroups = [NSMutableArray array];
2464         if (totalLogCount) *totalLogCount = 0;
2466     //Walk through every 'from' group
2467     fromEnumerator = [logFromGroupDict objectEnumerator];
2468     while ((fromGroup = [fromEnumerator nextObject])) {
2469                 NSEnumerator        *toEnumerator;
2470                 AILogToGroup        *toGroup;
2472                 //Walk through every 'to' group
2473                 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2474                 while ((toGroup = [toEnumerator nextObject])) {
2475                         if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2476                                 if (totalLogCount) {
2477                                         *totalLogCount += [toGroup logCount];
2478                                 }
2479                                 
2480                                 [allToGroups addObject:toGroup];
2481                         }
2482                 }
2483         }
2485         return allToGroups;
2489  * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2491  * The logs will be marked for readdition to the index
2492  */
2493 - (void)restoreDeletedToGroups:(NSArray *)toGroups
2495         NSEnumerator    *enumerator;
2496         AILogToGroup    *toGroup;
2497         NSFileManager   *fileManager = [NSFileManager defaultManager];
2498         NSString                *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2499         NSString                *logBasePath = [AILoggerPlugin logBasePath];
2501         enumerator = [toGroups objectEnumerator];
2502         while ((toGroup = [enumerator nextObject])) {
2503                 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup path]];
2505                 [fileManager createDirectoriesForPath:[toGroupPath stringByDeletingLastPathComponent]];
2506                 if ([fileManager fileExistsAtPath:toGroupPath]) {
2507                         AILog(@"Removing path %@ to make way for %@",
2508                                   toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2509                         [fileManager removeFileAtPath:toGroupPath
2510                                                                   handler:NULL];
2511                 }
2512                 [fileManager movePath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2513                                            toPath:toGroupPath
2514                                           handler:NULL];
2515                 
2516                 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2517                 AIChatLog        *aLog;
2518         
2519                 while ((aLog = [logEnumerator nextObject])) {
2520                         [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog path]]];
2521                 }
2522         }
2523         
2524         [self rebuildIndices];  
2527 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2529         NSArray *allSelectedToGroups = (NSArray *)contextInfo;
2530         if (returnCode == NSAlertFirstButtonReturn) {
2531                 AILogToGroup    *logToGroup;
2532                 NSEnumerator    *enumerator;
2533                 NSMutableSet    *logPaths = [NSMutableSet set];
2534                 
2535                 enumerator = [allSelectedToGroups objectEnumerator];
2536                 while ((logToGroup = [enumerator nextObject])) {
2537                         NSEnumerator *logEnumerator;
2538                         AIChatLog        *aLog;
2539                         
2540                         logEnumerator = [logToGroup logEnumerator];
2541                         while ((aLog = [logEnumerator nextObject])) {
2542                                 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2543                                 [logPaths addObject:logPath];
2544                         }
2545                         
2546                         AILogFromGroup  *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2547                         [logFromGroup removeToGroup:logToGroup];
2548                 }
2549                 
2550                 [plugin removePathsFromIndex:logPaths];
2551                 
2552                 [undoManager registerUndoWithTarget:self
2553                                                                    selector:@selector(restoreDeletedToGroups:)
2554                                                                          object:allSelectedToGroups];
2555                 [undoManager setActionName:DELETE];
2556                 
2557                 [self rebuildIndices];
2558                 [self updateProgressDisplay];
2559         }
2560         
2561         [allSelectedToGroups release];
2565  * @brief Delete entirely the logs of all contacts selected in the source list
2567  * Confirmation by the user will be required.
2569  * Note: A single item in the source list may have multiple associated AILogToGroups.
2570  */
2571 - (void)deleteSelectedContactsFromSourceList
2573         int totalLogCount;
2574         NSArray *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2576         if (totalLogCount > 1) {
2577                 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2578                 [alert beginSheetModalForWindow:[self window]
2579                                                   modalDelegate:self
2580                                                  didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2581                                                         contextInfo:[allSelectedToGroups retain]];
2582         } else {
2583                 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2584                                                                                                    returnCode:NSAlertFirstButtonReturn
2585                                                                                                   contextInfo:[allSelectedToGroups retain]];
2586         }
2590  * @brief Delete the current selection
2592  * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2593  * If anything else is selected, the currently selected search result logs will be trashed.
2594  */
2595 - (void)deleteSelection:(id)sender
2597         if ([[self window] firstResponder] == outlineView_contacts) {
2598                 [self deleteSelectedContactsFromSourceList];
2599                 
2600         } else {
2601                 [resultsLock lock];
2602                 NSArray *selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
2603                 [resultsLock unlock];
2604                 
2605                 [self deleteLogs:selectedLogs];
2606         }
2609 #pragma mark Undo
2611  * @brief Supply our undo manager when we are within the responder chain
2612  */
2613 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
2615         return undoManager;
2618 @end