2 // AIAbstractLogViewerWindowController.m
5 // Created by Evan Schoenberg on 3/24/06.
8 #import <Adium/AIAccountControllerProtocol.h>
10 #import <Adium/AIContactControllerProtocol.h>
11 #import <Adium/AIContentControllerProtocol.h>
12 #import "AILogFromGroup.h"
13 #import "AILogToGroup.h"
14 #import "AILogViewerWindowController.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 <Adium/AIHTMLDecoder.h>
33 #import <Adium/AIListContact.h>
34 #import <Adium/AIMetaContact.h>
35 #import <Adium/AIServiceIcons.h>
36 #import <Adium/AIUserIcons.h>
38 #import "AILogDateFormatter.h"
40 #import "KFTypeSelectTableView.h"
41 #import "KNShelfSplitView.h"
43 #define KEY_LOG_VIEWER_WINDOW_FRAME @"Log Viewer Frame"
44 #define PREF_GROUP_CONTACT_LIST @"Contact List"
45 #define KEY_LOG_VIEWER_GROUP_STATE @"Log Viewer Group State" //Expand/Collapse state of groups
46 #define TOOLBAR_LOG_VIEWER @"Log Viewer Toolbar"
48 #define MAX_LOGS_TO_SORT_WHILE_SEARCHING 3000 //Max number of logs we will live sort while searching
49 #define LOG_SEARCH_STATUS_INTERVAL 20 //1/60ths of a second to wait before refreshing search status
51 #define SEARCH_MENU AILocalizedString(@"Search Menu",nil)
52 #define FROM AILocalizedString(@"From",nil)
53 #define TO AILocalizedString(@"To",nil)
54 #define DATE AILocalizedString(@"Date",nil)
55 #define CONTENT AILocalizedString(@"Content",nil)
56 #define DELETE AILocalizedString(@"Delete",nil)
57 #define DELETEALL AILocalizedString(@"Delete All",nil)
58 #define SEARCH AILocalizedString(@"Search",nil)
60 #define HIDE_EMOTICONS AILocalizedString(@"Hide Emoticons",nil)
61 #define SHOW_EMOTICONS AILocalizedString(@"Show Emoticons",nil)
63 #define IMAGE_EMOTICONS_OFF @"emoticon32"
64 #define IMAGE_EMOTICONS_ON @"emoticon32_transparent"
66 #define REFRESH_RESULTS_INTERVAL 0.5 //Interval between results refreshes while searching
68 @interface AIAbstractLogViewerWindowController (PRIVATE)
69 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
70 - (void)initLogFiltering;
71 - (void)displayLog:(AIChatLog *)log;
72 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
73 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
74 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
75 - (void)buildSearchMenu;
76 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
77 - (void)_logContentFilter:(NSString *)searchString searchID:(int)searchID onSearchIndex:(SKIndexRef)logSearchIndex;
78 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode;
79 - (void)installToolbar;
80 - (void)updateRankColumnVisibility;
81 - (void)openLogAtPath:(NSString *)inPath;
82 - (void)rebuildContactsList;
83 - (void)filterForContact:(AIListContact *)inContact;
84 - (void)selectCachedIndex;
86 - (void)deleteSelection:(id)sender;
89 @implementation AIAbstractLogViewerWindowController
91 static AIAbstractLogViewerWindowController *sharedLogViewerInstance = nil;
92 static int toArraySort(id itemA, id itemB, void *context);
99 + (id)openForPlugin:(id)inPlugin
101 if (!sharedLogViewerInstance) {
102 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
105 [sharedLogViewerInstance showWindow:nil];
107 return sharedLogViewerInstance;
110 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
112 [self openForPlugin:inPlugin];
114 [sharedLogViewerInstance openLogAtPath:inPath];
116 return sharedLogViewerInstance;
119 //Open the log viewer window to a specific contact's logs
120 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
122 [self openForPlugin:inPlugin];
124 [sharedLogViewerInstance filterForContact:inContact];
126 return sharedLogViewerInstance;
129 //Returns the window controller if one exists
130 + (id)existingWindowController
132 return sharedLogViewerInstance;
135 //Close the log viewer window
136 + (void)closeSharedInstance
138 if (sharedLogViewerInstance) {
139 [sharedLogViewerInstance closeWindow:nil];
144 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
148 selectedColumn = nil;
151 automaticSearch = YES;
153 activeSearchString = nil;
154 displayedLogArray = nil;
155 windowIsClosing = NO;
156 desiredContactsSourceListDeltaX = 0;
158 blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
161 searchMode = LOG_SEARCH_CONTENT;
162 headerDateFormatter = [[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString]
163 allowNaturalLanguage:NO];
164 currentSearchResults = [[NSMutableArray alloc] init];
165 fromArray = [[NSMutableArray alloc] init];
166 fromServiceArray = [[NSMutableArray alloc] init];
167 logFromGroupDict = [[NSMutableDictionary alloc] init];
168 toArray = [[NSMutableArray alloc] init];
169 toServiceArray = [[NSMutableArray alloc] init];
170 logToGroupDict = [[NSMutableDictionary alloc] init];
171 resultsLock = [[NSRecursiveLock alloc] init];
172 searchingLock = [[NSLock alloc] init];
173 contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
175 allContactsIdentifier = [[NSNumber alloc] initWithInt:-1];
177 undoManager = [[NSUndoManager alloc] init];
179 [super initWithWindowNibName:windowNibName];
187 [resultsLock release];
188 [searchingLock release];
190 [fromServiceArray release];
192 [toServiceArray release];
193 [currentSearchResults release];
194 [selectedColumn release];
195 [headerDateFormatter release];
196 [displayedLogArray release];
197 [blankImage release];
198 [activeSearchString release];
199 [contactIDsToFilter release];
201 [logFromGroupDict release]; logFromGroupDict = nil;
202 [logToGroupDict release]; logToGroupDict = nil;
204 [filterForAccountName release]; filterForAccountName = nil;
206 [horizontalRule release]; horizontalRule = nil;
208 [adiumIcon release]; adiumIcon = nil;
209 [adiumIconHighlighted release]; adiumIconHighlighted = nil;
211 //We loaded view_DatePicker from a nib manually, so we must release it
212 [view_DatePicker release]; view_DatePicker = nil;
214 [allContactsIdentifier release];
215 [undoManager release]; undoManager = nil;
220 //Init our log filtering tree
221 - (void)initLogFiltering
223 NSEnumerator *enumerator;
224 NSString *folderName;
225 NSMutableDictionary *toDict = [NSMutableDictionary dictionary];
226 NSString *basePath = [AILoggerPlugin logBasePath];
227 NSString *fromUID, *serviceClass;
229 //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
230 //first by service, then by account name.
231 enumerator = [[[[NSFileManager defaultManager] directoryContentsAtPath:basePath] sortedArrayUsingSelector:@selector(compare:)] objectEnumerator];
232 while ((folderName = [enumerator nextObject])) {
233 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
234 NSEnumerator *toEnum;
235 AILogToGroup *currentToGroup;
236 AILogFromGroup *logFromGroup;
237 NSMutableSet *toSetForThisService;
238 NSArray *serviceAndFromUIDArray;
240 /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
241 * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
243 serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
245 if ([serviceAndFromUIDArray count] >= 2) {
246 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
248 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
249 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
251 //Fallback: blank non-nil serviceClass; folderName as the fromUID
253 fromUID = folderName;
256 logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
258 //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
259 [logFromGroupDict setObject:logFromGroup forKey:folderName];
262 if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
263 toSetForThisService = [NSMutableSet set];
264 [toDict setObject:toSetForThisService
265 forKey:serviceClass];
268 //Add the 'to' for each grouping on this account
269 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
270 while ((currentToGroup = [toEnum nextObject])) {
273 if ((currentTo = [currentToGroup to])) {
274 //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
275 [logToGroupDict setObject:currentToGroup forKey:[currentToGroup path]];
279 [logFromGroup release];
283 [self rebuildContactsList];
286 - (void)rebuildContactsList
288 NSEnumerator *enumerator = [logFromGroupDict objectEnumerator];
289 AILogFromGroup *logFromGroup;
291 int oldCount = [toArray count];
292 [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
294 while ((logFromGroup = [enumerator nextObject])) {
295 NSEnumerator *toEnum;
296 AILogToGroup *currentToGroup;
297 NSString *serviceClass = [logFromGroup serviceClass];
299 //Add the 'to' for each grouping on this account
300 toEnum = [[logFromGroup toGroupArray] objectEnumerator];
301 while ((currentToGroup = [toEnum nextObject])) {
304 if ((currentTo = [currentToGroup to])) {
305 AIListObject *listObject = ((serviceClass && currentTo) ?
306 [[adium contactController] existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
309 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
310 AIListContact *parentContact = [(AIListContact *)listObject parentContact];
311 if (![toArray containsObjectIdenticalTo:parentContact]) {
312 [toArray addObject:parentContact];
316 if (![toArray containsObject:currentToGroup]) {
317 [toArray addObject:currentToGroup];
324 [toArray sortUsingFunction:toArraySort context:NULL];
325 [outlineView_contacts reloadData];
327 [self outlineViewSelectionDidChange:nil];
331 - (NSString *)adiumFrameAutosaveName
333 return KEY_LOG_VIEWER_WINDOW_FRAME;
336 //Setup the window before it is displayed
337 - (void)windowDidLoad
339 [super windowDidLoad];
341 [[self window] setTitle:AILocalizedString(@"Chat Transcripts Viewer",nil)];
342 [textField_progress setStringValue:@""];
344 //Autosave doesn't do anything yet
345 [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
346 [shelf_splitView setFrame:[[[self window] contentView] frame]];
348 // Pull our main article/display split view out of the nib and position it in the shelf view
349 [containingView_results retain];
350 [containingView_results removeFromSuperview];
351 [shelf_splitView setContentView:containingView_results];
352 [containingView_results release];
354 // Pull our source view out of the nib and position it in the shelf view
355 [containingView_contactsSourceList retain];
356 [containingView_contactsSourceList removeFromSuperview];
357 [shelf_splitView setShelfView:containingView_contactsSourceList];
358 [containingView_contactsSourceList release];
360 //Set emoticon filtering
361 showEmoticons = [[[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
362 group:PREF_GROUP_LOGGING] boolValue];
363 [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
364 [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
367 [self installToolbar];
369 //This is the Mail.app source list background color... which differs from the iTunes one.
370 [outlineView_contacts setBackgroundColor:[NSColor colorWithCalibratedRed:.9059
375 AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
376 [[[outlineView_contacts tableColumns] objectAtIndex:0] setDataCell:dataCell];
377 [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
380 [outlineView_contacts setDrawsGradientSelection:YES];
382 //Localize tableView_results column headers
383 [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
384 [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
385 [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
386 [self tableViewColumnDidResize:nil];
388 [tableView_results sizeLastColumnToFit];
390 //Prepare the search controls
391 [self buildSearchMenu];
392 if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
393 [textView_content setUsesFindPanel:YES];
396 //Sort by preference, defaulting to sorting by date
397 NSString *selectedTableColumnPref;
398 if ((selectedTableColumnPref = [[adium preferenceController] preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
399 group:PREF_GROUP_LOGGING])) {
400 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
402 if (!selectedColumn) {
403 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
405 [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
407 //Prepare indexing and filter searching
408 [plugin prepareLogContentSearching];
409 [self initLogFiltering];
411 //Begin our initial search
412 [self setSearchMode:LOG_SEARCH_TO];
414 [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
415 [self startSearchingClearingCurrentResults:YES];
418 -(void)rebuildIndices
420 //Rebuild the 'global' log indexes
421 [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
422 [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
423 [toServiceArray removeAllObjects];
424 [fromArray removeAllObjects];
425 [fromServiceArray removeAllObjects];
427 [self initLogFiltering];
429 [tableView_results reloadData];
430 [self selectDisplayedLog];
433 //Called as the window closes
434 - (void)windowWillClose:(id)sender
436 [super windowWillClose:sender];
438 //Set preference for emoticon filtering
439 [[adium preferenceController] setPreference:[NSNumber numberWithBool:showEmoticons]
440 forKey:KEY_LOG_VIEWER_EMOTICONS
441 group:PREF_GROUP_LOGGING];
443 //Set preference for selected column
444 [[adium preferenceController] setPreference:[selectedColumn identifier]
445 forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
446 group:PREF_GROUP_LOGGING];
448 /* Disable the search field. If we don't disable the search field, it will often try to call its target action
449 * after the window has closed (and we are gone). I'm not sure why this happens, but disabling the field
450 * before we close the window down seems to prevent the crash.
452 [searchField_logs setEnabled:NO];
454 /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
455 * it was gone, like responding to a logIndexUpdated message
457 windowIsClosing = YES;
459 //Abort any in-progress searching and indexing, and wait for their completion
460 [self stopSearching];
461 [plugin cleanUpLogContentSearching];
463 //Reset our column widths if needed
464 [activeSearchString release]; activeSearchString = nil;
465 [self updateRankColumnVisibility];
467 [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
468 [toolbarItems autorelease]; toolbarItems = nil;
471 //Display --------------------------------------------------------------------------------------------------------------
473 //Update log viewer progress string to reflect current status
474 - (void)updateProgressDisplay
476 NSMutableString *progress = nil;
477 int indexNumber, indexTotal;
480 //We always convey the number of logs being displayed
482 unsigned count = [currentSearchResults count];
483 if (activeSearchString && [activeSearchString length]) {
484 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
485 AILocalizedString(@"%i matching logs",nil) :
486 AILocalizedString(@"1 matching log",nil)),count]];
488 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
489 AILocalizedString(@"%i logs",nil) :
490 AILocalizedString(@"1 log",nil)),count]];
492 //We are searching, but there is no active search string. This indicates we're still opening logs.
494 progress = [[AILocalizedString(@"Opening logs",nil) mutableCopy] autorelease];
497 [resultsLock unlock];
499 indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
501 //Append search progress
502 if (activeSearchString && [activeSearchString length]) {
504 [progress appendString:@" - "];
506 progress = [NSMutableString string];
509 if (searching || indexing) {
510 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
512 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];
516 //Append indexing progress
519 [progress appendString:@" - "];
521 progress = [NSMutableString string];
524 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %i of %i",nil), indexNumber, indexTotal]];
527 if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
528 [progress appendString:[NSString ellipsis]];
531 //Enable/disable the searching animation
532 if (searching || indexing) {
533 [progressIndicator startAnimation:nil];
535 [progressIndicator stopAnimation:nil];
538 [textField_progress setStringValue:(progress ? progress : @"")];
541 //The plugin is informing us that the log indexing changed
542 - (void)logIndexingProgressUpdate
544 //Don't do anything if the window is already closing
545 if (!windowIsClosing) {
546 [self updateProgressDisplay];
548 //If we are searching by content, we should re-search without clearing our current results so the
549 //the newly-indexed logs can be added without blanking the current table contents.
550 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
552 //We're already searching; reattempt when done
553 searchIDToReattemptWhenComplete = activeSearchID;
555 //We're not searching - restart the search immediately
556 [self startSearchingClearingCurrentResults:NO];
562 //Refresh the results table
563 - (void)refreshResults
565 [self updateProgressDisplay];
567 [self refreshResultsSearchIsComplete:NO];
570 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
573 int count = [currentSearchResults count];
574 [resultsLock unlock];
576 if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
577 //Sort the logs correctly which will also reload the table
580 if (searchIsComplete && automaticSearch) {
581 //If search is complete, select the first log if requested and possible
582 [self selectFirstLog];
585 BOOL oldAutomaticSearch = automaticSearch;
587 //We don't want the above re-selection to change our automaticSearch tracking
588 //(The only reason automaticSearch should change is in response to user action)
589 automaticSearch = oldAutomaticSearch;
593 if (searchIsComplete &&
594 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
595 searchIDToReattemptWhenComplete = -1;
596 [self startSearchingClearingCurrentResults:NO];
600 [self selectCachedIndex];
603 [self updateProgressDisplay];
606 - (void)searchComplete
608 [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
609 [self refreshResultsSearchIsComplete:YES];
612 //Displays the contents of the specified log in our window
613 - (void)displayLogs:(NSArray *)logArray;
615 NSMutableAttributedString *displayText = nil;
616 NSAttributedString *finalDisplayText = nil;
617 NSRange scrollRange = NSMakeRange(0,0);
618 BOOL appendedFirstLog = NO;
620 if (![logArray isEqualToArray:displayedLogArray]) {
621 [displayedLogArray release];
622 displayedLogArray = [logArray copy];
625 if ([logArray count] > 1) {
626 displayText = [[NSMutableAttributedString alloc] init];
629 NSEnumerator *enumerator = [logArray objectEnumerator];
631 NSString *logBasePath = [AILoggerPlugin logBasePath];
632 AILog(@"Displaying %@",logArray);
633 while ((theLog = [enumerator nextObject])) {
634 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
637 if (!horizontalRule) {
638 #define HORIZONTAL_BAR 0x2013
639 #define HORIZONTAL_RULE_LENGTH 18
641 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
642 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
643 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
644 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
646 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
649 [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
650 (appendedFirstLog ? @"\n" : @""),
652 ([NSApp isOnTigerOrBetter] ?
653 [headerDateFormatter stringFromDate:[theLog date]] :
654 [[theLog date] descriptionWithCalendarFormat:[headerDateFormatter dateFormat]
656 locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]]),
659 withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
662 if ([[theLog path] hasSuffix:@".AdiumHTMLLog"] || [[theLog path] hasSuffix:@".html"] || [[theLog path] hasSuffix:@".html.bak"]) {
664 NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
667 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
669 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
672 } else if ([[theLog path] hasSuffix:@".chatlog"]){
674 NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog path]];
676 //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.
678 failedUtf8BomLength = 6
680 NSData *data = [NSData dataWithContentsOfMappedFile:logFullPath];
681 const unsigned char *ptr = [data bytes];
682 unsigned len = [data length];
683 if ((len >= failedUtf8BomLength)
691 //Yup. Back up the old file, then strip it off.
692 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);
693 NSString *backupPath = [logFullPath stringByAppendingPathExtension:@"bak"];
694 if(![[NSFileManager defaultManager] movePath:logFullPath toPath:backupPath handler:nil])
695 NSLog(@"Could not back up file; recovery failed. This transcript will probably appear blank in the transcript viewer.");
697 NSRange range = { failedUtf8BomLength, len - failedUtf8BomLength };
698 NSData *theRestOfIt = [data subdataWithRange:range];
699 if([theRestOfIt writeToFile:logFullPath atomically:YES])
700 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]);
702 NSLog(@"Could not write fix!");
705 NSString *logFileText = [GBChatlogHTMLConverter readFile:logFullPath];
709 [displayText appendAttributedString:[AIHTMLDecoder decodeHTML:logFileText]];
711 displayText = [[AIHTMLDecoder decodeHTML:logFileText] mutableCopy];
715 //Fallback: Plain text log
716 NSString *logFileText = [NSString stringWithContentsOfFile:[logBasePath stringByAppendingPathComponent:[theLog path]]];
718 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
721 [displayText appendAttributedString:[[[NSAttributedString alloc] initWithString:logFileText
722 attributes:[textAttributes dictionary]] autorelease]];
724 displayText = [[NSMutableAttributedString alloc] initWithString:logFileText attributes:[textAttributes dictionary]];
729 appendedFirstLog = YES;
734 if (displayText && [displayText length]) {
735 //Add pretty formatting to links
736 [displayText addFormattingForLinks];
738 //If we are searching by content, highlight the search results
739 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
740 NSEnumerator *enumerator;
741 NSString *searchWord;
742 NSMutableArray *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
743 NSScanner *scanner = [NSScanner scannerWithString:activeSearchString];
745 //Look for an initial quote
746 while (![scanner isAtEnd]) {
747 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
749 [scanner scanUpToString:@"\"" intoString:NULL];
751 //Scan past the quote
752 if (![scanner scanString:@"\"" intoString:NULL]) continue;
754 NSString *quotedString;
756 if (![scanner isAtEnd] &&
757 [scanner scanUpToString:@"\"" intoString:"edString]) {
758 //Scan past the quote
759 [scanner scanString:@"\"" intoString:NULL];
760 /* If a string within quotes is found, remove the words from the quoted string and add the full string
761 * to what we'll be highlighting.
763 * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
764 * "killer attack ninja kittens" OR ninja
765 * wouldn't highlight the word ninja by itself.
767 NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
768 int quotedWordsCount = [quotedWords count];
770 for (int i = 0; i < quotedWordsCount; i++) {
771 NSString *quotedWord = [quotedWords objectAtIndex:i];
773 //Originally started with a quote, so put it back on
774 quotedWord = [@"\"" stringByAppendingString:quotedWord];
776 if (i == quotedWordsCount - 1) {
777 //Originally ended with a quote, so put it back on
778 quotedWord = [quotedWord stringByAppendingString:@"\""];
780 int searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
781 if (searchWordsIndex != NSNotFound) {
782 [searchWordsArray removeObjectAtIndex:searchWordsIndex];
784 NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
788 //Add the full quoted string
789 [searchWordsArray addObject:quotedString];
794 BOOL shouldScrollToWord = NO;
795 scrollRange = NSMakeRange([displayText length],0);
797 enumerator = [searchWordsArray objectEnumerator];
798 while ((searchWord = [enumerator nextObject])) {
801 //Check against and/or. We don't just remove it from the array because then we couldn't check case insensitively.
802 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
803 ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
804 [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
806 //We'll want to scroll to the first occurrance of any matching word or words
807 if (occurrence.location < scrollRange.location) {
808 scrollRange = occurrence;
809 shouldScrollToWord = YES;
814 //If we shouldn't be scrolling to a new range, we want to scroll to the top
815 if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
817 [searchWordsArray release];
822 finalDisplayText = [[adium contentController] filterAttributedString:displayText
823 usingFilterType:AIFilterMessageDisplay
824 direction:AIFilterOutgoing
827 finalDisplayText = displayText;
831 if (finalDisplayText) {
832 [[textView_content textStorage] setAttributedString:finalDisplayText];
834 //Set this string and scroll to the top/bottom/occurrence
835 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
836 [textView_content scrollRangeToVisible:scrollRange];
838 [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
842 //No log selected, empty the view
843 [textView_content setString:@""];
846 [displayText release];
849 - (void)displayLog:(AIChatLog *)theLog
851 [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
854 //Reselect the displayed log (Or another log if not possible)
855 - (void)selectDisplayedLog
857 int firstIndex = NSNotFound;
859 /* Is the log we had selected still in the table?
860 * (When performing an automatic search, we ignore the previous selection. This ensures that we always
861 * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
863 if (!automaticSearch) {
865 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
866 [resultsLock unlock];
868 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
871 if (firstIndex != NSNotFound) {
872 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
874 if (useSame == YES && sameSelection > 0) {
875 [tableView_results selectRow:sameSelection byExtendingSelection:NO];
877 [self selectFirstLog];
884 - (void)selectFirstLog
886 AIChatLog *theLog = nil;
888 //If our selected log is no more, select the first one in the list
890 if ([currentSearchResults count] != 0) {
891 theLog = [currentSearchResults objectAtIndex:0];
893 [resultsLock unlock];
895 //Change the table selection to this new log
896 //We need a little trickery here. When we change the row, the table view will call our tableViewSelectionDidChange: method.
897 //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
898 //search. As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
899 //that we instantiated this selection change, and not the user.
900 ignoreSelectionChange = YES;
901 [tableView_results selectRow:0 byExtendingSelection:NO];
902 [tableView_results scrollRowToVisible:0];
903 ignoreSelectionChange = NO;
905 [self displayLog:theLog]; //Manually update the displayed log
908 //Highlight the occurences of a search string within a displayed log
909 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
912 NSRange searchRange, foundRange;
913 NSString *plainBigString = [bigString string];
914 unsigned plainBigStringLength = [plainBigString length];
915 NSMutableDictionary *attributeDictionary = nil;
917 outRange->location = NSNotFound;
919 //Search for the little string in the big string
920 while (location != NSNotFound && location < plainBigStringLength) {
921 searchRange = NSMakeRange(location, plainBigStringLength-location);
922 foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
924 //Bold and color this match
925 if (foundRange.location != NSNotFound) {
926 if (outRange->location == NSNotFound) *outRange = foundRange;
928 if (!attributeDictionary) {
929 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
930 [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
931 [NSColor yellowColor], NSBackgroundColorAttributeName,
934 [bigString addAttributes:attributeDictionary
938 location = NSMaxRange(foundRange);
943 //Sorting --------------------------------------------------------------------------------------------------------------
947 NSString *identifier = [selectedColumn identifier];
951 if ([identifier isEqualToString:@"To"]) {
952 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
954 } else if ([identifier isEqualToString:@"From"]) {
955 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
957 } else if ([identifier isEqualToString:@"Date"]) {
958 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
960 } else if ([identifier isEqualToString:@"Rank"]) {
961 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
964 [resultsLock unlock];
967 [tableView_results reloadData];
969 //Reapply the selection
970 [self selectDisplayedLog];
973 //Sorts the selected log array and adjusts the selected column
974 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
976 //If there already was a sorted column, remove the indicator image from it.
977 if (selectedColumn && selectedColumn != tableColumn) {
978 [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
981 //Set the indicator image in the newly selected column
982 [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
983 inTableColumn:tableColumn];
985 //Set the highlighted table column.
986 [tableView_results setHighlightedTableColumn:tableColumn];
987 [selectedColumn release]; selectedColumn = [tableColumn retain];
988 sortDirection = direction;
993 //Searching ------------------------------------------------------------------------------------------------------------
994 #pragma mark Searching
995 //(Jag)Change search string
996 - (void)controlTextDidChange:(NSNotification *)notification
998 if (searchMode != LOG_SEARCH_CONTENT) {
999 [self updateSearch:nil];
1003 //Change search string (Called by searchfield)
1004 - (IBAction)updateSearch:(id)sender
1006 automaticSearch = NO;
1007 [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1008 [self startSearchingClearingCurrentResults:YES];
1011 //Change search mode (Called by mode menu)
1012 - (IBAction)selectSearchType:(id)sender
1014 automaticSearch = NO;
1016 //First, update the search mode to the newly selected type
1017 [self setSearchMode:[sender tag]];
1019 //Then, ensure we are ready to search using the current string
1020 [self setSearchString:activeSearchString];
1022 //Now we are ready to start searching
1023 [self startSearchingClearingCurrentResults:YES];
1026 //Begin a specific search
1027 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1029 automaticSearch = YES;
1030 //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1031 [self setSearchMode:inMode];
1032 [self setSearchString:inString];
1034 [self startSearchingClearingCurrentResults:YES];
1037 //Begin the current search
1038 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1040 NSDictionary *searchDict;
1042 //Once all searches have exited, we can start a new one
1043 if (clearCurrentResults) {
1045 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1046 [self stopSearching];
1048 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1049 [resultsLock unlock];
1051 //Stop any existing searches
1052 [self stopSearching];
1056 searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1057 [NSNumber numberWithInt:activeSearchID], @"ID",
1058 [NSNumber numberWithInt:searchMode], @"Mode",
1059 activeSearchString, @"String",
1060 [plugin logContentIndex], @"SearchIndex",
1062 [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1064 //Update the table periodically while the logs load.
1065 [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1066 refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1068 selector:@selector(refreshResults)
1070 repeats:YES] retain];
1073 //Abort any active searches
1074 - (void)stopSearching
1076 //Increase the active search ID so any existing searches stop, and then
1077 //wait for any active searches to finish and release the lock
1081 //Set the active search mode (Does not invoke a search)
1082 - (void)setSearchMode:(LogSearchMode)inMode
1084 //Get the NSTextFieldCell and use it only if it responds to setPlaceholderString: (10.3 and above)
1085 NSTextFieldCell *cell = [searchField_logs cell];
1086 if (![cell respondsToSelector:@selector(setPlaceholderString:)]) cell = nil;
1088 searchMode = inMode;
1090 //Clear any filter from the table if it's the current mode, as well
1091 switch (searchMode) {
1092 case LOG_SEARCH_FROM:
1093 [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1097 [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1100 case LOG_SEARCH_DATE:
1101 [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1104 case LOG_SEARCH_CONTENT:
1105 [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1109 [self updateRankColumnVisibility];
1110 [self buildSearchMenu];
1113 - (void)updateRankColumnVisibility
1115 NSTableColumn *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1117 if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1118 //Add the resultsColumn and resize if it should be shown but is not at present
1119 if (!resultsColumn) {
1120 NSArray *tableColumns;
1122 //Set up the results column
1123 resultsColumn = [[NSTableColumn alloc] initWithIdentifier:@"Rank"];
1124 [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1125 [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1127 //Add it to the table
1128 [tableView_results addTableColumn:resultsColumn];
1130 //Make it half again as large as the desired width from the @"Rank" header title
1131 [resultsColumn sizeToFit];
1132 [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1134 tableColumns = [tableView_results tableColumns];
1135 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1136 NSTableColumn *nextDoorNeighbor;
1138 //Adjust the column to the results column's left so results is now visible
1139 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1140 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1144 //Remove the resultsColumn and resize if it should not be shown but is at present
1145 if (resultsColumn) {
1146 NSArray *tableColumns;
1148 tableColumns = [tableView_results tableColumns];
1149 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1150 NSTableColumn *nextDoorNeighbor;
1152 //Adjust the column to the results column's left to take up the space again
1153 tableColumns = [tableView_results tableColumns];
1154 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1155 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1159 [tableView_results removeTableColumn:resultsColumn];
1164 //Set the active search string (Does not invoke a search)
1165 - (void)setSearchString:(NSString *)inString
1167 if (![[searchField_logs stringValue] isEqualToString:inString]) {
1168 [searchField_logs setStringValue:(inString ? inString : @"")];
1171 //Use autorelease so activeSearchString can be passed back to here
1172 if (activeSearchString != inString) {
1173 [activeSearchString release];
1174 activeSearchString = [inString retain];
1177 [self updateRankColumnVisibility];
1180 //Build the search mode menu
1181 - (void)buildSearchMenu
1183 NSMenu *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1184 [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1185 [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1186 [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1187 [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1189 [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1193 * @brief Focus the log viewer on a particular contact
1195 * If the contact is within a metacontact, the metacontact will be focused.
1197 - (void)filterForContact:(AIListContact *)inContact
1199 AIListContact *parentContact = [inContact parentContact];
1201 /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1202 * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1203 * with regard to inContact since the log viewer opened.
1205 [self rebuildContactsList];
1207 [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1208 unsigned int selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1209 if (selectedRow != NSNotFound) {
1210 [outlineView_contacts scrollRowToVisible:selectedRow];
1213 //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1214 if (searchMode == LOG_SEARCH_TO) {
1215 [self setSearchMode:LOG_SEARCH_CONTENT];
1217 //Update our search string to ensure we're configured for content searching
1218 [self setSearchString:activeSearchString];
1221 [self startSearchingClearingCurrentResults:YES];
1225 * @brief Returns a menu item for the search mode menu
1227 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1229 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
1230 action:@selector(selectSearchType:)
1232 [menuItem setTag:mode];
1233 [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1235 return [menuItem autorelease];
1238 #pragma mark Filtering search results
1240 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1242 BOOL matchesDateFilter;
1244 switch (filterDateType) {
1245 case AIDateTypeAfter:
1246 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1248 case AIDateTypeBefore:
1249 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1251 case AIDateTypeExactly:
1252 matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1255 matchesDateFilter = YES;
1259 return matchesDateFilter;
1263 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1265 CFURLRef url = SKDocumentCopyURL(inDocument);
1266 CFStringRef logPath = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
1267 NSArray *pathComponents = [(NSString *)logPath pathComponents];
1272 return pathComponents;
1276 * @brief Should a search display a document with the given information?
1278 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1280 BOOL shouldDisplayDocument = YES;
1282 if ([contactIDsToFilter count]) {
1283 //Determine the path components if we weren't supplied them
1284 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1286 unsigned int numPathComponents = [pathComponents count];
1288 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1289 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1291 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1293 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1296 if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1297 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1299 unsigned int numPathComponents = [pathComponents count];
1300 NSString *toPath = [NSString stringWithFormat:@"%@/%@",
1301 [pathComponents objectAtIndex:numPathComponents-3],
1302 [pathComponents objectAtIndex:numPathComponents-2]];
1303 NSString *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1306 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1308 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1311 return shouldDisplayDocument;
1314 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1315 #pragma mark Threaded filter/search methods
1316 //Search the logs, filtering out any matching logs into the currentSearchResults
1317 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1319 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1320 int mode = [[searchInfoDict objectForKey:@"Mode"] intValue];
1321 int searchID = [[searchInfoDict objectForKey:@"ID"] intValue];
1322 NSString *searchString = [searchInfoDict objectForKey:@"String"];
1324 if (searchID == activeSearchID) { //If we're still supposed to go
1328 if (searchString && [searchString length]) {
1330 case LOG_SEARCH_FROM:
1332 case LOG_SEARCH_DATE:
1333 [self _logFilter:searchString
1337 case LOG_SEARCH_CONTENT:
1338 [self _logContentFilter:searchString
1340 onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1344 [self _logFilter:nil
1351 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1358 //Perform a filter search based on source name, destination name, or date
1359 - (void)_logFilter:(NSString *)searchString searchID:(int)searchID mode:(LogSearchMode)mode
1361 NSEnumerator *fromEnumerator, *toEnumerator, *logEnumerator;
1362 AILogToGroup *toGroup;
1363 AILogFromGroup *fromGroup;
1365 UInt32 lastUpdate = TickCount();
1367 NSCalendarDate *searchStringDate = nil;
1369 if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1370 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString] dateWithCalendarFormat:nil timeZone:nil];
1373 //Walk through every 'from' group
1374 fromEnumerator = [logFromGroupDict objectEnumerator];
1375 while ((fromGroup = [fromEnumerator nextObject]) && (searchID == activeSearchID)) {
1377 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1378 if ((mode != LOG_SEARCH_FROM) ||
1380 ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1382 //Walk through every 'to' group
1383 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
1384 while ((toGroup = [toEnumerator nextObject]) && (searchID == activeSearchID)) {
1386 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1387 * For all other search modes, we always proceed here so long as either:
1388 * a) We are not filtering for specific contact names or
1389 * b) The contact name matches one of the names in contactIDsToFilter
1391 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1392 ((mode != LOG_SEARCH_TO) ||
1394 ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1396 //Walk through every log
1398 logEnumerator = [toGroup logEnumerator];
1399 while ((theLog = [logEnumerator nextObject]) && (searchID == activeSearchID)) {
1400 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1401 * For all other search modes, we always proceed here
1403 if ((mode != LOG_SEARCH_DATE) ||
1405 (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1407 if ([self chatLogMatchesDateFilter:theLog]) {
1410 [currentSearchResults addObject:theLog];
1411 [resultsLock unlock];
1414 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1415 [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1418 lastUpdate = TickCount();
1429 //Search results table view --------------------------------------------------------------------------------------------
1430 #pragma mark Search results table view
1431 //Since this table view's source data will be accessed from within other threads, we need to lock before
1432 //accessing it. We also must be very sure that an incorrect row request is handled silently, since this
1433 //can occur if the array size is changed during the reload.
1434 - (int)numberOfRowsInTableView:(NSTableView *)tableView
1439 count = [currentSearchResults count];
1440 [resultsLock unlock];
1446 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
1448 NSString *identifier = [tableColumn identifier];
1450 if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1451 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1453 [aCell setPercentage:[theLog rankingPercentage]];
1458 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
1460 NSString *identifier = [tableColumn identifier];
1464 if (row < 0 || row >= [currentSearchResults count]) {
1465 if ([identifier isEqualToString:@"Service"]) {
1472 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1474 if ([identifier isEqualToString:@"To"]) {
1475 value = [theLog to];
1477 } else if ([identifier isEqualToString:@"From"]) {
1478 value = [theLog from];
1480 } else if ([identifier isEqualToString:@"Date"]) {
1481 value = [theLog date];
1483 } else if ([identifier isEqualToString:@"Service"]) {
1484 NSString *serviceClass;
1487 serviceClass = [theLog serviceClass];
1488 image = [AIServiceIcons serviceIconForService:[[adium accountController] firstServiceWithServiceID:serviceClass]
1489 type:AIServiceIconSmall
1490 direction:AIIconNormal];
1491 value = (image ? image : blankImage);
1494 [resultsLock unlock];
1500 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1502 if (!ignoreSelectionChange) {
1503 NSArray *selectedLogs;
1505 //Update the displayed log
1506 automaticSearch = NO;
1509 selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
1510 [resultsLock unlock];
1512 [self displayLogs:selectedLogs];
1516 //Sort the log array & reflect the new column
1517 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1519 [self sortCurrentSearchResultsForTableColumn:tableColumn
1520 direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1523 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1525 [self deleteSelection:nil];
1528 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
1530 if (tableView == tableView_results) {
1531 [tableView setSearchColumnIdentifiers:[NSSet setWithObjects:@"To", @"From", nil]];
1532 [tableView setSearchWraps:YES];
1534 } else if (tableView == (KFTypeSelectTableView *)outlineView_contacts) {
1535 [tableView setSearchWraps:YES];
1539 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1541 NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1543 if (!aNotification ||
1544 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1545 NSDateFormatter *dateFormatter;
1546 NSCell *cell = [dateTableColumn dataCell];
1548 [cell setObjectValue:[NSDate date]];
1550 float width = [dateTableColumn width];
1552 if ([NSApp isOnTigerOrBetter]) {
1553 #define NUMBER_TIME_STYLES 2
1554 #define NUMBER_DATE_STYLES 4
1555 NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1556 NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1557 float requiredWidth;
1559 dateFormatter = [cell formatter];
1560 if (!dateFormatter) {
1561 dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1562 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1563 [cell setFormatter:dateFormatter];
1566 requiredWidth = width + 1;
1567 for (int i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1568 [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1570 for (int j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1571 [dateFormatter setDateStyle:formatterStyles[j]];
1572 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1573 //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1579 NSEnumerator *enumerator = [[NSArray arrayWithObjects:
1580 [[[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSDateFormatString]
1581 allowNaturalLanguage:NO] autorelease],
1582 [[[NSDateFormatter alloc] initWithDateFormat:[[NSUserDefaults standardUserDefaults] stringForKey:NSShortDateFormatString]
1583 allowNaturalLanguage:NO] autorelease],
1584 nil] objectEnumerator];
1585 float requiredWidth = width + 1;
1586 while ((requiredWidth > width) && (dateFormatter = [enumerator nextObject])) {
1587 [cell setFormatter:dateFormatter];
1588 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1595 - (IBAction)toggleEmoticonFiltering:(id)sender
1597 showEmoticons = !showEmoticons;
1598 [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1599 [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1601 [self displayLogs:displayedLogArray];
1604 #pragma mark Outline View Data source
1605 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item
1609 return allContactsIdentifier;
1612 return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1616 if ([item isKindOfClass:[AIMetaContact class]]) {
1617 return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1624 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1627 ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1628 [item isKindOfClass:[NSArray class]]);
1631 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1634 return [toArray count] + 1; //+1 for the All item
1636 } else if ([item isKindOfClass:[AIMetaContact class]]) {
1637 unsigned count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1648 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1650 if (![NSApp isOnTigerOrBetter]) {
1651 NSLog(@"item has address 0x%08x [class %@]", (unsigned long)item, [item class]);
1654 Class itemClass = [item class];
1656 if (itemClass == [AIMetaContact class]) {
1657 return [(AIMetaContact *)item longDisplayName];
1659 } else if (itemClass == [AIListContact class]) {
1660 if ([(AIListContact *)item parentContact] != item) {
1661 //This contact is within a metacontact - always show its UID
1662 return [(AIListContact *)item formattedUID];
1664 return [(AIListContact *)item longDisplayName];
1667 } else if (itemClass == [AILogToGroup class]) {
1668 return [(AILogToGroup *)item to];
1670 } else if (itemClass == [allContactsIdentifier class]) {
1671 int contactCount = [toArray count];
1672 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1673 ((contactCount == 1) ?
1674 AILocalizedString(@"1 Contact", nil) :
1675 [NSString stringWithFormat:AILocalizedString(@"%i Contacts", nil), contactCount])];
1677 } else if (itemClass == [NSString class]) {
1681 NSLog(@"%@: no idea",item);
1686 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1688 if ([item isKindOfClass:[AIMetaContact class]] &&
1689 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1690 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1691 * If it has multiple contacts, use no icon unless a user icon is present.
1693 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1694 size:NSMakeSize(16,16)];
1695 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1697 [cell setImage:image];
1699 } else if ([item isKindOfClass:[AIListContact class]]) {
1700 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1701 size:NSMakeSize(16,16)];
1702 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1703 type:AIServiceIconSmall
1704 direction:AIIconFlipped];
1705 [cell setImage:image];
1707 } else if ([item isKindOfClass:[AILogToGroup class]]) {
1708 [cell setImage:[AIServiceIcons serviceIconForServiceID:[(AILogToGroup *)item serviceClass]
1709 type:AIServiceIconSmall
1710 direction:AIIconFlipped]];
1712 } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1713 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1714 ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1715 if (!adiumIconHighlighted) {
1716 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
1717 forClass:[self class]] retain];
1720 [cell setImage:adiumIconHighlighted];
1724 adiumIcon = [[NSImage imageNamed:@"adium"
1725 forClass:[self class]] retain];
1728 [cell setImage:adiumIcon];
1731 } else if ([item isKindOfClass:[NSString class]]) {
1732 [cell setImage:nil];
1735 NSLog(@"%@: no idea",item);
1736 [cell setImage:nil];
1740 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
1742 [self deleteSelection:nil];
1745 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
1747 NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
1749 [contactIDsToFilter removeAllObjects];
1751 if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
1753 NSEnumerator *enumerator;
1755 enumerator = [selectedItems objectEnumerator];
1756 while ((item = [enumerator nextObject])) {
1757 if ([item isKindOfClass:[AIMetaContact class]]) {
1758 NSEnumerator *metaEnumerator;
1759 AIListContact *contact;
1761 metaEnumerator = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectEnumerator];
1762 while ((contact = [metaEnumerator nextObject])) {
1763 [contactIDsToFilter addObject:
1764 [[[NSString stringWithFormat:@"%@.%@",[contact serviceID],[contact UID]] compactedString] safeFilenameString]];
1767 } else if ([item isKindOfClass:[AIListContact class]]) {
1768 [contactIDsToFilter addObject:
1769 [[[NSString stringWithFormat:@"%@.%@",[(AIListContact *)item serviceID],[(AIListContact *)item UID]] compactedString] safeFilenameString]];
1771 } else if ([item isKindOfClass:[AILogToGroup class]]) {
1772 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]];
1777 [self startSearchingClearingCurrentResults:YES];
1780 static int toArraySort(id itemA, id itemB, void *context)
1782 NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
1783 NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
1785 return [nameA caseInsensitiveCompare:nameB];
1788 - (void)draggedDividerRightBy:(float)deltaX
1790 desiredContactsSourceListDeltaX = deltaX;
1791 [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
1792 desiredContactsSourceListDeltaX = 0;
1796 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
1798 if ((sender == splitView_contacts_results) &&
1799 desiredContactsSourceListDeltaX != 0) {
1800 float dividerThickness = [sender dividerThickness];
1802 NSRect newFrame = [sender frame];
1803 NSRect leftFrame = [containingView_contactsSourceList frame];
1804 NSRect rightFrame = [containingView_results frame];
1806 leftFrame.size.width += desiredContactsSourceListDeltaX;
1807 leftFrame.size.height = newFrame.size.height;
1808 leftFrame.origin = NSMakePoint(0,0);
1810 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
1811 rightFrame.size.height = newFrame.size.height;
1812 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
1814 [containingView_contactsSourceList setFrame:leftFrame];
1815 [containingView_contactsSourceList setNeedsDisplay:YES];
1816 [containingView_results setFrame:rightFrame];
1817 [containingView_results setNeedsDisplay:YES];
1820 //Perform the default implementation
1821 [sender adjustSubviews];
1826 //Window Toolbar -------------------------------------------------------------------------------------------------------
1827 #pragma mark Window Toolbar
1828 - (NSString *)dateItemNibName
1833 - (void)installToolbar
1835 [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
1837 NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
1838 NSToolbarItem *toolbarItem;
1840 [toolbar setDelegate:self];
1841 [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
1842 [toolbar setSizeMode:NSToolbarSizeModeRegular];
1843 [toolbar setVisible:YES];
1844 [toolbar setAllowsUserCustomization:YES];
1845 [toolbar setAutosavesConfiguration:YES];
1846 toolbarItems = [[NSMutableDictionary alloc] init];
1849 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1850 withIdentifier:@"delete"
1853 toolTip:AILocalizedString(@"Delete the selection",nil)
1855 settingSelector:@selector(setImage:)
1856 itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
1857 action:@selector(deleteSelection:)
1861 [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
1862 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
1865 toolTip:AILocalizedString(@"Search or filter logs",nil)
1867 settingSelector:@selector(setView:)
1868 itemContent:view_SearchField
1869 action:@selector(updateSearch:)
1871 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1872 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
1874 [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
1875 [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
1876 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1878 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
1879 label:AILocalizedString(@"Date", nil)
1880 paletteLabel:AILocalizedString(@"Date", nil)
1881 toolTip:AILocalizedString(@"Filter logs by date",nil)
1883 settingSelector:@selector(setView:)
1884 itemContent:view_DatePicker
1887 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
1888 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
1890 [toolbarItem setMinSize:[view_DatePicker frame].size];
1891 [toolbarItem setMaxSize:[view_DatePicker frame].size];
1892 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
1895 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
1896 withIdentifier:@"toggleemoticons"
1897 label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
1898 paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
1899 toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
1901 settingSelector:@selector(setImage:)
1902 itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
1903 action:@selector(toggleEmoticonFiltering:)
1906 [[self window] setToolbar:toolbar];
1908 [self configureDateFilter];
1911 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
1913 return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
1916 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
1918 return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
1919 @"delete", @"toggleemoticons", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
1923 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
1925 return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
1926 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
1927 NSToolbarSpaceItemIdentifier,
1928 NSToolbarFlexibleSpaceItemIdentifier,
1929 NSToolbarCustomizeToolbarItemIdentifier,
1930 NSToolbarPrintItemIdentifier, nil]];
1933 - (void)toolbarWillAddItem:(NSNotification *)notification
1935 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
1936 if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
1937 [item setTarget:self];
1938 [item setAction:@selector(adiumPrint:)];
1942 #pragma mark Date filter
1945 * @brief Returns a menu item for the date type filter menu
1947 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
1949 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInt:dateType]]
1950 action:@selector(selectDateType:)
1952 [menuItem setTag:dateType];
1954 return [menuItem autorelease];
1957 - (NSMenu *)dateTypeMenu
1959 NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
1960 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInt:AIDateTypeAnyDate],
1961 AILocalizedString(@"Today", nil), [NSNumber numberWithInt:AIDateTypeToday],
1962 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInt:AIDateTypeSinceYesterday],
1963 AILocalizedString(@"This Week", nil), [NSNumber numberWithInt:AIDateTypeThisWeek],
1964 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoWeeks],
1965 AILocalizedString(@"This Month", nil), [NSNumber numberWithInt:AIDateTypeThisMonth],
1966 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInt:AIDateTypeWithinLastTwoMonths],
1968 NSMenu *dateTypeMenu = [[NSMenu alloc] init];
1969 AIDateType dateType;
1971 [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
1972 [dateTypeMenu addItem:[NSMenuItem separatorItem]];
1974 for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
1975 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
1978 return [dateTypeMenu autorelease];
1981 - (int)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
1983 int todayDayOfWeek = [today dayOfWeek];
1985 //Try to look at the iCal preferences if possible
1986 if (!iCalFirstDayOfWeekDetermined) {
1987 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
1988 if (iCalFirstDayOfWeek) {
1989 //This should return a CFNumberRef... we're using another app's prefs, so make sure.
1990 if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
1991 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek intValue];
1994 CFRelease(iCalFirstDayOfWeek);
1998 iCalFirstDayOfWeekDetermined = YES;
2001 return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2005 * @brief A new date type was selected
2007 * This does not start a search
2009 - (void)selectedDateType:(AIDateType)dateType
2011 NSCalendarDate *today = [NSCalendarDate date];
2013 [filterDate release]; filterDate = nil;
2016 case AIDateTypeAnyDate:
2017 filterDateType = AIDateTypeAnyDate;
2020 case AIDateTypeToday:
2021 filterDateType = AIDateTypeExactly;
2022 filterDate = [today retain];
2025 case AIDateTypeSinceYesterday:
2026 filterDateType = AIDateTypeAfter;
2027 filterDate = [[today dateByAddingYears:0
2030 hours:-[today hourOfDay]
2031 minutes:-[today minuteOfHour]
2032 seconds:-([today secondOfMinute] + 1)] retain];
2035 case AIDateTypeThisWeek:
2036 filterDateType = AIDateTypeAfter;
2037 filterDate = [[today dateByAddingYears:0
2039 days:-[self daysSinceStartOfWeekGivenToday:today]
2040 hours:-[today hourOfDay]
2041 minutes:-[today minuteOfHour]
2042 seconds:-([today secondOfMinute] + 1)] retain];
2045 case AIDateTypeWithinLastTwoWeeks:
2046 filterDateType = AIDateTypeAfter;
2047 filterDate = [[today dateByAddingYears:0
2050 hours:-[today hourOfDay]
2051 minutes:-[today minuteOfHour]
2052 seconds:-([today secondOfMinute] + 1)] retain];
2055 case AIDateTypeThisMonth:
2056 filterDateType = AIDateTypeAfter;
2057 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2059 days:-[today dayOfMonth]
2062 seconds:-1] retain];
2065 case AIDateTypeWithinLastTwoMonths:
2066 filterDateType = AIDateTypeAfter;
2067 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2069 days:-[today dayOfMonth]
2072 seconds:-1] retain];
2081 * @brief Select the date type
2083 - (void)selectDateType:(id)sender
2085 [self selectedDateType:[sender tag]];
2086 [self startSearchingClearingCurrentResults:YES];
2089 - (void)configureDateFilter
2091 firstDayOfWeek = 0; /* Sunday */
2092 iCalFirstDayOfWeekDetermined = NO;
2094 [popUp_dateFilter setMenu:[self dateTypeMenu]];
2095 int index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2096 if(index != NSNotFound)
2097 [popUp_dateFilter selectItemAtIndex:index];
2098 [self selectedDateType:AIDateTypeAnyDate];
2101 #pragma mark Open Log
2103 - (void)openLogAtPath:(NSString *)inPath
2105 AIChatLog *chatLog = nil;
2106 NSString *basePath = [AILoggerPlugin logBasePath];
2108 //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2109 NSArray *pathComponents = [inPath pathComponents];
2110 int lastIndex = [pathComponents count];
2111 NSString *logName = [pathComponents objectAtIndex:--lastIndex];
2112 NSString *contactName = [pathComponents objectAtIndex:--lastIndex];
2113 NSString *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];
2114 NSString *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2116 NSString *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2117 //Filter for logs from the contact associated with the log we're loading
2118 [self filterForContact:[[adium contactController] contactWithService:[[adium accountController] firstServiceWithServiceID:serviceID]
2122 NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2123 NSString *canonicalInPath = [inPath stringByStandardizingPath];
2125 if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2126 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2128 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2131 /* Different Adium user... this sucks. We're given a path like this:
2132 * /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2133 * and we want to make it relative to our current user's logs folder, which might be
2134 * /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2136 * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2138 NSString *fakeRelativePath = @"";
2140 //Use .. to get back to the root from the base path
2141 int componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2142 for (int i = 0; i < componentsOfBasePath; i++) {
2143 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2146 //Now add the path from the root to the actual log
2147 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2148 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2149 from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2151 serviceClass:serviceID] autorelease];
2154 //Now display the requested log
2156 [self displayLog:chatLog];
2160 #pragma mark Printing
2162 - (void)adiumPrint:(id)sender
2164 [textView_content print:sender];
2167 - (BOOL)validatePrintMenuItem:(id <NSMenuItem>)menuItem
2169 return ([displayedLogArray count] > 0);
2172 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2174 if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2175 return [self validatePrintMenuItem:nil];
2182 - (void)selectCachedIndex
2184 int numberOfRows = [tableView_results numberOfRows];
2186 if (cachedSelectionIndex < numberOfRows) {
2187 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2188 byExtendingSelection:NO];
2191 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2192 byExtendingSelection:NO];
2196 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2199 deleteOccurred = NO;
2202 #pragma mark Deletion
2205 * @brief Get an NSAlert to request deletion of multiple logs
2207 - (NSAlert *)alertForDeletionOfLogCount:(int)logCount
2209 NSAlert *alert = [[NSAlert alloc] init];
2210 [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2211 [alert setInformativeText:[NSString stringWithFormat:
2212 AILocalizedString(@"Are you sure you want to send %i logs to the Trash?",nil), logCount]];
2213 [alert addButtonWithTitle:DELETE];
2214 [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2216 return [alert autorelease];
2220 * @brief Undo the deletion of one or more AIChatLogs
2222 * The logs will be marked for readdition to the index
2224 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2226 NSEnumerator *enumerator;
2228 NSFileManager *fileManager = [NSFileManager defaultManager];
2229 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2231 enumerator = [deletedLogs objectEnumerator];
2232 while ((aLog = [enumerator nextObject])) {
2233 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2235 [fileManager createDirectoriesForPath:[logPath stringByDeletingLastPathComponent]];
2237 [fileManager movePath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2241 [plugin markLogDirtyAtPath:logPath];
2244 [self rebuildIndices];
2247 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2249 NSArray *selectedLogs = (NSArray *)contextInfo;
2250 if (returnCode == NSAlertFirstButtonReturn) {
2254 NSEnumerator *enumerator;
2255 NSMutableSet *logPaths = [NSMutableSet set];
2257 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2259 enumerator = [selectedLogs objectEnumerator];
2260 while ((aLog = [enumerator nextObject])) {
2261 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2263 [[adium notificationCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2264 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@/%@",[aLog serviceClass],[aLog from],[aLog to]]];
2265 [logToGroup trashLog:aLog];
2267 //Clear the to group out if it no longer has anything of interest
2268 if ([logToGroup logCount] == 0) {
2269 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[aLog serviceClass],[aLog from]]];
2270 [logFromGroup removeToGroup:logToGroup];
2273 [logPaths addObject:logPath];
2274 [currentSearchResults removeObjectIdenticalTo:aLog];
2277 [plugin removePathsFromIndex:logPaths];
2279 [undoManager registerUndoWithTarget:self
2280 selector:@selector(restoreDeletedLogs:)
2281 object:selectedLogs];
2282 [undoManager setActionName:DELETE];
2284 [resultsLock unlock];
2285 [tableView_results reloadData];
2287 deleteOccurred = YES;
2289 [self rebuildContactsList];
2290 [self updateProgressDisplay];
2292 [selectedLogs release];
2296 * @brief Delete logs
2298 * If two or more logs are passed, confirmation will be requested.
2299 * This operation registers with the window controller's undo manager.
2301 * @param selectedLogs An NSArray of logs to delete
2303 - (void)deleteLogs:(NSArray *)selectedLogs
2305 if ([selectedLogs count] > 1) {
2306 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2307 [alert beginSheetModalForWindow:[self window]
2309 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2310 contextInfo:[selectedLogs retain]];
2312 [self deleteLogsAlertDidEnd:nil
2313 returnCode:NSAlertFirstButtonReturn
2314 contextInfo:[selectedLogs retain]];
2319 * @brief Returns a set of all selected to groups on all accounts
2321 * @param logCount If non-NULL, will be set to the total number of logs on return
2323 - (NSSet *)allSelectedToGroups:(int *)totalLogCount
2325 NSEnumerator *fromEnumerator;
2326 AILogFromGroup *fromGroup;
2327 NSMutableSet *allToGroups = [NSMutableSet set];
2329 if (totalLogCount) *totalLogCount = 0;
2331 //Walk through every 'from' group
2332 fromEnumerator = [logFromGroupDict objectEnumerator];
2333 while ((fromGroup = [fromEnumerator nextObject])) {
2334 NSEnumerator *toEnumerator;
2335 AILogToGroup *toGroup;
2337 //Walk through every 'to' group
2338 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2339 while ((toGroup = [toEnumerator nextObject])) {
2340 if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2341 if (totalLogCount) {
2342 *totalLogCount += [toGroup logCount];
2345 [allToGroups addObject:toGroup];
2354 * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2356 * The logs will be marked for readdition to the index
2358 - (void)restoreDeletedToGroups:(NSSet *)toGroups
2360 NSEnumerator *enumerator;
2361 AILogToGroup *toGroup;
2362 NSFileManager *fileManager = [NSFileManager defaultManager];
2363 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2364 NSString *logBasePath = [AILoggerPlugin logBasePath];
2366 enumerator = [toGroups objectEnumerator];
2367 while ((toGroup = [enumerator nextObject])) {
2368 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup path]];
2370 [fileManager createDirectoriesForPath:[toGroupPath stringByDeletingLastPathComponent]];
2371 if ([fileManager fileExistsAtPath:toGroupPath]) {
2372 AILog(@"Removing path %@ to make way for %@",
2373 toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2374 [fileManager removeFileAtPath:toGroupPath
2377 [fileManager movePath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2381 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2384 while ((aLog = [logEnumerator nextObject])) {
2385 [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog path]]];
2389 [self rebuildIndices];
2392 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo;
2394 NSSet *allSelectedToGroups = (NSSet *)contextInfo;
2395 if (returnCode == NSAlertFirstButtonReturn) {
2396 AILogToGroup *logToGroup;
2397 NSEnumerator *enumerator;
2398 NSMutableSet *logPaths = [NSMutableSet set];
2400 enumerator = [allSelectedToGroups objectEnumerator];
2401 while ((logToGroup = [enumerator nextObject])) {
2402 NSEnumerator *logEnumerator;
2405 logEnumerator = [logToGroup logEnumerator];
2406 while ((aLog = [logEnumerator nextObject])) {
2407 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog path]];
2408 [logPaths addObject:logPath];
2411 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2412 [logFromGroup removeToGroup:logToGroup];
2415 [plugin removePathsFromIndex:logPaths];
2417 [undoManager registerUndoWithTarget:self
2418 selector:@selector(restoreDeletedToGroups:)
2419 object:allSelectedToGroups];
2420 [undoManager setActionName:DELETE];
2422 [self rebuildIndices];
2423 [self updateProgressDisplay];
2426 [allSelectedToGroups release];
2430 * @brief Delete entirely the logs of all contacts selected in the source list
2432 * Confirmation by the user will be required.
2434 * Note: A single item in the source list may have multiple associated AILogToGroups.
2436 - (void)deleteSelectedContactsFromSourceList
2439 NSSet *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2441 if (totalLogCount > 1) {
2442 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2443 [alert beginSheetModalForWindow:[self window]
2445 didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2446 contextInfo:[allSelectedToGroups retain]];
2448 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2449 returnCode:NSAlertFirstButtonReturn
2450 contextInfo:[allSelectedToGroups retain]];
2455 * @brief Delete the current selection
2457 * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2458 * If anything else is selected, the currently selected search result logs will be trashed.
2460 - (void)deleteSelection:(id)sender
2462 if ([[self window] firstResponder] == outlineView_contacts) {
2463 [self deleteSelectedContactsFromSourceList];
2467 NSArray *selectedLogs = [tableView_results arrayOfSelectedItemsUsingSourceArray:currentSearchResults];
2468 [resultsLock unlock];
2470 [self deleteLogs:selectedLogs];
2476 * @brief Supply our undo manager when we are within the responder chain
2478 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender