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 <Adium/AIAccountControllerProtocol.h>
18 #import "AIAccountListPreferences.h"
19 #import <Adium/AIContactControllerProtocol.h>
20 #import "AIStatusController.h"
21 #import "AIEditAccountWindowController.h"
22 #import <AIUtilities/AIAutoScrollView.h>
23 #import <AIUtilities/AIImageTextCell.h>
24 #import <AIUtilities/AIVerticallyCenteredTextCell.h>
25 #import <AIUtilities/AITableViewAdditions.h>
26 #import <AIUtilities/AIImageAdditions.h>
27 #import <AIUtilities/AIMutableStringAdditions.h>
28 #import <Adium/AIAccount.h>
29 #import <Adium/AIListObject.h>
30 #import <Adium/AIService.h>
31 #import <Adium/AIServiceIcons.h>
32 #import <Adium/AIServiceMenu.h>
33 #import <Adium/AIStatusIcons.h>
34 #import <AIUtilities/AIAttributedStringAdditions.h>
35 #import "KFTypeSelectTableView.h"
37 #define MINIMUM_ROW_HEIGHT 34
38 #define MINIMUM_CELL_SPACING 4
40 #define ACCOUNT_DRAG_TYPE @"AIAccount" //ID for an account drag
42 #define NEW_ACCOUNT_DISPLAY_TEXT AILocalizedString(@"<New Account>", "Placeholder displayed as the name of a new account")
44 @interface AIAccountListPreferences (PRIVATE)
45 - (void)configureAccountList;
46 - (void)accountListChanged:(NSNotification *)notification;
48 - (void)calculateHeightForRow:(int)row;
49 - (void)calculateAllHeights;
53 * @class AIAccountListPreferences
54 * @brief Shows a list of accounts and provides for management of them
56 @implementation AIAccountListPreferences
59 * @brief Preference pane properties
61 - (NSString *)paneIdentifier
65 - (NSString *)paneName{
66 return AILocalizedString(@"Accounts","Accounts preferences label");
68 - (NSString *)nibName{
69 return @"AccountListPreferences";
73 return [NSImage imageNamed:@"pref-accounts" forClass:[self class]];
77 * @brief Configure the view initially
81 //Configure the account list
82 [self configureAccountList];
83 [self updateAccountOverview];
85 [requiredHeightDict release]; requiredHeightDict = [[NSMutableDictionary alloc] init];
87 //Build the 'add account' menu of each available service
88 NSMenu *serviceMenu = [AIServiceMenu menuOfServicesWithTarget:self
91 format:AILocalizedString(@"%@",nil)];
92 [serviceMenu setAutoenablesItems:YES];
94 //Indent each item in the service menu one level
95 NSEnumerator *enumerator = [[serviceMenu itemArray] objectEnumerator];
97 while ((menuItem = [enumerator nextObject])) {
98 [menuItem setIndentationLevel:[menuItem indentationLevel]+1];
101 //Add a label to the top of the menu to clarify why we're showing this list of services
102 [serviceMenu insertItemWithTitle:AILocalizedString(@"Add an account for:",nil)
108 [button_newAccount setMenu:serviceMenu];
110 //Observe status icon pack changes
111 [[adium notificationCenter] addObserver:self
112 selector:@selector(iconPackDidChange:)
113 name:AIStatusIconSetDidChangeNotification
116 //Observe service icon pack changes
117 [[adium notificationCenter] addObserver:self
118 selector:@selector(iconPackDidChange:)
119 name:AIServiceIconSetDidChangeNotification
124 * @brief Perform actions before the view closes
126 - (void)viewWillClose
128 [[adium contactController] unregisterListObjectObserver:self];
129 [[adium notificationCenter] removeObserver:self];
131 [accountArray release]; accountArray = nil;
132 [requiredHeightDict release]; requiredHeightDict = nil;
137 [accountArray release];
143 * @brief Account status changed.
145 * Disable the service menu and user name field for connected accounts
147 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
149 if ([inObject isKindOfClass:[AIAccount class]]) {
150 if ([inModifiedKeys containsObject:@"Online"] ||
151 [inModifiedKeys containsObject:@"Enabled"] ||
152 [inModifiedKeys containsObject:@"Connecting"] ||
153 [inModifiedKeys containsObject:@"Waiting to Reconnect"] ||
154 [inModifiedKeys containsObject:@"Disconnecting"] ||
155 [inModifiedKeys containsObject:@"ConnectionProgressString"] ||
156 [inModifiedKeys containsObject:@"ConnectionProgressPercent"] ||
157 [inModifiedKeys containsObject:@"IdleSince"] ||
158 [inModifiedKeys containsObject:@"StatusState"]) {
160 //Refresh this account in our list
161 int accountRow = [accountArray indexOfObject:inObject];
162 if (accountRow >= 0 && accountRow < [accountArray count]) {
163 [tableView_accountList setNeedsDisplayInRect:[tableView_accountList rectOfRow:accountRow]];
164 // Update the height of the row.
165 [self calculateHeightForRow:accountRow];
166 [tableView_accountList noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndex:accountRow]];
169 //Update our account overview
170 [self updateAccountOverview];
177 //Actions --------------------------------------------------------------------------------------------------------------
180 * @brief Create a new account
182 * Called when a service type is selected from the Add menu
184 - (IBAction)selectServiceType:(id)sender
186 AIService *service = [sender representedObject];
187 AIAccount *account = [[adium accountController] createAccountWithService:service
188 UID:[service defaultUserName]];
190 [AIEditAccountWindowController editAccount:account
191 onWindow:[[self view] window]
192 notifyingTarget:self];
195 - (void)editAccount:(AIAccount *)inAccount
197 [AIEditAccountWindowController editAccount:inAccount
198 onWindow:[[self view] window]
199 notifyingTarget:self];
203 * @brief Edit the currently selected account using <tt>AIEditAccountWindowController</tt>
205 - (IBAction)editSelectedAccount:(id)sender
207 int selectedRow = [tableView_accountList selectedRow];
208 if (selectedRow >= 0 && selectedRow < [accountArray count]) {
209 [self editAccount:[accountArray objectAtIndex:selectedRow]];
214 * @brief Handle a double click within our table
216 * Ignore double clicks on the enable/disable checkbox
218 - (void)doubleClickInTableView:(id)sender
220 if (!(NSPointInRect([tableView_accountList convertPoint:[[NSApp currentEvent] locationInWindow] fromView:nil],
221 [tableView_accountList rectOfColumn:[tableView_accountList columnWithIdentifier:@"enabled"]]))) {
222 [self editSelectedAccount:sender];
227 * @brief Editing of an account completed
229 - (void)editAccountSheetDidEndForAccount:(AIAccount *)inAccount withSuccess:(BOOL)successful
231 BOOL existingAccount = ([[[adium accountController] accounts] containsObject:inAccount]);
233 if (!existingAccount && successful) {
234 //New accounts need to be added to our account list once they're configured
235 [[adium accountController] addAccount:inAccount];
237 //Scroll the new account visible so that the user can see we added it
238 [tableView_accountList scrollRowToVisible:[accountArray indexOfObject:inAccount]];
240 //Put new accounts online by default
241 [inAccount setPreference:[NSNumber numberWithBool:YES] forKey:@"Online" group:GROUP_ACCOUNT_STATUS];
246 * @brief Delete the selected account
248 * Prompts for confirmation first
250 - (IBAction)deleteAccount:(id)sender
252 int index = [tableView_accountList selectedRow];
254 if (index >= 0 && index < [accountArray count])
255 [[[adium accountController] deleteAccount:[accountArray objectAtIndex:index]] beginSheetModalForWindow:[[self view] window]];
258 //Account List ---------------------------------------------------------------------------------------------------------
259 #pragma mark Account List
261 * @brief Configure the account list table
263 - (void)configureAccountList
265 AIImageTextCell *cell;
266 NSRect oldFrame, newFrame;
268 //Setup our edit button, keeping its right side in the same location
269 oldFrame = [button_editAccount frame];
270 [button_editAccount setTitle:AILocalizedStringFromTable(@"Edit", @"Buttons", "Verb 'edit' on a button")];
271 [button_editAccount sizeToFit];
272 newFrame = [button_editAccount frame];
273 if (newFrame.size.width < oldFrame.size.width) newFrame.size.width = oldFrame.size.width;
274 newFrame.origin.x = oldFrame.origin.x + oldFrame.size.width - newFrame.size.width;
275 [button_editAccount setFrame:newFrame];
277 //Configure our table view
278 [tableView_accountList setTarget:self];
279 [tableView_accountList setDoubleAction:@selector(doubleClickInTableView:)];
280 [tableView_accountList setIntercellSpacing:NSMakeSize(MINIMUM_CELL_SPACING, MINIMUM_CELL_SPACING)];
282 //Enable dragging of accounts
283 [tableView_accountList registerForDraggedTypes:[NSArray arrayWithObjects:ACCOUNT_DRAG_TYPE,nil]];
285 //Custom vertically-centered text cell for account names
286 cell = [[AIImageTextCell alloc] init];
287 [cell setFont:[NSFont boldSystemFontOfSize:13]];
288 [[tableView_accountList tableColumnWithIdentifier:@"name"] setDataCell:cell];
289 [cell setLineBreakMode:NSLineBreakByWordWrapping];
292 cell = [[AIVerticallyCenteredTextCell alloc] init];
293 [cell setFont:[NSFont systemFontOfSize:13]];
294 [cell setAlignment:NSRightTextAlignment];
295 [[tableView_accountList tableColumnWithIdentifier:@"status"] setDataCell:cell];
298 [tableView_accountList sizeToFit];
300 //Observe changes to the account list
301 [[adium notificationCenter] addObserver:self
302 selector:@selector(accountListChanged:)
303 name:Account_ListChanged
305 [self accountListChanged:nil];
307 //Observe accounts so we can display accurate status
308 [[adium contactController] registerListObjectObserver:self];
312 * @brief Account list changed, refresh our table
314 - (void)accountListChanged:(NSNotification *)notification
316 //Update our list of accounts
317 [accountArray release];
318 accountArray = [[[adium accountController] accounts] retain];
320 //Refresh the account table
321 [tableView_accountList reloadData];
322 [self updateControlAvailability];
323 [self updateAccountOverview];
324 [self calculateAllHeights];
328 * @brief Status icons changed, refresh our table
330 - (void)iconPackDidChange:(NSNotification *)notification
332 [tableView_accountList reloadData];
336 * @brief Update our account overview
338 * The overview indicates the total number of accounts and the number which are online.
340 - (void)updateAccountOverview
342 NSString *accountOverview;
343 int accountArrayCount = [accountArray count];
345 if (accountArrayCount == 0) {
346 accountOverview = AILocalizedString(@"Click the + to add a new account","Instructions on how to add an account when none are present");
349 NSEnumerator *enumerator = [accountArray objectEnumerator];
351 int online = 0, enabled = 0;
353 //Count online accounts
354 while ((account = [enumerator nextObject])) {
355 if ([account online]) online++;
356 if ([account enabled]) enabled++;
360 if ((accountArrayCount == enabled) ||
361 (online == enabled)){
362 accountOverview = [NSString stringWithFormat:AILocalizedString(@"%i accounts, %i online","Overview of total and online accounts"),
366 accountOverview = [NSString stringWithFormat:AILocalizedString(@"%i accounts, %i enabled, %i online","Overview of total, enabled, and online accounts"),
372 accountOverview = AILocalizedString(@"Check a box to enable an account","Instructions for enabling an account");
376 [textField_overview setStringValue:accountOverview];
380 * @brief Update control availability based on list selection
382 - (void)updateControlAvailability
384 BOOL selection = ([tableView_accountList selectedRow] != -1);
386 [button_editAccount setEnabled:selection];
387 [button_deleteAccount setEnabled:selection];
391 * @brief Returns the status string associated with the account
393 * Returns a connection status if connecting, or an error if disconnected with an error
395 - (NSString *)statusMessageForAccount:(AIAccount *)account
397 NSString *statusMessage = nil;
399 if ([account statusObjectForKey:@"ConnectionProgressString"] && [[account statusObjectForKey:@"Connecting"] boolValue]) {
400 // Connection status if we're currently connecting, with the percent at the end
401 statusMessage = [[account statusObjectForKey:@"ConnectionProgressString"] stringByAppendingFormat:@" (%2.f%%)", [[account statusObjectForKey:@"ConnectionProgressPercent"] floatValue]*100.0];
402 } else if ([account lastDisconnectionError] && ![[account statusObjectForKey:@"Online"] boolValue] && ![[account statusObjectForKey:@"Connecting"] boolValue]) {
403 // If there's an error and we're not online and not connecting
404 NSMutableString *returnedMessage = [[[account lastDisconnectionError] mutableCopy] autorelease];
406 // Replace the LibPurple error prefix
407 [returnedMessage replaceOccurrencesOfString:@"Could not establish a connection with the server:\n"
408 withString:@"Error: "
409 options:NSLiteralSearch
410 range:NSMakeRange(0, [returnedMessage length])];
411 // Remove newlines from the error message
412 [returnedMessage convertNewlinesToSlashes];
414 statusMessage = returnedMessage;
417 return statusMessage;
421 * @brief Calculates the height of a given row and stores it
423 - (void)calculateHeightForRow:(int)row
425 // Make sure this is a valid row.
426 if (row < 0 || row >= [accountArray count]) {
430 AIAccount *account = [accountArray objectAtIndex:row];
431 float necessaryHeight = MINIMUM_ROW_HEIGHT;
433 // If there's a status message, let's try size to fit it.
434 if ([self statusMessageForAccount:account]) {
435 NSTableColumn *tableColumn = [tableView_accountList tableColumnWithIdentifier:@"name"];
437 [self tableView:tableView_accountList willDisplayCell:[tableColumn dataCell] forTableColumn:tableColumn row:row];
439 // Main string (account name)
440 NSDictionary *mainStringAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont boldSystemFontOfSize:13], NSFontAttributeName, nil];
441 NSAttributedString *mainTitle = [[NSAttributedString alloc] initWithString:([[account formattedUID] length] ? [account formattedUID] : NEW_ACCOUNT_DISPLAY_TEXT)
442 attributes:mainStringAttributes];
444 // Substring (the status message)
445 NSDictionary *subStringAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont systemFontOfSize:10], NSFontAttributeName, nil];
446 NSAttributedString *subStringTitle = [[NSAttributedString alloc] initWithString:[self statusMessageForAccount:account]
447 attributes:subStringAttributes];
449 // Both heights combined, with spacing in-between
450 float combinedHeight = [mainTitle heightWithWidth:[tableColumn width]] + [subStringTitle heightWithWidth:[tableColumn width]] + MINIMUM_CELL_SPACING;
452 // Make sure we're not down-sizing
453 if (combinedHeight > necessaryHeight) {
454 necessaryHeight = combinedHeight;
457 [subStringTitle release];
461 // Cache the height value
462 [requiredHeightDict setObject:[NSNumber numberWithFloat:necessaryHeight]
463 forKey:[NSNumber numberWithInt:row]];
467 * @brief Calculates the height of all rows
469 - (void)calculateAllHeights
473 [requiredHeightDict removeAllObjects];
475 for (accountNumber = 0; accountNumber < [accountArray count]; accountNumber++) {
476 [self calculateHeightForRow:accountNumber];
481 //Account List Table Delegate ------------------------------------------------------------------------------------------
482 #pragma mark Account List (Table Delegate)
484 * @brief Delete the selected row
486 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
488 [self deleteAccount:nil];
492 * @brief Number of rows in the table
494 - (int)numberOfRowsInTableView:(NSTableView *)tableView
496 return [accountArray count];
500 * @brief Table values
502 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
504 if (row < 0 || row >= [accountArray count]) {
508 NSString *identifier = [tableColumn identifier];
509 AIAccount *account = [accountArray objectAtIndex:row];
511 if ([identifier isEqualToString:@"service"]) {
512 return [[AIServiceIcons serviceIconForObject:account
513 type:AIServiceIconLarge
514 direction:AIIconNormal] imageByScalingToSize:NSMakeSize(MINIMUM_ROW_HEIGHT-2, MINIMUM_ROW_HEIGHT-2)
515 fraction:([account enabled] ?
519 } else if ([identifier isEqualToString:@"name"]) {
520 return [[account formattedUID] length] ? [account formattedUID] : NEW_ACCOUNT_DISPLAY_TEXT;
522 } else if ([identifier isEqualToString:@"status"]) {
525 if ([account enabled]) {
526 if ([[account statusObjectForKey:@"Connecting"] boolValue]) {
527 title = AILocalizedString(@"Connecting",nil);
528 } else if ([[account statusObjectForKey:@"Disconnecting"] boolValue]) {
529 title = AILocalizedString(@"Disconnecting",nil);
530 } else if ([[account statusObjectForKey:@"Online"] boolValue]) {
531 title = AILocalizedString(@"Online",nil);
532 } else if ([account statusObjectForKey:@"Waiting to Reconnect"]) {
533 title = AILocalizedString(@"Reconnecting", @"Used when the account will perform an automatic reconnection after a certain period of time.");
535 title = [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_OFFLINE];
539 title = AILocalizedString(@"Disabled",nil);
544 } else if ([identifier isEqualToString:@"statusicon"]) {
546 return [AIStatusIcons statusIconForListObject:account type:AIStatusIconList direction:AIIconNormal];
548 } else if ([identifier isEqualToString:@"enabled"]) {
556 * @brief Configure the height of each account for error messages if necessary
558 - (float)tableView:(NSTableView *)tableView heightOfRow:(int)row
560 // We should probably have this value cached.
561 float necessaryHeight = MINIMUM_ROW_HEIGHT;
563 NSNumber *cachedHeight = [requiredHeightDict objectForKey:[NSNumber numberWithInt:row]];
565 necessaryHeight = [cachedHeight floatValue];
568 return necessaryHeight;
572 * @brief Configure cells before display
574 - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
576 // Make sure this row actually exists
577 if (row < 0 || row >= [accountArray count]) {
581 NSString *identifier = [tableColumn identifier];
582 AIAccount *account = [accountArray objectAtIndex:row];
584 if ([identifier isEqualToString:@"enabled"]) {
585 [cell setState:([account enabled] ? NSOnState : NSOffState)];
587 } else if ([identifier isEqualToString:@"name"]) {
588 [cell setEnabled:[account enabled]];
590 // Update the subString with our current status message (if it exists);
591 [cell setSubString:[self statusMessageForAccount:account]];
593 } else if ([identifier isEqualToString:@"status"]) {
594 [cell setEnabled:([[account statusObjectForKey:@"Connecting"] boolValue] ||
595 [account statusObjectForKey:@"Waiting to Reconnect"] ||
596 [[account statusObjectForKey:@"Disconnecting"] boolValue] ||
597 [[account statusObjectForKey:@"Online"] boolValue])];
603 * @brief Handle a clicked active/inactive checkbox
605 * Checking the box both takes the account online and sets it to autoconnect. Unchecking it does the opposite.
607 - (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(int)row
609 if (row >= 0 && row < [accountArray count] && [[tableColumn identifier] isEqualToString:@"enabled"]) {
610 [[accountArray objectAtIndex:row] setEnabled:[(NSNumber *)object boolValue]];
617 - (BOOL)tableView:(NSTableView *)tv writeRows:(NSArray*)rows toPasteboard:(NSPasteboard*)pboard
619 tempDragAccount = [accountArray objectAtIndex:[[rows objectAtIndex:0] intValue]];
621 [pboard declareTypes:[NSArray arrayWithObject:ACCOUNT_DRAG_TYPE] owner:self];
622 [pboard setString:@"Account" forType:ACCOUNT_DRAG_TYPE];
628 * @brief Drag validate
630 - (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)op
632 if (op == NSTableViewDropAbove && row != -1) {
633 return NSDragOperationPrivate;
635 return NSDragOperationNone;
640 * @brief Drag complete
642 - (BOOL)tableView:(NSTableView*)tv acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)op
644 NSString *avaliableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObject:ACCOUNT_DRAG_TYPE]];
646 if ([avaliableType isEqualToString:@"AIAccount"]) {
647 int newIndex = [[adium accountController] moveAccount:tempDragAccount toIndex:row];
648 [tableView_accountList selectRow:newIndex byExtendingSelection:NO];
657 * @brief Selection change
659 - (void)tableViewSelectionDidChange:(NSNotification *)notification
661 [self updateControlAvailability];
665 * @brief Set up KFTypeSelectTableView
667 * Only search the "name" column.
669 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
671 [tableView setSearchColumnIdentifiers:[NSSet setWithObject:@"name"]];