Unescape the HREF attribute's text before passing it to NSURL which does not expect...
[adiumx.git] / Source / AIAbstractLogViewerWindowController.m
blob814976dce2bb661eb986c8cce8985e58ddab6561
1 //
2 //  AIAbstractLogViewerWindowController.m
3 //  Adium
4 //
5 //  Created by Evan Schoenberg on 3/24/06.
6 //
8 #import "AIAbstractLogViewerWindowController.h"
9 #import "AIChatLog.h"
10 #import "AILogFromGroup.h"
11 #import "AILogToGroup.h"
12 #import "AILoggerPlugin.h"
13 #import "ESRankingCell.h" 
14 #import "GBChatlogHTMLConverter.h"
15 #import "AILogDateFormatter.h"
17 #import <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIPreferenceControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIContentControllerProtocol.h>
21 #import <Adium/AIMenuControllerProtocol.h>
22 #import <Adium/AIHTMLDecoder.h>
23 #import <Adium/AIListContact.h>
24 #import <Adium/AIMetaContact.h>
25 #import <Adium/AIServiceIcons.h>
26 #import <Adium/AIUserIcons.h>
27 #import <Adium/KFTypeSelectTableView.h>
28 #import <Adium/KNShelfSplitView.h>
29 #import <AIUtilities/AIArrayAdditions.h>
30 #import <AIUtilities/AIAttributedStringAdditions.h>
31 #import <AIUtilities/AIDateFormatterAdditions.h>
32 #import <AIUtilities/AIFileManagerAdditions.h>
33 #import <AIUtilities/AIImageAdditions.h>
34 #import <AIUtilities/AIImageTextCell.h>
35 #import <AIUtilities/AIOutlineViewAdditions.h>
36 #import <AIUtilities/AISplitView.h>
37 #import <AIUtilities/AIStringAdditions.h>
38 #import <AIUtilities/AITableViewAdditions.h>
39 #import <AIUtilities/AITextAttributes.h>
40 #import <AIUtilities/AIToolbarUtilities.h>
41 #import <AIUtilities/AIApplicationAdditions.h>
42 #import <AIUtilities/AIDividedAlternatingRowOutlineView.h>
44 #define KEY_LOG_VIEWER_WINDOW_FRAME             @"Log Viewer Frame"
45 #define PREF_GROUP_CONTACT_LIST                 @"Contact List"
46 #define KEY_LOG_VIEWER_GROUP_STATE              @"Log Viewer Group State"       //Expand/Collapse state of groups
47 #define TOOLBAR_LOG_VIEWER                              @"Log Viewer Toolbar"
49 #define MAX_LOGS_TO_SORT_WHILE_SEARCHING        10000   //Max number of logs we will live sort while searching
50 #define LOG_SEARCH_STATUS_INTERVAL                      20      //1/60ths of a second to wait before refreshing search status
52 #define SEARCH_MENU                                             AILocalizedString(@"Search Menu",nil)
53 #define FROM                                                    AILocalizedString(@"From",nil)
54 #define TO                                                              AILocalizedString(@"To",nil)
55 #define DATE                                                    AILocalizedString(@"Date",nil)
56 #define CONTENT                                                 AILocalizedString(@"Content",nil)
57 #define DELETE                                                  AILocalizedString(@"Delete",nil)
58 #define DELETEALL                                               AILocalizedString(@"Delete All",nil)
59 #define SEARCH                                                  AILocalizedString(@"Search",nil)
61 #define HIDE_EMOTICONS                                  AILocalizedString(@"Hide Emoticons",nil)
62 #define SHOW_EMOTICONS                                  AILocalizedString(@"Show Emoticons",nil)
64 #define IMAGE_EMOTICONS_OFF                             @"emoticon32"
65 #define IMAGE_EMOTICONS_ON                              @"emoticon32_transparent"
67 #define REFRESH_RESULTS_INTERVAL                1.0 //Interval between results refreshes while searching
69 @interface AIAbstractLogViewerWindowController (PRIVATE)
70 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
71 - (void)initLogFiltering;
72 - (void)displayLog:(AIChatLog *)log;
73 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
74 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
75 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
76 - (void)buildSearchMenu;
77 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
78 - (void)_logContentFilter:(NSString *)searchString searchID:(int)searchID onSearchIndex:(SKIndexRef)logSearchIndex;
79 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode;
80 - (void)installToolbar;
81 - (void)updateRankColumnVisibility;
82 - (void)openLogAtPath:(NSString *)inPath;
83 - (void)rebuildContactsList;
84 - (void)filterForContact:(AIListContact *)inContact;
85 - (void)selectCachedIndex;
87 - (void)_willOpenForContact;
88 - (void)_didOpenForContact;
90 - (void)deleteSelection:(id)sender;
91 @end
93 @implementation AIAbstractLogViewerWindowController
95 static AIAbstractLogViewerWindowController      *sharedLogViewerInstance = nil;
96 static int toArraySort(id itemA, id itemB, void *context);
98 + (NSString *)nibName
100         return @"LogViewer";    
103 + (id)openForPlugin:(id)inPlugin
105     if (!sharedLogViewerInstance) {
106                 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
107         }
109     [sharedLogViewerInstance showWindow:nil];
110     
111         return sharedLogViewerInstance;
114 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
116         [self openForPlugin:inPlugin];
117         
118         [sharedLogViewerInstance openLogAtPath:inPath];
119         
120         return sharedLogViewerInstance;
123 //Open the log viewer window to a specific contact's logs
124 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
126     if (!sharedLogViewerInstance) {
127                 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
128         }
130         [sharedLogViewerInstance _willOpenForContact];
131         [sharedLogViewerInstance showWindow:nil];
132         [sharedLogViewerInstance filterForContact:inContact];
133         [sharedLogViewerInstance _didOpenForContact];
135     return sharedLogViewerInstance;
138 //Returns the window controller if one exists
139 + (id)existingWindowController
141     return sharedLogViewerInstance;
144 //Close the log viewer window
145 + (void)closeSharedInstance
147     if (sharedLogViewerInstance) {
148         [sharedLogViewerInstance closeWindow:nil];
149     }
152 //init
153 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
155     //init
156     plugin = inPlugin;
157     selectedColumn = nil;
158     activeSearchID = 0;
159     searching = NO;
160     automaticSearch = YES;
161     showEmoticons = NO;
162     activeSearchString = nil;
163     displayedLogArray = nil;
164     windowIsClosing = NO;
165         desiredContactsSourceListDeltaX = 0;
167     blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
169     sortDirection = YES;
170     searchMode = LOG_SEARCH_CONTENT;
171     headerDateFormatter = [[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString] 
172                                                                                                  allowNaturalLanguage:NO];
173     currentSearchResults = [[NSMutableArray alloc] init];
174     fromArray = [[NSMutableArray alloc] init];
175     fromServiceArray = [[NSMutableArray alloc] init];
176     logFromGroupDict = [[NSMutableDictionary alloc] init];
177     toArray = [[NSMutableArray alloc] init];
178     toServiceArray = [[NSMutableArray alloc] init];
179     logToGroupDict = [[NSMutableDictionary alloc] init];
180     resultsLock = [[NSRecursiveLock alloc] init];
181     searchingLock = [[NSLock alloc] init];
182         contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
184         allContactsIdentifier = [[NSNumber alloc] initWithInt:-1];
186         undoManager = [[NSUndoManager alloc] init];
188     [super initWithWindowNibName:windowNibName];
189         
190     return self;
193 //dealloc
194 - (void)dealloc
196     [resultsLock release];
197     [searchingLock release];
198     [fromArray release];
199     [fromServiceArray release];
200     [toArray release];
201     [toServiceArray release];
202     [currentSearchResults release];
203     [selectedColumn release];
204     [headerDateFormatter release];
205     [displayedLogArray release];
206     [blankImage release];
207     [activeSearchString release];
208         [contactIDsToFilter release];
210         [logFromGroupDict release]; logFromGroupDict = nil;
211         [logToGroupDict release]; logToGroupDict = nil;
213     [filterForAccountName release]; filterForAccountName = nil;
215         [horizontalRule release]; horizontalRule = nil;
217         [adiumIcon release]; adiumIcon = nil;
218         [adiumIconHighlighted release]; adiumIconHighlighted = nil;
219         
220         //We loaded     view_DatePicker from a nib manually, so we must release it
221         [view_DatePicker release]; view_DatePicker = nil;
223         [allContactsIdentifier release];
224     [undoManager release]; undoManager = nil;
226     [super dealloc];
229 //Init our log filtering tree
230 - (void)initLogFiltering
232     NSEnumerator                        *enumerator;
233     NSString                            *folderName;
234     NSMutableDictionary         *toDict = [NSMutableDictionary dictionary];
235     NSString                            *basePath = [AILoggerPlugin logBasePath];
236     NSString                            *fromUID, *serviceClass;
238     //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
239         //first by service, then by account name.
240         enumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:basePath] sortedArrayUsingSelector:@selector(compare:)] objectEnumerator];
241     while ((folderName = [enumerator nextObject])) {
242                 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
243                         NSEnumerator    *toEnum;
244                         AILogToGroup    *currentToGroup;                        
245                         AILogFromGroup  *logFromGroup;
246                         NSMutableSet    *toSetForThisService;
247                         NSArray         *serviceAndFromUIDArray;
248                         
249                         /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
250                          * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
251                          */
252                         serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
254                         if ([serviceAndFromUIDArray count] >= 2) {
255                                 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
257                                 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
258                                 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
259                         } else {
260                                 //Fallback: blank non-nil serviceClass; folderName as the fromUID
261                                 serviceClass = @"";
262                                 fromUID = folderName;
263                         }
265                         logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
267                         //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
268                         [logFromGroupDict setObject:logFromGroup forKey:folderName];
270                         //To processing
271                         if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
272                                 toSetForThisService = [NSMutableSet set];
273                                 [toDict setObject:toSetForThisService
274                                                    forKey:serviceClass];
275                         }
277                         //Add the 'to' for each grouping on this account
278                         toEnum = [[logFromGroup toGroupArray] objectEnumerator];
279                         while ((currentToGroup = [toEnum nextObject])) {
280                                 NSString        *currentTo;
282                                 if ((currentTo = [currentToGroup to])) {
283                                         //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
284                                         [logToGroupDict setObject:currentToGroup forKey:[currentToGroup path]];
285                                 }
286                         }
288                         [logFromGroup release];
289                 }
290         }
292         [self rebuildContactsList];
295 - (void)rebuildContactsList
297         NSEnumerator    *enumerator = [logFromGroupDict objectEnumerator];
298         AILogFromGroup  *logFromGroup;
299         
300         int     oldCount = [toArray count];
301         [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
303         while ((logFromGroup = [enumerator nextObject])) {
304                 NSEnumerator    *toEnum;
305                 AILogToGroup    *currentToGroup;
306                 NSString                *serviceClass = [logFromGroup serviceClass];
308                 //Add the 'to' for each grouping on this account
309                 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
310                 while ((currentToGroup = [toEnum nextObject])) {
311                         NSString        *currentTo;
312                         
313                         if ((currentTo = [currentToGroup to])) {
314                                 AIListObject *listObject = ((serviceClass && currentTo) ?
315                                                                                         [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
316                                                                                                                                                                                                                                                                                          UID:currentTo]] :
317                                                                                         nil);
318                                 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
319                                         AIListContact *parentContact = [(AIListContact *)listObject parentContact];
320                                         if (![toArray containsObjectIdenticalTo:parentContact]) {
321                                                 [toArray addObject:parentContact];
322                                         }
323                                         
324                                 } else {
325                                         if (![toArray containsObject:currentToGroup]) {
326                                                 [toArray addObject:currentToGroup];
327                                         }
328                                 }
329                         }
330                 }               
331         }
332         
333         [toArray sortUsingFunction:toArraySort context:NULL];
334         [outlineView_contacts reloadData];
336         if (!isOpeningForContact) {
337                 //If we're opening for a contact, the outline view selection will be changed in a moment anyways
338                 [self outlineViewSelectionDidChange:nil];
339         }
343 - (NSString *)adiumFrameAutosaveName
345         return KEY_LOG_VIEWER_WINDOW_FRAME;
348 //Setup the window before it is displayed
349 - (void)windowDidLoad
351         suppressSearchRequests = YES;
353         [super windowDidLoad];
355         [plugin pauseIndexing];
357         [[self window] setTitle:AILocalizedString(@"Chat Transcript Viewer",nil)];
358     [textField_progress setStringValue:@""];
360         //Autosave doesn't do anything yet
361         [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
362         [shelf_splitView setFrame:[[[self window] contentView] frame]];
364         // Pull our main article/display split view out of the nib and position it in the shelf view
365         [containingView_results retain];
366         [containingView_results removeFromSuperview];
367         [shelf_splitView setContentView:containingView_results];
368         [containingView_results release];
369         
370         // Pull our source view out of the nib and position it in the shelf view
371         [containingView_contactsSourceList retain];
372         [containingView_contactsSourceList removeFromSuperview];
373         [shelf_splitView setShelfView:containingView_contactsSourceList];
374         [containingView_contactsSourceList release];
376     //Set emoticon filtering
377     showEmoticons = [[[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
378                                                               group:PREF_GROUP_LOGGING] boolValue];
379     [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
380     [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
382         //Toolbar
383         [self installToolbar];  
385         //This is the Mail.app source list background color... which differs from the iTunes one.
386         [outlineView_contacts setBackgroundColor:[NSColor colorWithCalibratedRed:.9059
387                                                                                                                                            green:.9294
388                                                                                                                                                 blue:.9647
389                                                                                                                                            alpha:1.0]];
391         AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
392         [[[outlineView_contacts tableColumns] objectAtIndex:0] setDataCell:dataCell];
393         [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
394         [dataCell release];
396         [outlineView_contacts setDrawsGradientSelection:YES];
398         //Localize tableView_results column headers
399         [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
400         [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
401         [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
402         [self tableViewColumnDidResize:nil];
404         [tableView_results sizeLastColumnToFit];
406     //Prepare the search controls
407     [self buildSearchMenu];
408     if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
409                 [textView_content setUsesFindPanel:YES];
410     }
412     //Sort by preference, defaulting to sorting by date
413         NSString        *selectedTableColumnPref;
414         if ((selectedTableColumnPref = [[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
415                                                                                                                                                    group:PREF_GROUP_LOGGING])) {
416                 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
417         }
418         if (!selectedColumn) {
419                 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
420         }
421         [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
423     //Prepare indexing and filter searching
424         [plugin prepareLogContentSearching];
425     [self initLogFiltering];
427     //Begin our initial search
428         [self setSearchMode:LOG_SEARCH_TO];
430     [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
431         suppressSearchRequests = NO;
433         if (!isOpeningForContact) {
434                 //If we're opening for a contact, we'll select it and then begin searching
435                 [self startSearchingClearingCurrentResults:YES];
436         }
438         [plugin resumeIndexing];
441 -(void)rebuildIndices
443     //Rebuild the 'global' log indexes
444     [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
445     [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
446     [toServiceArray removeAllObjects];
447     [fromArray removeAllObjects];
448     [fromServiceArray removeAllObjects];
449     
450     [self initLogFiltering];
451     
452     [tableView_results reloadData];
453     [self selectDisplayedLog];
456 //Called as the window closes
457 - (void)windowWillClose:(id)sender
459         [super windowWillClose:sender];
461         //Set preference for emoticon filtering
462         [[adium preferenceController] setPreference:[NSNumber numberWithBool:showEmoticons]
463                                                                                  forKey:KEY_LOG_VIEWER_EMOTICONS
464                                                                                   group:PREF_GROUP_LOGGING];
465         
466         //Set preference for selected column
467         [[adium preferenceController] setPreference:[selectedColumn identifier]
468                                                                                  forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
469                                                                                   group:PREF_GROUP_LOGGING];
471     /* Disable the search field.  If we don't disable the search field, it will often try to call its target action
472      * after the window has closed (and we are gone).  I'm not sure why this happens, but disabling the field
473      * before we close the window down seems to prevent the crash.
474          */
475     [searchField_logs setEnabled:NO];
476         
477         /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
478          * it was gone, like responding to a logIndexUpdated message
479          */
480         windowIsClosing = YES;
482     //Abort any in-progress searching and indexing, and wait for their completion
483     [self stopSearching];
484     [plugin cleanUpLogContentSearching];
486         //Reset our column widths if needed
487         [activeSearchString release]; activeSearchString = nil;
488         [self updateRankColumnVisibility];
489         
490         [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
491         [toolbarItems autorelease]; toolbarItems = nil;
494 //Display --------------------------------------------------------------------------------------------------------------
495 #pragma mark Display
496 //Update log viewer progress string to reflect current status
497 - (void)updateProgressDisplay
499     NSMutableString     *progress = nil;
500     int                                 indexNumber, indexTotal;
501     BOOL                                indexing;
503     //We always convey the number of logs being displayed
504     [resultsLock lock];
505         unsigned count = [currentSearchResults count];
506     if (activeSearchString && [activeSearchString length]) {
507                 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
508                                                                                                                                                            AILocalizedString(@"%i matching transcripts",nil) :
509                                                                                                                                                            AILocalizedString(@"1 matching transcript",nil)),count]];
510     } else {
511                 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
512                                                                                                                                                            AILocalizedString(@"%i transcripts",nil) :
513                                                                                                                                                            AILocalizedString(@"1 transcript",nil)),count]];
514                 
515                 //We are searching, but there is no active search  string. This indicates we're still opening logs.
516                 if (searching) {
517                         progress = [[AILocalizedString(@"Opening transcripts",nil) mutableCopy] autorelease];                   
518                 }
519     }
520     [resultsLock unlock];
522         indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
524     //Append search progress
525     if (activeSearchString && [activeSearchString length]) {
526                 if (progress) {
527                         [progress appendString:@" - "];
528                 } else {
529                         progress = [NSMutableString string];
530                 }
532                 if (searching || indexing) {
533                         [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
534                 } else {
535                         [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];                     
536                 }
537         }
539     //Append indexing progress
540     if (indexing) {
541                 if (progress) {
542                         [progress appendString:@" - "];
543                 } else {
544                         progress = [NSMutableString string];
545                 }
546                 
547                 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %i of %i transcripts",nil), indexNumber, indexTotal]];
548     }
549         
550         if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
551                 [progress appendString:[NSString ellipsis]];    
552         }
554     //Enable/disable the searching animation
555     if (searching || indexing) {
556                 [progressIndicator startAnimation:nil];
557     } else {
558                 [progressIndicator stopAnimation:nil];
559     }
560     
561     [textField_progress setStringValue:(progress ? progress : @"")];
564 //The plugin is informing us that the log indexing changed
565 - (void)logIndexingProgressUpdate
567         //Don't do anything if the window is already closing
568         if (!windowIsClosing) {
569                 [self updateProgressDisplay];
570                 
571                 //If we are searching by content, we should re-search without clearing our current results so the
572                 //the newly-indexed logs can be added without blanking the current table contents.
573                 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
574                         if (searching) {
575                                 //We're already searching; reattempt when done
576                                 searchIDToReattemptWhenComplete = activeSearchID;
577                         } else {
578                                 //We're not searching - restart the search immediately every 10 updates to utilize the newly indexed logs
579                                 indexingUpdatesReceivedWhileSearching++;
580                                 if ((indexingUpdatesReceivedWhileSearching % 10) == 0)
581                                         [self startSearchingClearingCurrentResults:NO];
582                         }
583                 }
584         }
587 //Refresh the results table
588 - (void)refreshResults
590         [self updateProgressDisplay];
592         [self refreshResultsSearchIsComplete:NO];
595 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
597     [resultsLock lock];
598     int count = [currentSearchResults count];
599     [resultsLock unlock];
600         AILog(@"refreshResultsSearchIsComplete: %i (count is %i)",searchIsComplete,count);
601     if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
602                 //Sort the logs correctly which will also reload the table
603                 [self resortLogs];
604                 
605                 if (searchIsComplete && automaticSearch) {
606                         //If search is complete, select the first log if requested and possible
607                         [self selectFirstLog];
608                         
609                 } else {
610                         BOOL oldAutomaticSearch = automaticSearch;
612                         //We don't want the above re-selection to change our automaticSearch tracking
613                         //(The only reason automaticSearch should change is in response to user action)
614                         automaticSearch = oldAutomaticSearch;
615                 }
616     }
617         
618         if (searchIsComplete &&
619                 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
620                 searchIDToReattemptWhenComplete = -1;
621                 [self startSearchingClearingCurrentResults:NO];
622         }
623         
624         if(deleteOccurred)
625                 [self selectCachedIndex];
627     //Update status
628     [self updateProgressDisplay];
631 - (void)searchComplete
633         [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
634         [self refreshResultsSearchIsComplete:YES];
637 //Displays the contents of the specified log in our window
638 - (void)displayLogs:(NSArray *)logArray;
639 {       
640     NSMutableAttributedString   *displayText = nil;
641         NSAttributedString                      *finalDisplayText = nil;
642         NSRange                                         scrollRange = NSMakeRange(0,0);
643         BOOL                                            appendedFirstLog = NO;
645     if (![logArray isEqualToArray:displayedLogArray]) {
646                 [displayedLogArray release];
647                 displayedLogArray = [logArray copy];
648         }
650         if ([logArray count] > 1) {
651                 displayText = [[NSMutableAttributedString alloc] init];
652         }
654         NSEnumerator *enumerator = [logArray objectEnumerator];
655         AIChatLog        *theLog;
656         NSString         *logBasePath = [AILoggerPlugin logBasePath];
657         AILog(@"Displaying %@",logArray);
658         while ((theLog = [enumerator nextObject])) {
659                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
660                 
661                 if (displayText) {
662                         if (!horizontalRule) {
663                                 #define HORIZONTAL_BAR                  0x2013
664                                 #define HORIZONTAL_RULE_LENGTH  18
665                                 
666                                 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
667                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
668                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
669                                         HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
670                                 };
671                                 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
672                         }       
673                         
674                         [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
675                                 (appendedFirstLog ? @"\n" : @""),
676                                 horizontalRule,
677                                 [headerDateFormatter stringFromDate:[theLog date]],
678                                 [theLog to],
679                                 horizontalRule]
680                                            withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
681                 }
682                 
683                 if ([[theLog path] hasSuffix:@".AdiumHTMLLog"] || [[theLog path] hasSuffix:@".html"] || [[theLog path] hasSuffix:@".html.bak"]) {
684                         //HTML log
685                         NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
686                         
687                         if (displayText) {
688                                 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
689                         } else {
690                                 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
691                         }
693                 } else if ([[theLog path] hasSuffix:@".chatlog"]){
694                         //XML log
695                         NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog path]];
696                         
697                         //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.
698                         enum {
699                                 failedUtf8BomLength = 6
700                         };
701                         NSData *data = [NSData dataWithContentsOfMappedFile:logFullPath];
702                         const unsigned char *ptr = [data bytes];
703                         unsigned len = [data length];
704                         if ((len >= failedUtf8BomLength)
705                                 &&  (ptr[0] == 0xC3)
706                                 &&  (ptr[1] == 0x94)
707                                 &&  (ptr[2] == 0xC2)
708                                 &&  (ptr[3] == 0xAA)
709                                 &&  (ptr[4] == 0xC3)
710                                 &&  (ptr[5] == 0xB8)
711                                 ) {
712                                 //Yup. Back up the old file, then strip it off.
713                                 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);
714                                 NSString *backupPath = [logFullPath stringByAppendingPathExtension:@"bak"];
715                                 if(![[NSFileManager defaultManager] movePath:logFullPath toPath:backupPath handler:nil])
716                                         NSLog(@"Could not back up file; recovery failed. This transcript will probably appear blank in the transcript viewer.");
717                                 else {
718                                         NSRange range = { failedUtf8BomLength, len - failedUtf8BomLength };
719                                         NSData *theRestOfIt = [data subdataWithRange:range];
720                                         if([theRestOfIt writeToFile:logFullPath atomically:YES])
721                                                 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]);
722                                         else
723                                                 NSLog(@"Could not write fix!");
724                                 }
725                         }
726                         NSString *logFileText = [GBChatlogHTMLConverter readFile:logFullPath];
728                         if (logFileText) {
729                                 if (displayText)
730                                         [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
731                                 else
732                                         displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
733                         }
735                 } else {
736                         //Fallback: Plain text log
737                         NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
738                         if (logFileText) {
739                                 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
740                                 
741                                 if (displayText) {
742                                         [displayText appendAttributedString:[[[NSAttributedString alloc] initWithString:logFileText 
743                                                                                                                                                                                  attributes:[textAttributes dictionary]] autorelease]];
744                                 } else {
745                                         displayText = [[NSMutableAttributedString alloc] initWithString:logFileText attributes:[textAttributes dictionary]];
746                                 }
747                         }
748                 }
749                 
750                 appendedFirstLog = YES;
751                 
752                 [pool release];
753         }
754         
755         if (displayText && [displayText length]) {
756                 //Add pretty formatting to links
757                 [displayText addFormattingForLinks];
759                 //If we are searching by content, highlight the search results
760                 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
761                         NSEnumerator                            *enumerator;
762                         NSString                                        *searchWord;
763                         NSMutableArray                          *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
764                         NSScanner                                       *scanner = [NSScanner scannerWithString:activeSearchString];
765                         
766                         //Look for an initial quote
767                         while (![scanner isAtEnd]) {
768                                 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
769                                 
770                                 [scanner scanUpToString:@"\"" intoString:NULL];
771                                 
772                                 //Scan past the quote
773                                 if (![scanner scanString:@"\"" intoString:NULL]) continue;
774                                 
775                                 NSString *quotedString;
776                                 //And a closing one
777                                 if (![scanner isAtEnd] &&
778                                         [scanner scanUpToString:@"\"" intoString:&quotedString]) {
779                                         //Scan past the quote
780                                         [scanner scanString:@"\"" intoString:NULL];
781                                         /* If a string within quotes is found, remove the words from the quoted string and add the full string
782                                          * to what we'll be highlighting.
783                                          *
784                                          * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
785                                          * "killer attack ninja kittens" OR ninja
786                                          * wouldn't highlight the word ninja by itself.
787                                          */
788                                         NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
789                                         int quotedWordsCount = [quotedWords count];
790                                         
791                                         for (int i = 0; i < quotedWordsCount; i++) {
792                                                 NSString        *quotedWord = [quotedWords objectAtIndex:i];
793                                                 if (i == 0) {
794                                                         //Originally started with a quote, so put it back on
795                                                         quotedWord = [@"\"" stringByAppendingString:quotedWord];
796                                                 }
797                                                 if (i == quotedWordsCount - 1) {
798                                                         //Originally ended with a quote, so put it back on
799                                                         quotedWord = [quotedWord stringByAppendingString:@"\""];
800                                                 }
801                                                 int searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
802                                                 if (searchWordsIndex != NSNotFound) {
803                                                         [searchWordsArray removeObjectAtIndex:searchWordsIndex];
804                                                 } else {
805                                                         NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
806                                                 }
807                                         }
808                                         
809                                         //Add the full quoted string
810                                         [searchWordsArray addObject:quotedString];
811                                 }
812                                 [pool release];
813                         }
815                         BOOL shouldScrollToWord = NO;
816                         scrollRange = NSMakeRange([displayText length],0);
818                         enumerator = [searchWordsArray objectEnumerator];
819                         while ((searchWord = [enumerator nextObject])) {
820                                 NSRange     occurrence;
821                                 
822                                 //Check against and/or.  We don't just remove it from the array because then we couldn't check case insensitively.
823                                 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
824                                         ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
825                                         [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
826                                         
827                                         //We'll want to scroll to the first occurrance of any matching word or words
828                                         if (occurrence.location < scrollRange.location) {
829                                                 scrollRange = occurrence;
830                                                 shouldScrollToWord = YES;
831                                         }
832                                 }
833                         }
834                         
835                         //If we shouldn't be scrolling to a new range, we want to scroll to the top
836                         if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
837                         
838                         [searchWordsArray release];
839                 }
840                 
841                 //Filter emoticons
842                 if (showEmoticons) {
843                         finalDisplayText = [[adium contentController] filterAttributedString:displayText
844                                                                                                                                  usingFilterType:AIFilterMessageDisplay
845                                                                                                                                            direction:AIFilterOutgoing
846                                                                                                                                                  context:nil];
847                 } else {
848                         finalDisplayText = displayText;
849                 }
850         }
852         if (finalDisplayText) {
853                 [[textView_content textStorage] setAttributedString:finalDisplayText];
855                 //Set this string and scroll to the top/bottom/occurrence
856                 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
857                         [textView_content scrollRangeToVisible:scrollRange];
858                 } else {
859                         [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
860                 }
862         } else {
863                 //No log selected, empty the view
864                 [textView_content setString:@""];
865         }
867         [displayText release];
870 - (void)displayLog:(AIChatLog *)theLog
872         [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
875 //Reselect the displayed log (Or another log if not possible)
876 - (void)selectDisplayedLog
878     int     firstIndex = NSNotFound;
879     
880     /* Is the log we had selected still in the table?
881          * (When performing an automatic search, we ignore the previous selection.  This ensures that we always
882      * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
883          */
884         if (!automaticSearch) {
885                 [resultsLock lock];
886                 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
887                 [resultsLock unlock];
888                 
889                 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
890         }
892         if (firstIndex != NSNotFound) {
893                 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
894     } else {
895         if (useSame == YES && sameSelection > 0) {
896             [tableView_results selectRow:sameSelection byExtendingSelection:NO];
897         } else {
898             [self selectFirstLog];
899         }
900     }
902     useSame = NO;
905 - (void)selectFirstLog
907         AIChatLog   *theLog = nil;
908         
909         //If our selected log is no more, select the first one in the list
910         [resultsLock lock];
911         if ([currentSearchResults count] != 0) {
912                 theLog = [currentSearchResults objectAtIndex:0];
913         }
914         [resultsLock unlock];
915         
916         //Change the table selection to this new log
917         //We need a little trickery here.  When we change the row, the table view will call our tableViewSelectionDidChange: method.
918         //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
919         //search.  As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
920         //that we instantiated this selection change, and not the user.
921         ignoreSelectionChange = YES;
922         [tableView_results selectRow:0 byExtendingSelection:NO];
923         [tableView_results scrollRowToVisible:0];
924         ignoreSelectionChange = NO;
926         [self displayLog:theLog];  //Manually update the displayed log
929 //Highlight the occurences of a search string within a displayed log
930 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
932     int                                 location = 0;
933     NSRange                             searchRange, foundRange;
934     NSString                    *plainBigString = [bigString string];
935         unsigned                        plainBigStringLength = [plainBigString length];
936         NSMutableDictionary *attributeDictionary = nil;
938     outRange->location = NSNotFound;
940     //Search for the little string in the big string
941     while (location != NSNotFound && location < plainBigStringLength) {
942         searchRange = NSMakeRange(location, plainBigStringLength-location);
943         foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
944                 
945                 //Bold and color this match
946         if (foundRange.location != NSNotFound) {
947                         if (outRange->location == NSNotFound) *outRange = foundRange;
949                         if (!attributeDictionary) {
950                                 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
951                                         [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
952                                         [NSColor yellowColor], NSBackgroundColorAttributeName,
953                                         nil];
954                         }
955                         [bigString addAttributes:attributeDictionary
956                                                            range:foundRange];
957         }
959         location = NSMaxRange(foundRange);
960     }
964 //Sorting --------------------------------------------------------------------------------------------------------------
965 #pragma mark Sorting
966 - (void)resortLogs
968         NSString *identifier = [selectedColumn identifier];
970     //Resort the data
971         [resultsLock lock];
972     if ([identifier isEqualToString:@"To"]) {
973                 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
974                 
975     } else if ([identifier isEqualToString:@"From"]) {
976         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
977                 
978     } else if ([identifier isEqualToString:@"Date"]) {
979         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
980                 
981     } else if ([identifier isEqualToString:@"Rank"]) {
982             [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
983         }
984         
985     [resultsLock unlock];
987     //Reload the data
988     [tableView_results reloadData];
990     //Reapply the selection
991     [self selectDisplayedLog];  
994 //Sorts the selected log array and adjusts the selected column
995 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
997     //If there already was a sorted column, remove the indicator image from it.
998     if (selectedColumn && selectedColumn != tableColumn) {
999         [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
1000     }
1001     
1002     //Set the indicator image in the newly selected column
1003     [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
1004                            inTableColumn:tableColumn];
1005     
1006     //Set the highlighted table column.
1007     [tableView_results setHighlightedTableColumn:tableColumn];
1008     [selectedColumn release]; selectedColumn = [tableColumn retain];
1009     sortDirection = direction;
1010         
1011         [self resortLogs];
1014 //Searching ------------------------------------------------------------------------------------------------------------
1015 #pragma mark Searching
1016 //(Jag)Change search string
1017 - (void)controlTextDidChange:(NSNotification *)notification
1019     if (searchMode != LOG_SEARCH_CONTENT) {
1020                 [self updateSearch:nil];
1021     }
1024 //Change search string (Called by searchfield)
1025 - (IBAction)updateSearch:(id)sender
1027     automaticSearch = NO;
1028     [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1029         AILog(@"updateSearch calling startSearching");
1030     [self startSearchingClearingCurrentResults:YES];
1033 //Change search mode (Called by mode menu)
1034 - (IBAction)selectSearchType:(id)sender
1036     automaticSearch = NO;
1038         //First, update the search mode to the newly selected type
1039     [self setSearchMode:[sender tag]]; 
1040         
1041         //Then, ensure we are ready to search using the current string
1042         [self setSearchString:activeSearchString];
1044         //Now we are ready to start searching
1045         AILog(@"selectSearchType calling startSearching");
1046     [self startSearchingClearingCurrentResults:YES];
1049 //Begin a specific search
1050 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1052     automaticSearch = YES;
1053         //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1054     [self setSearchMode:inMode];
1055     [self setSearchString:inString];
1057         AILog(@"setSearchString:mode: calling startSearching");
1058     [self startSearchingClearingCurrentResults:YES];
1061 //Begin the current search
1062 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1064     NSDictionary    *searchDict;
1066         if (suppressSearchRequests) return;
1067         AILog(@"Starting a search for %@",activeSearchString);
1069     //Once all searches have exited, we can start a new one
1070         if (clearCurrentResults) {
1071                 [resultsLock lock];
1072                 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1073                 [self stopSearching];
1075                 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1076                 [resultsLock unlock];
1077         } else {
1078             //Stop any existing searches
1079                 [self stopSearching];   
1080         }
1082         searching = YES;
1083         indexingUpdatesReceivedWhileSearching = 0;
1084     searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1085                 [NSNumber numberWithInt:activeSearchID], @"ID",
1086                 [NSNumber numberWithInt:searchMode], @"Mode",
1087                 activeSearchString, @"String",
1088                 [plugin logContentIndex], @"SearchIndex",
1089                 nil];
1090     [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1091     
1092         //Update the table periodically while the logs load.
1093         [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1094         refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1095                                                                                                                         target:self
1096                                                                                                                   selector:@selector(refreshResults)
1097                                                                                                                   userInfo:nil
1098                                                                                                                    repeats:YES] retain];
1101 //Abort any active searches
1102 - (void)stopSearching
1104         [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
1106     //Increase the active search ID so any existing searches stop, and then
1107     //wait for any active searches to finish and release the lock
1108     activeSearchID++;
1111 //Set the active search mode (Does not invoke a search)
1112 - (void)setSearchMode:(LogSearchMode)inMode
1114         NSTextFieldCell *cell = [searchField_logs cell];
1115         
1116     searchMode = inMode;
1117         
1118         //Clear any filter from the table if it's the current mode, as well
1119         switch (searchMode) {
1120                 case LOG_SEARCH_FROM:
1121                         [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1122                         break;
1124                 case LOG_SEARCH_TO:
1125                         [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1126                         break;
1127                         
1128                 case LOG_SEARCH_DATE:
1129                         [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1130                         break;
1132                 case LOG_SEARCH_CONTENT:
1133                         [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1134                         break;
1135         }
1137         [self updateRankColumnVisibility];
1138     [self buildSearchMenu];
1141 - (void)updateRankColumnVisibility
1143         NSTableColumn   *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1144         
1145         if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1146                 //Add the resultsColumn and resize if it should be shown but is not at present
1147                 if (!resultsColumn) {   
1148                         NSArray                 *tableColumns;
1150                         //Set up the results column
1151                         resultsColumn = [[NSTableColumn alloc] initWithIdentifier:@"Rank"];
1152                         [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1153                         [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1154                         
1155                         //Add it to the table
1156                         [tableView_results addTableColumn:resultsColumn];
1158                         //Make it half again as large as the desired width from the @"Rank" header title
1159                         [resultsColumn sizeToFit];
1160                         [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1161                         
1162                         tableColumns = [tableView_results tableColumns];
1163                         if ([tableColumns indexOfObject:resultsColumn] > 0) {
1164                                 NSTableColumn   *nextDoorNeighbor;
1166                                 //Adjust the column to the results column's left so results is now visible
1167                                 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1168                                 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1169                         }
1170                 }
1171         } else {
1172                 //Remove the resultsColumn and resize if it should not be shown but is at present
1173                 if (resultsColumn) {
1174                         NSArray                 *tableColumns;
1176                         tableColumns = [tableView_results tableColumns];
1177                         if ([tableColumns indexOfObject:resultsColumn] > 0) {
1178                                 NSTableColumn   *nextDoorNeighbor;
1180                                 //Adjust the column to the results column's left to take up the space again
1181                                 tableColumns = [tableView_results tableColumns];
1182                                 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1183                                 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1184                         }
1186                         //Remove it
1187                         [tableView_results removeTableColumn:resultsColumn];
1188                 }
1189         }
1192 //Set the active search string (Does not invoke a search)
1193 - (void)setSearchString:(NSString *)inString
1195     if (![[searchField_logs stringValue] isEqualToString:inString]) {
1196                 [searchField_logs setStringValue:(inString ? inString : @"")];
1197     }
1198         
1199         //Use autorelease so activeSearchString can be passed back to here
1200         if (activeSearchString != inString) {
1201                 [activeSearchString release];
1202                 activeSearchString = [inString retain];
1203         }
1205         [self updateRankColumnVisibility];
1208 //Build the search mode menu
1209 - (void)buildSearchMenu
1211     NSMenu  *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1212     [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1213     [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1214     [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1215     [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1217         [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1220 - (void)_willOpenForContact
1222         isOpeningForContact = YES;
1225 - (void)_didOpenForContact
1227         isOpeningForContact = NO;
1231  * @brief Focus the log viewer on a particular contact
1233  * If the contact is within a metacontact, the metacontact will be focused.
1234  */
1235 - (void)filterForContact:(AIListContact *)inContact
1237         AIListContact *parentContact = [inContact parentContact];
1239         if (!isOpeningForContact) {
1240                 /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1241                 * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1242                 * with regard to inContact since the log viewer opened.
1243                 *
1244                 * If we're opening initially, the list is guaranteed fresh.
1245                 */
1246                 [self rebuildContactsList];
1247         }
1249         //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1250         if (searchMode == LOG_SEARCH_TO) {
1251                 [self setSearchMode:LOG_SEARCH_CONTENT];
1252                 
1253                 //Update our search string to ensure we're configured for content searching
1254                 [self setSearchString:activeSearchString];
1255         }
1257         //Changing the selection will start a new search
1258         [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1259         unsigned int selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1260         if (selectedRow != NSNotFound) {
1261                 [outlineView_contacts scrollRowToVisible:selectedRow];
1262         }
1266  * @brief Returns a menu item for the search mode menu
1267  */
1268 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1270     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title 
1271                                                                                                                                                                  action:@selector(selectSearchType:) 
1272                                                                                                                                                   keyEquivalent:@""];
1273     [menuItem setTag:mode];
1274     [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1275     
1276     return [menuItem autorelease];
1279 #pragma mark Filtering search results
1281 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1283         BOOL matchesDateFilter;
1285         switch (filterDateType) {
1286                 case AIDateTypeAfter:
1287                         matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1288                         break;
1289                 case AIDateTypeBefore:
1290                         matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1291                         break;
1292                 case AIDateTypeExactly:
1293                         matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1294                         break;
1295                 default:
1296                         matchesDateFilter = YES;
1297                         break;
1298         }
1300         return matchesDateFilter;
1304 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1306         CFURLRef        url = SKDocumentCopyURL(inDocument);
1307         CFStringRef logPath = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
1308         NSArray         *pathComponents = [(NSString *)logPath pathComponents];
1309         
1310         CFRelease(url);
1311         CFRelease(logPath);
1312         
1313         return pathComponents;
1317  * @brief Should a search display a document with the given information?
1318  */
1319 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1321         BOOL shouldDisplayDocument = YES;
1323         if ([contactIDsToFilter count]) {
1324                 //Determine the path components if we weren't supplied them
1325                 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1327                 unsigned int numPathComponents = [pathComponents count];
1328                 
1329                 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1330                 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1332                 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1334                 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1335         } 
1336         
1337         if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1338                 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1340                 unsigned int    numPathComponents = [pathComponents count];
1341                 NSString                *toPath = [NSString stringWithFormat:@"%@/%@",
1342                         [pathComponents objectAtIndex:numPathComponents-3],
1343                         [pathComponents objectAtIndex:numPathComponents-2]];
1344                 NSString                *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1345                 AIChatLog               *theLog;
1346                 
1347                 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1348                 
1349                 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1350         }
1352         return shouldDisplayDocument;
1355 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1356 #pragma mark Threaded filter/search methods
1357 //Search the logs, filtering out any matching logs into the currentSearchResults
1358 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1360     NSAutoreleasePool       *pool = [[NSAutoreleasePool alloc] init];
1361     int                     mode = [[searchInfoDict objectForKey:@"Mode"] intValue];
1362     int                     searchID = [[searchInfoDict objectForKey:@"ID"] intValue];
1363     NSString                *searchString = [searchInfoDict objectForKey:@"String"];
1365     if (searchID == activeSearchID) { //If we're still supposed to go
1366                 searching = YES;
1367                 AILog(@"filterLogsWithSearch (search ID %i): %@",searchID,searchInfoDict);
1368                 //Search
1369                 [plugin pauseIndexing];
1370                 if (searchString && [searchString length]) {
1371                         switch (mode) {
1372                                 case LOG_SEARCH_FROM:
1373                                 case LOG_SEARCH_TO:
1374                                 case LOG_SEARCH_DATE:
1375                                         [self _logFilter:searchString
1376                                                         searchID:searchID
1377                                                                 mode:mode];
1378                                         break;
1379                                 case LOG_SEARCH_CONTENT:
1380                                         [self _logContentFilter:searchString
1381                                                                    searchID:searchID
1382                                                           onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1383                                         break;
1384                         }
1385                 } else {
1386                         [self _logFilter:nil
1387                                         searchID:searchID
1388                                                 mode:mode];
1389                 }
1390                 
1391                 //Refresh
1392                 searching = NO;
1393                 [plugin resumeIndexing];
1394                 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1395                 AILog(@"filterLogsWithSearch (search ID %i): finished",searchID);
1396     }
1397         
1398     //Cleanup
1399     [pool release];
1402 //Perform a filter search based on source name, destination name, or date
1403 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode
1405     NSEnumerator        *fromEnumerator, *toEnumerator, *logEnumerator;
1406     AILogToGroup        *toGroup;
1407     AILogFromGroup      *fromGroup;
1408     AIChatLog                   *theLog;
1409     UInt32              lastUpdate = TickCount();
1410     
1411     NSCalendarDate      *searchStringDate = nil;
1412         
1413         if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1414                 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString]  dateWithCalendarFormat:nil timeZone:nil];
1415         }
1416         
1417     //Walk through every 'from' group
1418     fromEnumerator = [logFromGroupDict objectEnumerator];
1419     while ((fromGroup = [fromEnumerator nextObject]) && (searchID == activeSearchID)) {
1420                 
1421                 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1422                 if ((mode != LOG_SEARCH_FROM) ||
1423                         (!searchString) || 
1424                         ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1426                         //Walk through every 'to' group
1427                         toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
1428                         while ((toGroup = [toEnumerator nextObject]) && (searchID == activeSearchID)) {
1429                                 
1430                                 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1431                                  * For all other search modes, we always proceed here so long as either:
1432                                  *      a) We are not filtering for specific contact names or
1433                                  *      b) The contact name matches one of the names in contactIDsToFilter
1434                                  */
1435                                 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1436                                    ((mode != LOG_SEARCH_TO) ||
1437                                    (!searchString) || 
1438                                    ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1439                                         
1440                                         //Walk through every log
1442                                         logEnumerator = [toGroup logEnumerator];
1443                                         while ((theLog = [logEnumerator nextObject]) && (searchID == activeSearchID)) {
1444                                                 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1445                                                  * For all other search modes, we always proceed here
1446                                                  */
1447                                                 if ((mode != LOG_SEARCH_DATE) ||
1448                                                    (!searchString) ||
1449                                                    (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1451                                                         if ([self chatLogMatchesDateFilter:theLog]) {
1452                                                                 //Add the log
1453                                                                 [resultsLock lock];
1454                                                                 [currentSearchResults addObject:theLog];
1455                                                                 [resultsLock unlock];                                                   
1456                                                                 
1457                                                                 //Update our status
1458                                                                 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1459                                                                         [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1460                                                                                                                    withObject:nil
1461                                                                                                                 waitUntilDone:NO];
1462                                                                         lastUpdate = TickCount();
1463                                                                 }
1464                                                         }
1465                                                 }
1466                                         }
1467                                 }
1468                         }           
1469                 }
1470     }
1473 //Search results table view --------------------------------------------------------------------------------------------
1474 #pragma mark Search results table view
1475 //Since this table view's source data will be accessed from within other threads, we need to lock before
1476 //accessing it.  We also must be very sure that an incorrect row request is handled silently, since this
1477 //can occur if the array size is changed during the reload.
1478 - (int)numberOfRowsInTableView:(NSTableView *)tableView
1480     int count;
1481     
1482     [resultsLock lock];
1483     count = [currentSearchResults count];
1484     [resultsLock unlock];
1485     
1486     return count;
1490 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
1492     NSString    *identifier = [tableColumn identifier];
1494         if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1495                 AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
1496                 
1497                 [aCell setPercentage:[theLog rankingPercentage]];
1498         }
1502 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
1504     NSString    *identifier = [tableColumn identifier];
1505     id          value = nil;
1506     
1507     [resultsLock lock];
1508     if (row < 0 || row >= [currentSearchResults count]) {
1509                 if ([identifier isEqualToString:@"Service"]) {
1510                         value = blankImage;
1511                 } else {
1512                         value = @"";
1513                 }
1514                 
1515         } else {
1516                 AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
1518                 if ([identifier isEqualToString:@"To"]) {
1519                         // Get ListObject for to-UID
1520                         AIListObject *listObject = [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:[theLog serviceClass]
1521                                                                                                                                                                                                                                                                                 UID:[theLog to]]];
1522                         if (listObject) {
1523                                 //Use the longDisplayName, following the user's contact list preferences as this is presumably how she wants to view contacts' names.
1524                                 value = [listObject longDisplayName];
1526                         } else {
1527                                 //No username available
1528                                 value = [theLog to];
1529                         }
1530                         
1531                 } else if ([identifier isEqualToString:@"From"]) {
1532                         value = [theLog from];
1533                         
1534                 } else if ([identifier isEqualToString:@"Date"]) {
1535                         value = [theLog date];
1536                         
1537                 } else if ([identifier isEqualToString:@"Service"]) {
1538                         NSString        *serviceClass;
1539                         NSImage         *image;
1540                         
1541                         serviceClass = [theLog serviceClass];
1542                         image = [AIServiceIcons serviceIconForService:[[adium accountController] firstServiceWithServiceID:serviceClass]
1543                                                                                                          type:AIServiceIconSmall
1544                                                                                                 direction:AIIconNormal];
1545                         value = (image ? image : blankImage);
1546                 }
1547     }
1548     [resultsLock unlock];
1549     
1550     return value;
1554 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1556     if (!ignoreSelectionChange) {
1557                 NSArray         *selectedLogs;
1558                 
1559                 //Update the displayed log
1560                 automaticSearch = NO;
1561                 
1562                 [resultsLock lock];
1563                 selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
1564                 [resultsLock unlock];
1565                 
1566                 [self displayLogs:selectedLogs];
1567     }
1570 //Sort the log array & reflect the new column
1571 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1572 {    
1573     [self sortCurrentSearchResultsForTableColumn:tableColumn
1574                                    direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1577 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1579     [self deleteSelection:nil];
1582 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
1584         if (tableView == tableView_results) {
1585                 [tableView setSearchColumnIdentifiers:[NSSet setWithObjects:@"To", @"From", nil]];
1586                 [tableView setSearchWraps:YES];
1588         } else if (tableView == (KFTypeSelectTableView *)outlineView_contacts) {
1589                 [tableView setSearchWraps:YES];
1590         }
1593 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1595         NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1597         if (!aNotification ||
1598                 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1599                 NSDateFormatter *dateFormatter;
1600                 NSCell                  *cell = [dateTableColumn dataCell];
1602                 [cell setObjectValue:[NSDate date]];
1604                 float width = [dateTableColumn width];
1606 #define NUMBER_TIME_STYLES      2
1607 #define NUMBER_DATE_STYLES      4
1608                 NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1609                 NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1610                 float requiredWidth;
1612                 dateFormatter = [cell formatter];
1613                 if (!dateFormatter) {
1614                         dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1615                         [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1616                         [cell setFormatter:dateFormatter];
1617                 }
1618                 
1619                 requiredWidth = width + 1;
1620                 for (int i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1621                         [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1623                         for (int j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1624                                 [dateFormatter setDateStyle:formatterStyles[j]];
1625                                 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1626                                 //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1627                                 requiredWidth += 3;                                     
1628                         }
1629                 }
1630         }
1633 - (IBAction)toggleEmoticonFiltering:(id)sender
1635         showEmoticons = !showEmoticons;
1636         [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1637         [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1639         [self displayLogs:displayedLogArray];
1642 #pragma mark Outline View Data source
1643 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
1645         if (!item) {
1646                 if (index == 0) {
1647                         return allContactsIdentifier;
1649                 } else {
1650                         return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1651                 }
1653         } else {
1654                 if ([item isKindOfClass:[AIMetaContact class]]) {
1655                         return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1656                 }
1657         }
1658         
1659         return nil;
1662 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1664         return (!item || 
1665                         ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1666                         [item isKindOfClass:[NSArray class]]);
1669 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1671         if (!item) {
1672                 return [toArray count] + 1; //+1 for the All item
1674         } else if ([item isKindOfClass:[AIMetaContact class]]) {
1675                 unsigned count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1676                 if (count > 1)
1677                         return count;
1678                 else
1679                         return 0;
1681         } else {
1682                 return 0;
1683         }
1686 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1688         Class itemClass = [item class];
1690         if (itemClass == [AIMetaContact class]) {
1691                 return [(AIMetaContact *)item longDisplayName];
1692                 
1693         } else if (itemClass == [AIListContact class]) {
1694                 if ([(AIListContact *)item parentContact] != item) {
1695                         //This contact is within a metacontact - always show its UID
1696                         return [(AIListContact *)item formattedUID];
1697                 } else {
1698                         return [(AIListContact *)item longDisplayName];
1699                 } 
1700                 
1701         } else if (itemClass == [AILogToGroup class]) {
1702                 return [(AILogToGroup *)item to];
1703                 
1704         } else if (itemClass == [allContactsIdentifier class]) {
1705                 int contactCount = [toArray count];
1706                 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1707                         ((contactCount == 1) ?
1708                          AILocalizedString(@"1 Contact", nil) :
1709                          [NSString stringWithFormat:AILocalizedString(@"%i Contacts", nil), contactCount])]; 
1711         } else if (itemClass == [NSString class]) {
1712                 return item;
1714         } else {
1715                 NSLog(@"%@: no idea",item);
1716                 return nil;
1717         }
1720 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1722         if ([item isKindOfClass:[AIMetaContact class]] &&
1723                 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1724                 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1725                  * If it has multiple contacts, use no icon unless a user icon is present.
1726                  */
1727                 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1728                                                                                                                 size:NSMakeSize(16,16)];
1729                 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1731                 [cell setImage:image];
1733         } else if ([item isKindOfClass:[AIListContact class]]) {
1734                 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1735                                                                                                                 size:NSMakeSize(16,16)];
1736                 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1737                                                                                                                         type:AIServiceIconSmall
1738                                                                                                            direction:AIIconFlipped];
1739                 [cell setImage:image];
1741         } else if ([item isKindOfClass:[AILogToGroup class]]) {
1742                 [cell setImage:[AIServiceIcons serviceIconForServiceID:[(AILogToGroup *)item serviceClass]
1743                                                                                                            type:AIServiceIconSmall
1744                                                                                                   direction:AIIconFlipped]];
1745                 
1746         } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1747                 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1748                         ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1749                         if (!adiumIconHighlighted) {
1750                                 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
1751                                                                                                    forClass:[self class]] retain];
1752                         }
1754                         [cell setImage:adiumIconHighlighted];
1756                 } else {
1757                         if (!adiumIcon) {
1758                                 adiumIcon = [[NSImage imageNamed:@"adium"
1759                                                                                 forClass:[self class]] retain];
1760                         }
1762                         [cell setImage:adiumIcon];
1763                 }
1765         } else if ([item isKindOfClass:[NSString class]]) {
1766                 [cell setImage:nil];
1767                 
1768         } else {
1769                 NSLog(@"%@: no idea",item);
1770                 [cell setImage:nil];
1771         }       
1775  * @brief Is item supposed to have a divider below?
1777  */
1778 - (AIDividerPosition)outlineView:(NSOutlineView*)outlineView dividerPositionForItem:(id)item
1780         if ([item isKindOfClass:[allContactsIdentifier class]]) {
1781                 return AIDividerPositionBelow;
1782         } else {
1783                 return AIDividerPositionNone;
1784         }
1787 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
1789         [self deleteSelection:nil];
1792 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1794         NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
1796         [contactIDsToFilter removeAllObjects];
1798         if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
1799                 id              item;
1800                 NSEnumerator *enumerator;
1802                 enumerator = [selectedItems objectEnumerator];
1803                 while ((item = [enumerator nextObject])) {
1804                         if ([item isKindOfClass:[AIMetaContact class]]) {
1805                                 NSEnumerator    *metaEnumerator;
1806                                 AIListContact   *contact;
1808                                 metaEnumerator = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectEnumerator];
1809                                 while ((contact = [metaEnumerator nextObject])) {
1810                                         [contactIDsToFilter addObject:
1811                                                 [[[NSString stringWithFormat:@"%@.%@",[contact serviceID],[contact UID]] compactedString] safeFilenameString]];
1812                                 }
1813                                 
1814                         } else if ([item isKindOfClass:[AIListContact class]]) {
1815                                 [contactIDsToFilter addObject:
1816                                         [[[NSString stringWithFormat:@"%@.%@",[(AIListContact *)item serviceID],[(AIListContact *)item UID]] compactedString] safeFilenameString]];
1817                                 
1818                         } else if ([item isKindOfClass:[AILogToGroup class]]) {
1819                                 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]]; 
1820                         }
1821                 }
1822         }
1823         
1824         [self startSearchingClearingCurrentResults:YES];
1827 - (NSMenu *)outlineView:(NSOutlineView *)outlineView menuForEvent:(NSEvent *)theEvent;
1829         if (outlineView == outlineView_contacts) {
1830                 int clickedRow = [outlineView_contacts rowAtPoint:[outlineView_contacts convertPoint:[theEvent locationInWindow]
1831                                                                                                                                                                         fromView:nil]];
1832                 id item = [outlineView_contacts itemAtRow:clickedRow];
1834                 //If we have a To group, see if we can make a contact out of it
1835                 if ([item isKindOfClass:[AILogToGroup class]]) {
1836                         if ([(AILogToGroup *)item to] && [(AILogToGroup *)item serviceClass]) {
1837                                 //We need a service with ther right service ID
1838                                 AIService *service = [[adium accountController] firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]];
1839                                 if (service) {
1840                                         NSEnumerator *enumerator = [[[adium accountController] accountsCompatibleWithService:service] objectEnumerator];
1841                                         AIAccount        *account;
1843                                         //Next, we want an online account
1844                                         while ((account = [enumerator nextObject])) {
1845                                                 if ([account online]) break;
1846                                         }
1847                                         
1848                                         if (account) {
1849                                                 //Finally, make a contact
1850                                                 item = [[adium contactController] contactWithService:service
1851                                                                                                                                          account:account
1852                                                                                                                                                  UID:[(AILogToGroup *)item to]];
1853                                         }
1854                                         
1855                                 }
1856                         }
1857                 }
1859                 if ([item isKindOfClass:[AIListContact class]]) {
1860                         NSArray                 *locationsArray = [NSArray arrayWithObjects:
1861                                 [NSNumber numberWithInt:Context_Contact_Message],
1862                                 [NSNumber numberWithInt:Context_Contact_Manage],
1863                                 [NSNumber numberWithInt:Context_Contact_Action],
1864                                 [NSNumber numberWithInt:Context_Contact_ListAction],
1865                                 [NSNumber numberWithInt:Context_Contact_NegativeAction],
1866                                 [NSNumber numberWithInt:Context_Contact_Additions], nil];
1868                         return [[adium menuController] contextualMenuWithLocations:locationsArray
1869                                                                                                                  forListObject:(AIListContact *)item];
1870                 }
1871         }
1872         
1873         return nil;
1876 static int toArraySort(id itemA, id itemB, void *context)
1878         NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
1879         NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
1880         NSComparisonResult result = [nameA caseInsensitiveCompare:nameB];
1881         if (result == NSOrderedSame) result = [nameA compare:nameB];
1883         return result;
1886 - (void)draggedDividerRightBy:(float)deltaX
1887 {       
1888         desiredContactsSourceListDeltaX = deltaX;
1889         [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
1890         desiredContactsSourceListDeltaX = 0;
1894 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
1896         if ((sender == splitView_contacts_results) &&
1897                 desiredContactsSourceListDeltaX != 0) {
1898                 float dividerThickness = [sender dividerThickness];
1900                 NSRect newFrame = [sender frame];               
1901                 NSRect leftFrame = [containingView_contactsSourceList frame]; 
1902                 NSRect rightFrame = [containingView_results frame];
1904                 leftFrame.size.width += desiredContactsSourceListDeltaX; 
1905                 leftFrame.size.height = newFrame.size.height;
1906                 leftFrame.origin = NSMakePoint(0,0);
1908                 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
1909                 rightFrame.size.height = newFrame.size.height;
1910                 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
1912                 [containingView_contactsSourceList setFrame:leftFrame];
1913                 [containingView_contactsSourceList setNeedsDisplay:YES];
1914                 [containingView_results setFrame:rightFrame];
1915                 [containingView_results setNeedsDisplay:YES];
1917         } else {
1918                 //Perform the default implementation
1919                 [sender adjustSubviews];
1920         }
1924 //Window Toolbar -------------------------------------------------------------------------------------------------------
1925 #pragma mark Window Toolbar
1926 - (NSString *)dateItemNibName
1928         return nil;
1931 - (void)installToolbar
1932 {       
1933         [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
1935     NSToolbar           *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
1936     NSToolbarItem       *toolbarItem;
1937         
1938     [toolbar setDelegate:self];
1939     [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
1940     [toolbar setSizeMode:NSToolbarSizeModeRegular];
1941     [toolbar setVisible:YES];
1942     [toolbar setAllowsUserCustomization:YES];
1943     [toolbar setAutosavesConfiguration:YES];
1944     toolbarItems = [[NSMutableDictionary alloc] init];
1946         //Delete Logs
1947         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1948                                         withIdentifier:@"delete"
1949                                                  label:DELETE
1950                                           paletteLabel:DELETE
1951                                                toolTip:AILocalizedString(@"Delete the selection",nil)
1952                                                 target:self
1953                                        settingSelector:@selector(setImage:)
1954                                            itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
1955                                                 action:@selector(deleteSelection:)
1956                                                   menu:nil];
1957         
1958         //Search
1959         [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
1960         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
1961                                                                                                                   label:SEARCH
1962                                                                                                    paletteLabel:SEARCH
1963                                                                                                                 toolTip:AILocalizedString(@"Search or filter logs",nil)
1964                                                                                                                  target:self
1965                                                                                                 settingSelector:@selector(setView:)
1966                                                                                                         itemContent:view_SearchField
1967                                                                                                                  action:@selector(updateSearch:)
1968                                                                                                                    menu:nil];
1969         if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1970                 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
1971         }
1972         [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
1973         [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
1974         [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1976         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
1977                                                                                                                   label:AILocalizedString(@"Date", nil)
1978                                                                                                    paletteLabel:AILocalizedString(@"Date", nil)
1979                                                                                                                 toolTip:AILocalizedString(@"Filter logs by date",nil)
1980                                                                                                                  target:self
1981                                                                                                 settingSelector:@selector(setView:)
1982                                                                                                         itemContent:view_DatePicker
1983                                                                                                                  action:nil
1984                                                                                                                    menu:nil];
1985         if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1986                 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
1987         }
1988         [toolbarItem setMinSize:[view_DatePicker frame].size];
1989         [toolbarItem setMaxSize:[view_DatePicker frame].size];
1990         [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1992         //Toggle Emoticons
1993         [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1994                                                                         withIdentifier:@"toggleemoticons"
1995                                                                                          label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
1996                                                                           paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
1997                                                                                    toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
1998                                                                                         target:self
1999                                                                    settingSelector:@selector(setImage:)
2000                                                                            itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
2001                                                                                         action:@selector(toggleEmoticonFiltering:)
2002                                                                                           menu:nil];
2004         [[self window] setToolbar:toolbar];
2006         [self configureDateFilter];
2009 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
2011     return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
2014 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
2016     return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
2017                 @"delete", @"toggleemoticons", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
2018                 @"search", nil];
2021 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
2023     return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
2024                 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
2025                         NSToolbarSpaceItemIdentifier,
2026                         NSToolbarFlexibleSpaceItemIdentifier,
2027                         NSToolbarCustomizeToolbarItemIdentifier, 
2028                         NSToolbarPrintItemIdentifier, nil]];
2031 - (void)toolbarWillAddItem:(NSNotification *)notification
2033         NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
2034         if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2035                 [item setTarget:self];
2036                 [item setAction:@selector(adiumPrint:)];
2037         }
2040 #pragma mark Date filter
2043  * @brief Returns a menu item for the date type filter menu
2044  */
2045 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
2047     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInt:dateType]] 
2048                                                                                                                                                                  action:@selector(selectDateType:) 
2049                                                                                                                                                   keyEquivalent:@""];
2050     [menuItem setTag:dateType];
2051     
2052     return [menuItem autorelease];
2055 - (NSMenu *)dateTypeMenu
2057         NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2058                 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInt:AIDateTypeAnyDate],
2059                 AILocalizedString(@"Today", nil), [NSNumber numberWithInt:AIDateTypeToday],
2060                 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInt:AIDateTypeSinceYesterday],
2061                 AILocalizedString(@"This Week", nil), [NSNumber numberWithInt:AIDateTypeThisWeek],
2062                 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoWeeks],
2063                 AILocalizedString(@"This Month", nil), [NSNumber numberWithInt:AIDateTypeThisMonth],
2064                 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoMonths],
2065                 nil];
2066         NSMenu  *dateTypeMenu = [[NSMenu alloc] init];
2067         AIDateType dateType;
2068         
2069         [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
2070         [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2072         for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
2073                 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2074         }
2075         
2076         return [dateTypeMenu autorelease];
2079 - (int)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
2081         int todayDayOfWeek = [today dayOfWeek];
2083         //Try to look at the iCal preferences if possible
2084         if (!iCalFirstDayOfWeekDetermined) {
2085                 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
2086                 if (iCalFirstDayOfWeek) {
2087                         //This should return a CFNumberRef... we're using another app's prefs, so make sure.
2088                         if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
2089                                 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek intValue];
2090                         }
2092                         CFRelease(iCalFirstDayOfWeek);
2093                 }
2095                 //Don't check again
2096                 iCalFirstDayOfWeekDetermined = YES;
2097         }
2099         return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2103  * @brief A new date type was selected
2105  * This does not start a search
2106  */
2107 - (void)selectedDateType:(AIDateType)dateType
2109         NSCalendarDate  *today = [NSCalendarDate date];
2110         
2111         [filterDate release]; filterDate = nil;
2112         
2113         switch (dateType) {
2114                 case AIDateTypeAnyDate:
2115                         filterDateType = AIDateTypeAnyDate;
2116                         break;
2117                         
2118                 case AIDateTypeToday:
2119                         filterDateType = AIDateTypeExactly;
2120                         filterDate = [today retain];
2121                         break;
2122                         
2123                 case AIDateTypeSinceYesterday:
2124                         filterDateType = AIDateTypeAfter;
2125                         filterDate = [[today dateByAddingYears:0
2126                                                                                         months:0
2127                                                                                           days:-1
2128                                                                                          hours:-[today hourOfDay]
2129                                                                                    minutes:-[today minuteOfHour]
2130                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2131                         break;
2132                         
2133                 case AIDateTypeThisWeek:
2134                         filterDateType = AIDateTypeAfter;
2135                         filterDate = [[today dateByAddingYears:0
2136                                                                                         months:0
2137                                                                                           days:-[self daysSinceStartOfWeekGivenToday:today]
2138                                                                                          hours:-[today hourOfDay]
2139                                                                                    minutes:-[today minuteOfHour]
2140                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2141                         break;
2142                         
2143                 case AIDateTypeWithinLastTwoWeeks:
2144                         filterDateType = AIDateTypeAfter;
2145                         filterDate = [[today dateByAddingYears:0
2146                                                                                         months:0
2147                                                                                           days:-14
2148                                                                                          hours:-[today hourOfDay]
2149                                                                                    minutes:-[today minuteOfHour]
2150                                                                                    seconds:-([today secondOfMinute] + 1)] retain];
2151                         break;
2152                         
2153                 case AIDateTypeThisMonth:
2154                         filterDateType = AIDateTypeAfter;
2155                         filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2156                                                                                                                         months:0
2157                                                                                                                           days:-[today dayOfMonth]
2158                                                                                                                          hours:0
2159                                                                                                                    minutes:0
2160                                                                                                                    seconds:-1] retain];
2161                         break;
2162                         
2163                 case AIDateTypeWithinLastTwoMonths:
2164                         filterDateType = AIDateTypeAfter;
2165                         filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2166                                                                                                                         months:-1
2167                                                                                                                           days:-[today dayOfMonth]
2168                                                                                                                          hours:0
2169                                                                                                                    minutes:0
2170                                                                                                                    seconds:-1] retain];                 
2171                         break;
2172                         
2173                 default:
2174                         break;
2175         }       
2179  * @brief Select the date type
2180  */
2181 - (void)selectDateType:(id)sender
2183         [self selectedDateType:[sender tag]];
2184         [self startSearchingClearingCurrentResults:YES];
2187 - (void)configureDateFilter
2189         firstDayOfWeek = 0; /* Sunday */
2190         iCalFirstDayOfWeekDetermined = NO;
2192         [popUp_dateFilter setMenu:[self dateTypeMenu]];
2193         int index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2194         if(index != NSNotFound)
2195                 [popUp_dateFilter selectItemAtIndex:index];
2196         [self selectedDateType:AIDateTypeAnyDate];
2199 #pragma mark Open Log
2201 - (void)openLogAtPath:(NSString *)inPath
2203         AIChatLog   *chatLog = nil;
2204         NSString        *basePath = [AILoggerPlugin logBasePath];
2206         //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2207         NSArray         *pathComponents = [inPath pathComponents];
2208         int                     lastIndex = [pathComponents count];
2209         NSString        *logName = [pathComponents objectAtIndex:--lastIndex];
2210         NSString        *contactName = [pathComponents objectAtIndex:--lastIndex];
2211         NSString        *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];    
2212         NSString                *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2214         NSString        *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2215         //Filter for logs from the contact associated with the log we're loading
2216         [self filterForContact:[[adium contactController] contactWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
2217                                                                                                                                  account:nil
2218                                                                                                                                          UID:contactName]];
2219         
2220         NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2221         NSString *canonicalInPath = [inPath stringByStandardizingPath];
2223         if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2224                 AILogToGroup    *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2225                 
2226                 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2227                 
2228         } else {
2229                 /* Different Adium user... this sucks. We're given a path like this:
2230                  *      /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2231                  * and we want to make it relative to our current user's logs folder, which might be
2232                  *  /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2233                  *
2234                  * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2235                  */
2236                 NSString        *fakeRelativePath = @"";
2237                 
2238                 //Use .. to get back to the root from the base path
2239                 int componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2240                 for (int i = 0; i < componentsOfBasePath; i++) {
2241                         fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2242                 }
2243                 
2244                 //Now add the path from the root to the actual log
2245                 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2246                 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2247                                                                                           from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2248                                                                                                 to:contactName
2249                                                                           serviceClass:serviceID] autorelease];
2250         }
2252         //Now display the requested log
2253         if (chatLog) {
2254                 [self displayLog:chatLog];
2255         }
2258 #pragma mark Printing
2260 - (void)adiumPrint:(id)sender
2262         NSTextView                      *printView;
2263     NSPrintOperation    *printOperation;
2264     NSPrintInfo                 *printInfo = [NSPrintInfo sharedPrintInfo];
2266     [printInfo setHorizontalPagination:NSFitPagination];
2267     [printInfo setHorizontallyCentered:NO];
2268     [printInfo setVerticallyCentered:NO];
2269     
2270         printView = [[NSTextView alloc] initWithFrame:[[NSPrintInfo sharedPrintInfo] imageablePageBounds]];
2271     [printView setVerticallyResizable:YES];
2272     [printView setHorizontallyResizable:NO];
2273         
2274     [[printView textStorage] setAttributedString:[textView_content textStorage]];
2275         
2276     printOperation = [NSPrintOperation printOperationWithView:printView printInfo:printInfo];
2277     [printOperation runOperationModalForWindow:[self window] delegate:nil
2278                                                                 didRunSelector:NULL contextInfo:NULL];
2279         [printView release];
2282 - (BOOL)validatePrintMenuItem:(NSMenuItem *)menuItem
2284         return ([displayedLogArray count] > 0);
2287 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2289         if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2290                 return [self validatePrintMenuItem:nil];
2292         } else {
2293                 return YES;
2294         }
2297 - (void)selectCachedIndex
2299         int numberOfRows = [tableView_results numberOfRows];
2300         
2301         if (cachedSelectionIndex <  numberOfRows) {
2302                 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2303                                            byExtendingSelection:NO];
2304         } else {
2305                 if (numberOfRows)
2306                         [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2307                                                    byExtendingSelection:NO];                    
2308         }
2310         if (numberOfRows) {
2311                 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2312         }
2314         deleteOccurred = NO;
2317 #pragma mark Deletion
2320  * @brief Get an NSAlert to request deletion of multiple logs
2321  */
2322 - (NSAlert *)alertForDeletionOfLogCount:(int)logCount
2324         NSAlert *alert = [[NSAlert alloc] init];
2325         [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2326         [alert setInformativeText:[NSString stringWithFormat:
2327                 AILocalizedString(@"Are you sure you want to send %i logs to the Trash?",nil), logCount]];
2328         [alert addButtonWithTitle:DELETE]; 
2329         [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2330         
2331         return [alert autorelease];
2335  * @brief Undo the deletion of one or more AIChatLogs
2337  * The logs will be marked for readdition to the index
2338  */
2339 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2341         NSEnumerator    *enumerator;
2342         AIChatLog               *aLog;
2343         NSFileManager   *fileManager = [NSFileManager defaultManager];
2344         NSString                *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2346         enumerator = [deletedLogs objectEnumerator];
2347         while ((aLog = [enumerator nextObject])) {
2348                 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2349                 
2350                 [fileManager createDirectoriesForPath:[logPath stringByDeletingLastPathComponent]];
2351                 
2352                 [fileManager movePath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2353                                            toPath:logPath 
2354                                           handler:NULL];
2355                 
2356                 [plugin markLogDirtyAtPath:logPath];
2357         }
2358         
2359         [self rebuildIndices];
2362 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode  contextInfo:(void *)contextInfo;
2364         NSArray *selectedLogs = (NSArray *)contextInfo;
2365         if (returnCode == NSAlertFirstButtonReturn) {
2366                 [resultsLock lock];
2367                 
2368                 AIChatLog               *aLog;
2369                 NSEnumerator    *enumerator;
2370                 NSMutableSet    *logPaths = [NSMutableSet set];
2371                 
2372                 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2373                 
2374                 enumerator = [selectedLogs objectEnumerator];
2375                 while ((aLog = [enumerator nextObject])) {
2376                         NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2377                         
2378                         [[adium notificationCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2379                         AILogToGroup    *logToGroup = [logToGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@/%@",[aLog serviceClass],[aLog from],[aLog to]]];
2380                         BOOL success = [logToGroup trashLog:aLog];
2381                         AILog(@"Trashing %@: %i",[aLog path], success);
2382                         //Clear the to group out if it no longer has anything of interest
2383                         if ([logToGroup logCount] == 0) {
2384                                 AILogFromGroup  *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[aLog serviceClass],[aLog from]]];
2385                                 [logFromGroup removeToGroup:logToGroup];
2386                         }
2387                         
2388                         [logPaths addObject:logPath];
2389                         [currentSearchResults removeObjectIdenticalTo:aLog];
2390                 }
2391                 
2392                 [plugin removePathsFromIndex:logPaths];
2393                 
2394                 [undoManager registerUndoWithTarget:self
2395                                                                    selector:@selector(restoreDeletedLogs:)
2396                                                                          object:selectedLogs];
2397                 [undoManager setActionName:DELETE];
2398                 
2399                 [resultsLock unlock];
2400                 [tableView_results reloadData];
2401                 
2402                 deleteOccurred = YES;
2403                 
2404                 [self rebuildContactsList];
2405                 [self updateProgressDisplay];
2406         }
2407         [selectedLogs release];
2411  * @brief Delete logs
2413  * If two or more logs are passed, confirmation will be requested.
2414  * This operation registers with the window controller's undo manager.
2416  * @param selectedLogs An NSArray of logs to delete
2417  */
2418 - (void)deleteLogs:(NSArray *)selectedLogs
2419 {       
2420         if ([selectedLogs count] > 1) {
2421                 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2422                 [alert beginSheetModalForWindow:[self window]
2423                                                   modalDelegate:self
2424                                                  didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2425                                                         contextInfo:[selectedLogs retain]];
2426         } else {
2427                 [self deleteLogsAlertDidEnd:nil
2428                                                  returnCode:NSAlertFirstButtonReturn
2429                                                 contextInfo:[selectedLogs retain]];
2430         }
2434  * @brief Returns a set of all selected to groups on all accounts
2436  * @param totalLogCount If non-NULL, will be set to the total number of logs on return
2437  */
2438 - (NSArray *)allSelectedToGroups:(int *)totalLogCount
2440     NSEnumerator        *fromEnumerator;
2441     AILogFromGroup      *fromGroup;
2442         NSMutableArray          *allToGroups = [NSMutableArray array];
2444         if (totalLogCount) *totalLogCount = 0;
2446     //Walk through every 'from' group
2447     fromEnumerator = [logFromGroupDict objectEnumerator];
2448     while ((fromGroup = [fromEnumerator nextObject])) {
2449                 NSEnumerator        *toEnumerator;
2450                 AILogToGroup        *toGroup;
2452                 //Walk through every 'to' group
2453                 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2454                 while ((toGroup = [toEnumerator nextObject])) {
2455                         if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2456                                 if (totalLogCount) {
2457                                         *totalLogCount += [toGroup logCount];
2458                                 }
2459                                 
2460                                 [allToGroups addObject:toGroup];
2461                         }
2462                 }
2463         }
2465         return allToGroups;
2469  * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2471  * The logs will be marked for readdition to the index
2472  */
2473 - (void)restoreDeletedToGroups:(NSArray *)toGroups
2475         NSEnumerator    *enumerator;
2476         AILogToGroup    *toGroup;
2477         NSFileManager   *fileManager = [NSFileManager defaultManager];
2478         NSString                *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2479         NSString                *logBasePath = [AILoggerPlugin logBasePath];
2481         enumerator = [toGroups objectEnumerator];
2482         while ((toGroup = [enumerator nextObject])) {
2483                 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup path]];
2485                 [fileManager createDirectoriesForPath:[toGroupPath stringByDeletingLastPathComponent]];
2486                 if ([fileManager fileExistsAtPath:toGroupPath]) {
2487                         AILog(@"Removing path %@ to make way for %@",
2488                                   toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2489                         [fileManager removeFileAtPath:toGroupPath
2490                                                                   handler:NULL];
2491                 }
2492                 [fileManager movePath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2493                                            toPath:toGroupPath
2494                                           handler:NULL];
2495                 
2496                 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2497                 AIChatLog        *aLog;
2498         
2499                 while ((aLog = [logEnumerator nextObject])) {
2500                         [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog path]]];
2501                 }
2502         }
2503         
2504         [self rebuildIndices];  
2507 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2509         NSArray *allSelectedToGroups = (NSArray *)contextInfo;
2510         if (returnCode == NSAlertFirstButtonReturn) {
2511                 AILogToGroup    *logToGroup;
2512                 NSEnumerator    *enumerator;
2513                 NSMutableSet    *logPaths = [NSMutableSet set];
2514                 
2515                 enumerator = [allSelectedToGroups objectEnumerator];
2516                 while ((logToGroup = [enumerator nextObject])) {
2517                         NSEnumerator *logEnumerator;
2518                         AIChatLog        *aLog;
2519                         
2520                         logEnumerator = [logToGroup logEnumerator];
2521                         while ((aLog = [logEnumerator nextObject])) {
2522                                 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2523                                 [logPaths addObject:logPath];
2524                         }
2525                         
2526                         AILogFromGroup  *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2527                         [logFromGroup removeToGroup:logToGroup];
2528                 }
2529                 
2530                 [plugin removePathsFromIndex:logPaths];
2531                 
2532                 [undoManager registerUndoWithTarget:self
2533                                                                    selector:@selector(restoreDeletedToGroups:)
2534                                                                          object:allSelectedToGroups];
2535                 [undoManager setActionName:DELETE];
2536                 
2537                 [self rebuildIndices];
2538                 [self updateProgressDisplay];
2539         }
2540         
2541         [allSelectedToGroups release];
2545  * @brief Delete entirely the logs of all contacts selected in the source list
2547  * Confirmation by the user will be required.
2549  * Note: A single item in the source list may have multiple associated AILogToGroups.
2550  */
2551 - (void)deleteSelectedContactsFromSourceList
2553         int totalLogCount;
2554         NSArray *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2556         if (totalLogCount > 1) {
2557                 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2558                 [alert beginSheetModalForWindow:[self window]
2559                                                   modalDelegate:self
2560                                                  didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2561                                                         contextInfo:[allSelectedToGroups retain]];
2562         } else {
2563                 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2564                                                                                                    returnCode:NSAlertFirstButtonReturn
2565                                                                                                   contextInfo:[allSelectedToGroups retain]];
2566         }
2570  * @brief Delete the current selection
2572  * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2573  * If anything else is selected, the currently selected search result logs will be trashed.
2574  */
2575 - (void)deleteSelection:(id)sender
2577         if ([[self window] firstResponder] == outlineView_contacts) {
2578                 [self deleteSelectedContactsFromSourceList];
2579                 
2580         } else {
2581                 [resultsLock lock];
2582                 NSArray *selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
2583                 [resultsLock unlock];
2584                 
2585                 [self deleteLogs:selectedLogs];
2586         }
2589 #pragma mark Undo
2591  * @brief Supply our undo manager when we are within the responder chain
2592  */
2593 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
2595         return undoManager;
2598 @end