Unescape the HREF attribute's text before passing it to NSURL which does not expect...
[adiumx.git] / Source / AIAccountListPreferences.m
bloba8d126906a9000d0c0f4f8f91b0f59220d8ec0d0
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
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.
8  * 
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.
12  * 
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.
15  */
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 <AIUtilities/AIStringAdditions.h>
29 #import <AIUtilities/AIDateFormatterAdditions.h>
30 #import <AIUtilities/AIMenuAdditions.h>
31 #import <Adium/AIAccount.h>
32 #import <Adium/AIAccountMenu.h>
33 #import <Adium/AIListObject.h>
34 #import <Adium/AIService.h>
35 #import <Adium/AIStatusMenu.h>
36 #import <Adium/AIServiceIcons.h>
37 #import <Adium/AIServiceMenu.h>
38 #import <Adium/AIStatusIcons.h>
39 #import <AIUtilities/AIAttributedStringAdditions.h>
40 #import "KFTypeSelectTableView.h"
42 #define MINIMUM_ROW_HEIGHT                              34
43 #define MINIMUM_CELL_SPACING                     4
45 #define ACCOUNT_DRAG_TYPE                               @"AIAccount"                            //ID for an account drag
47 #define NEW_ACCOUNT_DISPLAY_TEXT                AILocalizedString(@"<New Account>", "Placeholder displayed as the name of a new account")
49 @interface AIAccountListPreferences (PRIVATE)
50 - (void)configureAccountList;
51 - (void)accountListChanged:(NSNotification *)notification;
53 - (void)calculateHeightForRow:(int)row;
54 - (void)calculateAllHeights;
56 - (void)updateReconnectTime:(NSTimer *)timer;
57 @end
59 /*!
60  * @class AIAccountListPreferences
61  * @brief Shows a list of accounts and provides for management of them
62  */
63 @implementation AIAccountListPreferences
65 /*!
66  * @brief Preference pane properties
67  */
68 - (NSString *)paneIdentifier
70         return @"Accounts";
72 - (NSString *)paneName{
73     return AILocalizedString(@"Accounts","Accounts preferences label");
75 - (NSString *)nibName{
76     return @"AccountListPreferences";
78 - (NSImage *)paneIcon
80         return [NSImage imageNamed:@"pref-accounts" forClass:[self class]];
83 /*!
84  * @brief Configure the view initially
85  */
86 - (void)viewDidLoad
88         //Configure the account list
89         [self configureAccountList];
90         [self updateAccountOverview];
91         
92         //Build the 'add account' menu of each available service
93         NSMenu  *serviceMenu = [AIServiceMenu menuOfServicesWithTarget:self 
94                                                                                                 activeServicesOnly:NO
95                                                                                                    longDescription:YES
96                                                                                                                         format:AILocalizedString(@"%@",nil)];
97         [serviceMenu setAutoenablesItems:YES];
98         
99         //Indent each item in the service menu one level
100         NSEnumerator    *enumerator = [[serviceMenu itemArray] objectEnumerator];
101         NSMenuItem              *menuItem;
102         while ((menuItem = [enumerator nextObject])) {
103                 [menuItem setIndentationLevel:[menuItem indentationLevel]+1];
104         }
106         //Add a label to the top of the menu to clarify why we're showing this list of services
107         [serviceMenu insertItemWithTitle:AILocalizedString(@"Add an account for:",nil)
108                                                           action:NULL
109                                            keyEquivalent:@""
110                                                          atIndex:0];
111         
112         //Assign the menu
113         [button_newAccount setMenu:serviceMenu];
114         
115         //Set ourselves up for Account Menus
116         accountMenu_options = [[AIAccountMenu accountMenuWithDelegate:self
117                                                                                                           submenuType:AIAccountOptionsSubmenu
118                                                                                                    showTitleVerbs:NO] retain];
119         
120         accountMenu_status = [[AIAccountMenu accountMenuWithDelegate:self
121                                                                                                          submenuType:AIAccountStatusSubmenu
122                                                                                                   showTitleVerbs:NO] retain];
124         //Observe status icon pack changes
125         [[adium notificationCenter] addObserver:self
126                                                                    selector:@selector(iconPackDidChange:)
127                                                                            name:AIStatusIconSetDidChangeNotification
128                                                                          object:nil];
129         
130         //Observe service icon pack changes
131         [[adium notificationCenter] addObserver:self
132                                                                    selector:@selector(iconPackDidChange:)
133                                                                            name:AIServiceIconSetDidChangeNotification
134                                                                          object:nil];
135         
136         // Start updating the reconnect time if an account is already reconnecting.     
137         [self updateReconnectTime:nil];
141  * @brief Perform actions before the view closes
142  */
143 - (void)viewWillClose
145         [[adium contactController] unregisterListObjectObserver:self];
146         [[adium notificationCenter] removeObserver:self];
147         
148         [accountArray release]; accountArray = nil;
149         [requiredHeightDict release]; requiredHeightDict = nil;
150         [accountMenu_options release]; accountMenu_options = nil;
151         [accountMenu_status release]; accountMenu_status = nil;
152         
153         // Cancel our auto-refreshing reconnect countdown.
154         [reconnectTimeUpdater invalidate];
155         [reconnectTimeUpdater release]; reconnectTimeUpdater = nil;
158 - (void)dealloc
160         [accountArray release];
161         [super dealloc];
165  * @brief Account status changed.
167  * Disable the service menu and user name field for connected accounts
168  */
169 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
171         if ([inObject isKindOfClass:[AIAccount class]]) {
172                 if ([inModifiedKeys containsObject:@"Online"] ||
173                         [inModifiedKeys containsObject:@"Enabled"] ||
174                    [inModifiedKeys containsObject:@"Connecting"] ||
175                    [inModifiedKeys containsObject:@"Waiting to Reconnect"] ||
176                    [inModifiedKeys containsObject:@"Disconnecting"] ||
177                    [inModifiedKeys containsObject:@"ConnectionProgressString"] ||
178                    [inModifiedKeys containsObject:@"ConnectionProgressPercent"] ||
179                    [inModifiedKeys containsObject:@"Waiting for Network"] ||
180                    [inModifiedKeys containsObject:@"IdleSince"] ||
181                    [inModifiedKeys containsObject:@"StatusState"]) {
183                         //Refresh this account in our list
184                         int accountRow = [accountArray indexOfObject:inObject];
185                         if (accountRow >= 0 && accountRow < [accountArray count]) {
186                                 [tableView_accountList setNeedsDisplayInRect:[tableView_accountList rectOfRow:accountRow]];
187                                 // Update the height of the row.
188                                 [self calculateHeightForRow:accountRow];
189                                 [tableView_accountList noteHeightOfRowsWithIndexesChanged:[NSIndexSet indexSetWithIndex:accountRow]];
191                                 // If necessary, update our reconnection display time.
192                                 if (!reconnectTimeUpdater) {
193                                         [self updateReconnectTime:nil];
194                                 }
195                         }
196                         
197                         //Update our account overview
198                         [self updateAccountOverview];
199                 }
200         }
201     
202     return nil;
205 //Actions --------------------------------------------------------------------------------------------------------------
206 #pragma mark Actions
208  * @brief Create a new account
210  * Called when a service type is selected from the Add menu
211  */
212 - (IBAction)selectServiceType:(id)sender
214         AIService       *service = [sender representedObject];
215         AIAccount       *account = [[adium accountController] createAccountWithService:service
216                                                                                                                                                    UID:[service defaultUserName]];
218         [AIEditAccountWindowController editAccount:account
219                                                                           onWindow:[[self view] window]
220                                                            notifyingTarget:self];
223 - (void)editAccount:(AIAccount *)inAccount
225         [AIEditAccountWindowController editAccount:inAccount
226                                                                           onWindow:[[self view] window]
227                                                            notifyingTarget:self];       
231  * @brief Edit the currently selected account using <tt>AIEditAccountWindowController</tt>
232  */
233 - (IBAction)editSelectedAccount:(id)sender
235     int selectedRow = [tableView_accountList selectedRow];
236         if ([tableView_accountList numberOfSelectedRows] == 1 && selectedRow >= 0 && selectedRow < [accountArray count]) {
237                 [self editAccount:[accountArray objectAtIndex:selectedRow]];
238     }
242  * @brief Handle a double click within our table
244  * Ignore double clicks on the enable/disable checkbox
245  */
246 - (void)doubleClickInTableView:(id)sender
248         if (!(NSPointInRect([tableView_accountList convertPoint:[[NSApp currentEvent] locationInWindow] fromView:nil],
249                                                 [tableView_accountList rectOfColumn:[tableView_accountList columnWithIdentifier:@"enabled"]]))) {
250                 [self editSelectedAccount:sender];
251         }
255  * @brief Editing of an account completed
256  */
257 - (void)editAccountSheetDidEndForAccount:(AIAccount *)inAccount withSuccess:(BOOL)successful
259         BOOL existingAccount = ([[[adium accountController] accounts] containsObject:inAccount]);
260         
261         if (!existingAccount && successful) {
262                 //New accounts need to be added to our account list once they're configured
263                 [[adium accountController] addAccount:inAccount];
265                 //Scroll the new account visible so that the user can see we added it
266                 [tableView_accountList scrollRowToVisible:[accountArray indexOfObject:inAccount]];
267                 
268                 //Put new accounts online by default
269                 [inAccount setPreference:[NSNumber numberWithBool:YES] forKey:@"Online" group:GROUP_ACCOUNT_STATUS];
270         } else if (existingAccount && successful && [inAccount enabled]) {
271                 //If the user edited an account that is "reconnecting" or "connecting", disconnect it and try to reconnect.
272                 if ([[inAccount statusObjectForKey:@"Connecting"] boolValue] ||
273                         [inAccount statusObjectForKey:@"Waiting to Reconnect"]) {
274                         // Stop connecting or stop waiting to reconnect.
275                         [inAccount setShouldBeOnline:NO];
276                         // Connect it.
277                         [inAccount setShouldBeOnline:YES];
278                 }
279         }       
283  * @brief Delete the selected account
285  * Prompts for confirmation first
286  */
287 - (IBAction)deleteAccount:(id)sender
289     int index = [tableView_accountList selectedRow];
291     if ([tableView_accountList numberOfSelectedRows] == 1 && index >= 0 && index < [accountArray count])
292                 [[[adium accountController] deleteAccount:[accountArray objectAtIndex:index]] beginSheetModalForWindow:[[self view] window]];
296 * @brief Toggles an account online or offline.
297  */
298 - (void)toggleShouldBeOnline:(id)sender
300         AIAccount               *account = [sender representedObject];
301         if (![account enabled])
302                 [account setEnabled:YES];
303         else
304                 [account toggleOnline];
307 #pragma mark AIAccountMenu Delegates
310 * @brief AIAccountMenu delieate method
311  */
312 - (void)accountMenu:(AIAccountMenu *)inAccountMenu didRebuildMenuItems:(NSArray *)menuItems {
313         return;
317 * @brief AIAccountMenu delegate method -- this allows disabled items to have menus.
318  */
319 - (BOOL)accountMenu:(AIAccountMenu *)inAccountMenu shouldIncludeAccount:(AIAccount *)inAccount
321         return YES;
324 //Account List ---------------------------------------------------------------------------------------------------------
325 #pragma mark Account List
327  * @brief Configure the account list table
328  */
329 - (void)configureAccountList
331     AIImageTextCell             *cell;
332         NSRect                          oldFrame, newFrame;
333         
334         //Setup our edit button, keeping its right side in the same location
335         oldFrame = [button_editAccount frame];
336         [button_editAccount setTitle:AILocalizedStringFromTable(@"Edit", @"Buttons", "Verb 'edit' on a button")];
337         [button_editAccount sizeToFit];
338         newFrame = [button_editAccount frame];
339         if (newFrame.size.width < oldFrame.size.width) newFrame.size.width = oldFrame.size.width;
340         newFrame.origin.x = oldFrame.origin.x + oldFrame.size.width - newFrame.size.width;
341         [button_editAccount setFrame:newFrame];
343         //Configure our table view
344         [tableView_accountList setTarget:self];
345         [tableView_accountList setDoubleAction:@selector(doubleClickInTableView:)];
346         [tableView_accountList setIntercellSpacing:NSMakeSize(MINIMUM_CELL_SPACING, MINIMUM_CELL_SPACING)];
348         //Enable dragging of accounts
349         [tableView_accountList registerForDraggedTypes:[NSArray arrayWithObjects:ACCOUNT_DRAG_TYPE,nil]];
350         
351     //Custom vertically-centered text cell for account names
352     cell = [[AIImageTextCell alloc] init];
353     [cell setFont:[NSFont boldSystemFontOfSize:13]];
354         [cell setDrawsImageAfterMainString:YES];
355     [[tableView_accountList tableColumnWithIdentifier:@"name"] setDataCell:cell];
356         [cell setLineBreakMode:NSLineBreakByWordWrapping];
357         [cell release];
359     cell = [[AIImageTextCell alloc] init];
360     [cell setFont:[NSFont systemFontOfSize:13]];
361     [cell setAlignment:NSRightTextAlignment];
362         [cell setLineBreakMode:NSLineBreakByWordWrapping];
363     [[tableView_accountList tableColumnWithIdentifier:@"status"] setDataCell:cell];
364         [cell release];
365     
366         [tableView_accountList sizeToFit];
368         //Observe changes to the account list
369     [[adium notificationCenter] addObserver:self
370                                                                    selector:@selector(accountListChanged:) 
371                                                                            name:Account_ListChanged 
372                                                                          object:nil];
373         [self accountListChanged:nil];
374         
375         //Observe accounts so we can display accurate status
376     [[adium contactController] registerListObjectObserver:self];
380  * @brief Account list changed, refresh our table
381  */
382 - (void)accountListChanged:(NSNotification *)notification
384     //Update our list of accounts
385     [accountArray release];
386         accountArray = [[[adium accountController] accounts] retain];
388         //Refresh the account table
389         [tableView_accountList reloadData];
390         [self updateControlAvailability];
391         [self updateAccountOverview];
392         [self calculateAllHeights];
396  * @brief Returns the status menu associated with several rows
397  */
398 - (NSMenu *)menuForRowIndexes:(NSIndexSet *)indexes
400         NSMenu                  *statusMenu = nil, *optionsMenu = [[[NSMenu alloc] init] autorelease];
401         NSMenuItem              *statusMenuItem = nil;
402         NSArray                 *accounts = [accountArray objectsAtIndexes:indexes];
403         AIAccount               *account;
404         NSEnumerator    *enumerator = [accounts objectEnumerator];
405         BOOL                    atLeastOneDisabledAccount = NO, atLeastOneOfflineAccount = NO;
406         
407         // Check the accounts' enabled/disabled and online/offline status.
408         while ((account = [enumerator nextObject])) {
409                 if (![account enabled])
410                         atLeastOneDisabledAccount = YES;
411                 
412                 if (![account online] && ![[account statusObjectForKey:@"Connecting"] boolValue])
413                         atLeastOneOfflineAccount = YES;
414                 
415                 if (atLeastOneOfflineAccount && atLeastOneDisabledAccount)
416                         break;
417         }
418         
419         statusMenuItem = [optionsMenu addItemWithTitle:AILocalizedString(@"Set Status", "Used in the context menu for the accounts list for the sub menu to set status in.")
420                                                                                         target:nil
421                                                                                         action:nil
422                                                                          keyEquivalent:@""];
424         statusMenu = [AIStatusMenu staticStatusStatesMenuNotifyingTarget:self
425                                                                                                                 selector:@selector(updateAccountsForStatus:)];
426         [statusMenuItem setSubmenu:statusMenu];
427         
428         //If any accounts are offline, present the option to connect them all.
429         if (atLeastOneOfflineAccount) {
430                 [optionsMenu addItemWithTitle:AILocalizedString(@"Connect",nil)
431                                                            target:self
432                                                            action:@selector(toggleOnlineForAccounts:)
433                                                 keyEquivalent:@""
434                                         representedObject:[NSDictionary dictionaryWithObjectsAndKeys:accounts,@"Accounts",
435                                                 [NSNumber numberWithBool:YES],@"Connect",nil]];
436         }
437         [optionsMenu addItemWithTitle:AILocalizedString(@"Disconnect",nil)
438                                                    target:self
439                                                    action:@selector(toggleOnlineForAccounts:)
440                                         keyEquivalent:@""
441                                 representedObject:[NSDictionary dictionaryWithObjectsAndKeys:accounts,@"Accounts",
442                                         [NSNumber numberWithBool:NO],@"Connect",nil]];
443         
444         [optionsMenu addItem:[NSMenuItem separatorItem]];
445         
446         // If any accounts are disable,d show the option to enable them.
447         if (atLeastOneDisabledAccount) {
448                 [optionsMenu addItemWithTitle:AILocalizedString(@"Enable",nil)
449                                                            target:self
450                                                            action:@selector(toggleEnabledForAccounts:)
451                                                 keyEquivalent:@""
452                                         representedObject:[NSDictionary dictionaryWithObjectsAndKeys:accounts,@"Accounts",
453                                                 [NSNumber numberWithBool:YES],@"Enable",nil]];
454                 
455         }
456         [optionsMenu addItemWithTitle:AILocalizedString(@"Disable",nil)
457                                                    target:self
458                                                    action:@selector(toggleEnabledForAccounts:)
459                                         keyEquivalent:@""
460                                 representedObject:[NSDictionary dictionaryWithObjectsAndKeys:accounts,@"Accounts",
461                                         [NSNumber numberWithBool:NO],@"Enable",nil]];
462         
463         return optionsMenu;
467  * @brief Callback for the Connect/Disconnect menu item in a multiple account selection
468  */
469 - (void)toggleOnlineForAccounts:(id)sender
471         NSDictionary            *dict           = [sender representedObject];
472         NSEnumerator            *enumerator     = [[dict objectForKey:@"Accounts"] objectEnumerator];
473         BOOL                            connect         = [[dict objectForKey:@"Connect"] boolValue];
474         AIAccount                       *account;
476         while ((account = [enumerator nextObject])) {
477                 if (![account enabled] && connect)
478                         [account setEnabled:YES];
479                 [account setShouldBeOnline:connect];
480         }
484  * @brief Callback for the Enable/Disable menu item in a multiple account selection
485  */
486 - (void)toggleEnabledForAccounts:(id)sender
488         NSDictionary            *dict           = [sender representedObject];
489         NSEnumerator            *enumerator     = [[dict objectForKey:@"Accounts"] objectEnumerator];
490         BOOL                            enable          = [[dict objectForKey:@"Enable"] boolValue];
491         AIAccount                       *account;
493         while ((account = [enumerator nextObject])) {
494                 [account setEnabled:enable];
495         }       
499  * @brief Callback for the Set Status menu item in a multiple-account selection
500  */
501 - (void)updateAccountsForStatus:(id)sender
503         NSEnumerator    *enumerator     = [[accountArray objectsAtIndexes:[tableView_accountList selectedRowIndexes]] objectEnumerator];
504         AIStatus                *status         = [[sender representedObject] objectForKey:@"AIStatus"];
505         AIAccount               *account;
506         
507         while ((account = [enumerator nextObject])) {
508                 [account setStatusState:status];
509                 
510                 //Enable the account if it isn't currently enabled and this isn't an offline status
511                 if (![account enabled] && [status statusType] != AIOfflineStatusType) {
512                         [account setEnabled:YES];
513                 }
514         }
518 * @brief Callback for the Copy Error Message menu item for an account
519  */
520 - (void)copyStatusMessage:(id)sender
522         NSPasteboard            *generalPasteboard = [NSPasteboard generalPasteboard];
523         
524         [generalPasteboard declareTypes:[NSArray arrayWithObject:NSStringPboardType]
525                                                           owner:nil];
526         [generalPasteboard setString:[self statusMessageForAccount:[sender representedObject]]
527                                                  forType:NSStringPboardType];
531  * @brief Returns the status menu associated with a particular row
532  */
533 - (NSMenu *)menuForRow:(int)row
535         if (row >= 0 && row < [accountArray count]) {
536                 AIAccount               *account = [accountArray objectAtIndex:row];
537                 NSMenu                  *optionsMenu = [[[NSMenu alloc] init] autorelease];
538                 NSMenu                  *accountOptionsMenu = [[accountMenu_options menuItemForAccount:account] submenu];
540                 NSMenuItem      *statusMenuItem = [optionsMenu addItemWithTitle:AILocalizedString(@"Set Status", "Used in the context menu for the accounts list for the sub menu to set status in.")
541                                                                                                                          target:nil
542                                                                                                                          action:nil
543                                                                                                           keyEquivalent:@""];
545                 //We can't put the submenu into our menu directly or otherwise modify the accountMenu_status, as we may want to use it again
546                 [statusMenuItem setSubmenu:[[[[accountMenu_status menuItemForAccount:account] submenu] copy] autorelease]];
547                 
548                 if (![account online] && ![[account statusObjectForKey:@"Connecting"] boolValue] && [self statusMessageForAccount:account]) {
549                         [optionsMenu addItemWithTitle:AILocalizedString(@"Copy Error Message","Menu Item for the context menu of an account in the accounts list")
550                                                                    target:self
551                                                                    action:@selector(copyStatusMessage:)
552                                                         keyEquivalent:@""
553                                                 representedObject:account];
554                 }
555                 
556                 if ([[statusMenuItem submenu] numberOfItems] >= 2) {
557                         //Remove the 'Disable' item
558                         [[statusMenuItem submenu] removeItemAtIndex:([[statusMenuItem submenu] numberOfItems] - 1)];
559                         
560                         //And remove the separator above it
561                         [[statusMenuItem submenu] removeItemAtIndex:([[statusMenuItem submenu] numberOfItems] - 1)];
562                 }
563                 
564                 //Connect or disconnect the account. Enabling a disabled account will connect it, so this is only valid for non-disabled accounts.
565                 //Only online & connecting can be "Disconnected"; those offline or waiting to reconnect can be "Connected"
566                 [optionsMenu addItemWithTitle:(([account online] || [[account statusObjectForKey:@"Connecting"] boolValue]) ?
567                                                                            AILocalizedString(@"Disconnect",nil) :
568                                                                            AILocalizedString(@"Connect",nil))
569                                                            target:self
570                                                            action:@selector(toggleShouldBeOnline:)
571                                                 keyEquivalent:@""
572                                         representedObject:account];
573                                 
574                 //Add a separator if we have any items shown so far
575                 [optionsMenu addItem:[NSMenuItem separatorItem]];
576                 
577                 //Add account options
578                 NSMenuItem *menuItem;
579                 NSEnumerator *enumerator = [[accountOptionsMenu itemArray] objectEnumerator];
580                 while ((menuItem = [enumerator nextObject])) {
581                         //Use copies of the menu items rather than moving the actual items, as we may want to use them again
582                         [optionsMenu addItem:[[menuItem copy] autorelease]];
583                 }
585                 return optionsMenu;
586         }
587         
588         return nil;
592  * @brief Updates reconnecting time where necessary.
593  */
594 - (void)updateReconnectTime:(NSTimer *)timer
596         int                             accountRow;
597         BOOL                    moreUpdatesNeeded = NO;
599         for (accountRow = 0; accountRow < [accountArray count]; accountRow++) {
600                 if ([[accountArray objectAtIndex:accountRow] statusObjectForKey:@"Waiting to Reconnect"] != nil) {
601                         [tableView_accountList setNeedsDisplayInRect:[tableView_accountList rectOfRow:accountRow]];
602                         moreUpdatesNeeded = YES;
603                 }
604         }
606         if (moreUpdatesNeeded && reconnectTimeUpdater == nil) {
607                 reconnectTimeUpdater = [[NSTimer scheduledTimerWithTimeInterval:1.0
608                                                                                                                                  target:self 
609                                                                                                                            selector:@selector(updateReconnectTime:) 
610                                                                                                                            userInfo:nil
611                                                                                                                                 repeats:YES] retain];
612         } else if (!moreUpdatesNeeded && reconnectTimeUpdater != nil) {
613                 [reconnectTimeUpdater invalidate];
614                 [reconnectTimeUpdater release]; reconnectTimeUpdater = nil;
615         }
619  * @brief Status icons changed, refresh our table
620  */
621 - (void)iconPackDidChange:(NSNotification *)notification
623         [tableView_accountList reloadData];
627  * @brief Update our account overview
629  * The overview indicates the total number of accounts and the number which are online.
630  */
631 - (void)updateAccountOverview
633         NSString        *accountOverview;
634         int                     accountArrayCount = [accountArray count];
636         if (accountArrayCount == 0) {
637                 accountOverview = AILocalizedString(@"Click the + to add a new account","Instructions on how to add an account when none are present");
639         } else {
640                 NSEnumerator    *enumerator = [accountArray objectEnumerator];
641                 AIAccount               *account;
642                 int                             online = 0, enabled = 0;
643                 
644                 //Count online accounts
645                 while ((account = [enumerator nextObject])) {
646                         if ([account online]) online++;
647                         if ([account enabled]) enabled++;
648                 }
649                 
650                 if (enabled) {
651                         if ((accountArrayCount == enabled) ||
652                                 (online == enabled)){
653                                 accountOverview = [NSString stringWithFormat:AILocalizedString(@"%i accounts, %i online","Overview of total and online accounts"),
654                                         accountArrayCount,
655                                         online];
656                         } else {
657                                 accountOverview = [NSString stringWithFormat:AILocalizedString(@"%i accounts, %i enabled, %i online","Overview of total, enabled, and online accounts"),
658                                         accountArrayCount,
659                                         enabled,
660                                         online];                        
661                         }
662                 } else {
663                         accountOverview = AILocalizedString(@"Check a box to enable an account","Instructions for enabling an account");
664                 }
665         }
667         [textField_overview setStringValue:accountOverview];
671  * @brief Update control availability based on list selection
672  */
673 - (void)updateControlAvailability
675         BOOL    selection = ([tableView_accountList numberOfSelectedRows] == 1 && [tableView_accountList selectedRow] != -1);
677         [button_editAccount setEnabled:selection];
678         [button_deleteAccount setEnabled:selection];
682 * @brief Returns the status string associated with the account
684  * Returns a connection status if connecting, or an error if disconnected with an error
685  */
686 - (NSString *)statusMessageForAccount:(AIAccount *)account
688         NSString *statusMessage = nil;
689         
690         if ([account statusObjectForKey:@"ConnectionProgressString"] && [[account statusObjectForKey:@"Connecting"] boolValue]) {
691                 // Connection status if we're currently connecting, with the percent at the end
692                 statusMessage = [[account statusObjectForKey:@"ConnectionProgressString"] stringByAppendingFormat:@" (%2.f%%)", [[account statusObjectForKey:@"ConnectionProgressPercent"] floatValue]*100.0];
693         } else if ([account lastDisconnectionError] && ![[account statusObjectForKey:@"Online"] boolValue] && ![[account statusObjectForKey:@"Connecting"] boolValue]) {
694                 // If there's an error and we're not online and not connecting
695                 NSMutableString *returnedMessage = [[[account lastDisconnectionError] mutableCopy] autorelease];
696                 
697                 // Replace the LibPurple error prefixes
698                 [returnedMessage replaceOccurrencesOfString:@"Could not establish a connection with the server:\n"
699                                                                                  withString:@""
700                                                                                         options:NSLiteralSearch
701                                                                                           range:NSMakeRange(0, [returnedMessage length])];
702                 [returnedMessage replaceOccurrencesOfString:@"Connection error from Notification server:\n"
703                                                                                  withString:@""
704                                                                                         options:NSLiteralSearch
705                                                                                           range:NSMakeRange(0, [returnedMessage length])];
706                 [returnedMessage replaceOccurrencesOfString:@"Could not connect to authentication server:\n"
707                                                                                  withString:@""
708                                                                                         options:NSLiteralSearch
709                                                                                           range:NSMakeRange(0, [returnedMessage length])];
711                 // Remove newlines from the error message, replace them with spaces
712                 [returnedMessage replaceOccurrencesOfString:@"\n"
713                                                                                  withString:@" "
714                                                                                         options:NSLiteralSearch
715                                                                                           range:NSMakeRange(0, [returnedMessage length])];
716                 
717                 statusMessage = [NSString stringWithFormat:@"%@: %@", AILocalizedString(@"Error", "Prefix to error messages in the Account List."), returnedMessage];
718         }
719         
720         return statusMessage;
724 * @brief Calculates the height of a given row and stores it
725  */
726 - (void)calculateHeightForRow:(int)row
727 {       
728         // Make sure this is a valid row.
729         if (row < 0 || row >= [accountArray count]) {
730                 return;
731         }
732         
733         AIAccount               *account = [accountArray objectAtIndex:row];
734         float                   necessaryHeight = MINIMUM_ROW_HEIGHT;
735         
736         // If there's a status message, let's try size to fit it.
737         if ([self statusMessageForAccount:account]) {
738                 NSTableColumn           *tableColumn = [tableView_accountList tableColumnWithIdentifier:@"name"];
739                 
740                 [self tableView:tableView_accountList willDisplayCell:[tableColumn dataCell] forTableColumn:tableColumn row:row];
741                 
742                 // Main string (account name)
743                 NSDictionary            *mainStringAttributes   = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont boldSystemFontOfSize:13], NSFontAttributeName, nil];
744                 NSAttributedString      *mainTitle = [[NSAttributedString alloc] initWithString:([[account formattedUID] length] ? [account formattedUID] : NEW_ACCOUNT_DISPLAY_TEXT)
745                                                                                                                                                  attributes:mainStringAttributes];
746                 
747                 // Substring (the status message)
748                 NSDictionary            *subStringAttributes    = [NSDictionary dictionaryWithObjectsAndKeys:[NSFont systemFontOfSize:10], NSFontAttributeName, nil];
749                 NSAttributedString      *subStringTitle = [[NSAttributedString alloc] initWithString:[self statusMessageForAccount:account]
750                                                                                                                                                           attributes:subStringAttributes];
751                 
752                 // Both heights combined, with spacing in-between
753                 float combinedHeight = [mainTitle heightWithWidth:[tableColumn width]] + [subStringTitle heightWithWidth:[tableColumn width]] + MINIMUM_CELL_SPACING;
754                 
755                 // Make sure we're not down-sizing
756                 if (combinedHeight > necessaryHeight) {
757                         necessaryHeight = combinedHeight;
758                 }
759                 
760                 [subStringTitle release];
761                 [mainTitle release];
762         }
763         
764         // Cache the height value
765         [requiredHeightDict setObject:[NSNumber numberWithFloat:necessaryHeight]
766                                                    forKey:[NSNumber numberWithInt:row]];
770 * @brief Calculates the height of all rows
771  */
772 - (void)calculateAllHeights
774         int accountNumber;
776         [requiredHeightDict release]; requiredHeightDict = [[NSMutableDictionary alloc] init];
778         for (accountNumber = 0; accountNumber < [accountArray count]; accountNumber++) {
779                 [self calculateHeightForRow:accountNumber];
780         }
784 //Account List Table Delegate ------------------------------------------------------------------------------------------
785 #pragma mark Account List (Table Delegate)
787  * @brief Delete the selected row
788  */
789 - (void)tableViewDeleteSelectedRows:(NSTableView *)tableView
791     [self deleteAccount:nil];
795  * @brief Number of rows in the table
796  */
797 - (int)numberOfRowsInTableView:(NSTableView *)tableView
799         return [accountArray count];
803  * @brief Table values
804  */
805 - (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row
807         if (row < 0 || row >= [accountArray count]) {
808                 return nil;
809         }
810         
811         NSString        *identifier = [tableColumn identifier];
812         AIAccount       *account = [accountArray objectAtIndex:row];
813         
814         if ([identifier isEqualToString:@"service"]) {
815                 return [[AIServiceIcons serviceIconForObject:account
816                                                                                                 type:AIServiceIconLarge
817                                                                                    direction:AIIconNormal] imageByScalingToSize:NSMakeSize(MINIMUM_ROW_HEIGHT-2, MINIMUM_ROW_HEIGHT-2)
818                                                                                                                                                            fraction:([account enabled] ?
819                                                                                                                                                                                  1.0 :
820                                                                                                                                                                                  0.75)];
822         } else if ([identifier isEqualToString:@"name"]) {
823                 return [[account formattedUID] length] ? [account formattedUID] : NEW_ACCOUNT_DISPLAY_TEXT;
824                 
825         } else if ([identifier isEqualToString:@"status"]) {
826                 NSString        *title;
827                 
828                 if ([account enabled]) {
829                         if ([[account statusObjectForKey:@"Connecting"] boolValue]) {
830                                 title = AILocalizedString(@"Connecting",nil);
831                         } else if ([[account statusObjectForKey:@"Disconnecting"] boolValue]) {
832                                 title = AILocalizedString(@"Disconnecting",nil);
833                         } else if ([[account statusObjectForKey:@"Online"] boolValue]) {
834                                 title = AILocalizedString(@"Online",nil);
835                         } else if ([account statusObjectForKey:@"Waiting to Reconnect"]) {
836                                 title = AILocalizedString(@"Reconnecting", @"Used when the account will perform an automatic reconnection after a certain period of time.");
837                         } else if ([[account statusObjectForKey:@"Waiting for Network"] boolValue]) {
838                                 title = AILocalizedString(@"Network Offline", @"Used when the account will connect once the network returns.");
839                         } else {
840                                 title = [[adium statusController] localizedDescriptionForCoreStatusName:STATUS_NAME_OFFLINE];
841                         }
843                 } else {
844                         title = AILocalizedString(@"Disabled",nil);
845                 }
847                 return title;
848                 
849         } else if ([identifier isEqualToString:@"statusicon"]) {
851                 return [AIStatusIcons statusIconForListObject:account type:AIStatusIconList direction:AIIconNormal];
852                 
853         } else if ([identifier isEqualToString:@"enabled"]) {
854                 return nil;
856         }
858         return nil;
861  * @brief Configure the height of each account for error messages if necessary
862  */
863 - (float)tableView:(NSTableView *)tableView heightOfRow:(int)row
865         // We should probably have this value cached.
866         float necessaryHeight = MINIMUM_ROW_HEIGHT;
867         
868         NSNumber *cachedHeight = [requiredHeightDict objectForKey:[NSNumber numberWithInt:row]];
869         if (cachedHeight) {
870                 necessaryHeight = [cachedHeight floatValue];
871         }
872         
873         return necessaryHeight;
877  * @brief Configure cells before display
878  */
879 - (void)tableView:(NSTableView *)tableView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn row:(int)row
881         // Make sure this row actually exists
882         if (row < 0 || row >= [accountArray count]) {
883                 return;
884         }
886         NSString        *identifier = [tableColumn identifier];
887         AIAccount       *account = [accountArray objectAtIndex:row];
888         
889         if ([identifier isEqualToString:@"enabled"]) {
890                 [cell setState:([account enabled] ? NSOnState : NSOffState)];
892         } else if ([identifier isEqualToString:@"name"]) {
893                 if ([account encrypted]) {
894                         [cell setImage:[NSImage imageForSSL]];
895                 } else {
896                         [cell setImage:nil];
897                 }
899                 [cell setImageTextPadding:MINIMUM_CELL_SPACING/2.0];
900                 
901                 [cell setEnabled:[account enabled]];
903                 // Update the subString with our current status message (if it exists);
904                 [cell setSubString:[self statusMessageForAccount:account]];
905                 
906         } else if ([identifier isEqualToString:@"status"]) {
907                 if ([account enabled] && ![[account statusObjectForKey:@"Connecting"] boolValue] && [account statusObjectForKey:@"Waiting to Reconnect"]) {
908                         NSString *format = [NSDateFormatter stringForTimeInterval:[[account statusObjectForKey:@"Waiting to Reconnect"] timeIntervalSinceNow]
909                                                                                                            showingSeconds:YES
910                                                                                                                   abbreviated:YES
911                                                                                                                  approximated:NO];
912                         
913                         [cell setSubString:[NSString stringWithFormat:AILocalizedString(@"...in %@", @"The amount of time until a reconnect occurs. %@ is the formatted time remaining."), format]];
914                 } else {
915                         [cell setSubString:nil];
916                 }
917                 
918                 [cell setEnabled:([[account statusObjectForKey:@"Connecting"] boolValue] ||
919                                                   [account statusObjectForKey:@"Waiting to Reconnect"] ||
920                                                   [[account statusObjectForKey:@"Disconnecting"] boolValue] ||
921                                                   [[account statusObjectForKey:@"Online"] boolValue])];
922         }
923         
927  * @brief Handle a clicked active/inactive checkbox
929  * Checking the box both takes the account online and sets it to autoconnect. Unchecking it does the opposite.
930  */
931 - (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(int)row
933         if (row >= 0 && row < [accountArray count] && [[tableColumn identifier] isEqualToString:@"enabled"]) {
934                 [[accountArray objectAtIndex:row] setEnabled:[(NSNumber *)object boolValue]];
935         }
939  * @brief Drag start
940  */
941 - (BOOL)tableView:(NSTableView *)tv writeRowsWithIndexes:(NSIndexSet*)rows toPasteboard:(NSPasteboard*)pboard
943     tempDragAccounts = [accountArray objectsAtIndexes:rows];
945     [pboard declareTypes:[NSArray arrayWithObject:ACCOUNT_DRAG_TYPE] owner:self];
946     [pboard setString:@"Account" forType:ACCOUNT_DRAG_TYPE];
947     
948     return YES;
952  * @brief Drag validate
953  */
954 - (NSDragOperation)tableView:(NSTableView*)tv validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)op
956     if (op == NSTableViewDropAbove && row != -1) {
957         return NSDragOperationPrivate;
958     } else {
959         return NSDragOperationNone;
960     }
964  * @brief Drag complete
965  */
966 - (BOOL)tableView:(NSTableView*)tv acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)op
968     NSString            *avaliableType = [[info draggingPasteboard] availableTypeFromArray:[NSArray arrayWithObject:ACCOUNT_DRAG_TYPE]];
969         
970     if ([avaliableType isEqualToString:@"AIAccount"]) {
971                 NSEnumerator    *enumerator;
972                 AIAccount               *account;
974                 //Indexes are shifting as we're doing this, so we have to iterate in the right order
975                 //If we're moving accounts to an earlier point in the list, we've got to insert backwards
976                 if ([accountArray indexOfObject:[tempDragAccounts objectAtIndex:0]] >= row) 
977                         enumerator = [tempDragAccounts reverseObjectEnumerator];
978                 else //If we're inserting into a later part of the list, we've got to insert forwards
979                         enumerator = [tempDragAccounts objectEnumerator];
980                 
981                 [tableView_accountList deselectAll:nil];
982                 
983                 while ((account = [enumerator nextObject])) {
984                         [[adium accountController] moveAccount:account toIndex:row];
985                 }
986                 
987                 //Re-select our now-moved accounts
988                 [tableView_accountList selectRowIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange([accountArray indexOfObject:[tempDragAccounts objectAtIndex:0]], [tempDragAccounts count])]
989                                                    byExtendingSelection:NO];
991         return YES;
992     } else {
993         return NO;
994     }
998  * @brief Selection change
999  */
1000 - (void)tableViewSelectionDidChange:(NSNotification *)notification
1002         [self updateControlAvailability];
1005 - (NSMenu *)tableView:(NSTableView *)inTableView menuForEvent:(NSEvent *)theEvent
1007         NSIndexSet      *selectedIndexes        = [inTableView selectedRowIndexes];
1008         int                     mouseRow                        = [inTableView rowAtPoint:[inTableView convertPoint:[theEvent locationInWindow] toView:nil]];
1009         
1010         //Multiple rows selected where the right-clicked row is in the selection
1011         if ([selectedIndexes count] > 1 && [selectedIndexes containsIndex:mouseRow]) {
1012                 //Display a multi-selection menu
1013                 return [self menuForRowIndexes:selectedIndexes];
1014         } else {
1015                 // Otherwise, select our new row and provide a menu for it.
1016                 [inTableView selectRowIndexes:[NSIndexSet indexSetWithIndex:mouseRow] byExtendingSelection:NO];
1018                 // Return our delegate's menu for this row.
1019                 return [self menuForRow:mouseRow];
1020         }       
1024  * @brief Set up KFTypeSelectTableView
1026  * Only search the "name" column.
1027  */
1028 - (void)configureTypeSelectTableView:(KFTypeSelectTableView *)tableView
1030     [tableView setSearchColumnIdentifiers:[NSSet setWithObject:@"name"]];
1033 @end