I am new to the world of Cocoa programming, and I want to add Applescript support to my application. The examples at Apple's website seem out of date.
How do I add Applescript support to my Cocoa application?
If you want to send AppleScript from your application and need a sandboxed app, you need to create a temporary entitlement
You need to add those two keys in your info.plist
<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>MyAppName.sdef</string>
...of course you have to change "MyAppName" to your app's name
Create a .sdef file and add it to your project.
The further course now greatly depends on the needs of your application, there are:
Class Elements (create an object from AppleScript)
Command Elements (override NSScriptCommand and execute "verb-like" commands)
Enumeration Elements
Record-Type Elements
Value-Type Elements (KVC)
Cocoa Elements
-
Go here to find a detailed description and many details on their implementation: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ScriptableCocoaApplications/SApps_script_cmds/SAppsScriptCmds.html
I found working with Class and KVC Elements very complicated, as I just wanted to execute a single command, nothing fancy. So in order to help others, here's an example of how to create a new simple command with one argument. In this example it'll "lookup" one string like this:
tell application "MyAppName"
lookup "some string"
end tell
The .sdef file for this command looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="MyAppName">
<suite name="MyAppName Suite" code="MApN" description="MyAppName Scripts">
<command name="lookup" code="lkpstrng" description="Look up a string, searches for an entry">
<cocoa class="MyLookupCommand"/>
<direct-parameter description="The string to lookup">
<type type="text"/>
</direct-parameter>
</command>
</suite>
</dictionary>
Create a subclass of NSScriptCommand and name it MyLookupCommand
The MyLookupCommand.h
#import <Foundation/Foundation.h>
#interface MyLookupCommand : NSScriptCommand
#end
The MyLookupCommand.m
#import "MyLookupCommand.h"
#implementation MyLookupCommand
-(id)performDefaultImplementation {
// get the arguments
NSDictionary *args = [self evaluatedArguments];
NSString *stringToSearch = #"";
if(args.count) {
stringToSearch = [args valueForKey:#""]; // get the direct argument
} else {
// raise error
[self setScriptErrorNumber:-50];
[self setScriptErrorString:#"Parameter Error: A Parameter is expected for the verb 'lookup' (You have to specify _what_ you want to lookup!)."];
}
// Implement your code logic (in this example, I'm just posting an internal notification)
[[NSNotificationCenter defaultCenter] postNotificationName:#"AppShouldLookupStringNotification" object:stringToSearch];
return nil;
}
#end
That's basically it. The secret to this is to subclass NSScriptCommand and override performDefaultImplementation. I hope this helps someone to get it faster...
Modern versions of Cocoa can directly interpret the scripting definition (.sdef) property list, so all you need to do for basic AppleScript support is to create the sdef per the docs, add it to your "copy bundle resources" phase and declare AppleScript support in your Info.plist. To access objects other than NSApp, you define object specifiers, so each object knows its position in the scripting world's hierarchy. That gets you kvc manipulation of object properties, and the ability to use object methods as simple script commands.
A simple example to get you started,
place a script (named dialog) into the documents folder then you can run it from Xcode
NSArray *arrayPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *docDirectory = [arrayPaths objectAtIndex:0];
NSString *filePath = [docDirectory stringByAppendingString:#"/dialog.scpt"];
NSAppleScript *scriptObject = [[NSAppleScript alloc] initWithContentsOfURL:[NSURL fileURLWithPath:filePath] error:nil];
[scriptObject executeAndReturnError:nil];
The nice thing about keeping the script external is the ability to edit it outside of Xcode.
I would recommend adding the error checking if you did start editing as the applescript may not compile
maybe check with
if(scriptObject.isCompiled){
Related
I have an app that supports various AppleScript commands and I'm running into a memory leak issue when passing a list (array) object from a script into the app. There isn't a ton a 100% clear and helpful documentation on adding AppleScript support to your cocoa app, so I'm hoping this is just a simple oversight/error on my part and someone can help me find it.
My end goal is to have another Cocoa app compile and run the script via OSAKit but I've also tested using macOS's Script Editor.app to pass in a list and my main Cocoa app still leaks when doing it that way too:
-- This AppleScript command causes leaks
tell application "MyApp"
update database with things {"thing1", "thing2", "thing3"}
end tell
In my other Cocoa app (not the main Cocoa app), I run the above via OSAKit but it leaks there as well:
#import <OSAKit/OSAKit.h>
OSAScript *script= [[OSAScript alloc] initWithSource:[NSString stringWithFormat:#"tell application \"MyApp\"\nupdate database with things %#", listOfThings]];
NSDictionary * errorDict = nil;
NSAppleEventDescriptor *returnDescriptor = [script executeAndReturnError: &errorDict];
On the receiving end, my main Cocoa app has an NSScriptCommand class set up and can receive and respond to AppleScript commands without any other known issues. Here's what it looks like just for the list part:
MyScriptHandler.h
#import "AppDelegate.h"
#import <Cocoa/Cocoa.h>
#import <Foundation/Foundation.h>
#interface MyScriptHandler : NSScriptCommand
- (NSAppleEventDescriptor *) performDefaultImplementation;
#end
**MyScriptHandler.m**
#import "MyScriptHandler.h"
#implementation MyScriptHandler
- (NSAppleEventDescriptor *) performDefaultImplementation
{
NSString* cmdName = [[self commandDescription] commandName];
if ([cmdName isEqualToString: #"update database"])
{
// GET THE LIST AND PASS IT OFF TO APP DELEGATE FOR PROCESSING
NSMutableArray *things = [[self evaluatedArguments] valueForKey:#"things"];
[(AppDelegate *)[[NSApplication sharedApplication] delegate] updateDatabaseFromAppleScript:options];
}
return [NSAppleEventDescriptor descriptorWithBoolean:YES];
}
Not sure this is helpful, but my main Cocoa app's AppleScript dictionary (.sdef) looks like this for the command:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="MyApp Terminology">
<suite name="MyApp Suite" code="MyAp" description="Commands for MyApp">
<command name="update database" code="MyApUpDB" description="Update the database">
<parameter name="with things" code="optn" type="list of text" optional="yes" description="">
<cocoa key="things"/>
</parameter>
<cocoa class="MyScriptHandler"/>
</command>
</suite>
</dictionary>
Thank you in advance for any help!
I believe I have fixed the memory leak issue by making the following changes...
In my .sdef file, I changed the parameter type for the command to be type="record" instead of type="list of text". This means I have to pass some useless/duplicate information (the key name) into my main app, but that's acceptable since it's now not leaking and doesn't cost any extra CPU as far as I can tell.
<command name="update database" code="MyApUpDB" description="Update the database">
<parameter name="with things" code="optn" type="record" optional="yes" description="">
<cocoa key="things"/>
</parameter>
<cocoa class="MyScriptHandler"/>
</command>
Then, I updated the AppleScript to pass a record instead of a list:
-- This AppleScript command does NOT cause leaks
tell application "MyApp"
update database with things {thing1:"thing1", thing2:"thing2", thing3:"thing3"}
end tell
I am trying to create an applescript command in my document class. I know I am doing something wrong, but I am not sure how to do this.
From what I understand (which may be incorrect), when I create a new command
I need to specify a new class for that command. But from that new class, lets call it
ScriptResetCommand, how do I access the document object from the performDefaultImplementation method? The applescript call is something like
tell document 1 of application "DocScript" to simple reset
Here is my current code:
ScriptResetCommand.m file:
#implementation ScriptResetCommand
- (id)performDefaultImplementation {
// Somehow I need to access the correct document class and
// perform my reset.
NSLog(#"ScriptResetCommand performDefaultImplementation");
return #"Reset Stuff";
}
ScriptResetCommand.h file:
#interface ScriptResetCommand : NSScriptCommand
- (id)performDefaultImplementation;
.sdef file:
<command name="simple reset" code="jDsgSrst" description="run a simple reset">
<cocoa class="ScriptResetCommand"/>
<result type="text" description="returns the result"/>
</command>
So with this code, I can successfully call the performDefaultImplementation method in the ScriptResetCommand class, but how do I then access the Document object which has the desired reset command in it?
Thanks in advance.
You should be able to access the document object using the method -[NSScriptCommand evaluatedReceivers]. Try something like this:
- (id)performDefaultImplementation {
NSDocument *document = [self.evaluatedReceivers firstObject];
if (![document isKindOfClass:[NSDocument class]]) {
// I'm just guessing how this error should be handled; untested (for example, you'll want to make sure your app returns an error if you run "reset stuff" without specifying a document)
[self setScriptErrorExpectedTypeDescriptor:[NSAppleEventDescriptor descriptorWithTypeCode:cDocument]];
return nil;
}
NSLog(#"Got document: %#", document);
return #"Reset Stuff";
}
I'm not sure what
tell document 1 of application "DocScript" to simple reset
would do, but to process it, break it down into a verb (simple reset) and a target (document 1 of application "DocScript"). When the command is executed, a reference to the target will be provided in [command directParameter], from which you can get an objectSpecifier for your document.
Here's how I implemented text selection in my scriptable text editor, Ted. The verb (command) is "select" and the target is "paragraph 5 of document 1".
tell application "Ted"
select paragraph 5 of document 1
end tell
SDEF: The command is defined in the Text Suite objects:
<suite name="Text Suite" code="????">
...
<class name="paragraph" code="cpar" inherits="item" >
...
<responds-to command="select">
<cocoa method="handleSelectCommand:"/>
</responds-to>
</class>
...
</suite>
The "select" command actually is included for EVERY class in the text suite that can be selected: e.g. paragraph, line, word, character.
Implementation:
Since all the various scriptable text objects are represented as NSTextStorage classes, I just use a category on NSTextStorage to implement the command.
#implementation NSTextStorage (Scriptability)
- (void) handleSelectCommand:(NSScriptCommand *)command
{
NSScriptObjectSpecifier *directParameter =[command directParameter];
NSScriptObjectSpecifier *container = [directParameter containerSpecifier];
TedDocument *document = [self extractDocument:container];
NSString *contents = document.textView.string;
NSRange selRange;
[self getRangeForSelectCommand:directParameter contents:contents range:&selRange];
[document.textView setSelectedRange:selRange];
}
#end
A reference to the document is provided by the directParameter, but it is part of a containment hierarchy, in this case 'paragraph 5 of document 1 of application "Ted"', so we need a method to walk that hierarchy to extract the document from it. Here is that function:
// Navigate the containment hierarch using recursion, if necessary.
- (TedDocument *) extractDocument:(NSScriptObjectSpecifier *)container
{
if (container == nil)
{
NSLog(#"%#", #"ERROR: extractDocument received nil container reference");
return nil;
}
FourCharCode containerClass = [[container keyClassDescription] appleEventCode];
if (containerClass != 'docu')
{
return [self extractDocument:[container containerSpecifier]];
}
return [container objectsByEvaluatingSpecifier];
}
I'm using the AudioEncode plugin for PhoneGap (Cordova) on iOS. After updating a couple of lines for a newer version of Cordova, it appears to be correctly encoding wav files as m4a. In the Xcode console I see:
AVAssetExportSessionStatusCompleted
doing success callback
When I look at the file system on the phone, the wav file has indeed become a m4a. However, the success callback (where I upload the file to a server) is never run. This is the relevant code in the plugin:
-(void) doSuccessCallback:(NSString*)path {
NSLog(#"doing success callback");
NSString* jsCallback = [NSString stringWithFormat:#"%#(\"%#\");", self.successCallback, path];
[self writeJavascript: jsCallback];
[self.successCallback release];
}
My code in the app goes like this:
function encodeSuccess (path) {
console.log('Audio encoded to M4A! Preparing to upload...')
// file transfer code...
}
console.log('Preparing to encode audio file...')
window.plugins.AudioEncode.encodeAudio(entry.fullPath, encodeSuccess, fail)
I'm assuming the doSuccessCallback function in the plugin needs to be updated, but I don't have experience with Objective C or PhoneGap plugins, so I'm stuck at this point.
Any ideas?
UPDATE
In the Objective C function posted above, I tried logging self.successCallback, and it logged as <null>. Then I went up to the top of the main encodeAudio function, and the argument which is assigned to self.successCallback ([arguments objectAtIndex:1]) also logs as <null>. So, it seems to me that the callbacks are not being passed into the main function successfully.
This is the AudioEncode.h file, maybe someone can spot the problem here:
#interface AudioEncode : CDVPlugin {
NSString* successCallback;
NSString* failCallback;
}
#property (nonatomic, retain) NSString* successCallback;
#property (nonatomic, retain) NSString* failCallback;
- (void)encodeAudio:(NSArray*)arguments withDict:(NSDictionary*)options;
Ok, I figured this out by reading the basic examples in the Cordova Plugin Development Guide closely. The problem was with the ordering of parameters for cordova.exec(), which must have changed recently.
I plan to submit a pull request on GitHub with a working version of the plugin, but for now, here's the basic solution.
Before asking this question, I had updated the imports in AudioEncode.h from #import <PhoneGap/PGPlugin.h> to:
#import <Cordova/CDVPlugin.h>
#import <Cordova/CDVPluginResult.h>
Any reference to PGPlugin should also be updated to CDVPlugin, and PhoneGap should become cordova.
Here's the crux of the problem: in AudioEncode.js, cordova.exec() (where the original plugin calls PhoneGap.exec()) needs to be called like this:
AudioEncode.prototype.encodeAudio = function(audioPath, successCallback, failCallback) {
cordova.exec(successCallback, failCallback, "AudioEncode", "encodeAudio", [audioPath]);
};
If you don't order the parameters like this, the callbacks won't be passed in (although audioPath was...). Look at the docs for more details, but the parameters have to be the two callbacks first, the module name, the module action, and finally an array of extra parameters.
Then, you'll need to read in the parameters in the main encodeAudio function like this:
self.callback = [[arguments objectAtIndex:0] retain];
NSString* audioPath = [arguments objectAtIndex:1];
Note that there is only one callback object now, which contains references to the success and fail callbacks. This means that whenever the plugin sets up variables for successCallback and failCallback, you now only need callback (e.g. #synthesize callback). This is also declared in the AudioEncode.h file with #interface and #property.
Now, when actually firing the callbacks & returning data (in the doSuccessCallback and doFailCallback functions), you need to use CDVPluginResult, like this:
CDVPluginResult* pluginResult = nil;
NSString* javaScript = nil;
pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:path];
javaScript = [pluginResult toSuccessCallbackString:self.callback];
[self writeJavascript: javaScript];
[self.callback release];
Until I get the updated module up on GitHub, this should help anyone to get the plugin working.
I processed drag operation from browser view to custom view.It work well in snow lepoard,but not in Mountain Lion with sandbox.
in browser view:
NSMutableArray* urls = [[[NSMutableArray alloc] init] autorelease];
..............put some NSUrl to urls array....................
[pasteboard writeObjects:[NSArray arrayWithArray:urls]];
in my receive custom view:
NSArray* pasteboardItems = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSString class]] options:nil];
NSArray* pasteboardItems2 = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSURL class]] options:nil];
NSArray* pasteboardItems3 = [pasteboard readObjectsForClasses:[NSArray arrayWithObject:[NSImage class]] options:nil];
NSLog(#"%#",pasteboardItems);
NSLog(#"%#",pasteboardItems2);
NSLog(#"%#",pasteboardItems3);
my log is:
2012-08-09 18:33:43.886 iCollage[6885:303] __CFPasteboardIssueSandboxExtensionForPath: error for [/Users/xxxx/Library/Containers/xxxxxxxxxxxx/Data/Downloads/1343902069.jpg]
2012-08-09 18:33:44.546 iCollage[6885:303] ( "file://localhost/Users/xxx/Library/Containers/xxxxxxxx/Data/Downloads/1343902069.jpg")
2012-08-09 18:33:44.547 iCollage[6885:303] ( "file://localhost/Users/xxxxx/Library/Containers/xxxxxx/Data/Downloads/1343902069.jpg")
2012-08-09 18:33:44.547 iCollage[6885:303] ()
my question is:
1.how to fix this error __CFPasteboardIssueSandboxExtensionForPath;I refer the docs and found nothing about that.I am ensuer that i have the permission to access the file!google says, may be "startAccessingSecurityScopedResource" will help me, then i try and failed
2.why pasteboardItems2 have value?i write to pasteboard only url but not string.It disgusted me that I can get the url both from NSString type and NSUrl type! (I try drag a file from iFinder, the url will only exist in pasteboardItems but not pasteboardItems2).Anybody know why? I think the first problem will auto fixed when some one help me fix this problem.
I believe Apple answer question 1:
Important: Although you can support dragging file paths, in general,
you should avoid doing so unless you are certain that the destination
app will never be run in an app sandbox. If you use an NSString, OS X
has no way to know whether that string should be interpreted as a
path; thus, OS X does not expand the destination app’s sandbox to
allow access to the file or directory at that location. Instead, use
an NSURL, a bookmark, or a filename pasteboard type.
WRT to question 2, it looks like you have pasted URLs so reading NSURL objects would seem to be correct. However I think you should implement the dragging using the following code (also from the link above):
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
NSPasteboard *pboard = [sender draggingPasteboard];
if ( [[pboard types] containsObject:NSFilenamesPboardType] ) {
NSArray *files = [pboard propertyListForType:NSFilenamesPboardType];
int numberOfFiles = [files count];
// Perform operation using the list of files
}
return YES;
}
You need to generate security-scoped URL bookmark data on the sender side, and turn that data back into a URL on the receiver side. There's some other stuff you have to do after that when you want to actually access the URL; the documentation elaborates.
The receiving application, when running in a sandbox, will not be able to handle bare paths. This is a core part of being sandboxed; you are not allowed to use bare paths or their corresponding URLs to access files that aren't in your sandbox container and haven't been explicitly handed to you by the user.
Your pasteboardItems read object of NSString type, but you dragged a file(with jpg extension), you should register for NSString type in your init method:
[self registerForDraggedTypes:[NSArray arrayWithObject:NSPasteboardTypeString]];
You need to have Document Types defined in your application so that the sandboxing mechanism knows your application should be opening files with those extensions. You can do this by clicking the project on the left in Xcode, and in the Info tab, under Document Types add a new document type for each extension.
You just need to fill in the name and extensions field.
Also if you want to persist your permission to access the files dragged onto your application, you can use this class to wrap up all that logic. https://github.com/leighmcculloch/AppSandboxFileAccess
I tried to use GSTwitPicEngine class https://github.com/Gurpartap/GSTwitPicEngine for sharing image on twitter
but its giving error
As we have to set [twitpicEngine setAccessToken:token];
if I am right, I am using _accessToken of class SA_OAuthTwitterEngine as token
_accessToken was private in class SA_OAuthTwitterEngine, I set it to public and also
#property (nonatomic, readonly) OAToken *accessToken;
#synthesize accessToken = _accessToken;
and then in action
twitPicEngine = [GSTwitPicEngine twitpicEngineWithDelegate:self];
[twitPicEngine setAccessToken:twitterObj.accessToken];
[twitPicEngine uploadPicture:[shareDict objectForKey:#"image"] withMessage:[shareDict objectForKey:#"image_msg"]];
But Application getting crash while setAccessToken with log
-[OAToken parameters]: unrecognized selector sent to instance 0x6327e30
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[OAToken parameters]: unrecognized selector sent to instance 0x6327e30'
Please help if you able to find any thing wrong
Amit Battan
Unfortunately, GSTwitPicEngine is not using the same version of oauth-consumer than the fabulous Twitter+Oauth library (SAOAuthTwitterEngine). I assume you are using that library for posting messages to twitter.
The crash is because OAToken from Twitter+Oauth doesn´t implement the "parameters" method.
Today I spent the entire morning tweaking the several libraries to avoid crashes.
Here you can download a sample project I created for posting a twitpic photo to twitter with a message.
TestTwitpic
The project has all the latest versions of all libraries from github.
Instructions for making my TestTwitpic project work:
In TestTwitpic-Prefix.pch set the variables for:
#define kTwitterOAuthConsumerKey #""
#define kTwitterOAuthConsumerSecret #""
#define kTwitPicAPIKey #""
In RootViewController you can change these lines to your needs. To change the photo:
//change [UIImage imageNamed:#"image.jpg"] for whatever UIImage you want to upload
//change #"my photo" for whatever title you want for your photo in twitpic website
[twitpicEngine uploadPicture:[UIImage imageNamed:#"image.jpg"] withMessage:#"my photo"];
and this one to change message sent to twitter:
//I post to twitter the title of the photo and the twitpic url but you can post whatever you want
[engine sendUpdate:[NSString stringWithFormat:#"%# %#", [[[response objectForKey:#"request"] userInfo] objectForKey:#"message"], [[response objectForKey:#"parsedResponse"] objectForKey:#"url"]]];
If you want to create your own project based on this sample. Do the following:
Import into your project (drag and drop) the Twitpic folder with all the libraries that are inside.
Add these frameworks to your project:
CoreGraphics
libz.1.2.3
libxml2
MobileCoreServices
SystemConfiguration
CFNetwork
Security
In Build Settings add "$SDKROOT/usr/include/libxml2" to the "Header Search Paths" (mark it as recursive)
In Build Settings add -lxml2 to "Other linker flags"
If you want to know what I did for fixing the libraries, I will tell you more or less what I remember I did:
Import Twitter+Oauth, GSTwitPicEngine, OARequestHeader, TouchJSON, and ASIHTTPRequest libraries.
Set GSTwitPicEngine to use TouchJSON instead of YAJL.
Added the category NSString+URLEncoding from oauth-consumer github project as OARequestHeader was missing it.
Modified all occurrences of NSDictionary* foo = [toke parameters] inside OARequestHeader with the line:
NSDictionary *foo = [NSDictionary dictionaryWithObject:[token key] forKey:#"oauth_token"];
Created a method getAccessToken in SAOAuthTwitterEngine to return the private _accessToken variable.
In requestFinished: inside GSTwitPicEngine
change the line:
response = [[CJSONDeserializer deserializer] deserialize:responseString error:&error];
with the line:
response = [[CJSONDeserializer deserializer] deserialize:[responseString dataUsingEncoding:NSUTF8StringEncoding] error:&error];
as GSTwitPicEngine wrongly assumed deserialize:error: method accepts an NSString instead of an NSData.
You can set Access token & Access Secret for twitpic after getting from twitter engine i.e
SA_OAuthTwitterEngine.m
(void) setAccessToken: (OAServiceTicket *) ticket withData: (NSData *) data
Create object of "GSTwitPicEngine.m" class in "SA_OAuthTwitterEngine.m" & pass Access token & Access Secret to "GSTwitPicEngine.m" and set delegate methods of "GSTwitPicEngine.m" in "SA_OAuthTwitterEngine.m"
And one more change...
must cross check URLEncoding while genrating OAuth Header i.e
[requestHeader generateRequestHeaders]
this will be according to "OAuthConsumer" classes in Twitter Library