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

发布于 2024-08-18 21:50:57 字数 2192 浏览 3 评论 0原文

我有一个 Cocoa 应用程序,其中包含 .sdef XML 文件中描述的 AppleScript 字典。 sdef 中定义的所有 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 value”参数是记录 ->我遇到问题的 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>

和 Cocoa:

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

“tab”对象正确响应我定义的所有其他 AppleScript 命令。如果我不发送可选的“带值”参数,“选项卡”对象也会响应“提交表单”命令。所以我知道我的基本设置正确。唯一的问题似乎是任意的 record->NSDictionary 参数。

当我在 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

I have a Cocoa application with an AppleScript dictionary described in a .sdef XML file. All of the AppleScript classes, commands, etc. defined in the sdef are working property.

Except for my "submit form" command. The "submit form" command is my only command attempting to pass a parameter that is an arbitrary hashtable of information from AppleScript to Cocoa. I assume this should be done by passing an AppleScript record which will be automatically converted to an NSDictionary on the Cocoa side.

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

The "with values" parameter is the record -> NSDictionary parameter i am having trouble with. Note that the keys of the record/dictionary cannot be known/defined in advance. They are arbitrary.

Here is the definition of this command in my 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>

And I have a "tab" object which responds to this command in the sdef:

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

and Cocoa:

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

The "tab" object correctly responds to all the other AppleScript commands I have defined. The "tab" object also responds to the "submit form" command if I don't send the optional "with values" param. So I know I have the basics setup correctly. The only problem seems to be the arbitrary record->NSDictionary param.

When I execute the AppleScript above in AppleScript Editor.app, I get this error on the Cocoa side:

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

and this one on the AppleScript side:

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

Can anyone tell me what I'm missing? For reference the entire application is open source on GitHub:

http://github.com/itod/fluidium

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

樱花坊 2024-08-25 21:50:57

Cocoa 会将 NSDictionary 对象无缝转换为 AppleScript (AS) 记录,反之亦然,您只需告诉它如何执行此操作。

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

<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 键,则不需要 标记(successmethod、< code>body 在上面的例子中),如果没有,你可以使用 标签来告诉 Cocoa 读取这个值的正确键(在上面的例子中,code 是 AS 记录中的名称,但在 NSDictionary 中,键将是 replyCode;我只是出于演示目的而这样做)。

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

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

请参阅 Apple 的“Cocoa 脚本指南简介”的表 1-1

当然, value 本身可以是另一个嵌套记录,只需为其定义一个 record-type ,在 property 规范和中使用 record-type 名称NSDictionary 的值必须是匹配的字典。

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

<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 以防万一内部故障的原因是错误处理不当。我们可以返回一个错误。好吧,我们甚至可以在这里使用特殊的 AS 错误处理方法(例如设置我们从 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,最后将其转换回对象?这是一个非常简单但非常强大的方法。在 Cocoa 中解析属性列表或 JSON 可能需要四行代码。好吧,这可能不是最快的方法,但无论是谁在一句话中提到 AppleScript 和高性能,他一开始就犯了一个根本性的错误; AppleScript 当然可能很多,但“快”并不是您可以期望的属性。

Cocoa will seamlessly convert NSDictionary objects to AppleScript (AS) records and the other way round for you, you only need to tell it how to do that.

First of all you need to define a record-type in your scripting definition (.sdef) file, e.g.

<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>

The name is the name this value will have in the AS record. If the name equals the NSDictionary key, no <cocoa> tag is required (success, method, body in the example above), if not, you can use a <cocoa> tag to tell Cocoa the correct key for reading this value (in the example above, code is the name in the AS record, but in the NSDictionary the key will be replyCode instead; I just made this for demonstration purposes here).

It is very important that you tell Cocoa what AS type this field shall have, otherwise Cocoa doesn't know how to transform that value to an AS value. All values are optional by default but if they are present, they must have the expected type. Here's a small table of how the most common Foundation types match to AS types (incomplete):

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

See Table 1-1 of Apple's "Introduction to Cocoa Scripting Guide"

Of course, a value can itself be another nested record, just define a record-type for it, use the record-type name in the property specification and in the NSDictionary the value must then be a matching dictionary.

Well, let's try a full sample. Let's define a simple HTTP get command in our .sdef file:

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

Now we need to implement that command in Obj-C which is dead simple:

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

Of course, returning nil in case of internal failures is bad error handling. We could return an error instead. Well, there are even special error handling methods for AS we could use here (e.g. setting certain properties we inherited from NSScriptCommand), but it's just a sample after all.

Finally we need some AS code to test it:

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

Result:

{success:false}

As expected, now one that succeeds:

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

Result:

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

Also as expected.

But wait, you wanted it the other way round, right? Okay, let's try that as well. We just reuse our type and make another command:

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

And we implement that command as well:

#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

And we test it:

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

And her is what our app logs to console:

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

So, of course, it works both ways.

Well, you may complain now that this is not really arbitrary, after all you need to define which keys (may) exist and what type they will have if they exist. You are right. However, usually data is not that arbitrary, I mean, after all code must be able to understand it and therefor it must at least follow certain kind of rules and patterns.

If you really have no idea what data to expect, e.g. like a dump tool that just converts between two well defined data formats without any understand of the data itself, why do you pass it as a record at all? Why don't you just convert that record to an easily parse-able string value (e.g. Property List, JSON, XML, CSV), then pass it Cocoa as a string and finally convert it back to objects? This is a dead simple, yet very powerful approach. Parsing Property List or JSON in Cocoa is done with maybe four lines of code. Okay, it's maybe not the fastest approach but whoever mentions AppleScript and high performance in a single sentence already made a fundamental mistake to begin with; AppleScript certainly may be a lot but "fast" is none of the properties you can expect.

初心未许 2024-08-25 21:50:57

如果您知道要包装的字典中的字段以及要映射到 AppleScript 或从 AppleScript 映射的键类型是可预测的,那么最好的解决方案似乎是 使用记录定义,如另一个答案中所述,该答案也有助于链接到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: 和等效类型的自定义命令对我有用。

If you know the fields in the dictionary you are wrapping and the types of keys you want to map to / from AppleScript are predictable, it appears the best solution is to use a record definition as noted in another answer which also helpfully links to Apple's documentation that I at least personally had completely missed from the scripting guide.

If those above requirements do not for whatever reason fit your needs, an alternative solution is to implement +scriptingRecordWithDescriptor: as a category to NSDictionary. I found this solution in the Fluidium project the question referred to. Here's a paste from 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

I can confirm that using +scriptingRecordWithDecriptor: with a custom command of the equivalent kind worked for me.

骄兵必败 2024-08-25 21:50:57

正确 - NSDictionaries 和 AppleScript 记录看起来像是会混合在一起,但实际上它们不会混合(NSDictionaries 使用对象键 - 比如说字符串),而 AppleScript 记录使用四个字母字符代码(感谢它们的 AppleEvent/Classic Mac OS 传统)。

参阅 Apple 的 AppleScript Implementer 邮件列表中的此帖子

请 ,在您的情况下,您实际需要做的是解压您拥有的 AppleScript 记录并将其翻译到您的 NSDictionary 中。您可以自己编写代码,但它很复杂并且深入到 AE 管理器中。

然而,这项工作实际上已经在 appscript/appscript-objc 的一些底层代码中为您完成了(appscript 是 的糟糕限制较少。)

一个用于 Python、Ruby 和 Objective-C 的库,可以让您与 AppleScriptable 应用程序进行通信,而无需实际使用 AppleScript,您可以在使用 Cocoa 脚本的地方使用 appscript-objc,但该技术 代码可在sourceforge上获取。几周前我向作者提交了一个补丁,这样您就可以构建 appscript-objc 的底层基础,这就是本例中您所需要的:您所需要做的就是打包和解包 Applescript/AppleEvent 记录。

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

Right -- NSDictionaries and AppleScript records seem like they would mix, but they don't actually (NSDictionaries use object keys -- say strings) where AppleScript records use four letter character codes (thanks to their AppleEvent/Classic Mac OS heritage).

See this thread on Apple's AppleScript Implementer's mailing list

So, what you actually need to do, in your case, is to unpack the AppleScript record you have and translate it into your NSDictionary. You could write the code all by yourself, but it's complicated and dives deep into the AE manager.

However, this work has actually been done for you in some underlaying code for appscript/appscript-objc (appscript is an library for Python and Ruby and Objective-C that lets you communicate with AppleScriptable applications without actually having to use AppleScript. appscript-objc could be used where you would use Cocoa Scripting, but has less of the sucky limitations of that technology.)

The code is available on sourceforge. I submitted a patch a few weeks ago to the author so you could build JUST the underlaying foundation for appscript-objc, which is all you need in this case: all you need to do is pack and unpack Applescript/AppleEvent records.

For other googlers, there's another way to do this, that's not using appscript: ToxicAppleEvents. There's a method in there that translates dictionaries into Apple Event Records.

故乡的云 2024-08-25 21:50:57

2016 年 9 月 11 日,Mac 操作系统 10.11.6
问题是:如何在可可世界中将 AppleScript 记录转换为 NSDictionary ?

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

NSDictionary 使用相应的 cocoa 键作为 AppleScript 记录中四种最基本类型的键(以 NSString 对象的形式)和 NSNumber 或 NSString 值:字符串、整数、双精度和布尔值。

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

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

AppleScript 记录中的一项很特殊:{class:"script class name"}。这些代码测试它的存在。

您在代码中需要做的唯一替换是引入您的应用程序 AppleScript suite 的名称“Name of your apple script suite”。
该方法作为 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

11.9.2016, Mac OS 10.11.6
The problem is: how to convert an AppleScript record into an NSDictionary in the cocoa world ?

The AppleScript record uses AppleScript properties as keys and numbers or strings as values.

The NSDictionary uses the corresponding cocoa keys as keys (in form of NSString objects) and NSNumber or NSString values for four most basic types in the AppleScript record: string, integer, double, and boolean.

The proposed solution for + (id)scriptingRecordWithDescriptor:(NSAppleEventDescriptor *)inDesc did not work in my case.

The basic change in my implementation is that each class in the AppleScript environment defines its own properties and AppleScript codes. The key object to determine is a NSScriptClassDescription which contains the relationship between AppleScript codes and Cocoa keys.
An additional complication is that the NSAppleEventDescriptor used as parameter in the method represents the incoming AppleScript record (or list of records in my case). This NSAppleEventDescriptor can have different forms.

One entry in the AppleScript record is special: {class:"script class name"}. The codes tests for its presence.

The only replacement you have to do in the code is to introduce the name of your's application AppleScript suite for "Name of your apple script suite" .
The method is implemented as a Category on 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文