Request info to get user icons when a Jabber contact signs on. Fixes #4205
[adiumx.git] / Source / AIAbstractLogViewerWindowController.m
blobcbccd131adaa68eb4fcc5dd4e9e21b107fb9f383
1 //
2 //  AIAbstractLogViewerWindowController.m
3 //  Adium
4 //
5 //  Created by Evan Schoenberg on 3/24/06.
6 //
8 #import <Adium/AIAccountControllerProtocol.h>
9 #import "AIChatLog.h"
10 #import <Adium/AIContactControllerProtocol.h>
11 #import <Adium/AIContentControllerProtocol.h>
12 #import "AILogFromGroup.h"
13 #import "AILogToGroup.h"
14 #import "AILogViewerWindowController.h"
15 #import "AILoggerPlugin.h"
16 #import <Adium/AIPreferenceControllerProtocol.h>
17 #import "ESRankingCell.h" 
18 #import "GBChatlogHTMLConverter.h"
19 #import <AIUtilities/AIArrayAdditions.h>
20 #import <AIUtilities/AIAttributedStringAdditions.h>
21 #import <AIUtilities/AIDateFormatterAdditions.h>
22 #import <AIUtilities/AIFileManagerAdditions.h>
23 #import <AIUtilities/AIImageAdditions.h>
24 #import <AIUtilities/AIImageTextCell.h>
25 #import <AIUtilities/AIOutlineViewAdditions.h>
26 #import <AIUtilities/AISplitView.h>
27 #import <AIUtilities/AIStringAdditions.h>
28 #import <AIUtilities/AITableViewAdditions.h>
29 #import <AIUtilities/AITextAttributes.h>
30 #import <AIUtilities/AIToolbarUtilities.h>
31 #import <AIUtilities/AIApplicationAdditions.h>
32 #import <Adium/AIHTMLDecoder.h>
33 #import <Adium/AIListContact.h>
34 #import <Adium/AIMetaContact.h>
35 #import <Adium/AIServiceIcons.h>
36 #import <Adium/AIUserIcons.h>
38 #import "AILogDateFormatter.h"
40 #import "KFTypeSelectTableView.h"
41 #import "KNShelfSplitView.h"
43 #define KEY_LOG_VIEWER_WINDOW_FRAME             @"Log Viewer Frame"
44 #define PREF_GROUP_CONTACT_LIST                 @"Contact List"
45 #define KEY_LOG_VIEWER_GROUP_STATE              @"Log Viewer Group State"       //Expand/Collapse state of groups
46 #define TOOLBAR_LOG_VIEWER                              @"Log Viewer Toolbar"
48 #define MAX_LOGS_TO_SORT_WHILE_SEARCHING        3000    //Max number of logs we will live sort while searching
49 #define LOG_SEARCH_STATUS_INTERVAL                      20      //1/60ths of a second to wait before refreshing search status
51 #define SEARCH_MENU                                             AILocalizedString(@"Search Menu",nil)
52 #define FROM                                                    AILocalizedString(@"From",nil)
53 #define TO                                                              AILocalizedString(@"To",nil)
54 #define DATE                                                    AILocalizedString(@"Date",nil)
55 #define CONTENT                                                 AILocalizedString(@"Content",nil)
56 #define DELETE                                                  AILocalizedString(@"Delete",nil)
57 #define DELETEALL                                               AILocalizedString(@"Delete All",nil)
58 #define SEARCH                                                  AILocalizedString(@"Search",nil)
60 #define HIDE_EMOTICONS                                  AILocalizedString(@"Hide Emoticons",nil)
61 #define SHOW_EMOTICONS                                  AILocalizedString(@"Show Emoticons",nil)
63 #define IMAGE_EMOTICONS_OFF                             @"emoticon32"
64 #define IMAGE_EMOTICONS_ON                              @"emoticon32_transparent"
66 #define REFRESH_RESULTS_INTERVAL                0.5 //Interval between results refreshes while searching
68 @interface AIAbstractLogViewerWindowController (PRIVATE)
69 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
70 - (void)initLogFiltering;
71 - (void)displayLog:(AIChatLog *)log;
72 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
73 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
74 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
75 - (void)buildSearchMenu;
76 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
77 - (void)_logContentFilter:(NSString *)searchString searchID:(int)searchID onSearchIndex:(SKIndexRef)logSearchIndex;
78 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode;
79 - (void)installToolbar;
80 - (void)updateRankColumnVisibility;
81 - (void)openLogAtPath:(NSString *)inPath;
82 - (void)rebuildContactsList;
83 - (void)filterForContact:(AIListContact *)inContact;
84 - (void)selectCachedIndex;
86 - (void)deleteSelection:(id)sender;
87 @end
89 @implementation AIAbstractLogViewerWindowController
91 static AIAbstractLogViewerWindowController      *sharedLogViewerInstance = nil;
92 static int toArraySort(id itemA, id itemB, void *context);
94 + (NSString *)nibName
96         return @"LogViewer";    
99 + (id)openForPlugin:(id)inPlugin
101     if (!sharedLogViewerInstance) {
102                 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
103         }
105     [sharedLogViewerInstance showWindow:nil];
106     
107         return sharedLogViewerInstance;
110 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
112         [self openForPlugin:inPlugin];
113         
114         [sharedLogViewerInstance openLogAtPath:inPath];
115         
116         return sharedLogViewerInstance;
119 //Open the log viewer window to a specific contact's logs
120 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
122     [self openForPlugin:inPlugin];
124         [sharedLogViewerInstance filterForContact:inContact];
125         
126     return sharedLogViewerInstance;
129 //Returns the window controller if one exists
130 + (id)existingWindowController
132     return sharedLogViewerInstance;
135 //Close the log viewer window
136 + (void)closeSharedInstance
138     if (sharedLogViewerInstance) {
139         [sharedLogViewerInstance closeWindow:nil];
140     }
143 //init
144 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
146     //init
147     plugin = inPlugin;
148     selectedColumn = nil;
149     activeSearchID = 0;
150     searching = NO;
151     automaticSearch = YES;
152     showEmoticons = NO;
153     activeSearchString = nil;
154     displayedLogArray = nil;
155     windowIsClosing = NO;
156         desiredContactsSourceListDeltaX = 0;
158     blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
160     sortDirection = YES;
161     searchMode = LOG_SEARCH_CONTENT;
162     headerDateFormatter = [[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString] 
163                                                                                                  allowNaturalLanguage:NO];
164     currentSearchResults = [[NSMutableArray alloc] init];
165     fromArray = [[NSMutableArray alloc] init];
166     fromServiceArray = [[NSMutableArray alloc] init];
167     logFromGroupDict = [[NSMutableDictionary alloc] init];
168     toArray = [[NSMutableArray alloc] init];
169     toServiceArray = [[NSMutableArray alloc] init];
170     logToGroupDict = [[NSMutableDictionary alloc] init];
171     resultsLock = [[NSRecursiveLock alloc] init];
172     searchingLock = [[NSLock alloc] init];
173         contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
175         allContactsIdentifier = [[NSNumber alloc] initWithInt:-1];
177         undoManager = [[NSUndoManager alloc] init];
179     [super initWithWindowNibName:windowNibName];
180         
181     return self;
184 //dealloc
185 - (void)dealloc
187     [resultsLock release];
188     [searchingLock release];
189     [fromArray release];
190     [fromServiceArray release];
191     [toArray release];
192     [toServiceArray release];
193     [currentSearchResults release];
194     [selectedColumn release];
195     [headerDateFormatter release];
196     [displayedLogArray release];
197     [blankImage release];
198     [activeSearchString release];
199         [contactIDsToFilter release];
201         [logFromGroupDict release]; logFromGroupDict = nil;
202         [logToGroupDict release]; logToGroupDict = nil;
204     [filterForAccountName release]; filterForAccountName = nil;
206         [horizontalRule release]; horizontalRule = nil;
208         [adiumIcon release]; adiumIcon = nil;
209         [adiumIconHighlighted release]; adiumIconHighlighted = nil;
210         
211         //We loaded     view_DatePicker from a nib manually, so we must release it
212         [view_DatePicker release]; view_DatePicker = nil;
214         [allContactsIdentifier release];
215     [undoManager release]; undoManager = nil;
217     [super dealloc];
220 //Init our log filtering tree
221 - (void)initLogFiltering
223     NSEnumerator                        *enumerator;
224     NSString                            *folderName;
225     NSMutableDictionary         *toDict = [NSMutableDictionary dictionary];
226     NSString                            *basePath = [AILoggerPlugin logBasePath];
227     NSString                            *fromUID, *serviceClass;
229     //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
230         //first by service, then by account name.
231         enumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:basePath] sortedArrayUsingSelector:@selector(compare:)] objectEnumerator];
232     while ((folderName = [enumerator nextObject])) {
233                 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
234                         NSEnumerator    *toEnum;
235                         AILogToGroup    *currentToGroup;                        
236                         AILogFromGroup  *logFromGroup;
237                         NSMutableSet    *toSetForThisService;
238                         NSArray         *serviceAndFromUIDArray;
239                         
240                         /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
241                          * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
242                          */
243                         serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
245                         if ([serviceAndFromUIDArray count] >= 2) {
246                                 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
248                                 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
249                                 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
250                         } else {
251                                 //Fallback: blank non-nil serviceClass; folderName as the fromUID
252                                 serviceClass = @"";
253                                 fromUID = folderName;
254                         }
256                         logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
258                         //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
259                         [logFromGroupDict setObject:logFromGroup forKey:folderName];
261                         //To processing
262                         if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
263                                 toSetForThisService = [NSMutableSet set];
264                                 [toDict setObject:toSetForThisService
265                                                    forKey:serviceClass];
266                         }
268                         //Add the 'to' for each grouping on this account
269                         toEnum = [[logFromGroup toGroupArray] objectEnumerator];
270                         while ((currentToGroup = [toEnum nextObject])) {
271                                 NSString        *currentTo;
273                                 if ((currentTo = [currentToGroup to])) {
274                                         //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
275                                         [logToGroupDict setObject:currentToGroup forKey:[currentToGroup path]];
276                                 }
277                         }
279                         [logFromGroup release];
280                 }
281         }
283         [self rebuildContactsList];
286 - (void)rebuildContactsList
288         NSEnumerator    *enumerator = [logFromGroupDict objectEnumerator];
289         AILogFromGroup  *logFromGroup;
290         
291         int     oldCount = [toArray count];
292         [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
294         while ((logFromGroup = [enumerator nextObject])) {
295                 NSEnumerator    *toEnum;
296                 AILogToGroup    *currentToGroup;
297                 NSString                *serviceClass = [logFromGroup serviceClass];
299                 //Add the 'to' for each grouping on this account
300                 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
301                 while ((currentToGroup = [toEnum nextObject])) {
302                         NSString        *currentTo;
303                         
304                         if ((currentTo = [currentToGroup to])) {
305                                 AIListObject *listObject = ((serviceClass && currentTo) ?
306                                                                                         [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
307                                                                                                                                                                                                                                                                                          UID:currentTo]] :
308                                                                                         nil);
309                                 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
310                                         AIListContact *parentContact = [(AIListContact *)listObject parentContact];
311                                         if (![toArray containsObjectIdenticalTo:parentContact]) {
312                                                 [toArray addObject:parentContact];
313                                         }
314                                         
315                                 } else {
316                                         if (![toArray containsObject:currentToGroup]) {
317                                                 [toArray addObject:currentToGroup];
318                                         }
319                                 }
320                         }
321                 }               
322         }
323         
324         [toArray sortUsingFunction:toArraySort context:NULL];
325         [outlineView_contacts reloadData];
327         [self outlineViewSelectionDidChange:nil];
331 - (NSString *)adiumFrameAutosaveName
333         return KEY_LOG_VIEWER_WINDOW_FRAME;
336 //Setup the window before it is displayed
337 - (void)windowDidLoad
339         [super windowDidLoad];
341         [[self window] setTitle:AILocalizedString(@"Chat Transcripts Viewer",nil)];
342     [textField_progress setStringValue:@""];
344         //Autosave doesn't do anything yet
345         [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
346         [shelf_splitView setFrame:[[[self window] contentView] frame]];
348         // Pull our main article/display split view out of the nib and position it in the shelf view
349         [containingView_results retain];
350         [containingView_results removeFromSuperview];
351         [shelf_splitView setContentView:containingView_results];
352         [containingView_results release];
353         
354         // Pull our source view out of the nib and position it in the shelf view
355         [containingView_contactsSourceList retain];
356         [containingView_contactsSourceList removeFromSuperview];
357         [shelf_splitView setShelfView:containingView_contactsSourceList];
358         [containingView_contactsSourceList release];
360     //Set emoticon filtering
361     showEmoticons = [[[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
362                                                               group:PREF_GROUP_LOGGING] boolValue];
363     [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
364     [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
366         //Toolbar
367         [self installToolbar];  
369         //This is the Mail.app source list background color... which differs from the iTunes one.
370         [outlineView_contacts setBackgroundColor:[NSColor colorWithCalibratedRed:.9059
371                                                                                                                                            green:.9294
372                                                                                                                                                 blue:.9647
373                                                                                                                                            alpha:1.0]];
375         AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
376         [[[outlineView_contacts tableColumns] objectAtIndex:0] setDataCell:dataCell];
377         [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
378         [dataCell release];
380         [outlineView_contacts setDrawsGradientSelection:YES];
382         //Localize tableView_results column headers
383         [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
384         [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
385         [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
386         [self tableViewColumnDidResize:nil];
388         [tableView_results sizeLastColumnToFit];
390     //Prepare the search controls
391     [self buildSearchMenu];
392     if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
393                 [textView_content setUsesFindPanel:YES];
394     }
396     //Sort by preference, defaulting to sorting by date
397         NSString        *selectedTableColumnPref;
398         if ((selectedTableColumnPref = [[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
399                                                                                                                                                    group:PREF_GROUP_LOGGING])) {
400                 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
401         }
402         if (!selectedColumn) {
403                 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
404         }
405         [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
407     //Prepare indexing and filter searching
408         [plugin prepareLogContentSearching];
409     [self initLogFiltering];
411     //Begin our initial search
412         [self setSearchMode:LOG_SEARCH_TO];
414     [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
415     [self startSearchingClearingCurrentResults:YES];
418 -(void)rebuildIndices
420     //Rebuild the 'global' log indexes
421     [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
422     [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
423     [toServiceArray removeAllObjects];
424     [fromArray removeAllObjects];
425     [fromServiceArray removeAllObjects];
426     
427     [self initLogFiltering];
428     
429     [tableView_results reloadData];
430     [self selectDisplayedLog];
433 //Called as the window closes
434 - (void)windowWillClose:(id)sender
436         [super windowWillClose:sender];
438         //Set preference for emoticon filtering
439         [[adium preferenceController] setPreference:[NSNumber numberWithBool:showEmoticons]
440                                                                                  forKey:KEY_LOG_VIEWER_EMOTICONS
441                                                                                   group:PREF_GROUP_LOGGING];
442         
443         //Set preference for selected column
444         [[adium preferenceController] setPreference:[selectedColumn identifier]
445                                                                                  forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
446                                                                                   group:PREF_GROUP_LOGGING];
448     /* Disable the search field.  If we don't disable the search field, it will often try to call its target action
449      * after the window has closed (and we are gone).  I'm not sure why this happens, but disabling the field
450      * before we close the window down seems to prevent the crash.
451          */
452     [searchField_logs setEnabled:NO];
453         
454         /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
455          * it was gone, like responding to a logIndexUpdated message
456          */
457         windowIsClosing = YES;
459     //Abort any in-progress searching and indexing, and wait for their completion
460     [self stopSearching];
461     [plugin cleanUpLogContentSearching];
463         //Reset our column widths if needed
464         [activeSearchString release]; activeSearchString = nil;
465         [self updateRankColumnVisibility];
466         
467         [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
468         [toolbarItems autorelease]; toolbarItems = nil;
471 //Display --------------------------------------------------------------------------------------------------------------
472 #pragma mark Display
473 //Update log viewer progress string to reflect current status
474 - (void)updateProgressDisplay
476     NSMutableString     *progress = nil;
477     int                                 indexNumber, indexTotal;
478     BOOL                                indexing;
480     //We always convey the number of logs being displayed
481     [resultsLock lock];
482         unsigned count = [currentSearchResults count];
483     if (activeSearchString && [activeSearchString length]) {
484                 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
485                                                                                                                                                            AILocalizedString(@"%i matching logs",nil) :
486                                                                                                                                                            AILocalizedString(@"1 matching log",nil)),count]];
487     } else {
488                 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
489                                                                                                                                                            AILocalizedString(@"%i logs",nil) :
490                                                                                                                                                            AILocalizedString(@"1 log",nil)),count]];
491                 
492                 //We are searching, but there is no active search  string. This indicates we're still opening logs.
493                 if (searching) {
494                         progress = [[AILocalizedString(@"Opening logs",nil) mutableCopy] autorelease];                  
495                 }
496     }
497     [resultsLock unlock];
499         indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
501     //Append search progress
502     if (activeSearchString && [activeSearchString length]) {
503                 if (progress) {
504                         [progress appendString:@" - "];
505                 } else {
506                         progress = [NSMutableString string];
507                 }
509                 if (searching || indexing) {
510                         [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
511                 } else {
512                         [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];                     
513                 }
514         }
516     //Append indexing progress
517     if (indexing) {
518                 if (progress) {
519                         [progress appendString:@" - "];
520                 } else {
521                         progress = [NSMutableString string];
522                 }
523                 
524                 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %i of %i",nil), indexNumber, indexTotal]];
525     }
526         
527         if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
528                 [progress appendString:[NSString ellipsis]];    
529         }
531     //Enable/disable the searching animation
532     if (searching || indexing) {
533                 [progressIndicator startAnimation:nil];
534     } else {
535                 [progressIndicator stopAnimation:nil];
536     }
537     
538     [textField_progress setStringValue:(progress ? progress : @"")];
541 //The plugin is informing us that the log indexing changed
542 - (void)logIndexingProgressUpdate
544         //Don't do anything if the window is already closing
545         if (!windowIsClosing) {
546                 [self updateProgressDisplay];
547                 
548                 //If we are searching by content, we should re-search without clearing our current results so the
549                 //the newly-indexed logs can be added without blanking the current table contents.
550                 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
551                         if (searching) {
552                                 //We're already searching; reattempt when done
553                                 searchIDToReattemptWhenComplete = activeSearchID;
554                         } else {
555                                 //We're not searching - restart the search immediately
556                                 [self startSearchingClearingCurrentResults:NO];
557                         }
558                 }
559         }
562 //Refresh the results table
563 - (void)refreshResults
565         [self updateProgressDisplay];
567         [self refreshResultsSearchIsComplete:NO];
570 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
572     [resultsLock lock];
573     int count = [currentSearchResults count];
574     [resultsLock unlock];
575         
576     if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
577                 //Sort the logs correctly which will also reload the table
578                 [self resortLogs];
579                 
580                 if (searchIsComplete && automaticSearch) {
581                         //If search is complete, select the first log if requested and possible
582                         [self selectFirstLog];
583                         
584                 } else {
585                         BOOL oldAutomaticSearch = automaticSearch;
587                         //We don't want the above re-selection to change our automaticSearch tracking
588                         //(The only reason automaticSearch should change is in response to user action)
589                         automaticSearch = oldAutomaticSearch;
590                 }
591     }
592         
593         if (searchIsComplete &&
594                 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
595                 searchIDToReattemptWhenComplete = -1;
596                 [self startSearchingClearingCurrentResults:NO];
597         }
598         
599         if(deleteOccurred)
600                 [self selectCachedIndex];
602     //Update status
603     [self updateProgressDisplay];
606 - (void)searchComplete
608         [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
609         [self refreshResultsSearchIsComplete:YES];
612 //Displays the contents of the specified log in our window
613 - (void)displayLogs:(NSArray *)logArray;
614 {       
615     NSMutableAttributedString   *displayText = nil;
616         NSAttributedString                      *finalDisplayText = nil;
617         NSRange                                         scrollRange = NSMakeRange(0,0);
618         BOOL                                            appendedFirstLog = NO;
620     if (![logArray isEqualToArray:displayedLogArray]) {
621                 [displayedLogArray release];
622                 displayedLogArray = [logArray copy];
623         }
625         if ([logArray count] > 1) {
626                 displayText = [[NSMutableAttributedString alloc] init];
627         }
629         NSEnumerator *enumerator = [logArray objectEnumerator];
630         AIChatLog        *theLog;
631         NSString         *logBasePath = [AILoggerPlugin logBasePath];
632         AILog(@"Displaying %@",logArray);
633         while ((theLog = [enumerator nextObject])) {
634                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
635                 
636                 if (displayText) {
637                         if (!horizontalRule) {
638                                 #define HORIZONTAL_BAR                  0x2013
639                                 #define HORIZONTAL_RULE_LENGTH  18
640                                 
641                                 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
642                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
643                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
644                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
645                                 };
646                                 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
647                         }       
648                         
649                         [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
650                                 (appendedFirstLog ? @"\n" : @""),
651                                 horizontalRule,
652                                 ([NSApp isOnTigerOrBetter] ? 
653                                  [headerDateFormatter stringFromDate:[theLog date]] :
654                                  [[theLog date] descriptionWithCalendarFormat:[headerDateFormatter dateFormat]
655                                                                                                          timeZone:nil
656                                                                                                            locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]]),
657                                 [theLog to],
658                                 horizontalRule]
659                                            withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
660                 }
661                 
662                 if ([[theLog path] hasSuffix:@".AdiumHTMLLog"] || [[theLog path] hasSuffix:@".html"] || [[theLog path] hasSuffix:@".html.bak"]) {
663                         //HTML log
664                         NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
665                         
666                         if (displayText) {
667                                 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
668                         } else {
669                                 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
670                         }
672                 } else if ([[theLog path] hasSuffix:@".chatlog"]){
673                         //XML log
674                         NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog path]];
675                         
676                         //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.
677                         enum {
678                                 failedUtf8BomLength = 6
679                         };
680                         NSData *data = [NSData dataWithContentsOfMappedFile:logFullPath];
681                         const unsigned char *ptr = [data bytes];
682                         unsigned len = [data length];
683                         if ((len >= failedUtf8BomLength)
684                                 &&  (ptr[0] == 0xC3)
685                                 &&  (ptr[1] == 0x94)
686                                 &&  (ptr[2] == 0xC2)
687                                 &&  (ptr[3] == 0xAA)
688                                 &&  (ptr[4] == 0xC3)
689                                 &&  (ptr[5] == 0xB8)
690                                 ) {
691                                 //Yup. Back up the old file, then strip it off.
692                                 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);
693                                 NSString *backupPath = [logFullPath stringByAppendingPathExtension:@"bak"];
694                                 if(![[NSFileManager defaultManager] movePath:logFullPath toPath:backupPath handler:nil])
695                                         NSLog(@"Could not back up file; recovery failed. This transcript will probably appear blank in the transcript viewer.");
696                                 else {
697                                         NSRange range = { failedUtf8BomLength, len - failedUtf8BomLength };
698                                         NSData *theRestOfIt = [data subdataWithRange:range];
699                                         if([theRestOfIt writeToFile:logFullPath atomically:YES])
700                                                 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]);
701                                         else
702                                                 NSLog(@"Could not write fix!");
703                                 }
704                         }
705                         NSString *logFileText = [GBChatlogHTMLConverter readFile:logFullPath];
707                         if (logFileText) {
708                                 if (displayText)
709                                         [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
710                                 else
711                                         displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
712                         }
714                 } else {
715                         //Fallback: Plain text log
716                         NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
717                         if (logFileText) {
718                                 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
719                                 
720                                 if (displayText) {
721                                         [displayText appendAttributedString:[[[NSAttributedString alloc] initWithString:logFileText 
722                                                                                                                                                                                  attributes:[textAttributes dictionary]] autorelease]];
723                                 } else {
724                                         displayText = [[NSMutableAttributedString alloc] initWithString:logFileText attributes:[textAttributes dictionary]];
725                                 }
726                         }
727                 }
728                 
729                 appendedFirstLog = YES;
730                 
731                 [pool release];
732         }
733         
734         if (displayText && [displayText length]) {
735                 //Add pretty formatting to links
736                 [displayText addFormattingForLinks];
738                 //If we are searching by content, highlight the search results
739                 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
740                         NSEnumerator                            *enumerator;
741                         NSString                                        *searchWord;
742                         NSMutableArray                          *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
743                         NSScanner                                       *scanner = [NSScanner scannerWithString:activeSearchString];
744                         
745                         //Look for an initial quote
746                         while (![scanner isAtEnd]) {
747                                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
748                                 
749                                 [scanner scanUpToString:@"\"" intoString:NULL];
750                                 
751                                 //Scan past the quote
752                                 if (![scanner scanString:@"\"" intoString:NULL]) continue;
753                                 
754                                 NSString *quotedString;
755                                 //And a closing one
756                                 if (![scanner isAtEnd] &&
757                                         [scanner scanUpToString:@"\"" intoString:&quotedString]) {
758                                         //Scan past the quote
759                                         [scanner scanString:@"\"" intoString:NULL];
760                                         /* If a string within quotes is found, remove the words from the quoted string and add the full string
761                                          * to what we'll be highlighting.
762                                          *
763                                          * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
764                                          * "killer attack ninja kittens" OR ninja
765                                          * wouldn't highlight the word ninja by itself.
766                                          */
767                                         NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
768                                         int quotedWordsCount = [quotedWords count];
769                                         
770                                         for (int i = 0; i < quotedWordsCount; i++) {
771                                                 NSString        *quotedWord = [quotedWords objectAtIndex:i];
772                                                 if (i == 0) {
773                                                         //Originally started with a quote, so put it back on
774                                                         quotedWord = [@"\"" stringByAppendingString:quotedWord];
775                                                 }
776                                                 if (i == quotedWordsCount - 1) {
777                                                         //Originally ended with a quote, so put it back on
778                                                         quotedWord = [quotedWord stringByAppendingString:@"\""];
779                                                 }
780                                                 int searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
781                                                 if (searchWordsIndex != NSNotFound) {
782                                                         [searchWordsArray removeObjectAtIndex:searchWordsIndex];
783                                                 } else {
784                                                         NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
785                                                 }
786                                         }
787                                         
788                                         //Add the full quoted string
789                                         [searchWordsArray addObject:quotedString];
790                                 }
791                                 [pool release];
792                         }
794                         BOOL shouldScrollToWord = NO;
795                         scrollRange = NSMakeRange([displayText length],0);
797                         enumerator = [searchWordsArray objectEnumerator];
798                         while ((searchWord = [enumerator nextObject])) {
799                                 NSRange     occurrence;
800                                 
801                                 //Check against and/or.  We don't just remove it from the array because then we couldn't check case insensitively.
802                                 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
803                                         ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
804                                         [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
805                                         
806                                         //We'll want to scroll to the first occurrance of any matching word or words
807                                         if (occurrence.location < scrollRange.location) {
808                                                 scrollRange = occurrence;
809                                                 shouldScrollToWord = YES;
810                                         }
811                                 }
812                         }
813                         
814                         //If we shouldn't be scrolling to a new range, we want to scroll to the top
815                         if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
816                         
817                         [searchWordsArray release];
818                 }
819                 
820                 //Filter emoticons
821                 if (showEmoticons) {
822                         finalDisplayText = [[adium contentController] filterAttributedString:displayText
823                                                                                                                                  usingFilterType:AIFilterMessageDisplay
824                                                                                                                                            direction:AIFilterOutgoing
825                                                                                                                                                  context:nil];
826                 } else {
827                         finalDisplayText = displayText;
828                 }
829         }
831         if (finalDisplayText) {
832                 [[textView_content textStorage] setAttributedString:finalDisplayText];
834                 //Set this string and scroll to the top/bottom/occurrence
835                 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
836                         [textView_content scrollRangeToVisible:scrollRange];
837                 } else {
838                         [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
839                 }
841         } else {
842                 //No log selected, empty the view
843                 [textView_content setString:@""];
844         }
846         [displayText release];
849 - (void)displayLog:(AIChatLog *)theLog
851         [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
854 //Reselect the displayed log (Or another log if not possible)
855 - (void)selectDisplayedLog
857     int     firstIndex = NSNotFound;
858     
859     /* Is the log we had selected still in the table?
860          * (When performing an automatic search, we ignore the previous selection.  This ensures that we always
861      * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
862          */
863         if (!automaticSearch) {
864                 [resultsLock lock];
865                 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
866                 [resultsLock unlock];
867                 
868                 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
869         }
871         if (firstIndex != NSNotFound) {
872                 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
873     } else {
874         if (useSame == YES && sameSelection > 0) {
875             [tableView_results selectRow:sameSelection byExtendingSelection:NO];
876         } else {
877             [self selectFirstLog];
878         }
879     }
881     useSame = NO;
884 - (void)selectFirstLog
886         AIChatLog   *theLog = nil;
887         
888         //If our selected log is no more, select the first one in the list
889         [resultsLock lock];
890         if ([currentSearchResults count] != 0) {
891                 theLog = [currentSearchResults objectAtIndex:0];
892         }
893         [resultsLock unlock];
894         
895         //Change the table selection to this new log
896         //We need a little trickery here.  When we change the row, the table view will call our tableViewSelectionDidChange: method.
897         //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
898         //search.  As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
899         //that we instantiated this selection change, and not the user.
900         ignoreSelectionChange = YES;
901         [tableView_results selectRow:0 byExtendingSelection:NO];
902         [tableView_results scrollRowToVisible:0];
903         ignoreSelectionChange = NO;
905         [self displayLog:theLog];  //Manually update the displayed log
908 //Highlight the occurences of a search string within a displayed log
909 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
911     int                                 location = 0;
912     NSRange                             searchRange, foundRange;
913     NSString                    *plainBigString = [bigString string];
914         unsigned                        plainBigStringLength = [plainBigString length];
915         NSMutableDictionary *attributeDictionary = nil;
917     outRange->location = NSNotFound;
919     //Search for the little string in the big string
920     while (location != NSNotFound && location < plainBigStringLength) {
921         searchRange = NSMakeRange(location, plainBigStringLength-location);
922         foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
923                 
924                 //Bold and color this match
925         if (foundRange.location != NSNotFound) {
926                         if (outRange->location == NSNotFound) *outRange = foundRange;
928                         if (!attributeDictionary) {
929                                 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
930                                         [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
931                                         [NSColor yellowColor], NSBackgroundColorAttributeName,
932                                         nil];
933                         }
934                         [bigString addAttributes:attributeDictionary
935                                                            range:foundRange];
936         }
938         location = NSMaxRange(foundRange);
939     }
943 //Sorting --------------------------------------------------------------------------------------------------------------
944 #pragma mark Sorting
945 - (void)resortLogs
947         NSString *identifier = [selectedColumn identifier];
949     //Resort the data
950         [resultsLock lock];
951     if ([identifier isEqualToString:@"To"]) {
952                 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
953                 
954     } else if ([identifier isEqualToString:@"From"]) {
955         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
956                 
957     } else if ([identifier isEqualToString:@"Date"]) {
958         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
959                 
960     } else if ([identifier isEqualToString:@"Rank"]) {
961             [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
962         }
963         
964     [resultsLock unlock];
966     //Reload the data
967     [tableView_results reloadData];
969     //Reapply the selection
970     [self selectDisplayedLog];  
973 //Sorts the selected log array and adjusts the selected column
974 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
976     //If there already was a sorted column, remove the indicator image from it.
977     if (selectedColumn && selectedColumn != tableColumn) {
978         [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
979     }
980     
981     //Set the indicator image in the newly selected column
982     [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
983                            inTableColumn:tableColumn];
984     
985     //Set the highlighted table column.
986     [tableView_results setHighlightedTableColumn:tableColumn];
987     [selectedColumn release]; selectedColumn = [tableColumn retain];
988     sortDirection = direction;
989         
990         [self resortLogs];
993 //Searching ------------------------------------------------------------------------------------------------------------
994 #pragma mark Searching
995 //(Jag)Change search string
996 - (void)controlTextDidChange:(NSNotification *)notification
998     if (searchMode != LOG_SEARCH_CONTENT) {
999                 [self updateSearch:nil];
1000     }
1003 //Change search string (Called by searchfield)
1004 - (IBAction)updateSearch:(id)sender
1006     automaticSearch = NO;
1007     [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1008     [self startSearchingClearingCurrentResults:YES];
1011 //Change search mode (Called by mode menu)
1012 - (IBAction)selectSearchType:(id)sender
1014     automaticSearch = NO;
1016         //First, update the search mode to the newly selected type
1017     [self setSearchMode:[sender tag]]; 
1018         
1019         //Then, ensure we are ready to search using the current string
1020         [self setSearchString:activeSearchString];
1022         //Now we are ready to start searching
1023     [self startSearchingClearingCurrentResults:YES];
1026 //Begin a specific search
1027 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1029     automaticSearch = YES;
1030         //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1031     [self setSearchMode:inMode];
1032     [self setSearchString:inString];
1034     [self startSearchingClearingCurrentResults:YES];
1037 //Begin the current search
1038 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1040     NSDictionary    *searchDict;
1042     //Once all searches have exited, we can start a new one
1043         if (clearCurrentResults) {
1044                 [resultsLock lock];
1045                 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1046                 [self stopSearching];
1048                 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1049                 [resultsLock unlock];
1050         } else {
1051             //Stop any existing searches
1052                 [self stopSearching];   
1053         }
1055         searching = YES;
1056     searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1057                 [NSNumber numberWithInt:activeSearchID], @"ID",
1058                 [NSNumber numberWithInt:searchMode], @"Mode",
1059                 activeSearchString, @"String",
1060                 [plugin logContentIndex], @"SearchIndex",
1061                 nil];
1062     [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1063     
1064         //Update the table periodically while the logs load.
1065         [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1066         refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1067                                                                 target:self
1068                                                               selector:@selector(refreshResults)
1069                                                               userInfo:nil
1070                                                                repeats:YES] retain];
1073 //Abort any active searches
1074 - (void)stopSearching
1076     //Increase the active search ID so any existing searches stop, and then
1077     //wait for any active searches to finish and release the lock
1078     activeSearchID++;
1081 //Set the active search mode (Does not invoke a search)
1082 - (void)setSearchMode:(LogSearchMode)inMode
1084         //Get the NSTextFieldCell and use it only if it responds to setPlaceholderString: (10.3 and above)
1085         NSTextFieldCell *cell = [searchField_logs cell];
1086         if (![cell respondsToSelector:@selector(setPlaceholderString:)]) cell = nil;
1087         
1088     searchMode = inMode;
1089         
1090         //Clear any filter from the table if it's the current mode, as well
1091         switch (searchMode) {
1092                 case LOG_SEARCH_FROM:
1093                         [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1094                         break;
1096                 case LOG_SEARCH_TO:
1097                         [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1098                         break;
1099                         
1100                 case LOG_SEARCH_DATE:
1101                         [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1102                         break;
1104                 case LOG_SEARCH_CONTENT:
1105                         [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1106                         break;
1107         }
1109         [self updateRankColumnVisibility];
1110     [self buildSearchMenu];
1113 - (void)updateRankColumnVisibility
1115         NSTableColumn   *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1116         
1117         if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1118                 //Add the resultsColumn and resize if it should be shown but is not at present
1119                 if (!resultsColumn) {   
1120                         NSArray                 *tableColumns;
1122                         //Set up the results column
1123                         resultsColumn = [[NSTableColumn alloc] initWithIdentifier:@"Rank"];
1124                         [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1125                         [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1126                         
1127                         //Add it to the table
1128                         [tableView_results addTableColumn:resultsColumn];
1130                         //Make it half again as large as the desired width from the @"Rank" header title
1131                         [resultsColumn sizeToFit];
1132                         [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1133                         
1134                         tableColumns = [tableView_results tableColumns];
1135                         if ([tableColumns indexOfObject:resultsColumn] > 0) {
1136                                 NSTableColumn   *nextDoorNeighbor;
1138                                 //Adjust the column to the results column's left so results is now visible
1139                                 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1140                                 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1141                         }
1142                 }
1143         } else {
1144                 //Remove the resultsColumn and resize if it should not be shown but is at present
1145                 if (resultsColumn) {
1146                         NSArray                 *tableColumns;
1148                         tableColumns = [tableView_results tableColumns];
1149                         if ([tableColumns indexOfObject:resultsColumn] > 0) {
1150                                 NSTableColumn   *nextDoorNeighbor;
1152                                 //Adjust the column to the results column's left to take up the space again
1153                                 tableColumns = [tableView_results tableColumns];
1154                                 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1155                                 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1156                         }
1158                         //Remove it
1159                         [tableView_results removeTableColumn:resultsColumn];
1160                 }
1161         }
1164 //Set the active search string (Does not invoke a search)
1165 - (void)setSearchString:(NSString *)inString
1167     if (![[searchField_logs stringValue] isEqualToString:inString]) {
1168                 [searchField_logs setStringValue:(inString ? inString : @"")];
1169     }
1170         
1171         //Use autorelease so activeSearchString can be passed back to here
1172         if (activeSearchString != inString) {
1173                 [activeSearchString release];
1174                 activeSearchString = [inString retain];
1175         }
1177         [self updateRankColumnVisibility];
1180 //Build the search mode menu
1181 - (void)buildSearchMenu
1183     NSMenu  *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1184     [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1185     [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1186     [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1187     [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1189         [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1193  * @brief Focus the log viewer on a particular contact
1195  * If the contact is within a metacontact, the metacontact will be focused.
1196  */
1197 - (void)filterForContact:(AIListContact *)inContact
1199         AIListContact *parentContact = [inContact parentContact];
1200         
1201         /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1202          * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1203          * with regard to inContact since the log viewer opened.
1204          */
1205         [self rebuildContactsList];
1206         
1207         [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1208         unsigned int selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1209         if (selectedRow != NSNotFound) {
1210                 [outlineView_contacts scrollRowToVisible:selectedRow];
1211         }
1213         //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1214         if (searchMode == LOG_SEARCH_TO) {
1215                 [self setSearchMode:LOG_SEARCH_CONTENT];
1216                 
1217                 //Update our search string to ensure we're configured for content searching
1218                 [self setSearchString:activeSearchString];
1219         }
1220         
1221     [self startSearchingClearingCurrentResults:YES];
1225  * @brief Returns a menu item for the search mode menu
1226  */
1227 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1229     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title 
1230                                                                                                                                                                  action:@selector(selectSearchType:) 
1231                                                                                                                                                   keyEquivalent:@""];
1232     [menuItem setTag:mode];
1233     [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1234     
1235     return [menuItem autorelease];
1238 #pragma mark Filtering search results
1240 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1242         BOOL matchesDateFilter;
1244         switch (filterDateType) {
1245                 case AIDateTypeAfter:
1246                         matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1247                         break;
1248                 case AIDateTypeBefore:
1249                         matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1250                         break;
1251                 case AIDateTypeExactly:
1252                         matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1253                         break;
1254                 default:
1255                         matchesDateFilter = YES;
1256                         break;
1257         }
1259         return matchesDateFilter;
1263 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1265         CFURLRef        url = SKDocumentCopyURL(inDocument);
1266         CFStringRef logPath = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
1267         NSArray         *pathComponents = [(NSString *)logPath pathComponents];
1268         
1269         CFRelease(url);
1270         CFRelease(logPath);
1271         
1272         return pathComponents;
1276  * @brief Should a search display a document with the given information?
1277  */
1278 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1280         BOOL shouldDisplayDocument = YES;
1282         if ([contactIDsToFilter count]) {
1283                 //Determine the path components if we weren't supplied them
1284                 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1286                 unsigned int numPathComponents = [pathComponents count];
1287                 
1288                 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1289                 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1291                 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1293                 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1294         } 
1295         
1296         if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1297                 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1299                 unsigned int    numPathComponents = [pathComponents count];
1300                 NSString                *toPath = [NSString stringWithFormat:@"%@/%@",
1301                         [pathComponents objectAtIndex:numPathComponents-3],
1302                         [pathComponents objectAtIndex:numPathComponents-2]];
1303                 NSString                *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1304                 AIChatLog               *theLog;
1305                 
1306                 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1307                 
1308                 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1309         }
1311         return shouldDisplayDocument;
1314 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1315 #pragma mark Threaded filter/search methods
1316 //Search the logs, filtering out any matching logs into the currentSearchResults
1317 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1319     NSAutoreleasePool       *pool = [[NSAutoreleasePool alloc] init];
1320     int                     mode = [[searchInfoDict objectForKey:@"Mode"] intValue];
1321     int                     searchID = [[searchInfoDict objectForKey:@"ID"] intValue];
1322     NSString                *searchString = [searchInfoDict objectForKey:@"String"];
1324     if (searchID == activeSearchID) { //If we're still supposed to go
1325                 searching = YES;
1326                 
1327                 //Search
1328                 if (searchString && [searchString length]) {
1329                         switch (mode) {
1330                                 case LOG_SEARCH_FROM:
1331                                 case LOG_SEARCH_TO:
1332                                 case LOG_SEARCH_DATE:
1333                                         [self _logFilter:searchString
1334                                                         searchID:searchID
1335                                                                 mode:mode];
1336                                         break;
1337                                 case LOG_SEARCH_CONTENT:
1338                                         [self _logContentFilter:searchString
1339                                                                    searchID:searchID
1340                                                           onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1341                                         break;
1342                         }
1343                 } else {
1344                         [self _logFilter:nil
1345                                         searchID:searchID
1346                                                 mode:mode];
1347                 }
1348                 
1349                 //Refresh
1350                 searching = NO;
1351                 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1352     }
1353         
1354     //Cleanup
1355     [pool release];
1358 //Perform a filter search based on source name, destination name, or date
1359 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode
1361     NSEnumerator        *fromEnumerator, *toEnumerator, *logEnumerator;
1362     AILogToGroup        *toGroup;
1363     AILogFromGroup      *fromGroup;
1364     AIChatLog                   *theLog;
1365     UInt32              lastUpdate = TickCount();
1366     
1367     NSCalendarDate      *searchStringDate = nil;
1368         
1369         if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1370                 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString]  dateWithCalendarFormat:nil timeZone:nil];
1371         }
1372         
1373     //Walk through every 'from' group
1374     fromEnumerator = [logFromGroupDict objectEnumerator];
1375     while ((fromGroup = [fromEnumerator nextObject]) && (searchID == activeSearchID)) {
1376                 
1377                 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1378                 if ((mode != LOG_SEARCH_FROM) ||
1379                         (!searchString) || 
1380                         ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1382                         //Walk through every 'to' group
1383                         toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
1384                         while ((toGroup = [toEnumerator nextObject]) && (searchID == activeSearchID)) {
1385                                 
1386                                 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1387                                  * For all other search modes, we always proceed here so long as either:
1388                                  *      a) We are not filtering for specific contact names or
1389                                  *      b) The contact name matches one of the names in contactIDsToFilter
1390                                  */
1391                                 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1392                                    ((mode != LOG_SEARCH_TO) ||
1393                                    (!searchString) || 
1394                                    ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1395                                         
1396                                         //Walk through every log
1398                                         logEnumerator = [toGroup logEnumerator];
1399                                         while ((theLog = [logEnumerator nextObject]) && (searchID == activeSearchID)) {
1400                                                 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1401                                                  * For all other search modes, we always proceed here
1402                                                  */
1403                                                 if ((mode != LOG_SEARCH_DATE) ||
1404                                                    (!searchString) ||
1405                                                    (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1407                                                         if ([self chatLogMatchesDateFilter:theLog]) {
1408                                                                 //Add the log
1409                                                                 [resultsLock lock];
1410                                                                 [currentSearchResults addObject:theLog];
1411                                                                 [resultsLock unlock];                                                   
1412                                                                 
1413                                                                 //Update our status
1414                                                                 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1415                                                                         [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1416                                                                                                                    withObject:nil
1417                                                                                                                 waitUntilDone:NO];
1418                                                                         lastUpdate = TickCount();
1419                                                                 }
1420                                                         }
1421                                                 }
1422                                         }
1423                                 }
1424                         }           
1425                 }
1426     }
1429 //Search results table view --------------------------------------------------------------------------------------------
1430 #pragma mark Search results table view
1431 //Since this table view's source data will be accessed from within other threads, we need to lock before
1432 //accessing it.  We also must be very sure that an incorrect row request is handled silently, since this
1433 //can occur if the array size is changed during the reload.
1434 - (int)numberOfRowsInTableView:(NSTableView *)tableView
1436     int count;
1437     
1438     [resultsLock lock];
1439     count = [currentSearchResults count];
1440     [resultsLock unlock];
1441     
1442     return count;
1446 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
1448     NSString    *identifier = [tableColumn identifier];
1450         if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1451                 AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
1452                 
1453                 [aCell setPercentage:[theLog rankingPercentage]];
1454         }
1458 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
1460     NSString    *identifier = [tableColumn identifier];
1461     id          value = nil;
1462     
1463     [resultsLock lock];
1464     if (row < 0 || row >= [currentSearchResults count]) {
1465                 if ([identifier isEqualToString:@"Service"]) {
1466                         value = blankImage;
1467                 } else {
1468                         value = @"";
1469                 }
1470                 
1471         } else {
1472                 AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
1474                 if ([identifier isEqualToString:@"To"]) {
1475                         value = [theLog to]; 
1476                         
1477                 } else if ([identifier isEqualToString:@"From"]) {
1478                         value = [theLog from];
1479                         
1480                 } else if ([identifier isEqualToString:@"Date"]) {
1481                         value = [theLog date];
1482                         
1483                 } else if ([identifier isEqualToString:@"Service"]) {
1484                         NSString        *serviceClass;
1485                         NSImage         *image;
1486                         
1487                         serviceClass = [theLog serviceClass];
1488                         image = [AIServiceIcons serviceIconForService:[[adium accountController] firstServiceWithServiceID:serviceClass]
1489                                                                                                          type:AIServiceIconSmall
1490                                                                                                 direction:AIIconNormal];
1491                         value = (image ? image : blankImage);
1492                 }
1493     }
1494     [resultsLock unlock];
1495     
1496     return value;
1500 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1502     if (!ignoreSelectionChange) {
1503                 NSArray         *selectedLogs;
1504                 
1505                 //Update the displayed log
1506                 automaticSearch = NO;
1507                 
1508                 [resultsLock lock];
1509                 selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
1510                 [resultsLock unlock];
1511                 
1512                 [self displayLogs:selectedLogs];
1513     }
1516 //Sort the log array & reflect the new column
1517 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1518 {    
1519     [self sortCurrentSearchResultsForTableColumn:tableColumn
1520                                    direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1523 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1525     [self deleteSelection:nil];
1528 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
1530         if (tableView == tableView_results) {
1531                 [tableView setSearchColumnIdentifiers:[NSSet setWithObjects:@"To", @"From", nil]];
1532                 [tableView setSearchWraps:YES];
1534         } else if (tableView == (KFTypeSelectTableView *)outlineView_contacts) {
1535                 [tableView setSearchWraps:YES];
1536         }
1539 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1541         NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1543         if (!aNotification ||
1544                 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1545                 NSDateFormatter *dateFormatter;
1546                 NSCell                  *cell = [dateTableColumn dataCell];
1548                 [cell setObjectValue:[NSDate date]];
1550                 float width = [dateTableColumn width];
1552                 if ([NSApp isOnTigerOrBetter]) {
1553 #define NUMBER_TIME_STYLES      2
1554 #define NUMBER_DATE_STYLES      4
1555                         NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1556                         NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1557                         float requiredWidth;
1559                         dateFormatter = [cell formatter];
1560                         if (!dateFormatter) {
1561                                 dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1562                                 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1563                                 [cell setFormatter:dateFormatter];
1564                         }
1565                         
1566                         requiredWidth = width + 1;
1567                         for (int i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1568                                 [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1570                                 for (int j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1571                                         [dateFormatter setDateStyle:formatterStyles[j]];
1572                                         requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1573                                         //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1574                                         requiredWidth += 3;                                     
1575                                 }
1576                         }
1578                 } else {
1579                         NSEnumerator    *enumerator = [[NSArray arrayWithObjects:
1580                                 [[[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString] 
1581                                                                                 allowNaturalLanguage:NO] autorelease],
1582                                 [[[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSShortDateFormatString] 
1583                                                                                 allowNaturalLanguage:NO] autorelease],
1584                                 nil] objectEnumerator];
1585                         float requiredWidth = width + 1;
1586                         while ((requiredWidth > width) && (dateFormatter = [enumerator nextObject])) {
1587                                 [cell setFormatter:dateFormatter];
1588                                 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1589                                 requiredWidth += 3;
1590                         }
1591                 }
1592         }
1595 - (IBAction)toggleEmoticonFiltering:(id)sender
1597         showEmoticons = !showEmoticons;
1598         [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1599         [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1601         [self displayLogs:displayedLogArray];
1604 #pragma mark Outline View Data source
1605 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
1607         if (!item) {
1608                 if (index == 0) {
1609                         return allContactsIdentifier;
1611                 } else {
1612                         return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1613                 }
1615         } else {
1616                 if ([item isKindOfClass:[AIMetaContact class]]) {
1617                         return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1618                 }
1619         }
1620         
1621         return nil;
1624 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1626         return (!item || 
1627                         ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1628                         [item isKindOfClass:[NSArray class]]);
1631 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1633         if (!item) {
1634                 return [toArray count] + 1; //+1 for the All item
1636         } else if ([item isKindOfClass:[AIMetaContact class]]) {
1637                 unsigned count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1638                 if (count > 1)
1639                         return count;
1640                 else
1641                         return 0;
1643         } else {
1644                 return 0;
1645         }
1648 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1650         if (![NSApp isOnTigerOrBetter]) {
1651                 NSLog(@"item has address 0x%08x [class %@]", (unsigned long)item, [item class]);
1652         }
1653         
1654         Class itemClass = [item class];
1656         if (itemClass == [AIMetaContact class]) {
1657                 return [(AIMetaContact *)item longDisplayName];
1658                 
1659         } else if (itemClass == [AIListContact class]) {
1660                 if ([(AIListContact *)item parentContact] != item) {
1661                         //This contact is within a metacontact - always show its UID
1662                         return [(AIListContact *)item formattedUID];
1663                 } else {
1664                         return [(AIListContact *)item longDisplayName];
1665                 } 
1666                 
1667         } else if (itemClass == [AILogToGroup class]) {
1668                 return [(AILogToGroup *)item to];
1669                 
1670         } else if (itemClass == [allContactsIdentifier class]) {
1671                 int contactCount = [toArray count];
1672                 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1673                         ((contactCount == 1) ?
1674                          AILocalizedString(@"1 Contact", nil) :
1675                          [NSString stringWithFormat:AILocalizedString(@"%i Contacts", nil), contactCount])]; 
1677         } else if (itemClass == [NSString class]) {
1678                 return item;
1680         } else {
1681                 NSLog(@"%@: no idea",item);
1682                 return nil;
1683         }
1686 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1688         if ([item isKindOfClass:[AIMetaContact class]] &&
1689                 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1690                 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1691                  * If it has multiple contacts, use no icon unless a user icon is present.
1692                  */
1693                 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1694                                                                                                                 size:NSMakeSize(16,16)];
1695                 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1697                 [cell setImage:image];
1699         } else if ([item isKindOfClass:[AIListContact class]]) {
1700                 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1701                                                                                                                 size:NSMakeSize(16,16)];
1702                 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1703                                                                                                                         type:AIServiceIconSmall
1704                                                                                                            direction:AIIconFlipped];
1705                 [cell setImage:image];
1707         } else if ([item isKindOfClass:[AILogToGroup class]]) {
1708                 [cell setImage:[AIServiceIcons serviceIconForServiceID:[(AILogToGroup *)item serviceClass]
1709                                                                                                            type:AIServiceIconSmall
1710                                                                                                   direction:AIIconFlipped]];
1711                 
1712         } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1713                 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1714                         ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1715                         if (!adiumIconHighlighted) {
1716                                 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
1717                                                                                                    forClass:[self class]] retain];
1718                         }
1720                         [cell setImage:adiumIconHighlighted];
1722                 } else {
1723                         if (!adiumIcon) {
1724                                 adiumIcon = [[NSImage imageNamed:@"adium"
1725                                                                                 forClass:[self class]] retain];
1726                         }
1728                         [cell setImage:adiumIcon];
1729                 }
1731         } else if ([item isKindOfClass:[NSString class]]) {
1732                 [cell setImage:nil];
1733                 
1734         } else {
1735                 NSLog(@"%@: no idea",item);
1736                 [cell setImage:nil];
1737         }       
1740 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
1742         [self deleteSelection:nil];
1745 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1747         NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
1749         [contactIDsToFilter removeAllObjects];
1751         if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
1752                 id              item;
1753                 NSEnumerator *enumerator;
1755                 enumerator = [selectedItems objectEnumerator];
1756                 while ((item = [enumerator nextObject])) {
1757                         if ([item isKindOfClass:[AIMetaContact class]]) {
1758                                 NSEnumerator    *metaEnumerator;
1759                                 AIListContact   *contact;
1761                                 metaEnumerator = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectEnumerator];
1762                                 while ((contact = [metaEnumerator nextObject])) {
1763                                         [contactIDsToFilter addObject:
1764                                                 [[[NSString stringWithFormat:@"%@.%@",[contact serviceID],[contact UID]] compactedString] safeFilenameString]];
1765                                 }
1766                                 
1767                         } else if ([item isKindOfClass:[AIListContact class]]) {
1768                                 [contactIDsToFilter addObject:
1769                                         [[[NSString stringWithFormat:@"%@.%@",[(AIListContact *)item serviceID],[(AIListContact *)item UID]] compactedString] safeFilenameString]];
1770                                 
1771                         } else if ([item isKindOfClass:[AILogToGroup class]]) {
1772                                 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]]; 
1773                         }
1774                 }
1775         }
1776         
1777         [self startSearchingClearingCurrentResults:YES];
1780 static int toArraySort(id itemA, id itemB, void *context)
1782         NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
1783         NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
1785         return [nameA caseInsensitiveCompare:nameB];
1786 }       
1788 - (void)draggedDividerRightBy:(float)deltaX
1789 {       
1790         desiredContactsSourceListDeltaX = deltaX;
1791         [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
1792         desiredContactsSourceListDeltaX = 0;
1796 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
1798         if ((sender == splitView_contacts_results) &&
1799                 desiredContactsSourceListDeltaX != 0) {
1800                 float dividerThickness = [sender dividerThickness];
1802                 NSRect newFrame = [sender frame];               
1803                 NSRect leftFrame = [containingView_contactsSourceList frame]; 
1804                 NSRect rightFrame = [containingView_results frame];
1806                 leftFrame.size.width += desiredContactsSourceListDeltaX; 
1807                 leftFrame.size.height = newFrame.size.height;
1808                 leftFrame.origin = NSMakePoint(0,0);
1810                 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
1811                 rightFrame.size.height = newFrame.size.height;
1812                 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
1814                 [containingView_contactsSourceList setFrame:leftFrame];
1815                 [containingView_contactsSourceList setNeedsDisplay:YES];
1816                 [containingView_results setFrame:rightFrame];
1817                 [containingView_results setNeedsDisplay:YES];
1819         } else {
1820                 //Perform the default implementation
1821                 [sender adjustSubviews];
1822         }
1826 //Window Toolbar -------------------------------------------------------------------------------------------------------
1827 #pragma mark Window Toolbar
1828 - (NSString *)dateItemNibName
1830         return nil;
1833 - (void)installToolbar
1834 {       
1835         [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
1837     NSToolbar           *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
1838     NSToolbarItem       *toolbarItem;
1839         
1840     [toolbar setDelegate:self];
1841     [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
1842     [toolbar setSizeMode:NSToolbarSizeModeRegular];
1843     [toolbar setVisible:YES];
1844     [toolbar setAllowsUserCustomization:YES];
1845     [toolbar setAutosavesConfiguration:YES];
1846     toolbarItems = [[NSMutableDictionary alloc] init];
1848         //Delete Logs
1849         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1850                                         withIdentifier:@"delete"
1851                                                  label:DELETE
1852                                           paletteLabel:DELETE
1853                                                toolTip:AILocalizedString(@"Delete the selection",nil)
1854                                                 target:self
1855                                        settingSelector:@selector(setImage:)
1856                                            itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
1857                                                 action:@selector(deleteSelection:)
1858                                                   menu:nil];
1859         
1860         //Search
1861         [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
1862         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
1863                                                                                                                   label:SEARCH
1864                                                                                                    paletteLabel:SEARCH
1865                                                                                                                 toolTip:AILocalizedString(@"Search or filter logs",nil)
1866                                                                                                                  target:self
1867                                                                                                 settingSelector:@selector(setView:)
1868                                                                                                         itemContent:view_SearchField
1869                                                                                                                  action:@selector(updateSearch:)
1870                                                                                                                    menu:nil];
1871         if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1872                 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
1873         }
1874         [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
1875         [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
1876         [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1878         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
1879                                                                                                                   label:AILocalizedString(@"Date", nil)
1880                                                                                                    paletteLabel:AILocalizedString(@"Date", nil)
1881                                                                                                                 toolTip:AILocalizedString(@"Filter logs by date",nil)
1882                                                                                                                  target:self
1883                                                                                                 settingSelector:@selector(setView:)
1884                                                                                                         itemContent:view_DatePicker
1885                                                                                                                  action:nil
1886                                                                                                                    menu:nil];
1887         if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1888                 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
1889         }
1890         [toolbarItem setMinSize:[view_DatePicker frame].size];
1891         [toolbarItem setMaxSize:[view_DatePicker frame].size];
1892         [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1894         //Toggle Emoticons
1895         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1896                                                                         withIdentifier:@"toggleemoticons"
1897                                                                                          label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
1898                                                                           paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
1899                                                                                    toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
1900                                                                                         target:self
1901                                                                    settingSelector:@selector(setImage:)
1902                                                                            itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
1903                                                                                         action:@selector(toggleEmoticonFiltering:)
1904                                                                                           menu:nil];
1906         [[self window] setToolbar:toolbar];
1908         [self configureDateFilter];
1911 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
1913     return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
1916 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
1918     return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
1919                 @"delete", @"toggleemoticons", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
1920                 @"search", nil];
1923 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
1925     return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
1926                 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
1927                         NSToolbarSpaceItemIdentifier,
1928                         NSToolbarFlexibleSpaceItemIdentifier,
1929                         NSToolbarCustomizeToolbarItemIdentifier, 
1930                         NSToolbarPrintItemIdentifier, nil]];
1933 - (void)toolbarWillAddItem:(NSNotification *)notification
1935         NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
1936         if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
1937                 [item setTarget:self];
1938                 [item setAction:@selector(adiumPrint:)];
1939         }
1942 #pragma mark Date filter
1945  * @brief Returns a menu item for the date type filter menu
1946  */
1947 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
1949     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInt:dateType]] 
1950                                                                                                                                                                  action:@selector(selectDateType:) 
1951                                                                                                                                                   keyEquivalent:@""];
1952     [menuItem setTag:dateType];
1953     
1954     return [menuItem autorelease];
1957 - (NSMenu *)dateTypeMenu
1959         NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
1960                 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInt:AIDateTypeAnyDate],
1961                 AILocalizedString(@"Today", nil), [NSNumber numberWithInt:AIDateTypeToday],
1962                 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInt:AIDateTypeSinceYesterday],
1963                 AILocalizedString(@"This Week", nil), [NSNumber numberWithInt:AIDateTypeThisWeek],
1964                 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoWeeks],
1965                 AILocalizedString(@"This Month", nil), [NSNumber numberWithInt:AIDateTypeThisMonth],
1966                 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoMonths],
1967                 nil];
1968         NSMenu  *dateTypeMenu = [[NSMenu alloc] init];
1969         AIDateType dateType;
1970         
1971         [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
1972         [dateTypeMenu addItem:[NSMenuItem separatorItem]];
1974         for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
1975                 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
1976         }
1977         
1978         return [dateTypeMenu autorelease];
1981 - (int)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
1983         int todayDayOfWeek = [today dayOfWeek];
1985         //Try to look at the iCal preferences if possible
1986         if (!iCalFirstDayOfWeekDetermined) {
1987                 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
1988                 if (iCalFirstDayOfWeek) {
1989                         //This should return a CFNumberRef... we're using another app's prefs, so make sure.
1990                         if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
1991                                 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek intValue];
1992                         }
1994                         CFRelease(iCalFirstDayOfWeek);
1995                 }
1997                 //Don't check again
1998                 iCalFirstDayOfWeekDetermined = YES;
1999         }
2001         return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2005  * @brief A new date type was selected
2007  * This does not start a search
2008  */
2009 - (void)selectedDateType:(AIDateType)dateType
2011         NSCalendarDate  *today = [NSCalendarDate date];
2012         
2013         [filterDate release]; filterDate = nil;
2014         
2015         switch (dateType) {
2016                 case AIDateTypeAnyDate:
2017                         filterDateType = AIDateTypeAnyDate;
2018                         break;
2019                         
2020                 case AIDateTypeToday:
2021                         filterDateType = AIDateTypeExactly;
2022                         filterDate = [today retain];
2023                         break;
2024                         
2025                 case AIDateTypeSinceYesterday:
2026                         filterDateType = AIDateTypeAfter;
2027                         filterDate = [[today dateByAddingYears:0
2028                                                                                         months:0
2029                                                                                           days:-1
2030                                                                                          hours:-[today hourOfDay]
2031                                                                                    minutes:-[today minuteOfHour]
2032                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2033                         break;
2034                         
2035                 case AIDateTypeThisWeek:
2036                         filterDateType = AIDateTypeAfter;
2037                         filterDate = [[today dateByAddingYears:0
2038                                                                                         months:0
2039                                                                                           days:-[self daysSinceStartOfWeekGivenToday:today]
2040                                                                                          hours:-[today hourOfDay]
2041                                                                                    minutes:-[today minuteOfHour]
2042                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2043                         break;
2044                         
2045                 case AIDateTypeWithinLastTwoWeeks:
2046                         filterDateType = AIDateTypeAfter;
2047                         filterDate = [[today dateByAddingYears:0
2048                                                                                         months:0
2049                                                                                           days:-14
2050                                                                                          hours:-[today hourOfDay]
2051                                                                                    minutes:-[today minuteOfHour]
2052                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2053                         break;
2054                         
2055                 case AIDateTypeThisMonth:
2056                         filterDateType = AIDateTypeAfter;
2057                         filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2058                                                                                                                         months:0
2059                                                                                                                           days:-[today dayOfMonth]
2060                                                                                                                          hours:0
2061                                                                                                                    minutes:0
2062                                                                                                                    seconds:-1] retain];
2063                         break;
2064                         
2065                 case AIDateTypeWithinLastTwoMonths:
2066                         filterDateType = AIDateTypeAfter;
2067                         filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2068                                                                                                                         months:-1
2069                                                                                                                           days:-[today dayOfMonth]
2070                                                                                                                          hours:0
2071                                                                                                                    minutes:0
2072                                                                                                                    seconds:-1] retain];                 
2073                         break;
2074                         
2075                 default:
2076                         break;
2077         }       
2081  * @brief Select the date type
2082  */
2083 - (void)selectDateType:(id)sender
2085         [self selectedDateType:[sender tag]];
2086         [self startSearchingClearingCurrentResults:YES];
2089 - (void)configureDateFilter
2091         firstDayOfWeek = 0; /* Sunday */
2092         iCalFirstDayOfWeekDetermined = NO;
2094         [popUp_dateFilter setMenu:[self dateTypeMenu]];
2095         int index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2096         if(index != NSNotFound)
2097                 [popUp_dateFilter selectItemAtIndex:index];
2098         [self selectedDateType:AIDateTypeAnyDate];
2101 #pragma mark Open Log
2103 - (void)openLogAtPath:(NSString *)inPath
2105         AIChatLog   *chatLog = nil;
2106         NSString        *basePath = [AILoggerPlugin logBasePath];
2108         //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2109         NSArray         *pathComponents = [inPath pathComponents];
2110         int                     lastIndex = [pathComponents count];
2111         NSString        *logName = [pathComponents objectAtIndex:--lastIndex];
2112         NSString        *contactName = [pathComponents objectAtIndex:--lastIndex];
2113         NSString        *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];    
2114         NSString                *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2116         NSString        *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2117         //Filter for logs from the contact associated with the log we're loading
2118         [self filterForContact:[[adium contactController] contactWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
2119                                                                                                                                  account:nil
2120                                                                                                                                          UID:contactName]];
2121         
2122         NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2123         NSString *canonicalInPath = [inPath stringByStandardizingPath];
2125         if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2126                 AILogToGroup    *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2127                 
2128                 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2129                 
2130         } else {
2131                 /* Different Adium user... this sucks. We're given a path like this:
2132                  *      /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2133                  * and we want to make it relative to our current user's logs folder, which might be
2134                  *  /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2135                  *
2136                  * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2137                  */
2138                 NSString        *fakeRelativePath = @"";
2139                 
2140                 //Use .. to get back to the root from the base path
2141                 int componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2142                 for (int i = 0; i < componentsOfBasePath; i++) {
2143                         fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2144                 }
2145                 
2146                 //Now add the path from the root to the actual log
2147                 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2148                 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2149                                                                                           from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2150                                                                                                 to:contactName
2151                                                                           serviceClass:serviceID] autorelease];
2152         }
2154         //Now display the requested log
2155         if (chatLog) {
2156                 [self displayLog:chatLog];
2157         }
2160 #pragma mark Printing
2162 - (void)adiumPrint:(id)sender
2164         [textView_content print:sender];
2167 - (BOOL)validatePrintMenuItem:(id <NSMenuItem>)menuItem
2169         return ([displayedLogArray count] > 0);
2172 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2174         if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2175                 return [self validatePrintMenuItem:nil];
2177         } else {
2178                 return YES;
2179         }
2182 - (void)selectCachedIndex
2184         int numberOfRows = [tableView_results numberOfRows];
2185         
2186         if (cachedSelectionIndex <  numberOfRows) {
2187                 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2188                                            byExtendingSelection:NO];
2189         } else {
2190                 if (numberOfRows)
2191                         [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2192                                                    byExtendingSelection:NO];                    
2193         }
2195         if (numberOfRows) {
2196                 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2197         }
2199         deleteOccurred = NO;
2202 #pragma mark Deletion
2205  * @brief Get an NSAlert to request deletion of multiple logs
2206  */
2207 - (NSAlert *)alertForDeletionOfLogCount:(int)logCount
2209         NSAlert *alert = [[NSAlert alloc] init];
2210         [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2211         [alert setInformativeText:[NSString stringWithFormat:
2212                 AILocalizedString(@"Are you sure you want to send %i logs to the Trash?",nil), logCount]];
2213         [alert addButtonWithTitle:DELETE]; 
2214         [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2215         
2216         return [alert autorelease];
2220  * @brief Undo the deletion of one or more AIChatLogs
2222  * The logs will be marked for readdition to the index
2223  */
2224 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2226         NSEnumerator    *enumerator;
2227         AIChatLog               *aLog;
2228         NSFileManager   *fileManager = [NSFileManager defaultManager];
2229         NSString                *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2231         enumerator = [deletedLogs objectEnumerator];
2232         while ((aLog = [enumerator nextObject])) {
2233                 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2234                 
2235                 [fileManager createDirectoriesForPath:[logPath stringByDeletingLastPathComponent]];
2236                 
2237                 [fileManager movePath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2238                                            toPath:logPath 
2239                                           handler:NULL];
2240                 
2241                 [plugin markLogDirtyAtPath:logPath];
2242         }
2243         
2244         [self rebuildIndices];
2247 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode  contextInfo:(void *)contextInfo;
2249         NSArray *selectedLogs = (NSArray *)contextInfo;
2250         if (returnCode == NSAlertFirstButtonReturn) {
2251                 [resultsLock lock];
2252                 
2253                 AIChatLog               *aLog;
2254                 NSEnumerator    *enumerator;
2255                 NSMutableSet    *logPaths = [NSMutableSet set];
2256                 
2257                 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2258                 
2259                 enumerator = [selectedLogs objectEnumerator];
2260                 while ((aLog = [enumerator nextObject])) {
2261                         NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2262                         
2263                         [[adium notificationCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2264                         AILogToGroup    *logToGroup = [logToGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@/%@",[aLog serviceClass],[aLog from],[aLog to]]];
2265                         [logToGroup trashLog:aLog];
2266                         
2267                         //Clear the to group out if it no longer has anything of interest
2268                         if ([logToGroup logCount] == 0) {
2269                                 AILogFromGroup  *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[aLog serviceClass],[aLog from]]];
2270                                 [logFromGroup removeToGroup:logToGroup];
2271                         }
2272                         
2273                         [logPaths addObject:logPath];
2274                         [currentSearchResults removeObjectIdenticalTo:aLog];
2275                 }
2276                 
2277                 [plugin removePathsFromIndex:logPaths];
2278                 
2279                 [undoManager registerUndoWithTarget:self
2280                                                                    selector:@selector(restoreDeletedLogs:)
2281                                                                          object:selectedLogs];
2282                 [undoManager setActionName:DELETE];
2283                 
2284                 [resultsLock unlock];
2285                 [tableView_results reloadData];
2286                 
2287                 deleteOccurred = YES;
2288                 
2289                 [self rebuildContactsList];
2290                 [self updateProgressDisplay];
2291         }
2292         [selectedLogs release];
2296  * @brief Delete logs
2298  * If two or more logs are passed, confirmation will be requested.
2299  * This operation registers with the window controller's undo manager.
2301  * @param selectedLogs An NSArray of logs to delete
2302  */
2303 - (void)deleteLogs:(NSArray *)selectedLogs
2304 {       
2305         if ([selectedLogs count] > 1) {
2306                 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2307                 [alert beginSheetModalForWindow:[self window]
2308                                                   modalDelegate:self
2309                                                  didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2310                                                         contextInfo:[selectedLogs retain]];
2311         } else {
2312                 [self deleteLogsAlertDidEnd:nil
2313                                                  returnCode:NSAlertFirstButtonReturn
2314                                                 contextInfo:[selectedLogs retain]];
2315         }
2319  * @brief Returns a set of all selected to groups on all accounts
2321  * @param logCount If non-NULL, will be set to the total number of logs on return
2322  */
2323 - (NSSet *)allSelectedToGroups:(int *)totalLogCount
2325     NSEnumerator        *fromEnumerator;
2326     AILogFromGroup      *fromGroup;
2327         NSMutableSet            *allToGroups = [NSMutableSet set];
2329         if (totalLogCount) *totalLogCount = 0;
2331     //Walk through every 'from' group
2332     fromEnumerator = [logFromGroupDict objectEnumerator];
2333     while ((fromGroup = [fromEnumerator nextObject])) {
2334                 NSEnumerator        *toEnumerator;
2335                 AILogToGroup        *toGroup;
2337                 //Walk through every 'to' group
2338                 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2339                 while ((toGroup = [toEnumerator nextObject])) {
2340                         if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2341                                 if (totalLogCount) {
2342                                         *totalLogCount += [toGroup logCount];
2343                                 }
2344                                 
2345                                 [allToGroups addObject:toGroup];
2346                         }
2347                 }
2348         }
2349         
2350         return allToGroups;
2354  * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2356  * The logs will be marked for readdition to the index
2357  */
2358 - (void)restoreDeletedToGroups:(NSSet *)toGroups
2360         NSEnumerator    *enumerator;
2361         AILogToGroup    *toGroup;
2362         NSFileManager   *fileManager = [NSFileManager defaultManager];
2363         NSString                *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2364         NSString                *logBasePath = [AILoggerPlugin logBasePath];
2366         enumerator = [toGroups objectEnumerator];
2367         while ((toGroup = [enumerator nextObject])) {
2368                 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup path]];
2370                 [fileManager createDirectoriesForPath:[toGroupPath stringByDeletingLastPathComponent]];
2371                 if ([fileManager fileExistsAtPath:toGroupPath]) {
2372                         AILog(@"Removing path %@ to make way for %@",
2373                                   toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2374                         [fileManager removeFileAtPath:toGroupPath
2375                                                                   handler:NULL];
2376                 }
2377                 [fileManager movePath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2378                                            toPath:toGroupPath
2379                                           handler:NULL];
2380                 
2381                 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2382                 AIChatLog        *aLog;
2383         
2384                 while ((aLog = [logEnumerator nextObject])) {
2385                         [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog path]]];
2386                 }
2387         }
2388         
2389         [self rebuildIndices];  
2392 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2394         NSSet *allSelectedToGroups = (NSSet *)contextInfo;
2395         if (returnCode == NSAlertFirstButtonReturn) {
2396                 AILogToGroup    *logToGroup;
2397                 NSEnumerator    *enumerator;
2398                 NSMutableSet    *logPaths = [NSMutableSet set];
2399                 
2400                 enumerator = [allSelectedToGroups objectEnumerator];
2401                 while ((logToGroup = [enumerator nextObject])) {
2402                         NSEnumerator *logEnumerator;
2403                         AIChatLog        *aLog;
2404                         
2405                         logEnumerator = [logToGroup logEnumerator];
2406                         while ((aLog = [logEnumerator nextObject])) {
2407                                 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2408                                 [logPaths addObject:logPath];
2409                         }
2410                         
2411                         AILogFromGroup  *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2412                         [logFromGroup removeToGroup:logToGroup];
2413                 }
2414                 
2415                 [plugin removePathsFromIndex:logPaths];
2416                 
2417                 [undoManager registerUndoWithTarget:self
2418                                                                    selector:@selector(restoreDeletedToGroups:)
2419                                                                          object:allSelectedToGroups];
2420                 [undoManager setActionName:DELETE];
2421                 
2422                 [self rebuildIndices];
2423                 [self updateProgressDisplay];
2424         }
2425         
2426         [allSelectedToGroups release];
2430  * @brief Delete entirely the logs of all contacts selected in the source list
2432  * Confirmation by the user will be required.
2434  * Note: A single item in the source list may have multiple associated AILogToGroups.
2435  */
2436 - (void)deleteSelectedContactsFromSourceList
2438         int totalLogCount;
2439         NSSet *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2441         if (totalLogCount > 1) {
2442                 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2443                 [alert beginSheetModalForWindow:[self window]
2444                                                   modalDelegate:self
2445                                                  didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2446                                                         contextInfo:[allSelectedToGroups retain]];
2447         } else {
2448                 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2449                                                                                                    returnCode:NSAlertFirstButtonReturn
2450                                                                                                   contextInfo:[allSelectedToGroups retain]];
2451         }
2455  * @brief Delete the current selection
2457  * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2458  * If anything else is selected, the currently selected search result logs will be trashed.
2459  */
2460 - (void)deleteSelection:(id)sender
2462         if ([[self window] firstResponder] == outlineView_contacts) {
2463                 [self deleteSelectedContactsFromSourceList];
2464                 
2465         } else {
2466                 [resultsLock lock];
2467                 NSArray *selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
2468                 [resultsLock unlock];
2469                 
2470                 [self deleteLogs:selectedLogs];
2471         }
2474 #pragma mark Undo
2476  * @brief Supply our undo manager when we are within the responder chain
2477  */
2478 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
2480         return undoManager;
2483 @end