When opening for a contact, don't silently switch the search type to "to". Fixes #12001.
This also moves the automatic selection below the "we're now updating for search" line, so that highlighting is applied to the log (since when it's asked to display, there's no search term for it).
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;
91 - (void)tableViewSelectionDidChangeDelayed;
93 - (NSAlert *)alertForDeletionOfLogCount:(NSUInteger)logCount;
95 - (void)_willOpenForContact;
96 - (void)_didOpenForContact;
98 - (void)deleteSelection:(id)sender;
101 @implementation AILogViewerWindowController
103 static AILogViewerWindowController *sharedLogViewerInstance = nil;
104 static NSInteger toArraySort(id itemA, id itemB, void *context);
106 + (NSString *)nibName
111 + (id)openForPlugin:(id)inPlugin
113 if (!sharedLogViewerInstance) {
114 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
117 [sharedLogViewerInstance showWindow:nil];
119 return sharedLogViewerInstance;
122 + (id)openLogAtPath:(NSString *)inPath plugin:(id)inPlugin
124 [self openForPlugin:inPlugin];
126 [sharedLogViewerInstance openLogAtPath:inPath];
128 return sharedLogViewerInstance;
131 //Open the log viewer window to a specific contact's logs
132 + (id)openForContact:(AIListContact *)inContact plugin:(id)inPlugin
134 if (!sharedLogViewerInstance) {
135 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
138 [sharedLogViewerInstance _willOpenForContact];
139 [sharedLogViewerInstance showWindow:nil];
140 [sharedLogViewerInstance filterForContact:inContact];
141 [sharedLogViewerInstance _didOpenForContact];
143 return sharedLogViewerInstance;
146 + (id)openForChatName:(NSString *)inChatName withAccount:(AIAccount *)inAccount plugin:(id)inPlugin
148 if (!sharedLogViewerInstance) {
149 sharedLogViewerInstance = [[self alloc] initWithWindowNibName:[self nibName] plugin:inPlugin];
152 [sharedLogViewerInstance _willOpenForContact];
153 [sharedLogViewerInstance showWindow:nil];
154 [sharedLogViewerInstance filterForChatName:inChatName withAccount:inAccount];
155 [sharedLogViewerInstance _didOpenForContact];
157 return sharedLogViewerInstance;
160 //Returns the window controller if one exists
161 + (id)existingWindowController
163 return sharedLogViewerInstance;
166 //Close the log viewer window
167 + (void)closeSharedInstance
169 if (sharedLogViewerInstance) {
170 [sharedLogViewerInstance closeWindow:nil];
175 - (id)initWithWindowNibName:(NSString *)windowNibName plugin:(id)inPlugin
177 if((self = [super initWithWindowNibName:windowNibName])) {
179 selectedColumn = nil;
182 automaticSearch = YES;
184 showTimestamps = YES;
185 activeSearchString = nil;
186 displayedLogArray = nil;
187 windowIsClosing = NO;
188 desiredContactsSourceListDeltaX = 0;
190 blankImage = [[NSImage alloc] initWithSize:NSMakeSize(16,16)];
193 searchMode = LOG_SEARCH_CONTENT;
195 headerDateFormatter = [[NSDateFormatter localizedDateFormatter] retain];
197 currentSearchResults = [[NSMutableArray alloc] init];
198 fromArray = [[NSMutableArray alloc] init];
199 fromServiceArray = [[NSMutableArray alloc] init];
200 logFromGroupDict = [[NSMutableDictionary alloc] init];
201 toArray = [[NSMutableArray alloc] init];
202 toServiceArray = [[NSMutableArray alloc] init];
203 logToGroupDict = [[NSMutableDictionary alloc] init];
204 resultsLock = [[NSRecursiveLock alloc] init];
205 searchingLock = [[NSLock alloc] init];
206 [searchingLock setName:@"LogSearchingLock"];
207 contactIDsToFilter = [[NSMutableSet alloc] initWithCapacity:1];
209 allContactsIdentifier = [[NSNumber numberWithInteger:-1] retain];
211 undoManager = [[NSUndoManager alloc] init];
212 currentSearchLock = [[NSLock alloc] init];
213 [currentSearchLock setName:@"CurrentLogSearchLock"];
222 [filterDate release]; filterDate = nil;
223 [currentSearchLock release]; currentSearchLock = nil;
224 [resultsLock release];
225 [searchingLock release];
227 [fromServiceArray release];
229 [toServiceArray release];
230 [currentSearchResults release];
231 [selectedColumn release];
232 [headerDateFormatter release];
233 [displayedLogArray release];
234 [blankImage release];
235 [activeSearchString release];
236 [contactIDsToFilter release];
238 [logFromGroupDict release]; logFromGroupDict = nil;
239 [logToGroupDict release]; logToGroupDict = nil;
241 [filterForAccountName release]; filterForAccountName = nil;
243 [horizontalRule release]; horizontalRule = nil;
245 [adiumIcon release]; adiumIcon = nil;
246 [adiumIconHighlighted release]; adiumIconHighlighted = nil;
248 //We loaded view_DatePicker from a nib manually, so we must release it
249 [view_DatePicker release]; view_DatePicker = nil;
251 [allContactsIdentifier release];
252 [undoManager release]; undoManager = nil;
257 //Init our log filtering tree
258 - (void)initLogFiltering
260 NSMutableDictionary *toDict = [NSMutableDictionary dictionary];
261 NSString *basePath = [AILoggerPlugin logBasePath];
262 NSString *fromUID, *serviceClass;
264 //Process each account folder (/Logs/SERVICE.ACCOUNT_NAME/) - sorting by compare: will result in an ordered list
265 //first by service, then by account name.
266 for (NSString *folderName in [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:basePath error:NULL] sortedArrayUsingSelector:@selector(compare:)]) {
267 if (![folderName isEqualToString:@".DS_Store"]) { // avoid the directory info
268 AILogFromGroup *logFromGroup;
269 NSMutableSet *toSetForThisService;
270 NSArray *serviceAndFromUIDArray;
272 /* Determine the service and fromUID - should be SERVICE.ACCOUNT_NAME
273 * Check against count to guard in case of old, malformed or otherwise odd folders & whatnot sitting in log base
275 serviceAndFromUIDArray = [folderName componentsSeparatedByString:@"."];
277 if ([serviceAndFromUIDArray count] >= 2) {
278 serviceClass = [serviceAndFromUIDArray objectAtIndex:0];
280 //Use substringFromIndex so we include the rest of the string in the case of a UID with a . in it
281 fromUID = [folderName substringFromIndex:([serviceClass length] + 1)]; //One off for the '.'
283 //Fallback: blank non-nil serviceClass; folderName as the fromUID
285 fromUID = folderName;
288 logFromGroup = [[AILogFromGroup alloc] initWithPath:folderName fromUID:fromUID serviceClass:serviceClass];
290 //Store logFromGroup on a key in the form "SERVICE.ACCOUNT_NAME"
291 [logFromGroupDict setObject:logFromGroup forKey:folderName];
294 if (!(toSetForThisService = [toDict objectForKey:serviceClass])) {
295 toSetForThisService = [NSMutableSet set];
296 [toDict setObject:toSetForThisService
297 forKey:serviceClass];
300 //Add the 'to' for each grouping on this account
301 for (AILogToGroup *currentToGroup in [logFromGroup toGroupArray]) {
304 if ((currentTo = [currentToGroup to])) {
305 //Store currentToGroup on a key in the form "SERVICE.ACCOUNT_NAME/TARGET_CONTACT"
306 [logToGroupDict setObject:currentToGroup forKey:[currentToGroup relativePath]];
310 [logFromGroup release];
314 [self rebuildContactsList];
317 - (void)rebuildContactsList
319 NSInteger oldCount = toArray.count;
320 [toArray release]; toArray = [[NSMutableArray alloc] initWithCapacity:(oldCount ? oldCount : 20)];
322 for (AILogFromGroup *logFromGroup in [logFromGroupDict objectEnumerator]) {
323 //Add the 'to' for each grouping on this account
324 for (AILogToGroup *currentToGroup in [logFromGroup toGroupArray]) {
327 if ((currentTo = [currentToGroup to])) {
328 NSString *serviceClass = [currentToGroup serviceClass];
329 AIListObject *listObject = ((serviceClass && currentTo) ?
330 [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:serviceClass
333 if (listObject && [listObject isKindOfClass:[AIListContact class]]) {
334 AIListContact *parentContact = [(AIListContact *)listObject parentContact];
335 if (![toArray containsObjectIdenticalTo:parentContact]) {
336 [toArray addObject:parentContact];
340 if (![toArray containsObject:currentToGroup]) {
341 [toArray addObject:currentToGroup];
348 [toArray sortUsingFunction:toArraySort context:NULL];
349 [outlineView_contacts reloadData];
351 if (!isOpeningForContact) {
352 //If we're opening for a contact, the outline view selection will be changed in a moment anyways
353 [self outlineViewSelectionDidChange:nil];
357 - (NSString *)adiumFrameAutosaveName
359 return KEY_LOG_VIEWER_WINDOW_FRAME;
362 //Setup the window before it is displayed
363 - (void)windowDidLoad
365 suppressSearchRequests = YES;
367 [super windowDidLoad];
369 [plugin pauseIndexing];
371 [[self window] setTitle:AILocalizedString(@"Chat Transcript Viewer",nil)];
372 [textField_progress setStringValue:@""];
374 //Autosave doesn't do anything yet
375 [shelf_splitView setAutosaveName:@"LogViewer:Shelf"];
376 [shelf_splitView setFrame:[[[self window] contentView] frame]];
378 // Pull our main article/display split view out of the nib and position it in the shelf view
379 [containingView_results retain];
380 [containingView_results removeFromSuperview];
381 [shelf_splitView setContentView:containingView_results];
382 [containingView_results release];
383 [tableView_results accessibilitySetOverrideValue:AILocalizedString(@"Transcripts", nil)
384 forAttribute:NSAccessibilityRoleDescriptionAttribute];
386 // Pull our source view out of the nib and position it in the shelf view
387 [containingView_contactsSourceList retain];
388 [containingView_contactsSourceList removeFromSuperview];
389 [shelf_splitView setShelfView:containingView_contactsSourceList];
390 [outlineView_contacts accessibilitySetOverrideValue:AILocalizedString(@"Contacts", nil)
391 forAttribute:NSAccessibilityRoleDescriptionAttribute];
392 [containingView_contactsSourceList release];
394 //Set emoticon filtering
395 showEmoticons = [[adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_EMOTICONS
396 group:PREF_GROUP_LOGGING] boolValue];
397 [[toolbarItems objectForKey:@"toggleemoticons"] setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
398 [[toolbarItems objectForKey:@"toggleemoticons"] setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
400 // Set timestamp filtering
401 showTimestamps = [[adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_TIMESTAMPS
402 group:PREF_GROUP_LOGGING] boolValue];
403 [[toolbarItems objectForKey:@"toggletimestamps"] setLabel:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)];
404 [[toolbarItems objectForKey:@"toggletimestamps"] setImage:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]];
407 [self installToolbar];
409 [outlineView_contacts setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleSourceList];
411 AIImageTextCell *dataCell = [[AIImageTextCell alloc] init];
412 NSTableColumn *tableColumn = [[outlineView_contacts tableColumns] objectAtIndex:0];
413 [tableColumn setDataCell:dataCell];
414 [tableColumn setEditable:NO];
415 [dataCell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
418 // Set the selector for doubleAction
419 [outlineView_contacts setDoubleAction:@selector(openChatOnDoubleAction:)];
421 //Localize tableView_results column headers
422 [[[tableView_results tableColumnWithIdentifier:@"To"] headerCell] setStringValue:TO];
423 [[[tableView_results tableColumnWithIdentifier:@"From"] headerCell] setStringValue:FROM];
424 [[[tableView_results tableColumnWithIdentifier:@"Date"] headerCell] setStringValue:DATE];
425 [self tableViewColumnDidResize:nil];
427 [tableView_results sizeLastColumnToFit];
429 //Prepare the search controls
430 [self buildSearchMenu];
431 if ([textView_content respondsToSelector:@selector(setUsesFindPanel:)]) {
432 [textView_content setUsesFindPanel:YES];
435 //Sort by preference, defaulting to sorting by date
436 NSString *selectedTableColumnPref;
437 if ((selectedTableColumnPref = [adium.preferenceController preferenceForKey:KEY_LOG_VIEWER_SELECTED_COLUMN
438 group:PREF_GROUP_LOGGING])) {
439 selectedColumn = [[tableView_results tableColumnWithIdentifier:selectedTableColumnPref] retain];
441 if (!selectedColumn) {
442 selectedColumn = [[tableView_results tableColumnWithIdentifier:@"Date"] retain];
444 [self sortCurrentSearchResultsForTableColumn:selectedColumn direction:YES];
446 //Prepare indexing and filter searching
447 [plugin prepareLogContentSearching];
448 [self initLogFiltering];
450 //Begin our initial search
451 if (!isOpeningForContact)
452 [self setSearchMode:LOG_SEARCH_TO];
454 [searchField_logs setStringValue:(activeSearchString ? activeSearchString : @"")];
455 suppressSearchRequests = NO;
457 if (!isOpeningForContact) {
458 //If we're opening for a contact, we'll select it and then begin searching
459 [self startSearchingClearingCurrentResults:YES];
462 [tableView_results setAutosaveName:@"LogViewerResults"];
463 [tableView_results setAutosaveTableColumns:YES];
465 [plugin resumeIndexing];
468 -(void)rebuildIndices
470 //Rebuild the 'global' log indexes
471 [logFromGroupDict release]; logFromGroupDict = [[NSMutableDictionary alloc] init];
472 [toArray removeAllObjects]; //note: even if there are no logs, the name will remain [bug or feature?]
473 [toServiceArray removeAllObjects];
474 [fromArray removeAllObjects];
475 [fromServiceArray removeAllObjects];
477 [self initLogFiltering];
479 [tableView_results reloadData];
480 [self selectDisplayedLog];
483 //Called as the window closes
484 - (void)windowWillClose:(id)sender
486 [super windowWillClose:sender];
488 //Set preference for emoticon filtering
489 [adium.preferenceController setPreference:[NSNumber numberWithBool:showEmoticons]
490 forKey:KEY_LOG_VIEWER_EMOTICONS
491 group:PREF_GROUP_LOGGING];
493 // Set preference for timestamp filtering
494 [adium.preferenceController setPreference:[NSNumber numberWithBool:showTimestamps]
495 forKey:KEY_LOG_VIEWER_TIMESTAMPS
496 group:PREF_GROUP_LOGGING];
498 //Set preference for selected column
499 [adium.preferenceController setPreference:[selectedColumn identifier]
500 forKey:KEY_LOG_VIEWER_SELECTED_COLUMN
501 group:PREF_GROUP_LOGGING];
503 /* Disable the search field. If we don't disable the search field, it will often try to call its target action
504 * after the window has closed (and we are gone). I'm not sure why this happens, but disabling the field
505 * before we close the window down seems to prevent the crash.
507 [searchField_logs setEnabled:NO];
509 /* Note that the window is closing so we don't take behaviors which could cause messages to the window after
510 * it was gone, like responding to a logIndexUpdated message
512 windowIsClosing = YES;
514 //Abort any in-progress searching and indexing, and wait for their completion
515 [self stopSearching];
516 [plugin cleanUpLogContentSearching];
518 //Reset our column widths if needed
519 [activeSearchString release]; activeSearchString = nil;
520 [self updateRankColumnVisibility];
522 [sharedLogViewerInstance autorelease]; sharedLogViewerInstance = nil;
523 [toolbarItems autorelease]; toolbarItems = nil;
526 //Display --------------------------------------------------------------------------------------------------------------
528 //Update log viewer progress string to reflect current status
529 - (void)updateProgressDisplay
531 NSMutableString *progress = nil;
532 NSUInteger indexNumber, indexTotal;
535 //We always convey the number of logs being displayed
537 NSUInteger count = [currentSearchResults count];
538 if (activeSearchString && [activeSearchString length]) {
539 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
540 AILocalizedString(@"%lu matching transcripts",nil) :
541 AILocalizedString(@"1 matching transcript",nil)),count]];
543 [shelf_splitView setResizeThumbStringValue:[NSString stringWithFormat:((count != 1) ?
544 AILocalizedString(@"%lu transcripts",nil) :
545 AILocalizedString(@"1 transcript",nil)),count]];
547 //We are searching, but there is no active search string. This indicates we're still opening logs.
549 progress = [[AILocalizedString(@"Opening transcripts",nil) mutableCopy] autorelease];
552 [resultsLock unlock];
554 indexing = [plugin getIndexingProgress:&indexNumber outOf:&indexTotal];
556 //Append search progress
557 if (activeSearchString && [activeSearchString length]) {
559 [progress appendString:@" - "];
561 progress = [NSMutableString string];
564 if (searching || indexing) {
565 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Searching for '%@'",nil),activeSearchString]];
567 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Search for '%@' complete.",nil),activeSearchString]];
571 //Append indexing progress
574 [progress appendString:@" - "];
576 progress = [NSMutableString string];
579 [progress appendString:[NSString stringWithFormat:AILocalizedString(@"Indexing %lu of %lu transcripts",nil), indexNumber, indexTotal]];
582 if (progress && (searching || indexing || !(activeSearchString && [activeSearchString length]))) {
583 [progress appendString:[NSString ellipsis]];
586 //Enable/disable the searching animation
587 if (searching || indexing) {
588 [progressIndicator startAnimation:nil];
590 [progressIndicator stopAnimation:nil];
593 [textField_progress setStringValue:(progress ? progress : @"")];
596 //The plugin is informing us that the log indexing changed
597 - (void)logIndexingProgressUpdate
599 //Don't do anything if the window is already closing
600 if (!windowIsClosing) {
601 [self updateProgressDisplay];
603 //If we are searching by content, we should re-search without clearing our current results so the
604 //the newly-indexed logs can be added without blanking the current table contents.
605 if (searchMode == LOG_SEARCH_CONTENT && (activeSearchString && [activeSearchString length])) {
607 //We're already searching; reattempt when done
608 searchIDToReattemptWhenComplete = activeSearchID;
610 //We're not searching - restart the search immediately every 10 updates to utilize the newly indexed logs
611 indexingUpdatesReceivedWhileSearching++;
612 if ((indexingUpdatesReceivedWhileSearching % 10) == 0)
613 [self startSearchingClearingCurrentResults:NO];
619 //Refresh the results table
620 - (void)refreshResults
622 [self updateProgressDisplay];
624 [self refreshResultsSearchIsComplete:NO];
627 - (void)refreshResultsSearchIsComplete:(BOOL)searchIsComplete
630 NSInteger count = [currentSearchResults count];
631 [resultsLock unlock];
632 AILog(@"refreshResultsSearchIsComplete: %i (count is %i)",searchIsComplete,count);
634 if (searchIsComplete &&
635 ((activeSearchID == searchIDToReattemptWhenComplete) && !windowIsClosing)) {
636 searchIDToReattemptWhenComplete = -1;
637 [self startSearchingClearingCurrentResults:NO];
640 if (!searching || count <= MAX_LOGS_TO_SORT_WHILE_SEARCHING) {
641 //Sort the logs correctly which will also reload the table
644 if (searchIsComplete && automaticSearch) {
645 //If search is complete, select the first log if requested and possible
646 [self selectFirstLog];
649 BOOL oldAutomaticSearch = automaticSearch;
651 //We don't want the above re-selection to change our automaticSearch tracking
652 //(The only reason automaticSearch should change is in response to user action)
653 automaticSearch = oldAutomaticSearch;
658 [self selectCachedIndex];
661 [self updateProgressDisplay];
664 - (void)searchComplete
666 [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
667 [self refreshResultsSearchIsComplete:YES];
670 // Called on doubleAction to open a chat
671 -(void)openChatOnDoubleAction:(id)sender
673 id item = [outlineView_contacts firstSelectedItem];
674 if ([item isKindOfClass:[AIListContact class]]) {
675 //Open a new message with the contact
676 [adium.interfaceController setActiveChat:[adium.chatController openChatWithContact:(AIListContact *)item onPreferredAccount:YES]];
680 //Displays the contents of the specified log in our window
681 - (void)displayLogs:(NSArray *)logArray;
683 NSMutableAttributedString *displayText = nil;
684 NSAttributedString *finalDisplayText = nil;
685 NSRange scrollRange = NSMakeRange(0,0);
686 BOOL appendedFirstLog = NO;
688 if (![logArray isEqualToArray:displayedLogArray]) {
689 [displayedLogArray release];
690 displayedLogArray = [logArray copy];
693 if ([logArray count] > 1) {
694 displayText = [[NSMutableAttributedString alloc] init];
698 NSString *logBasePath = [AILoggerPlugin logBasePath];
699 AILog(@"Displaying %@",logArray);
700 for (theLog in logArray) {
701 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
704 if (!horizontalRule) {
705 #define HORIZONTAL_BAR 0x2013
706 #define HORIZONTAL_RULE_LENGTH 18
708 const unichar separatorUTF16[HORIZONTAL_RULE_LENGTH] = {
709 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
710 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR,
711 HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR, HORIZONTAL_BAR
713 horizontalRule = [[NSString alloc] initWithCharacters:separatorUTF16 length:HORIZONTAL_RULE_LENGTH];
716 [displayText appendString:[NSString stringWithFormat:@"%@%@\n%@ - %@\n%@\n\n",
717 (appendedFirstLog ? @"\n" : @""),
719 [headerDateFormatter stringFromDate:[theLog date]],
722 withAttributes:[[AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:NSBoldFontMask size:12] dictionary]];
725 if ([[theLog relativePath] hasSuffix:@".AdiumHTMLLog"] || [[theLog relativePath] hasSuffix:@".html"] || [[theLog relativePath] hasSuffix:@".html.bak"]) {
727 NSURL *logURL = [NSURL fileURLWithPath:[logBasePath stringByAppendingPathComponent:[theLog relativePath]]];
728 NSString *logFileText = [NSString stringWithContentsOfURL:logURL encoding:NSUTF8StringEncoding error:NULL];
729 NSAttributedString *attributedLogFileText = [AIHTMLDecoder decodeHTML:logFileText];
732 attributedLogFileText = [adium.contentController filterAttributedString:attributedLogFileText
733 usingFilterType:AIFilterMessageDisplay
734 direction:AIFilterOutgoing
739 [displayText appendAttributedString:attributedLogFileText];
741 displayText = [attributedLogFileText mutableCopy];
744 } else if ([[theLog relativePath] hasSuffix:@".chatlog"]){
746 NSString *logFullPath = [logBasePath stringByAppendingPathComponent:[theLog relativePath]];
749 if ([[NSFileManager defaultManager] fileExistsAtPath:logFullPath isDirectory:&isDir]) {
750 /* If we have a chatLog bundle, we want to get the text content for the xml file inside */
751 if (isDir) logFullPath = [logFullPath stringByAppendingPathComponent:
752 [[[logFullPath lastPathComponent] stringByDeletingPathExtension] stringByAppendingPathExtension:@"xml"]];
755 NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
756 [NSNumber numberWithBool:showTimestamps], @"showTimestamps",
757 [NSNumber numberWithBool:showEmoticons], @"showEmoticons",
759 NSAttributedString *attributedLogFileText = [AIXMLChatlogConverter readFile:logFullPath withOptions:options];
760 if (attributedLogFileText) {
762 [displayText appendAttributedString:attributedLogFileText];
764 displayText = [attributedLogFileText mutableCopy];
768 //Fallback: Plain text log
769 NSURL *logURL = [NSURL fileURLWithPath:[logBasePath stringByAppendingPathComponent:[theLog relativePath]]];
770 NSString *logFileText = [NSString stringWithContentsOfURL:logURL encoding:NSUTF8StringEncoding error:NULL];
772 AITextAttributes *textAttributes = [AITextAttributes textAttributesWithFontFamily:@"Helvetica" traits:0 size:12];
773 NSAttributedString *attributedLogFileText = [[[NSAttributedString alloc] initWithString:logFileText
774 attributes:[textAttributes dictionary]] autorelease];
776 attributedLogFileText = [adium.contentController filterAttributedString:attributedLogFileText
777 usingFilterType:AIFilterMessageDisplay
778 direction:AIFilterOutgoing
783 [displayText appendAttributedString:attributedLogFileText];
785 displayText = [attributedLogFileText mutableCopy];
790 appendedFirstLog = YES;
795 if (displayText && [displayText length]) {
796 //Add pretty formatting to links
797 [displayText addFormattingForLinks];
799 //If we are searching by content, highlight the search results
800 if ((searchMode == LOG_SEARCH_CONTENT) && [activeSearchString length]) {
801 NSString *searchWord;
802 NSMutableArray *searchWordsArray = [[activeSearchString componentsSeparatedByString:@" "] mutableCopy];
803 NSScanner *scanner = [NSScanner scannerWithString:activeSearchString];
805 //Look for an initial quote
806 NSAutoreleasePool *pool = nil;
807 while (![scanner isAtEnd]) {
809 pool = [[NSAutoreleasePool alloc] init];
811 [scanner scanUpToString:@"\"" intoString:NULL];
813 //Scan past the quote
814 if (![scanner scanString:@"\"" intoString:NULL]) {
815 [pool release]; pool = nil;
819 NSString *quotedString;
821 if (![scanner isAtEnd] &&
822 [scanner scanUpToString:@"\"" intoString:"edString]) {
823 //Scan past the quote
824 [scanner scanString:@"\"" intoString:NULL];
825 /* If a string within quotes is found, remove the words from the quoted string and add the full string
826 * to what we'll be highlighting.
828 * We'll use indexOfObject: and removeObjectAtIndex: so we only remove _one_ instance. Otherwise, this string:
829 * "killer attack ninja kittens" OR ninja
830 * wouldn't highlight the word ninja by itself.
832 NSArray *quotedWords = [quotedString componentsSeparatedByString:@" "];
833 NSInteger quotedWordsCount = [quotedWords count];
835 for (NSInteger i = 0; i < quotedWordsCount; i++) {
836 NSString *quotedWord = [quotedWords objectAtIndex:i];
838 //Originally started with a quote, so put it back on
839 quotedWord = [@"\"" stringByAppendingString:quotedWord];
841 if (i == quotedWordsCount - 1) {
842 //Originally ended with a quote, so put it back on
843 quotedWord = [quotedWord stringByAppendingString:@"\""];
845 NSInteger searchWordsIndex = [searchWordsArray indexOfObject:quotedWord];
846 if (searchWordsIndex != NSNotFound) {
847 [searchWordsArray removeObjectAtIndex:searchWordsIndex];
849 NSLog(@"displayLog: Couldn't find %@ in %@", quotedWord, searchWordsArray);
853 //Add the full quoted string
854 [searchWordsArray addObject:quotedString];
858 BOOL shouldScrollToWord = NO;
859 scrollRange = NSMakeRange([displayText length],0);
861 for (searchWord in searchWordsArray) {
864 //Check against and/or. We don't just remove it from the array because then we couldn't check case insensitively.
865 if (([searchWord caseInsensitiveCompare:@"and"] != NSOrderedSame) &&
866 ([searchWord caseInsensitiveCompare:@"or"] != NSOrderedSame)) {
867 [self hilightOccurrencesOfString:searchWord inString:displayText firstOccurrence:&occurrence];
869 //We'll want to scroll to the first occurrance of any matching word or words
870 if (occurrence.location < scrollRange.location) {
871 scrollRange = occurrence;
872 shouldScrollToWord = YES;
877 //If we shouldn't be scrolling to a new range, we want to scroll to the top
878 if (!shouldScrollToWord) scrollRange = NSMakeRange(0, 0);
880 [searchWordsArray release];
883 finalDisplayText = displayText;
886 if (finalDisplayText) {
887 [[textView_content textStorage] setAttributedString:finalDisplayText];
889 //Set this string and scroll to the top/bottom/occurrence
890 if ((searchMode == LOG_SEARCH_CONTENT) || automaticSearch) {
891 [textView_content scrollRangeToVisible:scrollRange];
893 [textView_content scrollRangeToVisible:NSMakeRange(0,0)];
897 //No log selected, empty the view
898 [textView_content setString:@""];
901 [displayText release];
904 - (void)displayLog:(AIChatLog *)theLog
906 [self displayLogs:(theLog ? [NSArray arrayWithObject:theLog] : nil)];
909 //Reselect the displayed log (Or another log if not possible)
910 - (void)selectDisplayedLog
912 NSInteger firstIndex = NSNotFound;
914 /* Is the log we had selected still in the table?
915 * (When performing an automatic search, we ignore the previous selection. This ensures that we always
916 * end up with the newest log selected, even when a search takes multiple passes/refreshes to complete).
918 if (!automaticSearch) {
920 [tableView_results selectItemsInArray:displayedLogArray usingSourceArray:currentSearchResults];
921 [resultsLock unlock];
923 firstIndex = [[tableView_results selectedRowIndexes] firstIndex];
926 if (firstIndex != NSNotFound) {
927 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
929 if (useSame == YES && sameSelection > 0) {
930 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:sameSelection] byExtendingSelection:NO];
932 [self selectFirstLog];
939 - (void)selectFirstLog
941 AIChatLog *theLog = nil;
943 //If our selected log is no more, select the first one in the list
945 if ([currentSearchResults count] != 0) {
946 theLog = [currentSearchResults objectAtIndex:0];
948 [resultsLock unlock];
950 //Change the table selection to this new log
951 //We need a little trickery here. When we change the row, the table view will call our tableViewSelectionDidChange: method.
952 //This method will clear the automaticSearch flag, and break any scroll-to-bottom behavior we have going on for the custom
953 //search. As a quick hack, I've added an ignoreSelectionChange flag that can be set to inform our selectionDidChange method
954 //that we instantiated this selection change, and not the user.
955 ignoreSelectionChange = YES;
956 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:0] byExtendingSelection:NO];
957 [tableView_results scrollRowToVisible:0];
958 ignoreSelectionChange = NO;
960 [self displayLog:theLog]; //Manually update the displayed log
963 //Highlight the occurences of a search string within a displayed log
964 - (void)hilightOccurrencesOfString:(NSString *)littleString inString:(NSMutableAttributedString *)bigString firstOccurrence:(NSRange *)outRange
966 NSInteger location = 0;
967 NSRange searchRange, foundRange;
968 NSString *plainBigString = [bigString string];
969 NSUInteger plainBigStringLength = [plainBigString length];
970 NSMutableDictionary *attributeDictionary = nil;
972 outRange->location = NSNotFound;
974 //Search for the little string in the big string
975 while (location != NSNotFound && location < plainBigStringLength) {
976 searchRange = NSMakeRange(location, plainBigStringLength-location);
977 foundRange = [plainBigString rangeOfString:littleString options:NSCaseInsensitiveSearch range:searchRange];
979 //Bold and color this match
980 if (foundRange.location != NSNotFound) {
981 if (outRange->location == NSNotFound) *outRange = foundRange;
983 if (!attributeDictionary) {
984 attributeDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
985 [NSFont boldSystemFontOfSize:14], NSFontAttributeName,
986 [NSColor yellowColor], NSBackgroundColorAttributeName,
989 [bigString addAttributes:attributeDictionary
993 location = NSMaxRange(foundRange);
998 //Sorting --------------------------------------------------------------------------------------------------------------
1002 NSString *identifier = [selectedColumn identifier];
1006 if ([identifier isEqualToString:@"To"]) {
1007 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareToReverse:) : @selector(compareTo:))];
1009 } else if ([identifier isEqualToString:@"From"]) {
1010 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareFromReverse:) : @selector(compareFrom:))];
1012 } else if ([identifier isEqualToString:@"Date"]) {
1013 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareDateReverse:) : @selector(compareDate:))];
1015 } else if ([identifier isEqualToString:@"Rank"]) {
1016 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareRankReverse:) : @selector(compareRank:))];
1018 } else if ([identifier isEqualToString:@"Service"]) {
1019 [currentSearchResults sortUsingSelector:(sortDirection ? @selector(compareServiceReverse:) : @selector(compareService:))];
1022 [resultsLock unlock];
1025 [tableView_results reloadData];
1027 //Reapply the selection
1028 [self selectDisplayedLog];
1031 //Sorts the selected log array and adjusts the selected column
1032 - (void)sortCurrentSearchResultsForTableColumn:(NSTableColumn *)tableColumn direction:(BOOL)direction
1034 //If there already was a sorted column, remove the indicator image from it.
1035 if (selectedColumn && selectedColumn != tableColumn) {
1036 [tableView_results setIndicatorImage:nil inTableColumn:selectedColumn];
1039 //Set the indicator image in the newly selected column
1040 [tableView_results setIndicatorImage:[NSImage imageNamed:(direction ? @"NSDescendingSortIndicator" : @"NSAscendingSortIndicator")]
1041 inTableColumn:tableColumn];
1043 //Set the highlighted table column.
1044 [tableView_results setHighlightedTableColumn:tableColumn];
1045 [selectedColumn release]; selectedColumn = [tableColumn retain];
1046 sortDirection = direction;
1051 //Searching ------------------------------------------------------------------------------------------------------------
1052 #pragma mark Searching
1053 //(Jag)Change search string
1054 - (void)controlTextDidChange:(NSNotification *)notification
1056 if (searchMode != LOG_SEARCH_CONTENT) {
1057 [self updateSearch:nil];
1061 //Change search string (Called by searchfield)
1062 - (IBAction)updateSearch:(id)sender
1064 automaticSearch = NO;
1065 [self setSearchString:[[[searchField_logs stringValue] copy] autorelease]];
1066 AILog(@"updateSearch calling startSearching");
1067 [self startSearchingClearingCurrentResults:YES];
1070 //Change search mode (Called by mode menu)
1071 - (IBAction)selectSearchType:(id)sender
1073 automaticSearch = NO;
1075 //First, update the search mode to the newly selected type
1076 [self setSearchMode:[sender tag]];
1078 //Then, ensure we are ready to search using the current string
1079 [self setSearchString:activeSearchString];
1081 //Now we are ready to start searching
1082 AILog(@"selectSearchType calling startSearching");
1083 [self startSearchingClearingCurrentResults:YES];
1086 //Begin a specific search
1087 - (void)setSearchString:(NSString *)inString mode:(LogSearchMode)inMode
1089 automaticSearch = YES;
1090 //Apply the search mode first since the behavior of setSearchString changes depending on the current mode
1091 [self setSearchMode:inMode];
1092 [self setSearchString:inString];
1094 AILog(@"setSearchString:mode: calling startSearching");
1095 [self startSearchingClearingCurrentResults:YES];
1098 //Begin the current search
1099 - (void)startSearchingClearingCurrentResults:(BOOL)clearCurrentResults
1101 NSDictionary *searchDict;
1103 if (suppressSearchRequests) return;
1104 AILog(@"Starting a search for %@",activeSearchString);
1106 //Once all searches have exited, we can start a new one
1107 if (clearCurrentResults) {
1109 //Stop any existing searches inside of resultsLock so we won't get any additions results added that we don't want
1110 [self stopSearching];
1112 [currentSearchResults release]; currentSearchResults = [[NSMutableArray alloc] init];
1113 [resultsLock unlock];
1115 //Stop any existing searches
1116 [self stopSearching];
1120 indexingUpdatesReceivedWhileSearching = 0;
1121 searchDict = [NSDictionary dictionaryWithObjectsAndKeys:
1122 [NSNumber numberWithInteger:activeSearchID], @"ID",
1123 [NSNumber numberWithInteger:searchMode], @"Mode",
1124 activeSearchString, @"String",
1125 [plugin logContentIndex], @"SearchIndex",
1127 [NSThread detachNewThreadSelector:@selector(filterLogsWithSearch:) toTarget:self withObject:searchDict];
1129 //Update the table periodically while the logs load.
1130 [refreshResultsTimer invalidate]; [refreshResultsTimer release];
1131 refreshResultsTimer = [[NSTimer scheduledTimerWithTimeInterval:REFRESH_RESULTS_INTERVAL
1133 selector:@selector(refreshResults)
1135 repeats:YES] retain];
1138 //Abort any active searches
1139 - (void)stopSearching
1141 [currentSearchLock lock];
1142 if (currentSearch) {
1143 SKSearchCancel(currentSearch);
1144 CFRelease(currentSearch); currentSearch = nil;
1146 [currentSearchLock unlock];
1148 [refreshResultsTimer invalidate]; [refreshResultsTimer release]; refreshResultsTimer = nil;
1150 //Increase the active search ID so any existing searches stop, and then
1151 //wait for any active searches to finish and release the lock
1155 //Set the active search mode (Does not invoke a search)
1156 - (void)setSearchMode:(LogSearchMode)inMode
1158 NSTextFieldCell *cell = [searchField_logs cell];
1160 searchMode = inMode;
1162 //Clear any filter from the table if it's the current mode, as well
1163 switch (searchMode) {
1164 case LOG_SEARCH_FROM:
1165 [cell setPlaceholderString:AILocalizedString(@"Search From","Placeholder for searching logs from an account")];
1169 [cell setPlaceholderString:AILocalizedString(@"Search To","Placeholder for searching logs with/to a contact")];
1172 case LOG_SEARCH_DATE:
1173 [cell setPlaceholderString:AILocalizedString(@"Search by Date","Placeholder for searching logs by date")];
1176 case LOG_SEARCH_CONTENT:
1177 [cell setPlaceholderString:AILocalizedString(@"Search Content","Placeholder for searching logs by content")];
1181 [self updateRankColumnVisibility];
1182 [self buildSearchMenu];
1185 - (void)updateRankColumnVisibility
1187 NSTableColumn *resultsColumn = [tableView_results tableColumnWithIdentifier:@"Rank"];
1189 if ((searchMode == LOG_SEARCH_CONTENT) && ([activeSearchString length])) {
1190 //Add the resultsColumn and resize if it should be shown but is not at present
1191 if (!resultsColumn) {
1192 NSArray *tableColumns;
1194 //Set up the results column
1195 resultsColumn = [[[NSTableColumn alloc] initWithIdentifier:@"Rank"] autorelease];
1196 [[resultsColumn headerCell] setTitle:AILocalizedString(@"Rank",nil)];
1197 [resultsColumn setDataCell:[[[ESRankingCell alloc] init] autorelease]];
1199 //Add it to the table
1200 [tableView_results addTableColumn:resultsColumn];
1202 //Make it half again as large as the desired width from the @"Rank" header title
1203 [resultsColumn sizeToFit];
1204 [resultsColumn setWidth:([resultsColumn width] * 1.5)];
1206 tableColumns = [tableView_results tableColumns];
1207 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1208 NSTableColumn *nextDoorNeighbor;
1210 //Adjust the column to the results column's left so results is now visible
1211 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1212 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]-[resultsColumn width]];
1216 //Remove the resultsColumn and resize if it should not be shown but is at present
1217 if (resultsColumn) {
1218 NSArray *tableColumns;
1220 tableColumns = [tableView_results tableColumns];
1221 if ([tableColumns indexOfObject:resultsColumn] > 0) {
1222 NSTableColumn *nextDoorNeighbor;
1224 //Adjust the column to the results column's left to take up the space again
1225 tableColumns = [tableView_results tableColumns];
1226 nextDoorNeighbor = [tableColumns objectAtIndex:([tableColumns indexOfObject:resultsColumn] - 1)];
1227 [nextDoorNeighbor setWidth:[nextDoorNeighbor width]+[resultsColumn width]];
1231 [tableView_results removeTableColumn:resultsColumn];
1236 //Set the active search string (Does not invoke a search)
1237 - (void)setSearchString:(NSString *)inString
1239 if (![[searchField_logs stringValue] isEqualToString:inString]) {
1240 [searchField_logs setStringValue:(inString ? inString : @"")];
1243 //Use autorelease so activeSearchString can be passed back to here
1244 if (activeSearchString != inString) {
1245 [activeSearchString release];
1246 activeSearchString = [inString retain];
1249 [self updateRankColumnVisibility];
1252 //Build the search mode menu
1253 - (void)buildSearchMenu
1255 NSMenu *cellMenu = [[[NSMenu allocWithZone:[NSMenu menuZone]] initWithTitle:SEARCH_MENU] autorelease];
1256 [cellMenu addItem:[self _menuItemWithTitle:FROM forSearchMode:LOG_SEARCH_FROM]];
1257 [cellMenu addItem:[self _menuItemWithTitle:TO forSearchMode:LOG_SEARCH_TO]];
1258 [cellMenu addItem:[self _menuItemWithTitle:DATE forSearchMode:LOG_SEARCH_DATE]];
1259 [cellMenu addItem:[self _menuItemWithTitle:CONTENT forSearchMode:LOG_SEARCH_CONTENT]];
1261 [[searchField_logs cell] setSearchMenuTemplate:cellMenu];
1264 - (void)_willOpenForContact
1266 isOpeningForContact = YES;
1269 - (void)_didOpenForContact
1271 isOpeningForContact = NO;
1275 * @brief Focus the log viewer on a particular contact
1277 * If the contact is within a metacontact, the metacontact will be focused.
1279 - (void)filterForContact:(AIListContact *)inContact
1281 AIListContact *parentContact = [inContact parentContact];
1283 if (!isOpeningForContact) {
1284 /* Ensure the contacts list includes this contact, since only existing AIListContacts are to be used
1285 * (with AILogToGroup objects used if an AIListContact isn't available) but that situation may have changed
1286 * with regard to inContact since the log viewer opened.
1288 * If we're opening initially, the list is guaranteed fresh.
1290 [self rebuildContactsList];
1293 //If the search mode is currently the TO field, switch it to content, which is what it should now intuitively do
1294 if (searchMode == LOG_SEARCH_TO) {
1295 [self setSearchMode:LOG_SEARCH_CONTENT];
1297 //Update our search string to ensure we're configured for content searching
1298 [self setSearchString:activeSearchString];
1301 //Changing the selection will start a new search
1302 [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(parentContact ? (id)parentContact : (id)allContactsIdentifier)]];
1303 NSUInteger selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1304 if (selectedRow != NSNotFound) {
1305 [outlineView_contacts scrollRowToVisible:selectedRow];
1309 - (void)filterForChatName:(NSString *)chatName withAccount:(AIAccount *)account
1311 if (!isOpeningForContact) {
1313 [self rebuildContactsList];
1316 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[[NSString stringWithFormat:@"%@.%@",
1317 account.service.serviceID,
1318 account.UID.safeFilenameString]
1319 stringByAppendingPathComponent:chatName]];
1321 //Changing the selection will start a new search
1322 [outlineView_contacts selectItemsInArray:[NSArray arrayWithObject:(logToGroup ?: (id)allContactsIdentifier)]];
1323 NSUInteger selectedRow = [[outlineView_contacts selectedRowIndexes] firstIndex];
1324 if (selectedRow != NSNotFound) {
1325 [outlineView_contacts scrollRowToVisible:selectedRow];
1330 * @brief Returns a menu item for the search mode menu
1332 - (NSMenuItem *)_menuItemWithTitle:(NSString *)title forSearchMode:(LogSearchMode)mode
1334 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
1335 action:@selector(selectSearchType:)
1337 [menuItem setTag:mode];
1338 [menuItem setState:(mode == searchMode ? NSOnState : NSOffState)];
1340 return [menuItem autorelease];
1343 #pragma mark Filtering search results
1345 - (BOOL)chatLogMatchesDateFilter:(AIChatLog *)inChatLog
1347 BOOL matchesDateFilter;
1349 switch (filterDateType) {
1350 case AIDateTypeAfter:
1351 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] > 0);
1353 case AIDateTypeBefore:
1354 matchesDateFilter = ([[inChatLog date] timeIntervalSinceDate:filterDate] < 0);
1356 case AIDateTypeExactly:
1357 matchesDateFilter = [inChatLog isFromSameDayAsDate:filterDate];
1360 matchesDateFilter = YES;
1364 return matchesDateFilter;
1368 NSArray *pathComponentsForDocument(SKDocumentRef inDocument)
1370 CFURLRef url = SKDocumentCopyURL(inDocument);
1372 AILogWithSignature(@"Could not get url for %p", inDocument);
1376 NSString *logPath = [(NSURL *)url path];
1378 AILogWithSignature(@"Could not get path for %@", url);
1379 NSArray *pathComponents = [logPath pathComponents];
1383 return pathComponents;
1388 * @brief Should a search display a document with the given information?
1390 - (BOOL)searchShouldDisplayDocument:(SKDocumentRef)inDocument pathComponents:(NSArray *)pathComponents testDate:(BOOL)testDate
1392 BOOL shouldDisplayDocument = YES;
1394 if ([contactIDsToFilter count]) {
1395 //Determine the path components if we weren't supplied them
1396 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1398 NSUInteger numPathComponents = [pathComponents count];
1400 NSArray *serviceAndFromUIDArray = [[pathComponents objectAtIndex:numPathComponents-3] componentsSeparatedByString:@"."];
1401 NSString *serviceClass = (([serviceAndFromUIDArray count] >= 2) ? [serviceAndFromUIDArray objectAtIndex:0] : @"");
1403 NSString *contactName = [pathComponents objectAtIndex:(numPathComponents-2)];
1405 shouldDisplayDocument = [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",serviceClass,contactName] compactedString]];
1408 if (shouldDisplayDocument && testDate && (filterDateType != AIDateTypeAnyDate)) {
1409 if (!pathComponents) pathComponents = pathComponentsForDocument(inDocument);
1411 NSUInteger numPathComponents = [pathComponents count];
1412 NSString *toPath = [NSString stringWithFormat:@"%@/%@",
1413 [pathComponents objectAtIndex:numPathComponents-3],
1414 [pathComponents objectAtIndex:numPathComponents-2]];
1415 NSString *relativePath = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1418 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:relativePath];
1420 shouldDisplayDocument = [self chatLogMatchesDateFilter:theLog];
1423 return shouldDisplayDocument;
1426 //Threaded filter/search methods ---------------------------------------------------------------------------------------
1427 #pragma mark Threaded filter/search methods
1430 * @brief Perform a content search of the indexed logs
1432 * This uses the 10.4+ asynchronous search functions.
1433 * Google-like search syntax (phrase, prefix/suffix, boolean, etc. searching) is automatically supported.
1435 - (void)_logContentFilter:(NSString *)searchString searchID:(NSInteger)searchID onSearchIndex:(SKIndexRef)logSearchIndex
1437 CGFloat largestRankingValue = 0;
1438 SKSearchRef thisSearch;
1439 Boolean more = true;
1440 UInt32 totalCount = 0;
1442 [currentSearchLock lock];
1443 if (currentSearch) {
1444 SKSearchCancel(currentSearch);
1445 CFRelease(currentSearch); currentSearch = NULL;
1448 NSMutableString *wildcardedSearchString = [NSMutableString string];
1449 for (NSString *searchComponent in [searchString componentsSeparatedByString:@" "]) {
1450 if ([searchComponent rangeOfString:@"*"].location == NSNotFound) {
1451 //If the user specifies particular wildcard behavior, respect it
1452 [wildcardedSearchString appendFormat:@"*%@* ", searchComponent];
1454 [wildcardedSearchString appendFormat:@"%@ ", searchComponent];
1457 thisSearch = SKSearchCreate(logSearchIndex,
1458 (CFStringRef)wildcardedSearchString,
1459 kSKSearchOptionDefault);
1460 currentSearch = (thisSearch ? (SKSearchRef)CFRetain(thisSearch) : NULL);
1461 [currentSearchLock unlock];
1463 //Retrieve matches as long as more are pending
1464 while (more && currentSearch) {
1465 #define BATCH_NUMBER 100
1466 SKDocumentID foundDocIDs[BATCH_NUMBER];
1467 float foundScores[BATCH_NUMBER];
1468 SKDocumentRef foundDocRefs[BATCH_NUMBER];
1470 CFIndex foundCount = 0;
1473 more = SKSearchFindMatches (
1478 0.5, // maximum time before func returns, in seconds
1482 totalCount += foundCount;
1484 SKIndexCopyDocumentRefsForDocumentIDs (
1490 for (i = 0; ((i < foundCount) && (searchID == activeSearchID)) ; i++) {
1491 SKDocumentRef document = foundDocRefs[i];
1493 AILogWithSignature(@"SearchKit returned NULL document for ID %ld", (long)foundDocIDs[i]);
1497 CFURLRef url = SKDocumentCopyURL(document);
1499 AILogWithSignature(@"No URL for document %p", document);
1504 * Nasty implementation note: As of 10.4.7 and all previous versions, a path longer than 1024 bytes (PATH_MAX)
1505 * will cause CFURLCopyFileSystemPath() to crash [ultimately in CFGetAllocator()]. This is the case for all
1506 * Cocoa applications...
1508 NSString *logPath = [(NSURL *)url path];
1510 AILogWithSignature(@"Could not get path for %@. ", url);
1512 NSArray *pathComponents = [(NSString *)logPath pathComponents];
1514 /* Handle chatlogs-as-bundles, which have an xml file inside our target .chatlog path */
1515 if ([[[pathComponents lastObject] pathExtension] caseInsensitiveCompare:@"xml"] == NSOrderedSame)
1516 pathComponents = [pathComponents subarrayWithRange:NSMakeRange(0, [pathComponents count] - 1)];
1518 //Don't test for the date now; we'll test once we've found the AIChatLog if we make it that far
1519 if ([self searchShouldDisplayDocument:document pathComponents:pathComponents testDate:NO]) {
1520 NSUInteger numPathComponents = [pathComponents count];
1521 NSString *toPath = [NSString stringWithFormat:@"%@/%@",
1522 [pathComponents objectAtIndex:numPathComponents-3],
1523 [pathComponents objectAtIndex:numPathComponents-2]];
1524 NSString *path = [NSString stringWithFormat:@"%@/%@",toPath,[pathComponents objectAtIndex:numPathComponents-1]];
1527 /* Add the log - if our index is currently out of date (for example, a log was just deleted)
1528 * we may get a null log, so be careful.
1530 theLog = [[logToGroupDict objectForKey:toPath] logAtPath:path];
1532 AILog(@"_logContentFilter: %x's key %@ yields %@; logAtPath:%@ gives %@",logToGroupDict,toPath,[logToGroupDict objectForKey:toPath],path,theLog);
1535 if ((theLog != nil) &&
1536 (![currentSearchResults containsObjectIdenticalTo:theLog]) &&
1537 [self chatLogMatchesDateFilter:theLog] &&
1538 (searchID == activeSearchID)) {
1539 [theLog setRankingValueOnArbitraryScale:foundScores[i]];
1541 //SearchKit does not normalize ranking scores, so we track the largest we've found and use it as 1.0
1542 if (foundScores[i] > largestRankingValue) largestRankingValue = foundScores[i];
1544 [currentSearchResults addObject:theLog];
1546 //Didn't get a valid log, so decrement our totalCount which is tracking how many logs we found
1549 [resultsLock unlock];
1552 //Didn't add this log, so decrement our totalCount which is tracking how many logs we found
1556 //if (logPath) CFRelease(logPath);
1557 if (url) CFRelease(url);
1558 if (document) CFRelease(document);
1561 //Scale all logs' ranking values to the largest ranking value we've seen thus far
1563 for (i = 0; ((i < totalCount) && (searchID == activeSearchID)); i++) {
1564 AIChatLog *theLog = [currentSearchResults objectAtIndex:i];
1565 [theLog setRankingPercentage:([theLog rankingValueOnArbitraryScale] / largestRankingValue)];
1567 [resultsLock unlock];
1569 [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1573 if (searchID != activeSearchID) {
1578 //Ensure current search isn't released in two places simultaneously
1579 [currentSearchLock lock];
1580 if (currentSearch) {
1581 CFRelease(currentSearch);
1582 currentSearch = NULL;
1584 [currentSearchLock unlock];
1586 if (thisSearch) CFRelease(thisSearch);
1589 //Search the logs, filtering out any matching logs into the currentSearchResults
1590 - (void)filterLogsWithSearch:(NSDictionary *)searchInfoDict
1592 NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
1593 NSInteger mode = [[searchInfoDict objectForKey:@"Mode"] integerValue];
1594 NSInteger searchID = [[searchInfoDict objectForKey:@"ID"] integerValue];
1595 NSString *searchString = [searchInfoDict objectForKey:@"String"];
1597 if (searchID == activeSearchID) { //If we're still supposed to go
1599 AILog(@"filterLogsWithSearch (search ID %i): %@",searchID,searchInfoDict);
1601 [plugin pauseIndexing];
1602 if (searchString && [searchString length]) {
1604 case LOG_SEARCH_FROM:
1606 case LOG_SEARCH_DATE:
1607 [self _logFilter:searchString
1611 case LOG_SEARCH_CONTENT:
1612 [self _logContentFilter:searchString
1614 onSearchIndex:(SKIndexRef)[searchInfoDict objectForKey:@"SearchIndex"]];
1618 [self _logFilter:nil
1625 [plugin resumeIndexing];
1626 [self performSelectorOnMainThread:@selector(searchComplete) withObject:nil waitUntilDone:NO];
1627 AILog(@"filterLogsWithSearch (search ID %i): finished",searchID);
1634 //Perform a filter search based on source name, destination name, or date
1635 - (void)_logFilter:(NSString *)searchString searchID:(NSInteger)searchID mode:(LogSearchMode)mode
1637 UInt32 lastUpdate = TickCount();
1639 NSCalendarDate *searchStringDate = nil;
1641 if ((mode == LOG_SEARCH_DATE) && (searchString != nil)) {
1642 searchStringDate = [[NSDate dateWithNaturalLanguageString:searchString] dateWithCalendarFormat:nil timeZone:nil];
1645 //Walk through every 'from' group
1646 for (AILogFromGroup *fromGroup in [logFromGroupDict objectEnumerator]) {
1647 if (searchID != activeSearchID) break;
1649 //When searching in LOG_SEARCH_FROM, we only proceed into matching groups
1650 if ((mode != LOG_SEARCH_FROM) ||
1652 ([[fromGroup fromUID] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound)) {
1654 //Walk through every 'to' group
1655 for (AILogToGroup *toGroup in [fromGroup toGroupArray]) {
1656 if (searchID != activeSearchID) break;
1658 /* When searching in LOG_SEARCH_TO, we only proceed into matching groups
1659 * For all other search modes, we always proceed here so long as either:
1660 * a) We are not filtering for specific contact names or
1661 * b) The contact name matches one of the names in contactIDsToFilter
1663 if ((![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) &&
1664 ((mode != LOG_SEARCH_TO) ||
1666 ([[toGroup to] rangeOfString:searchString options:NSCaseInsensitiveSearch].location != NSNotFound))) {
1668 //Walk through every log
1669 for (AIChatLog *theLog in [toGroup logEnumerator]) {
1670 if (searchID != activeSearchID) break;
1672 /* When searching in LOG_SEARCH_DATE, we must have matching dates
1673 * For all other search modes, we always proceed here
1675 if ((mode != LOG_SEARCH_DATE) ||
1677 (searchStringDate && [theLog isFromSameDayAsDate:searchStringDate])) {
1679 if ([self chatLogMatchesDateFilter:theLog]) {
1682 [currentSearchResults addObject:theLog];
1683 [resultsLock unlock];
1686 if (lastUpdate == 0 || TickCount() > lastUpdate + LOG_SEARCH_STATUS_INTERVAL) {
1687 [self performSelectorOnMainThread:@selector(updateProgressDisplay)
1690 lastUpdate = TickCount();
1701 //Search results table view --------------------------------------------------------------------------------------------
1702 #pragma mark Search results table view
1703 //Since this table view's source data will be accessed from within other threads, we need to lock before
1704 //accessing it. We also must be very sure that an incorrect row request is handled silently, since this
1705 //can occur if the array size is changed during the reload.
1706 - (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
1711 count = [currentSearchResults count];
1712 [resultsLock unlock];
1718 - (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
1720 NSString *identifier = [tableColumn identifier];
1722 if ([identifier isEqualToString:@"Rank"] && row >= 0 && row < [currentSearchResults count]) {
1723 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1725 [aCell setPercentage:[theLog rankingPercentage]];
1729 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
1731 NSString *identifier = [tableColumn identifier];
1735 if (row < 0 || row >= [currentSearchResults count]) {
1736 if ([identifier isEqualToString:@"Service"]) {
1743 AIChatLog *theLog = [currentSearchResults objectAtIndex:row];
1745 if ([identifier isEqualToString:@"To"]) {
1746 // Get ListObject for to-UID
1747 AIListObject *listObject = [adium.contactController existingListObjectWithUniqueID:[AIListObject internalObjectIDForServiceID:[theLog serviceClass]
1750 //Use the longDisplayName, following the user's contact list preferences as this is presumably how she wants to view contacts' names.
1751 if (![listObject.displayName isEqualToString:listObject.UID]) {
1752 value = [NSString stringWithFormat:@"%@ (%@)", listObject.displayName, listObject.UID];
1754 value = listObject.formattedUID;
1758 //No username available
1759 value = [theLog to];
1762 } else if ([identifier isEqualToString:@"From"]) {
1763 value = [theLog from];
1765 } else if ([identifier isEqualToString:@"Date"]) {
1766 value = [theLog date];
1768 } else if ([identifier isEqualToString:@"Service"]) {
1769 NSString *serviceClass;
1772 serviceClass = [theLog serviceClass];
1773 image = [AIServiceIcons serviceIconForService:[adium.accountController firstServiceWithServiceID:serviceClass]
1774 type:AIServiceIconSmall
1775 direction:AIIconNormal];
1776 value = (image ? image : blankImage);
1779 [resultsLock unlock];
1784 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1786 [NSObject cancelPreviousPerformRequestsWithTarget:self
1787 selector:@selector(tableViewSelectionDidChangeDelayed)
1790 [self performSelector:@selector(tableViewSelectionDidChangeDelayed)
1795 - (void)tableViewSelectionDidChangeDelayed
1797 if (!ignoreSelectionChange) {
1798 NSArray *selectedLogs;
1800 //Update the displayed log
1801 automaticSearch = NO;
1804 selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
1805 [resultsLock unlock];
1807 [self displayLogs:selectedLogs];
1811 //Sort the log array & reflect the new column
1812 - (void)tableView:(NSTableView*)tableView didClickTableColumn:(NSTableColumn *)tableColumn
1814 [self sortCurrentSearchResultsForTableColumn:tableColumn
1815 direction:(selectedColumn == tableColumn ? !sortDirection : sortDirection)];
1818 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
1821 NSArray *selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
1822 [resultsLock unlock];
1824 if ([selectedLogs count] > 0) {
1825 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
1826 [alert beginSheetModalForWindow:[self window]
1828 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
1829 contextInfo:[selectedLogs retain]];
1833 - (void)tableViewColumnDidResize:(NSNotification *)aNotification
1835 NSTableColumn *dateTableColumn = [tableView_results tableColumnWithIdentifier:@"Date"];
1837 if (!aNotification ||
1838 ([[aNotification userInfo] objectForKey:@"NSTableColumn"] == dateTableColumn)) {
1839 NSDateFormatter *dateFormatter;
1840 NSCell *cell = [dateTableColumn dataCell];
1842 [cell setObjectValue:[NSDate date]];
1844 CGFloat width = [dateTableColumn width];
1846 #define NUMBER_TIME_STYLES 2
1847 #define NUMBER_DATE_STYLES 4
1848 NSDateFormatterStyle timeFormatterStyles[NUMBER_TIME_STYLES] = { NSDateFormatterShortStyle, NSDateFormatterNoStyle};
1849 NSDateFormatterStyle formatterStyles[NUMBER_DATE_STYLES] = { NSDateFormatterFullStyle, NSDateFormatterLongStyle, NSDateFormatterMediumStyle, NSDateFormatterShortStyle };
1850 CGFloat requiredWidth;
1852 dateFormatter = [cell formatter];
1853 if (!dateFormatter) {
1854 dateFormatter = [[[AILogDateFormatter alloc] init] autorelease];
1855 [dateFormatter setFormatterBehavior:NSDateFormatterBehavior10_4];
1856 [cell setFormatter:dateFormatter];
1859 requiredWidth = width + 1;
1860 for (NSInteger i = 0; (i < NUMBER_TIME_STYLES) && (requiredWidth > width); i++) {
1861 [dateFormatter setTimeStyle:timeFormatterStyles[i]];
1863 for (NSInteger j = 0; (j < NUMBER_DATE_STYLES) && (requiredWidth > width); j++) {
1864 [dateFormatter setDateStyle:formatterStyles[j]];
1865 requiredWidth = [cell cellSizeForBounds:NSMakeRect(0,0,1e6,1e6)].width;
1866 //Require a bit of space so the date looks comfortable. Very long dates relative to the current date can still overflow...
1873 - (IBAction)toggleEmoticonFiltering:(id)sender
1875 showEmoticons = !showEmoticons;
1876 [sender setLabel:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)];
1877 [sender setImage:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]];
1879 [self displayLogs:displayedLogArray];
1882 - (IBAction)toggleTimestampFiltering:(id)sender
1884 showTimestamps = !showTimestamps;
1885 [sender setLabel:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)];
1886 [sender setImage:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]];
1888 [self displayLogs:displayedLogArray];
1891 #pragma mark Outline View Data source
1892 - (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item
1896 return allContactsIdentifier;
1899 return [toArray objectAtIndex:index-1]; //-1 for the All item, which is index 0
1903 if ([item isKindOfClass:[AIMetaContact class]]) {
1904 return [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] objectAtIndex:index];
1911 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item
1914 ([item isKindOfClass:[AIMetaContact class]] && ([[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1)) ||
1915 [item isKindOfClass:[NSArray class]]);
1918 - (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item
1921 return [toArray count] + 1; //+1 for the All item
1923 } else if ([item isKindOfClass:[AIMetaContact class]]) {
1924 NSUInteger count = [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count];
1935 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item
1937 Class itemClass = [item class];
1939 if (itemClass == [AIMetaContact class]) {
1940 return [(AIMetaContact *)item longDisplayName];
1942 } else if (itemClass == [AIListContact class]) {
1943 if ([(AIListContact *)item parentContact] != item) {
1944 //This contact is within a metacontact - always show its UID
1945 return [(AIListContact *)item formattedUID];
1947 return [(AIListContact *)item longDisplayName];
1950 } else if (itemClass == [AILogToGroup class]) {
1951 return [(AILogToGroup *)item to];
1953 } else if (itemClass == [allContactsIdentifier class]) {
1954 NSUInteger contactCount = [toArray count];
1955 return [NSString stringWithFormat:AILocalizedString(@"All (%@)", nil),
1956 ((contactCount == 1) ?
1957 AILocalizedString(@"1 Contact", nil) :
1958 [NSString stringWithFormat:AILocalizedString(@"%lu Contacts", nil), contactCount])];
1960 } else if (itemClass == [NSString class]) {
1964 NSLog(@"%@: no idea",item);
1969 - (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item
1971 if ([item isKindOfClass:[AIMetaContact class]] &&
1972 [[(AIMetaContact *)item listContactsIncludingOfflineAccounts] count] > 1) {
1973 /* If the metacontact contains a single contact, fall through (isKindOfClass:[AIListContact class]) and allow using of a service icon.
1974 * If it has multiple contacts, use no icon unless a user icon is present.
1976 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1977 size:NSMakeSize(16,16)];
1978 if (!image) image = [[[NSImage alloc] initWithSize:NSMakeSize(16, 16)] autorelease];
1980 [cell setImage:image];
1982 } else if ([item isKindOfClass:[AIListContact class]]) {
1983 NSImage *image = [AIUserIcons listUserIconForContact:(AIListContact *)item
1984 size:NSMakeSize(16,16)];
1985 if (!image) image = [AIServiceIcons serviceIconForObject:(AIListContact *)item
1986 type:AIServiceIconSmall
1987 direction:AIIconFlipped];
1988 [cell setImage:image];
1990 } else if ([item isKindOfClass:[AILogToGroup class]]) {
1991 [cell setImage:[AIServiceIcons serviceIconForService:[adium.accountController firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]]
1992 type:AIServiceIconSmall
1993 direction:AIIconNormal]];
1995 } else if ([item isKindOfClass:[allContactsIdentifier class]]) {
1996 if ([[outlineView arrayOfSelectedItems] containsObjectIdenticalTo:item] &&
1997 ([[self window] isKeyWindow] && ([[self window] firstResponder] == self))) {
1998 if (!adiumIconHighlighted) {
1999 adiumIconHighlighted = [[NSImage imageNamed:@"adiumHighlight"
2000 forClass:[self class]] retain];
2003 [cell setImage:adiumIconHighlighted];
2007 adiumIcon = [[NSImage imageNamed:@"adium"
2008 forClass:[self class]] retain];
2011 [cell setImage:adiumIcon];
2014 } else if ([item isKindOfClass:[NSString class]]) {
2015 [cell setImage:nil];
2018 NSLog(@"%@: no idea",item);
2019 [cell setImage:nil];
2024 * @brief Is item supposed to have a divider below?
2027 - (AIDividerPosition)outlineView:(NSOutlineView*)outlineView dividerPositionForItem:(id)item
2029 if ([item isKindOfClass:[allContactsIdentifier class]]) {
2030 return AIDividerPositionBelow;
2032 return AIDividerPositionNone;
2036 - (void)outlineViewDeleteSelectedRows:(NSTableView *)tableView
2038 [self deleteSelection:nil];
2042 - (void)outlineViewSelectionDidChange:(NSNotification *)notification
2044 [NSObject cancelPreviousPerformRequestsWithTarget:self
2045 selector:@selector(outlineViewSelectionDidChangeDelayed)
2048 [self performSelector:@selector(outlineViewSelectionDidChangeDelayed)
2053 - (void)outlineViewSelectionDidChangeDelayed
2055 NSArray *selectedItems = [outlineView_contacts arrayOfSelectedItems];
2057 [contactIDsToFilter removeAllObjects];
2059 if ([selectedItems count] && ![selectedItems containsObject:allContactsIdentifier]) {
2062 for (item in selectedItems) {
2063 if ([item isKindOfClass:[AIMetaContact class]]) {
2064 for (AIListContact *contact in [(AIMetaContact *)item listContactsIncludingOfflineAccounts]) {
2065 [contactIDsToFilter addObject:
2066 [[[NSString stringWithFormat:@"%@.%@", contact.service.serviceID, contact.UID] compactedString] safeFilenameString]];
2069 } else if ([item isKindOfClass:[AIListContact class]]) {
2070 [contactIDsToFilter addObject:
2071 [[[NSString stringWithFormat:@"%@.%@",((AIListContact *)item).service.serviceID,((AIListContact *)item).UID] compactedString] safeFilenameString]];
2073 } else if ([item isKindOfClass:[AILogToGroup class]]) {
2074 [contactIDsToFilter addObject:[[NSString stringWithFormat:@"%@.%@",[(AILogToGroup *)item serviceClass],[(AILogToGroup *)item to]] compactedString]];
2079 [self startSearchingClearingCurrentResults:YES];
2082 - (NSMenu *)outlineView:(NSOutlineView *)outlineView menuForEvent:(NSEvent *)theEvent;
2084 if (outlineView == outlineView_contacts) {
2085 NSInteger clickedRow = [outlineView_contacts rowAtPoint:[outlineView_contacts convertPoint:[theEvent locationInWindow]
2087 id item = [outlineView_contacts itemAtRow:clickedRow];
2089 //If we have a To group, see if we can make a contact out of it
2090 if ([item isKindOfClass:[AILogToGroup class]]) {
2091 if ([(AILogToGroup *)item to] && [(AILogToGroup *)item serviceClass]) {
2092 //We need a service with ther right service ID
2093 AIService *service = [adium.accountController firstServiceWithServiceID:[(AILogToGroup *)item serviceClass]];
2095 //Next, we want an online account
2096 AIAccount *account = nil;
2097 for (account in [adium.accountController accountsCompatibleWithService:service]) {
2098 if (account.online) break;
2102 //Finally, make a contact
2103 item = [adium.contactController contactWithService:service
2105 UID:[(AILogToGroup *)item to]];
2112 if ([item isKindOfClass:[AIListContact class]]) {
2113 NSArray *locationsArray = [NSArray arrayWithObjects:
2114 [NSNumber numberWithInteger:Context_Contact_Message],
2115 [NSNumber numberWithInteger:Context_Contact_Manage],
2116 [NSNumber numberWithInteger:Context_Contact_Action],
2117 [NSNumber numberWithInteger:Context_Contact_ListAction],
2118 [NSNumber numberWithInteger:Context_Contact_NegativeAction],
2119 [NSNumber numberWithInteger:Context_Contact_Additions], nil];
2121 return [adium.menuController contextualMenuWithLocations:locationsArray
2122 forListObject:(AIListContact *)item];
2129 static NSInteger toArraySort(id itemA, id itemB, void *context)
2131 NSString *nameA = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemA];
2132 NSString *nameB = [sharedLogViewerInstance outlineView:nil objectValueForTableColumn:nil byItem:itemB];
2133 NSComparisonResult result = [nameA caseInsensitiveCompare:nameB];
2134 if (result == NSOrderedSame) result = [nameA compare:nameB];
2139 - (void)draggedDividerRightBy:(CGFloat)deltaX
2141 desiredContactsSourceListDeltaX = deltaX;
2142 [splitView_contacts_results resizeSubviewsWithOldSize:[splitView_contacts_results frame].size];
2143 desiredContactsSourceListDeltaX = 0;
2147 - (void)splitView:(NSSplitView *)sender resizeSubviewsWithOldSize:(NSSize)oldSize
2149 if ((sender == splitView_contacts_results) &&
2150 desiredContactsSourceListDeltaX != 0) {
2151 float dividerThickness = [sender dividerThickness];
2153 NSRect newFrame = [sender frame];
2154 NSRect leftFrame = [containingView_contactsSourceList frame];
2155 NSRect rightFrame = [containingView_results frame];
2157 leftFrame.size.width += desiredContactsSourceListDeltaX;
2158 leftFrame.size.height = newFrame.size.height;
2159 leftFrame.origin = NSMakePoint(0,0);
2161 rightFrame.size.width = newFrame.size.width - leftFrame.size.width - dividerThickness;
2162 rightFrame.size.height = newFrame.size.height;
2163 rightFrame.origin.x = leftFrame.size.width + dividerThickness;
2165 [containingView_contactsSourceList setFrame:leftFrame];
2166 [containingView_contactsSourceList setNeedsDisplay:YES];
2167 [containingView_results setFrame:rightFrame];
2168 [containingView_results setNeedsDisplay:YES];
2171 //Perform the default implementation
2172 [sender adjustSubviews];
2177 //Window Toolbar -------------------------------------------------------------------------------------------------------
2178 #pragma mark Window Toolbar
2180 - (void)installToolbar
2182 [NSBundle loadNibNamed:[self dateItemNibName] owner:self];
2184 NSToolbar *toolbar = [[[NSToolbar alloc] initWithIdentifier:TOOLBAR_LOG_VIEWER] autorelease];
2185 NSToolbarItem *toolbarItem;
2187 [toolbar setDelegate:self];
2188 [toolbar setDisplayMode:NSToolbarDisplayModeIconAndLabel];
2189 [toolbar setSizeMode:NSToolbarSizeModeRegular];
2190 [toolbar setVisible:YES];
2191 [toolbar setAllowsUserCustomization:YES];
2192 [toolbar setAutosavesConfiguration:YES];
2193 toolbarItems = [[NSMutableDictionary alloc] init];
2196 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2197 withIdentifier:@"delete"
2200 toolTip:AILocalizedString(@"Delete the selection",nil)
2202 settingSelector:@selector(setImage:)
2203 itemContent:[NSImage imageNamed:@"remove" forClass:[self class]]
2204 action:@selector(deleteSelection:)
2208 [self window]; //Ensure the window is loaded, since we're pulling the search view from our nib
2209 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"search"
2212 toolTip:AILocalizedString(@"Search or filter logs",nil)
2214 settingSelector:@selector(setView:)
2215 itemContent:view_SearchField
2216 action:@selector(updateSearch:)
2218 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
2219 [toolbarItem setVisibilityPriority:(NSToolbarItemVisibilityPriorityHigh + 1)];
2221 [toolbarItem setMinSize:NSMakeSize(130, NSHeight([view_SearchField frame]))];
2222 [toolbarItem setMaxSize:NSMakeSize(230, NSHeight([view_SearchField frame]))];
2223 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
2225 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:DATE_ITEM_IDENTIFIER
2226 label:AILocalizedString(@"Date", nil)
2227 paletteLabel:AILocalizedString(@"Date", nil)
2228 toolTip:AILocalizedString(@"Filter logs by date",nil)
2230 settingSelector:@selector(setView:)
2231 itemContent:view_DatePicker
2234 if ([toolbarItem respondsToSelector:@selector(setVisibilityPriority:)]) {
2235 [toolbarItem setVisibilityPriority:NSToolbarItemVisibilityPriorityHigh];
2237 [toolbarItem setMinSize:[view_DatePicker frame].size];
2238 [toolbarItem setMaxSize:[view_DatePicker frame].size];
2239 [toolbarItems setObject:toolbarItem forKey:[toolbarItem itemIdentifier]];
2242 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2243 withIdentifier:@"toggleemoticons"
2244 label:(showEmoticons ? HIDE_EMOTICONS : SHOW_EMOTICONS)
2245 paletteLabel:AILocalizedString(@"Show/Hide Emoticons",nil)
2246 toolTip:AILocalizedString(@"Show or hide emoticons in logs",nil)
2248 settingSelector:@selector(setImage:)
2249 itemContent:[NSImage imageNamed:(showEmoticons ? IMAGE_EMOTICONS_ON : IMAGE_EMOTICONS_OFF) forClass:[self class]]
2250 action:@selector(toggleEmoticonFiltering:)
2252 // Toggle Timestamps
2253 [AIToolbarUtilities addToolbarItemToDictionary:toolbarItems
2254 withIdentifier:@"toggletimestamps"
2255 label:(showTimestamps ? HIDE_TIMESTAMPS : SHOW_TIMESTAMPS)
2256 paletteLabel:AILocalizedString(@"Show/Hide Timestamps", nil)
2257 toolTip:AILocalizedString(@"Show or hide timestamps in logs", nil)
2259 settingSelector:@selector(setImage:)
2260 itemContent:[NSImage imageNamed:(showTimestamps ? IMAGE_TIMESTAMPS_ON : IMAGE_TIMESTAMPS_OFF) forClass:[self class]]
2261 action:@selector(toggleTimestampFiltering:)
2264 [[self window] setToolbar:toolbar];
2266 [self configureDateFilter];
2269 - (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
2271 return [AIToolbarUtilities toolbarItemFromDictionary:toolbarItems withIdentifier:itemIdentifier];
2274 - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
2276 return [NSArray arrayWithObjects:DATE_ITEM_IDENTIFIER, NSToolbarFlexibleSpaceItemIdentifier,
2277 @"delete", @"toggleemoticons", @"toggletimestamps", NSToolbarPrintItemIdentifier, NSToolbarFlexibleSpaceItemIdentifier,
2281 - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
2283 return [[toolbarItems allKeys] arrayByAddingObjectsFromArray:
2284 [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
2285 NSToolbarSpaceItemIdentifier,
2286 NSToolbarFlexibleSpaceItemIdentifier,
2287 NSToolbarCustomizeToolbarItemIdentifier,
2288 NSToolbarPrintItemIdentifier, nil]];
2291 - (void)toolbarWillAddItem:(NSNotification *)notification
2293 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
2294 if ([[item itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2295 [item setTarget:self];
2296 [item setAction:@selector(adiumPrint:)];
2300 #pragma mark Date filter
2303 * @brief Returns a menu item for the date type filter menu
2305 - (NSMenuItem *)_menuItemForDateType:(AIDateType)dateType dict:(NSDictionary *)dateTypeTitleDict
2307 NSMenuItem *menuItem = [[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:[dateTypeTitleDict objectForKey:[NSNumber numberWithInteger:dateType]]
2308 action:@selector(selectDateType:)
2310 [menuItem setTag:dateType];
2312 return [menuItem autorelease];
2315 - (NSInteger)daysSinceStartOfWeekGivenToday:(NSCalendarDate *)today
2317 NSInteger todayDayOfWeek = [today dayOfWeek];
2319 //Try to look at the iCal preferences if possible
2320 if (!iCalFirstDayOfWeekDetermined) {
2321 CFPropertyListRef iCalFirstDayOfWeek = CFPreferencesCopyAppValue(CFSTR("first day of week"),CFSTR("com.apple.iCal"));
2322 if (iCalFirstDayOfWeek) {
2323 //This should return a CFNumberRef... we're using another app's prefs, so make sure.
2324 if (CFGetTypeID(iCalFirstDayOfWeek) == CFNumberGetTypeID()) {
2325 firstDayOfWeek = [(NSNumber *)iCalFirstDayOfWeek integerValue];
2328 CFRelease(iCalFirstDayOfWeek);
2332 iCalFirstDayOfWeekDetermined = YES;
2335 return ((todayDayOfWeek >= firstDayOfWeek) ? (todayDayOfWeek - firstDayOfWeek) : ((todayDayOfWeek + 7) - firstDayOfWeek));
2339 * @brief Select the date type
2341 - (void)selectDateType:(id)sender
2343 [self selectedDateType:[sender tag]];
2344 [self startSearchingClearingCurrentResults:YES];
2347 #pragma mark Open Log
2349 - (void)openLogAtPath:(NSString *)inPath
2351 AIChatLog *chatLog = nil;
2352 NSString *basePath = [AILoggerPlugin logBasePath];
2354 //inPath should be in a folder of the form SERVICE.ACCOUNT_NAME/CONTACT_NAME/log.extension
2355 NSArray *pathComponents = [inPath pathComponents];
2356 NSInteger lastIndex = [pathComponents count];
2357 NSString *logName = [pathComponents objectAtIndex:--lastIndex];
2358 NSString *contactName = [pathComponents objectAtIndex:--lastIndex];
2359 NSString *serviceAndAccountName = [pathComponents objectAtIndex:--lastIndex];
2360 NSString *relativeToGroupPath = [serviceAndAccountName stringByAppendingPathComponent:contactName];
2362 NSString *serviceID = [[serviceAndAccountName componentsSeparatedByString:@"."] objectAtIndex:0];
2363 //Filter for logs from the contact associated with the log we're loading
2364 [self filterForContact:[adium.contactController contactWithService:[adium.accountController firstServiceWithServiceID:serviceID]
2368 NSString *canonicalBasePath = [basePath stringByStandardizingPath];
2369 NSString *canonicalInPath = [inPath stringByStandardizingPath];
2371 if ([canonicalInPath hasPrefix:[canonicalBasePath stringByAppendingString:@"/"]]) {
2372 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[serviceAndAccountName stringByAppendingPathComponent:contactName]];
2374 chatLog = [logToGroup logAtPath:[relativeToGroupPath stringByAppendingPathComponent:logName]];
2377 /* Different Adium user... this sucks. We're given a path like this:
2378 * /Users/evands/Application Support/Adium 2.0/Users/OtherUser/Logs/AIM.Tekjew/HotChick001/HotChick001 (3-30-2005).AdiumLog
2379 * and we want to make it relative to our current user's logs folder, which might be
2380 * /Users/evands/Application Support/Adium 2.0/Users/Default/Logs
2382 * To achieve this, add a "/.." for each directory in our current user's logs folder, then add the full path to the log.
2384 NSString *fakeRelativePath = @"";
2386 //Use .. to get back to the root from the base path
2387 NSInteger componentsOfBasePath = [[canonicalBasePath pathComponents] count];
2388 for (NSInteger i = 0; i < componentsOfBasePath; i++) {
2389 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:@".."];
2392 //Now add the path from the root to the actual log
2393 fakeRelativePath = [fakeRelativePath stringByAppendingPathComponent:canonicalInPath];
2394 chatLog = [[[AIChatLog alloc] initWithPath:fakeRelativePath
2395 from:[serviceAndAccountName substringFromIndex:([serviceID length] + 1)] //One off for the '.'
2397 serviceClass:serviceID] autorelease];
2400 //Now display the requested log
2402 [self displayLog:chatLog];
2406 #pragma mark Printing
2408 - (void)adiumPrint:(id)sender
2410 NSTextView *printView;
2411 NSPrintOperation *printOperation;
2412 NSPrintInfo *printInfo = [NSPrintInfo sharedPrintInfo];
2414 [printInfo setHorizontalPagination:NSFitPagination];
2415 [printInfo setHorizontallyCentered:NO];
2416 [printInfo setVerticallyCentered:NO];
2418 printView = [[NSTextView alloc] initWithFrame:[[NSPrintInfo sharedPrintInfo] imageablePageBounds]];
2419 [printView setVerticallyResizable:YES];
2420 [printView setHorizontallyResizable:NO];
2422 [[printView textStorage] setAttributedString:[textView_content textStorage]];
2424 printOperation = [NSPrintOperation printOperationWithView:printView printInfo:printInfo];
2425 [printOperation runOperationModalForWindow:[self window] delegate:nil
2426 didRunSelector:NULL contextInfo:NULL];
2427 [printView release];
2430 - (BOOL)validatePrintMenuItem:(NSMenuItem *)menuItem
2432 return ([displayedLogArray count] > 0);
2435 - (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
2437 if ([[theItem itemIdentifier] isEqualToString:NSToolbarPrintItemIdentifier]) {
2438 return [self validatePrintMenuItem:nil];
2445 - (void)selectCachedIndex
2447 NSInteger numberOfRows = [tableView_results numberOfRows];
2449 if (cachedSelectionIndex < numberOfRows) {
2450 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:cachedSelectionIndex]
2451 byExtendingSelection:NO];
2454 [tableView_results selectRowIndexes:[NSIndexSet indexSetWithIndex:(numberOfRows-1)]
2455 byExtendingSelection:NO];
2459 [tableView_results scrollRowToVisible:[[tableView_results selectedRowIndexes] firstIndex]];
2462 deleteOccurred = NO;
2465 #pragma mark Deletion
2468 * @brief Get an NSAlert to request deletion of multiple logs
2470 - (NSAlert *)alertForDeletionOfLogCount:(NSUInteger)logCount
2472 NSAlert *alert = [[NSAlert alloc] init];
2473 [alert setMessageText:AILocalizedString(@"Delete Logs?",nil)];
2474 [alert setInformativeText:[NSString stringWithFormat:
2475 AILocalizedString(@"Are you sure you want to send %lu logs to the Trash?",nil), logCount]];
2476 [alert addButtonWithTitle:DELETE];
2477 [alert addButtonWithTitle:AILocalizedString(@"Cancel",nil)];
2479 return [alert autorelease];
2483 * @brief Undo the deletion of one or more AIChatLogs
2485 * The logs will be marked for readdition to the index
2487 - (void)restoreDeletedLogs:(NSArray *)deletedLogs
2490 NSFileManager *fileManager = [NSFileManager defaultManager];
2491 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2493 for (aLog in deletedLogs) {
2494 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
2496 [fileManager createDirectoryAtPath:[logPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL];
2498 [fileManager moveItemAtPath:[trashPath stringByAppendingPathComponent:[logPath lastPathComponent]]
2502 [plugin markLogDirtyAtPath:logPath];
2505 [self rebuildIndices];
2508 - (void)deleteLogsAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
2510 NSArray *selectedLogs = (NSArray *)contextInfo;
2511 if (returnCode == NSAlertFirstButtonReturn) {
2515 NSMutableSet *logPaths = [NSMutableSet set];
2517 cachedSelectionIndex = [[tableView_results selectedRowIndexes] firstIndex];
2519 for (aLog in selectedLogs) {
2520 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
2522 [[NSNotificationCenter defaultCenter] postNotificationName:ChatLog_WillDelete object:aLog userInfo:nil];
2523 AILogToGroup *logToGroup = [logToGroupDict objectForKey:[[aLog relativePath] stringByDeletingLastPathComponent]];
2525 // Success will be unused in deployment builds as AILog turns to nothing
2527 BOOL success = [logToGroup trashLog:aLog];
2528 AILog(@"Trashing %@: %i",[aLog relativePath], success);
2530 [logToGroup trashLog:aLog];
2532 //Clear the to group out if it no longer has anything of interest
2533 if ([logToGroup logCount] == 0) {
2534 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[[[aLog relativePath] stringByDeletingLastPathComponent] stringByDeletingLastPathComponent]];
2535 [logFromGroup removeToGroup:logToGroup];
2538 [logPaths addObject:logPath];
2539 [currentSearchResults removeObjectIdenticalTo:aLog];
2542 [plugin removePathsFromIndex:logPaths];
2544 [undoManager registerUndoWithTarget:self
2545 selector:@selector(restoreDeletedLogs:)
2546 object:selectedLogs];
2547 [undoManager setActionName:DELETE];
2549 [resultsLock unlock];
2550 [tableView_results reloadData];
2552 deleteOccurred = YES;
2554 [self rebuildContactsList];
2555 [self updateProgressDisplay];
2557 [selectedLogs release];
2561 * @brief Delete logs
2563 * If two or more logs are passed, confirmation will be requested.
2564 * This operation registers with the window controller's undo manager.
2566 * @param selectedLogs An NSArray of logs to delete
2568 - (void)deleteLogs:(NSArray *)selectedLogs
2570 if ([selectedLogs count] > 1) {
2571 NSAlert *alert = [self alertForDeletionOfLogCount:[selectedLogs count]];
2572 [alert beginSheetModalForWindow:[self window]
2574 didEndSelector:@selector(deleteLogsAlertDidEnd:returnCode:contextInfo:)
2575 contextInfo:[selectedLogs retain]];
2576 } else if ([selectedLogs count] == 1) {
2577 [self deleteLogsAlertDidEnd:nil
2578 returnCode:NSAlertFirstButtonReturn
2579 contextInfo:[selectedLogs retain]];
2584 * @brief Returns a set of all selected to groups on all accounts
2586 * @param totalLogCount If non-NULL, will be set to the total number of logs on return
2588 - (NSArray *)allSelectedToGroups:(NSInteger *)totalLogCount
2590 NSEnumerator *fromEnumerator;
2591 AILogFromGroup *fromGroup;
2592 NSMutableArray *allToGroups = [NSMutableArray array];
2594 if (totalLogCount) *totalLogCount = 0;
2596 //Walk through every 'from' group
2597 fromEnumerator = [logFromGroupDict objectEnumerator];
2598 while ((fromGroup = [fromEnumerator nextObject])) {
2599 NSEnumerator *toEnumerator;
2600 AILogToGroup *toGroup;
2602 //Walk through every 'to' group
2603 toEnumerator = [[fromGroup toGroupArray] objectEnumerator];
2604 while ((toGroup = [toEnumerator nextObject])) {
2605 if (![contactIDsToFilter count] || [contactIDsToFilter containsObject:[[NSString stringWithFormat:@"%@.%@",[toGroup serviceClass],[toGroup to]] compactedString]]) {
2606 if (totalLogCount) {
2607 *totalLogCount += [toGroup logCount];
2610 [allToGroups addObject:toGroup];
2619 * @brief Undo the deletion of one or more AILogToGroups and their associated logs
2621 * The logs will be marked for readdition to the index
2623 - (void)restoreDeletedToGroups:(NSArray *)toGroups
2625 AILogToGroup *toGroup;
2626 NSFileManager *fileManager = [NSFileManager defaultManager];
2627 NSString *trashPath = [fileManager findFolderOfType:kTrashFolderType inDomain:kUserDomain createFolder:NO];
2628 NSString *logBasePath = [AILoggerPlugin logBasePath];
2630 for (toGroup in toGroups) {
2631 NSString *toGroupPath = [logBasePath stringByAppendingPathComponent:[toGroup relativePath]];
2633 [fileManager createDirectoryAtPath:[toGroupPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:NULL];
2634 if ([fileManager fileExistsAtPath:toGroupPath]) {
2635 AILog(@"Removing path %@ to make way for %@",
2636 toGroupPath,[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]);
2637 [fileManager removeItemAtPath:toGroupPath
2640 [fileManager moveItemAtPath:[trashPath stringByAppendingPathComponent:[toGroupPath lastPathComponent]]
2644 NSEnumerator *logEnumerator = [toGroup logEnumerator];
2647 while ((aLog = [logEnumerator nextObject])) {
2648 [plugin markLogDirtyAtPath:[logBasePath stringByAppendingPathComponent:[aLog relativePath]]];
2652 [self rebuildIndices];
2655 - (void)deleteSelectedContactsFromSourceListAlertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
2657 NSArray *allSelectedToGroups = (NSArray *)contextInfo;
2658 if (returnCode == NSAlertFirstButtonReturn) {
2659 AILogToGroup *logToGroup;
2660 NSMutableSet *logPaths = [NSMutableSet set];
2662 for (logToGroup in allSelectedToGroups) {
2663 NSEnumerator *logEnumerator;
2666 logEnumerator = [logToGroup logEnumerator];
2667 while ((aLog = [logEnumerator nextObject])) {
2668 NSString *logPath = [[AILoggerPlugin logBasePath] stringByAppendingPathComponent:[aLog relativePath]];
2669 [logPaths addObject:logPath];
2672 AILogFromGroup *logFromGroup = [logFromGroupDict objectForKey:[NSString stringWithFormat:@"%@.%@",[logToGroup serviceClass],[logToGroup from]]];
2673 [logFromGroup removeToGroup:logToGroup];
2676 [plugin removePathsFromIndex:logPaths];
2678 [undoManager registerUndoWithTarget:self
2679 selector:@selector(restoreDeletedToGroups:)
2680 object:allSelectedToGroups];
2681 [undoManager setActionName:DELETE];
2683 [self rebuildIndices];
2684 [self updateProgressDisplay];
2687 [allSelectedToGroups release];
2691 * @brief Delete entirely the logs of all contacts selected in the source list
2693 * Confirmation by the user will be required.
2695 * Note: A single item in the source list may have multiple associated AILogToGroups.
2697 - (void)deleteSelectedContactsFromSourceList
2699 NSInteger totalLogCount;
2700 NSArray *allSelectedToGroups = [self allSelectedToGroups:&totalLogCount];
2702 if (totalLogCount > 1) {
2703 NSAlert *alert = [self alertForDeletionOfLogCount:totalLogCount];
2704 [alert beginSheetModalForWindow:[self window]
2706 didEndSelector:@selector(deleteSelectedContactsFromSourceListAlertDidEnd:returnCode:contextInfo:)
2707 contextInfo:[allSelectedToGroups retain]];
2709 [self deleteSelectedContactsFromSourceListAlertDidEnd:nil
2710 returnCode:NSAlertFirstButtonReturn
2711 contextInfo:[allSelectedToGroups retain]];
2716 * @brief Delete the current selection
2718 * If the contacts outline view is selected, one or more contacts' logs will be trashed.
2719 * If anything else is selected, the currently selected search result logs will be trashed.
2721 - (void)deleteSelection:(id)sender
2723 if ([[self window] firstResponder] == outlineView_contacts) {
2724 [self deleteSelectedContactsFromSourceList];
2728 NSArray *selectedLogs = [tableView_results selectedItemsFromArray:currentSearchResults];
2729 [resultsLock unlock];
2731 [self deleteLogs:selectedLogs];
2737 * @brief Supply our undo manager when we are within the responder chain
2739 - (NSUndoManager *)windowWillReturnUndoManager:(NSWindow *)sender
2744 #pragma mark Gestures
2746 * @brief Responds to a swipe gesture
2748 * This is a private method added in AppKit 949.18.0.
2750 - (void)swipeWithEvent:(NSEvent *)inEvent
2752 NSTableView *targetTableView;
2753 NSInteger changeValue, nextSelected;
2755 if ([inEvent deltaY] == 0) {
2756 // For horizontal swipes, switch between individual logs.
2757 targetTableView = tableView_results;
2758 changeValue = [inEvent deltaX];
2759 // Lock the results when we're dealing with the logs tableView
2762 // For vertical swipes, switch between contacts.
2763 targetTableView = outlineView_contacts;
2764 changeValue = [inEvent deltaY];
2767 // Swipe; +1f is left/up, -1f is right/down
2769 // Find the index of the next row to select.
2770 if (changeValue == -1) {
2771 // Going to the right.
2772 nextSelected = [[targetTableView selectedRowIndexes] lastIndex] + 1;
2774 // Going to the left.
2775 nextSelected = [[targetTableView selectedRowIndexes] firstIndex] - 1;
2778 // Loop around in circles.
2779 if (nextSelected >= [targetTableView numberOfRows]) {
2781 } else if (nextSelected < 0) {
2782 nextSelected = [targetTableView numberOfRows]-1;
2785 // Select either the next row or the previous row.
2786 [targetTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:nextSelected]
2787 byExtendingSelection:NO];
2789 [targetTableView scrollRowToVisible:nextSelected];
2791 if ([inEvent deltaY] == 0)
2792 [resultsLock unlock];
2795 #pragma mark Transcript services special-casing
2796 NSString *handleSpecialCasesForUIDAndServiceClass(NSString *contactUID, NSString *serviceClass)
2798 /* Jabber and its specified derivative services need special handling;
2799 * this is cross-contamination from ESPurpleJabberAccount.
2801 if ([serviceClass isEqualToString:@"Jabber"] ||
2802 [serviceClass isEqualToString:@"GTalk"] ||
2803 [serviceClass isEqualToString:@"LiveJournal"]) {
2805 if ([contactUID hasSuffix:@"@gmail.com"] ||
2806 [contactUID hasSuffix:@"@googlemail.com"]) {
2807 serviceClass = @"GTalk";
2809 } else if ([contactUID hasSuffix:@"@livejournal.com"]){
2810 serviceClass = @"LiveJournal";
2813 serviceClass = @"Jabber";
2816 /* OSCAR and its specified derivative services need special handling;
2817 * this is cross-contamination from CBPurpleOscarAccount.
2819 } else if ([serviceClass isEqualToString:@"AIM"] ||
2820 [serviceClass isEqualToString:@"ICQ"] ||
2821 [serviceClass isEqualToString:@"Mac"] ||
2822 [serviceClass isEqualToString:@"MobileMe"]) {
2823 const char firstCharacter = ([contactUID length] ? [contactUID characterAtIndex:0] : '\0');
2825 //Determine service based on UID
2826 if ([contactUID hasSuffix:@"@mac.com"]) {
2827 serviceClass = @"Mac";
2828 } else if ([contactUID hasSuffix:@"@me.com"]) {
2829 serviceClass = @"MobileMe";
2830 } else if (firstCharacter && (firstCharacter >= '0' && firstCharacter <= '9')) {
2831 serviceClass = @"ICQ";
2833 serviceClass = @"AIM";
2837 return serviceClass;
2840 #pragma mark Date type menu
2842 - (void)configureDateFilter
2844 firstDayOfWeek = 0; /* Sunday */
2845 iCalFirstDayOfWeekDetermined = NO;
2847 [popUp_dateFilter setMenu:[self dateTypeMenu]];
2848 NSInteger index = [popUp_dateFilter indexOfItemWithTag:AIDateTypeAnyDate];
2849 if(index != NSNotFound)
2850 [popUp_dateFilter selectItemAtIndex:index];
2851 [self selectedDateType:AIDateTypeAnyDate];
2853 [datePicker setDateValue:[NSDate date]];
2856 - (IBAction)selectDate:(id)sender
2858 [filterDate release];
2859 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2861 [self startSearchingClearingCurrentResults:YES];
2864 - (NSMenu *)dateTypeMenu
2866 NSDictionary *dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2867 AILocalizedString(@"Any Date", nil), [NSNumber numberWithInteger:AIDateTypeAnyDate],
2868 AILocalizedString(@"Today", nil), [NSNumber numberWithInteger:AIDateTypeToday],
2869 AILocalizedString(@"Since Yesterday", nil), [NSNumber numberWithInteger:AIDateTypeSinceYesterday],
2870 AILocalizedString(@"This Week", nil), [NSNumber numberWithInteger:AIDateTypeThisWeek],
2871 AILocalizedString(@"Within Last 2 Weeks", nil), [NSNumber numberWithInteger:AIDateTypeWithinLastTwoWeeks],
2872 AILocalizedString(@"This Month", nil), [NSNumber numberWithInteger:AIDateTypeThisMonth],
2873 AILocalizedString(@"Within Last 2 Months", nil), [NSNumber numberWithInteger:AIDateTypeWithinLastTwoMonths],
2875 NSMenu *dateTypeMenu = [[NSMenu alloc] init];
2876 AIDateType dateType;
2878 [dateTypeMenu addItem:[self _menuItemForDateType:AIDateTypeAnyDate dict:dateTypeTitleDict]];
2879 [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2881 for (dateType = AIDateTypeToday; dateType < AIDateTypeExactly; dateType++) {
2882 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2885 dateTypeTitleDict = [NSDictionary dictionaryWithObjectsAndKeys:
2886 AILocalizedString(@"Exactly", nil), [NSNumber numberWithInteger:AIDateTypeExactly],
2887 AILocalizedString(@"Before", nil), [NSNumber numberWithInteger:AIDateTypeBefore],
2888 AILocalizedString(@"After", nil), [NSNumber numberWithInteger:AIDateTypeAfter],
2891 [dateTypeMenu addItem:[NSMenuItem separatorItem]];
2893 for (dateType = AIDateTypeExactly; dateType <= AIDateTypeAfter; dateType++) {
2894 [dateTypeMenu addItem:[self _menuItemForDateType:dateType dict:dateTypeTitleDict]];
2897 return [dateTypeMenu autorelease];
2901 * @brief A new date type was selected
2903 * The date picker will be hidden/revealed as appropriate.
2904 * This does not start a search
2906 - (void)selectedDateType:(AIDateType)dateType
2908 BOOL showDatePicker = NO;
2910 NSCalendarDate *today = [NSCalendarDate date];
2912 [filterDate release]; filterDate = nil;
2915 case AIDateTypeAnyDate:
2916 filterDateType = AIDateTypeAnyDate;
2919 case AIDateTypeToday:
2920 filterDateType = AIDateTypeExactly;
2921 filterDate = [today retain];
2924 case AIDateTypeSinceYesterday:
2925 filterDateType = AIDateTypeAfter;
2926 filterDate = [[today dateByAddingYears:0
2929 hours:-[today hourOfDay]
2930 minutes:-[today minuteOfHour]
2931 seconds:-([today secondOfMinute] + 1)] retain];
2934 case AIDateTypeThisWeek:
2935 filterDateType = AIDateTypeAfter;
2936 filterDate = [[today dateByAddingYears:0
2938 days:-[self daysSinceStartOfWeekGivenToday:today]
2939 hours:-[today hourOfDay]
2940 minutes:-[today minuteOfHour]
2941 seconds:-([today secondOfMinute] + 1)] retain];
2944 case AIDateTypeWithinLastTwoWeeks:
2945 filterDateType = AIDateTypeAfter;
2946 filterDate = [[today dateByAddingYears:0
2949 hours:-[today hourOfDay]
2950 minutes:-[today minuteOfHour]
2951 seconds:-([today secondOfMinute] + 1)] retain];
2954 case AIDateTypeThisMonth:
2955 filterDateType = AIDateTypeAfter;
2956 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2958 days:-[today dayOfMonth]
2961 seconds:-1] retain];
2964 case AIDateTypeWithinLastTwoMonths:
2965 filterDateType = AIDateTypeAfter;
2966 filterDate = [[[NSCalendarDate date] dateByAddingYears:0
2968 days:-[today dayOfMonth]
2971 seconds:-1] retain];
2979 case AIDateTypeExactly:
2980 filterDateType = AIDateTypeExactly;
2981 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2982 showDatePicker = YES;
2985 case AIDateTypeBefore:
2986 filterDateType = AIDateTypeBefore;
2987 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2988 showDatePicker = YES;
2991 case AIDateTypeAfter:
2992 filterDateType = AIDateTypeAfter;
2993 filterDate = [[[datePicker dateValue] dateWithCalendarFormat:nil timeZone:nil] retain];
2994 showDatePicker = YES;
2998 showDatePicker = NO;
3002 BOOL updateSize = NO;
3003 if (showDatePicker && [datePicker isHidden]) {
3004 [datePicker setHidden:NO];
3007 } else if (!showDatePicker && ![datePicker isHidden]) {
3008 [datePicker setHidden:YES];
3013 NSEnumerator *enumerator = [[[[self window] toolbar] items] objectEnumerator];
3014 NSToolbarItem *toolbarItem;
3015 while ((toolbarItem = [enumerator nextObject])) {
3016 if ([[toolbarItem itemIdentifier] isEqualToString:DATE_ITEM_IDENTIFIER]) {
3017 NSSize newSize = NSMakeSize(([datePicker isHidden] ? 180 : 290), NSHeight([view_DatePicker frame]));
3018 [toolbarItem setMinSize:newSize];
3019 [toolbarItem setMaxSize:newSize];
3026 - (NSString *)dateItemNibName
3028 return @"LogViewerDateFilter";