How to support an AppleScript element (a one-to-many relation) of the application (NSApp) class?

634 views Asked by At

Here's what I have:

1) Include the proper "permissions" entries in the 'Info.plist

  • 'Scriptable': YES
  • 'Scripting definition file name': myApp.sdef

2) Include the element "element" tag within a class extension "element" tag:

`<class-extension extends="application" description="The application and top-level scripting object.">
    <!-- various property tags go here -->

<element type="object item" access="r">
<cocoa key="theseObjects"/>
</element>
</class-extension>`

3) Include the element class tag:

<class name="object item" code="Objs" description="Application 'too many' object collection" plural="object items" inherits="item"> // I don't believe 'inherits' name is critical for AS to work
<cocoa class="ObjectItem"/>                     
</class>

4) Include the delegate method that forwards 'NSApplication' scriptability support to its delegate:

- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key {    
if ([key isEqualToString:@"theseObjects"]) {
    return YES;
}
return NO;

}

5) Create a 'ObjectItem' class and put the object specifier there:

 - (NSScriptObjectSpecifier *)objectSpecifier { 
NSScriptObjectSpecifier *containerRef = nil;
NSScriptObjectSpecifier *specifier = [[NSNameSpecifier alloc] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:@"theseObjects" name:@"objectName"];
return [specifier autorelease];

6) Post the KVO accessor method within the Application's delegate:

 - (NSArray *)theseObjects;
   {
ObjectItem *thisObject = [[ObjectItem new] autorelease];
NSArray *thisArray = [NSArray arrayWithObject:thisObject];
return thisArray;
    }
}

7) Create an AppleScript that returns objects from my element getter method:

    tell application "SpellAnalysis"
    get theseObjects
    end tell

8) The result: error "The variable objects is not defined." number -2753 from "objects"

9) Pull my hair out

2

There are 2 answers

0
Ron Reuter On

Regarding the original One-to-many post, the author did a good job outlining the steps to get to the point where scripting almost works. Fortunately, only a few small changes are needed to fix the problem:

1) The sample AppleScript

tell application "SpellAnalysis" to get theseObjects 

can't possibly work. An AS can't refer to Objective-C variable names (theseObjects). Since the sdef defines a class and element type of "object item", that is what has to be used in the script:

tell application "SpellAnalysis" to get object items -- FIX #1

2) The object specifier in the ObjectItem class should return the actual name property value of the current object, not the string @"objectName". So it should be:

- (NSScriptObjectSpecifier *)objectSpecifier 
{ 
    NSScriptObjectSpecifier *containerRef = nil;
    NSScriptObjectSpecifier *specifier = [[NSNameSpecifier alloc] 
        initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] 
        containerSpecifier:containerRef 
        key:@"theseObjects" 
        name:self.name]; // FIX #2

    return specifier;
}

3) The complete and correct sdef to support the above is:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary xmlns:xi="http://www.w3.org/2003/XInclude">
    <xi:include href="file:///System/Library/ScriptingDefinitions/CocoaStandard.sdef" xpointer="xpointer(/dictionary/suite)"/>

    <suite name="SpellAnalysis Suite" code="sSIA" description="ToDo">
        <class name="object item" code="Objs" description="ToDo" plural="object items">
            <cocoa class="ObjectItem"/> <!-- KVC: Objective-C class ObjectItem : NSObject -->
            <property name="name" code="pnam" type="text" access="r" description="Its name.">
                <cocoa key="name"/>
            </property>
        </class>
        <class-extension name="SpellAnalysis application" extends="application" description="ToDo">
            <element type="object item" access="r">
                <cocoa key="theseObjects"/> <!-- KVC: app delegate's @property NSArray* theseObjects; -->
            </element>
        </class-extension>
    </suite>
</dictionary>

4) The ObjectItem.h interface to support the above:

@interface ObjectItem : NSObject
    @property (nonatomic, strong) NSString *name;

    - (instancetype)initWithName:(NSString *)name;
@end

5) The ObjectItem.m file to support the above:

#import "ObjectItem.h"
@implementation ObjectItem

- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init])
    {
        self.name = name;
    }
    return self;
}

- (NSScriptObjectSpecifier *)objectSpecifier 
{ 
    NSScriptObjectSpecifier *containerRef = nil;
    NSScriptObjectSpecifier *specifier = [[NSNameSpecifier alloc] 
        initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] 
        containerSpecifier:containerRef 
        key:@"theseObjects" 
        name:self.name]; // FIX #2

    return specifier;
}
@end

6) Finally, the code in your App delegate class to support the above:

#import "MyAppDelegate.h"
#import "ObjectItem.h"

@interface MyAppDelegate ()
@property (nonatomic, strong) NSArray *theseObjects;
@end

@implementation MyAppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    self.theseObjects = @[
        [[ObjectItem alloc] initWithName:@"Item A"],
        [[ObjectItem alloc] initWithName:@"Item B"],
        [[ObjectItem alloc] initWithName:@"Item C"]
    ];
}

- (BOOL)application:(NSApplication *)sender delegateHandlesKey:(NSString *)key
{
    if ([key isEqualToString:@"theseObjects"])
    {
        return YES;
    }
    return NO;
}
@end

That's it, just a couple of simple errors to fix (one in the script, one in the object specifier code), and the following script returns expected results:

tell application "SpellAnalysis"
    get object items -- expect a list of object references for all objects in array
end tell

--> {object item "Item A" of application "SpellAnalysis", object item "Item B" of application "SpellAnalysis", object item "Item C" of application "SpellAnalysis"}

Other scripts also work:

tell application "SpellAnalysis"
    get object item 2 -- expect a object reference to the second item in the array
end tell

--> object item "Item B" of application "SpellAnalysis"


tell application "SpellAnalysis"
    name of object item 2 -- Expected result: the name property of 2nd obj in array
end tell

--> "Item B"
0
Antony On

I thought I should post some addition information on Cocoa Scriptability support for Core-data applications since there is so little information out there. I spent a least a month trying to understand how to come to terms with this mechanism.

Although I was able to provide AppleScript support for my core-data application using index specifiers, I found that employing 'uniqueID' specifiers provided a better fit. Better, because core data to-many relationships are supported by unordered sets, rather then arrays. Core data provides a way for you to specify a managed object by an object ID that can be evaluated by a Cocoa Scriptability method. Nevertheless, to implement support for a "too-many" relation using uniqueID specifiers, additional code elements are needed.

1) Provide a property element in the 'sdef' for each entity you will be supporting. For example, to support my 'level' entity I post the following within the 'levels' class tags:

<property name="id" code="ID  " type="text" access="r" description="The level's unique id. This may be a temporary id for newly-    created level objects, until they are saved.">
    <cocoa key="uniqueID"/>
</property>

Note that the type is designated 'text' and not 'specifier'. The 'uniqueID' interchange between the AppleEvent mechanism and Cocoa AppleScript support will be a string value. Also notice that the 'cocoa key' value is 'uniqueID'. This key (typical of such keys in the 'sdef') is used by the AppleScript scriptability support to identify a method name in your application that conforms to a KVC pattern.

2) Post a 'valueInWithUniqueID' method within the class that contains the target objects. It is within this method that you provide a way to extract the managed object corresponding to whatever 'uniqueID' is passed to the method. Here's mine for my 'levelsArray' KVO method posted within my container class, 'Levels'.

- (id)valueInLevelsArrayWithUniqueID:(NSString *)uniqueID;
{
    NSManagedObject *managedObject= [[[NSApp delegate] myManagedObjectContext] objectWithID:[[[NSApp delegate] lessonsManager]    managedObjectIDForURIRepresentation:[NSURL URLWithString:uniqueID]]];
    return managedObject;
}

And here is my 'sdef' property declaration for the contained 'unit' class:

<property name="id" code="ID  " type="text" access="r" description="The unit's unique id.  This may be a temporary id for newly-created unit objects, until they are saved.">
    <cocoa key="uniqueID"/>
</property>

My 'Units' class also evokes the value method. Note the essential KVC name pattern:

- (id)valueInUnitsArrayWithUniqueID:(NSString *)uniqueID;
{
NSManagedObject *managedObject= [[[NSApp delegate] lessonsDBase] objectWithID:[[[NSApp     delegate] lessonsManager] managedObjectIDForURIRepresentation:[NSURL   URLWithString:uniqueID]]];

    return managedObject;
}

With these in place, if my AppleScript needs to specify a 'level' object from its 'uniqueID', it will be answered. This seems to come into play when you have a hierarchy of entity classes and you write an AppleScript that 1) evokes a command that returns a reference to its result, and 2) acts upon this returned result with another command:

count (make new section at unit 1 of level 1)

Curiously, the following does not evoke the value method:

count (unit 1 of level 1)

Note that it lacks condition 1--a primary command (e.g. 'make') is missing. In this case, the implied AppleScript 'get' command is evoked, however Cocoa scriptability seems to determine the values of the entity hierarchy by another means.

3) Provide 'uniqueID' object specifiers for all objects returned from commands that you support, as well as explicitly named 'objectSpecifier' for each of entity sub-classes that support their implied 'get' commands. For example, my 'clone' command, posted within its 'performDefaultImplementation' provides the following method:

 NSUniqueIDSpecifier *uniqueIDSpecifier = [[[NSUniqueIDSpecifier allocWithZone:[self zone]]  initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:sourceContainerSpecifier key:sourceKey uniqueID:uniqueID] autorelease];

Depending on whether you are manipulating a single object or a range of objects with your command, you return the 'uniqueIDSpecifier' directly or add it to an array of consecutive such specifiers that are returned after the last addition. To support the implied 'get' command, you post a 'uniqueID' object specifier in each of your managed object entity sub-classes. The following specifier supports my 'Unit' class sub-class:

- (NSScriptObjectSpecifier *)objectSpecifier {
    NSScriptObjectSpecifier *containerRef = [[NSApp delegate]levelsSpecifier];
    NSString *uniqueID = [[[self objectID] URIRepresentation] absoluteString]; // This is            the key method for determining the object's 'uniqueID'
if (uniqueID) {
    NSScriptObjectSpecifier *uniqueIDSpecifier = [[[NSUniqueIDSpecifier allocWithZone:   [self zone]]   initWithContainerClassDescription:[containerRef keyClassDescription]  containerSpecifier:containerRef key:@"unitsArray" uniqueID:uniqueID] autorelease];
   [[NSApp delegate] setUnitsSpecifier:uniqueIDSpecifier]; // Post specifier so Units  class specifier can access it
    return uniqueIDSpecifier;
   } else {
      return nil;
   }

Note the second commented line indicate that I post the results of this specifier in a global variable so that the object specifier of contained classes can use this specifier result as its container specifier. The application delegate is the one place all entity sub-classes can have access to application-wide accessors and methods such as this object specifier result.

I do hope this helps someone out there like myself, last month.