Merged [15040]: Trying some magic: 5 seconds after the last unreachable host is repor...
[adiumx.git] / Source / ESUserIconHandlingPlugin.m
blob8da309f7c0280d7331398fc5cb259e3a6613c216
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 "AIContactController.h"
18 #import "AIContentController.h"
19 #import "AIInterfaceController.h"
20 #import "AIPreferenceController.h"
21 #import "AIToolbarController.h"
22 #import "ESUserIconHandlingPlugin.h"
23 #import <AIUtilities/AIFileManagerAdditions.h>
24 #import <AIUtilities/AIMutableOwnerArray.h>
25 #import <AIUtilities/AIToolbarUtilities.h>
26 #import <AIUtilities/AIMenuAdditions.h>
27 #import <AIUtilities/ESImageAdditions.h>
28 #import <AIUtilities/ESImageButton.h>
29 #import <Adium/AIAccount.h>
30 #import <Adium/AIChat.h>
31 #import <Adium/AIListContact.h>
32 #import <Adium/AIListObject.h>
33 #import <Adium/AIServiceIcons.h>
35 #define TOOLBAR_ITEM_TAG        -999
37 @interface ESUserIconHandlingPlugin (PRIVATE)
38 - (BOOL)cacheAndSetUserIconFromPreferenceForListObject:(AIListObject *)inObject;
39 - (BOOL)_cacheUserIconData:(NSData *)inData forObject:(AIListObject *)inObject;
40 - (NSString *)_cachedImagePathForObject:(AIListObject *)inObject;
41 - (BOOL)destroyCacheForListObject:(AIListObject *)inObject;
42 - (void)registerToolbarItem;
44 - (void)_updateToolbarIconOfChat:(AIChat *)inChat inWindow:(NSWindow *)window;
45 - (void)_updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat;
47 - (void)updateToolbarItemForObject:(AIListObject *)inObject;
48 @end
50 /*!
51  * @class ESUserIconHandlingPlugin
52  * @brief User icon handling component
53  *
54  * This component manages the Adium user icon cache.  It also provides a toolbar icon which shows the user icon
55  * or service icon of the current chat in its window.
56  */
57 @implementation ESUserIconHandlingPlugin
59 /*!
60  * @brief Install
61  */
62 - (void)installPlugin
64         //Register our observers
65         [[adium contactController] registerListObjectObserver:self];
66         [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_USERICONS];
67         [[adium notificationCenter] addObserver:self selector:@selector(listObjectAttributesChanged:)
68                                                                            name:ListObject_AttributesChanged
69                                                                          object:nil];
70         
71         [self registerToolbarItem];
74 /*!
75  * @brief Uninstall
76  */
77 - (void)uninstallPlugin
79         [[adium contactController] unregisterListObjectObserver:self];
80         [[adium preferenceController] unregisterPreferenceObserver:self];
83 /*!
84  * @brief Update list object
85  *
86  * Handle object creation and changes to the userIcon status object, which should be set by account code
87  * when a user icon is retrieved for the object.
88  */
89 - (NSSet *)updateListObject:(AIListObject *)inObject keys:(NSSet *)inModifiedKeys silent:(BOOL)silent
91         if (inModifiedKeys == nil) {
92                 //At object creation, load the user icon.
93                 
94                 //Only load the cached image file if we do not load from a preference
95                 if (![self cacheAndSetUserIconFromPreferenceForListObject:inObject]) {
96                         //Load the cached image file by reference into the display array;
97                         //It will only be loaded into memory if needed
98                         NSString                        *cachedImagePath = [self _cachedImagePathForObject:inObject];
99                         if ([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath]) {
100                                 NSImage                         *cachedImage;
101                                 
102                                 cachedImage = [[NSImage alloc] initByReferencingFile:cachedImagePath];
103                                 
104                                 if (cachedImage) {
105                                         //A cache image is used at lowest priority, since it is outdated data
106                                         [inObject setDisplayUserIcon:cachedImage
107                                                                            withOwner:self
108                                                                    priorityLevel:Lowest_Priority];
109                                         [inObject setStatusObject:cachedImagePath
110                                                                            forKey:@"UserIconPath"
111                                                                            notify:NotifyNever];
112                                 }
113                                 
114                                 [cachedImage release];
115                         }
116                 }
117         } else if ([inModifiedKeys containsObject:KEY_USER_ICON]) {
118                 //The status UserIcon object is set by account code; apply this to the display array and cache it if necesssary
119                 NSImage                         *userIcon;
120                 NSImage                         *statusUserIcon = [inObject statusObjectForKey:KEY_USER_ICON];
121                 AIMutableOwnerArray *userIconDisplayArray = [inObject displayArrayForKey:KEY_USER_ICON];
122                 
123                 //Apply the image at medium priority if  we don't already have a higher priority (lower float value) icon set
124                 if (![userIconDisplayArray objectWithOwner:self] ||
125                         [userIconDisplayArray priorityOfObjectWithOwner:self] >= Medium_Priority) {
126                         [inObject setDisplayUserIcon:statusUserIcon
127                                                            withOwner:self
128                                                    priorityLevel:Medium_Priority];
129                         
130                         //If the new objectValue is what we just set, notify and cache
131                         userIcon = [inObject displayUserIcon];
132                         
133                         if (userIcon == statusUserIcon) {
134                                 //Cache using the raw data if possible, otherwise create a TIFF representation to cache
135                                 //Note: TIFF supports transparency but not animation
136                                 NSData  *userIconData = [inObject statusObjectForKey:@"UserIconData"];
137                                 [self _cacheUserIconData:(userIconData ? userIconData : [userIcon TIFFRepresentation]) forObject:inObject];
138                                 
139                                 [[adium contactController] listObjectAttributesChanged:inObject
140                                                                                                                   modifiedKeys:[NSSet setWithObject:KEY_USER_ICON]];
141                                 
142                                 [self updateToolbarItemForObject:inObject];
143                         }
144                 }
145         }
146         
147         return nil;
151  * @brief List object attributes changes
153  * A plugin, or this plugin, modified the display array for the object; ensure our cache is up to date.
154  */
155 - (void)listObjectAttributesChanged:(NSNotification *)notification
157         AIListObject    *inObject = [notification object];
158         NSSet                   *keys = [[notification userInfo] objectForKey:@"Keys"];
159         
160         if (inObject && [keys containsObject:KEY_USER_ICON]) {
161                 AIMutableOwnerArray *userIconDisplayArray = [inObject displayArrayForKey:KEY_USER_ICON];
162                 NSImage *userIcon = [userIconDisplayArray objectValue];
163                 NSImage *ownedUserIcon = [userIconDisplayArray objectWithOwner:self];
164                 
165                 /* If the new user icon is not the same as the one we own, we should update our cache
166                         * and our toolbar item. If we get here from -[self updateListObject:keys:silent:] doing a
167                         * listObjectAttributesChanged call, then the userIcon will be the same as ownedUserIcon, and we won't do anything
168                         * since it was already done previously.
169                         */
170                 if (userIcon != ownedUserIcon) {
171                         [self _cacheUserIconData:[userIcon TIFFRepresentation] forObject:inObject];
172                         
173                         [self updateToolbarItemForObject:inObject];
174                 }
175         }
179  * @brief The user icon preference was changed
180  */
181 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
182                                                         object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
184         if (object) {
185                 if (![self cacheAndSetUserIconFromPreferenceForListObject:object]) {
186                         [self destroyCacheForListObject:object];
187                 }
188         }
192  * @brief Cache and set the user icon from a listObject's preference
194  * This loads the user-set preference for a listObject, sets it at highest priority, and then caches the
195  * newly set image.
197  * @param inObject The listObject to modify if necessary.
198  * @result YES if the method resulted in setting an image
199  */
200 - (BOOL)cacheAndSetUserIconFromPreferenceForListObject:(AIListObject *)inObject
202         NSData  *imageData = [inObject preferenceForKey:KEY_USER_ICON
203                                                                                           group:PREF_GROUP_USERICONS
204                                                           ignoreInheritedValues:YES];
205         
206         //A preference is used at highest priority
207         if (imageData) {
208                 NSImage *image;
209                 
210                 image = [[NSImage alloc] initWithData:imageData];
211                 
212                 [inObject setDisplayUserIcon:image
213                                                    withOwner:self
214                                            priorityLevel:Highest_Priority];
215                 [self updateToolbarItemForObject:inObject];
216                 
217                 [image release];
218                 
219                 return YES;
220         } else {
221                 //If we had a preference set before (that is, there's an object set at Highest_Priority), clear it
222                 if ([[inObject displayArrayForKey:KEY_USER_ICON create:NO] priorityOfObjectWithOwner:self] == Highest_Priority) {
223                         
224                         [inObject setDisplayUserIcon:nil
225                                                            withOwner:self
226                                                    priorityLevel:Highest_Priority];
227                         
228                         //Update the list object to grab the serverside icon as the one we're using, if necessary
229                         [self updateListObject:inObject
230                                                           keys:[NSSet setWithObject:KEY_USER_ICON]
231                                                         silent:NO];
232                         
233                         [self updateToolbarItemForObject:inObject];
234                 }
235         }
236         
237         return NO;
241  * @brief Cache user icon data for an object
243  * @param inData Image data to cache
244  * @param inObject AIListObject to cache the data for
246  * @result YES if successful
247  */
248 - (BOOL)_cacheUserIconData:(NSData *)inData forObject:(AIListObject *)inObject
250         BOOL            success;
251         NSString        *cachedImagePath = [self _cachedImagePathForObject:inObject];
252         
253         success = ([inData writeToFile:cachedImagePath
254                                                 atomically:YES]);
255         if (success) {
256                 [inObject setStatusObject:cachedImagePath
257                                                    forKey:@"UserIconPath"
258                                                    notify:YES];
259         }
260         
261         return success;
265  * @brief Trash a list object's cached icon
267  * @result YES if successful
268  */
269 - (BOOL)destroyCacheForListObject:(AIListObject *)inObject
271         NSString        *cachedImagePath = [self _cachedImagePathForObject:inObject];
272         BOOL            success;
273         
274         if ((success = [[NSFileManager defaultManager] trashFileAtPath:cachedImagePath])) {
275                 [inObject setStatusObject:nil
276                                                    forKey:@"UserIconPath"
277                                                    notify:YES];
278         }
279         
280         return (success);
284  * @brief Retrieve the path at which to cache an <tt>AIListObject</tt>'s image
285  */
286 - (NSString *)_cachedImagePathForObject:(AIListObject *)inObject
288         return [[adium cachesPath] stringByAppendingPathComponent:[inObject internalObjectID]];
291 #pragma mark Toolbar Item
294  * @brief Register our toolbar item
296  * Our toolbar item shows an image for the current chat, displaying it full size/animating if clicked.
297  */
298 - (void)registerToolbarItem
300         ESImageButton   *button;
301         NSToolbarItem   *toolbarItem;
303         toolbarItems = [[NSMutableSet alloc] init];
304         validatedItems = [[NSMutableSet alloc] init];
306         //Toolbar item registration
307         [[NSNotificationCenter defaultCenter] addObserver:self
308                                                                                          selector:@selector(toolbarWillAddItem:)
309                                                                                                  name:NSToolbarWillAddItemNotification
310                                                                                            object:nil];
311         [[NSNotificationCenter defaultCenter] addObserver:self
312                                                                                          selector:@selector(toolbarDidRemoveItem:)
313                                                                                                  name:NSToolbarDidRemoveItemNotification
314                                                                                            object:nil];
316         button = [[ESImageButton alloc] initWithFrame:NSMakeRect(0,0,32,32)];
317         toolbarItem = [AIToolbarUtilities toolbarItemWithIdentifier:@"UserIcon"
318                                                                                                                   label:AILocalizedString(@"Icon",nil)
319                                                                                                    paletteLabel:AILocalizedString(@"Contact Icon",nil)
320                                                                                                                 toolTip:AILocalizedString(@"Show this contact's icon",nil)
321                                                                                                                  target:self
322                                                                                                 settingSelector:@selector(setView:)
323                                                                                                         itemContent:button
324                                                                                                                  action:@selector(dummyAction:)
325                                                                                                                    menu:nil];
327         [toolbarItem setMinSize:NSMakeSize(32,32)];
328         [toolbarItem setMaxSize:NSMakeSize(32,32)];
329         [button setToolbarItem:toolbarItem];
330         [button setImage:[NSImage imageNamed:@"userIconToolbar" forClass:[self class]]];
331         [button release];
333         //Register our toolbar item
334         [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"MessageWindow"];
338  * @brief After the toolbar has added the item we can set up the submenus
339  */
340 - (void)toolbarWillAddItem:(NSNotification *)notification
342         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
344         if([[item itemIdentifier] isEqualToString:@"UserIcon"]){
346                 [item setEnabled:YES];
348                 /* Add menu to toolbar item (for text mode)
349                  *
350                  * We depend on menuNeedsUpdate: for efficient safe updating, so only proceed if setDelegate is available.
351                  * This means that 10.2 will have this item greyed out in text-only mode. */
352                 if([NSMenu instancesRespondToSelector:@selector(setDelegate:)]){
353                         NSMenuItem      *menuFormRepresentation, *blankMenuItem;
354                         NSMenu          *menu;
356                         menuFormRepresentation = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
358                         menu = [[[NSMenu allocWithZone:[NSMenu menuZone]] init] autorelease];
359                         [menu setDelegate:self];
360                         [menu setAutoenablesItems:NO];
362                         blankMenuItem = [[NSMenuItem alloc] initWithTitle:@""
363                                                                                                            target:self
364                                                                                                            action:@selector(dummyAction:)
365                                                                                                 keyEquivalent:@""];
366                         [blankMenuItem setRepresentedObject:item];
367                         [blankMenuItem setEnabled:YES];
368                         [menu addItem:blankMenuItem];
370                         [menuFormRepresentation setSubmenu:menu];
371                         [menuFormRepresentation setTitle:[item label]];
372                         [item setMenuFormRepresentation:menuFormRepresentation];
373                 }
375                 //If this is the first item added, start observing for chats becoming visible so we can update the icon
376                 if([toolbarItems count] == 0){
377                         [[adium notificationCenter] addObserver:self
378                                                                                    selector:@selector(chatDidBecomeVisible:)
379                                                                                            name:@"AIChatDidBecomeVisible"
380                                                                                          object:nil];
381                 }
383                 [toolbarItems addObject:item];
384                 
385                 [self performSelector:@selector(toolbarDidAddItem:)
386                                    withObject:item
387                                    afterDelay:0];
388         }
391 - (void)toolbarDidAddItem:(NSToolbarItem *)item
393         /* Only need to take action if we haven't already validated the initial state of this item.
394         * This will only be true when the toolbar is revealed for the first time having been hidden when window opened.
395         */
396         if (![validatedItems containsObject:item]) {
397                 NSEnumerator *enumerator = [[NSApp windows] objectEnumerator];
398                 NSWindow         *window;
399                 NSToolbar        *thisItemsToolbar = [item toolbar];
400                 
401                 //Look at each window to find the toolbar we are in
402                 while ((window = [enumerator nextObject])) {
403                         if ([window toolbar] == thisItemsToolbar) break;
404                 }
405                 
406                 if (window) {
407                         [self _updateToolbarItem:item
408                                                          forChat:[[adium interfaceController] activeChatInWindow:window]];
409                 }
410         }
414  * @brief Toolbar removed an item.
416  * If the item is one of ours, stop tracking it.
418  * @param notification Notification with an @"item" userInfo key for an NSToolbarItem.
419  */
420 - (void)toolbarDidRemoveItem: (NSNotification *)notification
422         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
423         if([toolbarItems containsObject:item]){
424                 [toolbarItems removeObject:item];
425                 [validatedItems removeObject:item];
427                 if([toolbarItems count] == 0){
428                         [[adium notificationCenter] removeObserver:self
429                                                                                                   name:@"AIChatDidBecomeVisible"
430                                                                                                 object:nil];
431                 }
432         }
436  * @brief A chat became visible in a window.
438  * Update the item with the @"UserIcon" identifier if necessary
440  * @param notification Notification with an AIChat object and an @"NSWindow" userInfo key
441  */
442 - (void)chatDidBecomeVisible:(NSNotification *)notification
444         [self _updateToolbarIconOfChat:[notification object]
445                                                   inWindow:[[notification userInfo] objectForKey:@"NSWindow"]];
448 - (void)updateToolbarItemForObject:(AIListObject *)inObject
450         AIChat          *chat;
451         NSWindow        *window;
453         //Update the icon in the toolbar for this contact if a chat is open and we have any toolbar items
454         if (([toolbarItems count] > 0) &&
455                 [inObject isKindOfClass:[AIListContact class]] &&
456                 (chat = [[adium contentController] existingChatWithContact:(AIListContact *)inObject]) &&
457                 (window = [[adium interfaceController] windowForChat:chat])) {
458                 [self _updateToolbarIconOfChat:chat
459                                                           inWindow:window];
460         }
463 - (void)_updateToolbarItem:(NSToolbarItem *)item forChat:(AIChat *)chat
465         AIListContact   *listContact;
466         NSImage                 *image;
467         
468         if ((listContact = [[chat listObject] parentContact])) {
469                 image = [listContact userIcon];
471                 //Use the serviceIcon if no image can be found
472                 if (!image) image = [AIServiceIcons serviceIconForObject:listContact
473                                                                                                                         type:AIServiceIconLarge
474                                                                                                            direction:AIIconNormal];
475         } else {
476                 //If we have no listObject or we have a name, we are a group chat and
477                 //should use the account's service icon
478                 image = [AIServiceIcons serviceIconForObject:[chat account]
479                                                                                                 type:AIServiceIconLarge
480                                                                                    direction:AIIconNormal];
481         }
482         
483         [(ESImageButton *)[item view] setImage:image];
484         
485         [validatedItems addObject:item];
489  * @brief Update the user image toolbar icon in a chat
491  * @param chat The chat for which to retrieve an image
492  * @param window The window in which the chat resides
493  */
494 - (void)_updateToolbarIconOfChat:(AIChat *)chat inWindow:(NSWindow *)window
496         NSToolbar               *toolbar = [window toolbar];
497         NSEnumerator    *enumerator = [[toolbar items] objectEnumerator];
498         NSToolbarItem   *item;
500         while(item = [enumerator nextObject]){
501                 if([[item itemIdentifier] isEqualToString:@"UserIcon"]){
502                         [self _updateToolbarItem:item forChat:chat];
503                         break;
504                 }
505         }
509  * @brief Empty action for menu item validation purposes
510  */
511 - (IBAction)dummyAction:(id)sender{};
514  * @brief Menu needs update
516  * Should only be called for a menu off one of our toolbar items in text-only mode, and only when that menu is about
517  * to be displayed. The menu should have two items. The first is added by the system; the second has no title and is
518  * our menu item for showing the image.
519  */
520 - (void)menuNeedsUpdate:(NSMenu *)menu
522         //The first item is a root item inserted by the system. The second item is the single item
523         NSMenuItem              *menuItem = [menu itemAtIndex:1];
524         NSToolbarItem   *toolbarItem = [menuItem representedObject];
526         [menuItem setImage:[[[(ESImageButton *)[toolbarItem view] image] copy] autorelease]];
529 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
531         return YES;
534 @end