代码之家  ›  专栏  ›  技术社区  ›  Todd Ditchendorf

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

  •  5
  • Todd Ditchendorf  · 技术社区  · 15 年前

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

    record 将自动转换为 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”参数是 -&燃气轮机; 字典 我遇到麻烦了。请注意,记录/字典的键不能预先知道/定义。他们是武断的。

    <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”命令。所以我知道我的基本设置是正确的。唯一的问题似乎是武断 记录 -&燃气轮机; 字典

    当我在中执行上面的AppleScript时 AppleScript Editor.app ,我在Cocoa端得到此错误:

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

    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 回复  |  直到 8 年前
        1
  •  3
  •   RyanWilcox    14 年前

    没错——NSDictionary和AppleScript记录看起来好像是混合在一起的,但实际上它们并没有(NSDictionary使用对象键,比如字符串),而AppleScript记录使用四个字母的字符代码(多亏了AppleEvent/Classic Mac OS的传统)。

    看见 this thread on Apple's AppleScript Implementer's mailing list

    因此,在您的例子中,您实际需要做的是解压缩您拥有的AppleScript记录并将其翻译到您的NSDictionary中。您可以自己编写代码,但这很复杂,而且需要深入AE经理。

    然而,这项工作实际上已经在一些用于 appscript/appscript-objc

    代码是 available on sourceforge . 几周前,我向作者提交了一个补丁,这样您就可以构建AppScript Objc的底层基础,这就是您需要的所有情况:您需要做的是打包和解压Apple脚本/ Apple事件记录。

    对于其他谷歌用户,还有另一种方法可以做到这一点,那就是不使用appscript: ToxicAppleEvents . 其中有一种方法可以将字典翻译成苹果事件记录。

        2
  •  7
  •   Mecki    6 年前

    Cocoa将无缝转换 NSDictionary

    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 字典 <cocoa> 标签是必需的( success method , body 在上面的示例中),如果没有,则可以使用 <可可豆> code 是AS记录中的名称,但在 replyCode

     AS Type     | Foundation Type
    -------------+-----------------
     boolean     | NSNumber
     date        | NSDate
     file        | NSURL
     integer     | NSNumber
     number      | NSNumber
     real        | NSNumber
     text        | NSString
    

    Table 1-1 Cocoa脚本指南简介 "

    当然,一个值本身可以是另一个嵌套记录,只需定义一个 为此,请使用 记录类型 property 规范和 然后,该值必须是匹配的字典。

    好吧,让我们试试完整的样品。让我们在下面的示例中定义一个简单的HTTPGET命令 文件:

    <command name="http get" code="httpGET_">
        <cocoa class="HTTPFetcher"/>
        <direct-parameter type="text"
            description="URL to fetch."
        />
        <result type="http response"/>
    </command>
    

    现在我们需要在Obj-C中实现这个非常简单的命令:

    #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;
    }
    

    当然,回来 nil 在内部故障的情况下,错误处理是错误的。我们可以返回一个错误。我们在这里甚至可以使用一些特殊的错误处理方法(例如,设置继承的某些属性) NSScriptCommand ),但这毕竟只是一个样本。

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

    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
    

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

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

    Dictionary is {
        body = "<!DOCTYPE html>...";
        method = GET;
        replyCode = 200;
        success = 1;
    }
    

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

    如果您真的不知道需要什么样的数据,例如,像一个转储工具,只在两种定义良好的数据格式之间转换,而不了解数据本身,那么为什么要将其作为记录传递呢?为什么不将该记录转换为易于解析的字符串值(例如属性列表、JSON、XML、CSV),然后将其作为字符串传递,最后将其转换回对象?这是一个非常简单但非常强大的方法。在Cocoa中解析属性列表或JSON可能需要四行代码。好吧,这可能不是最快的方法,但无论谁在一句话中提到AppleScript和high performance都已经犯了一个根本性的错误;AppleScript当然可能很多,但“快”并不是您可以期望的属性。

        3
  •  3
  •   Community Romance    7 年前

    to use a record definition 正如在另一个答案中提到的,该答案也有助于链接到苹果的文档,而我本人至少完全没有从脚本指南中找到这些文档。

    如果上述要求由于任何原因不符合您的需要,则另一种解决方案是实施 +scriptingRecordWithDescriptor: 作为词典的一个类别。我在图书馆找到了这个解决方案 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:和同等类型的自定义命令对我有效。

        4
  •  0
  •   M Wilm    8 年前

    2016年9月11日,Mac OS 10.11.6

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

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

    针对+(id)scriptingRecordWithDescriptor:(NSAppleEventDescriptor*)索引的建议解决方案在我的案例中不起作用。

    我的实现的基本变化是AppleScript环境中的每个类都定义自己的属性和AppleScript代码。要确定的键对象是NSScriptClassDescription,其中包含AppleScript代码和Cocoa键之间的关系。 另一个复杂问题是,在方法中用作参数的NSAppleEventDescriptor表示传入的AppleScript记录(在我的例子中是记录列表)。此NSAppleEventDescriptor可以有不同的形式。

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

    该方法在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
    
    推荐文章