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

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