如何在可编写脚本的应用程序中将任意AppleScript记录传递给Cocoa?

时间:2010-01-23 18:35:40

标签: macos cocoa applescript cocoa-scripting

我有一个Cocoa应用程序,其中包含.sdef XML文件中描述的AppleScript字典。 sdef中定义的所有AppleScript类,命令等都是工作属性。

我的“提交表单”命令除外。 “提交表单”命令是我唯一的命令,它尝试将一个参数传递给AppleScript,该参数是从AppleScript到Cocoa的任意哈希表。我认为这应该通过传递AppleScript record来完成,它将自动转换为Cocoa端的NSDictionary

tell application "Fluidium"
    tell selected tab of browser window 1
        submit form with name "foo" with values {bar:"baz"}
    end tell
end tell

“with values”参数是record - > NSDictionary参数我遇到了麻烦。请注意,记录/字典的键不能事先知道/定义。它们是任意的。

以下是我的sdef XML中此命令的定义:

<command name="submit form" code="FuSSSbmt" description="...">
    <direct-parameter type="specifier" optional="yes" description="..."/>
    <parameter type="text" name="with name" code="Name" optional="yes" description="...">
        <cocoa key="name"/>
    </parameter>
    <parameter type="record" name="with values" code="Vals" optional="yes" description="...">
        <cocoa key="values"/>
    </parameter>
</command>

我有一个“tab”对象,它响应sdef中的这个命令:

<class name="tab" code="fTab" description="A browser tab.">
    ...
    <responds-to command="submit form">
        <cocoa method="handleSubmitFormCommand:"/>
    </responds-to>

和可可:

- (id)handleSubmitFormCommand:(NSScriptCommand *)cmd {
    ...
}

“tab”对象正确响应我定义的所有其他AppleScript命令。如果我不发送可选的“with values”参数,“tab”对象也会响应“submit form”命令。所以我知道我已经正确设置了基础知识。唯一的问题似乎是任意record - &gt; NSDictionary param。

当我在AppleScript Editor.app执行上面的AppleScript时,我在Cocoa端出现了这个错误:

+[NSDictionary scriptingRecordWithDescriptor:]: unrecognized selector sent to class 0x7fff707c6048

和AppleScript方面的这个:

error "Fluidium got an error: selected tab of browser window 1 doesn’t understand the submit form message." number -1708 from selected tab of browser window 1

谁能告诉我我缺少什么?作为参考,整个应用程序在GitHub上是开源的:

http://github.com/itod/fluidium

4 个答案:

答案 0 :(得分:6)

Cocoa会将NSDictionary个对象无缝转换为AppleScript(AS)记录,反之亦然,你只需告诉它如何做到这一点。

首先,您需要在脚本定义(record-type)文件中定义.sdef,例如

<record-type  name="http response" code="HTRE">
    <property name="success" code="HTSU" type="boolean"
        description="Was the HTTP call successful?"
    />

    <property name="method" code="HTME" type="text"
        description="Request method (GET|POST|...)."
    />

    <property name="code" code="HTRC" type="integer"
        description="HTTP response code (200|404|...)."
    >
        <cocoa key="replyCode"/>
    </property>

    <property name="body" code="HTBO" type="text"
        description="The body of the HTTP response."
    />
</record-type>

name是此值在AS记录中的名称。如果名称等于NSDictionary键,则上述示例中不需要<cocoa>successmethod,如果不是,则可以使用body标记告诉Cocoa读取此值的正确密钥(在上面的示例中,<cocoa>是AS记录中的名称,但在code中密钥将为{ {1}}而是;我只是为了演示目的而做了这个。)

告诉Cocoa这个字段应该具有哪种AS类型非常重要,否则Cocoa不知道如何将该值转换为AS值。默认情况下,所有值都是可选的,但如果它们存在,则它们必须具有预期的类型。这是一个关于最常见的Foundation类型如何与AS类型匹配的小表(不完整):

NSDictionary

请参阅Apple的#{3>}&#34; Cocoa脚本编写指南&#34;

当然,值本身可以是另一个嵌套记录,只需为其定义replyCode,使用 AS Type | Foundation Type -------------+----------------- boolean | NSNumber date | NSDate file | NSURL integer | NSNumber number | NSNumber real | NSNumber text | NSString 规范和record-type中的record-type名称那么该值必须是匹配的字典。

好吧,让我们尝试一下完整的样本。让我们在property文件中定义一个简单的HTTP get命令:

NSDictionary

现在我们需要在Obj-C中实现该命令,这很简单:

.sdef

当然,在内部故障的情况下返回<command name="http get" code="httpGET_"> <cocoa class="HTTPFetcher"/> <direct-parameter type="text" description="URL to fetch." /> <result type="http response"/> </command> 是错误的错误处理。我们可能会返回错误。好吧,我们可以在这里使用AS的特殊错误处理方法(例如设置我们从#import <Foundation/Foundation.h> // The code below assumes you are using ARC (Automatic Reference Counting). // It will leak memory if you don't! // We just subclass NSScriptCommand @interface HTTPFetcher : NSScriptCommand @end @implementation HTTPFetcher static NSString *const SuccessKey = @"success", *const MethodKey = @"method", *const ReplyCodeKey = @"replyCode", *const BodyKey = @"body" ; // This is the only method we must override - (id)performDefaultImplementation { // We expect a string parameter id directParameter = [self directParameter]; if (![directParameter isKindOfClass:[NSString class]]) return nil; // Valid URL? NSString * urlString = directParameter; NSURL * url = [NSURL URLWithString:urlString]; if (!url) return @{ SuccessKey : @(false) }; // We must run synchronously, even if that blocks main thread dispatch_semaphore_t sem = dispatch_semaphore_create(0); if (!sem) return nil; // Setup the simplest HTTP get request possible. NSURLRequest * req = [NSURLRequest requestWithURL:url]; if (!req) return nil; // This is where the final script result is stored. __block NSDictionary * result = nil; // Setup a data task NSURLSession * ses = [NSURLSession sharedSession]; NSURLSessionDataTask * tsk = [ses dataTaskWithRequest:req completionHandler:^( NSData *_Nullable data, NSURLResponse *_Nullable response, NSError *_Nullable error ) { if (error) { result = @{ SuccessKey : @(false) }; } else { NSHTTPURLResponse * urlResp = ( [response isKindOfClass:[NSHTTPURLResponse class]] ? (NSHTTPURLResponse *)response : nil ); // Of course that is bad code! Instead of always assuming UTF8 // encoding, we should look at the HTTP headers and see if // there is a charset enconding given. If we downloaded a // webpage it may also be found as a meta tag in the header // section of the HTML. If that all fails, we should at // least try to guess the correct encoding. NSString * body = ( data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding ] : nil ); NSMutableDictionary * mresult = [ @{ SuccessKey: @(true), MethodKey: req.HTTPMethod } mutableCopy ]; if (urlResp) { mresult[ReplyCodeKey] = @(urlResp.statusCode); } if (body) { mresult[BodyKey] = body; } result = mresult; } // Unblock the main thread dispatch_semaphore_signal(sem); } ]; if (!tsk) return nil; // Start the task and wait until it has finished [tsk resume]; dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); return result; } 继承的某些属性),但它毕竟只是一个样本。

最后我们需要一些AS代码来测试它:

nil

结果:

NSScriptCommand

正如所料,现在成功了:

tell application "MyCoolApp"
    set httpResp to http get "http://badserver.invalid"
end tell

结果:

{success:false}

也如预期的那样。

但是等等,你反过来想要它,对吧?好的,我们也试试吧。我们只是重用我们的类型并发出另一个命令:

tell application "MyCoolApp"
    set httpResp to http get "http://stackoverflow.com"
end tell

我们也实施了这个命令:

{success:true, body:"<!DOCTYPE html>...",  method:"GET", code:200}

我们测试它:

<command name="print http response" code="httpPRRE">
    <cocoa class="HTTPResponsePrinter"/>
    <direct-parameter type="http response"
        description="HTTP response to print"
    />
</command>

她就是我们的应用程序登录到控制台:

#import <Foundation/Foundation.h>

@interface HTTPResponsePrinter : NSScriptCommand
@end


@implementation HTTPResponsePrinter

- (id)performDefaultImplementation {
    // We expect a dictionary parameter
    id directParameter = [self directParameter];
    if (![directParameter isKindOfClass:[NSDictionary class]]) return nil;

    NSDictionary * dict = directParameter;
    NSLog(@"Dictionary is %@", dict);
    return nil;
}

@end

所以,当然,它有两种方式。

好吧,你现在可能会抱怨这不是任意,毕竟你需要定义哪些键(可能)存在以及它们存在时它们将具有什么类型。你是对的。但是,通常数据并不是随意的,我的意思是,在所有代码必须能够理解它之后,它必须至少遵循某种规则和模式。

如果你真的不知道期望什么数据,例如就像转储工具只是在两种定义良好的数据格式之间转换而不了解数据本身一样,为什么要将它作为记录传递?为什么不直接将该记录转换为易于解析的字符串值(例如Property List,JSON,XML,CSV),然后将Cocoa作为字符串传递给最终将其转换回对象?这是一种简单而又非常强大的方法。在Cocoa中解析属性列表或JSON可能使用四行代码完成。好吧,它可能不是最快的方法,但只要一句话提到AppleScript和高性能,就已经犯了一个根本性的错误; AppleScript肯定可能很多,但是快速#34;没有你能想到的属性。

答案 1 :(得分:3)

正确 - NSDictionaries和AppleScript记录似乎会混合,但它们实际上并不是(NSDictionaries使用对象键 - 比如字符串)AppleScript记录使用四个字母的字符代码(感谢他们的AppleEvent / Classic Mac OS遗产)。

请参阅this thread on Apple's AppleScript Implementer's mailing list

因此,在您的情况下,您实际需要做的是解压缩您拥有的AppleScript记录并将其转换为您的NSDictionary。您可以自己编写代码,但这很复杂,深入了解AE管理器。

然而,这项工作实际上是在appscript/appscript-objc的一些底层代码中为您完成的(appscript是一个Python和Ruby以及Objective-C的库,它允许您与AppleScriptable应用程序进行通信,而无需实际使用AppleScript。 appscript-objc可以在你使用Cocoa Scripting的地方使用,但是该技术的限制较少。)

代码为available on sourceforge。我几周前向作者提交了一个补丁,所以你可以构建appcript-objc的底层基础,这就是你需要的全部内容:你需要做的就是打包并解压缩Applescript / AppleEvent记录。

对于其他googlers,还有另一种方法,即不使用appscript:ToxicAppleEvents。那里有一种方法可以将字典翻译成Apple事件记录。

答案 2 :(得分:2)

如果您知道要包装的字典中的字段以及要映射到AppleScript或从AppleScript映射的键的类型是可预测的,那么最佳解决方案似乎是to use a record definition,如另一个答案中所述,这也有助于链接Apple的文档,至少我个人完全错过了脚本指南。

如果上述要求不符合您的需求,则另一种解决方案是将+scriptingRecordWithDescriptor:作为NSDictionary的类别来实现。我在Fluidium项目中找到了这个问题的解决方案。这是来自NSDictionary+FUScripting.m的粘贴:

@implementation NSDictionary (FUScripting)

+ (id)scriptingRecordWithDescriptor:(NSAppleEventDescriptor *)inDesc {
    //NSLog(@"inDesc: %@", inDesc);

    NSMutableDictionary *d = [NSMutableDictionary dictionary];

    NSAppleEventDescriptor *withValuesParam = [inDesc descriptorForKeyword:'usrf']; // 'usrf' keyASUserRecordFields
    //NSLog(@"withValuesParam: %@", withValuesParam);

    NSString *name = nil;
    NSString *value = nil;

    // this is 1-indexed!
    NSInteger i = 1;
    NSInteger count = [withValuesParam numberOfItems];
    for ( ; i <= count; i++) {
        NSAppleEventDescriptor *desc = [withValuesParam descriptorAtIndex:i];
        //NSLog(@"descriptorAtIndex: %@", desc);

        NSString *s = [desc stringValue];
        if (name) {
            value = s;
            [d setObject:value forKey:name];
            name = nil;
            value = nil;
        } else {
            name = s;
        }
    }

    return [d copy];
}

@end

我可以确认使用+ scriptingRecordWithDecriptor:使用同等类型的自定义命令为我工作。

答案 3 :(得分:0)

11.9.2016,Mac OS 10.11.6 问题是:如何在可可世界中将AppleScript记录转换为NSDictionary?

AppleScript记录使用AppleScript属性作为键,数字或字符串作为值。

NSDictionary使用相应的可可键作为键(以NSString对象的形式)和AppleScript记录中四种最基本类型的NSNumber或NSString值:string,integer,double和boolean。

+(id)scriptingRecordWithDescriptor :( NSAppleEventDescriptor *)inDesc的建议解决方案在我的情况下不起作用。

我的实现中的基本更改是AppleScript环境中的每个类都定义了自己的属性和AppleScript代码。要确定的关键对象是NSScriptClassDescription,它包含AppleScript代码和Cocoa密钥之间的关系。 另一个复杂因素是,在方法中用作参数的NSAppleEventDescriptor表示传入的AppleScript记录(或者我的情况下的记录列表)。这个NSAppleEventDescriptor可以有不同的形式。

AppleScript记录中的一个条目是特殊的:{class:“script class name”}。代码测试它的存在。

代码中唯一需要替换的是为“苹果脚本套件的名称”引入应用程序AppleScript套件的名称。 该方法在NSDictionary

上实现为类别
#import "NSDictionary+AppleScript.h"

@implementation NSDictionary (AppleScript)

// returns a Dictionary from a apple script record
+ (NSArray <NSDictionary *> * )scriptingRecordWithDescriptor:(NSAppleEventDescriptor *)anEventDescriptor {
    NSScriptSuiteRegistry * theRegistry = [NSScriptSuiteRegistry sharedScriptSuiteRegistry] ;

    DescType theScriptClassDescriptor = [anEventDescriptor descriptorType] ;

    DescType printDescriptorType = NSSwapInt(theScriptClassDescriptor) ;
    NSString * theEventDescriptorType = [[NSString alloc] initWithBytes:&printDescriptorType length:sizeof(DescType) encoding:NSUTF8StringEncoding] ;
    //NSLog(@"Event descriptor type: %@", theEventDescriptorType) ; // "list" if a list, "reco" if a simple record , class identifier if a class

    // Forming a list of AppleEventDescriptors
    NSInteger i ;
    NSAppleEventDescriptor * aDescriptor ;
    NSMutableArray <NSAppleEventDescriptor*> * listOfEventDescriptors = [NSMutableArray array] ;
    if ([theEventDescriptorType isEqualToString:@"list"]) {
        NSInteger numberOfEvents = [anEventDescriptor numberOfItems] ;
        for (i = 1 ; i <= numberOfEvents ; i++) {
            aDescriptor = [anEventDescriptor descriptorAtIndex:i] ;
            if (aDescriptor) [listOfEventDescriptors addObject:aDescriptor] ;
        }
    }
    else [listOfEventDescriptors addObject:anEventDescriptor] ;

    // transforming every NSAppleEventDescriptor into an NSDictionary - key: cocoa key - object: NSString - the parameter value as string
    NSMutableArray <NSDictionary *> * theResult = [NSMutableArray arrayWithCapacity:listOfEventDescriptors.count] ;
    for (aDescriptor in listOfEventDescriptors) {
        theScriptClassDescriptor = [aDescriptor descriptorType] ;

        DescType printDescriptorType = NSSwapInt(theScriptClassDescriptor) ;
        NSString * theEventDescriptorType = [[NSString alloc] initWithBytes:&printDescriptorType length:sizeof(DescType) encoding:NSUTF8StringEncoding] ;
        //NSLog(@"Event descriptor type: %@", theEventDescriptorType) ;

        NSMutableDictionary * aRecord = [NSMutableDictionary dictionary] ;
        NSInteger numberOfAppleEventItems = [aDescriptor numberOfItems] ;
        //NSLog(@"Number of items: %li", numberOfAppleEventItems) ;

        NSScriptClassDescription * (^determineClassDescription)() = ^NSScriptClassDescription *() {
            NSScriptClassDescription * theResult ;

            NSDictionary * theClassDescriptions = [theRegistry classDescriptionsInSuite:@"Arcadiate Suite"] ;
            NSArray * allClassDescriptions = theClassDescriptions.allValues ;
            NSInteger numOfClasses = allClassDescriptions.count ;
            if (numOfClasses == 0) return theResult ;

            NSMutableData * thePropertiesCounter = [NSMutableData dataWithLength:(numOfClasses * sizeof(NSInteger))] ;
            NSInteger *propertiesCounter = [thePropertiesCounter mutableBytes] ;
            AEKeyword aKeyWord  ;
            NSInteger classCounter = 0 ;
            NSScriptClassDescription * aClassDescription ;
            NSInteger i ;
            NSString * aCocoaKey ;
            for (aClassDescription in allClassDescriptions) {
                for (i = 1 ; i <= numberOfAppleEventItems ; i++) {
                    aKeyWord = [aDescriptor keywordForDescriptorAtIndex:i] ;
                    aCocoaKey = [aClassDescription keyWithAppleEventCode:aKeyWord] ;
                    if (aCocoaKey.length > 0) propertiesCounter[classCounter] ++ ;
                }
                classCounter ++ ;
            }
            NSInteger maxClassIndex = NSNotFound ;
            for (i = 0 ; i < numOfClasses ; i++) {
                if (propertiesCounter[i] > 0) {
                    if (maxClassIndex != NSNotFound) {
                        if (propertiesCounter[i] > propertiesCounter[maxClassIndex]) maxClassIndex = i ;
                    }
                    else maxClassIndex = i ;
                }
            }
            //NSLog(@"Max class index: %li", maxClassIndex) ;
            //if (maxClassIndex != NSNotFound) NSLog(@"Number of matching properties: %li", propertiesCounter[maxClassIndex]) ;
            if (maxClassIndex != NSNotFound) theResult = allClassDescriptions[maxClassIndex] ;
            return theResult ;
        } ;

        NSScriptClassDescription * theRelevantScriptClass ;
        if ([theEventDescriptorType isEqualToString:@"reco"]) theRelevantScriptClass = determineClassDescription() ;
        else theRelevantScriptClass = [theRegistry classDescriptionWithAppleEventCode:theScriptClassDescriptor] ;
        if (theRelevantScriptClass) {
        //NSLog(@"Targeted Script Class: %@", theRelevantScriptClass) ;

            NSString * aCocoaKey, *stringValue ;
            NSInteger integerValue ;
            BOOL booleanValue ;
            id aValue ;
            stringValue = [theRelevantScriptClass implementationClassName] ;
            if (stringValue.length > 0) aRecord[@"className"] = aValue ;
            AEKeyword aKeyWord ;
            NSAppleEventDescriptor * parameterDescriptor ;
            NSString * printableParameterDescriptorType ;
            DescType parameterDescriptorType ;
            for (i = 1 ; i <= numberOfAppleEventItems ; i++) {
                aValue = nil ;
                aKeyWord = [aDescriptor keywordForDescriptorAtIndex:i] ;
                aCocoaKey = [theRelevantScriptClass keyWithAppleEventCode:aKeyWord] ;
                parameterDescriptor = [aDescriptor paramDescriptorForKeyword:aKeyWord] ;
                parameterDescriptorType = [parameterDescriptor descriptorType] ;
                printDescriptorType = NSSwapInt(parameterDescriptorType) ;
                printableParameterDescriptorType = [[NSString alloc] initWithBytes:&printDescriptorType length:sizeof(DescType) encoding:NSUTF8StringEncoding] ;
                //NSLog(@"Parameter type: %@", printableParameterDescriptorType) ;

                if ([printableParameterDescriptorType isEqualToString:@"doub"]) {
                    stringValue = [parameterDescriptor stringValue] ;
                    if (stringValue.length > 0) {
                        aValue = @([stringValue doubleValue]) ;
                    }
                }
                else if ([printableParameterDescriptorType isEqualToString:@"long"]) {
                    integerValue = [parameterDescriptor int32Value] ;
                    aValue = @(integerValue) ;
                }
                else if ([printableParameterDescriptorType isEqualToString:@"utxt"]) {
                    stringValue = [parameterDescriptor stringValue] ;
                    if (stringValue.length > 0) {
                        aValue = stringValue ;
                    }
                }
                else if ( ([printableParameterDescriptorType isEqualToString:@"true"]) || ([printableParameterDescriptorType isEqualToString:@"fals"]) ) {
                    booleanValue = [parameterDescriptor booleanValue] ;
                    aValue = @(booleanValue) ;
                }
                else {
                    stringValue = [parameterDescriptor stringValue] ;
                    if (stringValue.length > 0) {
                        aValue = stringValue ;
                    }
                }
                if ((aCocoaKey.length != 0) && (aValue)) aRecord[aCocoaKey] = aValue ;
            }
        }
        [theResult addObject:aRecord] ;
    }
    return theResult ;
}
@end