2 * Adium is the legal property of its developers, whose names are listed in the copyright file included
3 * with this source distribution.
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.
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.
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.
17 #import "AIContentController.h"
18 #import "AIMenuController.h"
19 #import "AIToolbarController.h"
20 #import "GBApplescriptFiltersPlugin.h"
21 #import <AIUtilities/AIMenuAdditions.h>
22 #import <AIUtilities/AIToolbarUtilities.h>
23 #import <AIUtilities/ESImageAdditions.h>
24 #import <AIUtilities/MVMenuButton.h>
25 #import <AIUtilities/AIExceptionHandlingUtilities.h>
26 #import <Adium/AIContentObject.h>
27 #import <Adium/AIHTMLDecoder.h>
29 #define TITLE_INSERT_SCRIPT AILocalizedString(@"Insert Script",nil)
30 #define SCRIPT_BUNDLE_EXTENSION @"AdiumScripts"
31 #define SCRIPTS_PATH_NAME @"Scripts"
32 #define SCRIPT_EXTENSION @"scpt"
33 #define SCRIPT_IDENTIFIER @"InsertScript"
35 #define SCRIPT_TIMEOUT 30
37 @interface GBApplescriptFiltersPlugin (PRIVATE)
39 - (void)_replaceKeyword:(NSString *)keyword
40 withScript:(NSMutableDictionary *)infoDict
41 inString:(NSString *)inString
42 inAttributedString:(NSMutableAttributedString *)attributedString
43 uniqueID:(unsigned long long)uniqueID;
45 - (void)_executeScript:(NSMutableDictionary *)infoDict
46 withArguments:(NSArray *)arguments
47 forAttributedString:(NSMutableAttributedString *)attributedString
48 keywordRange:(NSRange)keywordRange
49 uniqueID:(unsigned long long)uniqueID;
51 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict;
53 - (void)buildScriptMenu;
54 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu;
55 - (void)_sortScriptsByTitle:(NSMutableArray *)sortArray;
57 - (void)registerToolbarItem;
59 - (void)scriptDidFinish:(NSNotification *)aNotification;
62 int _scriptTitleSort(id scriptA, id scriptB, void *context);
63 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context);
66 * @class GBApplescriptFiltersPlugin
67 * @brief Filter component to allow .AdiumScripts applescript-based filters for outgoing messages
69 @implementation GBApplescriptFiltersPlugin
77 [[AIObject sharedAdiumInstance] createResourcePathForName:@"Scripts"];
79 //We have an array of scripts for building the menu, and a dictionary of scripts used for the actual substition
81 flatScriptArray = nil;
83 //Prepare our script menu item (which will have the Scripts menu as its submenu)
84 scriptMenuItem = [[NSMenuItem alloc] initWithTitle:TITLE_INSERT_SCRIPT
86 action:@selector(dummyTarget:)
89 //Perform substitutions on outgoing content; we may be slow, so register as a delayed content filter
90 [[adium contentController] registerDelayedContentFilter:self
91 ofType:AIFilterContent
92 direction:AIFilterOutgoing];
94 //Observe for installation of new scripts
95 [[adium notificationCenter] addObserver:self
96 selector:@selector(xtrasChanged:)
97 name:Adium_Xtras_Changed
99 [[NSNotificationCenter defaultCenter] addObserver:self
100 selector:@selector(toolbarWillAddItem:)
101 name:NSToolbarWillAddItemNotification
104 //Start building the script menu
106 [self buildScriptMenu]; //this also sets the submenu for the menu item.
108 [[adium menuController] addMenuItem:scriptMenuItem toLocation:LOC_Edit_Additions];
110 contextualScriptMenuItem = [scriptMenuItem copy];
111 [[adium menuController] addContextualMenuItem:contextualScriptMenuItem toLocation:Context_TextView_Edit];
119 [[NSNotificationCenter defaultCenter] removeObserver:self];
120 [[adium notificationCenter] removeObserver:self];
122 [scriptArray release]; scriptArray = nil;
123 [flatScriptArray release]; flatScriptArray = nil;
124 [scriptMenuItem release]; scriptMenuItem = nil;
125 [contextualScriptMenuItem release]; contextualScriptMenuItem = nil;
131 * @brief Xtras changes
133 * If the scripts xtras changed, rebuild our menus.
135 - (void)xtrasChanged:(NSNotification *)notification
137 if ([[notification object] caseInsensitiveCompare:@"AdiumScripts"] == 0) {
138 [self buildScriptMenu];
140 [self registerToolbarItem];
142 //Update our toolbar item's menu
143 //[self toolbarWillAddItem:nil];
148 //Script Loading -------------------------------------------------------------------------------------------------------
149 #pragma mark Script Loading
151 * @brief Load our scripts
153 * This will clear out and then load from available scripts (external and internal) into flatScriptArray and scriptArray.
157 NSEnumerator *enumerator;
159 NSBundle *scriptBundle;
162 [scriptArray release]; scriptArray = [[NSMutableArray alloc] init];
163 [flatScriptArray release]; flatScriptArray = [[NSMutableArray alloc] init];
166 enumerator = [[adium allResourcesForName:@"Scripts" withExtensions:SCRIPT_BUNDLE_EXTENSION] objectEnumerator];
167 while ((filePath = [enumerator nextObject])) {
168 if ((scriptBundle = [NSBundle bundleWithPath:filePath])) {
170 NSString *scriptsSetName;
171 NSEnumerator *scriptEnumerator;
172 NSDictionary *scriptDict;
174 //Get the name of the set these scripts will go into
175 scriptsSetName = [scriptBundle objectForInfoDictionaryKey:@"Set"];
177 //Now enumerate each script the bundle claims as its own
178 scriptEnumerator = [[scriptBundle objectForInfoDictionaryKey:@"Scripts"] objectEnumerator];
180 while ((scriptDict = [scriptEnumerator nextObject])) {
181 NSString *scriptFileName, *scriptFilePath, *keyword, *title;
183 NSNumber *prefixOnlyNumber;
185 if ((scriptFileName = [scriptDict objectForKey:@"File"]) &&
186 (scriptFilePath = [scriptBundle pathForResource:scriptFileName
187 ofType:SCRIPT_EXTENSION])) {
189 keyword = [scriptDict objectForKey:@"Keyword"];
190 title = [scriptDict objectForKey:@"Title"];
192 if (keyword && [keyword length] && title && [title length]) {
193 NSMutableDictionary *infoDict;
195 arguments = [[scriptDict objectForKey:@"Arguments"] componentsSeparatedByString:@","];
197 //Assume "Prefix Only" is NO unless told otherwise or the keyword starts with '/'
198 prefixOnlyNumber = [scriptDict objectForKey:@"Prefix Only"];
199 if (!prefixOnlyNumber) {
200 prefixOnlyNumber = [NSNumber numberWithBool:([keyword characterAtIndex:0] == '/')];
203 infoDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
204 scriptFilePath, @"Path", keyword, @"Keyword", title, @"Title",
205 prefixOnlyNumber, @"PrefixOnly", nil];
207 //The bundle may not be part of (or for defining) a set of scripts
208 if (scriptsSetName) {
209 [infoDict setObject:scriptsSetName forKey:@"Set"];
211 //Arguments may be nil
213 [infoDict setObject:arguments forKey:@"Arguments"];
216 //Place the entry in our script arrays
217 [scriptArray addObject:infoDict];
218 [flatScriptArray addObject:infoDict];
220 //Scripts must always be updated via polling
221 [[adium contentController] registerFilterStringWhichRequiresPolling:keyword];
230 //Script Menu ----------------------------------------------------------------------------------------------------------
231 #pragma mark Script Menu
233 * @brief Build the script menu
235 * Loads the scrpts as necessary, sorts them, then builds menus for the menu bar, the contextual menu,
236 * and the toolbar item.
238 - (void)buildScriptMenu
243 [scriptArray sortUsingFunction:_scriptTitleSort context:nil];
244 [flatScriptArray sortUsingFunction:_scriptKeywordLengthSort context:nil];
247 [scriptMenu release]; scriptMenu = [[NSMenu alloc] initWithTitle:TITLE_INSERT_SCRIPT];
248 [self _appendScripts:scriptArray toMenu:scriptMenu];
249 [scriptMenuItem setSubmenu:scriptMenu];
250 [contextualScriptMenuItem setSubmenu:[[scriptMenu copy] autorelease]];
252 [self registerToolbarItem];
256 * @brief Sort first by set, then by title within sets
258 int _scriptTitleSort(id scriptA, id scriptB, void *context) {
259 NSComparisonResult result;
261 NSString *setA = [scriptA objectForKey:@"Set"];
262 NSString *setB = [scriptB objectForKey:@"Set"];
266 //If both are within sets, sort by set; if they are within the same set, sort by title
267 if ((result = [setA caseInsensitiveCompare:setB]) == NSOrderedSame) {
268 result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
271 //Sort by title if neither is in a set; otherwise sort the one in a set to the top
273 if (!setA && !setB) {
274 result = [(NSString *)[scriptA objectForKey:@"Title"] caseInsensitiveCompare:[scriptB objectForKey:@"Title"]];
277 result = NSOrderedDescending;
279 result = NSOrderedAscending;
287 * @brief Sort by descending length so the longest keywords are at the beginning of the array
289 int _scriptKeywordLengthSort(id scriptA, id scriptB, void *context)
291 NSComparisonResult result;
293 unsigned int lengthA = [(NSString *)[scriptA objectForKey:@"Keyword"] length];
294 unsigned int lengthB = [(NSString *)[scriptB objectForKey:@"Keyword"] length];
295 if (lengthA > lengthB) {
296 result = NSOrderedAscending;
297 } else if (lengthA < lengthB) {
298 result = NSOrderedDescending;
300 result = NSOrderedSame;
307 * @brief Append an array of scripts to a menu
309 * @param scripts The scripts, each of which is represented by an NSDictionary instance
310 * @param menu The menu to which to add the scripts
312 - (void)_appendScripts:(NSArray *)scripts toMenu:(NSMenu *)menu
314 NSEnumerator *enumerator;
315 NSDictionary *appendDict;
316 NSString *lastSet = nil;
318 int indentationLevel;
320 enumerator = [scripts objectEnumerator];
321 while ((appendDict = [enumerator nextObject])) {
325 if ((set = [appendDict objectForKey:@"Set"])) {
326 indentationLevel = 1;
328 if (![set isEqualToString:lastSet]) {
329 //We have a new set of scripts; create a section header for them
330 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:set
333 keyEquivalent:@""] autorelease];
334 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:0];
337 [lastSet release]; lastSet = [set retain];
340 //Scripts not in sets need not be indented
341 indentationLevel = 0;
342 [lastSet release]; lastSet = nil;
345 if ([appendDict objectForKey:@"Title"]) {
346 title = [NSString stringWithFormat:@"%@ (%@)", [appendDict objectForKey:@"Title"], [appendDict objectForKey:@"Keyword"]];
348 title = [appendDict objectForKey:@"Keyword"];
351 item = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] initWithTitle:title
353 action:@selector(selectScript:)
354 keyEquivalent:@""] autorelease];
356 [item setRepresentedObject:appendDict];
357 if ([item respondsToSelector:@selector(setIndentationLevel:)]) [item setIndentationLevel:indentationLevel];
363 * @brief Insert a script's keyword into the text entry area
365 * This will be called by an NSMenuItem when it is clicked.
367 - (IBAction)selectScript:(id)sender
369 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
371 //Append our string into the responder if possible
372 if (responder && [responder isKindOfClass:[NSTextView class]]) {
373 NSArray *arguments = [[sender representedObject] objectForKey:@"Arguments"];
374 NSString *replacementText = [[sender representedObject] objectForKey:@"Keyword"];
376 [(NSTextView *)responder insertText:replacementText];
378 //Append arg list to replacement string, to show the user what they can pass
380 NSEnumerator *argumentEnumerator = [arguments objectEnumerator];
381 NSDictionary *originalTypingAttributes = [(NSTextView *)responder typingAttributes];
382 NSMutableDictionary *italicizedTypingAttributes = [originalTypingAttributes mutableCopy];
383 NSString *anArgument;
384 BOOL insertedFirst = NO;
386 [italicizedTypingAttributes setObject:[[NSFontManager sharedFontManager] convertFont:[originalTypingAttributes objectForKey:NSFontAttributeName]
387 toHaveTrait:NSItalicFontMask]
388 forKey:NSFontAttributeName];
390 [(NSTextView *)responder insertText:@"{"];
392 //Will that be a five minute argument or the full half hour?
393 while ((anArgument = [argumentEnumerator nextObject])) {
394 //Insert a comma after each argument past the first
396 [(NSTextView *)responder insertText:@","];
401 //Turn on the italics version, insert the argument, then go back to normal for either the comma or the ending
402 [(NSTextView *)responder setTypingAttributes:italicizedTypingAttributes];
403 [(NSTextView *)responder insertText:anArgument];
404 [(NSTextView *)responder setTypingAttributes:originalTypingAttributes];
407 [(NSTextView *)responder insertText:@"}"];
409 [italicizedTypingAttributes release];
415 * @brief Fake target to allow validateMenuItem: to be called
417 -(IBAction)dummyTarget:(id)sender{
421 * @brief Validate menu item
422 * Disable the insertion if a text field is not active
424 - (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
426 if ((menuItem == scriptMenuItem) || (menuItem == contextualScriptMenuItem)) {
427 return(YES); //Always keep the submenu enabled so users can see the available scripts
429 NSResponder *responder = [[[NSApplication sharedApplication] keyWindow] firstResponder];
430 if (responder && [responder isKindOfClass:[NSText class]]) {
431 return [(NSText *)responder isEditable];
438 //Message Filtering ----------------------------------------------------------------------------------------------------
439 #pragma mark Message Filtering
441 * @brief Delayed filter messages for keywords to replace
443 * Will eventually replace any script keywords with the result of running the script (with arguments as appropriate).
444 * @result YES if we began a delayed filtration; NO if we did not
446 - (BOOL)delayedFilterAttributedString:(NSAttributedString *)inAttributedString context:(id)context uniqueID:(unsigned long long)uniqueID
448 BOOL beganProcessing = NO;
449 NSString *stringMessage;
451 if ((stringMessage = [inAttributedString string])) {
452 NSEnumerator *enumerator;
453 NSMutableDictionary *infoDict;
455 //Replace all keywords
456 enumerator = [flatScriptArray objectEnumerator];
457 while ((infoDict = [enumerator nextObject]) && !beganProcessing) {
458 NSString *keyword = [infoDict objectForKey:@"Keyword"];
459 BOOL prefixOnly = [[infoDict objectForKey:@"PrefixOnly"] boolValue];
461 if ((prefixOnly && ([stringMessage rangeOfString:keyword options:(NSCaseInsensitiveSearch | NSAnchoredSearch)].location == 0)) ||
462 (!prefixOnly && [stringMessage rangeOfString:keyword options:NSCaseInsensitiveSearch].location != NSNotFound)) {
463 NSNumber *shouldSendNumber;
465 [self _replaceKeyword:keyword
467 inString:stringMessage
468 inAttributedString:[[inAttributedString mutableCopy] autorelease]
471 shouldSendNumber = [infoDict objectForKey:@"ShouldSend"];
472 if ((shouldSendNumber) &&
473 (![shouldSendNumber boolValue]) &&
474 ([context isKindOfClass:[AIContentObject class]])) {
475 [(AIContentObject *)context setSendContent:NO];
478 beganProcessing = YES;
483 return beganProcessing;
487 * @brief Filter priority
489 * Filter earlier than the default
491 - (float)filterPriority
493 return HIGH_FILTER_PRIORITY;
497 * @brief Perform a thorough variable replacing scan
499 - (void)_replaceKeyword:(NSString *)keyword
500 withScript:(NSMutableDictionary *)infoDict
501 inString:(NSString *)inString
502 inAttributedString:(NSMutableAttributedString *)attributedString
503 uniqueID:(unsigned long long)uniqueID
506 BOOL foundKeyword = NO;
507 BOOL beganExecutingScript = NO;
509 //Scan for the keyword
510 scanner = [NSScanner scannerWithString:inString];
511 while (![scanner isAtEnd] && !foundKeyword) {
512 [scanner scanUpToString:keyword intoString:nil];
514 if (([scanner scanString:keyword intoString:nil]) &&
515 ([attributedString attribute:NSLinkAttributeName
516 atIndex:([scanner scanLocation]-1) /* The scanner ends up one past the keyword */
517 effectiveRange:nil] == nil)) {
518 //Scan the keyword and ensure it was not found within a link
519 int keywordStart, keywordEnd;
520 NSArray *argArray = nil;
524 keywordStart = [scanner scanLocation] - [keyword length];
525 if ([scanner scanString:@"{" intoString:nil]) {
526 if ([scanner scanUpToString:@"}" intoString:&argString]) {
527 argArray = [self _argumentsFromString:argString forScript:infoDict];
528 [scanner scanString:@"}" intoString:nil];
531 keywordEnd = [scanner scanLocation];
533 if (keywordStart != 0 && [inString characterAtIndex:keywordStart - 1] == '\\') {
534 //Ignore the script (It was escaped) and delete the escape character
535 [attributedString replaceCharactersInRange:NSMakeRange(keywordStart - 1, 1) withString:@""];
540 NSRange keywordRange = NSMakeRange(keywordStart, keywordEnd - keywordStart);
542 [self _executeScript:infoDict
543 withArguments:argArray
544 forAttributedString:attributedString
545 keywordRange:keywordRange
548 beganExecutingScript = YES;
555 * @brief Execute the script as a separate task
557 * When the task is complete, we will be notified, at which point we perform the replacement for the script result
558 * and pass the modified attributed string back to the content controller for use.
560 - (void)_executeScript:(NSMutableDictionary *)infoDict
561 withArguments:(NSArray *)arguments
562 forAttributedString:(NSMutableAttributedString *)attributedString
563 keywordRange:(NSRange)keywordRange
564 uniqueID:(unsigned long long)uniqueID
566 NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
568 NSMutableArray *applescriptRunnerArguments = [NSMutableArray arrayWithObject:[infoDict objectForKey:@"Path"]];
571 static NSString *applescriptRunnerPath = nil;
572 if (!applescriptRunnerPath) {
573 //Find and cache the path to the ApplescriptRunner application
574 applescriptRunnerPath = [[[NSBundle mainBundle] pathForResource:@"AdiumApplescriptRunner"
576 inDirectory:nil] retain];
579 //We run the substitute function
580 [applescriptRunnerArguments addObject:@"substitute"];
582 if (arguments && [arguments count]) {
583 [applescriptRunnerArguments addObjectsFromArray:arguments];
586 scriptTask = [[NSTask alloc] init];
588 //Set up a time out after which the scriptTask will terminate itself
589 [scriptTask performSelector:@selector(terminate)
591 afterDelay:SCRIPT_TIMEOUT];
593 [scriptTask setLaunchPath:applescriptRunnerPath];
594 [scriptTask setArguments:applescriptRunnerArguments];
596 outputPipe = [[NSPipe alloc] init];
599 [scriptTask setStandardOutput:outputPipe];
600 [outputPipe release];
602 NSLog(@"could not create pipe: %s", strerror(errno));
605 [scriptTask setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
606 attributedString, @"Mutable Attributed String",
607 NSStringFromRange(keywordRange), @"Range",
608 [NSNumber numberWithUnsignedLongLong:uniqueID], @"uniqueID",
611 [[NSNotificationCenter defaultCenter] addObserver:self
612 selector:@selector(scriptDidFinish:)
613 name:NSTaskDidTerminateNotification
618 //If the task fails to launch, send the termination notification
619 NSNotification *notification = [NSNotification notificationWithName:NSTaskDidTerminateNotification
621 NSLog(@"Couldn't launch (%@)",localException);
622 [self scriptDidFinish:notification];
625 [autoreleasePool release];
629 * @brief A script finished executing
631 * @param aNotification The notification, whose object is the NSTask which terminated
633 - (void)scriptDidFinish:(NSNotification *)aNotification
635 NSAutoreleasePool *autoreleasePool = [[NSAutoreleasePool alloc] init];
636 NSTask *scriptTask = [aNotification object];
637 NSDictionary *environment = [scriptTask environment];
638 id standardOutput = [scriptTask standardOutput];
639 NSMutableAttributedString *attributedString = [environment objectForKey:@"Mutable Attributed String"];
640 NSRange keywordRange = NSRangeFromString([environment objectForKey:@"Range"]);
641 NSNumber *uniqueID = [environment objectForKey:@"uniqueID"];
642 NSFileHandle *output = nil;
643 NSString *scriptResult = nil;
645 if ([standardOutput isKindOfClass:[NSPipe class]] &&
646 (output = [(NSPipe *)standardOutput fileHandleForReading])) {
648 scriptResult = [[NSString alloc] initWithData:[output readDataToEndOfFile]
649 encoding:NSUTF8StringEncoding];
654 //The NSFileHandle will be closed automatically when the NSPipe is deallocated... but let's do it immediately
658 //If the script fails, eat the keyword
659 if (!scriptResult) scriptResult = [@"" retain];
661 //Replace the substring with script result
662 if (NSMaxRange(keywordRange) <= [attributedString length]) {
663 if (([scriptResult hasPrefix:@"<HTML>"])) {
664 //Obtain the attributed string version of the HTML, passing our current attributes as the default ones
665 NSAttributedString *attributedScriptResult = [AIHTMLDecoder decodeHTML:scriptResult
666 withDefaultAttributes:[attributedString attributesAtIndex:keywordRange.location
667 effectiveRange:nil]];
668 [attributedString replaceCharactersInRange:keywordRange
669 withAttributedString:attributedScriptResult];
672 [attributedString replaceCharactersInRange:keywordRange
673 withString:scriptResult];
677 //Remove the delayed perform request for termination (which would have been used if the script timed out)
678 [NSObject cancelPreviousPerformRequestsWithTarget:scriptTask
679 selector:@selector(terminate)
682 /* Remove the observer. NSTask objects appear to be reused internally once released, so this will be called multiple
683 * times for future NSTask objects, since there is already an observer for the object, according to -[NSTask isEqual:]
686 [[NSNotificationCenter defaultCenter] removeObserver:self
687 name:NSTaskDidTerminateNotification
691 [scriptResult release];
692 [scriptTask release];
693 [autoreleasePool release];
695 //Inform the content controller that we're done
696 [[adium contentController] delayedFilterDidFinish:attributedString uniqueID:[uniqueID unsignedLongLongValue]];
700 * @brief Determine the arguments for a script execution
702 * @param inString The string of potential arguments
703 * @param scriptDict The script being executed
705 * @result An NSArray of NSString instances
707 - (NSArray *)_argumentsFromString:(NSString *)inString forScript:(NSMutableDictionary *)scriptDict
709 NSArray *scriptArguments = [scriptDict objectForKey:@"Arguments"];
710 NSMutableArray *argArray = [NSMutableArray array];
711 NSArray *inStringComponents = [inString componentsSeparatedByString:@","];
714 unsigned count = (scriptArguments ? [scriptArguments count] : 0);
715 unsigned inStringComponentsCount = [inStringComponents count];
717 //Add each argument of inString to argArray so long as the number of arguments is less
718 //than the number of expected arguments for the script and the number of supplied arguments
719 while ((i < count) && (i < inStringComponentsCount)) {
720 [argArray addObject:[inStringComponents objectAtIndex:i]];
724 //If more components were passed than were actually requested, the last argument gets the
726 if (i < inStringComponentsCount) {
727 NSRange remainingRange;
729 //i was incremented to end the while loop if i > 0, so subtract 1 to reexamine the last object
730 remainingRange.location = ((i > 0) ? i-1 : 0);
731 remainingRange.length = (inStringComponentsCount - remainingRange.location);
733 if (remainingRange.location >= 0) {
734 NSString *lastArgument;
736 //Remove that last, incomplete argument if it was added
737 if ([argArray count]) [argArray removeLastObject];
739 //Create the last argument by joining all remaining comma-separated arguments with a comma
740 lastArgument = [[inStringComponents subarrayWithRange:remainingRange] componentsJoinedByString:@","];
742 [argArray addObject:lastArgument];
749 #pragma mark Toolbar item
751 * @brief Register our insert script toolbar item
753 - (void)registerToolbarItem
755 MVMenuButton *button;
757 //Unregister the existing toolbar item first
759 [[adium toolbarController] unregisterToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
760 [toolbarItem release]; toolbarItem = nil;
763 //Register our toolbar item
764 button = [[[MVMenuButton alloc] initWithFrame:NSMakeRect(0,0,32,32)] autorelease];
765 [button setImage:[NSImage imageNamed:@"scriptToolbar" forClass:[self class]]];
766 toolbarItem = [[AIToolbarUtilities toolbarItemWithIdentifier:SCRIPT_IDENTIFIER
767 label:AILocalizedString(@"Scripts",nil)
768 paletteLabel:TITLE_INSERT_SCRIPT
769 toolTip:AILocalizedString(@"Insert a script",nil)
771 settingSelector:@selector(setView:)
773 action:@selector(selectScript:)
775 [toolbarItem setMinSize:NSMakeSize(32,32)];
776 [toolbarItem setMaxSize:NSMakeSize(32,32)];
777 [button setToolbarItem:toolbarItem];
778 [[adium toolbarController] registerToolbarItem:toolbarItem forToolbarType:@"TextEntry"];
782 * @brief After the toolbar has added the item we can set up the submenus
784 - (void)toolbarWillAddItem:(NSNotification *)notification
786 NSToolbarItem *item = [[notification userInfo] objectForKey:@"item"];
788 if (!notification || ([[item itemIdentifier] isEqualToString:SCRIPT_IDENTIFIER])) {
789 NSMenu *menu = [[[scriptMenuItem submenu] copy] autorelease];
792 [[item view] setMenu:menu];
794 //Add menu to toolbar item (for text mode)
795 NSMenuItem *mItem = [[[NSMenuItem allocWithZone:[NSMenu menuZone]] init] autorelease];
796 [mItem setSubmenu:menu];
797 [mItem setTitle:[menu title]];
798 [item setMenuFormRepresentation:mItem];