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 "AIContactInfoWindowController.h"
18 #import "AIContactAccountsPane.h"
19 #import "AIContactProfilePane.h"
20 #import "AIContactSettingsPane.h"
21 #import "ESContactAlertsPane.h"
22 #import "ESContactInfoListController.h"
23 #import "AIContactInfoImageViewWithImagePicker.h"
24 #import <Adium/AIInterfaceControllerProtocol.h>
25 #import <Adium/AIPreferenceControllerProtocol.h>
26 #import <Adium/AIAccountControllerProtocol.h>
27 #import <Adium/AIContactAlertsControllerProtocol.h>
28 #import <Adium/AIListGroup.h>
29 #import <Adium/AIMetaContact.h>
30 #import <Adium/AIModularPaneCategoryView.h>
31 #import <Adium/AIListObject.h>
32 #import <Adium/AIListOutlineView.h>
33 #import <Adium/AIService.h>
34 #import <AIUtilities/AIDictionaryAdditions.h>
35 #import <AIUtilities/AIImageAdditions.h>
36 #import <AIUtilities/AIImageViewWithImagePicker.h>
37 #import <AIUtilities/AIOutlineViewAdditions.h>
38 #import <AIUtilities/AIStringAdditions.h>
39 #import <AIUtilities/AITabViewAdditions.h>
41 #define CONTACT_INFO_NIB @"ContactInfoWindow" //Filename of the contact info nib
42 #define KEY_INFO_WINDOW_FRAME @"Contact Info Window Frame" //
43 #define KEY_INFO_SELECTED_CATEGORY @"Selected Info Category" //
45 #define CONTACT_INFO_THEME @"Contact Info List Theme"
46 #define CONTACT_INFO_LAYOUT @"Contact Info List Layout"
48 @interface AIContactInfoWindowController (PRIVATE)
49 - (id)initWithWindowNibName:(NSString *)windowNibName;
50 - (void)selectionChanged:(NSNotification *)notification;
52 - (void)localizeTabViewItemTitles;
53 - (void)configureDrawer;
54 - (void)configureVisiblityOfTabViewItemsForListObject:(AIListObject *)inObject;
55 - (void)configurePane:(AIContactInfoPane *)inPane;
56 - (void)setupMetaContactDrawer;
60 @implementation AIContactInfoWindowController
62 static AIContactInfoWindowController *sharedContactInfoInstance = nil;
64 //Return the shared contact info window
65 + (id)showInfoWindowForListObject:(AIListObject *)listObject
68 if (!sharedContactInfoInstance) {
69 sharedContactInfoInstance = [[self alloc] initWithWindowNibName:CONTACT_INFO_NIB];
72 //Configure and show window
74 if ([listObject isKindOfClass:[AIListContact class]]) {
75 AIListContact *parentContact = [(AIListContact *)listObject parentContact];
77 /* Use the parent contact if it is a valid meta contact which contains contacts
78 * If this contact is within a metacontact but not currently listed on any buddy list, we don't want to
79 * display the effectively-invisible metacontact's info but rather the info of this contact itself.
81 if (![parentContact isKindOfClass:[AIMetaContact class]] ||
82 [[(AIMetaContact *)parentContact listContacts] count]) {
83 listObject = parentContact;
87 [sharedContactInfoInstance configureForListObject:listObject];
88 [[sharedContactInfoInstance window] makeKeyAndOrderFront:nil];
90 return (sharedContactInfoInstance);
93 //Close the info window
94 + (void)closeInfoWindow
96 if (sharedContactInfoInstance) {
97 [sharedContactInfoInstance closeWindow:nil];
103 //If we removed the account and info tab view items, we're currently also retaining them
104 if ([tabView_category indexOfTabViewItem:tabViewItem_info] == NSNotFound) {
105 [tabViewItem_accounts release]; tabViewItem_accounts = nil;
106 [tabViewItem_info release]; tabViewItem_info = nil;
109 [displayedObject release]; displayedObject = nil;
110 [loadedPanes release]; loadedPanes = nil;
116 - (NSString *)adiumFrameAutosaveName
118 return KEY_INFO_WINDOW_FRAME;
121 //Setup the window before it is displayed
122 - (void)windowDidLoad
124 [super windowDidLoad];
127 NSTabViewItem *tabViewItem;
130 loadedPanes = [[NSMutableDictionary alloc] init];
133 [self localizeTabViewItemTitles];
134 [button_removeContact setToolTip:AILocalizedString(@"Disassociate the selected contact from this meta contact. This does not remove the contact from your contact list.",nil)];
135 [button_removeContact setEnabled:NO];
137 //Select the previously selected category
138 selectedTab = [[[adium preferenceController] preferenceForKey:KEY_INFO_SELECTED_CATEGORY
139 group:PREF_GROUP_WINDOW_POSITIONS] intValue];
140 if (selectedTab < 0 || selectedTab >= [tabView_category numberOfTabViewItems]) selectedTab = 0;
142 tabViewItem = [tabView_category tabViewItemAtIndex:selectedTab];
144 //NSTabView won't send the willSelectTabViewItem: properly when we call selectTabViewItem:
145 [self tabView:tabView_category willSelectTabViewItem:tabViewItem];
146 [tabView_category selectTabViewItem:tabViewItem];
148 [imageView_userIcon setAnimates:YES];
149 [imageView_userIcon setMaxSize:NSMakeSize(256, 256)];
151 //Monitor the selected contact
152 [[adium notificationCenter] addObserver:self
153 selector:@selector(selectionChanged:)
154 name:Interface_ContactSelectionChanged
157 contactListController = [[ESContactInfoListController alloc] initWithContactListView:contactListView
158 inScrollView:scrollView_contactList
161 [self setupMetaContactDrawer];
164 - (void)localizeTabViewItemTitles
166 NSEnumerator *enumerator = [[tabView_category tabViewItems] objectEnumerator];
167 NSTabViewItem *tabViewItem;
168 while ((tabViewItem = [enumerator nextObject])) {
169 NSString *label = nil;
170 int identifier = [[tabViewItem identifier] intValue];
172 switch (identifier) {
174 label = AILocalizedString(@"Info","short form of tab view item title for Contact Info window's first tab");
176 case AIInfo_Accounts:
177 label = AILocalizedString(@"Accounts",nil);
180 label = AILocalizedString(@"Events", "Name of preferences and tab for specifying what Adium should do when events occur - for example, display a Growl alert when John signs on.");
182 case AIInfo_Settings:
183 label = AILocalizedString(@"Settings","tab view item title for Contact Settings (Settings)");
187 [tabViewItem setLabel:label];
191 //called as the window closes
192 - (void)windowWillClose:(id)sender
194 NSEnumerator *enumerator;
195 AIContactInfoPane *pane;
197 [super windowWillClose:sender];
199 //Take focus away from any controls to ensure that they register changes and save
200 [[self window] makeFirstResponder:tabView_category];
202 //Close all open panes
203 enumerator = [loadedPanes objectEnumerator];
204 while ((pane = [enumerator nextObject])) {
208 //Save the selected category
209 [[adium preferenceController] setPreference:[NSNumber numberWithInt:[tabView_category indexOfSelectedTabViewItem]]
210 forKey:KEY_INFO_SELECTED_CATEGORY
211 group:PREF_GROUP_WINDOW_POSITIONS];
214 [[adium notificationCenter] removeObserver:self];
215 [self autorelease]; sharedContactInfoInstance = nil;
218 - (NSImage *)tabView:(NSTabView *)tabView imageForTabViewItem:(NSTabViewItem *)tabViewItem
220 if (tabView == tabView_category) {
221 return [NSImage imageNamed:[NSString stringWithFormat:@"info%@",[tabViewItem identifier]] forClass:[self class]];
228 - (void)tabView:(NSTabView *)tabView willSelectTabViewItem:(NSTabViewItem *)tabViewItem
230 if (tabView == tabView_category) {
231 AIContactInfoPane *pane = nil;
233 //Take focus away from any textual controls to ensure that they register changes and save
234 if ([[[self window] firstResponder] isKindOfClass:[NSText class]]) {
235 [[self window] makeFirstResponder:nil];
238 int identifier = [[tabViewItem identifier] intValue];
239 if (!(pane = [loadedPanes objectForKey:[NSNumber numberWithInt:identifier]])) {
240 switch (identifier) {
242 pane = [AIContactProfilePane contactInfoPane];
243 [view_Profile setPanes:[NSArray arrayWithObject:pane]];
246 case AIInfo_Accounts:
247 pane = [AIContactAccountsPane contactInfoPane];
248 [view_Accounts setPanes:[NSArray arrayWithObject:pane]];
251 pane = [ESContactAlertsPane contactInfoPane];
252 [view_Alerts setPanes:[NSArray arrayWithObject:pane]];
254 case AIInfo_Settings:
255 pane = [AIContactSettingsPane contactInfoPane];
256 [view_Settings setPanes:[NSArray arrayWithObject:pane]];
261 [loadedPanes setObject:pane
262 forKey:[NSNumber numberWithInt:identifier]];
264 NSLog(@"%@: Could not load pane for identifier %i",self,identifier);
268 //Configure the loaded panes
269 [self configurePane:pane];
273 //When the contact list selection changes, then configure the window for the new contact
274 - (void)selectionChanged:(NSNotification *)notification
276 AIListObject *object = [[adium interfaceController] selectedListObject];
277 if (object) [self configureForListObject:[[adium interfaceController] selectedListObject]];
280 - (void)updateUserIcon
283 NSSize userIconSize, imageView_userIconSize;
286 if (!(userIcon = [displayedObject userIcon])) {
287 userIcon = [NSImage imageNamed:@"DefaultIcon" forClass:[self class]];
290 /* NSScaleProportionally will lock an animated GIF into a single frame. We therefore use NSScaleNone if
291 * we are already at the right size or smaller than the right size; otherwise we scale proportionally to
294 userIconSize = [userIcon size];
295 imageView_userIconSize = [imageView_userIcon frame].size;
297 [imageView_userIcon setImageScaling:(((userIconSize.width <= imageView_userIconSize.width) && (userIconSize.height <= imageView_userIconSize.height)) ?
299 NSScaleProportionally)];
300 [imageView_userIcon setImage:userIcon];
301 [imageView_userIcon setTitle:(displayedObject ?
302 [NSString stringWithFormat:AILocalizedString(@"%@'s Image",nil),[displayedObject displayName]] :
303 AILocalizedString(@"Image Picker",nil))];
305 //Show the reset image button if a preference is set on this object, overriding its serverside icon
306 [imageView_userIcon setShowResetImageButton:([displayedObject preferenceForKey:KEY_USER_ICON
307 group:PREF_GROUP_USERICONS
308 ignoreInheritedValues:YES] != nil)];
311 //Change the list object
312 - (void)configureForListObject:(AIListObject *)inObject
314 if (inObject == nil || displayedObject != inObject) {
315 BOOL useDisplayName = NO;
317 //Update our displayed object
318 [displayedObject release];
319 displayedObject = [inObject retain];
321 //Update our window title
323 [[self window] setTitle:[NSString stringWithFormat:AILocalizedString(@"%@'s Info",nil),[inObject displayName]]];
325 [[self window] setTitle:AILocalizedString(@"Contact Info",nil)];
329 if ([inObject isKindOfClass:[AIListContact class]]) {
330 NSString *displayServiceID;
331 if ([inObject isKindOfClass:[AIMetaContact class]]) {
332 if ([[(AIMetaContact *)inObject listContacts] count] > 1) {
333 displayServiceID = AILocalizedString(@"Meta", "Short string used to identify the 'service' of a multiple-service meta contact");
334 useDisplayName = YES;
336 displayServiceID = [[[(AIMetaContact *)inObject preferredContact] service] shortDescription];
340 displayServiceID = [[inObject service] shortDescription];
343 [textField_service setStringValue:(displayServiceID ? displayServiceID : @"")];
345 } else if ([inObject isKindOfClass:[AIListGroup class]]) {
346 [textField_service setLocalizedString:AILocalizedString(@"Group",nil)];
348 [textField_service setStringValue:@""];
353 NSString *formattedUID;
355 if (!useDisplayName && (formattedUID = [inObject formattedUID])) {
356 [textField_accountName setStringValue:formattedUID];
358 NSString *displayName;
360 if ((displayName = [inObject displayName])) {
361 [textField_accountName setStringValue:displayName];
363 [textField_accountName setStringValue:[inObject UID]];
368 [textField_accountName setStringValue:@""];
371 [self updateUserIcon];
373 //Configure our subpanes
374 [self configureVisiblityOfTabViewItemsForListObject:inObject];
376 //Confiugre the drawer
377 [self configureDrawer];
379 //Reconfigure the currently selected tab view item
380 [self tabView:tabView_category willSelectTabViewItem:[tabView_category selectedTabViewItem]];
385 //Configure our views
386 - (void)configurePane:(AIContactInfoPane *)pane
388 if (displayedObject) {
389 [pane configureForListObject:displayedObject];
393 - (void)configureVisiblityOfTabViewItemsForListObject:(AIListObject *)inObject
395 if ([inObject isKindOfClass:[AIListGroup class]]) {
396 //Remove the info and account items for groups
397 if ([tabView_category indexOfTabViewItem:tabViewItem_info] != NSNotFound) {
398 [tabViewItem_accounts retain];
399 [tabViewItem_info retain];
401 //Store the tab view item selected out of accounts or info, if one is selected
402 NSTabViewItem *currentlySelected = [tabView_category selectedTabViewItem];
403 tabViewItem_lastSelectedForListContacts = ((currentlySelected == tabViewItem_accounts || currentlySelected == tabViewItem_info) ?
407 [tabView_category removeTabViewItem:tabViewItem_accounts];
408 [tabView_category removeTabViewItem:tabViewItem_info];
412 //Add the info and account items back in if they are missing
413 if ([tabView_category indexOfTabViewItem:tabViewItem_info] == NSNotFound) {
414 [tabView_category insertTabViewItem:tabViewItem_accounts atIndex:0];
415 [tabView_category insertTabViewItem:tabViewItem_info atIndex:0];
417 //Restore the tab view item last selected for a contact if we have one stored
418 if (tabViewItem_lastSelectedForListContacts) {
419 [tabView_category selectTabViewItem:tabViewItem_lastSelectedForListContacts];
420 tabViewItem_lastSelectedForListContacts = nil;
423 [tabViewItem_accounts release];
424 [tabViewItem_info release];
428 #warning need to hide panes for bookmarks
431 #pragma mark AIImageViewWithImagePicker Delegate
432 // AIImageViewWithImagePicker Delegate ---------------------------------------------------------------------
433 - (void)imageViewWithImagePicker:(AIImageViewWithImagePicker *)sender didChangeToImageData:(NSData *)imageData
435 if (displayedObject) {
436 [displayedObject setUserIconData:imageData];
439 [self updateUserIcon];
442 - (void)deleteInImageViewWithImagePicker:(AIImageViewWithImagePicker *)sender
444 if (displayedObject) {
445 //Remove the preference
446 [displayedObject setUserIconData:nil];
448 [self updateUserIcon];
453 If the userIcon was bigger than our image view's frame, it will have been clipped before being passed
454 to the AIImageViewWithImagePicker. This delegate method lets us pass the original, unmodified userIcon.
456 - (NSImage *)imageForImageViewWithImagePicker:(AIImageViewWithImagePicker *)picker
458 return ([displayedObject userIcon]);
461 - (NSImage *)emptyPictureImageForImageViewWithImagePicker:(AIImageViewWithImagePicker *)picker
463 return [AIServiceIcons serviceIconForObject:displayedObject type:AIServiceIconLarge direction:AIIconNormal];
466 - (NSString *)fileNameForImageInImagePicker:(AIImageViewWithImagePicker *)picker
468 NSString *fileName = [[displayedObject displayName] safeFilenameString];
469 if ([fileName hasPrefix:@"."]) {
470 fileName = [fileName substringFromIndex:1];
475 #pragma mark Contact List (metaContact)
476 - (void)setupMetaContactDrawer
478 NSDictionary *themeDict = [NSDictionary dictionaryNamed:CONTACT_INFO_THEME forClass:[self class]];
479 NSDictionary *layoutDict = [NSDictionary dictionaryNamed:CONTACT_INFO_LAYOUT forClass:[self class]];
481 [contactListController updateLayoutFromPrefDict:layoutDict andThemeFromPrefDict:themeDict];
483 [contactListController setHideRoot:YES];
486 - (void)configureDrawer
488 AIListObject *listObject = ([displayedObject isKindOfClass:[AIListContact class]] ?
489 [(AIListContact *)displayedObject parentContact] :
492 if ([listObject isKindOfClass:[AIMetaContact class]] &&
493 ([[(AIMetaContact *)listObject listContacts] count] > 1)) {
494 [contactListController setContactListRoot:(AIMetaContact *)listObject];
495 [drawer_metaContact open];
497 NSRect outlineFrame = [contactListView frame];
498 int totalHeight = [contactListView totalHeight];
500 if (outlineFrame.size.height != totalHeight) {
501 outlineFrame.size.height = totalHeight;
502 [contactListView setFrame:outlineFrame];
503 [contactListView setNeedsDisplay:YES];
507 [drawer_metaContact close];
508 [contactListController setContactListRoot:nil];
511 - (IBAction)addContact:(id)sender
516 - (IBAction)removeContact:(id)sender
518 NSEnumerator *enumerator;
519 AIListObject *aListObject;
520 AIMetaContact *contactListRoot = (AIMetaContact *)[contactListController contactListRoot];
522 enumerator = [[contactListView arrayOfSelectedItems] objectEnumerator];
523 while ((aListObject = [enumerator nextObject])) {
524 [[adium contactController] removeAllListObjectsMatching:aListObject
525 fromMetaContact:contactListRoot];
528 //The contents of the metaContact have now changed; reload
529 [contactListView reloadData];
531 [contactListController outlineViewSelectionDidChange:nil];
534 - (float)drawerTrailingOffset
536 return [drawer_metaContact trailingOffset];
539 - (void)performDefaultActionOnSelectedObject:(AIListObject *)listObject sender:(NSOutlineView *)sender
544 - (void)contactInfoListControllerSelectionDidChangeToListObject:(AIListObject *)listObject
546 AILog(@"Configuring Info List for %@",listObject);
547 [self configureForListObject:listObject];
549 //Only enable the remove contact button if a contact within the metacontact is selected
550 [button_removeContact setEnabled:(listObject && (listObject != (AIListObject *)[contactListController contactListRoot]))];