Libpurple.framework [484]: libpurple 2.2.0
[adiumx.git] / Source / ESiTunesPlugin.m
blobb238ab4c9473b1fe6eff3fdcb824b3ae137020a1
1 /*
2  * ESiTunesPlugin.m
3  * Adium
4  *
5  * Created by Evan Schoenberg on 6/11/05.
6  * Assigned to Kiel Gillard
7  * 
8  * Thanks to GrowlTunes from the Growl project for demonstrating how to receive notifications when 
9  * the iTunes track changes.
10  */
12 #import "ESiTunesPlugin.h"
13 #import <Adium/AIContentControllerProtocol.h>
14 #import <Adium/AIToolbarControllerProtocol.h>
15 #import "AIStatusController.h"
16 #import <Adium/AIMenuControllerProtocol.h>
17 #import <Adium/AIAccount.h>
18 #import <AIUtilities/AIAttributedStringAdditions.h>
19 #import <AIUtilities/AIToolbarUtilities.h>
20 #import <AIUtilities/AIImageAdditions.h>
21 #import <AIUtilities/MVMenuButton.h>
22 #import <AIUtilities/AIMenuAdditions.h>
23 #import <AIUtilities/AIWindowAdditions.h>
24 #import <AIUtilities/AIStringAdditions.h>
25 #import <Adium/AIHTMLDecoder.h>
26 #import <Adium/AIStatus.h>
27 #import <WebKit/WebKit.h>
29 #define ITUNES_MINIMUM_VERSION          4.6f
30 #define ITUNES_STATUS_ID                        -8000
32 #pragma mark -
34 #define PLAYER_STATE                            @"Player State"
35 #define KEY_PLAYING                                     @"Playing"
36 #define KEY_PAUSED                                      @"Paused"
37 #define KEY_STOPPED                                     @"Stopped"
38 #define CURRENT_TRACK_FORMAT_KEY        @"Current Track Format"
39 #define ITMS_SEARCH_URL                         @"itms://itunes.com/link?"
40                                                                         //itms://phobos.apple.com/WebObjects/MZSearch.woa/wa/search?"
41 #pragma mark -
43 #define ALBUM_TRIGGER                           AILocalizedString(@"%_album","Trigger for the album of the currently playing iTunes song")
44 #define ARTIST_TRIGGER                          AILocalizedString(@"%_artist","Trigger for the artist of the currently playing iTunes song")
45 #define COMPOSER_TRIGGER                        AILocalizedString(@"%_composer","Trigger for the composer of the currently playing iTunes song")
46 #define GENRE_TRIGGER                           AILocalizedString(@"%_genre","Trigger for the genre of the currently playing iTunes song")
47 #define STATUS_TRIGGER                          AILocalizedString(@"%_status","Trigger for the genre of the currently playing iTunes song")
48 #define TRACK_TRIGGER                           AILocalizedString(@"%_track","Trigger for the name of the currently playing iTunes song")
49 #define STORE_URL_TRIGGER                       AILocalizedString(@"%_iTMS","Trigger for an iTunes Music Store link to the currently playing iTunes song")
50 #define MUSIC_TRIGGER                           AILocalizedString(@"%_music","Command which triggers *is listening to %_track by %_artist*")
51 #define CURRENT_TRACK_TRIGGER           AILocalizedString(@"%_iTunes","Trigger for the song - artist of the currently playing iTunes song")
53 #define MUSICAL_NOTE                            [NSString stringWithUTF8String:"\342\231\253"]
54 #define CURRENT_ITUNES_TRACK            [NSString stringWithFormat:@"%@ iTunes", MUSICAL_NOTE]
56 #define TOOLBAR_LABEL                           AILocalizedString(@"iTunes","Label for iTunes toolbar menu item.")
58 #pragma mark -
60 #define ITUNES_TOOLBAR_ITEM                     @"iTunesItem"
61 #define INSERT_TRIGGERS_MENU            AILocalizedString(@"Insert iTunes Token", "Label used for edit and contextual menus of iTunes triggers")
63 #pragma mark -
65 @interface ESiTunesPlugin (PRIVATE)
66 - (NSMenuItem *)menuItemWithTitle:(NSString *)title action:(SEL)action representedObject:(id)representedObject kind:(KGiTunesPluginMenuItemKind)itemKind;
67 - (void)createiTunesCurrentTrackStatusState;
68 - (void)createiTunesToolbarItemWithPath:(NSString *)path;
69 - (void)createiTunesToolbarItemMenuItems:(NSMenu *)iTunesMenu;
70 - (void)createTriggersMenu;
71 - (void)filterAndInsertString:(NSString *)inString;
72 - (void)insertStringIntoMessageEntryView:(NSString *)inString;
73 - (void)insertAttributedStringIntoMessageEntryView:(NSAttributedString *)inString;
74 - (void)loadiTunesCurrentInfoViaApplescript;
75 @end
77 /*!
78  * @class ESiTunesPlugin
79  * @brief Fiiltering component to provide triggers which are replaced by information from the current iTunes track
80  */
81 @implementation ESiTunesPlugin
83 #pragma mark -
84 #pragma mark Accessor Methods
86 /*!
87  * @brief Is iTunes stopped?
88  */
89 - (BOOL)iTunesIsStopped
91         //Get the info if we don't already have it
92         if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
94         return iTunesIsStopped;
97 /*!
98  * @brief Set if iTunes is stopped
99  */
100 - (void)setiTunesIsStopped:(BOOL)yesOrNo
102         iTunesIsStopped = yesOrNo;
106 * @brief Is iTunes paused?
107  */
108 - (BOOL)iTunesIsPaused
110         //Get the info if we don't already have it
111         if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
112         
113         return iTunesIsPaused;
117  * @brief Set if iTunes is paused
118  */
119 - (void)setiTunesIsPaused:(BOOL)yesOrNo
121   iTunesIsPaused = yesOrNo;
126  * @brief Get current iTunes info dictionary
127  */
128 - (NSDictionary *)iTunesCurrentInfo
130         return iTunesCurrentInfo;
134  * @brief Store local copy of iTunes information
135  * 
136  * Retains new information, requests immediate content update and lets the plugin know what iTunes is doing.
137  */
138  - (void)setiTunesCurrentInfo:(NSDictionary *)newInfo
140         if (newInfo != iTunesCurrentInfo) {
141                 [iTunesCurrentInfo release];
142                 iTunesCurrentInfo = [newInfo retain];
144                 [self setiTunesIsStopped:[[newInfo objectForKey:PLAYER_STATE] isEqualToString:KEY_STOPPED]];
145                 [self setiTunesIsPaused:[[newInfo objectForKey:PLAYER_STATE] isEqualToString:KEY_PAUSED]];
146         
147         //Cancel any requests we had to fire updates.
148         [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(fireUpdateiTunesInfo) object:nil];
149         //fire an iTunes update in three seconds.
150         [self performSelector:@selector(fireUpdateiTunesInfo) withObject:nil afterDelay:3.0];
151         }
154  -(void)fireUpdateiTunesInfo
156      [[adium notificationCenter] postNotificationName:Adium_RequestImmediateDynamicContentUpdate object:nil];
159 #pragma mark -
160 #pragma mark Plugin Methods
163  * @brief Install
164  */
165 - (void)installPlugin
167         NSDictionary    *slashMusicDict = nil;
168         NSDictionary    *conditionalArtistTrackDict = nil;
169         NSString                *currentITunesTrackFormat = nil;
170         NSUserDefaults  *defaults = [NSUserDefaults standardUserDefaults];
171         NSString                *itunesPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.apple.iTunes"];
173         iTunesCurrentInfo = nil;
175         //Only install our items if a copy of iTunes which meets the minimum requirements is found
176         if ([[[NSBundle bundleWithPath:itunesPath] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] floatValue] > ITUNES_MINIMUM_VERSION) {
177                 
178                 //Perform substitutions on outgoing content
179                 [[adium contentController] registerContentFilter:self 
180                                                                                                   ofType:AIFilterContent
181                                                                                            direction:AIFilterOutgoing];
182                 
183                 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
184                                                                                                                         selector:@selector(iTunesUpdate:)
185                                                                                                                                 name:@"com.apple.iTunes.playerInfo"
186                                                                                                                           object:nil];
187                 
188                 substitutionDict = [[NSDictionary alloc] initWithObjectsAndKeys:
189                         @"Album", ALBUM_TRIGGER,
190                         @"Artist", ARTIST_TRIGGER,
191                         @"Composer", COMPOSER_TRIGGER,
192                         @"Genre", GENRE_TRIGGER,
193                         @"Player State", STATUS_TRIGGER,
194                         @"Name", TRACK_TRIGGER,
195                         @"Store URL", STORE_URL_TRIGGER,
196                         nil];
197                 
198                 slashMusicDict = [[NSDictionary alloc] initWithObjectsAndKeys:
199                         [NSString stringWithFormat:AILocalizedString(@"*is listening to %@ by %@*","Phrase sent in response to %_music.  The first %%@ is the track; the second %%@ is the artist."), TRACK_TRIGGER, ARTIST_TRIGGER],
200                         KEY_PLAYING,
201                         AILocalizedString(@"*is listening to nothing*","Phrase sent in response to %_music when nothing is playing."),
202                         KEY_STOPPED,
203                         nil];
204                 
205                 /* Provide flexibility with the %_iTunes substitution. By default, just store @"" for this key
206                  * so the item is present in the plist for the adventurous to find... but still not hardcoded to a particular
207                  * format.  This is done so that a default installation doesn't have its format broken if the locale switches...
208                  * since the format specifiers are themselves localized.
209                  */
210                 currentITunesTrackFormat = [defaults objectForKey:CURRENT_TRACK_FORMAT_KEY];
211                 if (!currentITunesTrackFormat) {
212                         [defaults setObject:@"" forKey:CURRENT_TRACK_FORMAT_KEY];
213                         currentITunesTrackFormat = @"";
214                 }
215                 
216                 if (![currentITunesTrackFormat length]) {
217                         currentITunesTrackFormat  = [NSString stringWithFormat:@"%@ - %@", TRACK_TRIGGER, ARTIST_TRIGGER];
218                 }
219                 
220                 conditionalArtistTrackDict = [[NSDictionary alloc] initWithObjectsAndKeys:
221                         currentITunesTrackFormat,
222                         KEY_PLAYING,
223                         @"",
224                         KEY_STOPPED,
225                         nil];
226                 
227                 phraseSubstitutionDict = [[NSDictionary alloc] initWithObjectsAndKeys:
228                         slashMusicDict,
229                         MUSIC_TRIGGER,
230                         conditionalArtistTrackDict,
231                         CURRENT_TRACK_TRIGGER,
232                         nil];
233                 
234                 [slashMusicDict release];
235                 [conditionalArtistTrackDict release];
236                 
237                 //Create the "Current iTunes Track" status item
238                 [self createiTunesCurrentTrackStatusState];
239                 
240                 //Create the toolbar item
241                 [self createiTunesToolbarItemWithPath:itunesPath];
242                 
243                 //Create the Edit > Insert and contextual menus
244                 [self createTriggersMenu];
245         }
249  * @brief Uninstall
250  */
251 - (void)uninstallPlugin
253         [[adium contentController] unregisterContentFilter:self];
256 #pragma mark -
257 #pragma mark AppleScript + iTunes methods
260  * @brief Get current iTunes track info
262  * Execute an applescript located in the resources folder that obtains current iTunes track info and assembles the dictionary
263  */
264 - (void)loadiTunesCurrentInfoViaApplescript
266         /*
267          * 1. get a url pointing to the script in the resources folder
268          * 2. prepare the script for execution
269          * 3. get results and create the dictionary based off it
270          */
271         
272         //get the path
273         NSString                                *path = [[NSBundle mainBundle] pathForResource:@"CurrentTunes" ofType:@"scpt"];
274         NSURL                                   *pathURL = [NSURL fileURLWithPath:path];
275         
276         //create the script complete with an error dictionary
277         NSDictionary                    *errors = [NSDictionary dictionary];
278         NSAppleScript                   *playingScript = [[NSAppleScript alloc] initWithContentsOfURL:pathURL error:&errors];
279         
280         //execute the script and get the results as a string
281     NSAppleEventDescriptor      *result = [playingScript executeAndReturnError:&errors];
282         NSString                                *concatenatediTunesData = [result stringValue];
283         
284         //if the player was playing when the script was executed
285         if (![concatenatediTunesData isEqualToString:@"None"]) {
286                 
287                 //get the expected number of entries in the dictionary
288                 unsigned infoCount = [substitutionDict count];
289                 //get the values for the current iTunes song from the string
290                 NSArray * iTunesValues = [concatenatediTunesData componentsSeparatedByString:@",$!$,"];
291                 
292                 //if the two are properly matched (which they always will be, but just in case)
293                 if ([iTunesValues count] == infoCount) {
294                         //create the dictionary
295                         [self setiTunesCurrentInfo:[NSDictionary dictionaryWithObjects:iTunesValues 
296                                                                                                                                    forKeys:[NSArray arrayWithObjects:@"Album",
297                                                                                                                                                                                                          @"Artist",
298                                                                                                                                                                                                          @"Composer",
299                                                                                                                                                                                                          @"Genre",
300                                                                                                                                                                                                          PLAYER_STATE,
301                                                                                                                                                                                                          @"Name",
302                                                                                                                                                                                                          @"Store URL",
303                                                                                                                                                                                                          nil]]];
304                 }
305                 
306         } else {
307                 //create a dictionary saying that iTunes is stopped
308                 [self setiTunesCurrentInfo:[NSDictionary dictionaryWithObjectsAndKeys:KEY_STOPPED, PLAYER_STATE, nil]];
309         }
310         
311         [playingScript release];
314 #pragma mark -
315 #pragma mark Status item creation
318  * @brief Create an available status state
320  * Create a Status which uses the current iTunes track data as it's message
321  */
322 - (void)createiTunesCurrentTrackStatusState
324         //create an iTunes status of state "Available" with default available status settings
325         AIStatus                   *currentiTunesStatusState = [[AIStatus statusOfType:AIAvailableStatusType] retain];
326         
327         //set status attributes
328         NSAttributedString *trackAndArtist = [NSAttributedString stringWithString:CURRENT_TRACK_TRIGGER];
329         [currentiTunesStatusState setStatusMessage:trackAndArtist];
330         [currentiTunesStatusState setTitle:CURRENT_ITUNES_TRACK];
331         [currentiTunesStatusState setMutabilityType:AISecondaryLockedStatusState];
332         [currentiTunesStatusState setUniqueStatusID:[NSNumber numberWithInt:ITUNES_STATUS_ID]];
333         [currentiTunesStatusState setSpecialStatusType:AINowPlayingSpecialStatusType];
335         //give it to the AIStatusController
336         [[adium statusController] addStatusState:currentiTunesStatusState];
337         [currentiTunesStatusState release];
340 #pragma mark -
341 #pragma mark Filter Protocol methods
344  * @brief Filter messages for keywords to replace
346  * Replace any iTunes triggers with the appropriate information
347  */
348 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)inAttributedString context:(id)context
350     NSMutableAttributedString   *filteredMessage = nil;
351         NSString                                        *stringMessage;
352         
353         //get the attributed string as a regular string so we can do string processing
354         if ((stringMessage = [inAttributedString string])) {
355                 NSEnumerator    *enumerator;
356                 NSString                *trigger;
357                 BOOL                    addStoreLinkAsSubtext = NO;
358                 
359                 /* Replace the phrases with the string containing the triggers.
360                  * For example, /music will become *is listening to %_track by %_artist*.
361                  * This will then become the actual track information in the next while().
362                  */
363                 enumerator = [phraseSubstitutionDict keyEnumerator];
364                 
365                 while ((trigger = [enumerator nextObject])) {
366                         //search for phrase in the string that needs to be filtered
367                         if (([stringMessage rangeOfString:trigger options:(NSLiteralSearch | NSCaseInsensitiveSearch)].location != NSNotFound)) {
368                                 NSDictionary    *replacementDict;
369                                 NSString                *replacement;
370                                 
371                                 //get the format for the current trigger
372                                 replacementDict = [phraseSubstitutionDict objectForKey:trigger];
373                                                                 
374                                 //replacement of phrase should reflect iTunes player state
375                                 if (![self iTunesIsStopped] && ![self iTunesIsPaused]) {
376                                         replacement = [replacementDict objectForKey:KEY_PLAYING];
377                                         
378                                         /* If the trigger is the trigger used for the Current iTunes Track status, we'll want to add a subtext of the store link
379                                          * so account code can send it out later on.
380                                          */
381                                         if ([trigger isEqualToString:CURRENT_TRACK_TRIGGER]) {
382                                                 addStoreLinkAsSubtext = YES;
383                                         }
385                                 } else {
386                                         replacement = [replacementDict objectForKey:KEY_STOPPED];                                       
387                                 }
388                                 
389                                 //create a attributedstring if it hasn't been created already
390                                 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
391                                 
392                                 //Perform the replacement
393                                 [filteredMessage replaceOccurrencesOfString:trigger
394                                                                                                  withString:replacement
395                                                                                                         options:(NSLiteralSearch | NSCaseInsensitiveSearch)
396                                                                                                           range:NSMakeRange(0, [filteredMessage length])];
397                         }
398                 }
399                 
400                 if (filteredMessage) {
401                         //Update our string for the simple trigger replacement process so we can replace the %_ tokens
402                         stringMessage = [filteredMessage string];
403                 }
404                 
405                 //Substitute simple triggers as appropriate
406                 enumerator = [substitutionDict keyEnumerator];
407                 while ((trigger = [enumerator nextObject])) {
408                         
409                         //Find if the current trigger is in the string
410                         if (([stringMessage rangeOfString:trigger options:(NSLiteralSearch | NSCaseInsensitiveSearch)].location != NSNotFound)) {
411                                 NSString *replacement;
412                                 
413                                 //Get the info if we don't already have it
414                                 if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
416                                 //Attempt to replace it with its proper value
417                                 if (!(replacement = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:trigger]])) {
418                                         //If no replacement is found, replace the trigger with an empty string
419                                         replacement = @"";
420                                 }
421                                 
422                                 //if a mutable attributed string for the string to be filtered doesn't exist, create it. 
423                                 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
424                                 
425                                 //Replace the current trigger with the value we found above
426                                 [filteredMessage replaceOccurrencesOfString:trigger
427                                                                                                  withString:replacement
428                                                                                                         options:(NSLiteralSearch | NSCaseInsensitiveSearch)
429                                                                                                           range:NSMakeRange(0, [filteredMessage length])];
430                         }
431                 }
432                 
433                 if (addStoreLinkAsSubtext && filteredMessage) {
434                         NSString *storeLinkForSubtext = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:STORE_URL_TRIGGER]];
435                         if (storeLinkForSubtext) {
436                                 [filteredMessage addAttribute:@"AIMessageSubtext"
437                                                                                 value:storeLinkForSubtext
438                                                                                 range:NSMakeRange(0, [filteredMessage length])];
439                         }
440                 }
441         }
442                 
443         //Give back the processed string
444         return (filteredMessage ? filteredMessage : inAttributedString);
448  * @brief Filter priority
450  * Filter at default priority
451  */
452 - (float)filterPriority
454         return DEFAULT_FILTER_PRIORITY;
457 #pragma mark -
458 #pragma mark Notification Selector
461  * @brief The iTunes song changed
463  * The accessor method caches the information and then requst an immediate update to dynamic content
464  */
465 - (void)iTunesUpdate:(NSNotification *)aNotification
467         NSDictionary *newInfo = [aNotification userInfo];
469         [self setiTunesCurrentInfo:newInfo];
472 #pragma mark -
473 #pragma mark Toolbar Item Methods
476  * @brief Create the toolbar item
478  * Create toolbar item and it's menu
479  */
480 - (void)createiTunesToolbarItemWithPath:(NSString *)iTunesPath
482         NSMenu            *menu = [[NSMenu alloc] init];
483         MVMenuButton  *button = [[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)];
485         //configure the popup button and its menu
486         [button setImage:[[NSWorkspace sharedWorkspace] iconForFile:iTunesPath]];
487         [self createiTunesToolbarItemMenuItems:menu];
489         NSToolbarItem * iTunesItem = [AIToolbarUtilities toolbarItemWithIdentifier:ITUNES_TOOLBAR_ITEM
490                                                                                                                                                  label:TOOLBAR_LABEL
491                                                                                                                                   paletteLabel:TOOLBAR_LABEL
492                                                                                                                                            toolTip:AILocalizedString(@"Insert current iTunes track information.","Label for iTunes toolbar menu item.")
493                                                                                                                                                 target:self
494                                                                                                                            settingSelector:@selector(setView:)
495                                                                                                                                    itemContent:button
496                                                                                                                                                 action:NULL
497                                                                                                                                                   menu:nil];
498         //configure the toolbar and button for use
499         [[iTunesItem view] setMenu:menu];
500         [iTunesItem setMinSize:NSMakeSize(32,32)];
501         [iTunesItem setMaxSize:NSMakeSize(32,32)];
502         [button setToolbarItem:iTunesItem];
503         
504         //Add menu to toolbar item (for text mode)
505         NSMenuItem      *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
506         [mItem setSubmenu:menu];
507         [mItem setTitle:TOOLBAR_LABEL];
508         [iTunesItem setMenuFormRepresentation:mItem];
509         
510         //give it to adium to use
511         [[adium toolbarController] registerToolbarItem:iTunesItem forToolbarType:@"TextEntry"];
512         [button release];
513         [menu release];
517  * @brief Create the toolbar item's menu
519  * Populate a menu with menu items that will insert appropriate values of the currently playing iTunes song.
520  */
522 - (void)createiTunesToolbarItemMenuItems:(NSMenu *)iTunesMenu
523 {       
524         NSMenu *insertTrackSubmenu = [[NSMenu alloc] init];
525         
526         [iTunesMenu addItem:[self menuItemWithTitle:CURRENT_ITUNES_TRACK 
527                                                                                  action:@selector(insertFilteredString:) 
528                                                           representedObject:CURRENT_TRACK_TRIGGER
529                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
530         [iTunesMenu addItem:[self menuItemWithTitle:MUSIC_TRIGGER
531                                                                                  action:@selector(insertFilteredString:) 
532                                                           representedObject:MUSIC_TRIGGER                                                                                  
533                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
534         [iTunesMenu addItem:[NSMenuItem separatorItem]];
535         
536         //submenu of actions related to a track
537         NSMenuItem *submenuRoot = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Track Information","Submenu for iTunes toolbar item menu for inserting current track information.")
538                                                                                                                  action:NULL
539                                                                                                   keyEquivalent:@""];
540         
541         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Album","Insert Current iTunes track album toolbar menu item.") 
542                                                                                                  action:@selector(insertFilteredString:)
543                                                                           representedObject:ALBUM_TRIGGER
544                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
545         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Artist","Insert Current iTunes track artist toolbar menu item.") 
546                                                                                                  action:@selector(insertFilteredString:) 
547                                                                           representedObject:ARTIST_TRIGGER
548                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
549         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Composer","Insert Current iTunes track composer toolbar menu item.") 
550                                                                                                  action:@selector(insertFilteredString:)
551                                                                           representedObject:COMPOSER_TRIGGER
552                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
553         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Genre","Insert Current iTunes track genre toolbar menu item.") 
554                                                                                                  action:@selector(insertFilteredString:)
555                                                                           representedObject:GENRE_TRIGGER
556                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
557         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Name","Insert Current iTunes track name toolbar menu item.") 
558                                                                                                  action:@selector(insertFilteredString:)
559                                                                           representedObject:TRACK_TRIGGER
560                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
561         
562         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"iTunes Music Store Link","Insert Current iTunes track store link toolbar menu item.") 
563                                                                                                  action:@selector(insertiTMSLink)
564                                                                           representedObject:nil
565                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
567         [iTunesMenu addItem:submenuRoot];
568         [iTunesMenu setSubmenu:insertTrackSubmenu forItem:submenuRoot];
569         [submenuRoot release];
570         [insertTrackSubmenu release];
571         [iTunesMenu addItem:[NSMenuItem separatorItem]];
573         //this isn't implemented yet, need some advice on this one
574         [iTunesMenu addItem:[self menuItemWithTitle:[AILocalizedString(@"Search Selection in Music Store","iTunes toolbar menu item title to search selection in iTMS.") stringByAppendingEllipsis]
575                                                                                  action:@selector(gatherSelection)
576                                                           representedObject:nil
577                                                                                    kind:RESPONDER_IS_WEBVIEW]];
578         [iTunesMenu addItem:[NSMenuItem separatorItem]];
580         [iTunesMenu addItem:[self menuItemWithTitle:[AILocalizedString(@"Bring iTunes to Front","iTunes toolbar menu item title to make iTunes frontmost app.") stringByAppendingEllipsis]
581                                                                                  action:@selector(bringiTunesToFront)
582                                                           representedObject:nil
583                                                                                    kind:ALWAYS_ENABLED]];
587  * @brief Create a menu item
589  * create a menu item targeting this plugin. Determine if it should disable itself when firstResponder != [textView class].
590  */
591 - (NSMenuItem *)menuItemWithTitle:(NSString *)title action:(SEL)action representedObject:(id)representedObject kind:(KGiTunesPluginMenuItemKind)itemKind
593         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:action keyEquivalent:@""];
594         [item setTarget:self];
595         [item setTag:itemKind];
596         [item setRepresentedObject:representedObject];
597         [item setEnabled:YES];
599         return [item autorelease];
602 #pragma mark -
603 #pragma mark Toolbar Item actions
606  * @brief Insert current song iTMS link
608  * Get the URL from the iTunesCurrentInfo dict or create a URL if one can't be found.
609  */
610 - (void)insertiTMSLink
612         NSMutableString         *url = [[NSMutableString alloc] init];
613         NSString                        *urlLabel = nil;
615         //get current information
616         NSString                        *artist = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:ARTIST_TRIGGER]];
617         NSString                        *trackName = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:TRACK_TRIGGER]];
618         
619         //see if we have a URL for us
620         NSString                        *storeURL = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:STORE_URL_TRIGGER]];
621         if ([storeURL length]) {
622                 [url appendString:storeURL];
623         }
624         
625         //if we have no url data from the iTunes notification to begin with - probably because we got the info using the applescript
626         if (![url length]) {
627                 
628                 //if iTunes is playing or paused something
629                 if (![self iTunesIsStopped] || ![self iTunesIsPaused]) {
630                         [url appendString:ITMS_SEARCH_URL];
631                         
632                         //if there is a name given to this song put it in the url
633                         if ([trackName length]) {
634                                 [url appendFormat:@"n=%@", [trackName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
635                         } else {
636                                 trackName = @"";
637                         }
638                         
639                         //if there is a name and an artist, we'll use both to refine our search
640                         if ([artist length] && [trackName length]) {
641                                 //[url appendFormat:@"?artistTerm=%@", [artist stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
642                                 [url appendFormat:@"&an=%@", [artist stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
643                                 
644                         } else if ([artist length]) {
645                                 //no proper track name but we have a decent artist name to include in the url
646                                 //[url appendFormat:@"artistTerm=%@", [trackName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
647                                 [url appendFormat:@"an=%@", [trackName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
648                         } else {
649                                 artist = @"";
650                         }
651                 }
652         }
653         
654         //if something has been added to our search request, create a lovely label for it
655         if (![url isEqualToString:ITMS_SEARCH_URL] && [url length]) {
656                 urlLabel = [[NSString alloc] initWithFormat:@"%@ - %@", trackName, artist];
657         } else {
658                 [url release]; url = nil;
659         }
660         
661         //if we have a url, give it to the user as a nice, formatted <a> tag
662         if (url) {
663                 NSAttributedString *attributedLink = [[NSAttributedString alloc] initWithAttributedString:[AIHTMLDecoder decodeHTML:[NSString stringWithFormat:@"<A HREF=\"%@\">%@</A>", url, urlLabel]]];
664                 [self insertAttributedStringIntoMessageEntryView:attributedLink];
665                 [attributedLink release];
666                 [url release];
667                 [urlLabel release];
668         } else {
669                 //the artist name and or the track name is literally @""
670                 NSBeep();
671         }
675  * @brief Filter and insert current iTunes song display into message entry
677  * Toolbar method. Take the trigger and filter it with real values
679  * @param sender An NSMenuItem whose representedObject is the appropriate trigger to filter
680  */
681 - (void)insertFilteredString:(id)sender
683         [self filterAndInsertString:[sender representedObject]];        
687  * @brief Search iTMS for inputtted data
689  * Build the necessary url and execute it
690  */
691 - (void)searchMusicStoreWithSelection:(NSString *)selectedText
693         //Create a general search request
694         NSString *url = [NSString stringWithFormat:@"itms://phobos.apple.com/WebObjects/MZSearch.woa/wa/search?term=%@", [selectedText stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
695         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
700  * @brief Get the selection from the webmessageview
702  * Get the selected text in the messageview and run it thru the iTMS
703  */
704 - (void)gatherSelection
706         id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
707         [self searchMusicStoreWithSelection:[responder selectedString]];
711  * @brief Bring iTunes to foreground
712  */
713 - (void)bringiTunesToFront
715         [[NSWorkspace sharedWorkspace] launchApplication:@"iTunes"];
718 #pragma mark -
719 #pragma mark Edit/Contextual menu item actions
722  * @brief Insert triggers into message entry
724  * Used in the "Edit" and contextual menus.
725  * @param sender An NSMenuItem whose representedObject is the appropriate trigger to insert
726  */
727 - (void)insertUnfilteredString:(id)sender
729         [self insertStringIntoMessageEntryView:[sender representedObject]];
732 #pragma mark -
733 #pragma mark Text Insertion methods
736  * @brief Filter and Insert plain string
738  * Converts the string to an attributed string and filters it, then inserting it into the message entry view
739  * Used by all the toolbar item actions.
740  */
742 - (void)filterAndInsertString:(NSString *)inString
744         id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
745         if (responder && [responder isKindOfClass:[NSTextView class]]) {
746                 NSAttributedString *attributedResult = [[NSAttributedString alloc] initWithString:inString
747                                                                                                                                                            attributes:[(NSTextView *)responder typingAttributes]];
748                 [self insertAttributedStringIntoMessageEntryView:[self filterAttributedString:attributedResult context:nil]];
749                 [attributedResult release];
750         }
754  * @brief Insert raw string into message view
756  * Converts the string to an attributed string and inserts it into the message entry view.
757  * Used with the insertUnfiltered... methods which are used by edit and contextual menus.
758  */
760 - (void)insertStringIntoMessageEntryView:(NSString *)inString
762         id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
763         if (responder && [responder isKindOfClass:[NSTextView class]]) {
764                 NSAttributedString *attributedResult = [[NSAttributedString alloc] initWithString:inString 
765                                                                                                                                                            attributes:[(NSTextView *)responder typingAttributes]];
766                 [self insertAttributedStringIntoMessageEntryView:attributedResult];
767                 [attributedResult release];
768         }
772  * @brief Insert attributed string into message view
774  * Inserts an attributed string it into the message entry view.
775  * Don't check to see if the responder is of class NSTextView because the validateMenuItem method checks.
776  */
778 - (void)insertAttributedStringIntoMessageEntryView:(NSAttributedString *)inString
780         NSResponder *textView = [[[NSApplication sharedApplication] keyWindow] firstResponder];
781         [textView insertText:inString];
782         
783         if (![inString length]) {
784                 NSBeep();
785         }
788 #pragma mark -
789 #pragma mark Edit/Contextual menu methods
792  * @brief Create Edit and Contextual menus of iTunes triggers
794  * Build the menus for the iTunes triggers that autodisables when a first responder isn't a textView
795  */
797 - (NSMenu *)menuOfTriggers
799         NSMenu                  *triggersMenu = [[NSMenu alloc] init];
800         NSEnumerator    *enumerator;
801         NSString                *trigger;
803         [triggersMenu addItem:[self menuItemWithTitle:CURRENT_TRACK_TRIGGER 
804                                                                                    action:@selector(insertUnfilteredString:)
805                                                                 representedObject:CURRENT_TRACK_TRIGGER
806                                                                                          kind:AUTODISABLES]];
807         [triggersMenu addItem:[self menuItemWithTitle:MUSIC_TRIGGER
808                                                                                    action:@selector(insertUnfilteredString:)
809                                                                 representedObject:MUSIC_TRIGGER
810                                                                                          kind:AUTODISABLES]];
812         
813         [triggersMenu addItem:[NSMenuItem separatorItem]];
814         
815         //Simple triggers
816         enumerator = [substitutionDict keyEnumerator];
817         while ((trigger = [enumerator nextObject])) {
818                 [triggersMenu addItem:[self menuItemWithTitle:trigger 
819                                                                                            action:@selector(insertUnfilteredString:) 
820                                                                         representedObject:trigger
821                                                                                                  kind:AUTODISABLES]];
822         }
823         
824         return [triggersMenu autorelease];
828  * @brief Create Edit and Contextual menus of iTunes triggers
830  * Users can then insert %_&lt;token name&gt; into any text view
831  */
832 - (void)createTriggersMenu
834         NSMenuItem      *menuItem = [[NSMenuItem alloc] initWithTitle:INSERT_TRIGGERS_MENU target:self action:NULL keyEquivalent:@""];
835         NSMenu          *menuOfTriggers = [self menuOfTriggers];
836         
837         [menuItem setSubmenu:menuOfTriggers];
838         [[adium menuController] addMenuItem:menuItem toLocation:LOC_Edit_Additions];
839         [menuItem release];
840         
841         menuItem = [[NSMenuItem alloc] initWithTitle:INSERT_TRIGGERS_MENU target:self action:NULL keyEquivalent:@""];
842         [menuItem setSubmenu:[[menuOfTriggers copy] autorelease]];
843         [[adium menuController] addContextualMenuItem:menuItem toLocation:Context_TextView_Edit];
844         [menuItem release];
848  * @brief Configure accessibility of menu items
850  * Depending on whether the responder is a textview and if it should be enabled when itunes isn't playing
851  */
852 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
854         NSResponder                                     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
855         KGiTunesPluginMenuItemKind      tag = [menuItem tag];
856         BOOL                                            enable;
858         //we only insert things into textviews
859         if (responder && [responder isKindOfClass:[NSTextView class]]) {
860                 
861                 //some menu items are only enabled if itunes is playing something
862                 if ((([self iTunesIsStopped] || [self iTunesIsPaused]) && (tag == ENABLED_IF_ITUNES_PLAYING)) || (tag == RESPONDER_IS_WEBVIEW)) {
863                         enable = NO;
864                 } else {
865                         enable = [(NSTextView *)responder isEditable];
866                 }
868         } else if (tag == RESPONDER_IS_WEBVIEW) {
869                 
870                 if ([responder respondsToSelector:@selector(selectedString)]) {
871                         NSString        *selectedString = [(id)responder selectedString];
872                         
873                         if (selectedString && [selectedString length]) {
874                                 enable = YES;
875                         } else {
876                                 enable = NO;
877                         }
879                 } else {
880                         enable = NO;                    
881                 }
882                 
883         } else {
884                 // enable it if it is always supposed to be on, disable if otherwise
885                 enable = (tag == ALWAYS_ENABLED);
886         }
887         
888         return enable;
892 #pragma mark -
893 #pragma mark Deallocation
895 - (void)dealloc
897         //Remove self from notifications list
898         [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
899         
900         //Release class variables
901         if (iTunesCurrentInfo) [iTunesCurrentInfo release];
902         if (substitutionDict) [substitutionDict release];
903         if (phraseSubstitutionDict) [phraseSubstitutionDict release];
904         
905         [super dealloc];
908 @end