Source/AILogViewerWindowController.m
author Zachary West <zacw@adium.im>
Thu Nov 26 00:22:17 2009 -0500 (2009-11-26)
changeset 2825 21c1813a173a
parent 2664 bc1492a4a7c7
child 3350 5823cc057254
permissions -rw-r--r--
When opening for a contact, don't silently switch the search type to "to". Fixes #12001.

This also moves the automatic selection below the "we're now updating for search" line, so that highlighting is applied to the log (since when it's asked to display, there's no search term for it).
     1 //
     2 //  AILogViewerWindowController.m
     3 //  Adium
     4 //
     5 //  Created by Evan Schoenberg on 3/24/06.
     6 //
     7 
     8 #import "AILogViewerWindowController.h"
     9 #import "AIChatLog.h"
    10 #import "AILogFromGroup.h"
    11 #import "AILogToGroup.h"
    12 #import "AILoggerPlugin.h"
    13 #import "ESRankingCell.h" 
    14 #import "AIXMLChatlogConverter.h"
    15 #import "AILogDateFormatter.h"
    16 
    17 #import <Adium/AIAccountControllerProtocol.h>
    18 #import <Adium/AIChatControllerProtocol.h>
    19 #import <Adium/AIContactControllerProtocol.h>
    20 #import <Adium/AIContentControllerProtocol.h>
    21 #import <Adium/AIMenuControllerProtocol.h>
    22 #import <Adium/AIHTMLDecoder.h>
    23 #import <Adium/AIInterfaceControllerProtocol.h>
    24 #import <Adium/AIListContact.h>
    25 #import <Adium/AIMetaContact.h>
    26 #import <Adium/AIService.h>
    27 #import <Adium/AIServiceIcons.h>
    28 #import <Adium/AIUserIcons.h>
    29 #import <Adium/KNShelfSplitView.h>
    30 #import <AIUtilities/AIArrayAdditions.h>
    31 #import <AIUtilities/AIAttributedStringAdditions.h>
    32 #import <AIUtilities/AIDateFormatterAdditions.h>
    33 #import <AIUtilities/AIFileManagerAdditions.h>
    34 #import <AIUtilities/AIImageAdditions.h>
    35 #import <AIUtilities/AIImageTextCell.h>
    36 #import <AIUtilities/AIOutlineViewAdditions.h>
    37 #import <AIUtilities/AISplitView.h>
    38 #import <AIUtilities/AIStringAdditions.h>
    39 #import <AIUtilities/AITableViewAdditions.h>
    40 #import <AIUtilities/AITextAttributes.h>
    41 #import <AIUtilities/AIToolbarUtilities.h>
    42 #import <AIUtilities/AIApplicationAdditions.h>
    43 #import <AIUtilities/AIDividedAlternatingRowOutlineView.h>
    44 
    45 #define KEY_LOG_VIEWER_WINDOW_FRAME		@"Log Viewer Frame"
    46 #define KEY_LOG_VIEWER_GROUP_STATE		@"Log Viewer Group State"	//Expand/Collapse state of groups
    47 #define TOOLBAR_LOG_VIEWER				@"Log Viewer Toolbar"
    48 
    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
    51 
    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)
    60 
    61 #define HIDE_EMOTICONS					AILocalizedString(@"Hide Emoticons",nil)
    62 #define SHOW_EMOTICONS					AILocalizedString(@"Show Emoticons",nil)
    63 #define HIDE_TIMESTAMPS					AILocalizedString(@"Hide Timestamps",nil)
    64 #define SHOW_TIMESTAMPS					AILocalizedString(@"Show Timestamps",nil)
    65 
    66 #define IMAGE_EMOTICONS_OFF				@"emoticon32"
    67 #define IMAGE_EMOTICONS_ON				@"emoticon32_transparent"
    68 #define IMAGE_TIMESTAMPS_OFF			@"timestamp32"
    69 #define IMAGE_TIMESTAMPS_ON				@"timestamp32_transparent"
    70 
    71 
    72 #define	REFRESH_RESULTS_INTERVAL		1.0 //Interval between results refreshes while searching
    73 
    74 @interface AILogViewerWindowController ()
    75 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
    76 - (void)initLogFiltering;
    77 - (void)displayLog:(AIChatLog *)log;
    78 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
    79 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
    80 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
    81 - (void)buildSearchMenu;
    82 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
    83 - (void)_logFilter:(NSString *)searchString searchID:(NSInteger)searchID mode:(LogSearchMode)mode;
    84 - (void)installToolbar;
    85 - (void)updateRankColumnVisibility;
    86 - (void)openLogAtPath:(NSString *)inPath;
    87 - (void)rebuildContactsList;
    88 - (void)filterForContact:(AIListContact *)inContact;
    89 - (void)filterForChatName:(NSString *)chatName withAccount:(AIAccount *)account;
    90 - (void)selectCachedIndex;
    91 - (void)tableViewSelectionDidChangeDelayed;
    92 
    93 - (NSAlert *)alertForDeletionOfLogCount:(NSUInteger)logCount;
    94 
    95 - (void)_willOpenForContact;
    96 - (void)_didOpenForContact;
    97 
    98 - (void)deleteSelection:(id)sender;
    99 @end
   100 
   101 @implementation AILogViewerWindowController
   102 
   103 static AILogViewerWindowController	*sharedLogViewerInstance = nil;
   104 static NSInteger toArraySort(id itemA, id itemB, void *context);
   105 
   106 + (NSString *)nibName
   107 {
   108 	return @"LogViewer";	
   109 }
   110 
   111 + (id)openForPlugin:(id)inPlugin
   112 {
   113     if (!sharedLogViewerInstance) {
   114 		sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
   115 	}
   116 
   117     [sharedLogViewerInstance showWindow:nil];
   118     
   119 	return sharedLogViewerInstance;
   120 }
   121 
   122 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
   123 {
   124 	[self openForPlugin:inPlugin];
   125 	
   126 	[sharedLogViewerInstance openLogAtPath:inPath];
   127 	
   128 	return sharedLogViewerInstance;
   129 }
   130 
   131 //Open the log viewer window to a specific contact's logs
   132 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
   133 {
   134     if (!sharedLogViewerInstance) {
   135 		sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
   136 	}
   137 
   138 	[sharedLogViewerInstance _willOpenForContact];
   139 	[sharedLogViewerInstance showWindow:nil];
   140 	[sharedLogViewerInstance filterForContact:inContact];
   141 	[sharedLogViewerInstance _didOpenForContact];
   142 
   143     return sharedLogViewerInstance;
   144 }
   145 
   146 + (id)openForChatName:(NSString *)inChatName withAccount:(AIAccount *)inAccount plugin:(id)inPlugin
   147 {
   148 	if (!sharedLogViewerInstance) {
   149 		sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
   150 	}
   151 	
   152 	[sharedLogViewerInstance _willOpenForContact];
   153 	[sharedLogViewerInstance showWindow:nil];
   154 	[sharedLogViewerInstance filterForChatName:inChatName withAccount:inAccount];
   155 	[sharedLogViewerInstance _didOpenForContact];
   156 	
   157     return sharedLogViewerInstance;
   158 }
   159 
   160 //Returns the window controller if one exists
   161 + (id)existingWindowController
   162 {
   163     return sharedLogViewerInstance;
   164 }
   165 
   166 //Close the log viewer window
   167 + (void)closeSharedInstance
   168 {
   169     if (sharedLogViewerInstance) {
   170         [sharedLogViewerInstance closeWindow:nil];
   171     }
   172 }
   173 
   174 //init
   175 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
   176 {
   177 	if((self = [super initWithWindowNibName:windowNibName])) {
   178 		plugin = inPlugin;
   179 		selectedColumn = nil;
   180 		activeSearchID = 0;
   181 		searching = NO;
   182 		automaticSearch = YES;
   183 		showEmoticons = NO;
   184 		showTimestamps = YES;
   185 		activeSearchString = nil;
   186 		displayedLogArray = nil;
   187 		windowIsClosing = NO;
   188 		desiredContactsSourceListDeltaX = 0;
   189 
   190 		blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
   191 
   192 		sortDirection = YES;
   193 		searchMode = LOG_SEARCH_CONTENT;
   194 
   195 		headerDateFormatter = [[NSDateFormatter localizedDateFormatter] retain];
   196 
   197 		currentSearchResults = [[NSMutableArray alloc] init];
   198 		fromArray = [[NSMutableArray alloc] init];
   199 		fromServiceArray = [[NSMutableArray alloc] init];
   200 		logFromGroupDict = [[NSMutableDictionary alloc] init];
   201 		toArray = [[NSMutableArray alloc] init];
   202 		toServiceArray = [[NSMutableArray alloc] init];
   203 		logToGroupDict = [[NSMutableDictionary alloc] init];
   204 		resultsLock = [[NSRecursiveLock alloc] init];
   205 		searchingLock = [[NSLock alloc] init];
   206 		[searchingLock setName:@"LogSearchingLock"];
   207 		contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
   208 
   209 		allContactsIdentifier = [[NSNumber numberWithInteger:-1] retain];
   210 
   211 		undoManager = [[NSUndoManager alloc] init];
   212 		currentSearchLock = [[NSLock alloc] init];
   213 		[currentSearchLock setName:@"CurrentLogSearchLock"];
   214 	}
   215 	
   216 	return self;
   217 }
   218 
   219 //dealloc
   220 - (void)dealloc
   221 {
   222 	[filterDate release]; filterDate = nil;
   223 	[currentSearchLock release]; currentSearchLock = nil;
   224 	[resultsLock release];
   225 	[searchingLock release];
   226 	[fromArray release];
   227 	[fromServiceArray release];
   228 	[toArray release];
   229 	[toServiceArray release];
   230 	[currentSearchResults release];
   231 	[selectedColumn release];
   232 	[headerDateFormatter release];
   233 	[displayedLogArray release];
   234 	[blankImage release];
   235 	[activeSearchString release];
   236 	[contactIDsToFilter release];
   237 
   238 	[logFromGroupDict release]; logFromGroupDict = nil;
   239 	[logToGroupDict release]; logToGroupDict = nil;
   240 
   241 	[filterForAccountName release]; filterForAccountName = nil;
   242 
   243 	[horizontalRule release]; horizontalRule = nil;
   244 
   245 	[adiumIcon release]; adiumIcon = nil;
   246 	[adiumIconHighlighted release]; adiumIconHighlighted = nil;
   247 
   248 	//We loaded	view_DatePicker from a nib manually, so we must release it
   249 	[view_DatePicker release]; view_DatePicker = nil;
   250 
   251 	[allContactsIdentifier release];
   252 	[undoManager release]; undoManager = nil;
   253 
   254 	[super dealloc];
   255 }
   256 
   257 //Init our log filtering tree
   258 - (void)initLogFiltering
   259 {
   260     NSMutableDictionary		*toDict = [NSMutableDictionary dictionary];
   261     NSString				*basePath = [AILoggerPlugin logBasePath];
   262     NSString				*fromUID, *serviceClass;
   263 
   264     //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
   265 	//first by service, then by account name.
   266     for (NSString *folderName in [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:basePath error:NULL] sortedArrayUsingSelector:@selector(compare:)]) {
   267 		if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
   268 			AILogFromGroup  *logFromGroup;
   269 			NSMutableSet	*toSetForThisService;
   270 			NSArray         *serviceAndFromUIDArray;
   271 			
   272 			/* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
   273 			 * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
   274 			 */
   275 			serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
   276 
   277 			if ([serviceAndFromUIDArray count] >= 2) {
   278 				serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
   279 
   280 				//Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
   281 				fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
   282 			} else {
   283 				//Fallback: blank non-nil serviceClass; folderName as the fromUID
   284 				serviceClass = @"";
   285 				fromUID = folderName;
   286 			}
   287 
   288 			logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
   289 
   290 			//Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
   291 			[logFromGroupDict setObject:logFromGroup forKey:folderName];
   292 
   293 			//To processing
   294 			if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
   295 				toSetForThisService = [NSMutableSet set];
   296 				[toDict setObject:toSetForThisService
   297 						   forKey:serviceClass];
   298 			}
   299 
   300 			//Add the 'to' for each grouping on this account
   301 			for (AILogToGroup *currentToGroup in [logFromGroup toGroupArray]) {
   302 				NSString	*currentTo;
   303 
   304 				if ((currentTo = [currentToGroup to])) {
   305 					//Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
   306 					[logToGroupDict setObject:currentToGroup forKey:[currentToGroup relativePath]];
   307 				}
   308 			}
   309 
   310 			[logFromGroup release];
   311 		}
   312 	}
   313 
   314 	[self rebuildContactsList];
   315 }
   316 
   317 - (void)rebuildContactsList
   318 {
   319 	NSInteger	oldCount = toArray.count;
   320 	[toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
   321 
   322 	for (AILogFromGroup *logFromGroup in [logFromGroupDict objectEnumerator]) {
   323 		//Add the 'to' for each grouping on this account
   324 		for (AILogToGroup *currentToGroup in [logFromGroup toGroupArray]) {
   325 			NSString	*currentTo;
   326 			
   327 			if ((currentTo = [currentToGroup to])) {
   328 				NSString *serviceClass = [currentToGroup serviceClass];
   329 				AIListObject *listObject = ((serviceClass && currentTo) ?
   330 											[adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
   331 																																			 UID:currentTo]] :
   332 											nil);
   333 				if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
   334 					AIListContact *parentContact = [(AIListContact *)listObject parentContact];
   335 					if (![toArray containsObjectIdenticalTo:parentContact]) {
   336 						[toArray addObject:parentContact];
   337 					}
   338 					
   339 				} else {
   340 					if (![toArray containsObject:currentToGroup]) {
   341 						[toArray addObject:currentToGroup];
   342 					}
   343 				}
   344 			}
   345 		}		
   346 	}
   347 	
   348 	[toArray sortUsingFunction:toArraySort context:NULL];
   349 	[outlineView_contacts reloadData];
   350 
   351 	if (!isOpeningForContact) {
   352 		//If we're opening for a contact, the outline view selection will be changed in a moment anyways
   353 		[self outlineViewSelectionDidChange:nil];
   354 	}
   355 }
   356 
   357 - (NSString *)adiumFrameAutosaveName
   358 {
   359 	return KEY_LOG_VIEWER_WINDOW_FRAME;
   360 }
   361 
   362 //Setup the window before it is displayed
   363 - (void)windowDidLoad
   364 {
   365 	suppressSearchRequests = YES;
   366 
   367 	[super windowDidLoad];
   368 
   369 	[plugin pauseIndexing];
   370 
   371 	[[self window] setTitle:AILocalizedString(@"Chat Transcript Viewer",nil)];
   372     [textField_progress setStringValue:@""];
   373 
   374 	//Autosave doesn't do anything yet
   375 	[shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
   376 	[shelf_splitView setFrame:[[[self window] contentView] frame]];
   377 
   378 	// Pull our main article/display split view out of the nib and position it in the shelf view
   379 	[containingView_results retain];
   380 	[containingView_results removeFromSuperview];
   381 	[shelf_splitView setContentView:containingView_results];
   382 	[containingView_results release];
   383 	[tableView_results accessibilitySetOverrideValue:AILocalizedString(@"Transcripts", nil)
   384 										forAttribute:NSAccessibilityRoleDescriptionAttribute];
   385 	
   386 	// Pull our source view out of the nib and position it in the shelf view
   387 	[containingView_contactsSourceList retain];
   388 	[containingView_contactsSourceList removeFromSuperview];
   389 	[shelf_splitView setShelfView:containingView_contactsSourceList];
   390 	[outlineView_contacts accessibilitySetOverrideValue:AILocalizedString(@"Contacts", nil)
   391 										forAttribute:NSAccessibilityRoleDescriptionAttribute];
   392 	[containingView_contactsSourceList release];
   393 
   394 	//Set emoticon filtering
   395 	showEmoticons = [[adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
   396 															  group:PREF_GROUP_LOGGING] boolValue];
   397 	[[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
   398 	[[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
   399 
   400 	// Set timestamp filtering
   401 	showTimestamps = [[adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_TIMESTAMPS
   402 															   group:PREF_GROUP_LOGGING] boolValue];
   403 	[[toolbarItems objectForKey:@"toggletimestamps"] setLabel:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)];
   404 	[[toolbarItems objectForKey:@"toggletimestamps"] setImage:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]];
   405 
   406 	//Toolbar
   407 	[self installToolbar];	
   408 
   409 	[outlineView_contacts setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleSourceList];
   410 
   411 	AIImageTextCell	*dataCell = [[AIImageTextCell alloc] init];
   412 	NSTableColumn	*tableColumn = [[outlineView_contacts tableColumns] objectAtIndex:0];
   413 	[tableColumn setDataCell:dataCell];
   414 	[tableColumn setEditable:NO];
   415 	[dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
   416 	[dataCell release];
   417 
   418 	// Set the selector for doubleAction
   419 	[outlineView_contacts setDoubleAction:@selector(openChatOnDoubleAction:)];
   420 	
   421 	//Localize tableView_results column headers
   422 	[[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
   423 	[[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
   424 	[[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
   425 	[self tableViewColumnDidResize:nil];
   426 
   427 	[tableView_results sizeLastColumnToFit];
   428 
   429 	//Prepare the search controls
   430 	[self buildSearchMenu];
   431 	if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
   432 		[textView_content setUsesFindPanel:YES];
   433 	}
   434 
   435     //Sort by preference, defaulting to sorting by date
   436 	NSString	*selectedTableColumnPref;
   437 	if ((selectedTableColumnPref = [adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
   438 																		   group:PREF_GROUP_LOGGING])) {
   439 		selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
   440 	}
   441 	if (!selectedColumn) {
   442 		selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
   443 	}
   444 	[self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
   445 
   446     //Prepare indexing and filter searching
   447 	[plugin prepareLogContentSearching];
   448     [self initLogFiltering];
   449 
   450     //Begin our initial search
   451 	if (!isOpeningForContact)
   452 		[self setSearchMode:LOG_SEARCH_TO];
   453 
   454     [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
   455 	suppressSearchRequests = NO;
   456 
   457 	if (!isOpeningForContact) {
   458 		//If we're opening for a contact, we'll select it and then begin searching
   459 		[self startSearchingClearingCurrentResults:YES];
   460 	}
   461 	
   462 	[tableView_results setAutosaveName:@"LogViewerResults"];
   463 	[tableView_results setAutosaveTableColumns:YES];
   464 
   465 	[plugin resumeIndexing];
   466 }
   467 
   468 -(void)rebuildIndices
   469 {
   470     //Rebuild the 'global' log indexes
   471     [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
   472     [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
   473     [toServiceArray removeAllObjects];
   474     [fromArray removeAllObjects];
   475     [fromServiceArray removeAllObjects];
   476     
   477     [self initLogFiltering];
   478     
   479     [tableView_results reloadData];
   480     [self selectDisplayedLog];
   481 }
   482 
   483 //Called as the window closes
   484 - (void)windowWillClose:(id)sender
   485 {
   486 	[super windowWillClose:sender];
   487 
   488 	//Set preference for emoticon filtering
   489 	[adium.preferenceController setPreference:[NSNumber numberWithBool:showEmoticons]
   490 										 forKey:KEY_LOG_VIEWER_EMOTICONS
   491 										  group:PREF_GROUP_LOGGING];
   492 											
   493 	// Set preference for timestamp filtering
   494 	[adium.preferenceController setPreference:[NSNumber numberWithBool:showTimestamps]
   495 																			 forKey:KEY_LOG_VIEWER_TIMESTAMPS
   496 																				group:PREF_GROUP_LOGGING];
   497 	
   498 	//Set preference for selected column
   499 	[adium.preferenceController setPreference:[selectedColumn identifier]
   500 										 forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
   501 										  group:PREF_GROUP_LOGGING];
   502 
   503     /* Disable the search field.  If we don't disable the search field, it will often try to call its target action
   504      * after the window has closed (and we are gone).  I'm not sure why this happens, but disabling the field
   505      * before we close the window down seems to prevent the crash.
   506 	 */
   507     [searchField_logs setEnabled:NO];
   508 	
   509 	/* Note that the window is closing so we don't take behaviors which could cause messages to the window after
   510 	 * it was gone, like responding to a logIndexUpdated message
   511 	 */
   512 	windowIsClosing = YES;
   513 
   514     //Abort any in-progress searching and indexing, and wait for their completion
   515     [self stopSearching];
   516     [plugin cleanUpLogContentSearching];
   517 
   518 	//Reset our column widths if needed
   519 	[activeSearchString release]; activeSearchString = nil;
   520 	[self updateRankColumnVisibility];
   521 	
   522 	[sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
   523 	[toolbarItems autorelease]; toolbarItems = nil;
   524 }
   525 
   526 //Display --------------------------------------------------------------------------------------------------------------
   527 #pragma mark Display
   528 //Update log viewer progress string to reflect current status
   529 - (void)updateProgressDisplay
   530 {
   531     NSMutableString     *progress = nil;
   532     NSUInteger					indexNumber, indexTotal;
   533     BOOL				indexing;
   534 
   535     //We always convey the number of logs being displayed
   536     [resultsLock lock];
   537 	NSUInteger count = [currentSearchResults count];
   538     if (activeSearchString && [activeSearchString length]) {
   539 		[shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
   540 																			   AILocalizedString(@"%lu matching transcripts",nil) :
   541 																			   AILocalizedString(@"1 matching transcript",nil)),count]];
   542     } else {
   543 		[shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ? 
   544 																			   AILocalizedString(@"%lu transcripts",nil) :
   545 																			   AILocalizedString(@"1 transcript",nil)),count]];
   546 		
   547 		//We are searching, but there is no active search  string. This indicates we're still opening logs.
   548 		if (searching) {
   549 			progress = [[AILocalizedString(@"Opening transcripts",nil) mutableCopy] autorelease];			
   550 		}
   551     }
   552     [resultsLock unlock];
   553 
   554 	indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
   555 
   556     //Append search progress
   557     if (activeSearchString && [activeSearchString length]) {
   558 		if (progress) {
   559 			[progress appendString:@" - "];
   560 		} else {
   561 			progress = [NSMutableString string];
   562 		}
   563 
   564 		if (searching || indexing) {
   565 			[progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
   566 		} else {
   567 			[progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];			
   568 		}
   569 	}
   570 
   571     //Append indexing progress
   572     if (indexing) {
   573 		if (progress) {
   574 			[progress appendString:@" - "];
   575 		} else {
   576 			progress = [NSMutableString string];
   577 		}
   578 		
   579 		[progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %lu of %lu transcripts",nil), indexNumber, indexTotal]];
   580     }
   581 	
   582 	if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
   583 		[progress appendString:[NSString ellipsis]];	
   584 	}
   585 
   586     //Enable/disable the searching animation
   587     if (searching || indexing) {
   588 		[progressIndicator startAnimation:nil];
   589     } else {
   590 		[progressIndicator stopAnimation:nil];
   591     }
   592     
   593     [textField_progress setStringValue:(progress ? progress : @"")];
   594 }
   595 
   596 //The plugin is informing us that the log indexing changed
   597 - (void)logIndexingProgressUpdate
   598 {
   599 	//Don't do anything if the window is already closing
   600 	if (!windowIsClosing) {
   601 		[self updateProgressDisplay];
   602 		
   603 		//If we are searching by content, we should re-search without clearing our current results so the
   604 		//the newly-indexed logs can be added without blanking the current table contents.
   605 		if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
   606 			if (searching) {
   607 				//We're already searching; reattempt when done
   608 				searchIDToReattemptWhenComplete = activeSearchID;
   609 			} else {
   610 				//We're not searching - restart the search immediately every 10 updates to utilize the newly indexed logs
   611 				indexingUpdatesReceivedWhileSearching++;
   612 				if ((indexingUpdatesReceivedWhileSearching % 10) == 0)
   613 					[self startSearchingClearingCurrentResults:NO];
   614 			}
   615 		}
   616 	}
   617 }
   618 
   619 //Refresh the results table
   620 - (void)refreshResults
   621 {
   622 	[self updateProgressDisplay];
   623 
   624 	[self refreshResultsSearchIsComplete:NO];
   625 }
   626 
   627 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
   628 {
   629     [resultsLock lock];
   630     NSInteger count = [currentSearchResults count];
   631     [resultsLock unlock];
   632 	AILog(@"refreshResultsSearchIsComplete: %i (count is %i)",searchIsComplete,count);
   633 	
   634 	if (searchIsComplete &&
   635 		((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
   636 		searchIDToReattemptWhenComplete = -1;
   637 		[self startSearchingClearingCurrentResults:NO];
   638 	}
   639 	
   640     if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
   641 		//Sort the logs correctly which will also reload the table
   642 		[self resortLogs];
   643 		
   644 		if (searchIsComplete && automaticSearch) {
   645 			//If search is complete, select the first log if requested and possible
   646 			[self selectFirstLog];
   647 			
   648 		} else {
   649 			BOOL oldAutomaticSearch = automaticSearch;
   650 			
   651 			//We don't want the above re-selection to change our automaticSearch tracking
   652 			//(The only reason automaticSearch should change is in response to user action)
   653 			automaticSearch = oldAutomaticSearch;
   654 		}
   655     }
   656 	
   657 	if(deleteOccurred)
   658 		[self selectCachedIndex];
   659 
   660     //Update status
   661     [self updateProgressDisplay];
   662 }
   663 
   664 - (void)searchComplete
   665 {
   666 	[refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
   667 	[self refreshResultsSearchIsComplete:YES];
   668 }
   669 
   670 // Called on doubleAction to open a chat
   671 -(void)openChatOnDoubleAction:(id)sender
   672 {
   673 	id item = [outlineView_contacts firstSelectedItem];
   674 	if ([item isKindOfClass:[AIListContact class]]) {
   675 		//Open a new message with the contact
   676 		[adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)item onPreferredAccount:YES]];
   677 	}
   678 }
   679 
   680 //Displays the contents of the specified log in our window
   681 - (void)displayLogs:(NSArray *)logArray;
   682 {	
   683     NSMutableAttributedString	*displayText = nil;
   684 	NSAttributedString			*finalDisplayText = nil;
   685 	NSRange						scrollRange = NSMakeRange(0,0);
   686 	BOOL						appendedFirstLog = NO;
   687 
   688     if (![logArray isEqualToArray:displayedLogArray]) {
   689 		[displayedLogArray release];
   690 		displayedLogArray = [logArray copy];
   691 	}
   692 
   693 	if ([logArray count] > 1) {
   694 		displayText = [[NSMutableAttributedString alloc] init];
   695 	}
   696 
   697 	AIChatLog	 *theLog;
   698 	NSString	 *logBasePath = [AILoggerPlugin logBasePath];
   699 	AILog(@"Displaying %@",logArray);
   700 	for (theLog in logArray) {
   701 		NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
   702 		
   703 		if (displayText) {
   704 			if (!horizontalRule) {
   705 				#define HORIZONTAL_BAR			0x2013
   706 				#define HORIZONTAL_RULE_LENGTH	18
   707 				
   708 				const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
   709 					HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
   710 					HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
   711 					HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
   712 				};
   713 				horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
   714 			}	
   715 			
   716 			[displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
   717 				(appendedFirstLog ? @"\n" : @""),
   718 				horizontalRule,
   719 				[headerDateFormatter stringFromDate:[theLog date]],
   720 				[theLog to],
   721 				horizontalRule]
   722 					   withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
   723 		}
   724 		
   725 		if ([[theLog relativePath] hasSuffix:@".AdiumHTMLLog"] || [[theLog relativePath] hasSuffix:@".html"] || [[theLog relativePath] hasSuffix:@".html.bak"]) {
   726 			//HTML log
   727 			NSURL *logURL = [NSURL fileURLWithPath:[logBasePath stringByAppendingPathComponent:[theLog relativePath]]];
   728 			NSString *logFileText = [NSString stringWithContentsOfURL:logURL encoding:NSUTF8StringEncoding error:NULL];
   729 			NSAttributedString *attributedLogFileText = [AIHTMLDecoder decodeHTML:logFileText];
   730 
   731 			if (showEmoticons) {
   732 				attributedLogFileText = [adium.contentController filterAttributedString:attributedLogFileText
   733 																		  usingFilterType:AIFilterMessageDisplay
   734 																				direction:AIFilterOutgoing
   735 																				  context:nil];						
   736 			}			
   737 
   738 			if (displayText) {
   739 				[displayText appendAttributedString:attributedLogFileText];
   740 			} else {
   741 				displayText = [attributedLogFileText mutableCopy];
   742 			}
   743 
   744 		} else if ([[theLog relativePath] hasSuffix:@".chatlog"]){
   745 			//XML log
   746 			NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog relativePath]];
   747 			
   748 			BOOL isDir;
   749 			if ([[NSFileManager defaultManager] fileExistsAtPath:logFullPath isDirectory:&isDir]) {
   750 				/* If we have a chatLog bundle, we want to get the text content for the xml file inside */
   751 				if (isDir) logFullPath = [logFullPath stringByAppendingPathComponent:
   752 										 [[[logFullPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
   753 			}
   754 
   755 			NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
   756 									 [NSNumber numberWithBool:showTimestamps], @"showTimestamps",
   757 									 [NSNumber numberWithBool:showEmoticons], @"showEmoticons", 
   758 									 nil];
   759 			NSAttributedString *attributedLogFileText = [AIXMLChatlogConverter readFile:logFullPath withOptions:options];
   760 			if (attributedLogFileText) {
   761 				if (displayText)
   762 					[displayText appendAttributedString:attributedLogFileText];
   763 				else
   764 					displayText = [attributedLogFileText mutableCopy];
   765 			}
   766 
   767 		} else {
   768 			//Fallback: Plain text log
   769 			NSURL *logURL = [NSURL fileURLWithPath:[logBasePath stringByAppendingPathComponent:[theLog relativePath]]];
   770 			NSString *logFileText = [NSString stringWithContentsOfURL:logURL encoding:NSUTF8StringEncoding error:NULL];
   771 			if (logFileText) {
   772 				AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
   773 				NSAttributedString *attributedLogFileText = [[[NSAttributedString alloc] initWithString:logFileText 
   774 																							 attributes:[textAttributes dictionary]] autorelease];
   775 				if (showEmoticons) {
   776 					attributedLogFileText = [adium.contentController filterAttributedString:attributedLogFileText
   777 																			  usingFilterType:AIFilterMessageDisplay
   778 																					direction:AIFilterOutgoing
   779 																					  context:nil];						
   780 				}
   781 				
   782 				if (displayText) {
   783 					[displayText appendAttributedString:attributedLogFileText];
   784 				} else {
   785 					displayText = [attributedLogFileText mutableCopy];
   786 				}
   787 			}
   788 		}
   789 		
   790 		appendedFirstLog = YES;
   791 		
   792 		[pool release];
   793 	}
   794 	
   795 	if (displayText && [displayText length]) {
   796 		//Add pretty formatting to links
   797 		[displayText addFormattingForLinks];
   798 
   799 		//If we are searching by content, highlight the search results
   800 		if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
   801 			NSString					*searchWord;
   802 			NSMutableArray				*searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
   803 			NSScanner					*scanner = [NSScanner scannerWithString:activeSearchString];
   804 			
   805 			//Look for an initial quote
   806 			NSAutoreleasePool *pool = nil;
   807 			while (![scanner isAtEnd]) {
   808 				[pool release];
   809 				pool = [[NSAutoreleasePool alloc] init];
   810 				
   811 				[scanner scanUpToString:@"\"" intoString:NULL];
   812 				
   813 				//Scan past the quote
   814 				if (![scanner scanString:@"\"" intoString:NULL]) {
   815 					[pool release]; pool = nil;
   816 					continue;
   817 				}
   818 				
   819 				NSString *quotedString;
   820 				//And a closing one
   821 				if (![scanner isAtEnd] &&
   822 					[scanner scanUpToString:@"\"" intoString:&quotedString]) {
   823 					//Scan past the quote
   824 					[scanner scanString:@"\"" intoString:NULL];
   825 					/* If a string within quotes is found, remove the words from the quoted string and add the full string
   826 					 * to what we'll be highlighting.
   827 					 *
   828 					 * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
   829 					 * "killer attack ninja kittens" OR ninja
   830 					 * wouldn't highlight the word ninja by itself.
   831 					 */
   832 					NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
   833 					NSInteger quotedWordsCount = [quotedWords count];
   834 					
   835 					for (NSInteger i = 0; i < quotedWordsCount; i++) {
   836 						NSString	*quotedWord = [quotedWords objectAtIndex:i];
   837 						if (i == 0) {
   838 							//Originally started with a quote, so put it back on
   839 							quotedWord = [@"\"" stringByAppendingString:quotedWord];
   840 						}
   841 						if (i == quotedWordsCount - 1) {
   842 							//Originally ended with a quote, so put it back on
   843 							quotedWord = [quotedWord stringByAppendingString:@"\""];
   844 						}
   845 						NSInteger searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
   846 						if (searchWordsIndex != NSNotFound) {
   847 							[searchWordsArray removeObjectAtIndex:searchWordsIndex];
   848 						} else {
   849 							NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
   850 						}
   851 					}
   852 					
   853 					//Add the full quoted string
   854 					[searchWordsArray addObject:quotedString];
   855 				}
   856 			}
   857 
   858 			BOOL shouldScrollToWord = NO;
   859 			scrollRange = NSMakeRange([displayText length],0);
   860 
   861 			for (searchWord in searchWordsArray) {
   862 				NSRange     occurrence;
   863 				
   864 				//Check against and/or.  We don't just remove it from the array because then we couldn't check case insensitively.
   865 				if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
   866 					([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
   867 					[self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
   868 					
   869 					//We'll want to scroll to the first occurrance of any matching word or words
   870 					if (occurrence.location < scrollRange.location) {
   871 						scrollRange = occurrence;
   872 						shouldScrollToWord = YES;
   873 					}
   874 				}
   875 			}
   876 			
   877 			//If we shouldn't be scrolling to a new range, we want to scroll to the top
   878 			if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
   879 			
   880 			[searchWordsArray release];
   881 		}
   882 		
   883 		finalDisplayText = displayText;
   884 	}
   885 
   886 	if (finalDisplayText) {
   887 		[[textView_content textStorage] setAttributedString:finalDisplayText];
   888 
   889 		//Set this string and scroll to the top/bottom/occurrence
   890 		if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
   891 			[textView_content scrollRangeToVisible:scrollRange];
   892 		} else {
   893 			[textView_content scrollRangeToVisible:NSMakeRange(0,0)];
   894 		}
   895 
   896 	} else {
   897 		//No log selected, empty the view
   898 		[textView_content setString:@""];
   899 	}
   900 
   901 	[displayText release];
   902 }
   903 
   904 - (void)displayLog:(AIChatLog *)theLog
   905 {
   906 	[self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
   907 }
   908 
   909 //Reselect the displayed log (Or another log if not possible)
   910 - (void)selectDisplayedLog
   911 {
   912     NSInteger     firstIndex = NSNotFound;
   913     
   914     /* Is the log we had selected still in the table?
   915 	 * (When performing an automatic search, we ignore the previous selection.  This ensures that we always
   916      * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
   917 	 */
   918 	if (!automaticSearch) {
   919 		[resultsLock lock];
   920 		[tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
   921 		[resultsLock unlock];
   922 		
   923 		firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
   924 	}
   925 
   926 	if (firstIndex != NSNotFound) {
   927 		[tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
   928     } else {
   929         if (useSame == YES && sameSelection > 0) {
   930             [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:sameSelection] byExtendingSelection:NO];
   931         } else {
   932             [self selectFirstLog];
   933         }
   934     }
   935 
   936     useSame = NO;
   937 }
   938 
   939 - (void)selectFirstLog
   940 {
   941 	AIChatLog   *theLog = nil;
   942 	
   943 	//If our selected log is no more, select the first one in the list
   944 	[resultsLock lock];
   945 	if ([currentSearchResults count] != 0) {
   946 		theLog = [currentSearchResults objectAtIndex:0];
   947 	}
   948 	[resultsLock unlock];
   949 	
   950 	//Change the table selection to this new log
   951 	//We need a little trickery here.  When we change the row, the table view will call our tableViewSelectionDidChange: method.
   952 	//This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
   953 	//search.  As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
   954 	//that we instantiated this selection change, and not the user.
   955 	ignoreSelectionChange = YES;
   956 	[tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
   957 	[tableView_results scrollRowToVisible:0];
   958 	ignoreSelectionChange = NO;
   959 
   960 	[self displayLog:theLog];  //Manually update the displayed log
   961 }
   962 
   963 //Highlight the occurences of a search string within a displayed log
   964 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
   965 {
   966     NSInteger					location = 0;
   967     NSRange				searchRange, foundRange;
   968     NSString			*plainBigString = [bigString string];
   969 	NSUInteger			plainBigStringLength = [plainBigString length];
   970 	NSMutableDictionary *attributeDictionary = nil;
   971 
   972     outRange->location = NSNotFound;
   973 
   974     //Search for the little string in the big string
   975     while (location != NSNotFound && location < plainBigStringLength) {
   976         searchRange = NSMakeRange(location, plainBigStringLength-location);
   977         foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
   978 		
   979 		//Bold and color this match
   980         if (foundRange.location != NSNotFound) {
   981 			if (outRange->location == NSNotFound) *outRange = foundRange;
   982 
   983 			if (!attributeDictionary) {
   984 				attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
   985 					[NSFont boldSystemFontOfSize:14], NSFontAttributeName,
   986 					[NSColor yellowColor], NSBackgroundColorAttributeName,
   987 					nil];
   988 			}
   989 			[bigString addAttributes:attributeDictionary
   990 							   range:foundRange];
   991         }
   992 
   993         location = NSMaxRange(foundRange);
   994     }
   995 }
   996 
   997 
   998 //Sorting --------------------------------------------------------------------------------------------------------------
   999 #pragma mark Sorting
  1000 - (void)resortLogs
  1001 {
  1002 	NSString *identifier = [selectedColumn identifier];
  1003 
  1004     //Resort the data
  1005 	[resultsLock lock];
  1006     if ([identifier isEqualToString:@"To"]) {
  1007 		[currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
  1008 		
  1009     } else if ([identifier isEqualToString:@"From"]) {
  1010         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
  1011 		
  1012     } else if ([identifier isEqualToString:@"Date"]) {
  1013         [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
  1014 		
  1015     } else if ([identifier isEqualToString:@"Rank"]) {
  1016 	    [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
  1017 
  1018 	} else if ([identifier isEqualToString:@"Service"]) {
  1019 	    [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareServiceReverse:) : @selector(compareService:))];
  1020 	}
  1021 	
  1022     [resultsLock unlock];
  1023 
  1024     //Reload the data
  1025     [tableView_results reloadData];
  1026 
  1027     //Reapply the selection
  1028     [self selectDisplayedLog];	
  1029 }
  1030 
  1031 //Sorts the selected log array and adjusts the selected column
  1032 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
  1033 {
  1034     //If there already was a sorted column, remove the indicator image from it.
  1035     if (selectedColumn && selectedColumn != tableColumn) {
  1036         [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
  1037     }
  1038     
  1039     //Set the indicator image in the newly selected column
  1040     [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
  1041                            inTableColumn:tableColumn];
  1042     
  1043     //Set the highlighted table column.
  1044     [tableView_results setHighlightedTableColumn:tableColumn];
  1045     [selectedColumn release]; selectedColumn = [tableColumn retain];
  1046     sortDirection = direction;
  1047 	
  1048 	[self resortLogs];
  1049 }
  1050 
  1051 //Searching ------------------------------------------------------------------------------------------------------------
  1052 #pragma mark Searching
  1053 //(Jag)Change search string
  1054 - (void)controlTextDidChange:(NSNotification *)notification
  1055 {
  1056     if (searchMode != LOG_SEARCH_CONTENT) {
  1057 		[self updateSearch:nil];
  1058     }
  1059 }
  1060 
  1061 //Change search string (Called by searchfield)
  1062 - (IBAction)updateSearch:(id)sender
  1063 {
  1064     automaticSearch = NO;
  1065     [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
  1066 	AILog(@"updateSearch calling startSearching");
  1067     [self startSearchingClearingCurrentResults:YES];
  1068 }
  1069 
  1070 //Change search mode (Called by mode menu)
  1071 - (IBAction)selectSearchType:(id)sender
  1072 {
  1073     automaticSearch = NO;
  1074 
  1075 	//First, update the search mode to the newly selected type
  1076     [self setSearchMode:[sender tag]]; 
  1077 	
  1078 	//Then, ensure we are ready to search using the current string
  1079 	[self setSearchString:activeSearchString];
  1080 
  1081 	//Now we are ready to start searching
  1082 	AILog(@"selectSearchType calling startSearching");
  1083     [self startSearchingClearingCurrentResults:YES];
  1084 }
  1085 
  1086 //Begin a specific search
  1087 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
  1088 {
  1089     automaticSearch = YES;
  1090 	//Apply the search mode first since the behavior of setSearchString changes depending on the current mode
  1091     [self setSearchMode:inMode];
  1092     [self setSearchString:inString];
  1093 
  1094 	AILog(@"setSearchString:mode: calling startSearching");
  1095     [self startSearchingClearingCurrentResults:YES];
  1096 }
  1097 
  1098 //Begin the current search
  1099 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
  1100 {
  1101     NSDictionary    *searchDict;
  1102 
  1103 	if (suppressSearchRequests) return;
  1104 	AILog(@"Starting a search for %@",activeSearchString);
  1105 
  1106     //Once all searches have exited, we can start a new one
  1107 	if (clearCurrentResults) {
  1108 		[resultsLock lock];
  1109 		//Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
  1110 		[self stopSearching];
  1111 
  1112 		[currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
  1113 		[resultsLock unlock];
  1114 	} else {
  1115 	    //Stop any existing searches
  1116 		[self stopSearching];	
  1117 	}
  1118 
  1119 	searching = YES;
  1120 	indexingUpdatesReceivedWhileSearching = 0;
  1121     searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
  1122 		[NSNumber numberWithInteger:activeSearchID], @"ID",
  1123 		[NSNumber numberWithInteger:searchMode], @"Mode",
  1124 		activeSearchString, @"String",
  1125 		[plugin logContentIndex], @"SearchIndex",
  1126 		nil];
  1127     [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
  1128     
  1129 	//Update the table periodically while the logs load.
  1130 	[refreshResultsTimer invalidate]; [refreshResultsTimer release];
  1131 	refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
  1132 															target:self
  1133 														  selector:@selector(refreshResults)
  1134 														  userInfo:nil
  1135 														   repeats:YES] retain];
  1136 }
  1137 
  1138 //Abort any active searches
  1139 - (void)stopSearching
  1140 {
  1141 	[currentSearchLock lock];
  1142 	if (currentSearch) {
  1143 		SKSearchCancel(currentSearch);
  1144 		CFRelease(currentSearch); currentSearch = nil;
  1145 	}
  1146 	[currentSearchLock unlock];
  1147 	
  1148 	[refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
  1149 
  1150 	//Increase the active search ID so any existing searches stop, and then
  1151 	//wait for any active searches to finish and release the lock
  1152 	activeSearchID++;
  1153 }
  1154 
  1155 //Set the active search mode (Does not invoke a search)
  1156 - (void)setSearchMode:(LogSearchMode)inMode
  1157 {
  1158 	NSTextFieldCell	*cell = [searchField_logs cell];
  1159 	
  1160     searchMode = inMode;
  1161 	
  1162 	//Clear any filter from the table if it's the current mode, as well
  1163 	switch (searchMode) {
  1164 		case LOG_SEARCH_FROM:
  1165 			[cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
  1166 			break;
  1167 
  1168 		case LOG_SEARCH_TO:
  1169 			[cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
  1170 			break;
  1171 			
  1172 		case LOG_SEARCH_DATE:
  1173 			[cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
  1174 			break;
  1175 
  1176 		case LOG_SEARCH_CONTENT:
  1177 			[cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
  1178 			break;
  1179 	}
  1180 
  1181 	[self updateRankColumnVisibility];
  1182     [self buildSearchMenu];
  1183 }
  1184 
  1185 - (void)updateRankColumnVisibility
  1186 {
  1187 	NSTableColumn	*resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
  1188 	
  1189 	if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
  1190 		//Add the resultsColumn and resize if it should be shown but is not at present
  1191 		if (!resultsColumn) {	
  1192 			NSArray			*tableColumns;
  1193 
  1194 			//Set up the results column
  1195 			resultsColumn = [[[NSTableColumn alloc] initWithIdentifier:@"Rank"] autorelease];
  1196 			[[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
  1197 			[resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
  1198 			
  1199 			//Add it to the table
  1200 			[tableView_results addTableColumn:resultsColumn];
  1201 
  1202 			//Make it half again as large as the desired width from the @"Rank" header title
  1203 			[resultsColumn sizeToFit];
  1204 			[resultsColumn setWidth:([resultsColumn width] * 1.5)];
  1205 			
  1206 			tableColumns = [tableView_results tableColumns];
  1207 			if ([tableColumns indexOfObject:resultsColumn] > 0) {
  1208 				NSTableColumn	*nextDoorNeighbor;
  1209 
  1210 				//Adjust the column to the results column's left so results is now visible
  1211 				nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
  1212 				[nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
  1213 			}
  1214 		}
  1215 	} else {
  1216 		//Remove the resultsColumn and resize if it should not be shown but is at present
  1217 		if (resultsColumn) {
  1218 			NSArray			*tableColumns;
  1219 
  1220 			tableColumns = [tableView_results tableColumns];
  1221 			if ([tableColumns indexOfObject:resultsColumn] > 0) {
  1222 				NSTableColumn	*nextDoorNeighbor;
  1223 
  1224 				//Adjust the column to the results column's left to take up the space again
  1225 				tableColumns = [tableView_results tableColumns];
  1226 				nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
  1227 				[nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
  1228 			}
  1229 
  1230 			//Remove it
  1231 			[tableView_results removeTableColumn:resultsColumn];
  1232 		}
  1233 	}
  1234 }
  1235 
  1236 //Set the active search string (Does not invoke a search)
  1237 - (void)setSearchString:(NSString *)inString
  1238 {
  1239     if (![[searchField_logs stringValue] isEqualToString:inString]) {
  1240 		[searchField_logs setStringValue:(inString ? inString : @"")];
  1241     }
  1242 	
  1243 	//Use autorelease so activeSearchString can be passed back to here
  1244 	if (activeSearchString != inString) {
  1245 		[activeSearchString release];
  1246 		activeSearchString = [inString retain];
  1247 	}
  1248 
  1249 	[self updateRankColumnVisibility];
  1250 }
  1251 
  1252 //Build the search mode menu
  1253 - (void)buildSearchMenu
  1254 {
  1255     NSMenu  *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
  1256     [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
  1257     [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
  1258     [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
  1259     [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
  1260 
  1261 	[[searchField_logs cell] setSearchMenuTemplate:cellMenu];
  1262 }
  1263 
  1264 - (void)_willOpenForContact
  1265 {
  1266 	isOpeningForContact = YES;
  1267 }
  1268 
  1269 - (void)_didOpenForContact
  1270 {
  1271 	isOpeningForContact = NO;
  1272 }
  1273 
  1274 /*!
  1275  * @brief Focus the log viewer on a particular contact
  1276  *
  1277  * If the contact is within a metacontact, the metacontact will be focused.
  1278  */
  1279 - (void)filterForContact:(AIListContact *)inContact
  1280 {
  1281 	AIListContact *parentContact = [inContact parentContact];
  1282 
  1283 	if (!isOpeningForContact) {
  1284 		/* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
  1285 		* (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
  1286 		* with regard to inContact since the log viewer opened.
  1287 		*
  1288 		* If we're opening initially, the list is guaranteed fresh.
  1289 		*/
  1290 		[self rebuildContactsList];
  1291 	}
  1292 
  1293 	//If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
  1294 	if (searchMode == LOG_SEARCH_TO) {
  1295 		[self setSearchMode:LOG_SEARCH_CONTENT];
  1296 		
  1297 		//Update our search string to ensure we're configured for content searching
  1298 		[self setSearchString:activeSearchString];
  1299 	}
  1300 
  1301 	//Changing the selection will start a new search
  1302 	[outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
  1303 	NSUInteger selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
  1304 	if (selectedRow != NSNotFound) {
  1305 		[outlineView_contacts scrollRowToVisible:selectedRow];
  1306 	}
  1307 }
  1308 
  1309 - (void)filterForChatName:(NSString *)chatName withAccount:(AIAccount *)account
  1310 {
  1311 	if (!isOpeningForContact) {
  1312 		// See above.
  1313 		[self rebuildContactsList];
  1314 	}
  1315 	
  1316 	AILogToGroup *logToGroup = [logToGroupDict objectForKey:[[NSString stringWithFormat:@"%@.%@",
  1317 															  account.service.serviceID,
  1318 															  account.UID.safeFilenameString]
  1319 															 stringByAppendingPathComponent:chatName]];
  1320 
  1321 	//Changing the selection will start a new search
  1322 	[outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(logToGroup ?: (id)allContactsIdentifier)]];
  1323 	NSUInteger selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
  1324 	if (selectedRow != NSNotFound) {
  1325 		[outlineView_contacts scrollRowToVisible:selectedRow];
  1326 	}
  1327 }
  1328 
  1329 /*!
  1330  * @brief Returns a menu item for the search mode menu
  1331  */
  1332 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
  1333 {
  1334     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title 
  1335 																				 action:@selector(selectSearchType:) 
  1336 																		  keyEquivalent:@""];
  1337     [menuItem setTag:mode];
  1338     [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
  1339     
  1340     return [menuItem autorelease];
  1341 }
  1342 
  1343 #pragma mark Filtering search results
  1344 
  1345 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
  1346 {
  1347 	BOOL matchesDateFilter;
  1348 
  1349 	switch (filterDateType) {
  1350 		case AIDateTypeAfter:
  1351 			matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
  1352 			break;
  1353 		case AIDateTypeBefore:
  1354 			matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
  1355 			break;
  1356 		case AIDateTypeExactly:
  1357 			matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
  1358 			break;
  1359 		default:
  1360 			matchesDateFilter = YES;
  1361 			break;
  1362 	}
  1363 
  1364 	return matchesDateFilter;
  1365 }
  1366 
  1367 
  1368 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
  1369 {
  1370 	CFURLRef	url = SKDocumentCopyURL(inDocument);
  1371 	if (!url) {
  1372 		AILogWithSignature(@"Could not get url for %p", inDocument);
  1373 		return nil;
  1374 	}
  1375 
  1376 	NSString	*logPath = [(NSURL *)url path];
  1377 	if (!logPath)
  1378 		AILogWithSignature(@"Could not get path for %@", url);
  1379 	NSArray		*pathComponents = [logPath pathComponents];
  1380 
  1381 	CFRelease(url);
  1382 
  1383 	return pathComponents;
  1384 }
  1385 
  1386 
  1387 /*!
  1388  * @brief Should a search display a document with the given information?
  1389  */
  1390 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
  1391 {
  1392 	BOOL shouldDisplayDocument = YES;
  1393 
  1394 	if ([contactIDsToFilter count]) {
  1395 		//Determine the path components if we weren't supplied them
  1396 		if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
  1397 
  1398 		NSUInteger numPathComponents = [pathComponents count];
  1399 		
  1400 		NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
  1401 		NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
  1402 
  1403 		NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
  1404 
  1405 		shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
  1406 	} 
  1407 	
  1408 	if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
  1409 		if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
  1410 
  1411 		NSUInteger	numPathComponents = [pathComponents count];
  1412 		NSString		*toPath = [NSString stringWithFormat:@"%@/%@",
  1413 			[pathComponents objectAtIndex:numPathComponents-3],
  1414 			[pathComponents objectAtIndex:numPathComponents-2]];
  1415 		NSString		*relativePath = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
  1416 		AIChatLog		*theLog;
  1417 		
  1418 		theLog = [[logToGroupDict objectForKey:toPath] logAtPath:relativePath];
  1419 		
  1420 		shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
  1421 	}
  1422 
  1423 	return shouldDisplayDocument;
  1424 }
  1425 
  1426 //Threaded filter/search methods ---------------------------------------------------------------------------------------
  1427 #pragma mark Threaded filter/search methods
  1428 
  1429 /*!
  1430  * @brief Perform a content search of the indexed logs
  1431  *
  1432  * This uses the 10.4+ asynchronous search functions.
  1433  * Google-like search syntax (phrase, prefix/suffix, boolean, etc. searching) is automatically supported.
  1434  */
  1435 - (void)_logContentFilter:(NSString *)searchString searchID:(NSInteger)searchID onSearchIndex:(SKIndexRef)logSearchIndex
  1436 {
  1437 	CGFloat			largestRankingValue = 0;
  1438 	SKSearchRef		thisSearch;
  1439     Boolean			more = true;
  1440     UInt32			totalCount = 0;
  1441 	
  1442 	[currentSearchLock lock];
  1443 	if (currentSearch) {
  1444 		SKSearchCancel(currentSearch);
  1445 		CFRelease(currentSearch); currentSearch = NULL;
  1446 	}
  1447 	
  1448 	NSMutableString *wildcardedSearchString = [NSMutableString string];
  1449 	for (NSString *searchComponent in [searchString componentsSeparatedByString:@" "]) {
  1450 		if ([searchComponent rangeOfString:@"*"].location == NSNotFound) {
  1451 			//If the user specifies particular wildcard behavior, respect it
  1452 			[wildcardedSearchString appendFormat:@"*%@* ", searchComponent];
  1453 		} else
  1454 			[wildcardedSearchString appendFormat:@"%@ ", searchComponent];
  1455 	}
  1456 	
  1457 	thisSearch = SKSearchCreate(logSearchIndex,
  1458 								(CFStringRef)wildcardedSearchString,
  1459 								kSKSearchOptionDefault);
  1460 	currentSearch = (thisSearch ? (SKSearchRef)CFRetain(thisSearch) : NULL);
  1461 	[currentSearchLock unlock];
  1462 	
  1463 	//Retrieve matches as long as more are pending
  1464     while (more && currentSearch) {
  1465 #define BATCH_NUMBER 100
  1466         SKDocumentID	foundDocIDs[BATCH_NUMBER];
  1467         float			foundScores[BATCH_NUMBER];
  1468         SKDocumentRef	foundDocRefs[BATCH_NUMBER];
  1469 		
  1470         CFIndex foundCount = 0;
  1471         CFIndex i;
  1472 		
  1473         more = SKSearchFindMatches (
  1474 									thisSearch,
  1475 									BATCH_NUMBER,
  1476 									foundDocIDs,
  1477 									foundScores,
  1478 									0.5, // maximum time before func returns, in seconds
  1479 									&foundCount
  1480 									);
  1481 		
  1482         totalCount += foundCount;
  1483 		
  1484         SKIndexCopyDocumentRefsForDocumentIDs (
  1485 											   logSearchIndex,
  1486 											   foundCount,
  1487 											   foundDocIDs,
  1488 											   foundDocRefs
  1489 											   );
  1490         for (i = 0; ((i < foundCount) && (searchID == activeSearchID)) ; i++) {
  1491 			SKDocumentRef	document = foundDocRefs[i];
  1492 					if (!document) {
  1493 						AILogWithSignature(@"SearchKit returned NULL document for ID %ld", (long)foundDocIDs[i]);
  1494 						totalCount--;
  1495 						continue;
  1496 					}
  1497 			CFURLRef		url = SKDocumentCopyURL(document);
  1498 			if (!url) {
  1499 				AILogWithSignature(@"No URL for document %p", document);
  1500 				totalCount--;
  1501 				continue;
  1502 			}
  1503 			/*
  1504 			 * Nasty implementation note: As of 10.4.7 and all previous versions, a path longer than 1024 bytes (PATH_MAX)
  1505 			 * will cause CFURLCopyFileSystemPath() to crash [ultimately in CFGetAllocator()].  This is the case for all
  1506 			 * Cocoa applications...
  1507 			 */
  1508 			NSString *logPath = [(NSURL *)url path];
  1509 			if (!logPath) 
  1510 				AILogWithSignature(@"Could not get path for %@. ", url);
  1511 			
  1512 			NSArray	 *pathComponents = [(NSString *)logPath pathComponents];
  1513 			
  1514 			/* Handle chatlogs-as-bundles, which have an xml file inside our target .chatlog path */
  1515 			if ([[[pathComponents lastObject] pathExtension] caseInsensitiveCompare:@"xml"] == NSOrderedSame)
  1516 				pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, [pathComponents count] - 1)];
  1517 			
  1518 			//Don't test for the date now; we'll test once we've found the AIChatLog if we make it that far
  1519 			if ([self searchShouldDisplayDocument:document pathComponents:pathComponents testDate:NO]) {
  1520 				NSUInteger	numPathComponents = [pathComponents count];
  1521 				NSString		*toPath = [NSString stringWithFormat:@"%@/%@",
  1522 										   [pathComponents objectAtIndex:numPathComponents-3],
  1523 										   [pathComponents objectAtIndex:numPathComponents-2]];
  1524 				NSString		*path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
  1525 				AIChatLog		*theLog;
  1526 				
  1527 				/* Add the log - if our index is currently out of date (for example, a log was just deleted) 
  1528 				 * we may get a null log, so be careful.
  1529 				 */
  1530 				theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
  1531 				if (!theLog) {
  1532 					AILog(@"_logContentFilter: %x's key %@ yields %@; logAtPath:%@ gives %@",logToGroupDict,toPath,[logToGroupDict objectForKey:toPath],path,theLog);
  1533 				}
  1534 				[resultsLock lock];
  1535 				if ((theLog != nil) &&
  1536 					(![currentSearchResults containsObjectIdenticalTo:theLog]) &&
  1537 					[self chatLogMatchesDateFilter:theLog] &&
  1538 					(searchID == activeSearchID)) {
  1539 					[theLog setRankingValueOnArbitraryScale:foundScores[i]];
  1540 					
  1541 					//SearchKit does not normalize ranking scores, so we track the largest we've found and use it as 1.0
  1542 					if (foundScores[i] > largestRankingValue) largestRankingValue = foundScores[i];
  1543 					
  1544 					[currentSearchResults addObject:theLog];
  1545 				} else {
  1546 					//Didn't get a valid log, so decrement our totalCount which is tracking how many logs we found
  1547 					totalCount--;
  1548 				}
  1549 				[resultsLock unlock];					
  1550 				
  1551 			} else {
  1552 				//Didn't add this log, so decrement our totalCount which is tracking how many logs we found
  1553 				totalCount--;
  1554 			}
  1555 			
  1556 			//if (logPath) CFRelease(logPath);
  1557 			if (url) CFRelease(url);
  1558 			if (document) CFRelease(document);
  1559         }
  1560 		
  1561 		//Scale all logs' ranking values to the largest ranking value we've seen thus far
  1562 		[resultsLock lock];
  1563 		for (i = 0; ((i < totalCount) && (searchID == activeSearchID)); i++) {
  1564 			AIChatLog	*theLog = [currentSearchResults objectAtIndex:i];
  1565 			[theLog setRankingPercentage:([theLog rankingValueOnArbitraryScale] / largestRankingValue)];
  1566 		}
  1567 		[resultsLock unlock];
  1568 		
  1569 		[self performSelectorOnMainThread:@selector(updateProgressDisplay)
  1570 							   withObject:nil
  1571 							waitUntilDone:NO];
  1572 		
  1573 		if (searchID != activeSearchID) {
  1574 			more = FALSE;
  1575 		}
  1576     }
  1577 	
  1578 	//Ensure current search isn't released in two places simultaneously
  1579 	[currentSearchLock lock];
  1580 	if (currentSearch) {
  1581 		CFRelease(currentSearch);
  1582 		currentSearch = NULL;
  1583 	}
  1584 	[currentSearchLock unlock];
  1585 	
  1586 	if (thisSearch) CFRelease(thisSearch);
  1587 }
  1588 
  1589 //Search the logs, filtering out any matching logs into the currentSearchResults
  1590 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
  1591 {
  1592     NSAutoreleasePool       *pool = [[NSAutoreleasePool alloc] init];
  1593     NSInteger                     mode = [[searchInfoDict objectForKey:@"Mode"] integerValue];
  1594     NSInteger                     searchID = [[searchInfoDict objectForKey:@"ID"] integerValue];
  1595     NSString                *searchString = [searchInfoDict objectForKey:@"String"];
  1596 
  1597     if (searchID == activeSearchID) { //If we're still supposed to go
  1598 		searching = YES;
  1599 		AILog(@"filterLogsWithSearch (search ID %i): %@",searchID,searchInfoDict);
  1600 		//Search
  1601 		[plugin pauseIndexing];
  1602 		if (searchString && [searchString length]) {
  1603 			switch (mode) {
  1604 				case LOG_SEARCH_FROM:
  1605 				case LOG_SEARCH_TO:
  1606 				case LOG_SEARCH_DATE:
  1607 					[self _logFilter:searchString
  1608 							searchID:searchID
  1609 								mode:mode];
  1610 					break;
  1611 				case LOG_SEARCH_CONTENT:
  1612 					[self _logContentFilter:searchString
  1613 								   searchID:searchID
  1614 							  onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
  1615 					break;
  1616 			}
  1617 		} else {
  1618 			[self _logFilter:nil
  1619 					searchID:searchID
  1620 						mode:mode];
  1621 		}
  1622 		
  1623 		//Refresh
  1624 		searching = NO;
  1625 		[plugin resumeIndexing];
  1626 		[self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
  1627 		AILog(@"filterLogsWithSearch (search ID %i): finished",searchID);
  1628     }
  1629 	
  1630     //Cleanup
  1631     [pool release];
  1632 }
  1633 
  1634 //Perform a filter search based on source name, destination name, or date
  1635 - (void)_logFilter:(NSString *)searchString searchID:(NSInteger)searchID mode:(LogSearchMode)mode
  1636 {
  1637     UInt32		lastUpdate = TickCount();
  1638     
  1639     NSCalendarDate	*searchStringDate = nil;
  1640 	
  1641 	if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
  1642 		searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString]  dateWithCalendarFormat:nil timeZone:nil];
  1643 	}
  1644 	
  1645     //Walk through every 'from' group
  1646     for (AILogFromGroup *fromGroup in [logFromGroupDict objectEnumerator]) {
  1647 		if (searchID != activeSearchID) break;
  1648 		
  1649 		//When searching in LOG_SEARCH_FROM, we only proceed into matching groups
  1650 		if ((mode != LOG_SEARCH_FROM) ||
  1651 			(!searchString) || 
  1652 			([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
  1653 
  1654 			//Walk through every 'to' group
  1655 			for (AILogToGroup *toGroup in [fromGroup toGroupArray]) {
  1656 				if (searchID != activeSearchID) break;
  1657 
  1658 				/* When searching in LOG_SEARCH_TO, we only proceed into matching groups
  1659 				 * For all other search modes, we always proceed here so long as either:
  1660 				 *	a) We are not filtering for specific contact names or
  1661 				 *	b) The contact name matches one of the names in contactIDsToFilter
  1662 				 */
  1663 				if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
  1664 				   ((mode != LOG_SEARCH_TO) ||
  1665 				   (!searchString) || 
  1666 				   ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
  1667 					
  1668 					//Walk through every log
  1669 					for (AIChatLog *theLog in [toGroup logEnumerator]) {
  1670 						if (searchID != activeSearchID) break;
  1671 
  1672 						/* When searching in LOG_SEARCH_DATE, we must have matching dates
  1673 						 * For all other search modes, we always proceed here
  1674 						 */
  1675 						if ((mode != LOG_SEARCH_DATE) ||
  1676 						   (!searchString) ||
  1677 						   (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
  1678 
  1679 							if ([self chatLogMatchesDateFilter:theLog]) {
  1680 								//Add the log
  1681 								[resultsLock lock];
  1682 								[currentSearchResults addObject:theLog];
  1683 								[resultsLock unlock];							
  1684 								
  1685 								//Update our status
  1686 								if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
  1687 									[self performSelectorOnMainThread:@selector(updateProgressDisplay)
  1688 														   withObject:nil
  1689 														waitUntilDone:NO];
  1690 									lastUpdate = TickCount();
  1691 								}
  1692 							}
  1693 						}
  1694 					}
  1695 				}
  1696 			}	    
  1697 		}
  1698     }
  1699 }
  1700 
  1701 //Search results table view --------------------------------------------------------------------------------------------
  1702 #pragma mark Search results table view
  1703 //Since this table view's source data will be accessed from within other threads, we need to lock before
  1704 //accessing it.  We also must be very sure that an incorrect row request is handled silently, since this
  1705 //can occur if the array size is changed during the reload.
  1706 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
  1707 {
  1708     NSInteger count;
  1709     
  1710     [resultsLock lock];
  1711     count = [currentSearchResults count];
  1712     [resultsLock unlock];
  1713     
  1714     return count;
  1715 }
  1716 
  1717 
  1718 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
  1719 {
  1720     NSString	*identifier = [tableColumn identifier];
  1721 
  1722 	if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
  1723 		AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
  1724 		
  1725 		[aCell setPercentage:[theLog rankingPercentage]];
  1726 	}
  1727 }
  1728 
  1729 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
  1730 {
  1731     NSString	*identifier = [tableColumn identifier];
  1732     id          value = nil;
  1733     
  1734     [resultsLock lock];
  1735     if (row < 0 || row >= [currentSearchResults count]) {
  1736 		if ([identifier isEqualToString:@"Service"]) {
  1737 			value = blankImage;
  1738 		} else {
  1739 			value = @"";
  1740 		}
  1741 		
  1742 	} else {
  1743 		AIChatLog       *theLog = [currentSearchResults objectAtIndex:row];
  1744 
  1745 		if ([identifier isEqualToString:@"To"]) {
  1746 			// Get ListObject for to-UID
  1747 			AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:[theLog serviceClass]
  1748 																																		UID:[theLog to]]];
  1749 			if (listObject) {
  1750 				//Use the longDisplayName, following the user's contact list preferences as this is presumably how she wants to view contacts' names.
  1751 				if (![listObject.displayName isEqualToString:listObject.UID]) {
  1752 					value = [NSString stringWithFormat:@"%@ (%@)", listObject.displayName, listObject.UID];
  1753 				} else {
  1754 					value = listObject.formattedUID;
  1755 				}
  1756 
  1757 			} else {
  1758 				//No username available
  1759 				value = [theLog to];
  1760 			}
  1761 			
  1762 		} else if ([identifier isEqualToString:@"From"]) {
  1763 			value = [theLog from];
  1764 			
  1765 		} else if ([identifier isEqualToString:@"Date"]) {
  1766 			value = [theLog date];
  1767 			
  1768 		} else if ([identifier isEqualToString:@"Service"]) {
  1769 			NSString	*serviceClass;
  1770 			NSImage		*image;
  1771 			
  1772 			serviceClass = [theLog serviceClass];
  1773 			image = [AIServiceIcons serviceIconForService:[adium.accountController firstServiceWithServiceID:serviceClass]
  1774 													 type:AIServiceIconSmall
  1775 												direction:AIIconNormal];
  1776 			value = (image ? image : blankImage);
  1777 		}
  1778     }
  1779     [resultsLock unlock];
  1780     
  1781     return value;
  1782 }
  1783 
  1784 - (void)tableViewSelectionDidChange:(NSNotification *)notification
  1785 {
  1786 	[NSObject cancelPreviousPerformRequestsWithTarget:self
  1787 											 selector:@selector(tableViewSelectionDidChangeDelayed)
  1788 											   object:nil];
  1789 	
  1790 	[self performSelector:@selector(tableViewSelectionDidChangeDelayed)
  1791 			   withObject:nil
  1792 			   afterDelay:0.05];
  1793 }
  1794 
  1795 - (void)tableViewSelectionDidChangeDelayed
  1796 {
  1797     if (!ignoreSelectionChange) {
  1798 		NSArray		*selectedLogs;
  1799 		
  1800 		//Update the displayed log
  1801 		automaticSearch = NO;
  1802 		
  1803 		[resultsLock lock];
  1804 		selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
  1805 		[resultsLock unlock];
  1806 		
  1807 		[self displayLogs:selectedLogs];
  1808     }
  1809 }
  1810 
  1811 //Sort the log array & reflect the new column
  1812 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
  1813 {    
  1814     [self sortCurrentSearchResultsForTableColumn:tableColumn
  1815                                    direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
  1816 }
  1817 
  1818 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
  1819 {
  1820 	[resultsLock lock];
  1821 	NSArray *selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
  1822 	[resultsLock unlock];
  1823 	
  1824 	if ([selectedLogs count] > 0) {
  1825 		NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
  1826 		[alert beginSheetModalForWindow:[self window] 
  1827 						  modalDelegate:self 
  1828 						 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:) 
  1829 							contextInfo:[selectedLogs retain]];
  1830 	}
  1831 }
  1832 
  1833 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
  1834 {
  1835 	NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
  1836 
  1837 	if (!aNotification ||
  1838 		([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
  1839 		NSDateFormatter *dateFormatter;
  1840 		NSCell			*cell = [dateTableColumn dataCell];
  1841 
  1842 		[cell setObjectValue:[NSDate date]];
  1843 
  1844 		CGFloat width = [dateTableColumn width];
  1845 
  1846 #define NUMBER_TIME_STYLES	2
  1847 #define NUMBER_DATE_STYLES	4
  1848 		NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
  1849 		NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
  1850 		CGFloat requiredWidth;
  1851 
  1852 		dateFormatter = [cell formatter];
  1853 		if (!dateFormatter) {
  1854 			dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
  1855 			[dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
  1856 			[cell setFormatter:dateFormatter];
  1857 		}
  1858 		
  1859 		requiredWidth = width + 1;
  1860 		for (NSInteger i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
  1861 			[dateFormatter setTimeStyle:timeFormatterStyles[i]];
  1862 
  1863 			for (NSInteger j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
  1864 				[dateFormatter setDateStyle:formatterStyles[j]];
  1865 				requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
  1866 				//Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
  1867 				requiredWidth += 3;					
  1868 			}
  1869 		}
  1870 	}
  1871 }
  1872 
  1873 - (IBAction)toggleEmoticonFiltering:(id)sender
  1874 {
  1875 	showEmoticons = !showEmoticons;
  1876 	[sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
  1877 	[sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
  1878 
  1879 	[self displayLogs:displayedLogArray];
  1880 }
  1881 
  1882 - (IBAction)toggleTimestampFiltering:(id)sender
  1883 {
  1884 	showTimestamps = !showTimestamps;
  1885 	[sender setLabel:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)];
  1886 	[sender setImage:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]];
  1887 
  1888 	[self displayLogs:displayedLogArray];
  1889 }
  1890 
  1891 #pragma mark Outline View Data source
  1892 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
  1893 {
  1894 	if (!item) {
  1895 		if (index == 0) {
  1896 			return allContactsIdentifier;
  1897 
  1898 		} else {
  1899 			return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
  1900 		}
  1901 
  1902 	} else {
  1903 		if ([item isKindOfClass:[AIMetaContact class]]) {
  1904 			return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
  1905 		}
  1906 	}
  1907 	
  1908 	return nil;
  1909 }
  1910 
  1911 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
  1912 {
  1913 	return (!item || 
  1914 			([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
  1915 			[item isKindOfClass:[NSArray class]]);
  1916 }
  1917 
  1918 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
  1919 {
  1920 	if (!item) {
  1921 		return [toArray count] + 1; //+1 for the All item
  1922 
  1923 	} else if ([item isKindOfClass:[AIMetaContact class]]) {
  1924 		NSUInteger count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
  1925 		if (count > 1)
  1926 			return count;
  1927 		else
  1928 			return 0;
  1929 
  1930 	} else {
  1931 		return 0;
  1932 	}
  1933 }
  1934 
  1935 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
  1936 {
  1937 	Class itemClass = [item class];
  1938 
  1939 	if (itemClass == [AIMetaContact class]) {
  1940 		return [(AIMetaContact *)item longDisplayName];
  1941 		
  1942 	} else if (itemClass == [AIListContact class]) {
  1943 		if ([(AIListContact *)item parentContact] != item) {
  1944 			//This contact is within a metacontact - always show its UID
  1945 			return [(AIListContact *)item formattedUID];
  1946 		} else {
  1947 			return [(AIListContact *)item longDisplayName];
  1948 		} 
  1949 		
  1950 	} else if (itemClass == [AILogToGroup class]) {
  1951 		return [(AILogToGroup *)item to];
  1952 		
  1953 	} else if (itemClass == [allContactsIdentifier class]) {
  1954 		NSUInteger contactCount = [toArray count];
  1955 		return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
  1956 			((contactCount == 1) ?
  1957 			 AILocalizedString(@"1 Contact", nil) :
  1958 			 [NSString stringWithFormat:AILocalizedString(@"%lu Contacts", nil), contactCount])]; 
  1959 
  1960 	} else if (itemClass == [NSString class]) {
  1961 		return item;
  1962 
  1963 	} else {
  1964 		NSLog(@"%@: no idea",item);
  1965 		return nil;
  1966 	}
  1967 }
  1968 
  1969 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
  1970 {
  1971 	if ([item isKindOfClass:[AIMetaContact class]] &&
  1972 		[[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
  1973 		/* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
  1974 		 * If it has multiple contacts, use no icon unless a user icon is present.
  1975 		 */
  1976 		NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
  1977 														size:NSMakeSize(16,16)];
  1978 		if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
  1979 
  1980 		[cell setImage:image];
  1981 
  1982 	} else if ([item isKindOfClass:[AIListContact class]]) {
  1983 		NSImage	*image = [AIUserIcons listUserIconForContact:(AIListContact *)item
  1984 														size:NSMakeSize(16,16)];
  1985 		if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
  1986 															type:AIServiceIconSmall
  1987 													   direction:AIIconFlipped];
  1988 		[cell setImage:image];
  1989 
  1990 	} else if ([item isKindOfClass:[AILogToGroup class]]) {
  1991 		[cell setImage:[AIServiceIcons serviceIconForService:[adium.accountController firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]]
  1992 														type:AIServiceIconSmall
  1993 												   direction:AIIconNormal]];
  1994 		
  1995 	} else if ([item isKindOfClass:[allContactsIdentifier class]]) {
  1996 		if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
  1997 			([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
  1998 			if (!adiumIconHighlighted) {
  1999 				adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
  2000 												   forClass:[self class]] retain];
  2001 			}
  2002 
  2003 			[cell setImage:adiumIconHighlighted];
  2004 
  2005 		} else {
  2006 			if (!adiumIcon) {
  2007 				adiumIcon = [[NSImage imageNamed:@"adium"
  2008 										forClass:[self class]] retain];
  2009 			}
  2010 
  2011 			[cell setImage:adiumIcon];
  2012 		}
  2013 
  2014 	} else if ([item isKindOfClass:[NSString class]]) {
  2015 		[cell setImage:nil];
  2016 		
  2017 	} else {
  2018 		NSLog(@"%@: no idea",item);
  2019 		[cell setImage:nil];
  2020 	}	
  2021 }
  2022 
  2023 /*
  2024  * @brief Is item supposed to have a divider below?
  2025  *
  2026  */
  2027 - (AIDividerPosition)outlineView:(NSOutlineView*)outlineView dividerPositionForItem:(id)item
  2028 {
  2029 	if ([item isKindOfClass:[allContactsIdentifier class]]) {
  2030 		return AIDividerPositionBelow;
  2031 	} else {
  2032 		return AIDividerPositionNone;
  2033 	}
  2034 }
  2035 
  2036 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
  2037 {
  2038 	[self deleteSelection:nil];
  2039 }
  2040 
  2041 
  2042 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
  2043 {
  2044 	[NSObject cancelPreviousPerformRequestsWithTarget:self
  2045 											 selector:@selector(outlineViewSelectionDidChangeDelayed)
  2046 											   object:nil];
  2047 	
  2048 	[self performSelector:@selector(outlineViewSelectionDidChangeDelayed)
  2049 			   withObject:nil
  2050 			   afterDelay:0.05];
  2051 }
  2052 
  2053 - (void)outlineViewSelectionDidChangeDelayed
  2054 {
  2055 	NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
  2056 
  2057 	[contactIDsToFilter removeAllObjects];
  2058 
  2059 	if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
  2060 		id		item;
  2061 
  2062 		for (item in selectedItems) {
  2063 			if ([item isKindOfClass:[AIMetaContact class]]) {
  2064 				for (AIListContact *contact in [(AIMetaContact *)item listContactsIncludingOfflineAccounts]) {
  2065 					[contactIDsToFilter addObject:
  2066 						[[[NSString stringWithFormat:@"%@.%@", contact.service.serviceID, contact.UID] compactedString] safeFilenameString]];
  2067 				}
  2068 				
  2069 			} else if ([item isKindOfClass:[AIListContact class]]) {
  2070 				[contactIDsToFilter addObject:
  2071 					[[[NSString stringWithFormat:@"%@.%@",((AIListContact *)item).service.serviceID,((AIListContact *)item).UID] compactedString] safeFilenameString]];
  2072 				
  2073 			} else if ([item isKindOfClass:[AILogToGroup class]]) {
  2074 				[contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]]; 
  2075 			}
  2076 		}
  2077 	}
  2078 	
  2079 	[self startSearchingClearingCurrentResults:YES];
  2080 }
  2081 
  2082 - (NSMenu *)outlineView:(NSOutlineView *)outlineView menuForEvent:(NSEvent *)theEvent;
  2083 {
  2084 	if (outlineView == outlineView_contacts) {
  2085 		NSInteger clickedRow = [outlineView_contacts rowAtPoint:[outlineView_contacts convertPoint:[theEvent locationInWindow]
  2086 																					fromView:nil]];
  2087 		id item = [outlineView_contacts itemAtRow:clickedRow];
  2088 
  2089 		//If we have a To group, see if we can make a contact out of it
  2090 		if ([item isKindOfClass:[AILogToGroup class]]) {
  2091 			if ([(AILogToGroup *)item to] && [(AILogToGroup *)item serviceClass]) {
  2092 				//We need a service with ther right service ID
  2093 				AIService *service = [adium.accountController firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]];
  2094 				if (service) {
  2095 					//Next, we want an online account
  2096 					AIAccount *account = nil;
  2097 					for (account in [adium.accountController accountsCompatibleWithService:service]) {
  2098 						if (account.online) break;
  2099 					}
  2100 					
  2101 					if (account) {
  2102 						//Finally, make a contact
  2103 						item = [adium.contactController contactWithService:service
  2104 																	 account:account
  2105 																		 UID:[(AILogToGroup *)item to]];
  2106 					}
  2107 					
  2108 				}
  2109 			}
  2110 		}
  2111 
  2112 		if ([item isKindOfClass:[AIListContact class]]) {
  2113 			NSArray			*locationsArray = [NSArray arrayWithObjects:
  2114 				[NSNumber numberWithInteger:Context_Contact_Message],
  2115 				[NSNumber numberWithInteger:Context_Contact_Manage],
  2116 				[NSNumber numberWithInteger:Context_Contact_Action],
  2117 				[NSNumber numberWithInteger:Context_Contact_ListAction],
  2118 				[NSNumber numberWithInteger:Context_Contact_NegativeAction],
  2119 				[NSNumber numberWithInteger:Context_Contact_Additions], nil];
  2120 
  2121 			return [adium.menuController contextualMenuWithLocations:locationsArray
  2122 														 forListObject:(AIListContact *)item];
  2123 		}
  2124 	}
  2125 	
  2126 	return nil;
  2127 }
  2128 
  2129 static NSInteger toArraySort(id itemA, id itemB, void *context)
  2130 {
  2131 	NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
  2132 	NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
  2133 	NSComparisonResult result = [nameA caseInsensitiveCompare:nameB];
  2134 	if (result == NSOrderedSame) result = [nameA compare:nameB];
  2135 
  2136 	return result;
  2137 }
  2138 
  2139 - (void)draggedDividerRightBy:(CGFloat)deltaX
  2140 {	
  2141 	desiredContactsSourceListDeltaX = deltaX;
  2142 	[splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
  2143 	desiredContactsSourceListDeltaX = 0;
  2144 }
  2145 
  2146 /*
  2147 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
  2148 {
  2149 	if ((sender == splitView_contacts_results) &&
  2150 		desiredContactsSourceListDeltaX != 0) {
  2151 		float dividerThickness = [sender dividerThickness];
  2152 
  2153 		NSRect newFrame = [sender frame];		
  2154 		NSRect leftFrame = [containingView_contactsSourceList frame]; 
  2155 		NSRect rightFrame = [containingView_results frame];
  2156 
  2157 		leftFrame.size.width += desiredContactsSourceListDeltaX; 
  2158 		leftFrame.size.height = newFrame.size.height;
  2159 		leftFrame.origin = NSMakePoint(0,0);
  2160 
  2161 		rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
  2162 		rightFrame.size.height = newFrame.size.height;
  2163 		rightFrame.origin.x = leftFrame.size.width + dividerThickness;
  2164 
  2165 		[containingView_contactsSourceList setFrame:leftFrame];
  2166 		[containingView_contactsSourceList setNeedsDisplay:YES];
  2167 		[containingView_results setFrame:rightFrame];
  2168 		[containingView_results setNeedsDisplay:YES];
  2169 
  2170 	} else {
  2171 		//Perform the default implementation
  2172 		[sender adjustSubviews];
  2173 	}
  2174 }
  2175 */
  2176 
  2177 //Window Toolbar -------------------------------------------------------------------------------------------------------
  2178 #pragma mark Window Toolbar
  2179 
  2180 - (void)installToolbar
  2181 {	
  2182 	[NSBundle loadNibNamed:[self dateItemNibName] owner:self];
  2183 
  2184     NSToolbar 		*toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
  2185     NSToolbarItem	*toolbarItem;
  2186 	
  2187     [toolbar setDelegate:self];
  2188     [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
  2189     [toolbar setSizeMode:NSToolbarSizeModeRegular];
  2190     [toolbar setVisible:YES];
  2191     [toolbar setAllowsUserCustomization:YES];
  2192     [toolbar setAutosavesConfiguration:YES];
  2193     toolbarItems = [[NSMutableDictionary alloc] init];
  2194 
  2195 	//Delete Logs
  2196 	[AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
  2197                                         withIdentifier:@"delete"
  2198                                                  label:DELETE
  2199                                           paletteLabel:DELETE
  2200                                                toolTip:AILocalizedString(@"Delete the selection",nil)
  2201                                                 target:self
  2202                                        settingSelector:@selector(setImage:)
  2203                                            itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
  2204                                                 action:@selector(deleteSelection:)
  2205                                                   menu:nil];
  2206 	
  2207 	//Search
  2208 	[self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
  2209 	toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
  2210 														  label:SEARCH
  2211 												   paletteLabel:SEARCH
  2212 														toolTip:AILocalizedString(@"Search or filter logs",nil)
  2213 														 target:self
  2214 												settingSelector:@selector(setView:)
  2215 													itemContent:view_SearchField
  2216 														 action:@selector(updateSearch:)
  2217 														   menu:nil];
  2218 	if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
  2219 		[toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
  2220 	}
  2221 	[toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
  2222 	[toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
  2223 	[toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
  2224 
  2225 	toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
  2226 														  label:AILocalizedString(@"Date", nil)
  2227 												   paletteLabel:AILocalizedString(@"Date", nil)
  2228 														toolTip:AILocalizedString(@"Filter logs by date",nil)
  2229 														 target:self
  2230 												settingSelector:@selector(setView:)
  2231 													itemContent:view_DatePicker
  2232 														 action:nil
  2233 														   menu:nil];
  2234 	if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
  2235 		[toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
  2236 	}
  2237 	[toolbarItem setMinSize:[view_DatePicker frame].size];
  2238 	[toolbarItem setMaxSize:[view_DatePicker frame].size];
  2239 	[toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
  2240 
  2241 	//Toggle Emoticons
  2242 	[AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
  2243 									withIdentifier:@"toggleemoticons"
  2244 											 label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
  2245 									  paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
  2246 										   toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
  2247 											target:self
  2248 								   settingSelector:@selector(setImage:)
  2249 									   itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
  2250 											action:@selector(toggleEmoticonFiltering:)
  2251 											  menu:nil];
  2252 	// Toggle Timestamps
  2253 	[AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
  2254 																	withIdentifier:@"toggletimestamps"
  2255 																					 label:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)
  2256 																		paletteLabel:AILocalizedString(@"Show/Hide Timestamps", nil)
  2257 																				 toolTip:AILocalizedString(@"Show or hide timestamps in logs", nil)
  2258 																				  target:self
  2259 																 settingSelector:@selector(setImage:)
  2260 																		 itemContent:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]
  2261 																					action:@selector(toggleTimestampFiltering:)
  2262 																						menu:nil];
  2263 
  2264 	[[self window] setToolbar:toolbar];
  2265 
  2266 	[self configureDateFilter];
  2267 }
  2268 
  2269 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
  2270 {
  2271     return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
  2272 }
  2273 
  2274 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
  2275 {
  2276     return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
  2277 		@"delete", @"toggleemoticons", @"toggletimestamps", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
  2278 		@"search", nil];
  2279 }
  2280 
  2281 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
  2282 {
  2283     return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
  2284 		[NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
  2285 			NSToolbarSpaceItemIdentifier,
  2286 			NSToolbarFlexibleSpaceItemIdentifier,
  2287 			NSToolbarCustomizeToolbarItemIdentifier, 
  2288 			NSToolbarPrintItemIdentifier, nil]];
  2289 }
  2290 
  2291 - (void)toolbarWillAddItem:(NSNotification *)notification
  2292 {
  2293 	NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
  2294 	if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
  2295 		[item setTarget:self];
  2296 		[item setAction:@selector(adiumPrint:)];
  2297 	}
  2298 }
  2299 
  2300 #pragma mark Date filter
  2301 
  2302 /*!
  2303  * @brief Returns a menu item for the date type filter menu
  2304  */
  2305 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
  2306 {
  2307     NSMenuItem  *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInteger:dateType]] 
  2308 																				 action:@selector(selectDateType:) 
  2309 																		  keyEquivalent:@""];
  2310     [menuItem setTag:dateType];
  2311     
  2312     return [menuItem autorelease];
  2313 }
  2314 
  2315 - (NSInteger)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
  2316 {
  2317 	NSInteger todayDayOfWeek = [today dayOfWeek];
  2318 
  2319 	//Try to look at the iCal preferences if possible
  2320 	if (!iCalFirstDayOfWeekDetermined) {
  2321 		CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
  2322 		if (iCalFirstDayOfWeek) {
  2323 			//This should return a CFNumberRef... we're using another app's prefs, so make sure.
  2324 			if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
  2325 				firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek integerValue];
  2326 			}
  2327 
  2328 			CFRelease(iCalFirstDayOfWeek);
  2329 		}
  2330 
  2331 		//Don't check again
  2332 		iCalFirstDayOfWeekDetermined = YES;
  2333 	}
  2334 
  2335 	return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
  2336 }
  2337 
  2338 /*!
  2339  * @brief Select the date type
  2340  */
  2341 - (void)selectDateType:(id)sender
  2342 {
  2343 	[self selectedDateType:[sender tag]];
  2344 	[self startSearchingClearingCurrentResults:YES];
  2345 }
  2346 
  2347 #pragma mark Open Log
  2348 
  2349 - (void)openLogAtPath:(NSString *)inPath
  2350 {
  2351 	AIChatLog   *chatLog = nil;
  2352 	NSString	*basePath = [AILoggerPlugin logBasePath];
  2353 
  2354 	//inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
  2355 	NSArray		*pathComponents = [inPath pathComponents];
  2356 	NSInteger			lastIndex = [pathComponents count];
  2357 	NSString	*logName = [pathComponents objectAtIndex:--lastIndex];
  2358 	NSString	*contactName = [pathComponents objectAtIndex:--lastIndex];
  2359 	NSString	*serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];	
  2360 	NSString		*relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
  2361 
  2362 	NSString	*serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
  2363 	//Filter for logs from the contact associated with the log we're loading
  2364 	[self filterForContact:[adium.contactController contactWithService:[adium.accountController firstServiceWithServiceID:serviceID]
  2365 																 account:nil
  2366 																	 UID:contactName]];
  2367 	
  2368 	NSString *canonicalBasePath = [basePath stringByStandardizingPath];
  2369 	NSString *canonicalInPath = [inPath stringByStandardizingPath];
  2370 
  2371 	if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
  2372 		AILogToGroup	*logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
  2373 		
  2374 		chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
  2375 		
  2376 	} else {
  2377 		/* Different Adium user... this sucks. We're given a path like this:
  2378 		 *	/Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
  2379 		 * and we want to make it relative to our current user's logs folder, which might be
  2380 		 *  /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
  2381 		 *
  2382 		 * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
  2383 		 */
  2384 		NSString	*fakeRelativePath = @"";
  2385 		
  2386 		//Use .. to get back to the root from the base path
  2387 		NSInteger componentsOfBasePath = [[canonicalBasePath pathComponents] count];
  2388 		for (NSInteger i = 0; i < componentsOfBasePath; i++) {
  2389 			fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
  2390 		}
  2391 		
  2392 		//Now add the path from the root to the actual log
  2393 		fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
  2394 		chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
  2395 											  from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
  2396 												to:contactName
  2397 									  serviceClass:serviceID] autorelease];
  2398 	}
  2399 
  2400 	//Now display the requested log
  2401 	if (chatLog) {
  2402 		[self displayLog:chatLog];
  2403 	}
  2404 }
  2405 
  2406 #pragma mark Printing
  2407 
  2408 - (void)adiumPrint:(id)sender
  2409 {
  2410 	NSTextView			*printView;
  2411     NSPrintOperation    *printOperation;
  2412     NSPrintInfo			*printInfo = [NSPrintInfo sharedPrintInfo];
  2413 
  2414     [printInfo setHorizontalPagination:NSFitPagination];
  2415     [printInfo setHorizontallyCentered:NO];
  2416     [printInfo setVerticallyCentered:NO];
  2417     
  2418 	printView = [[NSTextView alloc] initWithFrame:[[NSPrintInfo sharedPrintInfo] imageablePageBounds]];
  2419     [printView setVerticallyResizable:YES];
  2420     [printView setHorizontallyResizable:NO];
  2421 	
  2422     [[printView textStorage] setAttributedString:[textView_content textStorage]];
  2423 	
  2424     printOperation = [NSPrintOperation printOperationWithView:printView printInfo:printInfo];
  2425     [printOperation runOperationModalForWindow:[self window] delegate:nil
  2426 								didRunSelector:NULL contextInfo:NULL];
  2427 	[printView release];
  2428 }
  2429 
  2430 - (BOOL)validatePrintMenuItem:(NSMenuItem *)menuItem
  2431 {
  2432 	return ([displayedLogArray count] > 0);
  2433 }
  2434 
  2435 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
  2436 {
  2437 	if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
  2438 		return [self validatePrintMenuItem:nil];
  2439 
  2440 	} else {
  2441 		return YES;
  2442 	}
  2443 }
  2444 
  2445 - (void)selectCachedIndex
  2446 {
  2447 	NSInteger numberOfRows = [tableView_results numberOfRows];
  2448 	
  2449 	if (cachedSelectionIndex <  numberOfRows) {
  2450 		[tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
  2451 					   byExtendingSelection:NO];
  2452 	} else {
  2453 		if (numberOfRows)
  2454 			[tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
  2455 						   byExtendingSelection:NO];			
  2456 	}
  2457 
  2458 	if (numberOfRows) {
  2459 		[tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
  2460 	}
  2461 
  2462 	deleteOccurred = NO;
  2463 }
  2464 
  2465 #pragma mark Deletion
  2466 
  2467 /*!
  2468  * @brief Get an NSAlert to request deletion of multiple logs
  2469  */
  2470 - (NSAlert *)alertForDeletionOfLogCount:(NSUInteger)logCount
  2471 {
  2472 	NSAlert *alert = [[NSAlert alloc] init];
  2473 	[alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
  2474 	[alert setInformativeText:[NSString stringWithFormat:
  2475 		AILocalizedString(@"Are you sure you want to send %lu logs to the Trash?",nil), logCount]];
  2476 	[alert addButtonWithTitle:DELETE]; 
  2477 	[alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
  2478 	
  2479 	return [alert autorelease];
  2480 }
  2481 
  2482 /*!
  2483  * @brief Undo the deletion of one or more AIChatLogs
  2484  *
  2485  * The logs will be marked for readdition to the index
  2486  */
  2487 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
  2488 {
  2489 	AIChatLog		*aLog;
  2490 	NSFileManager	*fileManager = [NSFileManager defaultManager];
  2491 	NSString		*trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
  2492 
  2493 	for (aLog in deletedLogs) {
  2494 		NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
  2495 		
  2496 		[fileManager createDirectoryAtPath:[logPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL];
  2497 		
  2498 		[fileManager moveItemAtPath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
  2499 							 toPath:logPath 
  2500 							  error:NULL];
  2501 		
  2502 		[plugin markLogDirtyAtPath:logPath];
  2503 	}
  2504 	
  2505 	[self rebuildIndices];
  2506 }
  2507 
  2508 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode  contextInfo:(void *)contextInfo;
  2509 {
  2510 	NSArray *selectedLogs = (NSArray *)contextInfo;
  2511 	if (returnCode == NSAlertFirstButtonReturn) {
  2512 		[resultsLock lock];
  2513 		
  2514 		AIChatLog		*aLog;
  2515 		NSMutableSet	*logPaths = [NSMutableSet set];
  2516 		
  2517 		cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
  2518 		
  2519 		for (aLog in selectedLogs) {
  2520 			NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
  2521 			
  2522 			[[NSNotificationCenter defaultCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
  2523 			AILogToGroup	*logToGroup = [logToGroupDict objectForKey:[[aLog relativePath] stringByDeletingLastPathComponent]];
  2524 
  2525 			// Success will be unused in deployment builds as AILog turns to nothing
  2526 #ifdef DEBUG_BUILD
  2527 			BOOL success = [logToGroup trashLog:aLog];
  2528 			AILog(@"Trashing %@: %i",[aLog relativePath], success);
  2529 #else
  2530 			[logToGroup trashLog:aLog];
  2531 #endif
  2532 			//Clear the to group out if it no longer has anything of interest
  2533 			if ([logToGroup logCount] == 0) {
  2534 				AILogFromGroup	*logFromGroup = [logFromGroupDict objectForKey:[[[aLog relativePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent]];
  2535 				[logFromGroup removeToGroup:logToGroup];
  2536 			}
  2537 
  2538 			[logPaths addObject:logPath];
  2539 			[currentSearchResults removeObjectIdenticalTo:aLog];
  2540 		}
  2541 		
  2542 		[plugin removePathsFromIndex:logPaths];
  2543 		
  2544 		[undoManager registerUndoWithTarget:self
  2545 								   selector:@selector(restoreDeletedLogs:)
  2546 									 object:selectedLogs];
  2547 		[undoManager setActionName:DELETE];
  2548 		
  2549 		[resultsLock unlock];
  2550 		[tableView_results reloadData];
  2551 		
  2552 		deleteOccurred = YES;
  2553 		
  2554 		[self rebuildContactsList];
  2555 		[self updateProgressDisplay];
  2556 	}
  2557 	[selectedLogs release];
  2558 }
  2559 
  2560 /*!
  2561  * @brief Delete logs
  2562  *
  2563  * If two or more logs are passed, confirmation will be requested.
  2564  * This operation registers with the window controller's undo manager.
  2565  *
  2566  * @param selectedLogs An NSArray of logs to delete
  2567  */
  2568 - (void)deleteLogs:(NSArray *)selectedLogs
  2569 {	
  2570 	if ([selectedLogs count] > 1) {
  2571 		NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
  2572 		[alert beginSheetModalForWindow:[self window]
  2573 						  modalDelegate:self
  2574 						 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
  2575 							contextInfo:[selectedLogs retain]];
  2576 	} else if ([selectedLogs count] == 1) {
  2577 		[self deleteLogsAlertDidEnd:nil
  2578 						 returnCode:NSAlertFirstButtonReturn
  2579 						contextInfo:[selectedLogs retain]];
  2580 	}
  2581 }
  2582 
  2583 /*!
  2584  * @brief Returns a set of all selected to groups on all accounts
  2585  *
  2586  * @param totalLogCount If non-NULL, will be set to the total number of logs on return
  2587  */
  2588 - (NSArray *)allSelectedToGroups:(NSInteger *)totalLogCount
  2589 {
  2590     NSEnumerator        *fromEnumerator;
  2591     AILogFromGroup      *fromGroup;
  2592 	NSMutableArray		*allToGroups = [NSMutableArray array];
  2593 
  2594 	if (totalLogCount) *totalLogCount = 0;
  2595 
  2596     //Walk through every 'from' group
  2597     fromEnumerator = [logFromGroupDict objectEnumerator];
  2598     while ((fromGroup = [fromEnumerator nextObject])) {
  2599 		NSEnumerator        *toEnumerator;
  2600 		AILogToGroup        *toGroup;
  2601 
  2602 		//Walk through every 'to' group
  2603 		toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
  2604 		while ((toGroup = [toEnumerator nextObject])) {
  2605 			if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
  2606 				if (totalLogCount) {
  2607 					*totalLogCount += [toGroup logCount];
  2608 				}
  2609 				
  2610 				[allToGroups addObject:toGroup];
  2611 			}
  2612 		}
  2613 	}
  2614 
  2615 	return allToGroups;
  2616 }
  2617 
  2618 /*!
  2619  * @brief Undo the deletion of one or more AILogToGroups and their associated logs
  2620  *
  2621  * The logs will be marked for readdition to the index
  2622  */
  2623 - (void)restoreDeletedToGroups:(NSArray *)toGroups
  2624 {
  2625 	AILogToGroup	*toGroup;
  2626 	NSFileManager	*fileManager = [NSFileManager defaultManager];
  2627 	NSString		*trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
  2628 	NSString		*logBasePath = [AILoggerPlugin logBasePath];
  2629 
  2630 	for (toGroup in toGroups) {
  2631 		NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup relativePath]];
  2632 
  2633 		[fileManager createDirectoryAtPath:[toGroupPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL];
  2634 		if ([fileManager fileExistsAtPath:toGroupPath]) {
  2635 			AILog(@"Removing path %@ to make way for %@",
  2636 				  toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
  2637 			[fileManager removeItemAtPath:toGroupPath
  2638 									error:NULL];
  2639 		}
  2640 		[fileManager moveItemAtPath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
  2641 							 toPath:toGroupPath
  2642 							  error:NULL];
  2643 		
  2644 		NSEnumerator *logEnumerator = [toGroup logEnumerator];
  2645 		AIChatLog	 *aLog;
  2646 	
  2647 		while ((aLog = [logEnumerator nextObject])) {
  2648 			[plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog relativePath]]];
  2649 		}
  2650 	}
  2651 	
  2652 	[self rebuildIndices];	
  2653 }
  2654 
  2655 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
  2656 {
  2657 	NSArray *allSelectedToGroups = (NSArray *)contextInfo;
  2658 	if (returnCode == NSAlertFirstButtonReturn) {
  2659 		AILogToGroup	*logToGroup;
  2660 		NSMutableSet	*logPaths = [NSMutableSet set];
  2661 		
  2662 		for (logToGroup in allSelectedToGroups) {
  2663 			NSEnumerator *logEnumerator;
  2664 			AIChatLog	 *aLog;
  2665 			
  2666 			logEnumerator = [logToGroup logEnumerator];
  2667 			while ((aLog = [logEnumerator nextObject])) {
  2668 				NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
  2669 				[logPaths addObject:logPath];
  2670 			}
  2671 			
  2672 			AILogFromGroup	*logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
  2673 			[logFromGroup removeToGroup:logToGroup];
  2674 		}
  2675 		
  2676 		[plugin removePathsFromIndex:logPaths];
  2677 		
  2678 		[undoManager registerUndoWithTarget:self
  2679 								   selector:@selector(restoreDeletedToGroups:)
  2680 									 object:allSelectedToGroups];
  2681 		[undoManager setActionName:DELETE];
  2682 		
  2683 		[self rebuildIndices];
  2684 		[self updateProgressDisplay];
  2685 	}
  2686 	
  2687 	[allSelectedToGroups release];
  2688 }
  2689 
  2690 /*!
  2691  * @brief Delete entirely the logs of all contacts selected in the source list
  2692  *
  2693  * Confirmation by the user will be required.
  2694  *
  2695  * Note: A single item in the source list may have multiple associated AILogToGroups.
  2696  */
  2697 - (void)deleteSelectedContactsFromSourceList
  2698 {
  2699 	NSInteger totalLogCount;
  2700 	NSArray *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
  2701 
  2702 	if (totalLogCount > 1) {
  2703 		NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
  2704 		[alert beginSheetModalForWindow:[self window]
  2705 						  modalDelegate:self
  2706 						 didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
  2707 							contextInfo:[allSelectedToGroups retain]];
  2708 	} else {
  2709 		[self deleteSelectedContactsFromSourceListAlertDidEnd:nil
  2710 												   returnCode:NSAlertFirstButtonReturn
  2711 												  contextInfo:[allSelectedToGroups retain]];
  2712 	}
  2713 }
  2714 
  2715 /*!
  2716  * @brief Delete the current selection
  2717  *
  2718  * If the contacts outline view is selected, one or more contacts' logs will be trashed.
  2719  * If anything else is selected, the currently selected search result logs will be trashed.
  2720  */
  2721 - (void)deleteSelection:(id)sender
  2722 {
  2723 	if ([[self window] firstResponder] == outlineView_contacts) {
  2724 		[self deleteSelectedContactsFromSourceList];
  2725 		
  2726 	} else {
  2727 		[resultsLock lock];
  2728 		NSArray *selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
  2729 		[resultsLock unlock];
  2730 		
  2731 		[self deleteLogs:selectedLogs];
  2732 	}
  2733 }
  2734 
  2735 #pragma mark Undo
  2736 /*!
  2737  * @brief Supply our undo manager when we are within the responder chain
  2738  */
  2739 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
  2740 {
  2741 	return undoManager;
  2742 }
  2743 
  2744 #pragma mark Gestures
  2745 /*!
  2746  * @brief Responds to a swipe gesture
  2747  *
  2748  * This is a private method added in AppKit 949.18.0.
  2749  */
  2750 - (void)swipeWithEvent:(NSEvent *)inEvent
  2751 {
  2752 	NSTableView *targetTableView;
  2753 	NSInteger changeValue, nextSelected;
  2754 
  2755 	if ([inEvent deltaY] == 0) {
  2756 		// For horizontal swipes, switch between individual logs.
  2757 		targetTableView = tableView_results;
  2758 		changeValue = [inEvent deltaX];
  2759 		// Lock the results when we're dealing with the logs tableView
  2760 		[resultsLock lock];
  2761 	} else {
  2762 		// For vertical swipes, switch between contacts.
  2763 		targetTableView = outlineView_contacts;
  2764 		changeValue = [inEvent deltaY];
  2765 	}
  2766 	
  2767 	// Swipe; +1f is left/up, -1f is right/down
  2768 	
  2769 	// Find the index of the next row to select.
  2770 	if (changeValue == -1) {
  2771 		// Going to the right.
  2772 		nextSelected = [[targetTableView selectedRowIndexes] lastIndex] + 1;
  2773 	} else {
  2774 		// Going to the left.
  2775 		nextSelected = [[targetTableView selectedRowIndexes] firstIndex] - 1;
  2776 	}
  2777 	
  2778 	// Loop around in circles.
  2779 	if (nextSelected >= [targetTableView numberOfRows]) {
  2780 		nextSelected = 0;
  2781 	} else if (nextSelected < 0) {
  2782 		nextSelected = [targetTableView numberOfRows]-1;
  2783 	}
  2784 	
  2785 	// Select either the next row or the previous row.
  2786 	[targetTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:nextSelected]
  2787 				 byExtendingSelection:NO];
  2788 	
  2789 	[targetTableView scrollRowToVisible:nextSelected];
  2790 	
  2791 	if ([inEvent deltaY] == 0)
  2792 		[resultsLock unlock];		
  2793 }
  2794 
  2795 #pragma mark Transcript services special-casing
  2796 NSString *handleSpecialCasesForUIDAndServiceClass(NSString *contactUID, NSString *serviceClass)
  2797 {
  2798 	/* Jabber and its specified derivative services need special handling;
  2799 	 * this is cross-contamination from ESPurpleJabberAccount.
  2800 	 */
  2801 	if ([serviceClass isEqualToString:@"Jabber"] ||
  2802 		[serviceClass isEqualToString:@"GTalk"] ||
  2803 		[serviceClass isEqualToString:@"LiveJournal"]) {
  2804 		
  2805 		if ([contactUID hasSuffix:@"@gmail.com"] ||
  2806 			[contactUID hasSuffix:@"@googlemail.com"]) {
  2807 			serviceClass = @"GTalk";
  2808 			
  2809 		} else if ([contactUID hasSuffix:@"@livejournal.com"]){
  2810 			serviceClass = @"LiveJournal";
  2811 			
  2812 		} else {
  2813 			serviceClass = @"Jabber";
  2814 		}	
  2815 		
  2816 		/* OSCAR and its specified derivative services need special handling;
  2817 		 *  this is cross-contamination from CBPurpleOscarAccount.
  2818 		 */
  2819 	} else if ([serviceClass isEqualToString:@"AIM"] ||
  2820 			   [serviceClass isEqualToString:@"ICQ"] ||
  2821 			   [serviceClass isEqualToString:@"Mac"] ||
  2822 			   [serviceClass isEqualToString:@"MobileMe"]) {
  2823 		const char	firstCharacter = ([contactUID length] ? [contactUID characterAtIndex:0] : '\0');
  2824 		
  2825 		//Determine service based on UID
  2826 		if ([contactUID hasSuffix:@"@mac.com"]) {
  2827 			serviceClass = @"Mac";
  2828 		} else if ([contactUID hasSuffix:@"@me.com"]) {
  2829 			serviceClass = @"MobileMe";
  2830 		} else if (firstCharacter && (firstCharacter >= '0' && firstCharacter <= '9')) {
  2831 			serviceClass = @"ICQ";
  2832 		} else {
  2833 			serviceClass = @"AIM";
  2834 		}
  2835 	}
  2836 	
  2837 	return serviceClass;
  2838 }
  2839 
  2840 #pragma mark Date type menu
  2841 
  2842 - (void)configureDateFilter
  2843 {
  2844 	firstDayOfWeek = 0; /* Sunday */
  2845 	iCalFirstDayOfWeekDetermined = NO;
  2846 	
  2847 	[popUp_dateFilter setMenu:[self dateTypeMenu]];
  2848 	NSInteger index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
  2849 	if(index != NSNotFound)
  2850 		[popUp_dateFilter selectItemAtIndex:index];
  2851 	[self selectedDateType:AIDateTypeAnyDate];
  2852 	
  2853 	[datePicker setDateValue:[NSDate date]];
  2854 }
  2855 
  2856 - (IBAction)selectDate:(id)sender
  2857 {
  2858 	[filterDate release];
  2859 	filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
  2860 	
  2861 	[self startSearchingClearingCurrentResults:YES];
  2862 }
  2863 
  2864 - (NSMenu *)dateTypeMenu
  2865 {
  2866 	NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
  2867 									   AILocalizedString(@"Any Date", nil), [NSNumber numberWithInteger:AIDateTypeAnyDate],
  2868 									   AILocalizedString(@"Today", nil), [NSNumber numberWithInteger:AIDateTypeToday],
  2869 									   AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInteger:AIDateTypeSinceYesterday],
  2870 									   AILocalizedString(@"This Week", nil), [NSNumber numberWithInteger:AIDateTypeThisWeek],
  2871 									   AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInteger:AIDateTypeWithinLastTwoWeeks],
  2872 									   AILocalizedString(@"This Month", nil), [NSNumber numberWithInteger:AIDateTypeThisMonth],
  2873 									   AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInteger:AIDateTypeWithinLastTwoMonths],
  2874 									   nil];
  2875 	NSMenu	*dateTypeMenu = [[NSMenu alloc] init];
  2876 	AIDateType dateType;
  2877 	
  2878 	[dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
  2879 	[dateTypeMenu addItem:[NSMenuItem separatorItem]];
  2880 	
  2881 	for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
  2882 		[dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
  2883 	}
  2884 	
  2885 	dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
  2886 									   AILocalizedString(@"Exactly", nil), [NSNumber numberWithInteger:AIDateTypeExactly],
  2887 									   AILocalizedString(@"Before", nil), [NSNumber numberWithInteger:AIDateTypeBefore],
  2888 									   AILocalizedString(@"After", nil), [NSNumber numberWithInteger:AIDateTypeAfter],
  2889 									   nil];
  2890 	
  2891 	[dateTypeMenu addItem:[NSMenuItem separatorItem]];		
  2892 	
  2893 	for (dateType = AIDateTypeExactly; dateType <= AIDateTypeAfter; dateType++) {
  2894 		[dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
  2895 	}
  2896 	
  2897 	return [dateTypeMenu autorelease];
  2898 }
  2899 
  2900 /*!
  2901  * @brief A new date type was selected
  2902  *
  2903  * The date picker will be hidden/revealed as appropriate.
  2904  * This does not start a search
  2905  */ 
  2906 - (void)selectedDateType:(AIDateType)dateType
  2907 {
  2908 	BOOL			showDatePicker = NO;
  2909 	
  2910 	NSCalendarDate	*today = [NSCalendarDate date];
  2911 	
  2912 	[filterDate release]; filterDate = nil;
  2913 	
  2914 	switch (dateType) {
  2915 		case AIDateTypeAnyDate:
  2916 			filterDateType = AIDateTypeAnyDate;
  2917 			break;
  2918 			
  2919 		case AIDateTypeToday:
  2920 			filterDateType = AIDateTypeExactly;
  2921 			filterDate = [today retain];
  2922 			break;
  2923 			
  2924 		case AIDateTypeSinceYesterday:
  2925 			filterDateType = AIDateTypeAfter;
  2926 			filterDate = [[today dateByAddingYears:0
  2927 											months:0
  2928 											  days:-1
  2929 											 hours:-[today hourOfDay]
  2930 										   minutes:-[today minuteOfHour]
  2931 										   seconds:-([today secondOfMinute] + 1)] retain];
  2932 			break;
  2933 			
  2934 		case AIDateTypeThisWeek:
  2935 			filterDateType = AIDateTypeAfter;
  2936 			filterDate = [[today dateByAddingYears:0
  2937 											months:0
  2938 											  days:-[self daysSinceStartOfWeekGivenToday:today]
  2939 											 hours:-[today hourOfDay]
  2940 										   minutes:-[today minuteOfHour]
  2941 										   seconds:-([today secondOfMinute] + 1)] retain];
  2942 			break;
  2943 			
  2944 		case AIDateTypeWithinLastTwoWeeks:
  2945 			filterDateType = AIDateTypeAfter;
  2946 			filterDate = [[today dateByAddingYears:0
  2947 											months:0
  2948 											  days:-14
  2949 											 hours:-[today hourOfDay]
  2950 										   minutes:-[today minuteOfHour]
  2951 										   seconds:-([today secondOfMinute] + 1)] retain];
  2952 			break;
  2953 			
  2954 		case AIDateTypeThisMonth:
  2955 			filterDateType = AIDateTypeAfter;
  2956 			filterDate = [[[NSCalendarDate date] dateByAddingYears:0
  2957 															months:0
  2958 															  days:-[today dayOfMonth]
  2959 															 hours:0
  2960 														   minutes:0
  2961 														   seconds:-1] retain];
  2962 			break;
  2963 			
  2964 		case AIDateTypeWithinLastTwoMonths:
  2965 			filterDateType = AIDateTypeAfter;
  2966 			filterDate = [[[NSCalendarDate date] dateByAddingYears:0
  2967 															months:-1
  2968 															  days:-[today dayOfMonth]
  2969 															 hours:0
  2970 														   minutes:0
  2971 														   seconds:-1] retain];			
  2972 			break;
  2973 			
  2974 		default:
  2975 			break;
  2976 	}		
  2977 	
  2978 	switch (dateType) {
  2979 		case AIDateTypeExactly:
  2980 			filterDateType = AIDateTypeExactly;
  2981 			filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
  2982 			showDatePicker = YES;
  2983 			break;
  2984 			
  2985 		case AIDateTypeBefore:
  2986 			filterDateType = AIDateTypeBefore;
  2987 			filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
  2988 			showDatePicker = YES;
  2989 			break;
  2990 			
  2991 		case AIDateTypeAfter:
  2992 			filterDateType = AIDateTypeAfter;
  2993 			filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
  2994 			showDatePicker = YES;
  2995 			break;
  2996 			
  2997 		default:
  2998 			showDatePicker = NO;
  2999 			break;
  3000 	}
  3001 	
  3002 	BOOL updateSize = NO;
  3003 	if (showDatePicker && [datePicker isHidden]) {
  3004 		[datePicker setHidden:NO];		
  3005 		updateSize = YES;
  3006 		
  3007 	} else if (!showDatePicker && ![datePicker isHidden]) {
  3008 		[datePicker setHidden:YES];
  3009 		updateSize = YES;
  3010 	}
  3011 	
  3012 	if (updateSize) {
  3013 		NSEnumerator *enumerator = [[[[self window] toolbar] items] objectEnumerator];
  3014 		NSToolbarItem *toolbarItem;
  3015 		while ((toolbarItem = [enumerator nextObject])) {
  3016 			if ([[toolbarItem itemIdentifier] isEqualToString:DATE_ITEM_IDENTIFIER]) {
  3017 				NSSize newSize = NSMakeSize(([datePicker isHidden] ? 180 : 290), NSHeight([view_DatePicker frame]));
  3018 				[toolbarItem setMinSize:newSize];
  3019 				[toolbarItem setMaxSize:newSize];
  3020 				break;
  3021 			}
  3022 		}		
  3023 	}
  3024 }
  3025 
  3026 - (NSString *)dateItemNibName
  3027 {
  3028 	return @"LogViewerDateFilter";
  3029 }
  3030 
  3031 @end