2 // AIAbstractLogViewerWindowController.m
5 // Created by Evan Schoenberg on 3/24/06.
8 #import "AIAbstractLogViewerWindowController.h"
9 #import <Adium/AIAccountControllerProtocol.h>
11 #import <Adium/AIContactControllerProtocol.h>
12 #import <Adium/AIContentControllerProtocol.h>
13 #import "AILogFromGroup.h"
14 #import "AILogToGroup.h"
15 #import "AILoggerPlugin.h"
16 #import <Adium/AIPreferenceControllerProtocol.h>
17 #import "ESRankingCell.h"
18 #import "GBChatlogHTMLConverter.h"
19 #import <AIUtilities/AIArrayAdditions.h>
20 #import <AIUtilities/AIAttributedStringAdditions.h>
21 #import <AIUtilities/AIDateFormatterAdditions.h>
22 #import <AIUtilities/AIFileManagerAdditions.h>
23 #import <AIUtilities/AIImageAdditions.h>
24 #import <AIUtilities/AIImageTextCell.h>
25 #import <AIUtilities/AIOutlineViewAdditions.h>
26 #import <AIUtilities/AISplitView.h>
27 #import <AIUtilities/AIStringAdditions.h>
28 #import <AIUtilities/AITableViewAdditions.h>
29 #import <AIUtilities/AITextAttributes.h>
30 #import <AIUtilities/AIToolbarUtilities.h>
31 #import <AIUtilities/AIApplicationAdditions.h>
32 #import <AIUtilities/AIDividedAlternatingRowOutlineView.h>
33 #import <Adium/AIHTMLDecoder.h>
34 #import <Adium/AIListContact.h>
35 #import <Adium/AIMetaContact.h>
36 #import <Adium/AIServiceIcons.h>
37 #import <Adium/AIUserIcons.h>
39 #import "AILogDateFormatter.h"
41 #import "KFTypeSelectTableView.h"
42 #import "KNShelfSplitView.h"
44 #define KEY_LOG_VIEWER_WINDOW_FRAME @"Log Viewer Frame"
45 #define PREF_GROUP_CONTACT_LIST @"Contact List"
46 #define KEY_LOG_VIEWER_GROUP_STATE @"Log Viewer Group State" //Expand/Collapse state of groups
47 #define TOOLBAR_LOG_VIEWER @"Log Viewer Toolbar"
49 #define MAX_LOGS_TO_SORT_WHILE_SEARCHING 10000 //Max number of logs we will live sort while searching
50 #define LOG_SEARCH_STATUS_INTERVAL 20 //1/60ths of a second to wait before refreshing search status
52 #define SEARCH_MENU AILocalizedString(@"Search Menu",nil)
53 #define FROM AILocalizedString(@"From",nil)
54 #define TO AILocalizedString(@"To",nil)
55 #define DATE AILocalizedString(@"Date",nil)
56 #define CONTENT AILocalizedString(@"Content",nil)
57 #define DELETE AILocalizedString(@"Delete",nil)
58 #define DELETEALL AILocalizedString(@"Delete All",nil)
59 #define SEARCH AILocalizedString(@"Search",nil)
61 #define HIDE_EMOTICONS AILocalizedString(@"Hide Emoticons",nil)
62 #define SHOW_EMOTICONS AILocalizedString(@"Show Emoticons",nil)
64 #define IMAGE_EMOTICONS_OFF @"emoticon32"
65 #define IMAGE_EMOTICONS_ON @"emoticon32_transparent"
67 #define REFRESH_RESULTS_INTERVAL 1.0 //Interval between results refreshes while searching
69 @interface AIAbstractLogViewerWindowController (PRIVATE)
70 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
71 - (void)initLogFiltering;
72 - (void)displayLog:(AIChatLog *)log;
73 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
74 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
75 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
76 - (void)buildSearchMenu;
77 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
78 - (void)_logContentFilter:(NSString *)searchString searchID:(int)searchID onSearchIndex:(SKIndexRef)logSearchIndex;
79 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode;
80 - (void)installToolbar;
81 - (void)updateRankColumnVisibility;
82 - (void)openLogAtPath:(NSString *)inPath;
83 - (void)rebuildContactsList;
84 - (void)filterForContact:(AIListContact *)inContact;
85 - (void)selectCachedIndex;
87 - (void)_willOpenForContact;
88 - (void)_didOpenForContact;
90 - (void)deleteSelection:(id)sender;
93 @implementation AIAbstractLogViewerWindowController
95 static AIAbstractLogViewerWindowController *sharedLogViewerInstance = nil;
96 static int toArraySort(id itemA, id itemB, void *context);
103 + (id)openForPlugin:(id)inPlugin
105 if (!sharedLogViewerInstance) {
106 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
109 [sharedLogViewerInstance showWindow:nil];
111 return sharedLogViewerInstance;
114 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
116 [self openForPlugin:inPlugin];
118 [sharedLogViewerInstance openLogAtPath:inPath];
120 return sharedLogViewerInstance;
123 //Open the log viewer window to a specific contact's logs
124 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
126 if (!sharedLogViewerInstance) {
127 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
130 [sharedLogViewerInstance _willOpenForContact];
131 [sharedLogViewerInstance showWindow:nil];
132 [sharedLogViewerInstance filterForContact:inContact];
133 [sharedLogViewerInstance _didOpenForContact];
135 return sharedLogViewerInstance;
138 //Returns the window controller if one exists
139 + (id)existingWindowController
141 return sharedLogViewerInstance;
144 //Close the log viewer window
145 + (void)closeSharedInstance
147 if (sharedLogViewerInstance) {
148 [sharedLogViewerInstance closeWindow:nil];
153 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
157 selectedColumn = nil;
160 automaticSearch = YES;
162 activeSearchString = nil;
163 displayedLogArray = nil;
164 windowIsClosing = NO;
165 desiredContactsSourceListDeltaX = 0;
167 blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
170 searchMode = LOG_SEARCH_CONTENT;
171 headerDateFormatter = [[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString]
172 allowNaturalLanguage:NO];
173 currentSearchResults = [[NSMutableArray alloc] init];
174 fromArray = [[NSMutableArray alloc] init];
175 fromServiceArray = [[NSMutableArray alloc] init];
176 logFromGroupDict = [[NSMutableDictionary alloc] init];
177 toArray = [[NSMutableArray alloc] init];
178 toServiceArray = [[NSMutableArray alloc] init];
179 logToGroupDict = [[NSMutableDictionary alloc] init];
180 resultsLock = [[NSRecursiveLock alloc] init];
181 searchingLock = [[NSLock alloc] init];
182 contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
184 allContactsIdentifier = [[NSNumber alloc] initWithInt:-1];
186 undoManager = [[NSUndoManager alloc] init];
188 [super initWithWindowNibName:windowNibName];
196 [resultsLock release];
197 [searchingLock release];
199 [fromServiceArray release];
201 [toServiceArray release];
202 [currentSearchResults release];
203 [selectedColumn release];
204 [headerDateFormatter release];
205 [displayedLogArray release];
206 [blankImage release];
207 [activeSearchString release];
208 [contactIDsToFilter release];
210 [logFromGroupDict release]; logFromGroupDict = nil;
211 [logToGroupDict release]; logToGroupDict = nil;
213 [filterForAccountName release]; filterForAccountName = nil;
215 [horizontalRule release]; horizontalRule = nil;
217 [adiumIcon release]; adiumIcon = nil;
218 [adiumIconHighlighted release]; adiumIconHighlighted = nil;
220 //We loaded view_DatePicker from a nib manually, so we must release it
221 [view_DatePicker release]; view_DatePicker = nil;
223 [allContactsIdentifier release];
224 [undoManager release]; undoManager = nil;
229 //Init our log filtering tree
230 - (void)initLogFiltering
232 NSEnumerator *enumerator;
233 NSString *folderName;
234 NSMutableDictionary *toDict = [NSMutableDictionary dictionary];
235 NSString *basePath = [AILoggerPlugin logBasePath];
236 NSString *fromUID, *serviceClass;
238 //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
239 //first by service, then by account name.
240 enumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:basePath] sortedArrayUsingSelector:@selector(compare:)] objectEnumerator];
241 while ((folderName = [enumerator nextObject])) {
242 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
243 NSEnumerator *toEnum;
244 AILogToGroup *currentToGroup;
245 AILogFromGroup *logFromGroup;
246 NSMutableSet *toSetForThisService;
247 NSArray *serviceAndFromUIDArray;
249 /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
250 * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
252 serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
254 if ([serviceAndFromUIDArray count] >= 2) {
255 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
257 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
258 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
260 //Fallback: blank non-nil serviceClass; folderName as the fromUID
262 fromUID = folderName;
265 logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
267 //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
268 [logFromGroupDict setObject:logFromGroup forKey:folderName];
271 if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
272 toSetForThisService = [NSMutableSet set];
273 [toDict setObject:toSetForThisService
274 forKey:serviceClass];
277 //Add the 'to' for each grouping on this account
278 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
279 while ((currentToGroup = [toEnum nextObject])) {
282 if ((currentTo = [currentToGroup to])) {
283 //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
284 [logToGroupDict setObject:currentToGroup forKey:[currentToGroup path]];
288 [logFromGroup release];
292 [self rebuildContactsList];
295 - (void)rebuildContactsList
297 NSEnumerator *enumerator = [logFromGroupDict objectEnumerator];
298 AILogFromGroup *logFromGroup;
300 int oldCount = [toArray count];
301 [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
303 while ((logFromGroup = [enumerator nextObject])) {
304 NSEnumerator *toEnum;
305 AILogToGroup *currentToGroup;
306 NSString *serviceClass = [logFromGroup serviceClass];
308 //Add the 'to' for each grouping on this account
309 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
310 while ((currentToGroup = [toEnum nextObject])) {
313 if ((currentTo = [currentToGroup to])) {
314 AIListObject *listObject = ((serviceClass && currentTo) ?
315 [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
318 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
319 AIListContact *parentContact = [(AIListContact *)listObject parentContact];
320 if (![toArray containsObjectIdenticalTo:parentContact]) {
321 [toArray addObject:parentContact];
325 if (![toArray containsObject:currentToGroup]) {
326 [toArray addObject:currentToGroup];
333 [toArray sortUsingFunction:toArraySort context:NULL];
334 [outlineView_contacts reloadData];
336 if (!isOpeningForContact) {
337 //If we're opening for a contact, the outline view selection will be changed in a moment anyways
338 [self outlineViewSelectionDidChange:nil];
343 - (NSString *)adiumFrameAutosaveName
345 return KEY_LOG_VIEWER_WINDOW_FRAME;
348 //Setup the window before it is displayed
349 - (void)windowDidLoad
351 suppressSearchRequests = YES;
353 [super windowDidLoad];
355 [plugin pauseIndexing];
357 [[self window] setTitle:AILocalizedString(@"Chat Transcript Viewer",nil)];
358 [textField_progress setStringValue:@""];
360 //Autosave doesn't do anything yet
361 [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
362 [shelf_splitView setFrame:[[[self window] contentView] frame]];
364 // Pull our main article/display split view out of the nib and position it in the shelf view
365 [containingView_results retain];
366 [containingView_results removeFromSuperview];
367 [shelf_splitView setContentView:containingView_results];
368 [containingView_results release];
370 // Pull our source view out of the nib and position it in the shelf view
371 [containingView_contactsSourceList retain];
372 [containingView_contactsSourceList removeFromSuperview];
373 [shelf_splitView setShelfView:containingView_contactsSourceList];
374 [containingView_contactsSourceList release];
376 //Set emoticon filtering
377 showEmoticons = [[[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
378 group:PREF_GROUP_LOGGING] boolValue];
379 [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
380 [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
383 [self installToolbar];
385 //This is the Mail.app source list background color... which differs from the iTunes one.
386 [outlineView_contacts setBackgroundColor:[NSColor colorWithCalibratedRed:.9059
391 AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
392 [[[outlineView_contacts tableColumns] objectAtIndex:0] setDataCell:dataCell];
393 [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
396 [outlineView_contacts setDrawsGradientSelection:YES];
398 //Localize tableView_results column headers
399 [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
400 [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
401 [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
402 [self tableViewColumnDidResize:nil];
404 [tableView_results sizeLastColumnToFit];
406 //Prepare the search controls
407 [self buildSearchMenu];
408 if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
409 [textView_content setUsesFindPanel:YES];
412 //Sort by preference, defaulting to sorting by date
413 NSString *selectedTableColumnPref;
414 if ((selectedTableColumnPref = [[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
415 group:PREF_GROUP_LOGGING])) {
416 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
418 if (!selectedColumn) {
419 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
421 [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
423 //Prepare indexing and filter searching
424 [plugin prepareLogContentSearching];
425 [self initLogFiltering];
427 //Begin our initial search
428 [self setSearchMode:LOG_SEARCH_TO];
430 [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
431 suppressSearchRequests = NO;
433 if (!isOpeningForContact) {
434 //If we're opening for a contact, we'll select it and then begin searching
435 [self startSearchingClearingCurrentResults:YES];
438 [plugin resumeIndexing];
441 -(void)rebuildIndices
443 //Rebuild the 'global' log indexes
444 [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
445 [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
446 [toServiceArray removeAllObjects];
447 [fromArray removeAllObjects];
448 [fromServiceArray removeAllObjects];
450 [self initLogFiltering];
452 [tableView_results reloadData];
453 [self selectDisplayedLog];
456 //Called as the window closes
457 - (void)windowWillClose:(id)sender
459 [super windowWillClose:sender];
461 //Set preference for emoticon filtering
462 [[adium preferenceController] setPreference:[NSNumber numberWithBool:showEmoticons]
463 forKey:KEY_LOG_VIEWER_EMOTICONS
464 group:PREF_GROUP_LOGGING];
466 //Set preference for selected column
467 [[adium preferenceController] setPreference:[selectedColumn identifier]
468 forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
469 group:PREF_GROUP_LOGGING];
471 /* Disable the search field. If we don't disable the search field, it will often try to call its target action
472 * after the window has closed (and we are gone). I'm not sure why this happens, but disabling the field
473 * before we close the window down seems to prevent the crash.
475 [searchField_logs setEnabled:NO];
477 /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
478 * it was gone, like responding to a logIndexUpdated message
480 windowIsClosing = YES;
482 //Abort any in-progress searching and indexing, and wait for their completion
483 [self stopSearching];
484 [plugin cleanUpLogContentSearching];
486 //Reset our column widths if needed
487 [activeSearchString release]; activeSearchString = nil;
488 [self updateRankColumnVisibility];
490 [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
491 [toolbarItems autorelease]; toolbarItems = nil;
494 //Display --------------------------------------------------------------------------------------------------------------
496 //Update log viewer progress string to reflect current status
497 - (void)updateProgressDisplay
499 NSMutableString *progress = nil;
500 int indexNumber, indexTotal;
503 //We always convey the number of logs being displayed
505 unsigned count = [currentSearchResults count];
506 if (activeSearchString && [activeSearchString length]) {
507 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
508 AILocalizedString(@"%i matching transcripts",nil) :
509 AILocalizedString(@"1 matching transcript",nil)),count]];
511 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
512 AILocalizedString(@"%i transcripts",nil) :
513 AILocalizedString(@"1 transcript",nil)),count]];
515 //We are searching, but there is no active search string. This indicates we're still opening logs.
517 progress = [[AILocalizedString(@"Opening transcripts",nil) mutableCopy] autorelease];
520 [resultsLock unlock];
522 indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
524 //Append search progress
525 if (activeSearchString && [activeSearchString length]) {
527 [progress appendString:@" - "];
529 progress = [NSMutableString string];
532 if (searching || indexing) {
533 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
535 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];
539 //Append indexing progress
542 [progress appendString:@" - "];
544 progress = [NSMutableString string];
547 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %i of %i transcripts",nil), indexNumber, indexTotal]];
550 if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
551 [progress appendString:[NSString ellipsis]];
554 //Enable/disable the searching animation
555 if (searching || indexing) {
556 [progressIndicator startAnimation:nil];
558 [progressIndicator stopAnimation:nil];
561 [textField_progress setStringValue:(progress ? progress : @"")];
564 //The plugin is informing us that the log indexing changed
565 - (void)logIndexingProgressUpdate
567 //Don't do anything if the window is already closing
568 if (!windowIsClosing) {
569 [self updateProgressDisplay];
571 //If we are searching by content, we should re-search without clearing our current results so the
572 //the newly-indexed logs can be added without blanking the current table contents.
573 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
575 //We're already searching; reattempt when done
576 searchIDToReattemptWhenComplete = activeSearchID;
578 //We're not searching - restart the search immediately
579 // [self startSearchingClearingCurrentResults:NO];
585 //Refresh the results table
586 - (void)refreshResults
588 [self updateProgressDisplay];
590 [self refreshResultsSearchIsComplete:NO];
593 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
596 int count = [currentSearchResults count];
597 [resultsLock unlock];
598 AILog(@"refreshResultsSearchIsComplete: %i (count is %i)",searchIsComplete,count);
599 if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
600 //Sort the logs correctly which will also reload the table
603 if (searchIsComplete && automaticSearch) {
604 //If search is complete, select the first log if requested and possible
605 [self selectFirstLog];
608 BOOL oldAutomaticSearch = automaticSearch;
610 //We don't want the above re-selection to change our automaticSearch tracking
611 //(The only reason automaticSearch should change is in response to user action)
612 automaticSearch = oldAutomaticSearch;
616 if (searchIsComplete &&
617 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
618 searchIDToReattemptWhenComplete = -1;
619 [self startSearchingClearingCurrentResults:NO];
623 [self selectCachedIndex];
626 [self updateProgressDisplay];
629 - (void)searchComplete
631 [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
632 [self refreshResultsSearchIsComplete:YES];
635 //Displays the contents of the specified log in our window
636 - (void)displayLogs:(NSArray *)logArray;
638 NSMutableAttributedString *displayText = nil;
639 NSAttributedString *finalDisplayText = nil;
640 NSRange scrollRange = NSMakeRange(0,0);
641 BOOL appendedFirstLog = NO;
643 if (![logArray isEqualToArray:displayedLogArray]) {
644 [displayedLogArray release];
645 displayedLogArray = [logArray copy];
648 if ([logArray count] > 1) {
649 displayText = [[NSMutableAttributedString alloc] init];
652 NSEnumerator *enumerator = [logArray objectEnumerator];
654 NSString *logBasePath = [AILoggerPlugin logBasePath];
655 AILog(@"Displaying %@",logArray);
656 while ((theLog = [enumerator nextObject])) {
657 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
660 if (!horizontalRule) {
661 #define HORIZONTAL_BAR 0x2013
662 #define HORIZONTAL_RULE_LENGTH 18
664 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
665 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
666 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
667 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
669 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
672 [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
673 (appendedFirstLog ? @"\n" : @""),
675 [headerDateFormatter stringFromDate:[theLog date]],
678 withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
681 if ([[theLog path] hasSuffix:@".AdiumHTMLLog"] || [[theLog path] hasSuffix:@".html"] || [[theLog path] hasSuffix:@".html.bak"]) {
683 NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
686 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
688 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
691 } else if ([[theLog path] hasSuffix:@".chatlog"]){
693 NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog path]];
695 //If this log begins with a malformed UTF-8 BOM (which was written out by Adium for a brief time between 1.0b7 and 1.0b8), fix it before trying to read it in.
697 failedUtf8BomLength = 6
699 NSData *data = [NSData dataWithContentsOfMappedFile:logFullPath];
700 const unsigned char *ptr = [data bytes];
701 unsigned len = [data length];
702 if ((len >= failedUtf8BomLength)
710 //Yup. Back up the old file, then strip it off.
711 NSLog(@"Transcript file at %@ has unwanted bytes at the front of it. (This is a bug in a previous version of Adium, not this version.) Attempting recovery.", logFullPath);
712 NSString *backupPath = [logFullPath stringByAppendingPathExtension:@"bak"];
713 if(![[NSFileManager defaultManager] movePath:logFullPath toPath:backupPath handler:nil])
714 NSLog(@"Could not back up file; recovery failed. This transcript will probably appear blank in the transcript viewer.");
716 NSRange range = { failedUtf8BomLength, len - failedUtf8BomLength };
717 NSData *theRestOfIt = [data subdataWithRange:range];
718 if([theRestOfIt writeToFile:logFullPath atomically:YES])
719 NSLog(@"Wrote fixed version to same file. The corrupted version was renamed to %@; you may remove this file at your leisure after you are satisfied that the recovery succeeded. You can test this by viewing the transcript (%@) in the transcript viewer.", backupPath, [logFullPath lastPathComponent]);
721 NSLog(@"Could not write fix!");
724 NSString *logFileText = [GBChatlogHTMLConverter readFile:logFullPath];
728 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
730 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
734 //Fallback: Plain text log
735 NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
737 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
740 [displayText appendAttributedString:[[[NSAttributedString alloc] initWithString:logFileText
741 attributes:[textAttributes dictionary]] autorelease]];
743 displayText = [[NSMutableAttributedString alloc] initWithString:logFileText attributes:[textAttributes dictionary]];
748 appendedFirstLog = YES;
753 if (displayText && [displayText length]) {
754 //Add pretty formatting to links
755 [displayText addFormattingForLinks];
757 //If we are searching by content, highlight the search results
758 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
759 NSEnumerator *enumerator;
760 NSString *searchWord;
761 NSMutableArray *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
762 NSScanner *scanner = [NSScanner scannerWithString:activeSearchString];
764 //Look for an initial quote
765 while (![scanner isAtEnd]) {
766 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
768 [scanner scanUpToString:@"\"" intoString:NULL];
770 //Scan past the quote
771 if (![scanner scanString:@"\"" intoString:NULL]) continue;
773 NSString *quotedString;
775 if (![scanner isAtEnd] &&
776 [scanner scanUpToString:@"\"" intoString:"edString]) {
777 //Scan past the quote
778 [scanner scanString:@"\"" intoString:NULL];
779 /* If a string within quotes is found, remove the words from the quoted string and add the full string
780 * to what we'll be highlighting.
782 * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
783 * "killer attack ninja kittens" OR ninja
784 * wouldn't highlight the word ninja by itself.
786 NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
787 int quotedWordsCount = [quotedWords count];
789 for (int i = 0; i < quotedWordsCount; i++) {
790 NSString *quotedWord = [quotedWords objectAtIndex:i];
792 //Originally started with a quote, so put it back on
793 quotedWord = [@"\"" stringByAppendingString:quotedWord];
795 if (i == quotedWordsCount - 1) {
796 //Originally ended with a quote, so put it back on
797 quotedWord = [quotedWord stringByAppendingString:@"\""];
799 int searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
800 if (searchWordsIndex != NSNotFound) {
801 [searchWordsArray removeObjectAtIndex:searchWordsIndex];
803 NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
807 //Add the full quoted string
808 [searchWordsArray addObject:quotedString];
813 BOOL shouldScrollToWord = NO;
814 scrollRange = NSMakeRange([displayText length],0);
816 enumerator = [searchWordsArray objectEnumerator];
817 while ((searchWord = [enumerator nextObject])) {
820 //Check against and/or. We don't just remove it from the array because then we couldn't check case insensitively.
821 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
822 ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
823 [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
825 //We'll want to scroll to the first occurrance of any matching word or words
826 if (occurrence.location < scrollRange.location) {
827 scrollRange = occurrence;
828 shouldScrollToWord = YES;
833 //If we shouldn't be scrolling to a new range, we want to scroll to the top
834 if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
836 [searchWordsArray release];
841 finalDisplayText = [[adium contentController] filterAttributedString:displayText
842 usingFilterType:AIFilterMessageDisplay
843 direction:AIFilterOutgoing
846 finalDisplayText = displayText;
850 if (finalDisplayText) {
851 [[textView_content textStorage] setAttributedString:finalDisplayText];
853 //Set this string and scroll to the top/bottom/occurrence
854 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
855 [textView_content scrollRangeToVisible:scrollRange];
857 [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
861 //No log selected, empty the view
862 [textView_content setString:@""];
865 [displayText release];
868 - (void)displayLog:(AIChatLog *)theLog
870 [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
873 //Reselect the displayed log (Or another log if not possible)
874 - (void)selectDisplayedLog
876 int firstIndex = NSNotFound;
878 /* Is the log we had selected still in the table?
879 * (When performing an automatic search, we ignore the previous selection. This ensures that we always
880 * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
882 if (!automaticSearch) {
884 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
885 [resultsLock unlock];
887 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
890 if (firstIndex != NSNotFound) {
891 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
893 if (useSame == YES && sameSelection > 0) {
894 [tableView_results selectRow:sameSelection byExtendingSelection:NO];
896 [self selectFirstLog];
903 - (void)selectFirstLog
905 AIChatLog *theLog = nil;
907 //If our selected log is no more, select the first one in the list
909 if ([currentSearchResults count] != 0) {
910 theLog = [currentSearchResults objectAtIndex:0];
912 [resultsLock unlock];
914 //Change the table selection to this new log
915 //We need a little trickery here. When we change the row, the table view will call our tableViewSelectionDidChange: method.
916 //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
917 //search. As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
918 //that we instantiated this selection change, and not the user.
919 ignoreSelectionChange = YES;
920 [tableView_results selectRow:0 byExtendingSelection:NO];
921 [tableView_results scrollRowToVisible:0];
922 ignoreSelectionChange = NO;
924 [self displayLog:theLog]; //Manually update the displayed log
927 //Highlight the occurences of a search string within a displayed log
928 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
931 NSRange searchRange, foundRange;
932 NSString *plainBigString = [bigString string];
933 unsigned plainBigStringLength = [plainBigString length];
934 NSMutableDictionary *attributeDictionary = nil;
936 outRange->location = NSNotFound;
938 //Search for the little string in the big string
939 while (location != NSNotFound && location < plainBigStringLength) {
940 searchRange = NSMakeRange(location, plainBigStringLength-location);
941 foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
943 //Bold and color this match
944 if (foundRange.location != NSNotFound) {
945 if (outRange->location == NSNotFound) *outRange = foundRange;
947 if (!attributeDictionary) {
948 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
949 [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
950 [NSColor yellowColor], NSBackgroundColorAttributeName,
953 [bigString addAttributes:attributeDictionary
957 location = NSMaxRange(foundRange);
962 //Sorting --------------------------------------------------------------------------------------------------------------
966 NSString *identifier = [selectedColumn identifier];
970 if ([identifier isEqualToString:@"To"]) {
971 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
973 } else if ([identifier isEqualToString:@"From"]) {
974 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
976 } else if ([identifier isEqualToString:@"Date"]) {
977 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
979 } else if ([identifier isEqualToString:@"Rank"]) {
980 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
983 [resultsLock unlock];
986 [tableView_results reloadData];
988 //Reapply the selection
989 [self selectDisplayedLog];
992 //Sorts the selected log array and adjusts the selected column
993 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
995 //If there already was a sorted column, remove the indicator image from it.
996 if (selectedColumn && selectedColumn != tableColumn) {
997 [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
1000 //Set the indicator image in the newly selected column
1001 [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
1002 inTableColumn:tableColumn];
1004 //Set the highlighted table column.
1005 [tableView_results setHighlightedTableColumn:tableColumn];
1006 [selectedColumn release]; selectedColumn = [tableColumn retain];
1007 sortDirection = direction;
1012 //Searching ------------------------------------------------------------------------------------------------------------
1013 #pragma mark Searching
1014 //(Jag)Change search string
1015 - (void)controlTextDidChange:(NSNotification *)notification
1017 if (searchMode != LOG_SEARCH_CONTENT) {
1018 [self updateSearch:nil];
1022 //Change search string (Called by searchfield)
1023 - (IBAction)updateSearch:(id)sender
1025 automaticSearch = NO;
1026 [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1027 AILog(@"updateSearch calling startSearching");
1028 [self startSearchingClearingCurrentResults:YES];
1031 //Change search mode (Called by mode menu)
1032 - (IBAction)selectSearchType:(id)sender
1034 automaticSearch = NO;
1036 //First, update the search mode to the newly selected type
1037 [self setSearchMode:[sender tag]];
1039 //Then, ensure we are ready to search using the current string
1040 [self setSearchString:activeSearchString];
1042 //Now we are ready to start searching
1043 AILog(@"selectSearchType calling startSearching");
1044 [self startSearchingClearingCurrentResults:YES];
1047 //Begin a specific search
1048 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1050 automaticSearch = YES;
1051 //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1052 [self setSearchMode:inMode];
1053 [self setSearchString:inString];
1055 AILog(@"setSearchString:mode: calling startSearching");
1056 [self startSearchingClearingCurrentResults:YES];
1059 //Begin the current search
1060 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1062 NSDictionary *searchDict;
1064 if (suppressSearchRequests) return;
1065 AILog(@"Starting a search for %@",activeSearchString);
1067 //Once all searches have exited, we can start a new one
1068 if (clearCurrentResults) {
1070 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1071 [self stopSearching];
1073 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1074 [resultsLock unlock];
1076 //Stop any existing searches
1077 [self stopSearching];
1081 searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1082 [NSNumber numberWithInt:activeSearchID], @"ID",
1083 [NSNumber numberWithInt:searchMode], @"Mode",
1084 activeSearchString, @"String",
1085 [plugin logContentIndex], @"SearchIndex",
1087 [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1089 //Update the table periodically while the logs load.
1090 [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1091 refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1093 selector:@selector(refreshResults)
1095 repeats:YES] retain];
1098 //Abort any active searches
1099 - (void)stopSearching
1101 //Increase the active search ID so any existing searches stop, and then
1102 //wait for any active searches to finish and release the lock
1106 //Set the active search mode (Does not invoke a search)
1107 - (void)setSearchMode:(LogSearchMode)inMode
1109 //Get the NSTextFieldCell and use it only if it responds to setPlaceholderString: (10.3 and above)
1110 NSTextFieldCell *cell = [searchField_logs cell];
1111 if (![cell respondsToSelector:@selector(setPlaceholderString:)]) cell = nil;
1113 searchMode = inMode;
1115 //Clear any filter from the table if it's the current mode, as well
1116 switch (searchMode) {
1117 case LOG_SEARCH_FROM:
1118 [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1122 [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1125 case LOG_SEARCH_DATE:
1126 [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1129 case LOG_SEARCH_CONTENT:
1130 [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1134 [self updateRankColumnVisibility];
1135 [self buildSearchMenu];
1138 - (void)updateRankColumnVisibility
1140 NSTableColumn *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1142 if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1143 //Add the resultsColumn and resize if it should be shown but is not at present
1144 if (!resultsColumn) {
1145 NSArray *tableColumns;
1147 //Set up the results column
1148 resultsColumn = [[NSTableColumn alloc] initWithIdentifier:@"Rank"];
1149 [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1150 [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1152 //Add it to the table
1153 [tableView_results addTableColumn:resultsColumn];
1155 //Make it half again as large as the desired width from the @"Rank" header title
1156 [resultsColumn sizeToFit];
1157 [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1159 tableColumns = [tableView_results tableColumns];
1160 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1161 NSTableColumn *nextDoorNeighbor;
1163 //Adjust the column to the results column's left so results is now visible
1164 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1165 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1169 //Remove the resultsColumn and resize if it should not be shown but is at present
1170 if (resultsColumn) {
1171 NSArray *tableColumns;
1173 tableColumns = [tableView_results tableColumns];
1174 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1175 NSTableColumn *nextDoorNeighbor;
1177 //Adjust the column to the results column's left to take up the space again
1178 tableColumns = [tableView_results tableColumns];
1179 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1180 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1184 [tableView_results removeTableColumn:resultsColumn];
1189 //Set the active search string (Does not invoke a search)
1190 - (void)setSearchString:(NSString *)inString
1192 if (![[searchField_logs stringValue] isEqualToString:inString]) {
1193 [searchField_logs setStringValue:(inString ? inString : @"")];
1196 //Use autorelease so activeSearchString can be passed back to here
1197 if (activeSearchString != inString) {
1198 [activeSearchString release];
1199 activeSearchString = [inString retain];
1202 [self updateRankColumnVisibility];
1205 //Build the search mode menu
1206 - (void)buildSearchMenu
1208 NSMenu *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1209 [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1210 [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1211 [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1212 [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1214 [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1217 - (void)_willOpenForContact
1219 isOpeningForContact = YES;
1222 - (void)_didOpenForContact
1224 isOpeningForContact = NO;
1228 * @brief Focus the log viewer on a particular contact
1230 * If the contact is within a metacontact, the metacontact will be focused.
1232 - (void)filterForContact:(AIListContact *)inContact
1234 AIListContact *parentContact = [inContact parentContact];
1236 if (!isOpeningForContact) {
1237 /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1238 * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1239 * with regard to inContact since the log viewer opened.
1241 * If we're opening initially, the list is guaranteed fresh.
1243 [self rebuildContactsList];
1246 //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1247 if (searchMode == LOG_SEARCH_TO) {
1248 [self setSearchMode:LOG_SEARCH_CONTENT];
1250 //Update our search string to ensure we're configured for content searching
1251 [self setSearchString:activeSearchString];
1254 //Changing the selection will start a new search
1255 [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1256 unsigned int selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1257 if (selectedRow != NSNotFound) {
1258 [outlineView_contacts scrollRowToVisible:selectedRow];
1263 * @brief Returns a menu item for the search mode menu
1265 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1267 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
1268 action:@selector(selectSearchType:)
1270 [menuItem setTag:mode];
1271 [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1273 return [menuItem autorelease];
1276 #pragma mark Filtering search results
1278 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1280 BOOL matchesDateFilter;
1282 switch (filterDateType) {
1283 case AIDateTypeAfter:
1284 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1286 case AIDateTypeBefore:
1287 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1289 case AIDateTypeExactly:
1290 matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1293 matchesDateFilter = YES;
1297 return matchesDateFilter;
1301 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1303 CFURLRef url = SKDocumentCopyURL(inDocument);
1304 CFStringRef logPath = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
1305 NSArray *pathComponents = [(NSString *)logPath pathComponents];
1310 return pathComponents;
1314 * @brief Should a search display a document with the given information?
1316 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1318 BOOL shouldDisplayDocument = YES;
1320 if ([contactIDsToFilter count]) {
1321 //Determine the path components if we weren't supplied them
1322 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1324 unsigned int numPathComponents = [pathComponents count];
1326 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1327 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1329 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1331 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1334 if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1335 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1337 unsigned int numPathComponents = [pathComponents count];
1338 NSString *toPath = [NSString stringWithFormat:@"%@/%@",
1339 [pathComponents objectAtIndex:numPathComponents-3],
1340 [pathComponents objectAtIndex:numPathComponents-2]];
1341 NSString *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1344 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1346 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1349 return shouldDisplayDocument;
1352 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1353 #pragma mark Threaded filter/search methods
1354 //Search the logs, filtering out any matching logs into the currentSearchResults
1355 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1357 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1358 int mode = [[searchInfoDict objectForKey:@"Mode"] intValue];
1359 int searchID = [[searchInfoDict objectForKey:@"ID"] intValue];
1360 NSString *searchString = [searchInfoDict objectForKey:@"String"];
1362 if (searchID == activeSearchID) { //If we're still supposed to go
1364 AILog(@"filterLogsWithSearch (search ID %i): %@",searchID,searchInfoDict);
1366 [plugin pauseIndexing];
1367 if (searchString && [searchString length]) {
1369 case LOG_SEARCH_FROM:
1371 case LOG_SEARCH_DATE:
1372 [self _logFilter:searchString
1376 case LOG_SEARCH_CONTENT:
1377 [self _logContentFilter:searchString
1379 onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1383 [self _logFilter:nil
1390 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1391 [plugin resumeIndexing];
1392 AILog(@"filterLogsWithSearch (search ID %i): finished",searchID);
1399 //Perform a filter search based on source name, destination name, or date
1400 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode
1402 NSEnumerator *fromEnumerator, *toEnumerator, *logEnumerator;
1403 AILogToGroup *toGroup;
1404 AILogFromGroup *fromGroup;
1406 UInt32 lastUpdate = TickCount();
1408 NSCalendarDate *searchStringDate = nil;
1410 if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1411 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString] dateWithCalendarFormat:nil timeZone:nil];
1414 //Walk through every 'from' group
1415 fromEnumerator = [logFromGroupDict objectEnumerator];
1416 while ((fromGroup = [fromEnumerator nextObject]) && (searchID == activeSearchID)) {
1418 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1419 if ((mode != LOG_SEARCH_FROM) ||
1421 ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1423 //Walk through every 'to' group
1424 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
1425 while ((toGroup = [toEnumerator nextObject]) && (searchID == activeSearchID)) {
1427 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1428 * For all other search modes, we always proceed here so long as either:
1429 * a) We are not filtering for specific contact names or
1430 * b) The contact name matches one of the names in contactIDsToFilter
1432 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1433 ((mode != LOG_SEARCH_TO) ||
1435 ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1437 //Walk through every log
1439 logEnumerator = [toGroup logEnumerator];
1440 while ((theLog = [logEnumerator nextObject]) && (searchID == activeSearchID)) {
1441 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1442 * For all other search modes, we always proceed here
1444 if ((mode != LOG_SEARCH_DATE) ||
1446 (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1448 if ([self chatLogMatchesDateFilter:theLog]) {
1451 [currentSearchResults addObject:theLog];
1452 [resultsLock unlock];
1455 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1456 [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1459 lastUpdate = TickCount();
1470 //Search results table view --------------------------------------------------------------------------------------------
1471 #pragma mark Search results table view
1472 //Since this table view's source data will be accessed from within other threads, we need to lock before
1473 //accessing it. We also must be very sure that an incorrect row request is handled silently, since this
1474 //can occur if the array size is changed during the reload.
1475 - (int)numberOfRowsInTableView:(NSTableView *)tableView
1480 count = [currentSearchResults count];
1481 [resultsLock unlock];
1487 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
1489 NSString *identifier = [tableColumn identifier];
1491 if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1492 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1494 [aCell setPercentage:[theLog rankingPercentage]];
1499 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
1501 NSString *identifier = [tableColumn identifier];
1505 if (row < 0 || row >= [currentSearchResults count]) {
1506 if ([identifier isEqualToString:@"Service"]) {
1513 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1515 if ([identifier isEqualToString:@"To"]) {
1516 // Get ListObject for to-UID
1517 AIListObject *listObject = [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:[theLog serviceClass]
1520 //Use the longDisplayName, following the user's contact list preferences as this is presumably how she wants to view contacts' names.
1521 value = [listObject longDisplayName];
1524 //No username available
1525 value = [theLog to];
1528 } else if ([identifier isEqualToString:@"From"]) {
1529 value = [theLog from];
1531 } else if ([identifier isEqualToString:@"Date"]) {
1532 value = [theLog date];
1534 } else if ([identifier isEqualToString:@"Service"]) {
1535 NSString *serviceClass;
1538 serviceClass = [theLog serviceClass];
1539 image = [AIServiceIcons serviceIconForService:[[adium accountController] firstServiceWithServiceID:serviceClass]
1540 type:AIServiceIconSmall
1541 direction:AIIconNormal];
1542 value = (image ? image : blankImage);
1545 [resultsLock unlock];
1551 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1553 if (!ignoreSelectionChange) {
1554 NSArray *selectedLogs;
1556 //Update the displayed log
1557 automaticSearch = NO;
1560 selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
1561 [resultsLock unlock];
1563 [self displayLogs:selectedLogs];
1567 //Sort the log array & reflect the new column
1568 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1570 [self sortCurrentSearchResultsForTableColumn:tableColumn
1571 direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1574 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1576 [self deleteSelection:nil];
1579 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
1581 if (tableView == tableView_results) {
1582 [tableView setSearchColumnIdentifiers:[NSSet setWithObjects:@"To", @"From", nil]];
1583 [tableView setSearchWraps:YES];
1585 } else if (tableView == (KFTypeSelectTableView *)outlineView_contacts) {
1586 [tableView setSearchWraps:YES];
1590 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1592 NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1594 if (!aNotification ||
1595 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1596 NSDateFormatter *dateFormatter;
1597 NSCell *cell = [dateTableColumn dataCell];
1599 [cell setObjectValue:[NSDate date]];
1601 float width = [dateTableColumn width];
1603 #define NUMBER_TIME_STYLES 2
1604 #define NUMBER_DATE_STYLES 4
1605 NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1606 NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1607 float requiredWidth;
1609 dateFormatter = [cell formatter];
1610 if (!dateFormatter) {
1611 dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1612 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1613 [cell setFormatter:dateFormatter];
1616 requiredWidth = width + 1;
1617 for (int i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1618 [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1620 for (int j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1621 [dateFormatter setDateStyle:formatterStyles[j]];
1622 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1623 //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1630 - (IBAction)toggleEmoticonFiltering:(id)sender
1632 showEmoticons = !showEmoticons;
1633 [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1634 [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1636 [self displayLogs:displayedLogArray];
1639 #pragma mark Outline View Data source
1640 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
1644 return allContactsIdentifier;
1647 return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1651 if ([item isKindOfClass:[AIMetaContact class]]) {
1652 return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1659 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1662 ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1663 [item isKindOfClass:[NSArray class]]);
1666 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1669 return [toArray count] + 1; //+1 for the All item
1671 } else if ([item isKindOfClass:[AIMetaContact class]]) {
1672 unsigned count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1683 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1685 Class itemClass = [item class];
1687 if (itemClass == [AIMetaContact class]) {
1688 return [(AIMetaContact *)item longDisplayName];
1690 } else if (itemClass == [AIListContact class]) {
1691 if ([(AIListContact *)item parentContact] != item) {
1692 //This contact is within a metacontact - always show its UID
1693 return [(AIListContact *)item formattedUID];
1695 return [(AIListContact *)item longDisplayName];
1698 } else if (itemClass == [AILogToGroup class]) {
1699 return [(AILogToGroup *)item to];
1701 } else if (itemClass == [allContactsIdentifier class]) {
1702 int contactCount = [toArray count];
1703 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1704 ((contactCount == 1) ?
1705 AILocalizedString(@"1 Contact", nil) :
1706 [NSString stringWithFormat:AILocalizedString(@"%i Contacts", nil), contactCount])];
1708 } else if (itemClass == [NSString class]) {
1712 NSLog(@"%@: no idea",item);
1717 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1719 if ([item isKindOfClass:[AIMetaContact class]] &&
1720 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1721 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1722 * If it has multiple contacts, use no icon unless a user icon is present.
1724 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1725 size:NSMakeSize(16,16)];
1726 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1728 [cell setImage:image];
1730 } else if ([item isKindOfClass:[AIListContact class]]) {
1731 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1732 size:NSMakeSize(16,16)];
1733 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1734 type:AIServiceIconSmall
1735 direction:AIIconFlipped];
1736 [cell setImage:image];
1738 } else if ([item isKindOfClass:[AILogToGroup class]]) {
1739 [cell setImage:[AIServiceIcons serviceIconForServiceID:[(AILogToGroup *)item serviceClass]
1740 type:AIServiceIconSmall
1741 direction:AIIconFlipped]];
1743 } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1744 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1745 ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1746 if (!adiumIconHighlighted) {
1747 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
1748 forClass:[self class]] retain];
1751 [cell setImage:adiumIconHighlighted];
1755 adiumIcon = [[NSImage imageNamed:@"adium"
1756 forClass:[self class]] retain];
1759 [cell setImage:adiumIcon];
1762 } else if ([item isKindOfClass:[NSString class]]) {
1763 [cell setImage:nil];
1766 NSLog(@"%@: no idea",item);
1767 [cell setImage:nil];
1772 * @brief Is item supposed to have a divider below?
1775 - (AIDividerPosition)outlineView:(NSOutlineView*)outlineView dividerPositionForItem:(id)item
1777 if ([item isKindOfClass:[allContactsIdentifier class]]) {
1778 return AIDividerPositionBelow;
1780 return AIDividerPositionNone;
1784 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
1786 [self deleteSelection:nil];
1789 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1791 NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
1793 [contactIDsToFilter removeAllObjects];
1795 if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
1797 NSEnumerator *enumerator;
1799 enumerator = [selectedItems objectEnumerator];
1800 while ((item = [enumerator nextObject])) {
1801 if ([item isKindOfClass:[AIMetaContact class]]) {
1802 NSEnumerator *metaEnumerator;
1803 AIListContact *contact;
1805 metaEnumerator = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectEnumerator];
1806 while ((contact = [metaEnumerator nextObject])) {
1807 [contactIDsToFilter addObject:
1808 [[[NSString stringWithFormat:@"%@.%@",[contact serviceID],[contact UID]] compactedString] safeFilenameString]];
1811 } else if ([item isKindOfClass:[AIListContact class]]) {
1812 [contactIDsToFilter addObject:
1813 [[[NSString stringWithFormat:@"%@.%@",[(AIListContact *)item serviceID],[(AIListContact *)item UID]] compactedString] safeFilenameString]];
1815 } else if ([item isKindOfClass:[AILogToGroup class]]) {
1816 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]];
1821 [self startSearchingClearingCurrentResults:YES];
1824 static int toArraySort(id itemA, id itemB, void *context)
1826 NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
1827 NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
1828 NSComparisonResult result = [nameA caseInsensitiveCompare:nameB];
1829 if (result == NSOrderedSame) result = [nameA compare:nameB];
1834 - (void)draggedDividerRightBy:(float)deltaX
1836 desiredContactsSourceListDeltaX = deltaX;
1837 [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
1838 desiredContactsSourceListDeltaX = 0;
1842 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
1844 if ((sender == splitView_contacts_results) &&
1845 desiredContactsSourceListDeltaX != 0) {
1846 float dividerThickness = [sender dividerThickness];
1848 NSRect newFrame = [sender frame];
1849 NSRect leftFrame = [containingView_contactsSourceList frame];
1850 NSRect rightFrame = [containingView_results frame];
1852 leftFrame.size.width += desiredContactsSourceListDeltaX;
1853 leftFrame.size.height = newFrame.size.height;
1854 leftFrame.origin = NSMakePoint(0,0);
1856 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
1857 rightFrame.size.height = newFrame.size.height;
1858 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
1860 [containingView_contactsSourceList setFrame:leftFrame];
1861 [containingView_contactsSourceList setNeedsDisplay:YES];
1862 [containingView_results setFrame:rightFrame];
1863 [containingView_results setNeedsDisplay:YES];
1866 //Perform the default implementation
1867 [sender adjustSubviews];
1872 //Window Toolbar -------------------------------------------------------------------------------------------------------
1873 #pragma mark Window Toolbar
1874 - (NSString *)dateItemNibName
1879 - (void)installToolbar
1881 [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
1883 NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
1884 NSToolbarItem *toolbarItem;
1886 [toolbar setDelegate:self];
1887 [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
1888 [toolbar setSizeMode:NSToolbarSizeModeRegular];
1889 [toolbar setVisible:YES];
1890 [toolbar setAllowsUserCustomization:YES];
1891 [toolbar setAutosavesConfiguration:YES];
1892 toolbarItems = [[NSMutableDictionary alloc] init];
1895 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1896 withIdentifier:@"delete"
1899 toolTip:AILocalizedString(@"Delete the selection",nil)
1901 settingSelector:@selector(setImage:)
1902 itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
1903 action:@selector(deleteSelection:)
1907 [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
1908 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
1911 toolTip:AILocalizedString(@"Search or filter logs",nil)
1913 settingSelector:@selector(setView:)
1914 itemContent:view_SearchField
1915 action:@selector(updateSearch:)
1917 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1918 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
1920 [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
1921 [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
1922 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1924 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
1925 label:AILocalizedString(@"Date", nil)
1926 paletteLabel:AILocalizedString(@"Date", nil)
1927 toolTip:AILocalizedString(@"Filter logs by date",nil)
1929 settingSelector:@selector(setView:)
1930 itemContent:view_DatePicker
1933 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1934 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
1936 [toolbarItem setMinSize:[view_DatePicker frame].size];
1937 [toolbarItem setMaxSize:[view_DatePicker frame].size];
1938 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1941 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1942 withIdentifier:@"toggleemoticons"
1943 label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
1944 paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
1945 toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
1947 settingSelector:@selector(setImage:)
1948 itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
1949 action:@selector(toggleEmoticonFiltering:)
1952 [[self window] setToolbar:toolbar];
1954 [self configureDateFilter];
1957 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
1959 return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
1962 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
1964 return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
1965 @"delete", @"toggleemoticons", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
1969 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
1971 return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
1972 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
1973 NSToolbarSpaceItemIdentifier,
1974 NSToolbarFlexibleSpaceItemIdentifier,
1975 NSToolbarCustomizeToolbarItemIdentifier,
1976 NSToolbarPrintItemIdentifier, nil]];
1979 - (void)toolbarWillAddItem:(NSNotification *)notification
1981 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
1982 if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
1983 [item setTarget:self];
1984 [item setAction:@selector(adiumPrint:)];
1988 #pragma mark Date filter
1991 * @brief Returns a menu item for the date type filter menu
1993 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
1995 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInt:dateType]]
1996 action:@selector(selectDateType:)
1998 [menuItem setTag:dateType];
2000 return [menuItem autorelease];
2003 - (NSMenu *)dateTypeMenu
2005 NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2006 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInt:AIDateTypeAnyDate],
2007 AILocalizedString(@"Today", nil), [NSNumber numberWithInt:AIDateTypeToday],
2008 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInt:AIDateTypeSinceYesterday],
2009 AILocalizedString(@"This Week", nil), [NSNumber numberWithInt:AIDateTypeThisWeek],
2010 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoWeeks],
2011 AILocalizedString(@"This Month", nil), [NSNumber numberWithInt:AIDateTypeThisMonth],
2012 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoMonths],
2014 NSMenu *dateTypeMenu = [[NSMenu alloc] init];
2015 AIDateType dateType;
2017 [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
2018 [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2020 for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
2021 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2024 return [dateTypeMenu autorelease];
2027 - (int)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
2029 int todayDayOfWeek = [today dayOfWeek];
2031 //Try to look at the iCal preferences if possible
2032 if (!iCalFirstDayOfWeekDetermined) {
2033 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
2034 if (iCalFirstDayOfWeek) {
2035 //This should return a CFNumberRef... we're using another app's prefs, so make sure.
2036 if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
2037 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek intValue];
2040 CFRelease(iCalFirstDayOfWeek);
2044 iCalFirstDayOfWeekDetermined = YES;
2047 return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2051 * @brief A new date type was selected
2053 * This does not start a search
2055 - (void)selectedDateType:(AIDateType)dateType
2057 NSCalendarDate *today = [NSCalendarDate date];
2059 [filterDate release]; filterDate = nil;
2062 case AIDateTypeAnyDate:
2063 filterDateType = AIDateTypeAnyDate;
2066 case AIDateTypeToday:
2067 filterDateType = AIDateTypeExactly;
2068 filterDate = [today retain];
2071 case AIDateTypeSinceYesterday:
2072 filterDateType = AIDateTypeAfter;
2073 filterDate = [[today dateByAddingYears:0
2076 hours:-[today hourOfDay]
2077 minutes:-[today minuteOfHour]
2078 seconds:-([today secondOfMinute] + 1)] retain];
2081 case AIDateTypeThisWeek:
2082 filterDateType = AIDateTypeAfter;
2083 filterDate = [[today dateByAddingYears:0
2085 days:-[self daysSinceStartOfWeekGivenToday:today]
2086 hours:-[today hourOfDay]
2087 minutes:-[today minuteOfHour]
2088 seconds:-([today secondOfMinute] + 1)] retain];
2091 case AIDateTypeWithinLastTwoWeeks:
2092 filterDateType = AIDateTypeAfter;
2093 filterDate = [[today dateByAddingYears:0
2096 hours:-[today hourOfDay]
2097 minutes:-[today minuteOfHour]
2098 seconds:-([today secondOfMinute] + 1)] retain];
2101 case AIDateTypeThisMonth:
2102 filterDateType = AIDateTypeAfter;
2103 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2105 days:-[today dayOfMonth]
2108 seconds:-1] retain];
2111 case AIDateTypeWithinLastTwoMonths:
2112 filterDateType = AIDateTypeAfter;
2113 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2115 days:-[today dayOfMonth]
2118 seconds:-1] retain];
2127 * @brief Select the date type
2129 - (void)selectDateType:(id)sender
2131 [self selectedDateType:[sender tag]];
2132 [self startSearchingClearingCurrentResults:YES];
2135 - (void)configureDateFilter
2137 firstDayOfWeek = 0; /* Sunday */
2138 iCalFirstDayOfWeekDetermined = NO;
2140 [popUp_dateFilter setMenu:[self dateTypeMenu]];
2141 int index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2142 if(index != NSNotFound)
2143 [popUp_dateFilter selectItemAtIndex:index];
2144 [self selectedDateType:AIDateTypeAnyDate];
2147 #pragma mark Open Log
2149 - (void)openLogAtPath:(NSString *)inPath
2151 AIChatLog *chatLog = nil;
2152 NSString *basePath = [AILoggerPlugin logBasePath];
2154 //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2155 NSArray *pathComponents = [inPath pathComponents];
2156 int lastIndex = [pathComponents count];
2157 NSString *logName = [pathComponents objectAtIndex:--lastIndex];
2158 NSString *contactName = [pathComponents objectAtIndex:--lastIndex];
2159 NSString *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];
2160 NSString *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2162 NSString *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2163 //Filter for logs from the contact associated with the log we're loading
2164 [self filterForContact:[[adium contactController] contactWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
2168 NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2169 NSString *canonicalInPath = [inPath stringByStandardizingPath];
2171 if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2172 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2174 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2177 /* Different Adium user... this sucks. We're given a path like this:
2178 * /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2179 * and we want to make it relative to our current user's logs folder, which might be
2180 * /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2182 * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2184 NSString *fakeRelativePath = @"";
2186 //Use .. to get back to the root from the base path
2187 int componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2188 for (int i = 0; i < componentsOfBasePath; i++) {
2189 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2192 //Now add the path from the root to the actual log
2193 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2194 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2195 from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2197 serviceClass:serviceID] autorelease];
2200 //Now display the requested log
2202 [self displayLog:chatLog];
2206 #pragma mark Printing
2208 - (void)adiumPrint:(id)sender
2210 NSTextView *printView;
2211 NSPrintOperation *printOperation;
2212 NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];
2214 [printInfo setHorizontalPagination:NSFitPagination];
2215 [printInfo setHorizontallyCentered:NO];
2216 [printInfo setVerticallyCentered:NO];
2218 printView = [[NSTextView alloc] initWithFrame:[[NSPrintInfo sharedPrintInfo] imageablePageBounds]];
2219 [printView setVerticallyResizable:YES];
2220 [printView setHorizontallyResizable:NO];
2222 [[printView textStorage] setAttributedString:[textView_content textStorage]];
2224 printOperation = [NSPrintOperation printOperationWithView:printView printInfo:printInfo];
2225 [printOperation runOperationModalForWindow:[self window] delegate:nil
2226 didRunSelector:NULL contextInfo:NULL];
2227 [printView release];
2230 - (BOOL)validatePrintMenuItem:(NSMenuItem *)menuItem
2232 return ([displayedLogArray count] > 0);
2235 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2237 if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2238 return [self validatePrintMenuItem:nil];
2245 - (void)selectCachedIndex
2247 int numberOfRows = [tableView_results numberOfRows];
2249 if (cachedSelectionIndex < numberOfRows) {
2250 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2251 byExtendingSelection:NO];
2254 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2255 byExtendingSelection:NO];
2259 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2262 deleteOccurred = NO;
2265 #pragma mark Deletion
2268 * @brief Get an NSAlert to request deletion of multiple logs
2270 - (NSAlert *)alertForDeletionOfLogCount:(int)logCount
2272 NSAlert *alert = [[NSAlert alloc] init];
2273 [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2274 [alert setInformativeText:[NSString stringWithFormat:
2275 AILocalizedString(@"Are you sure you want to send %i logs to the Trash?",nil), logCount]];
2276 [alert addButtonWithTitle:DELETE];
2277 [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2279 return [alert autorelease];
2283 * @brief Undo the deletion of one or more AIChatLogs
2285 * The logs will be marked for readdition to the index
2287 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2289 NSEnumerator *enumerator;
2291 NSFileManager *fileManager = [NSFileManager defaultManager];
2292 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2294 enumerator = [deletedLogs objectEnumerator];
2295 while ((aLog = [enumerator nextObject])) {
2296 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2298 [fileManager createDirectoriesForPath:[logPath stringByDeletingLastPathComponent]];
2300 [fileManager movePath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2304 [plugin markLogDirtyAtPath:logPath];
2307 [self rebuildIndices];
2310 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2312 NSArray *selectedLogs = (NSArray *)contextInfo;
2313 if (returnCode == NSAlertFirstButtonReturn) {
2317 NSEnumerator *enumerator;
2318 NSMutableSet *logPaths = [NSMutableSet set];
2320 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2322 enumerator = [selectedLogs objectEnumerator];
2323 while ((aLog = [enumerator nextObject])) {
2324 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2326 [[adium notificationCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2327 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@/%@",[aLog serviceClass],[aLog from],[aLog to]]];
2328 BOOL success = [logToGroup trashLog:aLog];
2329 AILog(@"Trashing %@: %i",[aLog path], success);
2330 //Clear the to group out if it no longer has anything of interest
2331 if ([logToGroup logCount] == 0) {
2332 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[aLog serviceClass],[aLog from]]];
2333 [logFromGroup removeToGroup:logToGroup];
2336 [logPaths addObject:logPath];
2337 [currentSearchResults removeObjectIdenticalTo:aLog];
2340 [plugin removePathsFromIndex:logPaths];
2342 [undoManager registerUndoWithTarget:self
2343 selector:@selector(restoreDeletedLogs:)
2344 object:selectedLogs];
2345 [undoManager setActionName:DELETE];
2347 [resultsLock unlock];
2348 [tableView_results reloadData];
2350 deleteOccurred = YES;
2352 [self rebuildContactsList];
2353 [self updateProgressDisplay];
2355 [selectedLogs release];
2359 * @brief Delete logs
2361 * If two or more logs are passed, confirmation will be requested.
2362 * This operation registers with the window controller's undo manager.
2364 * @param selectedLogs An NSArray of logs to delete
2366 - (void)deleteLogs:(NSArray *)selectedLogs
2368 if ([selectedLogs count] > 1) {
2369 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2370 [alert beginSheetModalForWindow:[self window]
2372 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2373 contextInfo:[selectedLogs retain]];
2375 [self deleteLogsAlertDidEnd:nil
2376 returnCode:NSAlertFirstButtonReturn
2377 contextInfo:[selectedLogs retain]];
2382 * @brief Returns a set of all selected to groups on all accounts
2384 * @param totalLogCount If non-NULL, will be set to the total number of logs on return
2386 - (NSArray *)allSelectedToGroups:(int *)totalLogCount
2388 NSEnumerator *fromEnumerator;
2389 AILogFromGroup *fromGroup;
2390 NSMutableArray *allToGroups = [NSMutableArray array];
2392 if (totalLogCount) *totalLogCount = 0;
2394 //Walk through every 'from' group
2395 fromEnumerator = [logFromGroupDict objectEnumerator];
2396 while ((fromGroup = [fromEnumerator nextObject])) {
2397 NSEnumerator *toEnumerator;
2398 AILogToGroup *toGroup;
2400 //Walk through every 'to' group
2401 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2402 while ((toGroup = [toEnumerator nextObject])) {
2403 if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2404 if (totalLogCount) {
2405 *totalLogCount += [toGroup logCount];
2408 [allToGroups addObject:toGroup];
2417 * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2419 * The logs will be marked for readdition to the index
2421 - (void)restoreDeletedToGroups:(NSArray *)toGroups
2423 NSEnumerator *enumerator;
2424 AILogToGroup *toGroup;
2425 NSFileManager *fileManager = [NSFileManager defaultManager];
2426 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2427 NSString *logBasePath = [AILoggerPlugin logBasePath];
2429 enumerator = [toGroups objectEnumerator];
2430 while ((toGroup = [enumerator nextObject])) {
2431 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup path]];
2433 [fileManager createDirectoriesForPath:[toGroupPath stringByDeletingLastPathComponent]];
2434 if ([fileManager fileExistsAtPath:toGroupPath]) {
2435 AILog(@"Removing path %@ to make way for %@",
2436 toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2437 [fileManager removeFileAtPath:toGroupPath
2440 [fileManager movePath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2444 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2447 while ((aLog = [logEnumerator nextObject])) {
2448 [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog path]]];
2452 [self rebuildIndices];
2455 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2457 NSArray *allSelectedToGroups = (NSArray *)contextInfo;
2458 if (returnCode == NSAlertFirstButtonReturn) {
2459 AILogToGroup *logToGroup;
2460 NSEnumerator *enumerator;
2461 NSMutableSet *logPaths = [NSMutableSet set];
2463 enumerator = [allSelectedToGroups objectEnumerator];
2464 while ((logToGroup = [enumerator nextObject])) {
2465 NSEnumerator *logEnumerator;
2468 logEnumerator = [logToGroup logEnumerator];
2469 while ((aLog = [logEnumerator nextObject])) {
2470 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2471 [logPaths addObject:logPath];
2474 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2475 [logFromGroup removeToGroup:logToGroup];
2478 [plugin removePathsFromIndex:logPaths];
2480 [undoManager registerUndoWithTarget:self
2481 selector:@selector(restoreDeletedToGroups:)
2482 object:allSelectedToGroups];
2483 [undoManager setActionName:DELETE];
2485 [self rebuildIndices];
2486 [self updateProgressDisplay];
2489 [allSelectedToGroups release];
2493 * @brief Delete entirely the logs of all contacts selected in the source list
2495 * Confirmation by the user will be required.
2497 * Note: A single item in the source list may have multiple associated AILogToGroups.
2499 - (void)deleteSelectedContactsFromSourceList
2502 NSArray *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2504 if (totalLogCount > 1) {
2505 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2506 [alert beginSheetModalForWindow:[self window]
2508 didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2509 contextInfo:[allSelectedToGroups retain]];
2511 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2512 returnCode:NSAlertFirstButtonReturn
2513 contextInfo:[allSelectedToGroups retain]];
2518 * @brief Delete the current selection
2520 * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2521 * If anything else is selected, the currently selected search result logs will be trashed.
2523 - (void)deleteSelection:(id)sender
2525 if ([[self window] firstResponder] == outlineView_contacts) {
2526 [self deleteSelectedContactsFromSourceList];
2530 NSArray *selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
2531 [resultsLock unlock];
2533 [self deleteLogs:selectedLogs];
2539 * @brief Supply our undo manager when we are within the responder chain
2541 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender