Added `-[NSArray validateAsPropertyList]` and `-[NSDictionary validateAsPropertyList...
[adiumx.git] / Source / GBApplescriptFiltersPlugin.m
blobfcbb2a22e7c760fdffc596d13d1768cec4bd4ffe
1 /* 
2  * Adium is the legal property of its developers, whose names are listed in the copyright file included
3  * with this source distribution.
4  * 
5  * This program is free software; you can redistribute it and/or modify it under the terms of the GNU
6  * General Public License as published by the Free Software Foundation; either version 2 of the License,
7  * or (at your option) any later version.
8  * 
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
10  * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
11  * Public License for more details.
12  * 
13  * You should have received a copy of the GNU General Public License along with this program; if not,
14  * write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
15  */
17 #import <Adium/AIContentControllerProtocol.h>
18 #import <Adium/AIMenuControllerProtocol.h>
19 #import <Adium/AIToolbarControllerProtocol.h>
20 #import "ESApplescriptabilityController.h"
21 #import "GBApplescriptFiltersPlugin.h"
22 #import <AIUtilities/AIMenuAdditions.h>
23 #import <AIUtilities/AIToolbarUtilities.h>
24 #import <AIUtilities/AIImageAdditions.h>
25 #import <AIUtilities/MVMenuButton.h>
26 #import <Adium/AIContentObject.h>
27 #import <Adium/AIHTMLDecoder.h>
29 #include <sys/errno.h>
30 #include <string.h>
32 #define TITLE_INSERT_SCRIPT             AILocalizedString(@"Insert Script",nil)
33 #define SCRIPT_BUNDLE_EXTENSION @"AdiumScripts"
34 #define SCRIPTS_PATH_NAME               @"Scripts"
35 #define SCRIPT_EXTENSION                @"scpt"
36 #define SCRIPT_IDENTIFIER               @"InsertScript"
38 #define SCRIPT_TIMEOUT                  30
40 @interface GBApplescriptFiltersPlugin (PRIVATE)
42 - (void)_replaceKeyword:(NSString *)keyword
43                          withScript:(NSMutableDictionary *)infoDict
44                            inString:(NSString *)inString
45          inAttributedString:(NSMutableAttributedString *)attributedString
46                            uniqueID:(unsigned long long)uniqueID;
48 - (void)_executeScript:(NSMutableDictionary *)infoDict 
49                  withArguments:(NSArray *)arguments
50                  forAttributedString:(NSMutableAttributedString *)attributedString
51                   keywordRange:(NSRange)keywordRange
52                           uniqueID:(unsigned long long)uniqueID;
54 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict;
56 - (void)buildScriptMenu;
57 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu;
58 - (void)_sortScriptsByTitle:(NSMutableArray *)sortArray;
60 - (void)registerToolbarItem;
62 - (void)scriptDidFinish:(NSNotification *)aNotification;
64 - (void)_replaceKeyword:(NSString *)keyword
65                          withScript:(NSMutableDictionary *)infoDict
66                            inString:(NSString *)inString
67          inAttributedString:(NSMutableAttributedString *)attributedString
68                                 context:(id)context
69                            uniqueID:(unsigned long long)uniqueID;
71 - (void)_executeScript:(NSMutableDictionary *)infoDict 
72                  withArguments:(NSArray *)arguments
73                  forAttributedString:(NSMutableAttributedString *)attributedString
74                   keywordRange:(NSRange)keywordRange
75                            context:(id)context
76                           uniqueID:(unsigned long long)uniqueID;
77 @end
79 int _scriptTitleSort(id scriptA, id scriptB, void *context);
80 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context);
82 /*!
83  * @class GBApplescriptFiltersPlugin
84  * @brief Filter component to allow .AdiumScripts applescript-based filters for outgoing messages
85  */
86 @implementation GBApplescriptFiltersPlugin
88 /*!
89  * @brief Install
90  */
91 - (void)installPlugin
93         //User scripts
94         [[AIObject sharedAdiumInstance] createResourcePathForName:@"Scripts"];
95         
96         //We have an array of scripts for building the menu, and a dictionary of scripts used for the actual substition
97         scriptArray = nil;
98         flatScriptArray = nil;
99         
100         //Prepare our script menu item (which will have the Scripts menu as its submenu)
101         scriptMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_SCRIPT 
102                                                                                                 target:self
103                                                                                                 action:@selector(dummyTarget:)
104                                                                                  keyEquivalent:@""];
106         //Perform substitutions on outgoing content; we may be slow, so register as a delayed content filter
107         [[adium contentController] registerDelayedContentFilter:self 
108                                                                                                          ofType:AIFilterContent
109                                                                                                   direction:AIFilterOutgoing];
110         
111         //Observe for installation of new scripts
112         [[adium notificationCenter] addObserver:self
113                                                                    selector:@selector(xtrasChanged:)
114                                                                            name:AIXtrasDidChangeNotification
115                                                                          object:nil];
116         [[NSNotificationCenter defaultCenter] addObserver:self
117                                                                                          selector:@selector(toolbarWillAddItem:)
118                                                                                                  name:NSToolbarWillAddItemNotification
119                                                                                            object:nil]; 
120         
121         //Start building the script menu
122         scriptMenu = nil;
123         [self buildScriptMenu]; //this also sets the submenu for the menu item.
124         
125         [[adium menuController] addMenuItem:scriptMenuItem toLocation:LOC_Edit_Additions];
126         
127         contextualScriptMenuItem = [scriptMenuItem copy];
128         [[adium menuController] addContextualMenuItem:contextualScriptMenuItem toLocation:Context_TextView_Edit];
132  * @brief Deallocate
133  */
134 - (void)dealloc
136         [[NSNotificationCenter defaultCenter] removeObserver:self];
137         [[adium notificationCenter] removeObserver:self];
138         
139         [scriptArray release]; scriptArray = nil;
140     [flatScriptArray release]; flatScriptArray = nil;
141         [scriptMenuItem release]; scriptMenuItem = nil;
142         [contextualScriptMenuItem release]; contextualScriptMenuItem = nil;
143         
144         [super dealloc];
148  * @brief Xtras changes
150  * If the scripts xtras changed, rebuild our menus.
151  */
152 - (void)xtrasChanged:(NSNotification *)notification
154         if ([[notification object] caseInsensitiveCompare:@"AdiumScripts"] == 0) {
155                 [self buildScriptMenu];
156                                 
157                 [self registerToolbarItem];
158                 
159                 //Update our toolbar item's menu
160                 //[self toolbarWillAddItem:nil];
161         }
165 //Script Loading -------------------------------------------------------------------------------------------------------
166 #pragma mark Script Loading
168  * @brief Load our scripts
170  * This will clear out and then load from available scripts (external and internal) into flatScriptArray and scriptArray.
171  */
172 - (void)loadScripts
174         NSEnumerator    *enumerator;
175         NSString                *filePath;
176         NSBundle                *scriptBundle;
178         //
179         [scriptArray release]; scriptArray = [[NSMutableArray alloc] init];
180         [flatScriptArray release]; flatScriptArray = [[NSMutableArray alloc] init];
181         
182         // Load scripts
183         enumerator = [[adium allResourcesForName:@"Scripts" withExtensions:SCRIPT_BUNDLE_EXTENSION] objectEnumerator];
184         while ((filePath = [enumerator nextObject])) {
185                 if ((scriptBundle = [NSBundle bundleWithPath:filePath])) {
186                         NSString                *scriptsSetName;
187                         NSEnumerator    *scriptEnumerator;
188                         NSDictionary    *scriptDict;
189                         NSDictionary    *infoDict = [NSDictionary dictionaryWithContentsOfFile:[[scriptBundle bundlePath] stringByAppendingPathComponent:@"Info.plist"]];
190                         if (!infoDict) infoDict= [scriptBundle infoDictionary];
192                         NSDictionary    *localizedInfoDict = [scriptBundle localizedInfoDictionary];
194                         //Get the name of the set these scripts will go into
195                         scriptsSetName = [localizedInfoDict objectForKey:@"Set"];
196                         if (!scriptsSetName) scriptsSetName = [infoDict objectForKey:@"Set"];
198                         //Now enumerate each script the bundle claims as its own
199                         scriptEnumerator = [[infoDict objectForKey:@"Scripts"] objectEnumerator];
200                         
201                         while ((scriptDict = [scriptEnumerator nextObject])) {
202                                 NSString                *scriptFileName, *scriptFilePath, *keyword, *title;
203                                 NSArray                 *arguments;
204                                 NSNumber                *prefixOnlyNumber;
205                                 
206                                 if ((scriptFileName = [scriptDict objectForKey:@"File"]) &&
207                                         (scriptFilePath = [scriptBundle pathForResource:scriptFileName
208                                                                                                                          ofType:SCRIPT_EXTENSION])) {
209                                         
210                                         keyword = [scriptDict objectForKey:@"Keyword"];
211                                         title = [scriptDict objectForKey:@"Title"];
213                                         //The keywords titles are keyed by their English version in the localized info dict
214                                         NSString *localizedKeyword = [localizedInfoDict objectForKey:keyword];
215                                         if (localizedKeyword) keyword = localizedKeyword;
217                                         NSString *localizedTitle = [localizedInfoDict objectForKey:title];
218                                         if (localizedTitle) title = localizedTitle;
220                                         if (keyword && [keyword length] && title && [title length]) {
221                                                 NSMutableDictionary     *infoDict;
222                                                 
223                                                 arguments = [[scriptDict objectForKey:@"Arguments"] componentsSeparatedByString:@","];
224                                                 
225                                                 //Assume "Prefix Only" is NO unless told otherwise or the keyword starts with '/'
226                                                 prefixOnlyNumber = [scriptDict objectForKey:@"Prefix Only"];
227                                                 if (!prefixOnlyNumber) {
228                                                         prefixOnlyNumber = [NSNumber numberWithBool:([keyword characterAtIndex:0] == '/')];
229                                                 }
231                                                 infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
232                                                         scriptFilePath, @"Path", keyword, @"Keyword", title, @"Title", 
233                                                         prefixOnlyNumber, @"PrefixOnly", nil];
234                                                 
235                                                 //The bundle may not be part of (or for defining) a set of scripts
236                                                 if (scriptsSetName) {
237                                                         [infoDict setObject:scriptsSetName forKey:@"Set"];
238                                                 }
239                                                 //Arguments may be nil
240                                                 if (arguments) {
241                                                         [infoDict setObject:arguments forKey:@"Arguments"];
242                                                 }
243                                                 
244                                                 //Place the entry in our script arrays
245                                                 [scriptArray addObject:infoDict];
246                                                 [flatScriptArray addObject:infoDict];
247                                                 
248                                                 //Scripts must always be updated via polling
249                                                 [[adium contentController] registerFilterStringWhichRequiresPolling:keyword];
250                                         }
251                                 }
252                         }
253                 } else {
254                         NSLog(@"Warning: Could not load Adium script bundle at %@",filePath);
255                 }
256         }
260 //Script Menu ----------------------------------------------------------------------------------------------------------
261 #pragma mark Script Menu
263  * @brief Build the script menu
265  * Loads the scrpts as necessary, sorts them, then builds menus for the menu bar, the contextual menu,
266  * and the toolbar item.
267  */
268 - (void)buildScriptMenu
270         [self loadScripts];
271         
272         //Sort the scripts
273         [scriptArray sortUsingFunction:_scriptTitleSort context:nil];
274         [flatScriptArray sortUsingFunction:_scriptKeywordLengthSort context:nil];
275         
276         //Build the menu
277         [scriptMenu release]; scriptMenu = [[NSMenu alloc] initWithTitle:TITLE_INSERT_SCRIPT];
278         [self _appendScripts:scriptArray toMenu:scriptMenu];
279         [scriptMenuItem setSubmenu:scriptMenu];
280         [contextualScriptMenuItem setSubmenu:[[scriptMenu copy] autorelease]];
281                 
282         [self registerToolbarItem];
286  * @brief Sort first by set, then by title within sets
287  */
288 int _scriptTitleSort(id scriptA, id scriptB, void *context) {
289         NSComparisonResult result;
290         
291         NSString        *setA = [scriptA objectForKey:@"Set"];
292         NSString        *setB = [scriptB objectForKey:@"Set"];
293         
294         if (setA && setB) {
295                 
296                 //If both are within sets, sort by set; if they are within the same set, sort by title
297                 if ((result = [setA caseInsensitiveCompare:setB]) == NSOrderedSame) {
298                         result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
299                 }
300         } else {
301                 //Sort by title if neither is in a set; otherwise sort the one in a set to the top
302                 
303                 if (!setA && !setB) {
304                         result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
305                 
306                 } else if (!setA) {
307                         result = NSOrderedDescending;
308                 } else {
309                         result = NSOrderedAscending;
310                 }
311         }
312         
313         return result;
317  * @brief Sort by descending length so the longest keywords are at the beginning of the array
318  */
319 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context)
321         NSComparisonResult result;
322         
323         unsigned int lengthA = [(NSString *)[scriptA objectForKey:@"Keyword"] length];
324         unsigned int lengthB = [(NSString *)[scriptB objectForKey:@"Keyword"] length];
325         if (lengthA > lengthB) {
326                 result = NSOrderedAscending;
327         } else if (lengthA < lengthB) {
328                 result = NSOrderedDescending;
329         } else {
330                 result = NSOrderedSame;
331         }
332         
333         return result;
337  * @brief Append an array of scripts to a menu
339  * @param scripts The scripts, each of which is represented by an NSDictionary instance
340  * @param menu The menu to which to add the scripts
341  */
342 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu
344         NSEnumerator    *enumerator;
345         NSDictionary    *appendDict;
346         NSString                *lastSet = nil;
347         NSString                *set;
348         int                             indentationLevel;
349         
350         enumerator = [scripts objectEnumerator];
351         while ((appendDict = [enumerator nextObject])) {
352                 NSString        *title;
353                 NSMenuItem      *item;
354                 
355                 if ((set = [appendDict objectForKey:@"Set"])) {
356                         indentationLevel = 1;
357                         
358                         if (![set isEqualToString:lastSet]) {
359                                 //We have a new set of scripts; create a section header for them
360                                 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:set
361                                                                                                                                                          target:nil
362                                                                                                                                                          action:nil
363                                                                                                                                           keyEquivalent:@""] autorelease];
364                                 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:0];
365                                 [menu addItem:item];
366                                 
367                                 [lastSet release]; lastSet = [set retain];
368                         }
369                 } else {
370                         //Scripts not in sets need not be indented
371                         indentationLevel = 0;
372                         [lastSet release]; lastSet = nil;
373                 }
374         
375                 if ([appendDict objectForKey:@"Title"]) {
376                         title = [NSString stringWithFormat:@"%@ (%@)", [appendDict objectForKey:@"Title"], [appendDict objectForKey:@"Keyword"]];
377                 } else {
378                         title = [appendDict objectForKey:@"Keyword"];
379                 }
380                 
381                 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
382                                                                                                                                          target:self
383                                                                                                                                          action:@selector(selectScript:)
384                                                                                                                           keyEquivalent:@""] autorelease];
385                 
386                 [item setRepresentedObject:appendDict];
387                 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:indentationLevel];
388                 [menu addItem:item];
389         }
393  * @brief Insert a script's keyword into the text entry area
395  * This will be called by an NSMenuItem when it is clicked.
396  */
397 - (IBAction)selectScript:(id)sender
399         NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
400         
401         //Append our string into the responder if possible
402         if (responder && [responder isKindOfClass:[NSTextView class]]) {
403                 NSArray         *arguments = [[sender representedObject] objectForKey:@"Arguments"];
404                 NSString        *replacementText = [[sender representedObject] objectForKey:@"Keyword"];
405                 
406                 [(NSTextView *)responder insertText:replacementText];
407                 
408                 //Append arg list to replacement string, to show the user what they can pass
409                 if (arguments) {
410                         NSEnumerator            *argumentEnumerator = [arguments objectEnumerator];
411                         NSDictionary            *originalTypingAttributes = [(NSTextView *)responder typingAttributes];
412                         NSMutableDictionary *italicizedTypingAttributes = [originalTypingAttributes mutableCopy];
413                         NSString                        *anArgument;
414                         BOOL                            insertedFirst = NO;
415                         
416                         [italicizedTypingAttributes setObject:[[NSFontManager sharedFontManager] convertFont:[originalTypingAttributes objectForKey:NSFontAttributeName]
417                                                                                                                                                                          toHaveTrait:NSItalicFontMask]
418                                                                                    forKey:NSFontAttributeName];
419                         
420                         [(NSTextView *)responder insertText:@"{"];
421                         
422                         //Will that be a five minute argument or the full half hour?
423                         while ((anArgument = [argumentEnumerator nextObject])) {
424                                 //Insert a comma after each argument past the first
425                                 if (insertedFirst) {
426                                         [(NSTextView *)responder insertText:@","];                                      
427                                 } else {
428                                         insertedFirst = YES;
429                                 }
430                                 
431                                 //Turn on the italics version, insert the argument, then go back to normal for either the comma or the ending
432                                 [(NSTextView *)responder setTypingAttributes:italicizedTypingAttributes];
433                                 [(NSTextView *)responder insertText:anArgument];
434                                 [(NSTextView *)responder setTypingAttributes:originalTypingAttributes];
435                         }
437                         [(NSTextView *)responder insertText:@"}"];
438                         
439                         [italicizedTypingAttributes release];
440                 }
441         }
445  * @brief Fake target to allow validateMenuItem: to be called
446  */
447 -(IBAction)dummyTarget:(id)sender{
451  * @brief Validate menu item
452  * Disable the insertion if a text field is not active
453  */
454 - (BOOL)validateMenuItem:(NSMenuItem *)menuItem
456         if ((menuItem == scriptMenuItem) || (menuItem == contextualScriptMenuItem)) {
457                 return YES; //Always keep the submenu enabled so users can see the available scripts
458         } else {
459                 NSResponder     *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
460                 if (responder && [responder isKindOfClass:[NSText class]]) {
461                         return [(NSText *)responder isEditable];
462                 } else {
463                         return NO;
464                 }
465         }
468 //Message Filtering ----------------------------------------------------------------------------------------------------
469 #pragma mark Message Filtering
471  * @brief Delayed filter messages for keywords to replace
473  * Will eventually replace any script keywords with the result of running the script (with arguments as appropriate).
474  * @result YES if we began a delayed filtration; NO if we did not
475  */
476 - (BOOL)delayedFilterAttributedString:(NSAttributedString *)inAttributedString context:(id)context uniqueID:(unsigned long long)uniqueID
478         BOOL            beganProcessing = NO; 
479         NSString        *stringMessage;
481         if ((stringMessage = [inAttributedString string])) {
482                 NSEnumerator                            *enumerator;
483                 NSMutableDictionary                     *infoDict;
484                 
485                 //Replace all keywords
486                 enumerator = [flatScriptArray objectEnumerator];
487                 while ((infoDict = [enumerator nextObject]) && !beganProcessing) {
488                         NSString        *keyword = [infoDict objectForKey:@"Keyword"];
489                         BOOL            prefixOnly = [[infoDict objectForKey:@"PrefixOnly"] boolValue];
491                         if ((prefixOnly && ([stringMessage rangeOfString:keyword options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0)) ||
492                            (!prefixOnly && [stringMessage rangeOfString:keyword options:NSCaseInsensitiveSearch].location != NSNotFound)) {
493                                 NSNumber        *shouldSendNumber;
495                                 [self _replaceKeyword:keyword
496                                                    withScript:infoDict
497                                                          inString:stringMessage
498                                    inAttributedString:[[inAttributedString mutableCopy] autorelease]
499                                                           context:context
500                                                          uniqueID:uniqueID];
502                                 shouldSendNumber = [infoDict objectForKey:@"ShouldSend"];
503                                 if ((shouldSendNumber) &&
504                                         (![shouldSendNumber boolValue]) &&
505                                         ([context isKindOfClass:[AIContentObject class]])) {
506                                         [(AIContentObject *)context setSendContent:NO];
507                                 }
508                                 
509                                 beganProcessing = YES;
510                         }
511                 }
512         }
513         
514     return beganProcessing;
518  * @brief Filter priority
520  * Filter earlier than the default
521  */
522 - (float)filterPriority
524         return HIGH_FILTER_PRIORITY;
528  * @brief Replace one instance of a keyword within a string. This will be called once for each instance.
529  */
530 - (void)_replaceKeyword:(NSString *)keyword
531                          withScript:(NSMutableDictionary *)infoDict
532                            inString:(NSString *)inString
533          inAttributedString:(NSMutableAttributedString *)attributedString
534                                 context:(id)context
535                            uniqueID:(unsigned long long)uniqueID
537         NSScanner       *scanner;
538         BOOL            foundKeyword = NO;
540         //Scan for the keyword
541         scanner = [NSScanner scannerWithString:inString];
542         while (![scanner isAtEnd] && !foundKeyword) {
543                 [scanner scanUpToString:keyword intoString:nil];
544                 
545                 if (([scanner scanString:keyword intoString:nil]) &&
546                         ([attributedString attribute:NSLinkAttributeName
547                                                                  atIndex:([scanner scanLocation]-1) /* The scanner ends up one past the keyword */
548                                                   effectiveRange:nil] == nil)) {
549                         //Scan the keyword and ensure it was not found within a link
550                         int             keywordStart, keywordEnd;
551                         NSArray         *argArray = nil;
552                         NSString        *argString;
553                         
554                         //Scan arguments
555                         keywordStart = [scanner scanLocation] - [keyword length];
556                         if ([scanner scanString:@"{" intoString:nil]) {
557                                 if ([scanner scanUpToString:@"}" intoString:&argString]) {
558                                         argArray = [self _argumentsFromString:argString forScript:infoDict];
559                                         [scanner scanString:@"}" intoString:nil];
560                                 }                               
561                         }
562                         keywordEnd = [scanner scanLocation];            
563                         
564                         //Run the script.
565                         NSRange keywordRange = NSMakeRange(keywordStart, keywordEnd - keywordStart);
566                         [self _executeScript:infoDict 
567                                    withArguments:argArray
568                          forAttributedString:attributedString
569                                         keywordRange:keywordRange
570                                                  context:context
571                                                 uniqueID:uniqueID];
572                         
573                         foundKeyword = YES;
574                 }
575         }
579  * @brief Execute the script as a separate task
581  * When the task is complete, we will be notified, at which point we perform the replacement for the script result
582  * and pass the modified attributed string back to the content controller for use.
583  */
584 - (void)_executeScript:(NSMutableDictionary *)infoDict 
585                            withArguments:(NSArray *)arguments
586                  forAttributedString:(NSMutableAttributedString *)attributedString
587                                 keywordRange:(NSRange)keywordRange
588                                          context:(id)context
589                                         uniqueID:(unsigned long long)uniqueID
591         NSDictionary    *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
592                 attributedString, @"Mutable Attributed String",
593                 NSStringFromRange(keywordRange), @"Range",
594                 [NSNumber numberWithUnsignedLongLong:uniqueID], @"uniqueID",
595                 (context ? context : [NSNull null]), @"context",
596                 nil];
597         
598         [[adium applescriptabilityController] runApplescriptAtPath:[infoDict objectForKey:@"Path"]
599                                                                                                           function:@"substitute"
600                                                                                                          arguments:arguments
601                                                                                            notifyingTarget:self
602                                                                                                           selector:@selector(applescriptDidRun:resultString:)
603                                                                                                           userInfo:userInfo];
607  * @brief A script finished running
608  */
609 - (void)applescriptDidRun:(id)userInfo resultString:(NSString *)resultString
611         NSMutableAttributedString       *attributedString = [userInfo objectForKey:@"Mutable Attributed String"];
612         NSRange                                         keywordRange = NSRangeFromString([userInfo objectForKey:@"Range"]);
613         unsigned long long                      uniqueID = [[userInfo objectForKey:@"uniqueID"] unsignedLongLongValue];
615         //If the script fails, eat the keyword
616         if (!resultString) resultString = @"";
618         //Replace the substring with script result
619         if (NSMaxRange(keywordRange) <= [attributedString length]) {
620                 if (([resultString hasPrefix:@"<HTML>"])) {
621                         //Obtain the attributed string version of the HTML, passing our current attributes as the default ones
622                         NSAttributedString *attributedScriptResult = [AIHTMLDecoder decodeHTML:resultString
623                                                                                                                          withDefaultAttributes:[attributedString attributesAtIndex:keywordRange.location
624                                                                                                                                                                                                                 effectiveRange:nil]];
625                         [attributedString replaceCharactersInRange:keywordRange
626                                                                   withAttributedString:attributedScriptResult];
627                         
628                 } else {
629                         [attributedString replaceCharactersInRange:keywordRange
630                                                                                         withString:resultString];
631                 }
632         }
634         //Inform the content controller that we're done if we don't need to do any more filtering
635         if (![self delayedFilterAttributedString:attributedString
636                                                                          context:[userInfo objectForKey:@"context"]
637                                                                         uniqueID:uniqueID]) {
638                 [[adium contentController] delayedFilterDidFinish:attributedString
639                                                                                                  uniqueID:uniqueID];
640         }
644  * @brief Determine the arguments for a script execution
646  * @param inString The string of potential arguments
647  * @param scriptDict The script being executed
649  * @result An NSArray of NSString instances
650  */
651 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict
653         NSArray                 *scriptArguments = [scriptDict objectForKey:@"Arguments"];
654         NSMutableArray  *argArray = [NSMutableArray array];
655         NSArray                 *inStringComponents = [inString componentsSeparatedByString:@","];
656         
657         unsigned                i = 0;
658         unsigned                count = (scriptArguments ? [scriptArguments count] : 0);
659         unsigned                inStringComponentsCount = [inStringComponents count];
660         
661         //Add each argument of inString to argArray so long as the number of arguments is less
662         //than the number of expected arguments for the script and the number of supplied arguments
663         while ((i < count) && (i < inStringComponentsCount)) {
664                 [argArray addObject:[inStringComponents objectAtIndex:i]];
665                 i++;
666         }
667         
668         //If more components were passed than were actually requested, the last argument gets the
669         //remainder
670         if (i < inStringComponentsCount) {
671                 NSRange remainingRange;
672                 
673                 //i was incremented to end the while loop if i > 0, so subtract 1 to reexamine the last object
674                 remainingRange.location = ((i > 0) ? i-1 : 0);
675                 remainingRange.length = (inStringComponentsCount - remainingRange.location);
677                 if (remainingRange.location >= 0) {
678                         NSString        *lastArgument;
680                         //Remove that last, incomplete argument if it was added
681                         if ([argArray count]) [argArray removeLastObject];
683                         //Create the last argument by joining all remaining comma-separated arguments with a comma
684                         lastArgument = [[inStringComponents subarrayWithRange:remainingRange] componentsJoinedByString:@","];
686                         [argArray addObject:lastArgument];
687                 }
688         }
689         
690         return argArray;
693 #pragma mark Toolbar item
695  * @brief Register our insert script toolbar item
696  */
697 - (void)registerToolbarItem
699         MVMenuButton *button;
700         
701         //Unregister the existing toolbar item first
702         if (toolbarItem) {
703                 [[adium toolbarController] unregisterToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
704                 [toolbarItem release]; toolbarItem = nil;
705         }
706         
707         //Register our toolbar item
708         button = [[[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)] autorelease];
709         [button setImage:[NSImage imageNamed:@"scriptToolbar" forClass:[self class] loadLazily:YES]];
710         toolbarItem = [[AIToolbarUtilities toolbarItemWithIdentifier:SCRIPT_IDENTIFIER
711                                                                                                                    label:AILocalizedString(@"Scripts",nil)
712                                                                                                         paletteLabel:TITLE_INSERT_SCRIPT
713                                                                                                                  toolTip:AILocalizedString(@"Insert a script",nil)
714                                                                                                                   target:self
715                                                                                                  settingSelector:@selector(setView:)
716                                                                                                          itemContent:button
717                                                                                                                   action:@selector(selectScript:)
718                                                                                                                         menu:nil] retain];
719         [toolbarItem setMinSize:NSMakeSize(32,32)];
720         [toolbarItem setMaxSize:NSMakeSize(32,32)];
721         [button setToolbarItem:toolbarItem];
722     [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
726  * @brief After the toolbar has added the item we can set up the submenus
727  */
728 - (void)toolbarWillAddItem:(NSNotification *)notification
730         NSToolbarItem   *item = [[notification userInfo] objectForKey:@"item"];
731         
732         if (!notification || ([[item itemIdentifier] isEqualToString:SCRIPT_IDENTIFIER])) {
733                 NSMenu          *menu = [[[scriptMenuItem submenu] copy] autorelease];
734                 
735                 //Add menu to view
736                 [[item view] setMenu:menu];
737                 
738                 //Add menu to toolbar item (for text mode)
739                 NSMenuItem      *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
740                 [mItem setSubmenu:menu];
741                 [mItem setTitle:[menu title]];
742                 [item setMenuFormRepresentation:mItem];
743         }
746 @end