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