5 * Created by Evan Schoenberg on 6/11/05.
6 * Assigned to Kiel Gillard
8 * Thanks to GrowlTunes from the Growl project for demonstrating how to receive notifications when
9 * the iTunes track changes.
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
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?"
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.")
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")
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;
78 * @class ESiTunesPlugin
79 * @brief Fiiltering component to provide triggers which are replaced by information from the current iTunes track
81 @implementation ESiTunesPlugin
84 #pragma mark Accessor Methods
87 * @brief Is iTunes stopped?
89 - (BOOL)iTunesIsStopped
91 //Get the info if we don't already have it
92 if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
94 return iTunesIsStopped;
98 * @brief Set if iTunes is stopped
100 - (void)setiTunesIsStopped:(BOOL)yesOrNo
102 iTunesIsStopped = yesOrNo;
106 * @brief Is iTunes paused?
108 - (BOOL)iTunesIsPaused
110 //Get the info if we don't already have it
111 if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
113 return iTunesIsPaused;
117 * @brief Set if iTunes is paused
119 - (void)setiTunesIsPaused:(BOOL)yesOrNo
121 iTunesIsPaused = yesOrNo;
126 * @brief Get current iTunes info dictionary
128 - (NSDictionary *)iTunesCurrentInfo
130 return iTunesCurrentInfo;
134 * @brief Store local copy of iTunes information
136 * Retains new information, requests immediate content update and lets the plugin know what iTunes is doing.
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]];
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];
154 -(void)fireUpdateiTunesInfo
156 [[adium notificationCenter] postNotificationName:Adium_RequestImmediateDynamicContentUpdate object:nil];
160 #pragma mark Plugin Methods
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) {
178 //Perform substitutions on outgoing content
179 [[adium contentController] registerContentFilter:self
180 ofType:AIFilterContent
181 direction:AIFilterOutgoing];
183 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
184 selector:@selector(iTunesUpdate:)
185 name:@"com.apple.iTunes.playerInfo"
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,
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],
201 AILocalizedString(@"*is listening to nothing*","Phrase sent in response to %_music when nothing is playing."),
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.
210 currentITunesTrackFormat = [defaults objectForKey:CURRENT_TRACK_FORMAT_KEY];
211 if (!currentITunesTrackFormat) {
212 [defaults setObject:@"" forKey:CURRENT_TRACK_FORMAT_KEY];
213 currentITunesTrackFormat = @"";
216 if (![currentITunesTrackFormat length]) {
217 currentITunesTrackFormat = [NSString stringWithFormat:@"%@ - %@", TRACK_TRIGGER, ARTIST_TRIGGER];
220 conditionalArtistTrackDict = [[NSDictionary alloc] initWithObjectsAndKeys:
221 currentITunesTrackFormat,
227 phraseSubstitutionDict = [[NSDictionary alloc] initWithObjectsAndKeys:
230 conditionalArtistTrackDict,
231 CURRENT_TRACK_TRIGGER,
234 [slashMusicDict release];
235 [conditionalArtistTrackDict release];
237 //Create the "Current iTunes Track" status item
238 [self createiTunesCurrentTrackStatusState];
240 //Create the toolbar item
241 [self createiTunesToolbarItemWithPath:itunesPath];
243 //Create the Edit > Insert and contextual menus
244 [self createTriggersMenu];
251 - (void)uninstallPlugin
253 [[adium contentController] unregisterContentFilter:self];
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
264 - (void)loadiTunesCurrentInfoViaApplescript
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
273 NSString *path = [[NSBundle mainBundle] pathForResource:@"CurrentTunes" ofType:@"scpt"];
274 NSURL *pathURL = [NSURL fileURLWithPath:path];
276 //create the script complete with an error dictionary
277 NSDictionary *errors = [NSDictionary dictionary];
278 NSAppleScript *playingScript = [[NSAppleScript alloc] initWithContentsOfURL:pathURL error:&errors];
280 //execute the script and get the results as a string
281 NSAppleEventDescriptor *result = [playingScript executeAndReturnError:&errors];
282 NSString *concatenatediTunesData = [result stringValue];
284 //if the player was playing when the script was executed
285 if (![concatenatediTunesData isEqualToString:@"None"]) {
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:@",$!$,"];
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",
307 //create a dictionary saying that iTunes is stopped
308 [self setiTunesCurrentInfo:[NSDictionary dictionaryWithObjectsAndKeys:KEY_STOPPED, PLAYER_STATE, nil]];
311 [playingScript release];
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
322 - (void)createiTunesCurrentTrackStatusState
324 //create an iTunes status of state "Available" with default available status settings
325 AIStatus *currentiTunesStatusState = [[AIStatus statusOfType:AIAvailableStatusType] retain];
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];
341 #pragma mark Filter Protocol methods
344 * @brief Filter messages for keywords to replace
346 * Replace any iTunes triggers with the appropriate information
348 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)inAttributedString context:(id)context
350 NSMutableAttributedString *filteredMessage = nil;
351 NSString *stringMessage;
353 //get the attributed string as a regular string so we can do string processing
354 if ((stringMessage = [inAttributedString string])) {
355 NSEnumerator *enumerator;
357 BOOL addStoreLinkAsSubtext = NO;
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().
363 enumerator = [phraseSubstitutionDict keyEnumerator];
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;
371 //get the format for the current trigger
372 replacementDict = [phraseSubstitutionDict objectForKey:trigger];
374 //replacement of phrase should reflect iTunes player state
375 if (![self iTunesIsStopped] && ![self iTunesIsPaused]) {
376 replacement = [replacementDict objectForKey:KEY_PLAYING];
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.
381 if ([trigger isEqualToString:CURRENT_TRACK_TRIGGER]) {
382 addStoreLinkAsSubtext = YES;
386 replacement = [replacementDict objectForKey:KEY_STOPPED];
389 //create a attributedstring if it hasn't been created already
390 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
392 //Perform the replacement
393 [filteredMessage replaceOccurrencesOfString:trigger
394 withString:replacement
395 options:(NSLiteralSearch | NSCaseInsensitiveSearch)
396 range:NSMakeRange(0, [filteredMessage length])];
400 if (filteredMessage) {
401 //Update our string for the simple trigger replacement process so we can replace the %_ tokens
402 stringMessage = [filteredMessage string];
405 //Substitute simple triggers as appropriate
406 enumerator = [substitutionDict keyEnumerator];
407 while ((trigger = [enumerator nextObject])) {
409 //Find if the current trigger is in the string
410 if (([stringMessage rangeOfString:trigger options:(NSLiteralSearch | NSCaseInsensitiveSearch)].location != NSNotFound)) {
411 NSString *replacement;
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
422 //if a mutable attributed string for the string to be filtered doesn't exist, create it.
423 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
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])];
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])];
443 //Give back the processed string
444 return (filteredMessage ? filteredMessage : inAttributedString);
448 * @brief Filter priority
450 * Filter at default priority
452 - (float)filterPriority
454 return DEFAULT_FILTER_PRIORITY;
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
465 - (void)iTunesUpdate:(NSNotification *)aNotification
467 NSDictionary *newInfo = [aNotification userInfo];
469 [self setiTunesCurrentInfo:newInfo];
473 #pragma mark Toolbar Item Methods
476 * @brief Create the toolbar item
478 * Create toolbar item and it's menu
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
491 paletteLabel:TOOLBAR_LABEL
492 toolTip:AILocalizedString(@"Insert current iTunes track information.","Label for iTunes toolbar menu item.")
494 settingSelector:@selector(setView:)
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];
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];
510 //give it to adium to use
511 [[adium toolbarController] registerToolbarItem:iTunesItem forToolbarType:@"TextEntry"];
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.
522 - (void)createiTunesToolbarItemMenuItems:(NSMenu *)iTunesMenu
524 NSMenu *insertTrackSubmenu = [[NSMenu alloc] init];
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]];
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.")
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]];
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].
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];
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.
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]];
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];
625 //if we have no url data from the iTunes notification to begin with - probably because we got the info using the applescript
628 //if iTunes is playing or paused something
629 if (![self iTunesIsStopped] || ![self iTunesIsPaused]) {
630 [url appendString:ITMS_SEARCH_URL];
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]];
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]];
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]];
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];
658 [url release]; url = nil;
661 //if we have a url, give it to the user as a nice, formatted <a> tag
663 NSAttributedString *attributedLink = [[NSAttributedString alloc] initWithAttributedString:[AIHTMLDecoder decodeHTML:[NSString stringWithFormat:@"<A HREF=\"%@\">%@</A>", url, urlLabel]]];
664 [self insertAttributedStringIntoMessageEntryView:attributedLink];
665 [attributedLink release];
669 //the artist name and or the track name is literally @""
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
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
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
704 - (void)gatherSelection
706 id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
707 [self searchMusicStoreWithSelection:[responder selectedString]];
711 * @brief Bring iTunes to foreground
713 - (void)bringiTunesToFront
715 [[NSWorkspace sharedWorkspace] launchApplication:@"iTunes"];
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
727 - (void)insertUnfilteredString:(id)sender
729 [self insertStringIntoMessageEntryView:[sender representedObject]];
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.
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];
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.
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];
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.
778 - (void)insertAttributedStringIntoMessageEntryView:(NSAttributedString *)inString
780 NSResponder *textView = [[[NSApplication sharedApplication] keyWindow] firstResponder];
781 [textView insertText:inString];
783 if (![inString length]) {
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
797 - (NSMenu *)menuOfTriggers
799 NSMenu *triggersMenu = [[NSMenu alloc] init];
800 NSEnumerator *enumerator;
803 [triggersMenu addItem:[self menuItemWithTitle:CURRENT_TRACK_TRIGGER
804 action:@selector(insertUnfilteredString:)
805 representedObject:CURRENT_TRACK_TRIGGER
807 [triggersMenu addItem:[self menuItemWithTitle:MUSIC_TRIGGER
808 action:@selector(insertUnfilteredString:)
809 representedObject:MUSIC_TRIGGER
813 [triggersMenu addItem:[NSMenuItem separatorItem]];
816 enumerator = [substitutionDict keyEnumerator];
817 while ((trigger = [enumerator nextObject])) {
818 [triggersMenu addItem:[self menuItemWithTitle:trigger
819 action:@selector(insertUnfilteredString:)
820 representedObject:trigger
824 return [triggersMenu autorelease];
828 * @brief Create Edit and Contextual menus of iTunes triggers
830 * Users can then insert %_<token name> into any text view
832 - (void)createTriggersMenu
834 NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:INSERT_TRIGGERS_MENU target:self action:NULL keyEquivalent:@""];
835 NSMenu *menuOfTriggers = [self menuOfTriggers];
837 [menuItem setSubmenu:menuOfTriggers];
838 [[adium menuController] addMenuItem:menuItem toLocation:LOC_Edit_Additions];
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];
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
852 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
854 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
855 KGiTunesPluginMenuItemKind tag = [menuItem tag];
858 //we only insert things into textviews
859 if (responder && [responder isKindOfClass:[NSTextView class]]) {
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)) {
865 enable = [(NSTextView *)responder isEditable];
868 } else if (tag == RESPONDER_IS_WEBVIEW) {
870 if ([responder respondsToSelector:@selector(selectedString)]) {
871 NSString *selectedString = [(id)responder selectedString];
873 if (selectedString && [selectedString length]) {
884 // enable it if it is always supposed to be on, disable if otherwise
885 enable = (tag == ALWAYS_ENABLED);
893 #pragma mark Deallocation
897 //Remove self from notifications list
898 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
900 //Release class variables
901 if (iTunesCurrentInfo) [iTunesCurrentInfo release];
902 if (substitutionDict) [substitutionDict release];
903 if (phraseSubstitutionDict) [phraseSubstitutionDict release];