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 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.")
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")
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;
79 * @class ESiTunesPlugin
80 * @brief Fiiltering component to provide triggers which are replaced by information from the current iTunes track
82 @implementation ESiTunesPlugin
85 #pragma mark Accessor Methods
88 * @brief Is iTunes stopped?
90 - (BOOL)iTunesIsStopped
92 //Get the info if we don't already have it
93 if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
95 return iTunesIsStopped;
99 * @brief Set if iTunes is stopped
101 - (void)setiTunesIsStopped:(BOOL)yesOrNo
103 iTunesIsStopped = yesOrNo;
107 * @brief Is iTunes paused?
109 - (BOOL)iTunesIsPaused
111 //Get the info if we don't already have it
112 if (!iTunesCurrentInfo) [self loadiTunesCurrentInfoViaApplescript];
114 return iTunesIsPaused;
118 * @brief Set if iTunes is paused
120 - (void)setiTunesIsPaused:(BOOL)yesOrNo
122 iTunesIsPaused = yesOrNo;
127 * @brief Get current iTunes info dictionary
129 - (NSDictionary *)iTunesCurrentInfo
131 return iTunesCurrentInfo;
135 * @brief Store local copy of iTunes information
137 * Retains new information, requests immediate content update and lets the plugin know what iTunes is doing.
139 - (void)setiTunesCurrentInfo:(NSDictionary *)newInfo
141 if (newInfo != iTunesCurrentInfo) {
142 [iTunesCurrentInfo release];
143 NSMutableDictionary *mutableNewInfo = [newInfo mutableCopy];
145 NSEnumerator *enumerator = [newInfo keyEnumerator];
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]
155 //A future version might send some other data entirely. Drop it rather than having non-strings in the dict.
156 [mutableNewInfo removeObjectForKey:key];
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];
172 -(void)fireUpdateiTunesInfo
174 [[adium notificationCenter] postNotificationName:Adium_RequestImmediateDynamicContentUpdate object:nil];
175 [[adium notificationCenter] postNotificationName:Adium_iTunesTrackChangedNotification object:iTunesCurrentInfo];
179 #pragma mark Plugin Methods
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) {
197 //Perform substitutions on outgoing content
198 [[adium contentController] registerContentFilter:self
199 ofType:AIFilterContent
200 direction:AIFilterOutgoing];
202 [[NSDistributedNotificationCenter defaultCenter] addObserver:self
203 selector:@selector(iTunesUpdate:)
204 name:@"com.apple.iTunes.playerInfo"
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,
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],
221 AILocalizedString(@"*is listening to nothing*","Phrase sent in response to %_music when nothing is playing."),
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.
230 currentITunesTrackFormat = [defaults objectForKey:CURRENT_TRACK_FORMAT_KEY];
231 if (!currentITunesTrackFormat) {
232 [defaults setObject:@"" forKey:CURRENT_TRACK_FORMAT_KEY];
233 currentITunesTrackFormat = @"";
236 if (![currentITunesTrackFormat length]) {
237 currentITunesTrackFormat = [NSString stringWithFormat:@"%@ - %@", TRACK_TRIGGER, ARTIST_TRIGGER];
240 conditionalArtistTrackDict = [[NSDictionary alloc] initWithObjectsAndKeys:
241 currentITunesTrackFormat,
247 phraseSubstitutionDict = [[NSDictionary alloc] initWithObjectsAndKeys:
250 conditionalArtistTrackDict,
251 CURRENT_TRACK_TRIGGER,
254 [slashMusicDict release];
255 [conditionalArtistTrackDict release];
257 //Create the "Current iTunes Track" status item
258 [self createiTunesCurrentTrackStatusState];
260 //Create the toolbar item
261 [self createiTunesToolbarItemWithPath:itunesPath];
263 //Create the Edit > Insert and contextual menus
264 [self createTriggersMenu];
271 - (void)uninstallPlugin
273 [[adium contentController] unregisterContentFilter:self];
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
284 - (void)loadiTunesCurrentInfoViaApplescript
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
293 NSString *path = [[NSBundle mainBundle] pathForResource:@"CurrentTunes" ofType:@"scpt"];
294 NSURL *pathURL = [NSURL fileURLWithPath:path];
296 //create the script complete with an error dictionary
297 NSDictionary *errors = [NSDictionary dictionary];
298 NSAppleScript *playingScript = [[NSAppleScript alloc] initWithContentsOfURL:pathURL error:&errors];
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"]) {
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:@",$!$,"];
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:
327 NSLog(@"iTunesValues was %@ (%i items), but I was expecting %i. Perhaps CurrentTunes is not updated to match ESiTunesPlugin?",
328 iTunesValues, [iTunesValues count], infoCount);
332 //create a dictionary saying that iTunes is stopped
333 [self setiTunesCurrentInfo:[NSDictionary dictionaryWithObjectsAndKeys:KEY_STOPPED, PLAYER_STATE, nil]];
336 [playingScript release];
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
347 - (void)createiTunesCurrentTrackStatusState
349 //create an iTunes status of state "Available" with default available status settings
350 AIStatus *currentiTunesStatusState = [[AIStatus statusOfType:AIAvailableStatusType] retain];
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];
366 #pragma mark Filter Protocol methods
369 * @brief Filter messages for keywords to replace
371 * Replace any iTunes triggers with the appropriate information
373 - (NSAttributedString *)filterAttributedString:(NSAttributedString *)inAttributedString context:(id)context
375 NSMutableAttributedString *filteredMessage = nil;
376 NSString *stringMessage;
378 //get the attributed string as a regular string so we can do string processing
379 if ((stringMessage = [inAttributedString string])) {
380 NSEnumerator *enumerator;
382 BOOL addStoreLinkAsSubtext = NO;
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().
388 enumerator = [phraseSubstitutionDict keyEnumerator];
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;
396 //get the format for the current trigger
397 replacementDict = [phraseSubstitutionDict objectForKey:trigger];
399 //replacement of phrase should reflect iTunes player state
400 if (![self iTunesIsStopped] && ![self iTunesIsPaused]) {
401 replacement = [replacementDict objectForKey:KEY_PLAYING];
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.
406 if ([trigger isEqualToString:CURRENT_TRACK_TRIGGER]) {
407 addStoreLinkAsSubtext = YES;
411 replacement = [replacementDict objectForKey:KEY_STOPPED];
414 //create a attributedstring if it hasn't been created already
415 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
417 //Perform the replacement
418 [filteredMessage replaceOccurrencesOfString:trigger
419 withString:replacement
420 options:(NSLiteralSearch | NSCaseInsensitiveSearch)
421 range:NSMakeRange(0, [filteredMessage length])];
425 if (filteredMessage) {
426 //Update our string for the simple trigger replacement process so we can replace the %_ tokens
427 stringMessage = [filteredMessage string];
430 //Substitute simple triggers as appropriate
431 enumerator = [substitutionDict keyEnumerator];
432 while ((trigger = [enumerator nextObject])) {
434 //Find if the current trigger is in the string
435 if (([stringMessage rangeOfString:trigger options:(NSLiteralSearch | NSCaseInsensitiveSearch)].location != NSNotFound)) {
436 NSString *replacement;
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
447 //if a mutable attributed string for the string to be filtered doesn't exist, create it.
448 if (!filteredMessage) filteredMessage = [[inAttributedString mutableCopy] autorelease];
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])];
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])];
468 //Give back the processed string
469 return (filteredMessage ? filteredMessage : inAttributedString);
473 * @brief Filter priority
475 * Filter at default priority
477 - (float)filterPriority
479 return DEFAULT_FILTER_PRIORITY;
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
490 - (void)iTunesUpdate:(NSNotification *)aNotification
492 NSDictionary *newInfo = [aNotification userInfo];
494 [self setiTunesCurrentInfo:newInfo];
498 #pragma mark Toolbar Item Methods
501 * @brief Create the toolbar item
503 * Create toolbar item and it's menu
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
516 paletteLabel:TOOLBAR_LABEL
517 toolTip:AILocalizedString(@"Insert current iTunes track information.","Label for iTunes toolbar menu item.")
519 settingSelector:@selector(setView:)
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];
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];
535 //give it to adium to use
536 [[adium toolbarController] registerToolbarItem:iTunesItem forToolbarType:@"TextEntry"];
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.
547 - (void)createiTunesToolbarItemMenuItems:(NSMenu *)iTunesMenu
549 NSMenu *insertTrackSubmenu = [[NSMenu alloc] init];
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]];
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.")
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]];
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].
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];
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.
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]];
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];
654 //if we have no url data from the iTunes notification to begin with - probably because we got the info using the applescript
657 //if iTunes is playing or paused something
658 if (![self iTunesIsStopped] || ![self iTunesIsPaused]) {
659 [url appendString:ITMS_SEARCH_URL];
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]];
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]];
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]];
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];
687 [url release]; url = nil;
690 //if we have a url, give it to the user as a nice, formatted <a> tag
692 NSAttributedString *attributedLink = [[NSAttributedString alloc] initWithAttributedString:[AIHTMLDecoder decodeHTML:[NSString stringWithFormat:@"<A HREF=\"%@\">%@</A>", url, urlLabel]]];
693 [self insertAttributedStringIntoMessageEntryView:attributedLink];
694 [attributedLink release];
698 //the artist name and or the track name is literally @""
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
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
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
733 - (void)gatherSelection
735 id responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
736 [self searchMusicStoreWithSelection:[responder selectedString]];
740 * @brief Bring iTunes to foreground
742 - (void)bringiTunesToFront
744 [[NSWorkspace sharedWorkspace] launchApplication:@"iTunes"];
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
756 - (void)insertUnfilteredString:(id)sender
758 [self insertStringIntoMessageEntryView:[sender representedObject]];
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.
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];
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.
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];
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.
807 - (void)insertAttributedStringIntoMessageEntryView:(NSAttributedString *)inString
809 NSResponder *textView = [[[NSApplication sharedApplication] keyWindow] firstResponder];
810 [textView insertText:inString];
812 if (![inString length]) {
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
826 - (NSMenu *)menuOfTriggers
828 NSMenu *triggersMenu = [[NSMenu alloc] init];
829 NSEnumerator *enumerator;
832 [triggersMenu addItem:[self menuItemWithTitle:CURRENT_TRACK_TRIGGER
833 action:@selector(insertUnfilteredString:)
834 representedObject:CURRENT_TRACK_TRIGGER
836 [triggersMenu addItem:[self menuItemWithTitle:MUSIC_TRIGGER
837 action:@selector(insertUnfilteredString:)
838 representedObject:MUSIC_TRIGGER
842 [triggersMenu addItem:[NSMenuItem separatorItem]];
845 enumerator = [substitutionDict keyEnumerator];
846 while ((trigger = [enumerator nextObject])) {
847 [triggersMenu addItem:[self menuItemWithTitle:trigger
848 action:@selector(insertUnfilteredString:)
849 representedObject:trigger
853 return [triggersMenu autorelease];
857 * @brief Create Edit and Contextual menus of iTunes triggers
859 * Users can then insert %_<token name> into any text view
861 - (void)createTriggersMenu
863 NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:INSERT_TRIGGERS_MENU target:self action:NULL keyEquivalent:@""];
864 NSMenu *menuOfTriggers = [self menuOfTriggers];
866 [menuItem setSubmenu:menuOfTriggers];
867 [[adium menuController] addMenuItem:menuItem toLocation:LOC_Edit_Additions];
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];
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
881 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
883 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
884 KGiTunesPluginMenuItemKind tag = [menuItem tag];
887 //we only insert things into textviews
888 if (responder && [responder isKindOfClass:[NSTextView class]]) {
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)) {
894 enable = [(NSTextView *)responder isEditable];
897 } else if (tag == RESPONDER_IS_WEBVIEW) {
899 if ([responder respondsToSelector:@selector(selectedString)]) {
900 NSString *selectedString = [(id)responder selectedString];
902 if (selectedString && [selectedString length]) {
913 // enable it if it is always supposed to be on, disable if otherwise
914 enable = (tag == ALWAYS_ENABLED);
922 #pragma mark Deallocation
926 //Remove self from notifications list
927 [[NSDistributedNotificationCenter defaultCenter] removeObserver:self];
929 //Release class variables
930 if (iTunesCurrentInfo) [iTunesCurrentInfo release];
931 if (substitutionDict) [substitutionDict release];
932 if (phraseSubstitutionDict) [phraseSubstitutionDict release];