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 "AIEmoticonController.h"
18 #import "AIMenuController.h"
19 #import "AIPreferenceController.h"
20 #import "AIToolbarController.h"
21 #import "BGEmoticonMenuPlugin.h"
22 #import <AIUtilities/AIMenuAdditions.h>
23 #import <AIUtilities/AIToolbarUtilities.h>
24 #import <AIUtilities/CBApplicationAdditions.h>
25 #import <AIUtilities/ESImageAdditions.h>
26 #import <AIUtilities/MVMenuButton.h>
27 #import <Adium/AIEmoticon.h>
29 @interface BGEmoticonMenuPlugin(PRIVATE)
30 - (void)registerToolbarItem;
31 - (void)menuNeedsUpdate:(NSMenu *)inMenu;
35 * @class BGEmoticonMenuPlugin
36 * @brief Component to manage the Emoticons menu in its various forms
38 @implementation BGEmoticonMenuPlugin
40 #define PREF_GROUP_EMOTICONS @"Emoticons"
42 #define TITLE_INSERT_EMOTICON AILocalizedString(@"Insert Emoticon",nil)
43 #define TOOLTIP_INSERT_EMOTICON AILocalizedString(@"Insert an emoticon into the text",nil)
44 #define TITLE_EMOTICON AILocalizedString(@"Emoticon",nil)
46 #define TOOLBAR_EMOTICON_IDENTIFIER @"InsertEmoticon"
53 //init the menus and menuItems
54 quickMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_EMOTICON
56 action:@selector(dummyTarget:)
58 quickContextualMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_EMOTICON
60 action:@selector(dummyTarget:)
62 needToRebuildMenus = YES;
64 if([NSApp isOnPantherOrBetter]){
65 /* On Panther and better, create a submenu for these so menuNeedsUpdate will be called
66 * to populate them later. Don't need to check respondsToSelector:@selector(setDelegate:).
68 NSMenu *tempMenu = [[NSMenu alloc] init];
69 [tempMenu setDelegate:self];
70 [quickMenuItem setSubmenu:tempMenu];
73 tempMenu = [[NSMenu alloc] init];
74 [tempMenu setDelegate:self];
75 [quickContextualMenuItem setSubmenu:tempMenu];
79 /* On Jaguar, call menu needs update immediately to make the menus initially populate. They won't autoupdate in
80 * the future, but that's yet another cost of living in 1986. Then again, they do get to hear Run D.M.C.'s
81 * awesome "Walk This Way" cover, so maybe that's worth it to them.
83 [self menuNeedsUpdate:nil];
86 //add the items to their menus.
87 AIMenuController *menuController = [adium menuController];
88 [menuController addContextualMenuItem:quickContextualMenuItem toLocation:Context_TextView_Edit];
89 [menuController addMenuItem:quickMenuItem toLocation:LOC_Edit_Additions];
91 toolbarItems = [[NSMutableSet alloc] init];
92 [self registerToolbarItem];
95 [[NSNotificationCenter defaultCenter] addObserver:self
96 selector:@selector(toolbarWillAddItem:)
97 name:NSToolbarWillAddItemNotification
99 [[NSNotificationCenter defaultCenter] addObserver:self
100 selector:@selector(toolbarDidRemoveItem:)
101 name:NSToolbarDidRemoveItemNotification
105 [[adium preferenceController] registerPreferenceObserver:self forGroup:PREF_GROUP_EMOTICONS];
111 - (void)uninstallPlugin
113 [[NSNotificationCenter defaultCenter] removeObserver:self];
114 [[adium preferenceController] unregisterPreferenceObserver:self];
122 [toolbarItems release];
128 * @brief Add the emoticon menu as an item goes into a toolbar
130 - (void)toolbarWillAddItem:(NSNotification *)notification
132 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
134 if([[item itemIdentifier] isEqualToString:TOOLBAR_EMOTICON_IDENTIFIER]){
135 NSMenu *theEmoticonMenu = [self emoticonMenu];
138 [[item view] setMenu:theEmoticonMenu];
140 //Add menu to toolbar item (for text mode)
141 NSMenuItem *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
142 [mItem setSubmenu:theEmoticonMenu];
143 [mItem setTitle:AILocalizedString(@"Emoticon",nil)];
144 [item setMenuFormRepresentation:mItem];
146 [toolbarItems addObject:item];
151 * @brief Stop tracking when an item is removed from a toolbar
153 - (void)toolbarDidRemoveItem:(NSNotification *)notification
155 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
156 if([[item itemIdentifier] isEqualToString:TOOLBAR_EMOTICON_IDENTIFIER]){
157 [toolbarItems removeObject:item];
162 * @brief Emoticons changed
164 - (void)preferencesChangedForGroup:(NSString *)group key:(NSString *)key
165 object:(AIListObject *)object preferenceDict:(NSDictionary *)prefDict firstTime:(BOOL)firstTime
167 //Flush the cached emoticon menu
168 [emoticonMenu release]; emoticonMenu = nil;
170 //Flag our menus as dirty
171 needToRebuildMenus = YES;
175 * @brief Register our toolbar item
177 - (void)registerToolbarItem
179 NSToolbarItem *toolbarItem;
180 MVMenuButton *button;
182 //Register our toolbar item
183 button = [[[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)] autorelease];
184 [button setImage:[NSImage imageNamed:@"emoticon32" forClass:[self class]]];
185 toolbarItem = [[AIToolbarUtilities toolbarItemWithIdentifier:TOOLBAR_EMOTICON_IDENTIFIER
187 paletteLabel:TITLE_INSERT_EMOTICON
188 toolTip:TOOLTIP_INSERT_EMOTICON
190 settingSelector:@selector(setView:)
192 action:@selector(insertEmoticon:)
194 [toolbarItem setMinSize:NSMakeSize(32,32)];
195 [toolbarItem setMaxSize:NSMakeSize(32,32)];
196 [button setToolbarItem:toolbarItem];
197 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
201 //Menu Generation ------------------------------------------------------------------------------------------------------
202 #pragma mark Menu Generation
204 * @brief Build the emoticon menu
206 * Generation of the menu itself is cached.
208 * @result An autoreleased copy of the cached emoticon menu
210 - (NSMenu *)emoticonMenu
212 NSMenu *emoticonMenuCopy;
215 NSArray *emoticonPacks = [[adium emoticonController] activeEmoticonPacks];
217 if([emoticonPacks count] == 1){
218 //If there is only 1 emoticon pack loaded, do not create submenus
219 emoticonMenu = [[self flatEmoticonMenuForPack:[emoticonPacks objectAtIndex:0]] retain];
222 NSEnumerator *packEnum = [emoticonPacks objectEnumerator];
223 AIEmoticonPack *pack;
224 NSMenuItem *packItem;
226 emoticonMenu = [[NSMenu alloc] initWithTitle:@""];
228 [emoticonMenu setMenuChangedMessagesEnabled:NO];
229 while(pack = [packEnum nextObject]){
230 packItem = [[NSMenuItem alloc] initWithTitle:[pack name] action:nil keyEquivalent:@""];
231 [packItem setSubmenu:[self flatEmoticonMenuForPack:pack]];
232 [emoticonMenu addItem:packItem];
235 [emoticonMenu setMenuChangedMessagesEnabled:YES];
239 //Always return a copy so we can freely modify the menu's item array without messing up our cached copy
240 emoticonMenuCopy = [emoticonMenu copy];
241 if([emoticonMenuCopy respondsToSelector:@selector(setDelegate:)]){
242 [emoticonMenuCopy setDelegate:self];
245 return [emoticonMenuCopy autorelease];
249 * @brief Build a flat emoticon menu for a single pack
251 * @result A menu for the pack
253 - (NSMenu *)flatEmoticonMenuForPack:(AIEmoticonPack *)incomingPack
255 NSMenu *packMenu = [[NSMenu alloc] initWithTitle:TITLE_EMOTICON];
256 NSEnumerator *emoteEnum = [[incomingPack emoticons] objectEnumerator];
257 AIEmoticon *anEmoticon;
259 [packMenu setMenuChangedMessagesEnabled:NO];
261 //loop through each emoticon and add a menu item for each
262 while(anEmoticon = [emoteEnum nextObject]){
263 if([anEmoticon isEnabled] == YES){
264 NSMenuItem *newItem = [[NSMenuItem alloc] initWithTitle:[anEmoticon name]
266 action:@selector(insertEmoticon:)
269 //We need to make a copy of the emoticons for our menu, otherwise the menu flips them in an unpredictable
270 //way, causing problems in the emoticon preferences
271 [newItem setImage:[[anEmoticon image] imageByScalingToSize:NSMakeSize(16, 16)]];
272 [newItem setRepresentedObject:anEmoticon];
273 [packMenu addItem:newItem];
278 [packMenu setMenuChangedMessagesEnabled:YES];
280 return([packMenu autorelease]);
284 //Menu Control ---------------------------------------------------------------------------------------------------------
285 #pragma mark Menu Control
287 * @brief Insert an emoticon into the first responder if possible
289 * First responder must be an editable NSTextView.
291 * @param sender An NSMenuItem whose representedObject is an AIEmoticon
293 - (void)insertEmoticon:(id)sender
295 if([sender isKindOfClass:[NSMenuItem class]]){
296 NSString *emoString = [[[sender representedObject] textEquivalents] objectAtIndex:0];
298 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
299 if(emoString && [responder isKindOfClass:[NSTextView class]] && [(NSTextView *)responder isEditable]){
300 NSRange tmpRange = [(NSTextView *)responder selectedRange];
301 if(0 != tmpRange.length){
302 [(NSTextView *)responder setSelectedRange:NSMakeRange((tmpRange.location + tmpRange.length),0)];
304 [responder insertText:emoString];
310 * @brief Just a target so we get the validateMenuItem: call for the emoticon menu
312 - (IBAction)dummyTarget:(id)sender
318 * @brief Validate menu item
320 * Disable the emoticon menu if a text field is not active
322 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
324 if(menuItem == quickMenuItem || menuItem == quickContextualMenuItem){
325 BOOL haveEmoticons = ([[[adium emoticonController] activeEmoticonPacks] count] != 0);
327 //Disable the main emoticon menu items if no emoticons are available
328 return(haveEmoticons);
331 //Disable the emoticon menu items if we're not in a text field
332 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
333 if(responder && [responder isKindOfClass:[NSText class]]){
334 return([(NSText *)responder isEditable]);
343 * @brief We don't want to get -menuNeedsUpdate: called on every keystroke. This method suppresses that.
345 - (BOOL)menuHasKeyEquivalent:(NSMenu *)menu forEvent:(NSEvent *)event target:(id *)target action:(SEL *)action {
346 *target = nil; //use menu's target
347 *action = NULL; //use menu's action
352 * @brief Update our menus if necessary
354 * Called each time before any of our menus are displayed. If needToRebuildMenus is YES, rebuild them all now,
355 * then set needToRebuildMenus to NO so we don't have to do it next time.
357 * We set the delegate each time we copy because it seems that NSMenu doesn't do so itself when copying. Odd.
359 - (void)menuNeedsUpdate:(NSMenu *)inMenu
361 //Build the emoticon menus if necessary
362 if(needToRebuildMenus){
363 NSMenu *theEmoticonMenu, *tempMenu;
364 NSMenuItem *menuItem;
365 NSEnumerator *enumerator;
366 NSToolbarItem *toolbarItem;
368 //Build the new emoticon menu
369 theEmoticonMenu = [self emoticonMenu];
371 /* For each item, only set its submenu (so we won't have to worry about it in the future) if its current
372 * submenu isn't the one for which we are currently updating. One of them WILL be identical to inMenu, as
373 * that's why we got here (the delegate call) in the first place. For that one, we'll remove inMenu's items
374 * and then add the items from emoticonMenu. */
375 if([quickMenuItem submenu] != inMenu){
376 tempMenu = [theEmoticonMenu copy];
377 [quickMenuItem setSubmenu:tempMenu];
378 if([tempMenu respondsToSelector:@selector(setDelegate:)]){
379 [tempMenu setDelegate:self];
384 if([quickContextualMenuItem submenu] != inMenu){
385 tempMenu = [theEmoticonMenu copy];
386 [quickContextualMenuItem setSubmenu:tempMenu];
387 if([tempMenu respondsToSelector:@selector(setDelegate:)]){
388 [tempMenu setDelegate:self];
393 enumerator = [toolbarItems objectEnumerator];
394 while(toolbarItem = [enumerator nextObject]){
395 if([[toolbarItem view] menu] != inMenu){
396 //We can use the same menu for both
397 tempMenu = [theEmoticonMenu copy];
399 if([tempMenu respondsToSelector:@selector(setDelegate:)]){
400 [tempMenu setDelegate:self];
404 [[toolbarItem view] setMenu:tempMenu];
406 //Add menu to toolbar item (for text mode)
407 [[toolbarItem menuFormRepresentation] setSubmenu:tempMenu];
413 /* Now update inMenu. We update the menu rather than replacing it with another menu so that
414 * the menu will appear properly immediately rather than next time it is viewed. Also, I suspect
415 * it's a bad idea to release inMenu (by replacing it with another one) in the middle of this
418 * Have to copy and autorelease here since the itemArray will change as we go through the items.
420 [inMenu removeAllItems];
421 enumerator = [[[[theEmoticonMenu itemArray] copy] autorelease] objectEnumerator];
422 while (menuItem = [enumerator nextObject]){
424 [theEmoticonMenu removeItem:menuItem];
425 [inMenu addItem:menuItem];
429 needToRebuildMenus = NO;