Patch from wildwobby which confirms before removing logs when hitting the delete key. Fixes #13097.
2 // AILogViewerWindowController.m
5 // Created by Evan Schoenberg on 3/24/06.
8 #import "AILogViewerWindowController.h"
10 #import "AILogFromGroup.h"
11 #import "AILogToGroup.h"
12 #import "AILoggerPlugin.h"
13 #import "ESRankingCell.h"
14 #import "AIXMLChatlogConverter.h"
15 #import "AILogDateFormatter.h"
17 #import <Adium/AIAccountControllerProtocol.h>
18 #import <Adium/AIChatControllerProtocol.h>
19 #import <Adium/AIContactControllerProtocol.h>
20 #import <Adium/AIContentControllerProtocol.h>
21 #import <Adium/AIMenuControllerProtocol.h>
22 #import <Adium/AIHTMLDecoder.h>
23 #import <Adium/AIInterfaceControllerProtocol.h>
24 #import <Adium/AIListContact.h>
25 #import <Adium/AIMetaContact.h>
26 #import <Adium/AIService.h>
27 #import <Adium/AIServiceIcons.h>
28 #import <Adium/AIUserIcons.h>
29 #import <Adium/KNShelfSplitView.h>
30 #import <AIUtilities/AIArrayAdditions.h>
31 #import <AIUtilities/AIAttributedStringAdditions.h>
32 #import <AIUtilities/AIDateFormatterAdditions.h>
33 #import <AIUtilities/AIFileManagerAdditions.h>
34 #import <AIUtilities/AIImageAdditions.h>
35 #import <AIUtilities/AIImageTextCell.h>
36 #import <AIUtilities/AIOutlineViewAdditions.h>
37 #import <AIUtilities/AISplitView.h>
38 #import <AIUtilities/AIStringAdditions.h>
39 #import <AIUtilities/AITableViewAdditions.h>
40 #import <AIUtilities/AITextAttributes.h>
41 #import <AIUtilities/AIToolbarUtilities.h>
42 #import <AIUtilities/AIApplicationAdditions.h>
43 #import <AIUtilities/AIDividedAlternatingRowOutlineView.h>
45 #define KEY_LOG_VIEWER_WINDOW_FRAME @"Log Viewer Frame"
46 #define KEY_LOG_VIEWER_GROUP_STATE @"Log Viewer Group State" //Expand/Collapse state of groups
47 #define TOOLBAR_LOG_VIEWER @"Log Viewer Toolbar"
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)
63 #define HIDE_TIMESTAMPS AILocalizedString(@"Hide Timestamps",nil)
64 #define SHOW_TIMESTAMPS AILocalizedString(@"Show Timestamps",nil)
66 #define IMAGE_EMOTICONS_OFF @"emoticon32"
67 #define IMAGE_EMOTICONS_ON @"emoticon32_transparent"
68 #define IMAGE_TIMESTAMPS_OFF @"timestamp32"
69 #define IMAGE_TIMESTAMPS_ON @"timestamp32_transparent"
72 #define REFRESH_RESULTS_INTERVAL 1.0 //Interval between results refreshes while searching
74 @interface AILogViewerWindowController ()
75 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin;
76 - (void)initLogFiltering;
77 - (void)displayLog:(AIChatLog *)log;
78 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange;
79 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction;
80 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults;
81 - (void)buildSearchMenu;
82 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode;
83 - (void)_logFilter:(NSString *)searchString searchID:(NSInteger)searchID mode:(LogSearchMode)mode;
84 - (void)installToolbar;
85 - (void)updateRankColumnVisibility;
86 - (void)openLogAtPath:(NSString *)inPath;
87 - (void)rebuildContactsList;
88 - (void)filterForContact:(AIListContact *)inContact;
89 - (void)filterForChatName:(NSString *)chatName withAccount:(AIAccount *)account;
90 - (void)selectCachedIndex;
92 - (void)_willOpenForContact;
93 - (void)_didOpenForContact;
95 - (void)deleteSelection:(id)sender;
98 @implementation AILogViewerWindowController
100 static AILogViewerWindowController *sharedLogViewerInstance = nil;
101 static NSInteger toArraySort(id itemA, id itemB, void *context);
103 + (NSString *)nibName
108 + (id)openForPlugin:(id)inPlugin
110 if (!sharedLogViewerInstance) {
111 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
114 [sharedLogViewerInstance showWindow:nil];
116 return sharedLogViewerInstance;
119 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
121 [self openForPlugin:inPlugin];
123 [sharedLogViewerInstance openLogAtPath:inPath];
125 return sharedLogViewerInstance;
128 //Open the log viewer window to a specific contact's logs
129 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
131 if (!sharedLogViewerInstance) {
132 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
135 [sharedLogViewerInstance _willOpenForContact];
136 [sharedLogViewerInstance showWindow:nil];
137 [sharedLogViewerInstance filterForContact:inContact];
138 [sharedLogViewerInstance _didOpenForContact];
140 return sharedLogViewerInstance;
143 + (id)openForChatName:(NSString *)inChatName withAccount:(AIAccount *)inAccount plugin:(id)inPlugin
145 if (!sharedLogViewerInstance) {
146 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
149 [sharedLogViewerInstance _willOpenForContact];
150 [sharedLogViewerInstance showWindow:nil];
151 [sharedLogViewerInstance filterForChatName:inChatName withAccount:inAccount];
152 [sharedLogViewerInstance _didOpenForContact];
154 return sharedLogViewerInstance;
157 //Returns the window controller if one exists
158 + (id)existingWindowController
160 return sharedLogViewerInstance;
163 //Close the log viewer window
164 + (void)closeSharedInstance
166 if (sharedLogViewerInstance) {
167 [sharedLogViewerInstance closeWindow:nil];
172 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
174 if((self = [super initWithWindowNibName:windowNibName])) {
176 selectedColumn = nil;
179 automaticSearch = YES;
181 showTimestamps = YES;
182 activeSearchString = nil;
183 displayedLogArray = nil;
184 windowIsClosing = NO;
185 desiredContactsSourceListDeltaX = 0;
187 blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
190 searchMode = LOG_SEARCH_CONTENT;
192 headerDateFormatter = [[NSDateFormatter localizedDateFormatter] retain];
194 currentSearchResults = [[NSMutableArray alloc] init];
195 fromArray = [[NSMutableArray alloc] init];
196 fromServiceArray = [[NSMutableArray alloc] init];
197 logFromGroupDict = [[NSMutableDictionary alloc] init];
198 toArray = [[NSMutableArray alloc] init];
199 toServiceArray = [[NSMutableArray alloc] init];
200 logToGroupDict = [[NSMutableDictionary alloc] init];
201 resultsLock = [[NSRecursiveLock alloc] init];
202 searchingLock = [[NSLock alloc] init];
203 [searchingLock setName:@"LogSearchingLock"];
204 contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
206 allContactsIdentifier = [[NSNumber numberWithInteger:-1] retain];
208 undoManager = [[NSUndoManager alloc] init];
209 currentSearchLock = [[NSLock alloc] init];
210 [currentSearchLock setName:@"CurrentLogSearchLock"];
219 [filterDate release]; filterDate = nil;
220 [currentSearchLock release]; currentSearchLock = nil;
221 [resultsLock release];
222 [searchingLock release];
224 [fromServiceArray release];
226 [toServiceArray release];
227 [currentSearchResults release];
228 [selectedColumn release];
229 [headerDateFormatter release];
230 [displayedLogArray release];
231 [blankImage release];
232 [activeSearchString release];
233 [contactIDsToFilter release];
235 [logFromGroupDict release]; logFromGroupDict = nil;
236 [logToGroupDict release]; logToGroupDict = nil;
238 [filterForAccountName release]; filterForAccountName = nil;
240 [horizontalRule release]; horizontalRule = nil;
242 [adiumIcon release]; adiumIcon = nil;
243 [adiumIconHighlighted release]; adiumIconHighlighted = nil;
245 //We loaded view_DatePicker from a nib manually, so we must release it
246 [view_DatePicker release]; view_DatePicker = nil;
248 [allContactsIdentifier release];
249 [undoManager release]; undoManager = nil;
254 //Init our log filtering tree
255 - (void)initLogFiltering
257 NSMutableDictionary *toDict = [NSMutableDictionary dictionary];
258 NSString *basePath = [AILoggerPlugin logBasePath];
259 NSString *fromUID, *serviceClass;
261 //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
262 //first by service, then by account name.
263 for (NSString *folderName in [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:basePath error:NULL] sortedArrayUsingSelector:@selector(compare:)]) {
264 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
265 AILogFromGroup *logFromGroup;
266 NSMutableSet *toSetForThisService;
267 NSArray *serviceAndFromUIDArray;
269 /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
270 * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
272 serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
274 if ([serviceAndFromUIDArray count] >= 2) {
275 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
277 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
278 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
280 //Fallback: blank non-nil serviceClass; folderName as the fromUID
282 fromUID = folderName;
285 logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
287 //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
288 [logFromGroupDict setObject:logFromGroup forKey:folderName];
291 if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
292 toSetForThisService = [NSMutableSet set];
293 [toDict setObject:toSetForThisService
294 forKey:serviceClass];
297 //Add the 'to' for each grouping on this account
298 for (AILogToGroup *currentToGroup in [logFromGroup toGroupArray]) {
301 if ((currentTo = [currentToGroup to])) {
302 //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
303 [logToGroupDict setObject:currentToGroup forKey:[currentToGroup relativePath]];
307 [logFromGroup release];
311 [self rebuildContactsList];
314 - (void)rebuildContactsList
316 NSInteger oldCount = toArray.count;
317 [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
319 for (AILogFromGroup *logFromGroup in [logFromGroupDict objectEnumerator]) {
320 //Add the 'to' for each grouping on this account
321 for (AILogToGroup *currentToGroup in [logFromGroup toGroupArray]) {
324 if ((currentTo = [currentToGroup to])) {
325 NSString *serviceClass = [currentToGroup serviceClass];
326 AIListObject *listObject = ((serviceClass && currentTo) ?
327 [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
330 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
331 AIListContact *parentContact = [(AIListContact *)listObject parentContact];
332 if (![toArray containsObjectIdenticalTo:parentContact]) {
333 [toArray addObject:parentContact];
337 if (![toArray containsObject:currentToGroup]) {
338 [toArray addObject:currentToGroup];
345 [toArray sortUsingFunction:toArraySort context:NULL];
346 [outlineView_contacts reloadData];
348 if (!isOpeningForContact) {
349 //If we're opening for a contact, the outline view selection will be changed in a moment anyways
350 [self outlineViewSelectionDidChange:nil];
354 - (NSString *)adiumFrameAutosaveName
356 return KEY_LOG_VIEWER_WINDOW_FRAME;
359 //Setup the window before it is displayed
360 - (void)windowDidLoad
362 suppressSearchRequests = YES;
364 [super windowDidLoad];
366 [plugin pauseIndexing];
368 [[self window] setTitle:AILocalizedString(@"Chat Transcript Viewer",nil)];
369 [textField_progress setStringValue:@""];
371 //Autosave doesn't do anything yet
372 [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
373 [shelf_splitView setFrame:[[[self window] contentView] frame]];
375 // Pull our main article/display split view out of the nib and position it in the shelf view
376 [containingView_results retain];
377 [containingView_results removeFromSuperview];
378 [shelf_splitView setContentView:containingView_results];
379 [containingView_results release];
380 [tableView_results accessibilitySetOverrideValue:AILocalizedString(@"Transcripts", nil)
381 forAttribute:NSAccessibilityRoleDescriptionAttribute];
383 // Pull our source view out of the nib and position it in the shelf view
384 [containingView_contactsSourceList retain];
385 [containingView_contactsSourceList removeFromSuperview];
386 [shelf_splitView setShelfView:containingView_contactsSourceList];
387 [outlineView_contacts accessibilitySetOverrideValue:AILocalizedString(@"Contacts", nil)
388 forAttribute:NSAccessibilityRoleDescriptionAttribute];
389 [containingView_contactsSourceList release];
391 //Set emoticon filtering
392 showEmoticons = [[adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
393 group:PREF_GROUP_LOGGING] boolValue];
394 [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
395 [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
397 // Set timestamp filtering
398 showTimestamps = [[adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_TIMESTAMPS
399 group:PREF_GROUP_LOGGING] boolValue];
400 [[toolbarItems objectForKey:@"toggletimestamps"] setLabel:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)];
401 [[toolbarItems objectForKey:@"toggletimestamps"] setImage:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]];
404 [self installToolbar];
406 [outlineView_contacts setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleSourceList];
408 AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
409 NSTableColumn *tableColumn = [[outlineView_contacts tableColumns] objectAtIndex:0];
410 [tableColumn setDataCell:dataCell];
411 [tableColumn setEditable:NO];
412 [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
415 // Set the selector for doubleAction
416 [outlineView_contacts setDoubleAction:@selector(openChatOnDoubleAction:)];
418 //Localize tableView_results column headers
419 [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
420 [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
421 [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
422 [self tableViewColumnDidResize:nil];
424 [tableView_results sizeLastColumnToFit];
426 //Prepare the search controls
427 [self buildSearchMenu];
428 if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
429 [textView_content setUsesFindPanel:YES];
432 //Sort by preference, defaulting to sorting by date
433 NSString *selectedTableColumnPref;
434 if ((selectedTableColumnPref = [adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
435 group:PREF_GROUP_LOGGING])) {
436 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
438 if (!selectedColumn) {
439 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
441 [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
443 //Prepare indexing and filter searching
444 [plugin prepareLogContentSearching];
445 [self initLogFiltering];
447 //Begin our initial search
448 [self setSearchMode:LOG_SEARCH_TO];
450 [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
451 suppressSearchRequests = NO;
453 if (!isOpeningForContact) {
454 //If we're opening for a contact, we'll select it and then begin searching
455 [self startSearchingClearingCurrentResults:YES];
458 [tableView_results setAutosaveName:@"LogViewerResults"];
459 [tableView_results setAutosaveTableColumns:YES];
461 [plugin resumeIndexing];
464 -(void)rebuildIndices
466 //Rebuild the 'global' log indexes
467 [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
468 [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
469 [toServiceArray removeAllObjects];
470 [fromArray removeAllObjects];
471 [fromServiceArray removeAllObjects];
473 [self initLogFiltering];
475 [tableView_results reloadData];
476 [self selectDisplayedLog];
479 //Called as the window closes
480 - (void)windowWillClose:(id)sender
482 [super windowWillClose:sender];
484 //Set preference for emoticon filtering
485 [adium.preferenceController setPreference:[NSNumber numberWithBool:showEmoticons]
486 forKey:KEY_LOG_VIEWER_EMOTICONS
487 group:PREF_GROUP_LOGGING];
489 // Set preference for timestamp filtering
490 [adium.preferenceController setPreference:[NSNumber numberWithBool:showTimestamps]
491 forKey:KEY_LOG_VIEWER_TIMESTAMPS
492 group:PREF_GROUP_LOGGING];
494 //Set preference for selected column
495 [adium.preferenceController setPreference:[selectedColumn identifier]
496 forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
497 group:PREF_GROUP_LOGGING];
499 /* Disable the search field. If we don't disable the search field, it will often try to call its target action
500 * after the window has closed (and we are gone). I'm not sure why this happens, but disabling the field
501 * before we close the window down seems to prevent the crash.
503 [searchField_logs setEnabled:NO];
505 /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
506 * it was gone, like responding to a logIndexUpdated message
508 windowIsClosing = YES;
510 //Abort any in-progress searching and indexing, and wait for their completion
511 [self stopSearching];
512 [plugin cleanUpLogContentSearching];
514 //Reset our column widths if needed
515 [activeSearchString release]; activeSearchString = nil;
516 [self updateRankColumnVisibility];
518 [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
519 [toolbarItems autorelease]; toolbarItems = nil;
522 //Display --------------------------------------------------------------------------------------------------------------
524 //Update log viewer progress string to reflect current status
525 - (void)updateProgressDisplay
527 NSMutableString *progress = nil;
528 NSUInteger indexNumber, indexTotal;
531 //We always convey the number of logs being displayed
533 NSUInteger count = [currentSearchResults count];
534 if (activeSearchString && [activeSearchString length]) {
535 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
536 AILocalizedString(@"%lu matching transcripts",nil) :
537 AILocalizedString(@"1 matching transcript",nil)),count]];
539 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
540 AILocalizedString(@"%lu transcripts",nil) :
541 AILocalizedString(@"1 transcript",nil)),count]];
543 //We are searching, but there is no active search string. This indicates we're still opening logs.
545 progress = [[AILocalizedString(@"Opening transcripts",nil) mutableCopy] autorelease];
548 [resultsLock unlock];
550 indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
552 //Append search progress
553 if (activeSearchString && [activeSearchString length]) {
555 [progress appendString:@" - "];
557 progress = [NSMutableString string];
560 if (searching || indexing) {
561 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
563 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];
567 //Append indexing progress
570 [progress appendString:@" - "];
572 progress = [NSMutableString string];
575 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %lu of %lu transcripts",nil), indexNumber, indexTotal]];
578 if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
579 [progress appendString:[NSString ellipsis]];
582 //Enable/disable the searching animation
583 if (searching || indexing) {
584 [progressIndicator startAnimation:nil];
586 [progressIndicator stopAnimation:nil];
589 [textField_progress setStringValue:(progress ? progress : @"")];
592 //The plugin is informing us that the log indexing changed
593 - (void)logIndexingProgressUpdate
595 //Don't do anything if the window is already closing
596 if (!windowIsClosing) {
597 [self updateProgressDisplay];
599 //If we are searching by content, we should re-search without clearing our current results so the
600 //the newly-indexed logs can be added without blanking the current table contents.
601 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
603 //We're already searching; reattempt when done
604 searchIDToReattemptWhenComplete = activeSearchID;
606 //We're not searching - restart the search immediately every 10 updates to utilize the newly indexed logs
607 indexingUpdatesReceivedWhileSearching++;
608 if ((indexingUpdatesReceivedWhileSearching % 10) == 0)
609 [self startSearchingClearingCurrentResults:NO];
615 //Refresh the results table
616 - (void)refreshResults
618 [self updateProgressDisplay];
620 [self refreshResultsSearchIsComplete:NO];
623 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
626 NSInteger count = [currentSearchResults count];
627 [resultsLock unlock];
628 AILog(@"refreshResultsSearchIsComplete: %i (count is %i)",searchIsComplete,count);
629 if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
630 //Sort the logs correctly which will also reload the table
633 if (searchIsComplete && automaticSearch) {
634 //If search is complete, select the first log if requested and possible
635 [self selectFirstLog];
638 BOOL oldAutomaticSearch = automaticSearch;
640 //We don't want the above re-selection to change our automaticSearch tracking
641 //(The only reason automaticSearch should change is in response to user action)
642 automaticSearch = oldAutomaticSearch;
646 if (searchIsComplete &&
647 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
648 searchIDToReattemptWhenComplete = -1;
649 [self startSearchingClearingCurrentResults:NO];
653 [self selectCachedIndex];
656 [self updateProgressDisplay];
659 - (void)searchComplete
661 [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
662 [self refreshResultsSearchIsComplete:YES];
665 // Called on doubleAction to open a chat
666 -(void)openChatOnDoubleAction:(id)sender
668 id item = [outlineView_contacts firstSelectedItem];
669 if ([item isKindOfClass:[AIListContact class]]) {
670 //Open a new message with the contact
671 [adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)item onPreferredAccount:YES]];
675 //Displays the contents of the specified log in our window
676 - (void)displayLogs:(NSArray *)logArray;
678 NSMutableAttributedString *displayText = nil;
679 NSAttributedString *finalDisplayText = nil;
680 NSRange scrollRange = NSMakeRange(0,0);
681 BOOL appendedFirstLog = NO;
683 if (![logArray isEqualToArray:displayedLogArray]) {
684 [displayedLogArray release];
685 displayedLogArray = [logArray copy];
688 if ([logArray count] > 1) {
689 displayText = [[NSMutableAttributedString alloc] init];
693 NSString *logBasePath = [AILoggerPlugin logBasePath];
694 AILog(@"Displaying %@",logArray);
695 for (theLog in logArray) {
696 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
699 if (!horizontalRule) {
700 #define HORIZONTAL_BAR 0x2013
701 #define HORIZONTAL_RULE_LENGTH 18
703 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
704 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
705 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
706 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
708 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
711 [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
712 (appendedFirstLog ? @"\n" : @""),
714 [headerDateFormatter stringFromDate:[theLog date]],
717 withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
720 if ([[theLog relativePath] hasSuffix:@".AdiumHTMLLog"] || [[theLog relativePath] hasSuffix:@".html"] || [[theLog relativePath] hasSuffix:@".html.bak"]) {
722 NSURL *logURL = [NSURL fileURLWithPath:[logBasePath stringByAppendingPathComponent:[theLog relativePath]]];
723 NSString *logFileText = [NSString stringWithContentsOfURL:logURL encoding:NSUTF8StringEncoding error:NULL];
724 NSAttributedString *attributedLogFileText = [AIHTMLDecoder decodeHTML:logFileText];
727 attributedLogFileText = [adium.contentController filterAttributedString:attributedLogFileText
728 usingFilterType:AIFilterMessageDisplay
729 direction:AIFilterOutgoing
734 [displayText appendAttributedString:attributedLogFileText];
736 displayText = [attributedLogFileText mutableCopy];
739 } else if ([[theLog relativePath] hasSuffix:@".chatlog"]){
741 NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog relativePath]];
744 if ([[NSFileManager defaultManager] fileExistsAtPath:logFullPath isDirectory:&isDir]) {
745 /* If we have a chatLog bundle, we want to get the text content for the xml file inside */
746 if (isDir) logFullPath = [logFullPath stringByAppendingPathComponent:
747 [[[logFullPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
750 NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
751 [NSNumber numberWithBool:showTimestamps], @"showTimestamps",
752 [NSNumber numberWithBool:showEmoticons], @"showEmoticons",
754 NSAttributedString *attributedLogFileText = [AIXMLChatlogConverter readFile:logFullPath withOptions:options];
755 if (attributedLogFileText) {
757 [displayText appendAttributedString:attributedLogFileText];
759 displayText = [attributedLogFileText mutableCopy];
763 //Fallback: Plain text log
764 NSURL *logURL = [NSURL fileURLWithPath:[logBasePath stringByAppendingPathComponent:[theLog relativePath]]];
765 NSString *logFileText = [NSString stringWithContentsOfURL:logURL encoding:NSUTF8StringEncoding error:NULL];
767 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
768 NSAttributedString *attributedLogFileText = [[[NSAttributedString alloc] initWithString:logFileText
769 attributes:[textAttributes dictionary]] autorelease];
771 attributedLogFileText = [adium.contentController filterAttributedString:attributedLogFileText
772 usingFilterType:AIFilterMessageDisplay
773 direction:AIFilterOutgoing
778 [displayText appendAttributedString:attributedLogFileText];
780 displayText = [attributedLogFileText mutableCopy];
785 appendedFirstLog = YES;
790 if (displayText && [displayText length]) {
791 //Add pretty formatting to links
792 [displayText addFormattingForLinks];
794 //If we are searching by content, highlight the search results
795 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
796 NSString *searchWord;
797 NSMutableArray *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
798 NSScanner *scanner = [NSScanner scannerWithString:activeSearchString];
800 //Look for an initial quote
801 NSAutoreleasePool *pool = nil;
802 while (![scanner isAtEnd]) {
804 pool = [[NSAutoreleasePool alloc] init];
806 [scanner scanUpToString:@"\"" intoString:NULL];
808 //Scan past the quote
809 if (![scanner scanString:@"\"" intoString:NULL]) {
810 [pool release]; pool = nil;
814 NSString *quotedString;
816 if (![scanner isAtEnd] &&
817 [scanner scanUpToString:@"\"" intoString:"edString]) {
818 //Scan past the quote
819 [scanner scanString:@"\"" intoString:NULL];
820 /* If a string within quotes is found, remove the words from the quoted string and add the full string
821 * to what we'll be highlighting.
823 * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
824 * "killer attack ninja kittens" OR ninja
825 * wouldn't highlight the word ninja by itself.
827 NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
828 NSInteger quotedWordsCount = [quotedWords count];
830 for (NSInteger i = 0; i < quotedWordsCount; i++) {
831 NSString *quotedWord = [quotedWords objectAtIndex:i];
833 //Originally started with a quote, so put it back on
834 quotedWord = [@"\"" stringByAppendingString:quotedWord];
836 if (i == quotedWordsCount - 1) {
837 //Originally ended with a quote, so put it back on
838 quotedWord = [quotedWord stringByAppendingString:@"\""];
840 NSInteger searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
841 if (searchWordsIndex != NSNotFound) {
842 [searchWordsArray removeObjectAtIndex:searchWordsIndex];
844 NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
848 //Add the full quoted string
849 [searchWordsArray addObject:quotedString];
853 BOOL shouldScrollToWord = NO;
854 scrollRange = NSMakeRange([displayText length],0);
856 for (searchWord in searchWordsArray) {
859 //Check against and/or. We don't just remove it from the array because then we couldn't check case insensitively.
860 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
861 ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
862 [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
864 //We'll want to scroll to the first occurrance of any matching word or words
865 if (occurrence.location < scrollRange.location) {
866 scrollRange = occurrence;
867 shouldScrollToWord = YES;
872 //If we shouldn't be scrolling to a new range, we want to scroll to the top
873 if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
875 [searchWordsArray release];
878 finalDisplayText = displayText;
881 if (finalDisplayText) {
882 [[textView_content textStorage] setAttributedString:finalDisplayText];
884 //Set this string and scroll to the top/bottom/occurrence
885 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
886 [textView_content scrollRangeToVisible:scrollRange];
888 [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
892 //No log selected, empty the view
893 [textView_content setString:@""];
896 [displayText release];
899 - (void)displayLog:(AIChatLog *)theLog
901 [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
904 //Reselect the displayed log (Or another log if not possible)
905 - (void)selectDisplayedLog
907 NSInteger firstIndex = NSNotFound;
909 /* Is the log we had selected still in the table?
910 * (When performing an automatic search, we ignore the previous selection. This ensures that we always
911 * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
913 if (!automaticSearch) {
915 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
916 [resultsLock unlock];
918 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
921 if (firstIndex != NSNotFound) {
922 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
924 if (useSame == YES && sameSelection > 0) {
925 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:sameSelection] byExtendingSelection:NO];
927 [self selectFirstLog];
934 - (void)selectFirstLog
936 AIChatLog *theLog = nil;
938 //If our selected log is no more, select the first one in the list
940 if ([currentSearchResults count] != 0) {
941 theLog = [currentSearchResults objectAtIndex:0];
943 [resultsLock unlock];
945 //Change the table selection to this new log
946 //We need a little trickery here. When we change the row, the table view will call our tableViewSelectionDidChange: method.
947 //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
948 //search. As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
949 //that we instantiated this selection change, and not the user.
950 ignoreSelectionChange = YES;
951 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
952 [tableView_results scrollRowToVisible:0];
953 ignoreSelectionChange = NO;
955 [self displayLog:theLog]; //Manually update the displayed log
958 //Highlight the occurences of a search string within a displayed log
959 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
961 NSInteger location = 0;
962 NSRange searchRange, foundRange;
963 NSString *plainBigString = [bigString string];
964 NSUInteger plainBigStringLength = [plainBigString length];
965 NSMutableDictionary *attributeDictionary = nil;
967 outRange->location = NSNotFound;
969 //Search for the little string in the big string
970 while (location != NSNotFound && location < plainBigStringLength) {
971 searchRange = NSMakeRange(location, plainBigStringLength-location);
972 foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
974 //Bold and color this match
975 if (foundRange.location != NSNotFound) {
976 if (outRange->location == NSNotFound) *outRange = foundRange;
978 if (!attributeDictionary) {
979 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
980 [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
981 [NSColor yellowColor], NSBackgroundColorAttributeName,
984 [bigString addAttributes:attributeDictionary
988 location = NSMaxRange(foundRange);
993 //Sorting --------------------------------------------------------------------------------------------------------------
997 NSString *identifier = [selectedColumn identifier];
1001 if ([identifier isEqualToString:@"To"]) {
1002 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
1004 } else if ([identifier isEqualToString:@"From"]) {
1005 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
1007 } else if ([identifier isEqualToString:@"Date"]) {
1008 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
1010 } else if ([identifier isEqualToString:@"Rank"]) {
1011 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
1013 } else if ([identifier isEqualToString:@"Service"]) {
1014 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareServiceReverse:) : @selector(compareService:))];
1017 [resultsLock unlock];
1020 [tableView_results reloadData];
1022 //Reapply the selection
1023 [self selectDisplayedLog];
1026 //Sorts the selected log array and adjusts the selected column
1027 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
1029 //If there already was a sorted column, remove the indicator image from it.
1030 if (selectedColumn && selectedColumn != tableColumn) {
1031 [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
1034 //Set the indicator image in the newly selected column
1035 [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
1036 inTableColumn:tableColumn];
1038 //Set the highlighted table column.
1039 [tableView_results setHighlightedTableColumn:tableColumn];
1040 [selectedColumn release]; selectedColumn = [tableColumn retain];
1041 sortDirection = direction;
1046 //Searching ------------------------------------------------------------------------------------------------------------
1047 #pragma mark Searching
1048 //(Jag)Change search string
1049 - (void)controlTextDidChange:(NSNotification *)notification
1051 if (searchMode != LOG_SEARCH_CONTENT) {
1052 [self updateSearch:nil];
1056 //Change search string (Called by searchfield)
1057 - (IBAction)updateSearch:(id)sender
1059 automaticSearch = NO;
1060 [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1061 AILog(@"updateSearch calling startSearching");
1062 [self startSearchingClearingCurrentResults:YES];
1065 //Change search mode (Called by mode menu)
1066 - (IBAction)selectSearchType:(id)sender
1068 automaticSearch = NO;
1070 //First, update the search mode to the newly selected type
1071 [self setSearchMode:[sender tag]];
1073 //Then, ensure we are ready to search using the current string
1074 [self setSearchString:activeSearchString];
1076 //Now we are ready to start searching
1077 AILog(@"selectSearchType calling startSearching");
1078 [self startSearchingClearingCurrentResults:YES];
1081 //Begin a specific search
1082 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1084 automaticSearch = YES;
1085 //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1086 [self setSearchMode:inMode];
1087 [self setSearchString:inString];
1089 AILog(@"setSearchString:mode: calling startSearching");
1090 [self startSearchingClearingCurrentResults:YES];
1093 //Begin the current search
1094 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1096 NSDictionary *searchDict;
1098 if (suppressSearchRequests) return;
1099 AILog(@"Starting a search for %@",activeSearchString);
1101 //Once all searches have exited, we can start a new one
1102 if (clearCurrentResults) {
1104 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1105 [self stopSearching];
1107 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1108 [resultsLock unlock];
1110 //Stop any existing searches
1111 [self stopSearching];
1115 indexingUpdatesReceivedWhileSearching = 0;
1116 searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1117 [NSNumber numberWithInteger:activeSearchID], @"ID",
1118 [NSNumber numberWithInteger:searchMode], @"Mode",
1119 activeSearchString, @"String",
1120 [plugin logContentIndex], @"SearchIndex",
1122 [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1124 //Update the table periodically while the logs load.
1125 [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1126 refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1128 selector:@selector(refreshResults)
1130 repeats:YES] retain];
1133 //Abort any active searches
1134 - (void)stopSearching
1136 [currentSearchLock lock];
1137 if (currentSearch) {
1138 SKSearchCancel(currentSearch);
1139 CFRelease(currentSearch); currentSearch = nil;
1141 [currentSearchLock unlock];
1143 [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
1145 //Increase the active search ID so any existing searches stop, and then
1146 //wait for any active searches to finish and release the lock
1150 //Set the active search mode (Does not invoke a search)
1151 - (void)setSearchMode:(LogSearchMode)inMode
1153 NSTextFieldCell *cell = [searchField_logs cell];
1155 searchMode = inMode;
1157 //Clear any filter from the table if it's the current mode, as well
1158 switch (searchMode) {
1159 case LOG_SEARCH_FROM:
1160 [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1164 [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1167 case LOG_SEARCH_DATE:
1168 [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1171 case LOG_SEARCH_CONTENT:
1172 [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1176 [self updateRankColumnVisibility];
1177 [self buildSearchMenu];
1180 - (void)updateRankColumnVisibility
1182 NSTableColumn *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1184 if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1185 //Add the resultsColumn and resize if it should be shown but is not at present
1186 if (!resultsColumn) {
1187 NSArray *tableColumns;
1189 //Set up the results column
1190 resultsColumn = [[[NSTableColumn alloc] initWithIdentifier:@"Rank"] autorelease];
1191 [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1192 [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1194 //Add it to the table
1195 [tableView_results addTableColumn:resultsColumn];
1197 //Make it half again as large as the desired width from the @"Rank" header title
1198 [resultsColumn sizeToFit];
1199 [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1201 tableColumns = [tableView_results tableColumns];
1202 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1203 NSTableColumn *nextDoorNeighbor;
1205 //Adjust the column to the results column's left so results is now visible
1206 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1207 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1211 //Remove the resultsColumn and resize if it should not be shown but is at present
1212 if (resultsColumn) {
1213 NSArray *tableColumns;
1215 tableColumns = [tableView_results tableColumns];
1216 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1217 NSTableColumn *nextDoorNeighbor;
1219 //Adjust the column to the results column's left to take up the space again
1220 tableColumns = [tableView_results tableColumns];
1221 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1222 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1226 [tableView_results removeTableColumn:resultsColumn];
1231 //Set the active search string (Does not invoke a search)
1232 - (void)setSearchString:(NSString *)inString
1234 if (![[searchField_logs stringValue] isEqualToString:inString]) {
1235 [searchField_logs setStringValue:(inString ? inString : @"")];
1238 //Use autorelease so activeSearchString can be passed back to here
1239 if (activeSearchString != inString) {
1240 [activeSearchString release];
1241 activeSearchString = [inString retain];
1244 [self updateRankColumnVisibility];
1247 //Build the search mode menu
1248 - (void)buildSearchMenu
1250 NSMenu *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1251 [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1252 [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1253 [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1254 [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1256 [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1259 - (void)_willOpenForContact
1261 isOpeningForContact = YES;
1264 - (void)_didOpenForContact
1266 isOpeningForContact = NO;
1270 * @brief Focus the log viewer on a particular contact
1272 * If the contact is within a metacontact, the metacontact will be focused.
1274 - (void)filterForContact:(AIListContact *)inContact
1276 AIListContact *parentContact = [inContact parentContact];
1278 if (!isOpeningForContact) {
1279 /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1280 * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1281 * with regard to inContact since the log viewer opened.
1283 * If we're opening initially, the list is guaranteed fresh.
1285 [self rebuildContactsList];
1288 //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1289 if (searchMode == LOG_SEARCH_TO) {
1290 [self setSearchMode:LOG_SEARCH_CONTENT];
1292 //Update our search string to ensure we're configured for content searching
1293 [self setSearchString:activeSearchString];
1296 //Changing the selection will start a new search
1297 [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1298 NSUInteger selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1299 if (selectedRow != NSNotFound) {
1300 [outlineView_contacts scrollRowToVisible:selectedRow];
1304 - (void)filterForChatName:(NSString *)chatName withAccount:(AIAccount *)account
1306 if (!isOpeningForContact) {
1308 [self rebuildContactsList];
1311 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[[NSString stringWithFormat:@"%@.%@",
1312 account.service.serviceID,
1313 account.UID.safeFilenameString]
1314 stringByAppendingPathComponent:chatName]];
1316 //Changing the selection will start a new search
1317 [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(logToGroup ?: (id)allContactsIdentifier)]];
1318 NSUInteger selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1319 if (selectedRow != NSNotFound) {
1320 [outlineView_contacts scrollRowToVisible:selectedRow];
1325 * @brief Returns a menu item for the search mode menu
1327 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1329 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
1330 action:@selector(selectSearchType:)
1332 [menuItem setTag:mode];
1333 [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1335 return [menuItem autorelease];
1338 #pragma mark Filtering search results
1340 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1342 BOOL matchesDateFilter;
1344 switch (filterDateType) {
1345 case AIDateTypeAfter:
1346 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1348 case AIDateTypeBefore:
1349 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1351 case AIDateTypeExactly:
1352 matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1355 matchesDateFilter = YES;
1359 return matchesDateFilter;
1363 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1365 CFURLRef url = SKDocumentCopyURL(inDocument);
1367 AILogWithSignature(@"Could not get url for %p", inDocument);
1371 NSString *logPath = [(NSURL *)url path];
1373 AILogWithSignature(@"Could not get path for %@", url);
1374 NSArray *pathComponents = [logPath pathComponents];
1378 return pathComponents;
1383 * @brief Should a search display a document with the given information?
1385 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1387 BOOL shouldDisplayDocument = YES;
1389 if ([contactIDsToFilter count]) {
1390 //Determine the path components if we weren't supplied them
1391 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1393 NSUInteger numPathComponents = [pathComponents count];
1395 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1396 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1398 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1400 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1403 if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1404 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1406 NSUInteger numPathComponents = [pathComponents count];
1407 NSString *toPath = [NSString stringWithFormat:@"%@/%@",
1408 [pathComponents objectAtIndex:numPathComponents-3],
1409 [pathComponents objectAtIndex:numPathComponents-2]];
1410 NSString *relativePath = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1413 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:relativePath];
1415 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1418 return shouldDisplayDocument;
1421 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1422 #pragma mark Threaded filter/search methods
1425 * @brief Perform a content search of the indexed logs
1427 * This uses the 10.4+ asynchronous search functions.
1428 * Google-like search syntax (phrase, prefix/suffix, boolean, etc. searching) is automatically supported.
1430 - (void)_logContentFilter:(NSString *)searchString searchID:(NSInteger)searchID onSearchIndex:(SKIndexRef)logSearchIndex
1432 CGFloat largestRankingValue = 0;
1433 SKSearchRef thisSearch;
1434 Boolean more = true;
1435 UInt32 totalCount = 0;
1437 [currentSearchLock lock];
1438 if (currentSearch) {
1439 SKSearchCancel(currentSearch);
1440 CFRelease(currentSearch); currentSearch = NULL;
1443 NSMutableString *wildcardedSearchString = [NSMutableString string];
1444 for (NSString *searchComponent in [searchString componentsSeparatedByString:@" "]) {
1445 if ([searchComponent rangeOfString:@"*"].location == NSNotFound) {
1446 //If the user specifies particular wildcard behavior, respect it
1447 [wildcardedSearchString appendFormat:@"*%@* ", searchComponent];
1449 [wildcardedSearchString appendFormat:@"%@ ", searchComponent];
1452 thisSearch = SKSearchCreate(logSearchIndex,
1453 (CFStringRef)wildcardedSearchString,
1454 kSKSearchOptionDefault);
1455 currentSearch = (thisSearch ? (SKSearchRef)CFRetain(thisSearch) : NULL);
1456 [currentSearchLock unlock];
1458 //Retrieve matches as long as more are pending
1459 while (more && currentSearch) {
1460 #define BATCH_NUMBER 100
1461 SKDocumentID foundDocIDs[BATCH_NUMBER];
1462 float foundScores[BATCH_NUMBER];
1463 SKDocumentRef foundDocRefs[BATCH_NUMBER];
1465 CFIndex foundCount = 0;
1468 more = SKSearchFindMatches (
1473 0.5, // maximum time before func returns, in seconds
1477 totalCount += foundCount;
1479 SKIndexCopyDocumentRefsForDocumentIDs (
1485 for (i = 0; ((i < foundCount) && (searchID == activeSearchID)) ; i++) {
1486 SKDocumentRef document = foundDocRefs[i];
1488 AILogWithSignature(@"SearchKit returned NULL document for ID %ld", (long)foundDocIDs[i]);
1492 CFURLRef url = SKDocumentCopyURL(document);
1494 AILogWithSignature(@"No URL for document %p", document);
1499 * Nasty implementation note: As of 10.4.7 and all previous versions, a path longer than 1024 bytes (PATH_MAX)
1500 * will cause CFURLCopyFileSystemPath() to crash [ultimately in CFGetAllocator()]. This is the case for all
1501 * Cocoa applications...
1503 NSString *logPath = [(NSURL *)url path];
1505 AILogWithSignature(@"Could not get path for %@. ", url);
1507 NSArray *pathComponents = [(NSString *)logPath pathComponents];
1509 /* Handle chatlogs-as-bundles, which have an xml file inside our target .chatlog path */
1510 if ([[[pathComponents lastObject] pathExtension] caseInsensitiveCompare:@"xml"] == NSOrderedSame)
1511 pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, [pathComponents count] - 1)];
1513 //Don't test for the date now; we'll test once we've found the AIChatLog if we make it that far
1514 if ([self searchShouldDisplayDocument:document pathComponents:pathComponents testDate:NO]) {
1515 NSUInteger numPathComponents = [pathComponents count];
1516 NSString *toPath = [NSString stringWithFormat:@"%@/%@",
1517 [pathComponents objectAtIndex:numPathComponents-3],
1518 [pathComponents objectAtIndex:numPathComponents-2]];
1519 NSString *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1522 /* Add the log - if our index is currently out of date (for example, a log was just deleted)
1523 * we may get a null log, so be careful.
1525 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1527 AILog(@"_logContentFilter: %x's key %@ yields %@; logAtPath:%@ gives %@",logToGroupDict,toPath,[logToGroupDict objectForKey:toPath],path,theLog);
1530 if ((theLog != nil) &&
1531 (![currentSearchResults containsObjectIdenticalTo:theLog]) &&
1532 [self chatLogMatchesDateFilter:theLog] &&
1533 (searchID == activeSearchID)) {
1534 [theLog setRankingValueOnArbitraryScale:foundScores[i]];
1536 //SearchKit does not normalize ranking scores, so we track the largest we've found and use it as 1.0
1537 if (foundScores[i] > largestRankingValue) largestRankingValue = foundScores[i];
1539 [currentSearchResults addObject:theLog];
1541 //Didn't get a valid log, so decrement our totalCount which is tracking how many logs we found
1544 [resultsLock unlock];
1547 //Didn't add this log, so decrement our totalCount which is tracking how many logs we found
1551 //if (logPath) CFRelease(logPath);
1552 if (url) CFRelease(url);
1553 if (document) CFRelease(document);
1556 //Scale all logs' ranking values to the largest ranking value we've seen thus far
1558 for (i = 0; ((i < totalCount) && (searchID == activeSearchID)); i++) {
1559 AIChatLog *theLog = [currentSearchResults objectAtIndex:i];
1560 [theLog setRankingPercentage:([theLog rankingValueOnArbitraryScale] / largestRankingValue)];
1562 [resultsLock unlock];
1564 [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1568 if (searchID != activeSearchID) {
1573 //Ensure current search isn't released in two places simultaneously
1574 [currentSearchLock lock];
1575 if (currentSearch) {
1576 CFRelease(currentSearch);
1577 currentSearch = NULL;
1579 [currentSearchLock unlock];
1581 if (thisSearch) CFRelease(thisSearch);
1584 //Search the logs, filtering out any matching logs into the currentSearchResults
1585 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1587 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1588 NSInteger mode = [[searchInfoDict objectForKey:@"Mode"] integerValue];
1589 NSInteger searchID = [[searchInfoDict objectForKey:@"ID"] integerValue];
1590 NSString *searchString = [searchInfoDict objectForKey:@"String"];
1592 if (searchID == activeSearchID) { //If we're still supposed to go
1594 AILog(@"filterLogsWithSearch (search ID %i): %@",searchID,searchInfoDict);
1596 [plugin pauseIndexing];
1597 if (searchString && [searchString length]) {
1599 case LOG_SEARCH_FROM:
1601 case LOG_SEARCH_DATE:
1602 [self _logFilter:searchString
1606 case LOG_SEARCH_CONTENT:
1607 [self _logContentFilter:searchString
1609 onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1613 [self _logFilter:nil
1620 [plugin resumeIndexing];
1621 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1622 AILog(@"filterLogsWithSearch (search ID %i): finished",searchID);
1629 //Perform a filter search based on source name, destination name, or date
1630 - (void)_logFilter:(NSString *)searchString searchID:(NSInteger)searchID mode:(LogSearchMode)mode
1632 UInt32 lastUpdate = TickCount();
1634 NSCalendarDate *searchStringDate = nil;
1636 if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1637 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString] dateWithCalendarFormat:nil timeZone:nil];
1640 //Walk through every 'from' group
1641 for (AILogFromGroup *fromGroup in [logFromGroupDict objectEnumerator]) {
1642 if (searchID != activeSearchID) break;
1644 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1645 if ((mode != LOG_SEARCH_FROM) ||
1647 ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1649 //Walk through every 'to' group
1650 for (AILogToGroup *toGroup in [fromGroup toGroupArray]) {
1651 if (searchID != activeSearchID) break;
1653 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1654 * For all other search modes, we always proceed here so long as either:
1655 * a) We are not filtering for specific contact names or
1656 * b) The contact name matches one of the names in contactIDsToFilter
1658 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1659 ((mode != LOG_SEARCH_TO) ||
1661 ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1663 //Walk through every log
1664 for (AIChatLog *theLog in [toGroup logEnumerator]) {
1665 if (searchID != activeSearchID) break;
1667 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1668 * For all other search modes, we always proceed here
1670 if ((mode != LOG_SEARCH_DATE) ||
1672 (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1674 if ([self chatLogMatchesDateFilter:theLog]) {
1677 [currentSearchResults addObject:theLog];
1678 [resultsLock unlock];
1681 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1682 [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1685 lastUpdate = TickCount();
1696 //Search results table view --------------------------------------------------------------------------------------------
1697 #pragma mark Search results table view
1698 //Since this table view's source data will be accessed from within other threads, we need to lock before
1699 //accessing it. We also must be very sure that an incorrect row request is handled silently, since this
1700 //can occur if the array size is changed during the reload.
1701 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
1706 count = [currentSearchResults count];
1707 [resultsLock unlock];
1713 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
1715 NSString *identifier = [tableColumn identifier];
1717 if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1718 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1720 [aCell setPercentage:[theLog rankingPercentage]];
1724 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
1726 NSString *identifier = [tableColumn identifier];
1730 if (row < 0 || row >= [currentSearchResults count]) {
1731 if ([identifier isEqualToString:@"Service"]) {
1738 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1740 if ([identifier isEqualToString:@"To"]) {
1741 // Get ListObject for to-UID
1742 AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:[theLog serviceClass]
1745 //Use the longDisplayName, following the user's contact list preferences as this is presumably how she wants to view contacts' names.
1746 if (![listObject.displayName isEqualToString:listObject.UID]) {
1747 value = [NSString stringWithFormat:@"%@ (%@)", listObject.displayName, listObject.UID];
1749 value = listObject.formattedUID;
1753 //No username available
1754 value = [theLog to];
1757 } else if ([identifier isEqualToString:@"From"]) {
1758 value = [theLog from];
1760 } else if ([identifier isEqualToString:@"Date"]) {
1761 value = [theLog date];
1763 } else if ([identifier isEqualToString:@"Service"]) {
1764 NSString *serviceClass;
1767 serviceClass = [theLog serviceClass];
1768 image = [AIServiceIcons serviceIconForService:[adium.accountController firstServiceWithServiceID:serviceClass]
1769 type:AIServiceIconSmall
1770 direction:AIIconNormal];
1771 value = (image ? image : blankImage);
1774 [resultsLock unlock];
1779 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1781 [NSObject cancelPreviousPerformRequestsWithTarget:self
1782 selector:@selector(tableViewSelectionDidChangeDelayed)
1785 [self performSelector:@selector(tableViewSelectionDidChangeDelayed)
1790 - (void)tableViewSelectionDidChangeDelayed
1792 if (!ignoreSelectionChange) {
1793 NSArray *selectedLogs;
1795 //Update the displayed log
1796 automaticSearch = NO;
1799 selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
1800 [resultsLock unlock];
1802 [self displayLogs:selectedLogs];
1806 //Sort the log array & reflect the new column
1807 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1809 [self sortCurrentSearchResultsForTableColumn:tableColumn
1810 direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1813 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1816 NSArray *selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
1817 [resultsLock unlock];
1819 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
1820 [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:) contextInfo:[selectedLogs retain]];
1823 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1825 NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1827 if (!aNotification ||
1828 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1829 NSDateFormatter *dateFormatter;
1830 NSCell *cell = [dateTableColumn dataCell];
1832 [cell setObjectValue:[NSDate date]];
1834 CGFloat width = [dateTableColumn width];
1836 #define NUMBER_TIME_STYLES 2
1837 #define NUMBER_DATE_STYLES 4
1838 NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1839 NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1840 CGFloat requiredWidth;
1842 dateFormatter = [cell formatter];
1843 if (!dateFormatter) {
1844 dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1845 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1846 [cell setFormatter:dateFormatter];
1849 requiredWidth = width + 1;
1850 for (NSInteger i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1851 [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1853 for (NSInteger j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1854 [dateFormatter setDateStyle:formatterStyles[j]];
1855 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1856 //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1863 - (IBAction)toggleEmoticonFiltering:(id)sender
1865 showEmoticons = !showEmoticons;
1866 [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1867 [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1869 [self displayLogs:displayedLogArray];
1872 - (IBAction)toggleTimestampFiltering:(id)sender
1874 showTimestamps = !showTimestamps;
1875 [sender setLabel:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)];
1876 [sender setImage:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]];
1878 [self displayLogs:displayedLogArray];
1881 #pragma mark Outline View Data source
1882 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1886 return allContactsIdentifier;
1889 return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1893 if ([item isKindOfClass:[AIMetaContact class]]) {
1894 return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1901 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1904 ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1905 [item isKindOfClass:[NSArray class]]);
1908 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1911 return [toArray count] + 1; //+1 for the All item
1913 } else if ([item isKindOfClass:[AIMetaContact class]]) {
1914 NSUInteger count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1925 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1927 Class itemClass = [item class];
1929 if (itemClass == [AIMetaContact class]) {
1930 return [(AIMetaContact *)item longDisplayName];
1932 } else if (itemClass == [AIListContact class]) {
1933 if ([(AIListContact *)item parentContact] != item) {
1934 //This contact is within a metacontact - always show its UID
1935 return [(AIListContact *)item formattedUID];
1937 return [(AIListContact *)item longDisplayName];
1940 } else if (itemClass == [AILogToGroup class]) {
1941 return [(AILogToGroup *)item to];
1943 } else if (itemClass == [allContactsIdentifier class]) {
1944 NSUInteger contactCount = [toArray count];
1945 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1946 ((contactCount == 1) ?
1947 AILocalizedString(@"1 Contact", nil) :
1948 [NSString stringWithFormat:AILocalizedString(@"%lu Contacts", nil), contactCount])];
1950 } else if (itemClass == [NSString class]) {
1954 NSLog(@"%@: no idea",item);
1959 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1961 if ([item isKindOfClass:[AIMetaContact class]] &&
1962 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1963 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1964 * If it has multiple contacts, use no icon unless a user icon is present.
1966 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1967 size:NSMakeSize(16,16)];
1968 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1970 [cell setImage:image];
1972 } else if ([item isKindOfClass:[AIListContact class]]) {
1973 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1974 size:NSMakeSize(16,16)];
1975 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1976 type:AIServiceIconSmall
1977 direction:AIIconFlipped];
1978 [cell setImage:image];
1980 } else if ([item isKindOfClass:[AILogToGroup class]]) {
1981 [cell setImage:[AIServiceIcons serviceIconForService:[adium.accountController firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]]
1982 type:AIServiceIconSmall
1983 direction:AIIconNormal]];
1985 } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1986 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1987 ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1988 if (!adiumIconHighlighted) {
1989 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
1990 forClass:[self class]] retain];
1993 [cell setImage:adiumIconHighlighted];
1997 adiumIcon = [[NSImage imageNamed:@"adium"
1998 forClass:[self class]] retain];
2001 [cell setImage:adiumIcon];
2004 } else if ([item isKindOfClass:[NSString class]]) {
2005 [cell setImage:nil];
2008 NSLog(@"%@: no idea",item);
2009 [cell setImage:nil];
2014 * @brief Is item supposed to have a divider below?
2017 - (AIDividerPosition)outlineView:(NSOutlineView*)outlineView dividerPositionForItem:(id)item
2019 if ([item isKindOfClass:[allContactsIdentifier class]]) {
2020 return AIDividerPositionBelow;
2022 return AIDividerPositionNone;
2026 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
2028 [self deleteSelection:nil];
2032 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
2034 [NSObject cancelPreviousPerformRequestsWithTarget:self
2035 selector:@selector(outlineViewSelectionDidChangeDelayed)
2038 [self performSelector:@selector(outlineViewSelectionDidChangeDelayed)
2043 - (void)outlineViewSelectionDidChangeDelayed
2045 NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
2047 [contactIDsToFilter removeAllObjects];
2049 if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
2052 for (item in selectedItems) {
2053 if ([item isKindOfClass:[AIMetaContact class]]) {
2054 for (AIListContact *contact in [(AIMetaContact *)item listContactsIncludingOfflineAccounts]) {
2055 [contactIDsToFilter addObject:
2056 [[[NSString stringWithFormat:@"%@.%@", contact.service.serviceID, contact.UID] compactedString] safeFilenameString]];
2059 } else if ([item isKindOfClass:[AIListContact class]]) {
2060 [contactIDsToFilter addObject:
2061 [[[NSString stringWithFormat:@"%@.%@",((AIListContact *)item).service.serviceID,((AIListContact *)item).UID] compactedString] safeFilenameString]];
2063 } else if ([item isKindOfClass:[AILogToGroup class]]) {
2064 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]];
2069 [self startSearchingClearingCurrentResults:YES];
2072 - (NSMenu *)outlineView:(NSOutlineView *)outlineView menuForEvent:(NSEvent *)theEvent;
2074 if (outlineView == outlineView_contacts) {
2075 NSInteger clickedRow = [outlineView_contacts rowAtPoint:[outlineView_contacts convertPoint:[theEvent locationInWindow]
2077 id item = [outlineView_contacts itemAtRow:clickedRow];
2079 //If we have a To group, see if we can make a contact out of it
2080 if ([item isKindOfClass:[AILogToGroup class]]) {
2081 if ([(AILogToGroup *)item to] && [(AILogToGroup *)item serviceClass]) {
2082 //We need a service with ther right service ID
2083 AIService *service = [adium.accountController firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]];
2085 //Next, we want an online account
2086 AIAccount *account = nil;
2087 for (account in [adium.accountController accountsCompatibleWithService:service]) {
2088 if (account.online) break;
2092 //Finally, make a contact
2093 item = [adium.contactController contactWithService:service
2095 UID:[(AILogToGroup *)item to]];
2102 if ([item isKindOfClass:[AIListContact class]]) {
2103 NSArray *locationsArray = [NSArray arrayWithObjects:
2104 [NSNumber numberWithInteger:Context_Contact_Message],
2105 [NSNumber numberWithInteger:Context_Contact_Manage],
2106 [NSNumber numberWithInteger:Context_Contact_Action],
2107 [NSNumber numberWithInteger:Context_Contact_ListAction],
2108 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
2109 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
2111 return [adium.menuController contextualMenuWithLocations:locationsArray
2112 forListObject:(AIListContact *)item];
2119 static NSInteger toArraySort(id itemA, id itemB, void *context)
2121 NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
2122 NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
2123 NSComparisonResult result = [nameA caseInsensitiveCompare:nameB];
2124 if (result == NSOrderedSame) result = [nameA compare:nameB];
2129 - (void)draggedDividerRightBy:(CGFloat)deltaX
2131 desiredContactsSourceListDeltaX = deltaX;
2132 [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
2133 desiredContactsSourceListDeltaX = 0;
2137 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
2139 if ((sender == splitView_contacts_results) &&
2140 desiredContactsSourceListDeltaX != 0) {
2141 float dividerThickness = [sender dividerThickness];
2143 NSRect newFrame = [sender frame];
2144 NSRect leftFrame = [containingView_contactsSourceList frame];
2145 NSRect rightFrame = [containingView_results frame];
2147 leftFrame.size.width += desiredContactsSourceListDeltaX;
2148 leftFrame.size.height = newFrame.size.height;
2149 leftFrame.origin = NSMakePoint(0,0);
2151 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
2152 rightFrame.size.height = newFrame.size.height;
2153 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
2155 [containingView_contactsSourceList setFrame:leftFrame];
2156 [containingView_contactsSourceList setNeedsDisplay:YES];
2157 [containingView_results setFrame:rightFrame];
2158 [containingView_results setNeedsDisplay:YES];
2161 //Perform the default implementation
2162 [sender adjustSubviews];
2167 //Window Toolbar -------------------------------------------------------------------------------------------------------
2168 #pragma mark Window Toolbar
2170 - (void)installToolbar
2172 [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
2174 NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
2175 NSToolbarItem *toolbarItem;
2177 [toolbar setDelegate:self];
2178 [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
2179 [toolbar setSizeMode:NSToolbarSizeModeRegular];
2180 [toolbar setVisible:YES];
2181 [toolbar setAllowsUserCustomization:YES];
2182 [toolbar setAutosavesConfiguration:YES];
2183 toolbarItems = [[NSMutableDictionary alloc] init];
2186 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2187 withIdentifier:@"delete"
2190 toolTip:AILocalizedString(@"Delete the selection",nil)
2192 settingSelector:@selector(setImage:)
2193 itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
2194 action:@selector(deleteSelection:)
2198 [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
2199 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
2202 toolTip:AILocalizedString(@"Search or filter logs",nil)
2204 settingSelector:@selector(setView:)
2205 itemContent:view_SearchField
2206 action:@selector(updateSearch:)
2208 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
2209 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
2211 [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
2212 [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
2213 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
2215 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
2216 label:AILocalizedString(@"Date", nil)
2217 paletteLabel:AILocalizedString(@"Date", nil)
2218 toolTip:AILocalizedString(@"Filter logs by date",nil)
2220 settingSelector:@selector(setView:)
2221 itemContent:view_DatePicker
2224 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
2225 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
2227 [toolbarItem setMinSize:[view_DatePicker frame].size];
2228 [toolbarItem setMaxSize:[view_DatePicker frame].size];
2229 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
2232 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2233 withIdentifier:@"toggleemoticons"
2234 label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
2235 paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
2236 toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
2238 settingSelector:@selector(setImage:)
2239 itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
2240 action:@selector(toggleEmoticonFiltering:)
2242 // Toggle Timestamps
2243 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2244 withIdentifier:@"toggletimestamps"
2245 label:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)
2246 paletteLabel:AILocalizedString(@"Show/Hide Timestamps", nil)
2247 toolTip:AILocalizedString(@"Show or hide timestamps in logs", nil)
2249 settingSelector:@selector(setImage:)
2250 itemContent:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]
2251 action:@selector(toggleTimestampFiltering:)
2254 [[self window] setToolbar:toolbar];
2256 [self configureDateFilter];
2259 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
2261 return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
2264 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
2266 return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
2267 @"delete", @"toggleemoticons", @"toggletimestamps", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
2271 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
2273 return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
2274 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
2275 NSToolbarSpaceItemIdentifier,
2276 NSToolbarFlexibleSpaceItemIdentifier,
2277 NSToolbarCustomizeToolbarItemIdentifier,
2278 NSToolbarPrintItemIdentifier, nil]];
2281 - (void)toolbarWillAddItem:(NSNotification *)notification
2283 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
2284 if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2285 [item setTarget:self];
2286 [item setAction:@selector(adiumPrint:)];
2290 #pragma mark Date filter
2293 * @brief Returns a menu item for the date type filter menu
2295 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
2297 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInteger:dateType]]
2298 action:@selector(selectDateType:)
2300 [menuItem setTag:dateType];
2302 return [menuItem autorelease];
2305 - (NSInteger)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
2307 NSInteger todayDayOfWeek = [today dayOfWeek];
2309 //Try to look at the iCal preferences if possible
2310 if (!iCalFirstDayOfWeekDetermined) {
2311 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
2312 if (iCalFirstDayOfWeek) {
2313 //This should return a CFNumberRef... we're using another app's prefs, so make sure.
2314 if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
2315 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek integerValue];
2318 CFRelease(iCalFirstDayOfWeek);
2322 iCalFirstDayOfWeekDetermined = YES;
2325 return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2329 * @brief Select the date type
2331 - (void)selectDateType:(id)sender
2333 [self selectedDateType:[sender tag]];
2334 [self startSearchingClearingCurrentResults:YES];
2337 #pragma mark Open Log
2339 - (void)openLogAtPath:(NSString *)inPath
2341 AIChatLog *chatLog = nil;
2342 NSString *basePath = [AILoggerPlugin logBasePath];
2344 //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2345 NSArray *pathComponents = [inPath pathComponents];
2346 NSInteger lastIndex = [pathComponents count];
2347 NSString *logName = [pathComponents objectAtIndex:--lastIndex];
2348 NSString *contactName = [pathComponents objectAtIndex:--lastIndex];
2349 NSString *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];
2350 NSString *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2352 NSString *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2353 //Filter for logs from the contact associated with the log we're loading
2354 [self filterForContact:[adium.contactController contactWithService:[adium.accountController firstServiceWithServiceID:serviceID]
2358 NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2359 NSString *canonicalInPath = [inPath stringByStandardizingPath];
2361 if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2362 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2364 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2367 /* Different Adium user... this sucks. We're given a path like this:
2368 * /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2369 * and we want to make it relative to our current user's logs folder, which might be
2370 * /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2372 * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2374 NSString *fakeRelativePath = @"";
2376 //Use .. to get back to the root from the base path
2377 NSInteger componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2378 for (NSInteger i = 0; i < componentsOfBasePath; i++) {
2379 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2382 //Now add the path from the root to the actual log
2383 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2384 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2385 from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2387 serviceClass:serviceID] autorelease];
2390 //Now display the requested log
2392 [self displayLog:chatLog];
2396 #pragma mark Printing
2398 - (void)adiumPrint:(id)sender
2400 NSTextView *printView;
2401 NSPrintOperation *printOperation;
2402 NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];
2404 [printInfo setHorizontalPagination:NSFitPagination];
2405 [printInfo setHorizontallyCentered:NO];
2406 [printInfo setVerticallyCentered:NO];
2408 printView = [[NSTextView alloc] initWithFrame:[[NSPrintInfo sharedPrintInfo] imageablePageBounds]];
2409 [printView setVerticallyResizable:YES];
2410 [printView setHorizontallyResizable:NO];
2412 [[printView textStorage] setAttributedString:[textView_content textStorage]];
2414 printOperation = [NSPrintOperation printOperationWithView:printView printInfo:printInfo];
2415 [printOperation runOperationModalForWindow:[self window] delegate:nil
2416 didRunSelector:NULL contextInfo:NULL];
2417 [printView release];
2420 - (BOOL)validatePrintMenuItem:(NSMenuItem *)menuItem
2422 return ([displayedLogArray count] > 0);
2425 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2427 if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2428 return [self validatePrintMenuItem:nil];
2435 - (void)selectCachedIndex
2437 NSInteger numberOfRows = [tableView_results numberOfRows];
2439 if (cachedSelectionIndex < numberOfRows) {
2440 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2441 byExtendingSelection:NO];
2444 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2445 byExtendingSelection:NO];
2449 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2452 deleteOccurred = NO;
2455 #pragma mark Deletion
2458 * @brief Get an NSAlert to request deletion of multiple logs
2460 - (NSAlert *)alertForDeletionOfLogCount:(NSUInteger)logCount
2462 NSAlert *alert = [[NSAlert alloc] init];
2463 [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2464 [alert setInformativeText:[NSString stringWithFormat:
2465 AILocalizedString(@"Are you sure you want to send %lu logs to the Trash?",nil), logCount]];
2466 [alert addButtonWithTitle:DELETE];
2467 [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2469 return [alert autorelease];
2473 * @brief Undo the deletion of one or more AIChatLogs
2475 * The logs will be marked for readdition to the index
2477 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2480 NSFileManager *fileManager = [NSFileManager defaultManager];
2481 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2483 for (aLog in deletedLogs) {
2484 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
2486 [fileManager createDirectoryAtPath:[logPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL];
2488 [fileManager moveItemAtPath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2492 [plugin markLogDirtyAtPath:logPath];
2495 [self rebuildIndices];
2498 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
2500 NSArray *selectedLogs = (NSArray *)contextInfo;
2501 if (returnCode == NSAlertFirstButtonReturn) {
2505 NSMutableSet *logPaths = [NSMutableSet set];
2507 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2509 for (aLog in selectedLogs) {
2510 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
2512 [[NSNotificationCenter defaultCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2513 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[[aLog relativePath] stringByDeletingLastPathComponent]];
2515 // Success will be unused in deployment builds as AILog turns to nothing
2517 BOOL success = [logToGroup trashLog:aLog];
2518 AILog(@"Trashing %@: %i",[aLog relativePath], success);
2520 [logToGroup trashLog:aLog];
2522 //Clear the to group out if it no longer has anything of interest
2523 if ([logToGroup logCount] == 0) {
2524 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[[[aLog relativePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent]];
2525 [logFromGroup removeToGroup:logToGroup];
2528 [logPaths addObject:logPath];
2529 [currentSearchResults removeObjectIdenticalTo:aLog];
2532 [plugin removePathsFromIndex:logPaths];
2534 [undoManager registerUndoWithTarget:self
2535 selector:@selector(restoreDeletedLogs:)
2536 object:selectedLogs];
2537 [undoManager setActionName:DELETE];
2539 [resultsLock unlock];
2540 [tableView_results reloadData];
2542 deleteOccurred = YES;
2544 [self rebuildContactsList];
2545 [self updateProgressDisplay];
2547 [selectedLogs release];
2551 * @brief Delete logs
2553 * If two or more logs are passed, confirmation will be requested.
2554 * This operation registers with the window controller's undo manager.
2556 * @param selectedLogs An NSArray of logs to delete
2558 - (void)deleteLogs:(NSArray *)selectedLogs
2560 if ([selectedLogs count] > 1) {
2561 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2562 [alert beginSheetModalForWindow:[self window]
2564 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2565 contextInfo:[selectedLogs retain]];
2567 [self deleteLogsAlertDidEnd:nil
2568 returnCode:NSAlertFirstButtonReturn
2569 contextInfo:[selectedLogs retain]];
2574 * @brief Returns a set of all selected to groups on all accounts
2576 * @param totalLogCount If non-NULL, will be set to the total number of logs on return
2578 - (NSArray *)allSelectedToGroups:(NSInteger *)totalLogCount
2580 NSEnumerator *fromEnumerator;
2581 AILogFromGroup *fromGroup;
2582 NSMutableArray *allToGroups = [NSMutableArray array];
2584 if (totalLogCount) *totalLogCount = 0;
2586 //Walk through every 'from' group
2587 fromEnumerator = [logFromGroupDict objectEnumerator];
2588 while ((fromGroup = [fromEnumerator nextObject])) {
2589 NSEnumerator *toEnumerator;
2590 AILogToGroup *toGroup;
2592 //Walk through every 'to' group
2593 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2594 while ((toGroup = [toEnumerator nextObject])) {
2595 if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2596 if (totalLogCount) {
2597 *totalLogCount += [toGroup logCount];
2600 [allToGroups addObject:toGroup];
2609 * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2611 * The logs will be marked for readdition to the index
2613 - (void)restoreDeletedToGroups:(NSArray *)toGroups
2615 AILogToGroup *toGroup;
2616 NSFileManager *fileManager = [NSFileManager defaultManager];
2617 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2618 NSString *logBasePath = [AILoggerPlugin logBasePath];
2620 for (toGroup in toGroups) {
2621 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup relativePath]];
2623 [fileManager createDirectoryAtPath:[toGroupPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL];
2624 if ([fileManager fileExistsAtPath:toGroupPath]) {
2625 AILog(@"Removing path %@ to make way for %@",
2626 toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2627 [fileManager removeItemAtPath:toGroupPath
2630 [fileManager moveItemAtPath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2634 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2637 while ((aLog = [logEnumerator nextObject])) {
2638 [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog relativePath]]];
2642 [self rebuildIndices];
2645 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
2647 NSArray *allSelectedToGroups = (NSArray *)contextInfo;
2648 if (returnCode == NSAlertFirstButtonReturn) {
2649 AILogToGroup *logToGroup;
2650 NSMutableSet *logPaths = [NSMutableSet set];
2652 for (logToGroup in allSelectedToGroups) {
2653 NSEnumerator *logEnumerator;
2656 logEnumerator = [logToGroup logEnumerator];
2657 while ((aLog = [logEnumerator nextObject])) {
2658 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
2659 [logPaths addObject:logPath];
2662 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2663 [logFromGroup removeToGroup:logToGroup];
2666 [plugin removePathsFromIndex:logPaths];
2668 [undoManager registerUndoWithTarget:self
2669 selector:@selector(restoreDeletedToGroups:)
2670 object:allSelectedToGroups];
2671 [undoManager setActionName:DELETE];
2673 [self rebuildIndices];
2674 [self updateProgressDisplay];
2677 [allSelectedToGroups release];
2681 * @brief Delete entirely the logs of all contacts selected in the source list
2683 * Confirmation by the user will be required.
2685 * Note: A single item in the source list may have multiple associated AILogToGroups.
2687 - (void)deleteSelectedContactsFromSourceList
2689 NSInteger totalLogCount;
2690 NSArray *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2692 if (totalLogCount > 1) {
2693 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2694 [alert beginSheetModalForWindow:[self window]
2696 didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2697 contextInfo:[allSelectedToGroups retain]];
2699 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2700 returnCode:NSAlertFirstButtonReturn
2701 contextInfo:[allSelectedToGroups retain]];
2706 * @brief Delete the current selection
2708 * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2709 * If anything else is selected, the currently selected search result logs will be trashed.
2711 - (void)deleteSelection:(id)sender
2713 if ([[self window] firstResponder] == outlineView_contacts) {
2714 [self deleteSelectedContactsFromSourceList];
2718 NSArray *selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
2719 [resultsLock unlock];
2721 [self deleteLogs:selectedLogs];
2727 * @brief Supply our undo manager when we are within the responder chain
2729 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
2734 #pragma mark Gestures
2736 * @brief Responds to a swipe gesture
2738 * This is a private method added in AppKit 949.18.0.
2740 - (void)swipeWithEvent:(NSEvent *)inEvent
2742 NSTableView *targetTableView;
2743 NSInteger changeValue, nextSelected;
2745 if ([inEvent deltaY] == 0) {
2746 // For horizontal swipes, switch between individual logs.
2747 targetTableView = tableView_results;
2748 changeValue = [inEvent deltaX];
2749 // Lock the results when we're dealing with the logs tableView
2752 // For vertical swipes, switch between contacts.
2753 targetTableView = outlineView_contacts;
2754 changeValue = [inEvent deltaY];
2757 // Swipe; +1f is left/up, -1f is right/down
2759 // Find the index of the next row to select.
2760 if (changeValue == -1) {
2761 // Going to the right.
2762 nextSelected = [[targetTableView selectedRowIndexes] lastIndex] + 1;
2764 // Going to the left.
2765 nextSelected = [[targetTableView selectedRowIndexes] firstIndex] - 1;
2768 // Loop around in circles.
2769 if (nextSelected >= [targetTableView numberOfRows]) {
2771 } else if (nextSelected < 0) {
2772 nextSelected = [targetTableView numberOfRows]-1;
2775 // Select either the next row or the previous row.
2776 [targetTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:nextSelected]
2777 byExtendingSelection:NO];
2779 [targetTableView scrollRowToVisible:nextSelected];
2781 if ([inEvent deltaY] == 0)
2782 [resultsLock unlock];
2785 #pragma mark Transcript services special-casing
2786 NSString *handleSpecialCasesForUIDAndServiceClass(NSString *contactUID, NSString *serviceClass)
2788 /* Jabber and its specified derivative services need special handling;
2789 * this is cross-contamination from ESPurpleJabberAccount.
2791 if ([serviceClass isEqualToString:@"Jabber"] ||
2792 [serviceClass isEqualToString:@"GTalk"] ||
2793 [serviceClass isEqualToString:@"LiveJournal"]) {
2795 if ([contactUID hasSuffix:@"@gmail.com"] ||
2796 [contactUID hasSuffix:@"@googlemail.com"]) {
2797 serviceClass = @"GTalk";
2799 } else if ([contactUID hasSuffix:@"@livejournal.com"]){
2800 serviceClass = @"LiveJournal";
2803 serviceClass = @"Jabber";
2806 /* OSCAR and its specified derivative services need special handling;
2807 * this is cross-contamination from CBPurpleOscarAccount.
2809 } else if ([serviceClass isEqualToString:@"AIM"] ||
2810 [serviceClass isEqualToString:@"ICQ"] ||
2811 [serviceClass isEqualToString:@"Mac"] ||
2812 [serviceClass isEqualToString:@"MobileMe"]) {
2813 const char firstCharacter = ([contactUID length] ? [contactUID characterAtIndex:0] : '\0');
2815 //Determine service based on UID
2816 if ([contactUID hasSuffix:@"@mac.com"]) {
2817 serviceClass = @"Mac";
2818 } else if ([contactUID hasSuffix:@"@me.com"]) {
2819 serviceClass = @"MobileMe";
2820 } else if (firstCharacter && (firstCharacter >= '0' && firstCharacter <= '9')) {
2821 serviceClass = @"ICQ";
2823 serviceClass = @"AIM";
2827 return serviceClass;
2830 #pragma mark Date type menu
2832 - (void)configureDateFilter
2834 firstDayOfWeek = 0; /* Sunday */
2835 iCalFirstDayOfWeekDetermined = NO;
2837 [popUp_dateFilter setMenu:[self dateTypeMenu]];
2838 NSInteger index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2839 if(index != NSNotFound)
2840 [popUp_dateFilter selectItemAtIndex:index];
2841 [self selectedDateType:AIDateTypeAnyDate];
2843 [datePicker setDateValue:[NSDate date]];
2846 - (IBAction)selectDate:(id)sender
2848 [filterDate release];
2849 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2851 [self startSearchingClearingCurrentResults:YES];
2854 - (NSMenu *)dateTypeMenu
2856 NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2857 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInteger:AIDateTypeAnyDate],
2858 AILocalizedString(@"Today", nil), [NSNumber numberWithInteger:AIDateTypeToday],
2859 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInteger:AIDateTypeSinceYesterday],
2860 AILocalizedString(@"This Week", nil), [NSNumber numberWithInteger:AIDateTypeThisWeek],
2861 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInteger:AIDateTypeWithinLastTwoWeeks],
2862 AILocalizedString(@"This Month", nil), [NSNumber numberWithInteger:AIDateTypeThisMonth],
2863 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInteger:AIDateTypeWithinLastTwoMonths],
2865 NSMenu *dateTypeMenu = [[NSMenu alloc] init];
2866 AIDateType dateType;
2868 [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
2869 [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2871 for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
2872 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2875 dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2876 AILocalizedString(@"Exactly", nil), [NSNumber numberWithInteger:AIDateTypeExactly],
2877 AILocalizedString(@"Before", nil), [NSNumber numberWithInteger:AIDateTypeBefore],
2878 AILocalizedString(@"After", nil), [NSNumber numberWithInteger:AIDateTypeAfter],
2881 [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2883 for (dateType = AIDateTypeExactly; dateType <= AIDateTypeAfter; dateType++) {
2884 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2887 return [dateTypeMenu autorelease];
2891 * @brief A new date type was selected
2893 * The date picker will be hidden/revealed as appropriate.
2894 * This does not start a search
2896 - (void)selectedDateType:(AIDateType)dateType
2898 BOOL showDatePicker = NO;
2900 NSCalendarDate *today = [NSCalendarDate date];
2902 [filterDate release]; filterDate = nil;
2905 case AIDateTypeAnyDate:
2906 filterDateType = AIDateTypeAnyDate;
2909 case AIDateTypeToday:
2910 filterDateType = AIDateTypeExactly;
2911 filterDate = [today retain];
2914 case AIDateTypeSinceYesterday:
2915 filterDateType = AIDateTypeAfter;
2916 filterDate = [[today dateByAddingYears:0
2919 hours:-[today hourOfDay]
2920 minutes:-[today minuteOfHour]
2921 seconds:-([today secondOfMinute] + 1)] retain];
2924 case AIDateTypeThisWeek:
2925 filterDateType = AIDateTypeAfter;
2926 filterDate = [[today dateByAddingYears:0
2928 days:-[self daysSinceStartOfWeekGivenToday:today]
2929 hours:-[today hourOfDay]
2930 minutes:-[today minuteOfHour]
2931 seconds:-([today secondOfMinute] + 1)] retain];
2934 case AIDateTypeWithinLastTwoWeeks:
2935 filterDateType = AIDateTypeAfter;
2936 filterDate = [[today dateByAddingYears:0
2939 hours:-[today hourOfDay]
2940 minutes:-[today minuteOfHour]
2941 seconds:-([today secondOfMinute] + 1)] retain];
2944 case AIDateTypeThisMonth:
2945 filterDateType = AIDateTypeAfter;
2946 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2948 days:-[today dayOfMonth]
2951 seconds:-1] retain];
2954 case AIDateTypeWithinLastTwoMonths:
2955 filterDateType = AIDateTypeAfter;
2956 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2958 days:-[today dayOfMonth]
2961 seconds:-1] retain];
2969 case AIDateTypeExactly:
2970 filterDateType = AIDateTypeExactly;
2971 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2972 showDatePicker = YES;
2975 case AIDateTypeBefore:
2976 filterDateType = AIDateTypeBefore;
2977 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2978 showDatePicker = YES;
2981 case AIDateTypeAfter:
2982 filterDateType = AIDateTypeAfter;
2983 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2984 showDatePicker = YES;
2988 showDatePicker = NO;
2992 BOOL updateSize = NO;
2993 if (showDatePicker && [datePicker isHidden]) {
2994 [datePicker setHidden:NO];
2997 } else if (!showDatePicker && ![datePicker isHidden]) {
2998 [datePicker setHidden:YES];
3003 NSEnumerator *enumerator = [[[[self window] toolbar] items] objectEnumerator];
3004 NSToolbarItem *toolbarItem;
3005 while ((toolbarItem = [enumerator nextObject])) {
3006 if ([[toolbarItem itemIdentifier] isEqualToString:DATE_ITEM_IDENTIFIER]) {
3007 NSSize newSize = NSMakeSize(([datePicker isHidden] ? 180 : 290), NSHeight([view_DatePicker frame]));
3008 [toolbarItem setMinSize:newSize];
3009 [toolbarItem setMaxSize:newSize];
3016 - (NSString *)dateItemNibName
3018 return @"LogViewerDateFilter";