Allow localization of the Alert Text label in the Display an Alert action
[adiumx.git] / Source / ESiTunesPlugin.m
blobc829333621eb6a86a863f75f7b93a18acd06997f
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 YEAR_TRIGGER                            AILocalizedString(@"%_year","Trigger for the year of the currently playing iTunes song")
50 #define STORE_URL_TRIGGER                       AILocalizedString(@"%_iTMS","Trigger for an iTunes Music Store link to the currently playing iTunes song")
51 #define MUSIC_TRIGGER                           AILocalizedString(@"%_music","Command which triggers *is listening to %_track by %_artist*")
52 #define CURRENT_TRACK_TRIGGER           AILocalizedString(@"%_iTunes","Trigger for the song - artist of the currently playing iTunes song")
54 #define MUSICAL_NOTE                            [NSString stringWithUTF8String:"\342\231\253"]
55 #define CURRENT_ITUNES_TRACK            [NSString stringWithFormat:@"%@ iTunes", MUSICAL_NOTE]
57 #define TOOLBAR_LABEL                           AILocalizedString(@"iTunes","Label for iTunes toolbar menu item.")
59 #pragma mark -
61 #define ITUNES_TOOLBAR_ITEM                     @"iTunesItem"
62 #define INSERT_TRIGGERS_MENU            AILocalizedString(@"Insert iTunes Token", "Label used for edit and contextual menus of iTunes triggers")
64 #pragma mark -
66 @interface ESiTunesPlugin (PRIVATE)
67 - (NSMenuItem *)menuItemWithTitle:(NSString *)title action:(SEL)action representedObject:(id)representedObject kind:(KGiTunesPluginMenuItemKind)itemKind;
68 - (void)createiTunesCurrentTrackStatusState;
69 - (void)createiTunesToolbarItemWithPath:(NSString *)path;
70 - (void)createiTunesToolbarItemMenuItems:(NSMenu *)iTunesMenu;
71 - (void)createTriggersMenu;
72 - (void)filterAndInsertString:(NSString *)inString;
73 - (void)insertStringIntoMessageEntryView:(NSString *)inString;
74 - (void)insertAttributedStringIntoMessageEntryView:(NSAttributedString *)inString;
75 - (void)loadiTunesCurrentInfoViaApplescript;
76 @end
78 /*!
79  * @class ESiTunesPlugin
80  * @brief Fiiltering component to provide triggers which are replaced by information from the current iTunes track
81  */
82 @implementation ESiTunesPlugin
84 #pragma mark -
85 #pragma mark Accessor Methods
87 /*!
88  * @brief Is iTunes stopped?
89  */
90 - (BOOL)iTunesIsStopped
92         //Get the info if we don't already have it
93         if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
95         return iTunesIsStopped;
98 /*!
99  * @brief Set if iTunes is stopped
100  */
101 - (void)setiTunesIsStopped:(BOOL)yesOrNo
103         iTunesIsStopped = yesOrNo;
107 * @brief Is iTunes paused?
108  */
109 - (BOOL)iTunesIsPaused
111         //Get the info if we don't already have it
112         if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
113         
114         return iTunesIsPaused;
118  * @brief Set if iTunes is paused
119  */
120 - (void)setiTunesIsPaused:(BOOL)yesOrNo
122   iTunesIsPaused = yesOrNo;
127  * @brief Get current iTunes info dictionary
128  */
129 - (NSDictionary *)iTunesCurrentInfo
131         return iTunesCurrentInfo;
135  * @brief Store local copy of iTunes information
136  * 
137  * Retains new information, requests immediate content update and lets the plugin know what iTunes is doing.
138  */
139  - (void)setiTunesCurrentInfo:(NSDictionary *)newInfo
141         if (newInfo != iTunesCurrentInfo) {
142                 [iTunesCurrentInfo release];
143                 NSMutableDictionary *mutableNewInfo = [newInfo mutableCopy];
145                 NSEnumerator *enumerator = [newInfo keyEnumerator];
146                 NSString *key;
147                 while ((key = [enumerator nextObject])) {
148                         //Some versions of iTunes may send numbers as numbers rather than strings. Change these to numbers for our use.
149                         id value = [newInfo objectForKey:key];
150                         if (![value isKindOfClass:[NSString class]]) {
151                                 if ([value respondsToSelector:@selector(stringValue)]) {
152                                         [mutableNewInfo setObject:[value stringValue]
153                                                                            forKey:key];
154                                 } else {
155                                         //A future version might send some other data entirely.  Drop it rather than having non-strings in the dict.
156                                         [mutableNewInfo removeObjectForKey:key];
157                                 }
158                         }
159                 }
161                 iTunesCurrentInfo = mutableNewInfo;
162                 [self setiTunesIsStopped:[[iTunesCurrentInfo objectForKey:PLAYER_STATE] isEqualToString:KEY_STOPPED]];
163                 [self setiTunesIsPaused:[[iTunesCurrentInfo objectForKey:PLAYER_STATE] isEqualToString:KEY_PAUSED]];
165         //Cancel any requests we had to fire updates.
166         [[self class] cancelPreviousPerformRequestsWithTarget:self selector:@selector(fireUpdateiTunesInfo) object:nil];
167         //fire an iTunes update in three seconds.
168         [self performSelector:@selector(fireUpdateiTunesInfo) withObject:nil afterDelay:3.0];
169         }
172  -(void)fireUpdateiTunesInfo
174      [[adium notificationCenter] postNotificationName:Adium_RequestImmediateDynamicContentUpdate object:nil];
175          [[adium notificationCenter] postNotificationName:Adium_iTunesTrackChangedNotification object:iTunesCurrentInfo];
178 #pragma mark -
179 #pragma mark Plugin Methods
182  * @brief Install
183  */
184 - (void)installPlugin
186         NSDictionary    *slashMusicDict = nil;
187         NSDictionary    *conditionalArtistTrackDict = nil;
188         NSString                *currentITunesTrackFormat = nil;
189         NSUserDefaults  *defaults = [NSUserDefaults standardUserDefaults];
190         NSString                *itunesPath = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:@"com.apple.iTunes"];
192         iTunesCurrentInfo = nil;
194         //Only install our items if a copy of iTunes which meets the minimum requirements is found
195         if ([[[NSBundle bundleWithPath:itunesPath] objectForInfoDictionaryKey:@"CFBundleShortVersionString"] floatValue] > ITUNES_MINIMUM_VERSION) {
196                 
197                 //Perform substitutions on outgoing content
198                 [[adium contentController] registerContentFilter:self 
199                                                                                                   ofType:AIFilterContent
200                                                                                            direction:AIFilterOutgoing];
201                 
202                 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
203                                                                                                                         selector:@selector(iTunesUpdate:)
204                                                                                                                                 name:@"com.apple.iTunes.playerInfo"
205                                                                                                                           object:nil];
206                 
207                 substitutionDict = [[NSDictionary alloc] initWithObjectsAndKeys:
208                         ITUNES_ALBUM, ALBUM_TRIGGER,
209                         ITUNES_ARTIST, ARTIST_TRIGGER,
210                         ITUNES_COMPOSER, COMPOSER_TRIGGER,
211                         ITUNES_GENRE, GENRE_TRIGGER,
212                         ITUNES_PLAYER_STATE, STATUS_TRIGGER,
213                         ITUNES_NAME, TRACK_TRIGGER,
214                         ITUNES_YEAR, YEAR_TRIGGER,
215                         ITUNES_STORE_URL, STORE_URL_TRIGGER,
216                         nil];
217                 
218                 slashMusicDict = [[NSDictionary alloc] initWithObjectsAndKeys:
219                         [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],
220                         KEY_PLAYING,
221                         AILocalizedString(@"*is listening to nothing*","Phrase sent in response to %_music when nothing is playing."),
222                         KEY_STOPPED,
223                         nil];
224                 
225                 /* Provide flexibility with the %_iTunes substitution. By default, just store @"" for this key
226                  * so the item is present in the plist for the adventurous to find... but still not hardcoded to a particular
227                  * format.  This is done so that a default installation doesn't have its format broken if the locale switches...
228                  * since the format specifiers are themselves localized.
229                  */
230                 currentITunesTrackFormat = [defaults objectForKey:CURRENT_TRACK_FORMAT_KEY];
231                 if (!currentITunesTrackFormat) {
232                         [defaults setObject:@"" forKey:CURRENT_TRACK_FORMAT_KEY];
233                         currentITunesTrackFormat = @"";
234                 }
235                 
236                 if (![currentITunesTrackFormat length]) {
237                         currentITunesTrackFormat  = [NSString stringWithFormat:@"%@ - %@", TRACK_TRIGGER, ARTIST_TRIGGER];
238                 }
239                 
240                 conditionalArtistTrackDict = [[NSDictionary alloc] initWithObjectsAndKeys:
241                         currentITunesTrackFormat,
242                         KEY_PLAYING,
243                         @"",
244                         KEY_STOPPED,
245                         nil];
246                 
247                 phraseSubstitutionDict = [[NSDictionary alloc] initWithObjectsAndKeys:
248                         slashMusicDict,
249                         MUSIC_TRIGGER,
250                         conditionalArtistTrackDict,
251                         CURRENT_TRACK_TRIGGER,
252                         nil];
253                 
254                 [slashMusicDict release];
255                 [conditionalArtistTrackDict release];
256                 
257                 //Create the "Current iTunes Track" status item
258                 [self createiTunesCurrentTrackStatusState];
259                 
260                 //Create the toolbar item
261                 [self createiTunesToolbarItemWithPath:itunesPath];
262                 
263                 //Create the Edit > Insert and contextual menus
264                 [self createTriggersMenu];
265         }
269  * @brief Uninstall
270  */
271 - (void)uninstallPlugin
273         [[adium contentController] unregisterContentFilter:self];
276 #pragma mark -
277 #pragma mark AppleScript + iTunes methods
280  * @brief Get current iTunes track info
282  * Execute an applescript located in the resources folder that obtains current iTunes track info and assembles the dictionary
283  */
284 - (void)loadiTunesCurrentInfoViaApplescript
286         /*
287          * 1. get a url pointing to the script in the resources folder
288          * 2. prepare the script for execution
289          * 3. get results and create the dictionary based off it
290          */
291         
292         //get the path
293         NSString                                *path = [[NSBundle mainBundle] pathForResource:@"CurrentTunes" ofType:@"scpt"];
294         NSURL                                   *pathURL = [NSURL fileURLWithPath:path];
295         
296         //create the script complete with an error dictionary
297         NSDictionary                    *errors = [NSDictionary dictionary];
298         NSAppleScript                   *playingScript = [[NSAppleScript alloc] initWithContentsOfURL:pathURL error:&errors];
299         
300         //execute the script and get the results as a string
301     NSAppleEventDescriptor      *result = [playingScript executeAndReturnError:&errors];
302         NSString                                *concatenatediTunesData = [result stringValue];
304         //if the player was playing when the script was executed
305         if (![concatenatediTunesData isEqualToString:@"None"]) {
306                 
307                 //get the expected number of entries in the dictionary
308                 unsigned infoCount = [substitutionDict count];
309                 //get the values for the current iTunes song from the string
310                 NSArray * iTunesValues = [concatenatediTunesData componentsSeparatedByString:@",$!$,"];
311                 
312                 //if the two are properly matched (which they always will be, but just in case)
313                 if ([iTunesValues count] == infoCount) {
314                         //create the dictionary
315                         [self setiTunesCurrentInfo:[NSDictionary dictionaryWithObjects:iTunesValues
316                                                                                                                                    forKeys:[NSArray arrayWithObjects:
317                                                                                                                                                         ITUNES_ALBUM,
318                                                                                                                                                         ITUNES_ARTIST,
319                                                                                                                                                         ITUNES_COMPOSER,
320                                                                                                                                                         ITUNES_GENRE,
321                                                                                                                                                         ITUNES_PLAYER_STATE,
322                                                                                                                                                         ITUNES_NAME,
323                                                                                                                                                         ITUNES_YEAR,
324                                                                                                                                                         ITUNES_STORE_URL,
325                                                                                                                                                         nil]]];
326                 } else {
327                         NSLog(@"iTunesValues was %@ (%i items), but I was expecting %i. Perhaps CurrentTunes is not updated to match ESiTunesPlugin?",
328                                   iTunesValues, [iTunesValues count], infoCount);
329                 }
330                 
331         } else {
332                 //create a dictionary saying that iTunes is stopped
333                 [self setiTunesCurrentInfo:[NSDictionary dictionaryWithObjectsAndKeys:KEY_STOPPED, PLAYER_STATE, nil]];
334         }
335         
336         [playingScript release];
339 #pragma mark -
340 #pragma mark Status item creation
343  * @brief Create an available status state
345  * Create a Status which uses the current iTunes track data as it's message
346  */
347 - (void)createiTunesCurrentTrackStatusState
349         //create an iTunes status of state "Available" with default available status settings
350         AIStatus                   *currentiTunesStatusState = [[AIStatus statusOfType:AIAvailableStatusType] retain];
351         
352         //set status attributes
353         NSAttributedString *trackAndArtist = [NSAttributedString stringWithString:CURRENT_TRACK_TRIGGER];
354         [currentiTunesStatusState setStatusMessage:trackAndArtist];
355         [currentiTunesStatusState setTitle:CURRENT_ITUNES_TRACK];
356         [currentiTunesStatusState setMutabilityType:AISecondaryLockedStatusState];
357         [currentiTunesStatusState setUniqueStatusID:[NSNumber numberWithInt:ITUNES_STATUS_ID]];
358         [currentiTunesStatusState setSpecialStatusType:AINowPlayingSpecialStatusType];
360         //give it to the AIStatusController
361         [[adium statusController] addStatusState:currentiTunesStatusState];
362         [currentiTunesStatusState release];
365 #pragma mark -
366 #pragma mark Filter Protocol methods
369  * @brief Filter messages for keywords to replace
371  * Replace any iTunes triggers with the appropriate information
372  */
373 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)inAttributedString context:(id)context
375     NSMutableAttributedString   *filteredMessage = nil;
376         NSString                                        *stringMessage;
377         
378         //get the attributed string as a regular string so we can do string processing
379         if ((stringMessage = [inAttributedString string])) {
380                 NSEnumerator    *enumerator;
381                 NSString                *trigger;
382                 BOOL                    addStoreLinkAsSubtext = NO;
383                 
384                 /* Replace the phrases with the string containing the triggers.
385                  * For example, /music will become *is listening to %_track by %_artist*.
386                  * This will then become the actual track information in the next while().
387                  */
388                 enumerator = [phraseSubstitutionDict keyEnumerator];
389                 
390                 while ((trigger = [enumerator nextObject])) {
391                         //search for phrase in the string that needs to be filtered
392                         if (([stringMessage rangeOfString:trigger options:(NSLiteralSearch | NSCaseInsensitiveSearch)].location != NSNotFound)) {
393                                 NSDictionary    *replacementDict;
394                                 NSString                *replacement;
395                                 
396                                 //get the format for the current trigger
397                                 replacementDict = [phraseSubstitutionDict objectForKey:trigger];
398                                                                 
399                                 //replacement of phrase should reflect iTunes player state
400                                 if (![self iTunesIsStopped] && ![self iTunesIsPaused]) {
401                                         replacement = [replacementDict objectForKey:KEY_PLAYING];
402                                         
403                                         /* If the trigger is the trigger used for the Current iTunes Track status, we'll want to add a subtext of the store link
404                                          * so account code can send it out later on.
405                                          */
406                                         if ([trigger isEqualToString:CURRENT_TRACK_TRIGGER]) {
407                                                 addStoreLinkAsSubtext = YES;
408                                         }
410                                 } else {
411                                         replacement = [replacementDict objectForKey:KEY_STOPPED];                                       
412                                 }
413                                 
414                                 //create a attributedstring if it hasn't been created already
415                                 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
416                                 
417                                 //Perform the replacement
418                                 [filteredMessage replaceOccurrencesOfString:trigger
419                                                                                                  withString:replacement
420                                                                                                         options:(NSLiteralSearch | NSCaseInsensitiveSearch)
421                                                                                                           range:NSMakeRange(0, [filteredMessage length])];
422                         }
423                 }
424                 
425                 if (filteredMessage) {
426                         //Update our string for the simple trigger replacement process so we can replace the %_ tokens
427                         stringMessage = [filteredMessage string];
428                 }
429                 
430                 //Substitute simple triggers as appropriate
431                 enumerator = [substitutionDict keyEnumerator];
432                 while ((trigger = [enumerator nextObject])) {
433                         
434                         //Find if the current trigger is in the string
435                         if (([stringMessage rangeOfString:trigger options:(NSLiteralSearch | NSCaseInsensitiveSearch)].location != NSNotFound)) {
436                                 NSString *replacement;
437                                 
438                                 //Get the info if we don't already have it
439                                 if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
441                                 //Attempt to replace it with its proper value
442                                 if (!(replacement = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:trigger]])) {
443                                         //If no replacement is found, replace the trigger with an empty string
444                                         replacement = @"";
445                                 }
446                                 
447                                 //if a mutable attributed string for the string to be filtered doesn't exist, create it. 
448                                 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
449                                 
450                                 //Replace the current trigger with the value we found above
451                                 [filteredMessage replaceOccurrencesOfString:trigger
452                                                                                                  withString:replacement
453                                                                                                         options:(NSLiteralSearch | NSCaseInsensitiveSearch)
454                                                                                                           range:NSMakeRange(0, [filteredMessage length])];
455                         }
456                 }
457                 
458                 if (addStoreLinkAsSubtext && filteredMessage) {
459                         NSString *storeLinkForSubtext = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:STORE_URL_TRIGGER]];
460                         if (storeLinkForSubtext) {
461                                 [filteredMessage addAttribute:@"AIMessageSubtext"
462                                                                                 value:storeLinkForSubtext
463                                                                                 range:NSMakeRange(0, [filteredMessage length])];
464                         }
465                 }
466         }
467                 
468         //Give back the processed string
469         return (filteredMessage ? filteredMessage : inAttributedString);
473  * @brief Filter priority
475  * Filter at default priority
476  */
477 - (float)filterPriority
479         return DEFAULT_FILTER_PRIORITY;
482 #pragma mark -
483 #pragma mark Notification Selector
486  * @brief The iTunes song changed
488  * The accessor method caches the information and then requst an immediate update to dynamic content
489  */
490 - (void)iTunesUpdate:(NSNotification *)aNotification
492         NSDictionary *newInfo = [aNotification userInfo];
494         [self setiTunesCurrentInfo:newInfo];
497 #pragma mark -
498 #pragma mark Toolbar Item Methods
501  * @brief Create the toolbar item
503  * Create toolbar item and it's menu
504  */
505 - (void)createiTunesToolbarItemWithPath:(NSString *)iTunesPath
507         NSMenu            *menu = [[NSMenu alloc] init];
508         MVMenuButton  *button = [[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)];
510         //configure the popup button and its menu
511         [button setImage:[[NSWorkspace sharedWorkspace] iconForFile:iTunesPath]];
512         [self createiTunesToolbarItemMenuItems:menu];
514         NSToolbarItem * iTunesItem = [AIToolbarUtilities toolbarItemWithIdentifier:ITUNES_TOOLBAR_ITEM
515                                                                                                                                                  label:TOOLBAR_LABEL
516                                                                                                                                   paletteLabel:TOOLBAR_LABEL
517                                                                                                                                            toolTip:AILocalizedString(@"Insert current iTunes track information.","Label for iTunes toolbar menu item.")
518                                                                                                                                                 target:self
519                                                                                                                            settingSelector:@selector(setView:)
520                                                                                                                                    itemContent:button
521                                                                                                                                                 action:NULL
522                                                                                                                                                   menu:nil];
523         //configure the toolbar and button for use
524         [[iTunesItem view] setMenu:menu];
525         [iTunesItem setMinSize:NSMakeSize(32,32)];
526         [iTunesItem setMaxSize:NSMakeSize(32,32)];
527         [button setToolbarItem:iTunesItem];
528         
529         //Add menu to toolbar item (for text mode)
530         NSMenuItem      *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
531         [mItem setSubmenu:menu];
532         [mItem setTitle:TOOLBAR_LABEL];
533         [iTunesItem setMenuFormRepresentation:mItem];
534         
535         //give it to adium to use
536         [[adium toolbarController] registerToolbarItem:iTunesItem forToolbarType:@"TextEntry"];
537         [button release];
538         [menu release];
542  * @brief Create the toolbar item's menu
544  * Populate a menu with menu items that will insert appropriate values of the currently playing iTunes song.
545  */
547 - (void)createiTunesToolbarItemMenuItems:(NSMenu *)iTunesMenu
548 {       
549         NSMenu *insertTrackSubmenu = [[NSMenu alloc] init];
550         
551         [iTunesMenu addItem:[self menuItemWithTitle:CURRENT_ITUNES_TRACK 
552                                                                                  action:@selector(insertFilteredString:) 
553                                                           representedObject:CURRENT_TRACK_TRIGGER
554                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
555         [iTunesMenu addItem:[self menuItemWithTitle:MUSIC_TRIGGER
556                                                                                  action:@selector(insertFilteredString:) 
557                                                           representedObject:MUSIC_TRIGGER                                                                                  
558                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
559         [iTunesMenu addItem:[NSMenuItem separatorItem]];
560         
561         //submenu of actions related to a track
562         NSMenuItem *submenuRoot = [[NSMenuItem alloc] initWithTitle:AILocalizedString(@"Track Information","Submenu for iTunes toolbar item menu for inserting current track information.")
563                                                                                                                  action:NULL
564                                                                                                   keyEquivalent:@""];
565         
566         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Album","Insert Current iTunes track album toolbar menu item.") 
567                                                                                                  action:@selector(insertFilteredString:)
568                                                                           representedObject:ALBUM_TRIGGER
569                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
570         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Artist","Insert Current iTunes track artist toolbar menu item.") 
571                                                                                                  action:@selector(insertFilteredString:) 
572                                                                           representedObject:ARTIST_TRIGGER
573                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
574         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Composer","Insert Current iTunes track composer toolbar menu item.") 
575                                                                                                  action:@selector(insertFilteredString:)
576                                                                           representedObject:COMPOSER_TRIGGER
577                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
578         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Genre","Insert Current iTunes track genre toolbar menu item.") 
579                                                                                                  action:@selector(insertFilteredString:)
580                                                                           representedObject:GENRE_TRIGGER
581                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
582         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Name","Insert Current iTunes track name toolbar menu item.") 
583                                                                                                  action:@selector(insertFilteredString:)
584                                                                           representedObject:TRACK_TRIGGER
585                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
586         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"Year","Insert Current iTunes track year toolbar menu item.") 
587                                                                                                  action:@selector(insertFilteredString:)
588                                                                           representedObject:YEAR_TRIGGER
589                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
590         
591         [insertTrackSubmenu addItem:[self menuItemWithTitle:AILocalizedString(@"iTunes Music Store Link","Insert Current iTunes track store link toolbar menu item.") 
592                                                                                                  action:@selector(insertiTMSLink)
593                                                                           representedObject:nil
594                                                                                                    kind:ENABLED_IF_ITUNES_PLAYING]];
596         [iTunesMenu addItem:submenuRoot];
597         [iTunesMenu setSubmenu:insertTrackSubmenu forItem:submenuRoot];
598         [submenuRoot release];
599         [insertTrackSubmenu release];
600         [iTunesMenu addItem:[NSMenuItem separatorItem]];
602         //this isn't implemented yet, need some advice on this one
603         [iTunesMenu addItem:[self menuItemWithTitle:[AILocalizedString(@"Search Selection in Music Store","iTunes toolbar menu item title to search selection in iTMS.") stringByAppendingEllipsis]
604                                                                                  action:@selector(gatherSelection)
605                                                           representedObject:nil
606                                                                                    kind:RESPONDER_IS_WEBVIEW]];
607         [iTunesMenu addItem:[NSMenuItem separatorItem]];
609         [iTunesMenu addItem:[self menuItemWithTitle:[AILocalizedString(@"Bring iTunes to Front","iTunes toolbar menu item title to make iTunes frontmost app.") stringByAppendingEllipsis]
610                                                                                  action:@selector(bringiTunesToFront)
611                                                           representedObject:nil
612                                                                                    kind:ALWAYS_ENABLED]];
616  * @brief Create a menu item
618  * create a menu item targeting this plugin. Determine if it should disable itself when firstResponder != [textView class].
619  */
620 - (NSMenuItem *)menuItemWithTitle:(NSString *)title action:(SEL)action representedObject:(id)representedObject kind:(KGiTunesPluginMenuItemKind)itemKind
622         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:title action:action keyEquivalent:@""];
623         [item setTarget:self];
624         [item setTag:itemKind];
625         [item setRepresentedObject:representedObject];
626         [item setEnabled:YES];
628         return [item autorelease];
631 #pragma mark -
632 #pragma mark Toolbar Item actions
635  * @brief Insert current song iTMS link
637  * Get the URL from the iTunesCurrentInfo dict or create a URL if one can't be found.
638  */
639 - (void)insertiTMSLink
641         NSMutableString         *url = [[NSMutableString alloc] init];
642         NSString                        *urlLabel = nil;
644         //get current information
645         NSString                        *artist = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:ARTIST_TRIGGER]];
646         NSString                        *trackName = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:TRACK_TRIGGER]];
647         
648         //see if we have a URL for us
649         NSString                        *storeURL = [iTunesCurrentInfo objectForKey:[substitutionDict objectForKey:STORE_URL_TRIGGER]];
650         if ([storeURL length]) {
651                 [url appendString:storeURL];
652         }
653         
654         //if we have no url data from the iTunes notification to begin with - probably because we got the info using the applescript
655         if (![url length]) {
656                 
657                 //if iTunes is playing or paused something
658                 if (![self iTunesIsStopped] || ![self iTunesIsPaused]) {
659                         [url appendString:ITMS_SEARCH_URL];
660                         
661                         //if there is a name given to this song put it in the url
662                         if ([trackName length]) {
663                                 [url appendFormat:@"n=%@", [trackName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
664                         } else {
665                                 trackName = @"";
666                         }
667                         
668                         //if there is a name and an artist, we'll use both to refine our search
669                         if ([artist length] && [trackName length]) {
670                                 //[url appendFormat:@"?artistTerm=%@", [artist stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
671                                 [url appendFormat:@"&an=%@", [artist stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
672                                 
673                         } else if ([artist length]) {
674                                 //no proper track name but we have a decent artist name to include in the url
675                                 //[url appendFormat:@"artistTerm=%@", [trackName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
676                                 [url appendFormat:@"an=%@", [trackName stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
677                         } else {
678                                 artist = @"";
679                         }
680                 }
681         }
682         
683         //if something has been added to our search request, create a lovely label for it
684         if (![url isEqualToString:ITMS_SEARCH_URL] && [url length]) {
685                 urlLabel = [[NSString alloc] initWithFormat:@"%@ - %@", trackName, artist];
686         } else {
687                 [url release]; url = nil;
688         }
689         
690         //if we have a url, give it to the user as a nice, formatted <a> tag
691         if (url) {
692                 NSAttributedString *attributedLink = [[NSAttributedString alloc] initWithAttributedString:[AIHTMLDecoder decodeHTML:[NSString stringWithFormat:@"<A HREF=\"%@\">%@</A>", url, urlLabel]]];
693                 [self insertAttributedStringIntoMessageEntryView:attributedLink];
694                 [attributedLink release];
695                 [url release];
696                 [urlLabel release];
697         } else {
698                 //the artist name and or the track name is literally @""
699                 NSBeep();
700         }
704  * @brief Filter and insert current iTunes song display into message entry
706  * Toolbar method. Take the trigger and filter it with real values
708  * @param sender An NSMenuItem whose representedObject is the appropriate trigger to filter
709  */
710 - (void)insertFilteredString:(id)sender
712         [self filterAndInsertString:[sender representedObject]];        
716  * @brief Search iTMS for inputtted data
718  * Build the necessary url and execute it
719  */
720 - (void)searchMusicStoreWithSelection:(NSString *)selectedText
722         //Create a general search request
723         NSString *url = [NSString stringWithFormat:@"itms://phobos.apple.com/WebObjects/MZSearch.woa/wa/search?term=%@", [selectedText stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
724         [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:url]];
729  * @brief Get the selection from the webmessageview
731  * Get the selected text in the messageview and run it thru the iTMS
732  */
733 - (void)gatherSelection
735         id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
736         [self searchMusicStoreWithSelection:[responder selectedString]];
740  * @brief Bring iTunes to foreground
741  */
742 - (void)bringiTunesToFront
744         [[NSWorkspace sharedWorkspace] launchApplication:@"iTunes"];
747 #pragma mark -
748 #pragma mark Edit/Contextual menu item actions
751  * @brief Insert triggers into message entry
753  * Used in the "Edit" and contextual menus.
754  * @param sender An NSMenuItem whose representedObject is the appropriate trigger to insert
755  */
756 - (void)insertUnfilteredString:(id)sender
758         [self insertStringIntoMessageEntryView:[sender representedObject]];
761 #pragma mark -
762 #pragma mark Text Insertion methods
765  * @brief Filter and Insert plain string
767  * Converts the string to an attributed string and filters it, then inserting it into the message entry view
768  * Used by all the toolbar item actions.
769  */
771 - (void)filterAndInsertString:(NSString *)inString
773         id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
774         if (responder && [responder isKindOfClass:[NSTextView class]]) {
775                 NSAttributedString *attributedResult = [[NSAttributedString alloc] initWithString:inString
776                                                                                                                                                            attributes:[(NSTextView *)responder typingAttributes]];
777                 [self insertAttributedStringIntoMessageEntryView:[self filterAttributedString:attributedResult context:nil]];
778                 [attributedResult release];
779         }
783  * @brief Insert raw string into message view
785  * Converts the string to an attributed string and inserts it into the message entry view.
786  * Used with the insertUnfiltered... methods which are used by edit and contextual menus.
787  */
789 - (void)insertStringIntoMessageEntryView:(NSString *)inString
791         id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
792         if (responder && [responder isKindOfClass:[NSTextView class]]) {
793                 NSAttributedString *attributedResult = [[NSAttributedString alloc] initWithString:inString 
794                                                                                                                                                            attributes:[(NSTextView *)responder typingAttributes]];
795                 [self insertAttributedStringIntoMessageEntryView:attributedResult];
796                 [attributedResult release];
797         }
801  * @brief Insert attributed string into message view
803  * Inserts an attributed string it into the message entry view.
804  * Don't check to see if the responder is of class NSTextView because the validateMenuItem method checks.
805  */
807 - (void)insertAttributedStringIntoMessageEntryView:(NSAttributedString *)inString
809         NSResponder *textView = [[[NSApplication sharedApplication] keyWindow] firstResponder];
810         [textView insertText:inString];
811         
812         if (![inString length]) {
813                 NSBeep();
814         }
817 #pragma mark -
818 #pragma mark Edit/Contextual menu methods
821  * @brief Create Edit and Contextual menus of iTunes triggers
823  * Build the menus for the iTunes triggers that autodisables when a first responder isn't a textView
824  */
826 - (NSMenu *)menuOfTriggers
828         NSMenu                  *triggersMenu = [[NSMenu alloc] init];
829         NSEnumerator    *enumerator;
830         NSString                *trigger;
832         [triggersMenu addItem:[self menuItemWithTitle:CURRENT_TRACK_TRIGGER 
833                                                                                    action:@selector(insertUnfilteredString:)
834                                                                 representedObject:CURRENT_TRACK_TRIGGER
835                                                                                          kind:AUTODISABLES]];
836         [triggersMenu addItem:[self menuItemWithTitle:MUSIC_TRIGGER
837                                                                                    action:@selector(insertUnfilteredString:)
838                                                                 representedObject:MUSIC_TRIGGER
839                                                                                          kind:AUTODISABLES]];
841         
842         [triggersMenu addItem:[NSMenuItem separatorItem]];
843         
844         //Simple triggers
845         enumerator = [substitutionDict keyEnumerator];
846         while ((trigger = [enumerator nextObject])) {
847                 [triggersMenu addItem:[self menuItemWithTitle:trigger 
848                                                                                            action:@selector(insertUnfilteredString:) 
849                                                                         representedObject:trigger
850                                                                                                  kind:AUTODISABLES]];
851         }
852         
853         return [triggersMenu autorelease];
857  * @brief Create Edit and Contextual menus of iTunes triggers
859  * Users can then insert %_&lt;token name&gt; into any text view
860  */
861 - (void)createTriggersMenu
863         NSMenuItem      *menuItem = [[NSMenuItem alloc] initWithTitle:INSERT_TRIGGERS_MENU target:self action:NULL keyEquivalent:@""];
864         NSMenu          *menuOfTriggers = [self menuOfTriggers];
865         
866         [menuItem setSubmenu:menuOfTriggers];
867         [[adium menuController] addMenuItem:menuItem toLocation:LOC_Edit_Additions];
868         [menuItem release];
869         
870         menuItem = [[NSMenuItem alloc] initWithTitle:INSERT_TRIGGERS_MENU target:self action:NULL keyEquivalent:@""];
871         [menuItem setSubmenu:[[menuOfTriggers copy] autorelease]];
872         [[adium menuController] addContextualMenuItem:menuItem toLocation:Context_TextView_Edit];
873         [menuItem release];
877  * @brief Configure accessibility of menu items
879  * Depending on whether the responder is a textview and if it should be enabled when itunes isn't playing
880  */
881 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
883         NSResponder                                     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
884         KGiTunesPluginMenuItemKind      tag = [menuItem tag];
885         BOOL                                            enable;
887         //we only insert things into textviews
888         if (responder && [responder isKindOfClass:[NSTextView class]]) {
889                 
890                 //some menu items are only enabled if itunes is playing something
891                 if ((([self iTunesIsStopped] || [self iTunesIsPaused]) && (tag == ENABLED_IF_ITUNES_PLAYING)) || (tag == RESPONDER_IS_WEBVIEW)) {
892                         enable = NO;
893                 } else {
894                         enable = [(NSTextView *)responder isEditable];
895                 }
897         } else if (tag == RESPONDER_IS_WEBVIEW) {
898                 
899                 if ([responder respondsToSelector:@selector(selectedString)]) {
900                         NSString        *selectedString = [(id)responder selectedString];
901                         
902                         if (selectedString && [selectedString length]) {
903                                 enable = YES;
904                         } else {
905                                 enable = NO;
906                         }
908                 } else {
909                         enable = NO;                    
910                 }
911                 
912         } else {
913                 // enable it if it is always supposed to be on, disable if otherwise
914                 enable = (tag == ALWAYS_ENABLED);
915         }
916         
917         return enable;
921 #pragma mark -
922 #pragma mark Deallocation
924 - (void)dealloc
926         //Remove self from notifications list
927         [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
928         
929         //Release class variables
930         if (iTunesCurrentInfo) [iTunesCurrentInfo release];
931         if (substitutionDict) [substitutionDict release];
932         if (phraseSubstitutionDict) [phraseSubstitutionDict release];
933         
934         [super dealloc];
937 @end