#import #import #import #import #import #import "MVBuddyListController.h" #import "JVBuddy.h" #import "JVChatController.h" #import "MVApplicationController.h" #import "MVConnectionsController.h" #import "MVFileTransferController.h" #import "JVNotificationController.h" #import "JVInspectorController.h" #import "MVTableView.h" #import "JVDetailCell.h" static MVBuddyListController *sharedInstance = nil; @class ABUIController; @class ABRoundedTextField; @class ABSplitView; @class ABTableView; @class ABScrollView; @interface ABPeoplePickerController : NSObject { @public NSView *_peoplePicker; ABUIController *_uiController; NSWindow *_window; ABSplitView *_mainSplit; NSTextField *_label; ABRoundedTextField *_searchField; } - (id) initWithWindow:(NSWindow *) window; - (NSView *) peoplePickerView; - (NSArray *) selectedGroups; - (NSArray *) selectedRecords; - (NSArray *) stringsFromSelectionExpanding:(BOOL) expand; - (void) addColumnFilter:(NSDictionary *) filter forColumnTitle:(NSString *) title; - (void) removeAllColumnFilters; - (void) removeColumnFilter:(NSDictionary *) filter; - (void) selectColumnTitle:(NSString *) title; - (void) setAllowSubrowSelection:(BOOL) allow; - (void) setAllowGroupSelection:(BOOL) allow; - (NSArray *) displayedColumns; - (void) editInAddressBook:(id) sender; - (void) setGroupDoubleClickTarget:(id) target andAction:(SEL) action; - (void) setPeopleDoubleClickTarget:(id) target andAction:(SEL) action; @end #pragma mark - @interface ABPerson (ABPersonPrivate) + (ABPerson *) personFromDictionary:(NSDictionary *) dictionary; - (NSDictionary *) dictionaryRepresentation; @end #pragma mark - @interface MVBuddyListController (MVBuddyListControllerPrivate) - (void) _saveBuddyList; - (void) _loadBuddyList; - (void) _sortBuddies; - (void) _manuallySortAndUpdate; - (void) _setBuddiesNeedSortAnimated; - (void) _sortBuddiesAnimatedIfNeeded:(id) sender; - (void) _addBuddyToList:(JVBuddy *) buddy; @end #pragma mark - @implementation MVBuddyListController + (MVBuddyListController *) sharedBuddyList { extern MVBuddyListController *sharedInstance; if( ! sharedInstance && [MVApplicationController isTerminating] ) return nil; return ( sharedInstance ? sharedInstance : ( sharedInstance = [[self alloc] initWithWindowNibName:nil] ) ); } #pragma mark - - (id) initWithWindowNibName:(NSString *) windowNibName { if( ( self = [super initWithWindowNibName:@"MVBuddyList"] ) ) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _buddyOnline: ) name:JVBuddyCameOnlineNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _buddyOffline: ) name:JVBuddyWentOfflineNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _buddyChanged: ) name:JVBuddyNicknameCameOnlineNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _buddyChanged: ) name:JVBuddyNicknameWentOfflineNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _buddyChanged: ) name:JVBuddyNicknameStatusChangedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector( _buddyChanged: ) name:JVBuddyActiveNicknameChangedNotification object:nil]; _sortTimer = [[NSTimer scheduledTimerWithTimeInterval:( .5 ) target:self selector:@selector( _sortBuddiesAnimatedIfNeeded: ) userInfo:nil repeats:YES] retain]; _onlineBuddies = [[NSMutableSet set] retain]; _buddyList = [[NSMutableSet set] retain]; _buddyOrder = [[NSMutableArray array] retain]; _picker = nil; [self _loadBuddyList]; [JVBuddy setPreferredName:(JVBuddyName)[[NSUserDefaults standardUserDefaults] integerForKey:@"JVChatBuddyNameStyle"]]; [self setShowIcons:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatBuddyListShowIcons"]]; [self setShowFullNames:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatBuddyListShowFullNames"]]; [self setShowNicknameAndServer:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatBuddyListShowNicknameAndServer"]]; [self setShowOfflineBuddies:[[NSUserDefaults standardUserDefaults] boolForKey:@"JVChatBuddyListShowOfflineBuddies"]]; [self setSortOrder:[[NSUserDefaults standardUserDefaults] integerForKey:@"JVChatBuddyListSortOrder"]]; } return self; } - (void) release { if( ( [self retainCount] - 1 ) == 1 ) [_sortTimer invalidate]; [super release]; } - (void) dealloc { extern MVBuddyListController *sharedInstance; [self _saveBuddyList]; [[NSNotificationCenter defaultCenter] removeObserver:self]; if( self == sharedInstance ) sharedInstance = nil; [_onlineBuddies release]; [_buddyList release]; [_buddyOrder release]; [_picker release]; [_oldPositions release]; [_sortTimer release]; [_addPerson release]; _onlineBuddies = nil; _buddyList = nil; _buddyOrder = nil; _picker = nil; _oldPositions = nil; _sortTimer = nil; _addPerson = nil; [super dealloc]; } - (void) windowDidLoad { NSTableColumn *theColumn = nil; [(NSPanel *)[self window] setFloatingPanel:NO]; [(NSPanel *)[self window] setHidesOnDeactivate:NO]; [[self window] setFrameAutosaveName:@"buddylist"]; [buddies setVerticalMotionCanBeginDrag:NO]; [buddies setTarget:self]; [buddies setDoubleAction:@selector( messageSelectedBuddy: )]; theColumn = [buddies tableColumnWithIdentifier:@"buddy"]; JVDetailCell *prototypeCell = [[JVDetailCell new] autorelease]; [prototypeCell setFont:[NSFont systemFontOfSize:11.]]; [theColumn setDataCell:prototypeCell]; _picker = [[[ABPeoplePickerController alloc] initWithWindow:pickerWindow] retain]; [_picker setAllowSubrowSelection:NO]; [_picker setAllowGroupSelection:NO]; [_picker addColumnFilter:[NSDictionary dictionaryWithObject:@"" forKey:@"IRCNickname"] forColumnTitle:@"IRC Nickname"]; [_picker setPeopleDoubleClickTarget:self andAction:@selector( confirmBuddySelection: )]; [[_picker peoplePickerView] setFrame:[pickerView frame]]; [[pickerWindow contentView] replaceSubview:pickerView with:[_picker peoplePickerView]]; [[_picker peoplePickerView] setAutoresizingMask:(NSViewWidthSizable | NSViewHeightSizable)]; NSTableView *table = [[[[[[[[(NSView *)_picker -> _mainSplit subviews] objectAtIndex:0] subviews] objectAtIndex:0] subviews] objectAtIndex:0] subviews] objectAtIndex:0]; [table setAllowsMultipleSelection:NO]; [table setAllowsEmptySelection:NO]; table = [[[[[[[[(NSView *)_picker -> _mainSplit subviews] objectAtIndex:1] subviews] objectAtIndex:0] subviews] objectAtIndex:0] subviews] objectAtIndex:0]; [table setAllowsMultipleSelection:NO]; [table setAllowsEmptySelection:NO]; [buddies registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]]; [buddies reloadData]; [self _setBuddiesNeedSortAnimated]; } #pragma mark - - (id ) objectToInspect { if( [buddies selectedRow] == -1 ) return nil; id item = [_buddyOrder objectAtIndex:[buddies selectedRow]]; if( [item conformsToProtocol:@protocol( JVInspection )] ) return item; else return nil; } - (IBAction) getInfo:(id) sender { if( [buddies selectedRow] == -1 ) return; id item = [_buddyOrder objectAtIndex:[buddies selectedRow]]; if( [item conformsToProtocol:@protocol( JVInspection )] ) [[JVInspectorController inspectorOfObject:item] show:sender]; } #pragma mark - - (IBAction) showBuddyList:(id) sender { [[self window] makeKeyAndOrderFront:nil]; } - (IBAction) hideBuddyList:(id) sender { [[self window] orderOut:nil]; } #pragma mark - - (JVBuddy *) buddyForNickname:(NSString *) name onServer:(NSString *) address { NSEnumerator *enumerator = [_buddyList objectEnumerator]; JVBuddy *buddy = nil; NSURL *nick = nil; while( ( buddy = [enumerator nextObject] ) ) { nick = [NSURL URLWithString:[NSString stringWithFormat:@"irc://%@@%@", [name stringByEncodingIllegalURLCharacters], [address stringByEncodingIllegalURLCharacters]]]; if( [[buddy nicknames] containsObject:nick] ) return buddy; } return nil; } - (NSArray *) buddies { return [[_buddyOrder retain] autorelease]; } - (NSArray *) onlineBuddies { return [_onlineBuddies allObjects]; } #pragma mark - - (IBAction) showBuddyPickerSheet:(id) sender { [self showBuddyList:nil]; if( [[self window] attachedSheet] ) { [[NSApplication sharedApplication] endSheet:[[self window] attachedSheet]]; [[[self window] attachedSheet] orderOut:nil]; } [_addPerson autorelease]; _addPerson = nil; NSEnumerator *enumerator = [[[MVConnectionsController defaultManager] connections] objectEnumerator]; MVChatConnection *connection = nil; NSMenu *menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; NSMenuItem *item = nil; while( ( connection = [enumerator nextObject] ) ) { item = [[[NSMenuItem alloc] initWithTitle:[connection server] action:NULL keyEquivalent:@""] autorelease]; [menu addItem:item]; } [server setMenu:menu]; [firstName setObjectValue:@""]; [lastName setObjectValue:@""]; [email setObjectValue:@""]; [image setImage:nil]; [[NSApplication sharedApplication] beginSheet:pickerWindow modalForWindow:[self window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL]; } - (IBAction) cancelBuddySelection:(id) sender { if( [[self window] attachedSheet] ) { [[NSApplication sharedApplication] endSheet:[[self window] attachedSheet]]; [[[self window] attachedSheet] orderOut:nil]; } } - (IBAction) confirmBuddySelection:(id) sender { if( [[self window] attachedSheet] ) { [[NSApplication sharedApplication] endSheet:[[self window] attachedSheet]]; [[[self window] attachedSheet] orderOut:nil]; } ABPerson *person = [[_picker selectedRecords] lastObject]; if( ! [(NSDictionary *)[person valueForProperty:@"IRCNickname"] count] ) { [_addPerson autorelease]; _addPerson = [[person uniqueId] copy]; [self showNewPersonSheet:nil]; } else { JVBuddy *buddy = [JVBuddy buddyWithPerson:person]; [self _addBuddyToList:buddy]; [self _saveBuddyList]; } } #pragma mark - - (IBAction) showNewPersonSheet:(id) sender { if( [[self window] attachedSheet] ) { [[NSApplication sharedApplication] endSheet:[[self window] attachedSheet]]; [[[self window] attachedSheet] orderOut:nil]; } ABPerson *person = nil; if( _addPerson ) person = (ABPerson *)[[ABAddressBook sharedAddressBook] recordForUniqueId:_addPerson]; if( person ) { [firstName setObjectValue:[person valueForProperty:kABFirstNameProperty]]; [lastName setObjectValue:[person valueForProperty:kABLastNameProperty]]; ABMultiValue *value = [person valueForProperty:kABEmailProperty]; unsigned index = [value indexForIdentifier:[value primaryIdentifier]]; if( index != NSNotFound ) [email setObjectValue:[value valueAtIndex:index]]; [image setImage:[[[NSImage alloc] initWithData:[person imageData]] autorelease]]; } [[NSApplication sharedApplication] beginSheet:newPersonWindow modalForWindow:[self window] modalDelegate:nil didEndSelector:NULL contextInfo:NULL]; } - (IBAction) cancelNewBuddy:(id) sender { if( [[self window] attachedSheet] ) { [[NSApplication sharedApplication] endSheet:[[self window] attachedSheet]]; [[[self window] attachedSheet] orderOut:nil]; } [_addPerson autorelease]; _addPerson = nil; } - (IBAction) confirmNewBuddy:(id) sender { if( [[self window] attachedSheet] ) { [[NSApplication sharedApplication] endSheet:[[self window] attachedSheet]]; [[[self window] attachedSheet] orderOut:nil]; } [ABPerson addPropertiesAndTypes:[NSDictionary dictionaryWithObject:[NSNumber numberWithUnsignedInt:kABMultiStringProperty] forKey:@"IRCNickname"]]; ABPerson *person = nil; if( _addPerson ) person = (ABPerson *)[[ABAddressBook sharedAddressBook] recordForUniqueId:_addPerson]; if( ! person ) { NSMutableDictionary *info = [NSMutableDictionary dictionary]; NSMutableDictionary *sub = nil; if( [(NSString *)[firstName objectValue] length] || [(NSString *)[lastName objectValue] length] || [(NSString *)[email objectValue] length] ) { [info setObject:[firstName objectValue] forKey:kABFirstNameProperty]; } else [info setObject:[nickname objectValue] forKey:kABFirstNameProperty]; if( [(NSString *)[lastName objectValue] length] ) [info setObject:[lastName objectValue] forKey:kABLastNameProperty]; if( [(NSString *)[email objectValue] length] ) { sub = [NSMutableDictionary dictionary]; [sub setObject:[NSArray arrayWithObject:kABOtherLabel] forKey:@"labels"]; [sub setObject:[NSArray arrayWithObject:[email objectValue]] forKey:@"values"]; [info setObject:sub forKey:kABEmailProperty]; } sub = [NSMutableDictionary dictionary]; [sub setObject:[NSArray arrayWithObject:[server titleOfSelectedItem]] forKey:@"labels"]; [sub setObject:[NSArray arrayWithObject:[nickname objectValue]] forKey:@"values"]; [info setObject:sub forKey:@"IRCNickname"]; [info setObject:[NSString stringWithFormat:NSLocalizedString( @"IRC Nickname: %@ (%@)", "new buddy card note" ), [nickname objectValue], [server titleOfSelectedItem]] forKey:kABNoteProperty]; person = [ABPerson personFromDictionary:info]; [person setImageData:[[image image] TIFFRepresentation]]; [[ABAddressBook sharedAddressBook] addRecord:person]; [[ABAddressBook sharedAddressBook] save]; } else { ABMutableMultiValue *value = [[[ABMutableMultiValue alloc] init] autorelease]; [value addValue:[nickname objectValue] withLabel:[server titleOfSelectedItem]]; [person setValue:value forProperty:@"IRCNickname"]; if( [(NSString *)[firstName objectValue] length] || [(NSString *)[lastName objectValue] length] || [(NSString *)[email objectValue] length] ) { [person setValue:[firstName objectValue] forProperty:kABFirstNameProperty]; } else [person setValue:[nickname objectValue] forProperty:kABFirstNameProperty]; [person setValue:[lastName objectValue] forProperty:kABLastNameProperty]; ABMutableMultiValue *emailValue = [[[person valueForProperty:kABEmailProperty] mutableCopy] autorelease]; unsigned index = [emailValue indexForIdentifier:[emailValue primaryIdentifier]]; if( emailValue && index != NSNotFound ) { [emailValue replaceValueAtIndex:index withValue:[email objectValue]]; } else if( [(NSString *)[email objectValue] length] ) { emailValue = [[[ABMutableMultiValue alloc] init] autorelease]; [emailValue addValue:[email objectValue] withLabel:kABOtherLabel]; } if( [emailValue count] ) [person setValue:emailValue forProperty:kABEmailProperty]; [person setImageData:[[image image] TIFFRepresentation]]; [[ABAddressBook sharedAddressBook] save]; } if( person ) { JVBuddy *buddy = [JVBuddy buddyWithPerson:person]; [self _addBuddyToList:buddy]; [self _saveBuddyList]; } [_addPerson autorelease]; _addPerson = nil; } - (void) controlTextDidChange:(NSNotification *) notification { if( [(NSString *)[nickname objectValue] length] >= 1 ) [addButton setEnabled:YES]; else [addButton setEnabled:NO]; } #pragma mark - - (void) setNewBuddyNickname:(NSString *) nick { [nickname setObjectValue:nick]; if( [nick length] >= 1 ) [addButton setEnabled:YES]; else [addButton setEnabled:NO]; } - (void) setNewBuddyFullname:(NSString *) name { NSRange range = [name rangeOfString:@" "]; if( range.location != NSNotFound ) { [firstName setObjectValue:[name substringToIndex:range.location]]; if( ( range.location + 1 ) < [name length] ) { [lastName setObjectValue:[name substringFromIndex:( range.location + 1 )]]; } else [lastName setObjectValue:@""]; } else { [firstName setObjectValue:@""]; [lastName setObjectValue:@""]; } } - (void) setNewBuddyServer:(MVChatConnection *) connection { [server selectItemWithTitle:[connection server]]; } #pragma mark - - (IBAction) messageSelectedBuddy:(id) sender { if( [buddies selectedRow] == -1 ) return; JVBuddy *buddy = [_buddyOrder objectAtIndex:[buddies selectedRow]]; NSURL *url = [buddy activeNickname]; [[JVChatController defaultManager] chatViewControllerForUser:[url user] withConnection:[[MVConnectionsController defaultManager] connectionForServerAddress:[url host]] ifExists:NO]; } - (IBAction) sendFileToSelectedBuddy:(id) sender { if( [buddies selectedRow] == -1 ) return; BOOL passive = [[NSUserDefaults standardUserDefaults] boolForKey:@"JVSendFilesPassively"]; JVBuddy *buddy = [_buddyOrder objectAtIndex:[buddies selectedRow]]; NSURL *url = [buddy activeNickname]; NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setResolvesAliases:YES]; [panel setCanChooseFiles:YES]; [panel setCanChooseDirectories:NO]; [panel setAllowsMultipleSelection:YES]; NSView *view = [[[NSView alloc] initWithFrame:NSMakeRect( 0., 0., 200., 28. )] autorelease]; [view setAutoresizingMask:( NSViewWidthSizable | NSViewMaxXMargin )]; NSButton *passiveButton = [[[NSButton alloc] initWithFrame:NSMakeRect( 0., 6., 200., 18. )] autorelease]; [[passiveButton cell] setButtonType:NSSwitchButton]; [passiveButton setState:passive]; [passiveButton setTitle:NSLocalizedString( @"Send File Passively", "send files passively file send open dialog button" )]; [passiveButton sizeToFit]; NSRect frame = [view frame]; frame.size.width = NSWidth( [passiveButton frame] ); [view setFrame:frame]; [view addSubview:passiveButton]; [panel setAccessoryView:view]; if( [panel runModalForTypes:nil] == NSOKButton ) { MVChatConnection *connection = [[MVConnectionsController defaultManager] connectionForServerAddress:[url host]]; NSEnumerator *enumerator = [[panel filenames] objectEnumerator]; passive = [passiveButton state]; NSString *path = nil; while( ( path = [enumerator nextObject] ) ) [[MVFileTransferController defaultManager] addFileTransfer:[connection sendFile:path toUser:[url user] passively:passive]]; } } #pragma mark - - (void) setShowFullNames:(BOOL) flag { _showFullNames = flag; [buddies reloadData]; [self _setBuddiesNeedSortAnimated]; [self _sortBuddiesAnimatedIfNeeded:nil]; [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"JVChatBuddyListShowFullNames"]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (BOOL) showFullNames { return _showFullNames; } - (IBAction) toggleShowFullNames:(id) sender { [self setShowFullNames:(! _showFullNames)]; } #pragma mark - - (void) setShowNicknameAndServer:(BOOL) flag { _showNicknameAndServer = flag; if( _showIcons || _showNicknameAndServer ) [buddies setRowHeight:36.]; else [buddies setRowHeight:18.]; [buddies reloadData]; [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"JVChatBuddyListShowNicknameAndServer"]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (BOOL) showNicknameAndServer { return _showNicknameAndServer; } - (IBAction) toggleShowNicknameAndServer:(id) sender { [self setShowNicknameAndServer:(! _showNicknameAndServer)]; } #pragma mark - - (void) setShowIcons:(BOOL) flag { _showIcons = flag; if( _showIcons || _showNicknameAndServer ) [buddies setRowHeight:36.]; else [buddies setRowHeight:18.]; [buddies reloadData]; [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"JVChatBuddyListShowIcons"]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (BOOL) showIcons { return _showIcons; } - (IBAction) toggleShowIcons:(id) sender { [self setShowIcons:(! _showIcons)]; } #pragma mark - - (void) setShowOfflineBuddies:(BOOL) flag { _showOfflineBuddies = flag; NSMutableSet *offlineBuddies = [NSMutableSet setWithSet:_buddyList]; [offlineBuddies minusSet:_onlineBuddies]; if( ! _showOfflineBuddies ) [_buddyOrder removeObjectsInArray:[offlineBuddies allObjects]]; else [_buddyOrder addObjectsFromArray:[offlineBuddies allObjects]]; [self _manuallySortAndUpdate]; [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"JVChatBuddyListShowOfflineBuddies"]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (BOOL) showOfflineBuddies { return _showOfflineBuddies; } - (IBAction) toggleShowOfflineBuddies:(id) sender { [self setShowOfflineBuddies:(! _showOfflineBuddies)]; } #pragma mark - - (void) setSortOrder:(MVBuddyListSortOrder) order { _sortOrder = order; [self _setBuddiesNeedSortAnimated]; [self _sortBuddiesAnimatedIfNeeded:nil]; [[NSUserDefaults standardUserDefaults] setInteger:order forKey:@"JVChatBuddyListSortOrder"]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (MVBuddyListSortOrder) sortOrder { return _sortOrder; } - (IBAction) sortByAvailability:(id) sender { [self setSortOrder:MVAvailabilitySortOrder]; } - (IBAction) sortByFirstName:(id) sender { [self setSortOrder:MVFirstNameSortOrder]; } - (IBAction) sortByLastName:(id) sender { [self setSortOrder:MVLastNameSortOrder]; } - (IBAction) sortByServer:(id) sender { [self setSortOrder:MVServerSortOrder]; } #pragma mark - - (BOOL) validateMenuItem:(NSMenuItem *) menuItem { if( [menuItem action] == @selector( toggleShowFullNames: ) ) { [menuItem setState:( _showFullNames ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( toggleShowNicknameAndServer: ) ) { [menuItem setState:( _showNicknameAndServer ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( toggleShowIcons: ) ) { [menuItem setState:( _showIcons ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( toggleShowOfflineBuddies: ) ) { [menuItem setState:( _showOfflineBuddies ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( messageSelectedBuddy: ) ) { if( [buddies selectedRow] == -1 ) return NO; else return YES; } else if( [menuItem action] == @selector( sendFileToSelectedBuddy: ) ) { if( [buddies selectedRow] == -1 ) return NO; else return YES; } else if( [menuItem action] == @selector( getInfo: ) ) { if( [buddies selectedRow] == -1 ) return NO; else return YES; } else if( [menuItem action] == @selector( sortByAvailability: ) ) { [menuItem setState:( _sortOrder == MVAvailabilitySortOrder || ! _sortOrder ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( sortByFirstName: ) ) { [menuItem setState:( _sortOrder == MVFirstNameSortOrder ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( sortByLastName: ) ) { [menuItem setState:( _sortOrder == MVLastNameSortOrder ? NSOnState : NSOffState )]; return YES; } else if( [menuItem action] == @selector( sortByServer: ) ) { [menuItem setState:( _sortOrder == MVServerSortOrder ? NSOnState : NSOffState )]; return YES; } return YES; } @end #pragma mark - @implementation MVBuddyListController (MVBuddyListControllerDelegate) - (void) clear:(id) sender { if( [buddies selectedRow] == -1 ) return; JVBuddy *buddy = [[[_buddyOrder objectAtIndex:[buddies selectedRow]] retain] autorelease]; [_buddyList removeObject:buddy]; [_onlineBuddies removeObject:buddy]; [_buddyOrder removeObjectIdenticalTo:buddy]; [self _manuallySortAndUpdate]; [self _saveBuddyList]; } - (int) numberOfRowsInTableView:(NSTableView *) view { return [_buddyOrder count]; } - (id) tableView:(NSTableView *) view objectValueForTableColumn:(NSTableColumn *) column row:(int) row { if( row == -1 || row >= [_buddyOrder count] ) return nil; if( [[column identifier] isEqualToString:@"buddy"] ) { if( _showIcons ) { JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSImage *ret = [buddy picture]; if( ! ret ) ret = [[[NSImage imageNamed:@"largePerson"] copy] autorelease]; if( [ret size].width > 32 || [ret size].height > 32 ) { [ret setScalesWhenResized:YES]; [ret setSize:NSMakeSize( 32., 32. )]; } return ret; } else return nil; } return nil; } - (void) tableView:(NSTableView *) view willDisplayCell:(id) cell forTableColumn:(NSTableColumn *) column row:(int) row { if( row == -1 || row >= [_buddyOrder count] ) return; if( [[column identifier] isEqualToString:@"buddy"] ) { JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSURL *url = [buddy activeNickname]; if( url && ( ! _showFullNames || ! [[buddy compositeName] length] ) ) { [cell setMainText:[url user]]; if( _showNicknameAndServer ) [cell setInformationText:[url host]]; else [cell setInformationText:nil]; } else { [cell setMainText:[buddy compositeName]]; if( url && _showNicknameAndServer ) [cell setInformationText:[NSString stringWithFormat:@"%@ (%@)", [url user], [url host]]]; else [cell setInformationText:nil]; } [cell setEnabled:[buddy isOnline]]; switch( [buddy status] ) { case JVBuddyAwayStatus: [cell setStatusImage:[NSImage imageNamed:@"statusAway"]]; break; case JVBuddyIdleStatus: [cell setStatusImage:[NSImage imageNamed:@"statusIdle"]]; break; case JVBuddyAvailableStatus: [cell setStatusImage:[NSImage imageNamed:@"statusAvailable"]]; break; default: [cell setStatusImage:[NSImage imageNamed:@"statusOffline"]]; } } else if( [[column identifier] isEqualToString:@"switch"] ) { JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSSet *onlineNicks = [buddy onlineNicknames]; NSSet *nicks = nil; if( _showOfflineBuddies ) nicks = [buddy nicknames]; else nicks = [buddy onlineNicknames]; if( [nicks count] >= 2 ) { NSMenu *menu = [[[NSMenu alloc] initWithTitle:@""] autorelease]; NSMenuItem *item = nil; NSEnumerator *nickEnumerator = [nicks objectEnumerator]; NSURL *activeNick = [buddy activeNickname]; NSURL *nick = nil; [menu setAutoenablesItems:NO]; while( ( nick = [nickEnumerator nextObject] ) ) { item = [[[NSMenuItem alloc] initWithTitle:[NSString stringWithFormat:@"%@ (%@)", [nick user], [nick host]] action:NULL keyEquivalent:@""] autorelease]; if( [nick isEqual:activeNick] ) [item setState:NSOnState]; if( ! [onlineNicks containsObject:nick] ) [item setEnabled:NO]; [menu addItem:item]; } [cell setMenu:menu]; [cell setArrowPosition:NSPopUpArrowAtCenter]; [cell setEnabled:YES]; if( _showIcons || _showNicknameAndServer ) [cell setControlSize:NSRegularControlSize]; else [cell setControlSize:NSSmallControlSize]; } else { [cell setArrowPosition:NSPopUpNoArrow]; [cell setEnabled:NO]; } } } - (void) tableView:(NSTableView *) tableView setObjectValue:(id) object forTableColumn:(NSTableColumn *) tableColumn row:(int) row { if( row == -1 || row >= [_buddyOrder count] ) return; JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSArray *nicks = nil; if( _showOfflineBuddies ) nicks = [[buddy nicknames] allObjects]; else nicks = [[buddy onlineNicknames] allObjects]; [buddy setActiveNickname:[nicks objectAtIndex:[object unsignedIntValue]]]; [buddies reloadData]; [self _setBuddiesNeedSortAnimated]; [self _sortBuddiesAnimatedIfNeeded:nil]; } - (NSMenu *) tableView:(MVTableView *) tableView menuForTableColumn:(NSTableColumn *) tableColumn row:(int) row { NSMenu *menu = [[actionMenu copyWithZone:[self zone]] autorelease]; JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSMethodSignature *signature = [NSMethodSignature methodSignatureWithReturnAndArgumentTypes:@encode( NSArray * ), @encode( id ), @encode( id ), nil]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; id view = nil; [invocation setSelector:@selector( contextualMenuItemsForObject:inView: )]; [invocation setArgument:&buddy atIndex:2]; [invocation setArgument:&view atIndex:3]; NSArray *results = [[MVChatPluginManager defaultManager] makePluginsPerformInvocation:invocation]; if( [results count] ) { [menu addItem:[NSMenuItem separatorItem]]; NSArray *items = nil; NSMenuItem *item = nil; NSEnumerator *enumerator = [results objectEnumerator]; while( ( items = [enumerator nextObject] ) ) { if( ! [items respondsToSelector:@selector( objectEnumerator )] ) continue; NSEnumerator *ienumerator = [items objectEnumerator]; while( ( item = [ienumerator nextObject] ) ) if( [item isKindOfClass:[NSMenuItem class]] ) [menu addItem:item]; } if( [[[menu itemArray] lastObject] isSeparatorItem] ) [menu removeItem:[[menu itemArray] lastObject]]; } return menu; } - (NSString *) tableView:(MVTableView *) tableView toolTipForTableColumn:(NSTableColumn *) column row:(int) row { if( row == -1 || row >= [_buddyOrder count] ) return nil; NSMutableString *ret = [NSMutableString string]; JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSURL *url = [buddy activeNickname]; [ret appendFormat:@"%@\n", [buddy compositeName]]; [ret appendFormat:@"%@ (%@)\n", [url user], [url host]]; switch( [buddy status] ) { case JVBuddyAwayStatus: [ret appendString:NSLocalizedString( @"Away", "away buddy status" )]; break; case JVBuddyIdleStatus: [ret appendString:NSLocalizedString( @"Idle", "idle buddy status" )]; break; case JVBuddyAvailableStatus: [ret appendString:NSLocalizedString( @"Available", "available buddy status" )]; break; default: [ret appendString:NSLocalizedString( @"Offline", "offline buddy status" )]; } return ret; } - (void) tableViewSelectionDidChange:(NSNotification *) notification { BOOL enabled = ! ( [buddies selectedRow] == -1 ); [sendMessageButton setEnabled:enabled]; [infoButton setEnabled:enabled]; [[JVInspectorController sharedInspector] inspectObject:[self objectToInspect]]; } - (NSDragOperation) tableView:(NSTableView *) tableView validateDrop:(id ) info proposedRow:(int) row proposedDropOperation:(NSTableViewDropOperation) operation { if( operation == NSTableViewDropOn && row != -1 ) return NSDragOperationMove; return NSDragOperationNone; } - (BOOL) tableView:(NSTableView *) tableView acceptDrop:(id ) info row:(int) row dropOperation:(NSTableViewDropOperation) operation { NSPasteboard *board = [info draggingPasteboard]; if( [board availableTypeFromArray:[NSArray arrayWithObject:NSFilenamesPboardType]] ) { BOOL passive = [[NSUserDefaults standardUserDefaults] boolForKey:@"JVSendFilesPassively"]; JVBuddy *buddy = [_buddyOrder objectAtIndex:row]; NSURL *url = [buddy activeNickname]; MVChatConnection *connection = [[MVConnectionsController defaultManager] connectionForServerAddress:[url host]]; NSArray *files = [[info draggingPasteboard] propertyListForType:NSFilenamesPboardType]; NSEnumerator *enumerator = [files objectEnumerator]; id file = nil; while( ( file = [enumerator nextObject] ) ) [[MVFileTransferController defaultManager] addFileTransfer:[connection sendFile:file toUser:[url user] passively:passive]]; return YES; } return NO; } - (NSRange) tableView:(MVTableView *) tableView rowsInRect:(NSRect) rect defaultRange:(NSRange) defaultRange { if( _animating ) return NSMakeRange( 0, [_buddyOrder count] ); else return defaultRange; } #define curveFunction(t,p) ( pow( 1 - pow( ( 1 - t ), p ), ( 1 / p ) ) ) #define easeFunction(t) ( ( sin( ( t * M_PI ) - M_PI_2 ) + 1. ) / 2. ) - (NSRect) tableView:(MVTableView *) tableView rectOfRow:(int) row defaultRect:(NSRect) defaultRect { if( _animating ) { int oldPosition = [[_oldPositions objectAtIndex:row] intValue]; NSRect oldR = [tableView originalRectOfRow:oldPosition]; NSRect newR = [tableView originalRectOfRow:row]; float t = _animationPosition; float rowPos = ( (float) row / [_buddyOrder count] ); float rowPosAdjusted = _viewingTop ? ( 1. - rowPos ) : rowPos; float curve = 0.3; float p = rowPosAdjusted * ( curve * 2. ) + 1. - curve; t = curveFunction( t, p ); t = easeFunction( t ); return NSMakeRect( NSMinX( oldR ) + ( t * ( NSMinX( newR ) - NSMinX( oldR ) ) ), NSMinY( oldR ) + ( t * ( NSMinY( newR ) - NSMinY( oldR ) ) ), NSWidth( newR ), NSHeight( newR ) ); } else return defaultRect; } @end #pragma mark - @implementation MVBuddyListController (MVBuddyListControllerPrivate) - (void) _animateStep:(NSTimer *) timer { static NSDate *start = nil; if( ! _animationPosition ) { [start autorelease]; start = [[NSDate date] retain]; } NSTimeInterval elapsed = fabs( [start timeIntervalSinceNow] ); _animationPosition = MIN( 1., elapsed / .25 ); if( fabs( _animationPosition - 1. ) <= 0.01 ) { [start autorelease]; start = nil; [timer invalidate]; [timer autorelease]; _animationPosition = 0.; _animating = NO; [buddies display]; } else [buddies display]; } - (void) _manuallySortAndUpdate { if( _animationPosition ) return; [self _sortBuddies]; [buddies reloadData]; } - (void) _sortBuddies { switch( _sortOrder ) { default: case MVAvailabilitySortOrder: [_buddyOrder sortUsingSelector:@selector( availabilityCompare: )]; break; case MVFirstNameSortOrder: [_buddyOrder sortUsingSelector:@selector( firstNameCompare: )]; break; case MVLastNameSortOrder: [_buddyOrder sortUsingSelector:@selector( lastNameCompare: )]; break; case MVServerSortOrder: [_buddyOrder sortUsingSelector:@selector( serverCompare: )]; } } - (void) _setBuddiesNeedSortAnimated { _needsToAnimate = YES; } - (void) _sortBuddiesAnimatedIfNeeded:(id) sender { if( ! _needsToAnimate || _animating ) return; _needsToAnimate = NO; NSRange visibleRows; NSArray *oldOrder = [[_buddyOrder copy] autorelease]; id selectedObject = nil; if( [buddies selectedRow] != -1 && [buddies selectedRow] < [oldOrder count] ) selectedObject = [oldOrder objectAtIndex:[buddies selectedRow]]; [self _sortBuddies]; if( [oldOrder isEqualToArray:_buddyOrder] ) return; if( selectedObject ) { [buddies deselectAll:nil]; [buddies selectRow:[_buddyOrder indexOfObjectIdenticalTo:selectedObject] byExtendingSelection:NO]; [buddies setNeedsDisplay:NO]; } _animating = YES; [_oldPositions autorelease]; _oldPositions = [[NSMutableArray arrayWithCapacity:[_buddyOrder count]] retain]; NSEnumerator *enumerator = [_buddyOrder objectEnumerator]; id object = nil; while( ( object = [enumerator nextObject] ) ) [_oldPositions addObject:[NSNumber numberWithInt:[oldOrder indexOfObject:object]]]; visibleRows = [buddies rowsInRect:[buddies visibleRect]]; _viewingTop = NSMaxRange( visibleRows ) < 0.6 * [_buddyOrder count]; [[NSTimer scheduledTimerWithTimeInterval:( 1. / 240. ) target:self selector:@selector( _animateStep: ) userInfo:nil repeats:YES] retain]; } - (void) _buddyOnline:(NSNotification *) notification { JVBuddy *buddy = [notification object]; if( ! [_buddyList containsObject:buddy] ) return; [_onlineBuddies addObject:buddy]; if( ! [_buddyOrder containsObject:buddy] ) { [_buddyOrder addObject:buddy]; [buddies noteNumberOfRowsChanged]; } [self _setBuddiesNeedSortAnimated]; if( ! _animating ) [buddies reloadData]; NSMutableDictionary *context = [NSMutableDictionary dictionary]; [context setObject:NSLocalizedString( @"Buddy Available", "available buddy bubble title" ) forKey:@"title"]; [context setObject:[NSString stringWithFormat:NSLocalizedString( @"Your buddy %@ is now online.", "available buddy bubble text" ), [buddy preferredName]] forKey:@"description"]; NSImage *icon = [buddy picture]; if( ! icon ) icon = [NSImage imageNamed:@"largePerson"]; [context setObject:icon forKey:@"image"]; [[JVNotificationController defaultManager] performNotification:@"JVChatBuddyOnline" withContextInfo:context]; } - (void) _buddyOffline:(NSNotification *) notification { JVBuddy *buddy = [notification object]; if( ! [_onlineBuddies containsObject:buddy] ) return; [_onlineBuddies removeObject:buddy]; if( ! _showOfflineBuddies ) { [_buddyOrder removeObject:buddy]; [buddies noteNumberOfRowsChanged]; } [self _setBuddiesNeedSortAnimated]; if( ! _animating ) [buddies reloadData]; MVChatConnection *buddyConnection = [[MVConnectionsController defaultManager] connectionForServerAddress:[[buddy activeNickname] host]]; if( [buddyConnection isConnected] ) { NSMutableDictionary *context = [NSMutableDictionary dictionary]; [context setObject:NSLocalizedString( @"Buddy Unavailable", "unavailable buddy bubble title" ) forKey:@"title"]; [context setObject:[NSString stringWithFormat:NSLocalizedString( @"Your buddy %@ is now offline.", "unavailable buddy bubble text" ), [buddy preferredName]] forKey:@"description"]; NSImage *icon = [buddy picture]; if( ! icon ) icon = [NSImage imageNamed:@"largePerson"]; [context setObject:icon forKey:@"image"]; [[JVNotificationController defaultManager] performNotification:@"JVChatBuddyOffline" withContextInfo:context]; } } - (void) _buddyChanged:(NSNotification *) notification { JVBuddy *buddy = [notification object]; if( ! [_onlineBuddies containsObject:buddy] ) return; [self _setBuddiesNeedSortAnimated]; if( ! _animating ) [buddies reloadData]; } - (void) _addBuddyToList:(JVBuddy *) buddy { [_buddyList addObject:buddy]; if( _showOfflineBuddies && ! [_buddyOrder containsObject:buddy] ) { [_buddyOrder addObject:buddy]; [self _manuallySortAndUpdate]; } } - (void) _saveBuddyList { NSMutableArray *list = [NSMutableArray arrayWithCapacity:[_buddyList count]]; NSEnumerator *enumerator = [_buddyList objectEnumerator]; JVBuddy *buddy = nil; while( ( buddy = [enumerator nextObject] ) ) [list addObject:[buddy uniqueIdentifier]]; [[NSUserDefaults standardUserDefaults] setObject:list forKey:@"JVChatBuddies"]; [[NSUserDefaults standardUserDefaults] synchronize]; } - (void) _loadBuddyList { NSArray *list = [[NSUserDefaults standardUserDefaults] objectForKey:@"JVChatBuddies"]; NSEnumerator *enumerator = [list objectEnumerator]; NSString *identifier = nil; while( ( identifier = [enumerator nextObject] ) ) { JVBuddy *buddy = [JVBuddy buddyWithUniqueIdentifier:identifier]; if( [[buddy nicknames] count] ) [self _addBuddyToList:buddy]; } } @end #pragma mark - @implementation JVBuddy (JVBuddyObjectSpecifier) - (NSScriptObjectSpecifier *) objectSpecifier { id classDescription = [NSClassDescription classDescriptionForClass:[MVBuddyListController class]]; NSScriptObjectSpecifier *container = [[MVBuddyListController sharedBuddyList] objectSpecifier]; return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:classDescription containerSpecifier:container key:@"buddies" uniqueID:[self uniqueIdentifier]] autorelease]; } @end #pragma mark - @implementation MVBuddyListController (MVBuddyListControllerScripting) - (MVChatConnection *) valueInBuddiesAtIndex:(unsigned) index { return [_buddyOrder objectAtIndex:index]; } - (void) addInBuddies:(JVBuddy *) buddy { [NSException raise:NSOperationNotSupportedForKeyException format:@"Can't add a buddy."]; } - (void) insertInBuddies:(JVBuddy *) buddy { [NSException raise:NSOperationNotSupportedForKeyException format:@"Can't insert a buddy."]; } - (void) insertInBuddies:(JVBuddy *) buddy atIndex:(unsigned) index { [NSException raise:NSOperationNotSupportedForKeyException format:@"Can't insert a buddy."]; } - (void) removeFromBuddiesAtIndex:(unsigned) index { JVBuddy *buddy = [[[_buddyOrder objectAtIndex:index] retain] autorelease]; [_buddyList removeObject:buddy]; [_onlineBuddies removeObject:buddy]; [_buddyOrder removeObjectIdenticalTo:buddy]; [self _manuallySortAndUpdate]; [self _saveBuddyList]; } - (void) replaceInBuddies:(JVBuddy *) buddy atIndex:(unsigned) index { [NSException raise:NSOperationNotSupportedForKeyException format:@"Can't replace a buddy."]; } - (JVBuddy *) valueInBuddiesWithUniqueID:(id) identifier { NSEnumerator *enumerator = [_buddyList objectEnumerator]; JVBuddy *buddy = nil; while( ( buddy = [enumerator nextObject] ) ) if( [[buddy uniqueIdentifier] isEqualTo:identifier] ) return buddy; return nil; } - (JVBuddy *) valueInBuddiesWithName:(NSString *) name { NSEnumerator *enumerator = [_buddyList objectEnumerator]; JVBuddy *buddy = nil; while( ( buddy = [enumerator nextObject] ) ) if( [[buddy compositeName] isEqualToString:name] ) return buddy; return nil; } @end