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/AIChatControllerProtocol.h>
18 #import <Adium/AIContactControllerProtocol.h>
19 #import <Adium/AIContentControllerProtocol.h>
20 #import <Adium/AIInterfaceControllerProtocol.h>
21 #import <Adium/AIPreferenceControllerProtocol.h>
22 #import <Adium/AIToolbarControllerProtocol.h>
23 #import "ESUserIconHandlingPlugin.h"
24 #import <AIUtilities/AIFileManagerAdditions.h>
25 #import <AIUtilities/AIMutableOwnerArray.h>
26 #import <AIUtilities/AIToolbarUtilities.h>
27 #import <AIUtilities/AIMenuAdditions.h>
28 #import <AIUtilities/AIImageAdditions.h>
29 #import <AIUtilities/AIImageButton.h>
30 #import <Adium/AIAccount.h>
31 #import <Adium/AIChat.h>
32 #import <Adium/AIListContact.h>
33 #import <Adium/AIListObject.h>
34 #import <Adium/AIServiceIcons.h>
36 #define TOOLBAR_ITEM_TAG -999
38 @interface ESUserIconHandlingPlugin (PRIVATE)
39 - (BOOL)cacheAndSetUserIconFromPreferenceForListObject:(AIListObject *)inObject;
40 - (BOOL)_cacheUserIconData:(NSData *)inData forObject:(AIListObject *)inObject;
41 - (NSString *)_cachedImagePathForObject:(AIListObject *)inObject;
42 - (BOOL)destroyCacheForListObject:(AIListObject *)inObject;
43 - (void)registerToolbarItem;
45 - (void)_updateToolbarIconOfChat:(AIChat *)inChat inWindow:(NSWindow *)window;
46 - (void)_updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat;
48 - (void)updateToolbarItemForObject:(AIListObject *)inObject;
52 * @class ESUserIconHandlingPlugin
53 * @brief User icon handling component
55 * This component manages the Adium user icon cache. It also provides a toolbar icon which shows the user icon
56 * or service icon of the current chat in its window.
58 @implementation ESUserIconHandlingPlugin
65 //Register our observers
66 [[adium contactController] registerListObjectObserver:self];
67 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_USERICONS];
68 [[adium notificationCenter] addObserver:self
69 selector:@selector(listObjectAttributesChanged:)
70 name:ListObject_AttributesChanged
73 [self registerToolbarItem];
79 - (void)uninstallPlugin
81 [[adium contactController] unregisterListObjectObserver:self];
82 [[adium preferenceController] unregisterPreferenceObserver:self];
86 * @brief Update list object
88 * Handle object creation and changes to the userIcon status object, which should be set by account code
89 * when a user icon is retrieved for the object.
91 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
93 if (inModifiedKeys == nil) {
94 //At object creation, load the user icon.
96 //Only load the cached image file if we do not load from a preference
97 if (![self cacheAndSetUserIconFromPreferenceForListObject:inObject]) {
98 //Load the cached image file by reference into the display array;
99 //It will only be loaded into memory if needed
100 NSString *cachedImagePath = [self _cachedImagePathForObject:inObject];
102 if ([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath]) {
103 NSImage *cachedImage;
105 cachedImage = [[NSImage alloc] initWithContentsOfFile:cachedImagePath];
106 [cachedImage setDataRetained:YES];
109 //A cache image is used at lowest priority, since it is outdated data
110 [inObject setDisplayUserIcon:cachedImage
112 priorityLevel:Lowest_Priority];
113 [inObject setStatusObject:cachedImagePath
114 forKey:@"UserIconPath"
118 [cachedImage release];
121 } else if ([inModifiedKeys containsObject:KEY_USER_ICON]) {
122 //The status UserIcon object is set by account code; apply this to the display array and cache it if necesssary
124 NSImage *statusUserIcon = [inObject statusObjectForKey:KEY_USER_ICON];
125 AIMutableOwnerArray *userIconDisplayArray = [inObject displayArrayForKey:KEY_USER_ICON];
127 //Apply the image at medium priority if we don't already have a higher priority (lower float value) icon set
128 if (![userIconDisplayArray objectWithOwner:self] ||
129 [userIconDisplayArray priorityOfObjectWithOwner:self] >= Medium_Priority) {
130 [inObject setDisplayUserIcon:statusUserIcon
132 priorityLevel:Medium_Priority];
134 //If the new objectValue is what we just set, notify and cache
135 userIcon = [inObject displayUserIcon];
137 if (userIcon == statusUserIcon) {
138 //Cache using the raw data if possible, otherwise create a TIFF representation to cache
139 //Note: TIFF supports transparency but not animation
140 NSData *userIconData = [inObject statusObjectForKey:@"UserIconData"];
142 [self _cacheUserIconData:(userIconData ? userIconData : [userIcon TIFFRepresentation]) forObject:inObject];
144 [[adium contactController] listObjectAttributesChanged:inObject
145 modifiedKeys:[NSSet setWithObject:KEY_USER_ICON]];
147 [self updateToolbarItemForObject:inObject];
156 * @brief List object attributes changes
158 * A plugin, or this plugin, modified the display array for the object; ensure our cache is up to date.
160 - (void)listObjectAttributesChanged:(NSNotification *)notification
162 AIListObject *inObject = [notification object];
163 NSSet *keys = [[notification userInfo] objectForKey:@"Keys"];
165 if (inObject && [keys containsObject:KEY_USER_ICON]) {
166 AIMutableOwnerArray *userIconDisplayArray = [inObject displayArrayForKey:KEY_USER_ICON];
167 NSImage *userIcon = [userIconDisplayArray objectValue];
168 NSImage *ownedUserIcon = [userIconDisplayArray objectWithOwner:self];
170 /* If the new user icon is not the same as the one we own, we should update our cache
171 * and our toolbar item. If we get here from -[self updateListObject:keys:silent:] doing a
172 * listObjectAttributesChanged call, then the userIcon will be the same as ownedUserIcon, and we won't do anything
173 * since it was already done previously.
175 if (userIcon != ownedUserIcon) {
176 [self _cacheUserIconData:[userIcon TIFFRepresentation] forObject:inObject];
178 [self updateToolbarItemForObject:inObject];
184 * @brief The user icon preference was changed
186 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
187 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
190 if (![self cacheAndSetUserIconFromPreferenceForListObject:object]) {
191 [self destroyCacheForListObject:object];
197 * @brief Cache and set the user icon from a listObject's preference
199 * This loads the user-set preference for a listObject, sets it at highest priority, and then caches the
202 * @param inObject The listObject to modify if necessary.
203 * @result YES if the method resulted in setting an image
205 - (BOOL)cacheAndSetUserIconFromPreferenceForListObject:(AIListObject *)inObject
207 NSData *imageData = [inObject preferenceForKey:KEY_USER_ICON
208 group:PREF_GROUP_USERICONS
209 ignoreInheritedValues:YES];
211 //A preference is used at highest priority
215 image = [[NSImage alloc] initWithData:imageData];
216 [image setDataRetained:YES];
218 [inObject setDisplayUserIcon:image
220 priorityLevel:Highest_Priority];
221 [self updateToolbarItemForObject:inObject];
227 //If we had a preference set before (that is, there's an object set at Highest_Priority), clear it
228 if ([[inObject displayArrayForKey:KEY_USER_ICON create:NO] objectWithOwner:self] &&
229 ([[inObject displayArrayForKey:KEY_USER_ICON create:NO] priorityOfObjectWithOwner:self] == Highest_Priority)) {
230 [inObject setDisplayUserIcon:nil
232 priorityLevel:Highest_Priority];
234 //Update the list object to grab the serverside icon as the one we're using, if necessary
235 [self updateListObject:inObject
236 keys:[NSSet setWithObject:KEY_USER_ICON]
239 [self updateToolbarItemForObject:inObject];
247 * @brief Cache user icon data for an object
249 * @param inData Image data to cache
250 * @param inObject AIListObject to cache the data for
252 * @result YES if successful
254 - (BOOL)_cacheUserIconData:(NSData *)inData forObject:(AIListObject *)inObject
257 NSString *cachedImagePath = [self _cachedImagePathForObject:inObject];
259 if (inData && [inData length]) {
260 success = ([inData writeToFile:cachedImagePath
263 success = [[NSFileManager defaultManager] removeFileAtPath:cachedImagePath
265 cachedImagePath = nil;
269 [inObject setStatusObject:cachedImagePath
270 forKey:@"UserIconPath"
277 * @brief Trash a list object's cached icon
279 * @result YES if successful
281 - (BOOL)destroyCacheForListObject:(AIListObject *)inObject
283 NSString *cachedImagePath = [self _cachedImagePathForObject:inObject];
286 if ((success = [[NSFileManager defaultManager] trashFileAtPath:cachedImagePath])) {
287 [inObject setStatusObject:nil
288 forKey:@"UserIconPath"
296 * @brief Retrieve the path at which to cache an <tt>AIListObject</tt>'s image
298 - (NSString *)_cachedImagePathForObject:(AIListObject *)inObject
300 return [[adium cachesPath] stringByAppendingPathComponent:[inObject internalObjectID]];
303 #pragma mark Toolbar Item
306 * @brief Register our toolbar item
308 * Our toolbar item shows an image for the current chat, displaying it full size/animating if clicked.
310 - (void)registerToolbarItem
312 AIImageButton *button;
313 NSToolbarItem *toolbarItem;
315 toolbarItems = [[NSMutableSet alloc] init];
316 validatedItems = [[NSMutableSet alloc] init];
318 //Toolbar item registration
319 [[NSNotificationCenter defaultCenter] addObserver:self
320 selector:@selector(toolbarWillAddItem:)
321 name:NSToolbarWillAddItemNotification
323 [[NSNotificationCenter defaultCenter] addObserver:self
324 selector:@selector(toolbarDidRemoveItem:)
325 name:NSToolbarDidRemoveItemNotification
328 button = [[AIImageButton alloc] initWithFrame:NSMakeRect(0,0,32,32)];
329 toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"UserIcon"
330 label:AILocalizedString(@"Icon",nil)
331 paletteLabel:AILocalizedString(@"Contact Icon",nil)
332 toolTip:AILocalizedString(@"Show this contact's icon",nil)
334 settingSelector:@selector(setView:)
336 action:@selector(dummyAction:)
339 [toolbarItem setMinSize:NSMakeSize(32,32)];
340 [toolbarItem setMaxSize:NSMakeSize(32,32)];
341 [button setToolbarItem:toolbarItem];
342 [button setImage:[NSImage imageNamed:@"userIconToolbar" forClass:[self class] loadLazily:YES]];
345 //Register our toolbar item
346 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"MessageWindow"];
350 * @brief After the toolbar has added the item we can set up the submenus
352 - (void)toolbarWillAddItem:(NSNotification *)notification
354 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
356 if ([[item itemIdentifier] isEqualToString:@"UserIcon"]) {
358 [item setEnabled:YES];
360 //Add menu to toolbar item (for text mode)
361 NSMenuItem *menuFormRepresentation, *blankMenuItem;
364 menuFormRepresentation = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
366 menu = [[[NSMenu allocWithZone:[NSMenu menuZone]] init] autorelease];
367 [menu setDelegate:self];
368 [menu setAutoenablesItems:NO];
370 blankMenuItem = [[NSMenuItem alloc] initWithTitle:@""
372 action:@selector(dummyAction:)
374 [blankMenuItem setRepresentedObject:item];
375 [blankMenuItem setEnabled:YES];
376 [menu addItem:blankMenuItem];
378 [menuFormRepresentation setSubmenu:menu];
379 [menuFormRepresentation setTitle:[item label]];
380 [item setMenuFormRepresentation:menuFormRepresentation];
382 //If this is the first item added, start observing for chats becoming visible so we can update the icon
383 if ([toolbarItems count] == 0) {
384 [[adium notificationCenter] addObserver:self
385 selector:@selector(chatDidBecomeVisible:)
386 name:@"AIChatDidBecomeVisible"
390 [toolbarItems addObject:item];
392 [self performSelector:@selector(toolbarDidAddItem:)
398 - (void)toolbarDidAddItem:(NSToolbarItem *)item
400 /* Only need to take action if we haven't already validated the initial state of this item.
401 * This will only be true when the toolbar is revealed for the first time having been hidden when window opened.
403 if (![validatedItems containsObject:item]) {
404 NSEnumerator *enumerator = [[NSApp windows] objectEnumerator];
406 NSToolbar *thisItemsToolbar = [item toolbar];
408 //Look at each window to find the toolbar we are in
409 while ((window = [enumerator nextObject])) {
410 if ([window toolbar] == thisItemsToolbar) break;
414 [self _updateToolbarItem:item
415 forChat:[[adium interfaceController] activeChatInWindow:window]];
421 * @brief Toolbar removed an item.
423 * If the item is one of ours, stop tracking it.
425 * @param notification Notification with an @"item" userInfo key for an NSToolbarItem.
427 - (void)toolbarDidRemoveItem: (NSNotification *)notification
429 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
430 if ([toolbarItems containsObject:item]) {
432 [toolbarItems removeObject:item];
433 [validatedItems removeObject:item];
435 if ([toolbarItems count] == 0) {
436 [[adium notificationCenter] removeObserver:self
437 name:@"AIChatDidBecomeVisible"
444 * @brief A chat became visible in a window.
446 * Update the item with the @"UserIcon" identifier if necessary
448 * @param notification Notification with an AIChat object and an @"NSWindow" userInfo key
450 - (void)chatDidBecomeVisible:(NSNotification *)notification
452 [self _updateToolbarIconOfChat:[notification object]
453 inWindow:[[notification userInfo] objectForKey:@"NSWindow"]];
456 - (void)updateToolbarItemForObject:(AIListObject *)inObject
461 //Update the icon in the toolbar for this contact if a chat is open and we have any toolbar items
462 if (([toolbarItems count] > 0) &&
463 [inObject isKindOfClass:[AIListContact class]] &&
464 (chat = [[adium chatController] existingChatWithContact:(AIListContact *)inObject]) &&
465 (window = [[adium interfaceController] windowForChat:chat])) {
466 [self _updateToolbarIconOfChat:chat
471 - (void)_updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat
473 AIListContact *listContact;
476 if ((listContact = [[chat listObject] parentContact]) && ![chat isGroupChat]) {
477 image = [listContact userIcon];
479 //Use the serviceIcon if no image can be found
480 if (!image) image = [AIServiceIcons serviceIconForObject:listContact
481 type:AIServiceIconLarge
482 direction:AIIconNormal];
484 //If we have no listObject or we have a name, we are a group chat and
485 //should use the account's service icon
486 image = [AIServiceIcons serviceIconForObject:[chat account]
487 type:AIServiceIconLarge
488 direction:AIIconNormal];
491 [(AIImageButton *)[item view] setImage:image];
493 [validatedItems addObject:item];
497 * @brief Update the user image toolbar icon in a chat
499 * @param chat The chat for which to retrieve an image
500 * @param window The window in which the chat resides
502 - (void)_updateToolbarIconOfChat:(AIChat *)chat inWindow:(NSWindow *)window
504 NSToolbar *toolbar = [window toolbar];
505 NSEnumerator *enumerator = [[toolbar items] objectEnumerator];
508 while ((item = [enumerator nextObject])) {
509 if ([[item itemIdentifier] isEqualToString:@"UserIcon"]) {
510 [self _updateToolbarItem:item forChat:chat];
517 * @brief Empty action for menu item validation purposes
519 - (IBAction)dummyAction:(id)sender{};
522 * @brief Menu needs update
524 * Should only be called for a menu off one of our toolbar items in text-only mode, and only when that menu is about
525 * to be displayed. The menu should have two items. The first is added by the system; the second has no title and is
526 * our menu item for showing the image.
528 - (void)menuNeedsUpdate:(NSMenu *)menu
530 //The first item is a root item inserted by the system. The second item is the single item
531 NSMenuItem *menuItem = [menu itemAtIndex:1];
532 NSToolbarItem *toolbarItem = [menuItem representedObject];
534 [menuItem setImage:[[[(AIImageButton *)[toolbarItem view] image] copy] autorelease]];
537 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem