2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
5 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6 * General Public License as published by the Free Software Foundation; either version 2 of the License,
7 * or (at your option) any later version.
9 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
11 * Public License for more details.
13 * You should have received a copy of the GNU General Public License along with this program; if not,
14 * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
17 #import "AIAccountController.h"
18 #import "AIContactController.h"
19 #import "AINewContactWindowController.h"
20 #import "AINewGroupWindowController.h"
21 #import <AIUtilities/AIMenuAdditions.h>
22 #import <AIUtilities/AIPopUpButtonAdditions.h>
23 #import <AIUtilities/AIStringAdditions.h>
24 #import <Adium/AIAccount.h>
25 #import <Adium/AIListContact.h>
26 #import <Adium/AIListGroup.h>
27 #import <Adium/AILocalizationTextField.h>
28 #import <Adium/AIService.h>
30 #define ADD_CONTACT_PROMPT_NIB @"AddContact"
31 #define DEFAULT_GROUP_NAME AILocalizedString(@"Contacts",nil)
33 @interface AINewContactWindowController (PRIVATE)
34 - (void)buildContactTypeMenu;
35 - (void)buildGroupMenu;
36 - (void)_buildGroupMenu:(NSMenu *)menu forGroup:(AIListGroup *)group level:(int)level;
37 - (void)validateEnteredName;
38 - (void)updateAccountList;
39 - (void)configureNameAndService;
40 - (void)configureForCurrentServiceType;
41 - (void)setContactName:(NSString *)contact;
42 - (void)setService:(AIService *)inService;
43 - (void)selectGroup:(id)sender;
44 - (void)selectFirstValidServiceType;
45 - (void)selectServiceType:(id)sender;
46 - (void)updateContactNameLabel;
50 * @class AINewContactWindowController
51 * @brief Window controller for adding a new contact
53 @implementation AINewContactWindowController
56 * @brief Prompt for adding a new contact.
58 * @param parentWindow Window on which to show the prompt as a sheet. Pass nil for a panel prompt.
59 * @param inName Initial value for the contact name field
60 * @param inService <tt>AIService</tt> for determining the initial service type selection
62 + (void)promptForNewContactOnWindow:(NSWindow *)parentWindow name:(NSString *)inName service:(AIService *)inService
64 AINewContactWindowController *newContactWindow;
66 newContactWindow = [[self alloc] initWithWindowNibName:ADD_CONTACT_PROMPT_NIB];
67 [newContactWindow setContactName:inName];
68 [newContactWindow setService:inService];
71 [parentWindow makeKeyAndOrderFront:nil];
73 [NSApp beginSheet:[newContactWindow window]
74 modalForWindow:parentWindow
75 modalDelegate:newContactWindow
76 didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
79 [newContactWindow showWindow:nil];
80 [[newContactWindow window] makeKeyAndOrderFront:nil];
88 - (id)initWithWindowNibName:(NSString *)windowNibName
90 self = [super initWithWindowNibName:windowNibName];
101 [contactName release];
108 * @brief Setup the window before it is displayed
110 - (void)windowDidLoad
112 [textField_type setLocalizedString:AILocalizedString(@"Contact Type:","Contact type service dropdown label in Add Contact")];
113 [textField_alias setLocalizedString:AILocalizedString(@"Alias:",nil)];
114 [textField_inGroup setLocalizedString:AILocalizedString(@"In Group:",nil)];
115 [textField_addToAccounts setLocalizedString:AILocalizedString(@"Add to Accounts:",nil)];
117 [button_add setLocalizedString:AILocalizedString(@"Add",nil)];
118 [button_cancel setLocalizedString:AILocalizedString(@"Cancel",nil)];
120 originalContactNameLabelFrame = [textField_contactNameLabel frame];
122 [self buildContactTypeMenu];
123 [self buildGroupMenu];
125 [self configureNameAndService];
127 [[adium notificationCenter] addObserver:self
128 selector:@selector(accountListChanged:)
129 name:Account_ListChanged
132 [[adium contactController] registerListObjectObserver:self];
134 [[self window] setTitle:AILocalizedString(@"Add Contact",nil)];
135 [[self window] center];
139 * @brief Window is closing
141 - (void)windowWillClose:(id)sender
143 [super windowWillClose:sender];
144 [[adium contactController] unregisterListObjectObserver:self];
145 [[adium notificationCenter] removeObserver:self];
149 * @brief Called as the user list edit sheet closes, dismisses the sheet
151 - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
153 [sheet orderOut:nil];
159 - (IBAction)cancel:(id)sender
161 if([[self window] isSheet]){
162 [NSApp endSheet:[self window]];
164 [self closeWindow:nil];
169 * @brief Perform the addition of the contact
171 - (IBAction)addContact:(id)sender
173 NSString *UID = [service filterUID:[textField_contactName stringValue] removeIgnoredCharacters:YES];
174 NSEnumerator *enumerator = [accounts objectEnumerator];
178 NSMutableArray *contactArray = [NSMutableArray array];
180 alias = [textField_contactAlias stringValue];
181 if([alias length] == 0) alias = nil;
183 group = ([popUp_targetGroup numberOfItems] ?
184 [[popUp_targetGroup selectedItem] representedObject] :
187 if (!group) group = [[adium contactController] groupWithUID:DEFAULT_GROUP_NAME];
189 while(account = [enumerator nextObject]){
190 if([account contactListEditable] &&
191 [[account preferenceForKey:KEY_ADD_CONTACT_TO group:PREF_GROUP_ADD_CONTACT] boolValue]){
192 AIListContact *contact = [[adium contactController] contactWithService:service
195 if(alias) [contact setDisplayName:alias];
197 [contactArray addObject:contact];
201 [[adium contactController] addContacts:contactArray
204 if([[self window] isSheet]){
205 [NSApp endSheet:[self window]];
207 [self closeWindow:nil];
212 //Service Type ---------------------------------------------------------------------------------------------------------
213 #pragma mark Service Type
215 * @brief Build and configure the menu of contact service types
217 - (void)buildContactTypeMenu
219 NSMenuItem *selectedItem;
221 [popUp_contactType setMenu:[[adium accountController] menuOfServicesWithTarget:self
222 activeServicesOnly:YES
226 //- (BOOL)validateMenuItem:(NSMenuItem *)menuItem below will automatically manage the enabling/disabling
227 //when we call update.
228 [[popUp_contactType menu] update];
230 //If there is no selection or the current selection is now disabled, select the first valid service type
232 !(selectedItem = (NSMenuItem *)[popUp_contactType selectedItem]) ||
233 (![selectedItem isEnabled])){
234 [self selectFirstValidServiceType];
236 //Otherwise, just perform needed configuration for the current selection
237 [self configureForCurrentServiceType];
242 * @brief Select the first valid service type
244 * 'valid' in this context means that an account on the appropriate service is online.
246 - (void)selectFirstValidServiceType
248 NSEnumerator *enumerator;
250 enumerator = [[popUp_contactType itemArray] objectEnumerator];
251 NSMenuItem *menuItem;
252 while(menuItem = [enumerator nextObject]) {
253 if([menuItem isEnabled]) {
254 [popUp_contactType selectItem:menuItem];
259 [self selectServiceType:nil];
263 * Validate a menu item
265 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
267 NSEnumerator *enumerator;
270 enumerator = [[[adium accountController] accountsWithServiceClassOfService:[menuItem representedObject]] objectEnumerator];
271 while(account = [enumerator nextObject]){
272 if([account contactListEditable]){
280 * @brief Service type was selected from the menu
282 - (void)selectServiceType:(id)sender
284 service = [[popUp_contactType selectedItem] representedObject];
286 [self configureForCurrentServiceType];
290 * @brief Configure for the current service type
292 - (void)configureForCurrentServiceType
294 [self updateContactNameLabel];
295 [self updateAccountList];
296 [self validateEnteredName];
299 //Add to Group ---------------------------------------------------------------------------------------------------------
300 #pragma mark Add to Group
302 * @brief Build the menu of available destination groups
304 - (void)buildGroupMenu
306 AIListObject *selectedObject;
309 menu = [[adium contactController] menuOfAllGroupsInGroup:nil withTarget:self];
311 //Add a default group name to the menu if there are no groups listed
312 if ([menu numberOfItems] == 0) {
313 [menu addItemWithTitle:DEFAULT_GROUP_NAME
315 action:@selector(selectGroup:)
319 [menu addItem:[NSMenuItem separatorItem]];
320 [menu addItemWithTitle:[AILocalizedString(@"New Group",nil) stringByAppendingEllipsis]
322 action:@selector(newGroup:)
325 //Select the group of the currently selected object on the contact list
326 selectedObject = [[adium contactController] selectedListObject];
327 while (selectedObject && ![selectedObject isKindOfClass:[AIListGroup class]]) {
328 selectedObject = [selectedObject containingObject];
331 [popUp_targetGroup setMenu:menu];
333 //If there was no selected group, just select the first item
334 if (selectedObject) {
335 if (![popUp_targetGroup selectItemWithRepresentedObject:selectedObject]) {
336 [popUp_targetGroup selectItemAtIndex:0];
340 [popUp_targetGroup selectItemAtIndex:0];
345 * @brief Prompt the user to add a new group immediately
347 - (void)newGroup:(id)sender
349 AINewGroupWindowController *newGroupWindowController;
351 newGroupWindowController = [AINewGroupWindowController promptForNewGroupOnWindow:[self window]];
353 //Observe for the New Group window to close
354 [[adium notificationCenter] addObserver:self
355 selector:@selector(newGroupDidEnd:)
356 name:@"NewGroupWindowControllerDidEnd"
357 object:[newGroupWindowController window]];
360 - (void)newGroupDidEnd:(NSNotification *)inNotification
362 NSWindow *window = [inNotification object];
364 if ([[window windowController] isKindOfClass:[AINewGroupWindowController class]]) {
365 NSString *newGroupUID = [[window windowController] newGroupUID];
366 AIListGroup *group = [[adium contactController] existingGroupWithUID:newGroupUID];
368 //Rebuild the group menu
369 [self buildGroupMenu];
371 /* Select the new group if it exists; otherwise select the first group (so we don't still have New Group... selected).
372 * If the user canceled, group will be nil since the group doesn't exist.
374 if (![popUp_targetGroup selectItemWithRepresentedObject:group]) {
375 [popUp_targetGroup selectItemAtIndex:0];
378 [[self window] performSelector:@selector(makeKeyAndOrderFront:)
384 [[adium notificationCenter] removeObserver:self
385 name:@"NewGroupWindowControllerDidEnd"
389 //Contact Name ---------------------------------------------------------------------------------------------------------
390 #pragma mark Contact Name
392 * @brief Fill in the name field if we came from a tab
394 - (void)configureNameAndService
397 [textField_contactName setStringValue:contactName];
402 NSEnumerator *enumerator = [[popUp_contactType itemArray] objectEnumerator];
404 while (item = [enumerator nextObject]){
405 if([item representedObject] == service){
406 [popUp_contactType selectItem:item];
412 [self configureForCurrentServiceType];
416 * @brief Set the contact name
418 * This does not perform subsequent validation.
420 - (void)setContactName:(NSString *)contact
422 if(contactName != contact){
423 [contactName release];
424 contactName = [contact retain];
431 * This does not perform subsequent validation.
433 - (void)setService:(AIService *)inService
435 if(service != inService){
437 service = [inService retain];
442 * @brief Entered name is changing; validate it.
444 - (void)controlTextDidChange:(NSNotification *)notification
446 if([notification object] == textField_contactName){
447 [self validateEnteredName];
452 * @brief Validate the entered name, enabling the add button if it is valid
454 - (void)validateEnteredName
456 NSString *name = [textField_contactName stringValue];
459 if([name length] != 0 && [name length] <= [service allowedLengthForUIDs]){
460 BOOL caseSensitive = [service caseSensitive];
461 NSScanner *scanner = [NSScanner scannerWithString:(caseSensitive ? name : [name lowercaseString])];
462 NSString *validSegment = nil;
464 [scanner scanCharactersFromSet:[service allowedCharactersForUIDs] intoString:&validSegment];
465 if(!validSegment || [validSegment length] != [name length]){
472 //If enabled so far, make sure an account is checked
474 NSEnumerator *enumerator = [accounts objectEnumerator];
477 BOOL anAccountIsChecked = NO;
479 while(account = [enumerator nextObject]){
480 if([account contactListEditable] &&
481 [[account preferenceForKey:KEY_ADD_CONTACT_TO group:PREF_GROUP_ADD_CONTACT] boolValue]){
482 anAccountIsChecked = YES;
487 enabled = anAccountIsChecked;
490 [button_add setEnabled:enabled];
493 //Add to Accounts ------------------------------------------------------------------------------------------------------
494 #pragma mark Add to Accounts
496 * @brief Update the accounts list
498 - (void)updateAccountList
500 NSEnumerator *enumerator;
505 accounts = [[[adium accountController] accountsWithServiceClassOfService:service] retain];
507 //Select accounts by default
508 enumerator = [accounts objectEnumerator];
509 while(account = [enumerator nextObject]) {
510 addTo = [account preferenceForKey:KEY_ADD_CONTACT_TO group:PREF_GROUP_ADD_CONTACT];
512 [account setPreference:[NSNumber numberWithBool:YES] forKey:KEY_ADD_CONTACT_TO
513 group:PREF_GROUP_ADD_CONTACT];
515 [tableView_accounts reloadData];
519 * @brief The account list changed
521 - (void)accountListChanged:(NSNotification *)notification
523 //Attempt to retain the current contact type selection
524 id representedObject = [[popUp_contactType selectedItem] representedObject];
525 [self buildContactTypeMenu];
527 int index = [popUp_contactType indexOfItemWithRepresentedObject:representedObject];
528 if (index != NSNotFound){
529 [popUp_contactType selectItemAtIndex:index];
532 [self updateAccountList];
536 * @brief Update when account connectivity changes
538 * If the selected service is still valid, just reload the accounts table view to update it.
539 * If it is no longer valid (the last account on this service just signed off), select the first valid one.
541 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
543 if ([inObject isKindOfClass:[AIAccount class]] && [inModifiedKeys containsObject:@"Online"]){
544 if([self validateMenuItem:(NSMenuItem *)[popUp_contactType selectedItem]]){
545 //If the current selection in the contact type menu is still valid (an account is still online), reload the accounts data
546 [tableView_accounts reloadData];
548 //If it is not, switch to the first valid contact type and update accordingly
549 [self selectFirstValidServiceType];
557 * @brief Update the contact name label
559 * The label is customized for the selected service as well as localized.
560 * Updating the label may resize the window to fit.
562 - (void)updateContactNameLabel
567 oldFrame = [textField_contactNameLabel frame];
569 //If the old frame is smaller than our original frame, treat the old frame as that original frame
570 //for resizing and positioning purposes
571 if(oldFrame.size.width < originalContactNameLabelFrame.size.width){
572 oldFrame = originalContactNameLabelFrame;
575 NSString *userNameLabel = (service ? [service userNameLabel] : nil);
577 //Set to the userNameLabel, using a default value if we have no userNameLabel, then sizeToFit
578 [textField_contactNameLabel setStringValue:[(userNameLabel ? userNameLabel : AILocalizedString(@"Contact ID",nil)) stringByAppendingString:@":"]];
579 [textField_contactNameLabel sizeToFit];
580 newFrame = [textField_contactNameLabel frame];
582 //Enforce a minimum width of the original contact name label frame width
583 if(newFrame.size.width < originalContactNameLabelFrame.size.width){
584 newFrame.size.width = originalContactNameLabelFrame.size.width;
587 //Only use integral widths to keep alignment correct;
588 //round up as an extra pixel of whitespace never hurt anybody
589 newFrame.size.width = round(newFrame.size.width + 0.5);
591 //Keep the right edge in the same place at all times
592 newFrame.origin.x = oldFrame.origin.x + oldFrame.size.width - newFrame.size.width;
594 [textField_contactNameLabel setFrame:newFrame];
595 [textField_contactNameLabel setNeedsDisplay:YES];
597 //Resize the window to fit the contactNameLabel if the current origin is not correct; the resut
598 if(newFrame.origin.x < 17){
599 NSRect windowFrame = [[self window] frame];
600 float difference = 17 - newFrame.origin.x;
602 windowFrame.origin.x -= difference;
603 windowFrame.size.width += difference;
604 [[self window] setFrame:windowFrame display:YES animate:YES];
606 }else if(oldFrame.origin.x <= 17){
607 NSRect windowFrame = [[self window] frame];
608 float difference = oldFrame.origin.x - newFrame.origin.x;
610 if(newFrame.origin.x + difference < originalContactNameLabelFrame.origin.x){
611 difference = originalContactNameLabelFrame.origin.x - newFrame.origin.x;
614 windowFrame.origin.x -= difference;
615 windowFrame.size.width += difference;
616 [[self window] setFrame:windowFrame display:YES animate:YES];
619 //Display to remove any artifacts from the frame changing
620 [[self window] display];
625 * @brief Rows in the accounts table view
627 - (int)numberOfRowsInTableView:(NSTableView *)tableView
629 return([accounts count]);
633 * @brief Object value for columns in the accounts table view
635 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
637 NSString *identifier = [tableColumn identifier];
639 if([identifier isEqualToString:@"check"]){
640 return([[accounts objectAtIndex:row] contactListEditable] ?
641 [[accounts objectAtIndex:row] preferenceForKey:KEY_ADD_CONTACT_TO
642 group:PREF_GROUP_ADD_CONTACT] :
643 [NSNumber numberWithBool:NO]);
644 }else if([identifier isEqualToString:@"account"]){
645 return([[accounts objectAtIndex:row] formattedUID]);
652 * @brief Will display cell
654 * Enable/disable account checkbox as appropriate
656 - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
658 [cell setEnabled:[[accounts objectAtIndex:row] contactListEditable]];
662 * @brief Set the enabled/disabled state for an account in the account list
664 - (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(int)row
666 NSString *identifier = [tableColumn identifier];
668 if([identifier isEqualToString:@"check"]){
669 [[accounts objectAtIndex:row] setPreference:[NSNumber numberWithBool:[object boolValue]]
670 forKey:KEY_ADD_CONTACT_TO
671 group:PREF_GROUP_ADD_CONTACT];
672 [self validateEnteredName];
677 * @brief Selection is changing
679 * Adam: I don't want the table to display its selection.
680 * Returning NO from 'shouldSelectRow' would work, but if we do that the checkbox cells stop working.
681 * The best solution I've come up with so far is to just force a deselect here :( .
683 - (void)tableViewSelectionIsChanging:(NSNotification *)notification{
684 [tableView_accounts deselectAll:nil];
688 * @brief Selection is changing
690 * Adam: I don't want the table to display its selection.
691 * Returning NO from 'shouldSelectRow' would work, but if we do that the checkbox cells stop working.
692 * The best solution I've come up with so far is to just force a deselect here :( .
694 - (void)tableViewSelectionDidChange:(NSNotification *)notification{
695 [tableView_accounts deselectAll:nil];
699 * @brief Empty selector called by the group popUp menu
701 - (void)selectGroup:(id)sender