Japanese updates from Daisuke Okada
[adiumx.git] / Source / ESUserIconHandlingPlugin.m
bloba1004e85bde31653cce0be1066973a1357e726a9
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/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;
49 @end
51 /*!
52  * @class ESUserIconHandlingPlugin
53  * @brief User icon handling component
54  *
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.
57  */
58 @implementation ESUserIconHandlingPlugin
60 /*!
61  * @brief Install
62  */
63 - (void)installPlugin
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
71                                                                          object:nil];
73         [self registerToolbarItem];
76 /*!
77  * @brief Uninstall
78  */
79 - (void)uninstallPlugin
81         [[adium contactController] unregisterListObjectObserver:self];
82         [[adium preferenceController] unregisterPreferenceObserver:self];
85 /*!
86  * @brief Update list object
87  *
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.
90  */
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];
108                                 if (cachedImage) {
109                                         //A cache image is used at lowest priority, since it is outdated data
110                                         [inObject setDisplayUserIcon:cachedImage
111                                                                            withOwner:self
112                                                                    priorityLevel:Lowest_Priority];
113                                         [inObject setStatusObject:cachedImagePath
114                                                                            forKey:@"UserIconPath"
115                                                                            notify:NotifyNever];
116                                 }
118                                 [cachedImage release];
119                         }
120                 }
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
123                 NSImage                         *userIcon;
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
131                                                            withOwner:self
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]];
146                                 
147                                 [self updateToolbarItemForObject:inObject];
148                         }
149                 }
150         }
152         return nil;
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.
159  */
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.
174                  */
175                 if (userIcon != ownedUserIcon) {
176                         [self _cacheUserIconData:[userIcon TIFFRepresentation] forObject:inObject];
177                         
178                         [self updateToolbarItemForObject:inObject];
179                 }
180         }
184  * @brief The user icon preference was changed
185  */
186 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
187                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
189         if (object) {
190                 if (![self cacheAndSetUserIconFromPreferenceForListObject:object]) {
191                         [self destroyCacheForListObject:object];
192                 }
193         }
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
200  * newly set image.
202  * @param inObject The listObject to modify if necessary.
203  * @result YES if the method resulted in setting an image
204  */
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
212         if (imageData) {
213                 NSImage *image;
215                 image = [[NSImage alloc] initWithData:imageData];
216                 [image setDataRetained:YES];
218                 [inObject setDisplayUserIcon:image
219                                                    withOwner:self
220                                            priorityLevel:Highest_Priority];
221                 [self updateToolbarItemForObject:inObject];
223                 [image release];
225                 return YES;
226         } else {
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
231                                                            withOwner:self
232                                                    priorityLevel:Highest_Priority];
233                         
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]
237                                                         silent:NO];
238                         
239                         [self updateToolbarItemForObject:inObject];
240                 }
241         }
243         return NO;
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
253  */
254 - (BOOL)_cacheUserIconData:(NSData *)inData forObject:(AIListObject *)inObject
256         BOOL            success;
257         NSString        *cachedImagePath = [self _cachedImagePathForObject:inObject];
259         if (inData && [inData length]) {
260                 success = ([inData writeToFile:cachedImagePath
261                                                         atomically:YES]);
262         } else {
263                 success = [[NSFileManager defaultManager] removeFileAtPath:cachedImagePath
264                                                                                                                    handler:NULL];
265                 cachedImagePath = nil;
266         }
268         if (success) {
269                 [inObject setStatusObject:cachedImagePath
270                                                    forKey:@"UserIconPath"
271                                                    notify:YES];
272         }
274         return success;
277  * @brief Trash a list object's cached icon
279  * @result YES if successful
280  */
281 - (BOOL)destroyCacheForListObject:(AIListObject *)inObject
283         NSString        *cachedImagePath = [self _cachedImagePathForObject:inObject];
284         BOOL            success;
286         if ((success = [[NSFileManager defaultManager] trashFileAtPath:cachedImagePath])) {
287                 [inObject setStatusObject:nil
288                                                    forKey:@"UserIconPath"
289                                                    notify:YES];
290         }
292         return (success);
296  * @brief Retrieve the path at which to cache an <tt>AIListObject</tt>'s image
297  */
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.
309  */
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
322                                                                                            object:nil];
323         [[NSNotificationCenter defaultCenter] addObserver:self
324                                                                                          selector:@selector(toolbarDidRemoveItem:)
325                                                                                                  name:NSToolbarDidRemoveItemNotification
326                                                                                            object:nil];
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)
333                                                                                                                  target:self
334                                                                                                 settingSelector:@selector(setView:)
335                                                                                                         itemContent:button
336                                                                                                                  action:@selector(dummyAction:)
337                                                                                                                    menu:nil];
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]];
343         [button release];
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
351  */
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;
362                 NSMenu          *menu;
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:@""
371                                                                                                    target:self
372                                                                                                    action:@selector(dummyAction:)
373                                                                                         keyEquivalent:@""];
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"
387                                                                                          object:nil];
388                 }
390                 [toolbarItems addObject:item];
391                 
392                 [self performSelector:@selector(toolbarDidAddItem:)
393                                    withObject:item
394                                    afterDelay:0];
395         }
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.
402         */
403         if (![validatedItems containsObject:item]) {
404                 NSEnumerator *enumerator = [[NSApp windows] objectEnumerator];
405                 NSWindow         *window;
406                 NSToolbar        *thisItemsToolbar = [item toolbar];
407                 
408                 //Look at each window to find the toolbar we are in
409                 while ((window = [enumerator nextObject])) {
410                         if ([window toolbar] == thisItemsToolbar) break;
411                 }
412                 
413                 if (window) {
414                         [self _updateToolbarItem:item
415                                                          forChat:[[adium interfaceController] activeChatInWindow:window]];
416                 }
417         }
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.
426  */
427 - (void)toolbarDidRemoveItem: (NSNotification *)notification
429         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
430         if ([toolbarItems containsObject:item]) {
431                 [item setView:nil];
432                 [toolbarItems removeObject:item];
433                 [validatedItems removeObject:item];
435                 if ([toolbarItems count] == 0) {
436                         [[adium notificationCenter] removeObserver:self
437                                                                                                   name:@"AIChatDidBecomeVisible"
438                                                                                                 object:nil];
439                 }
440         }
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
449  */
450 - (void)chatDidBecomeVisible:(NSNotification *)notification
452         [self _updateToolbarIconOfChat:[notification object]
453                                                   inWindow:[[notification userInfo] objectForKey:@"NSWindow"]];
456 - (void)updateToolbarItemForObject:(AIListObject *)inObject
458         AIChat          *chat;
459         NSWindow        *window;
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
467                                                           inWindow:window];
468         }
471 - (void)_updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat
473         AIListContact   *listContact;
474         NSImage                 *image;
475         
476         if ((listContact = [[chat listObject] parentContact]) && ![chat isGroupChat]) {
477                 image = [listContact userIcon];
478                 
479                 //Use the serviceIcon if no image can be found
480                 if (!image) image = [AIServiceIcons serviceIconForObject:listContact
481                                                                                                                         type:AIServiceIconLarge
482                                                                                                            direction:AIIconNormal];
483         } else {
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];
489         }
490         
491         [(AIImageButton *)[item view] setImage:image];
492         
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
501  */
502 - (void)_updateToolbarIconOfChat:(AIChat *)chat inWindow:(NSWindow *)window
504         NSToolbar               *toolbar = [window toolbar];
505         NSEnumerator    *enumerator = [[toolbar items] objectEnumerator];
506         NSToolbarItem   *item;
508         while ((item = [enumerator nextObject])) {
509                 if ([[item itemIdentifier] isEqualToString:@"UserIcon"]) {
510                         [self _updateToolbarItem:item forChat:chat];
511                         break;
512                 }
513         }
517  * @brief Empty action for menu item validation purposes
518  */
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.
527  */
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
539         return YES;
542 @end