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