How to declare window subclasses in sdef?

My scriptable app knows different types of windows, based on the ObjC classes SearchWindow and ResultWindow.

I cannot figure out how to make their specific script classes appear when I use get windows - they come back as generic window types.

Here’s the .sdef, slightly shortened:

<dictionary title="Find Any File Terminology">

	<suite name="Standard Suite" code="????" description="Common classes and commands for all applications.">
		… here's all the stuff from /System/Library/ScriptingDefinitions/CocoaStandard.sdef
	</suite>

	<suite name="FAF Suite" code="FAF2" description="Find Any File commands">
		<class name="Find Any File" code="FAFa" inherits="application" description="The application's top-level scripting object.">
			<cocoa class="NSApplication"/>
			<element type="document">
				<cocoa key="orderedDocuments"/>
				<accessor style="id"/>
				<accessor style="index"/>
			</element>
			<element type="window" access="r">
				<cocoa key="orderedWindows"/>
				<accessor style="id"/>
				<accessor style="index"/>
			</element>
		</class>

		<class name="search window" inherits="window" code="FinW" description="A window with search rules and a Find button.">
			<cocoa class="SearchWindow"/>
		</class>
		
		<class name="results window" inherits="window" code="ResW" description="A window containing a list of files and folders.">
			<cocoa class="ResultWindow"/>
		</class>

	</suite>
</dictionary>

Do I have my declarations wrong?

I can verify, by overwriting -[NSApplication orderedWindows], that it’s really returning objects of my two window classes.

update

I guess I had my expectations wrong. I thought that the specific window types would be identifyable as such, i.e. that they’d appear as e.g. “search window id xxxx” instead of just “window id xxxx”. But that’s never the case for subclassed items, is it?

So, how is an AppleScript user supposed to identify the type of a subclassed object, then? I see that Script Debugger doesn’t tell me, either, when I try this, for instance, with the old Sketch example - the “graphics” objects do not dynamically identify them as their real classes, but as the classes declared by that property.

Here’s what SD shows:

As you can see, “graphics” only shows the basic super class members, and only the “text areas” shows the specific members of the subclass.

Is that normal, or is that just a bad example implementation?

Hey Thomas,

Have you looked through Script Debugger’s own sdef?

-Chris

image

1 Like

Indeed, SD itself manages to show different window classes in the “windows” property when using the Explorer:

That confirms what I hoped to be possible: That subclasses can identify themselves in an array / list.

I’ll dig into this tomorrow. It remains to be seen if this can be solve purely in the .sdef or whether this requires extra code in the scriptable app. Or maybe it’s even just a special trick that only SD can do with itself.

1 Like

Decades ago now I and I think John Delacour got Steve Dorner to add window discriminators to Eudora.

We mostly wanted an Outgoing Message class, but Steve surprised us by defining every major window class.

I still curse when I remember how functional, flexible, and scriptable Eudora was and then think about what I have to live with today…

2 Likes

I have the suspicion that I’ll need to construct and return specific NSUniqueIDSpecifier or NSAppleEventDescriptor objects instead of my NSWindow subclass objects, because the default Cocoa scripting handler that converts NSObject classes into AE objects is not smart enough for this.

I did some more digging. In my window subclass, I overwrote objectSpecifier, calling its super, to see what the default implementation (of NSWindow) returns:


-(NSScriptObjectSpecifier *)objectSpecifier {
	NSScriptObjectSpecifier *res = super.objectSpecifier;
	return res;
}

This is what I get:

<NSUniqueIDSpecifier: orderedWindows with an ID of "3799">

So, it refers to the default implementation of NSWindow.orderedWindows. I guess I’ll have to add a new accessor, e.g. “searchWindows”, which only returns windows of that subclass, and then return an NSUniqueIDSpecifier specifier referring to that new accessor method, with my own ID for it. And I’ll then also probably have to implement the lookup function for it.

Does someone know if those IDs that – I suspect – the Cocoa bridge assigns for these NSUniqueIDSpecifiers are accessible somewhere, so that I can re-use them for my subclasses? Or maybe I just keep invoking the super’s objectSpecifier method, read its ID and create a new specifier using that same ID with reference to the searchWindows accessor method?

There is no magic to this. I declare the various window sub-classes in my SDEF, and then the window collections within each container class (e.g. application, etc.) are declared as follows:

      <element type="window" access="rw">
        <cocoa key="orderedSDWindows"/>
      </element>
      <element type="script window" access="rw" description="To create an empty script window, make a new document.">
        <cocoa key="orderedScriptWindows"/>
      </element>
      <element type="dictionary window" access="rw" description="When you make a new dictionary window, it is empty; set its file spec to specify an application.">
        <cocoa key="orderedDictionaryWindows"/>
      </element>
      <element type="explorer window" access="rw" description="To create an explorer window, open a result value. For example, you could tell a document to open last result, or tell an event log entry to open event result.">
        <cocoa key="orderedViewerWindows"/>
      </element>
      <element type="event log window" access="rw" description="To open an event log window, tell a document to make new event log window.">
        <cocoa key="orderedEventLogWindows"/>
      </element>

The important bits are:

  • separate collections for each window class
  • each collection has its own Cocoa accessor: orderedWindows , orderedScriptWindows, etc.

Our Cocoa code looks like this - it’s very simplistic. If your collection is large you’ll want to use a more efficient approach:

// "user" script windows only
- (NSArray *) orderedWindows {
    NSMutableOrderedSet *ordered = [NSMutableOrderedSet orderedSet];
    for (NSWindow* aWindow in [super orderedWindows]) {
        if (aWindow.tabbedWindows) {
            [ordered addObject:aWindow.tabGroup.selectedWindow];
        } else {
            [ordered addObject:aWindow];
        }
    } 
    return [ordered array];
}

- (NSArray*) orderedScriptWindows {
	NSMutableArray* result = [NSMutableArray array];
	
	for (NSWindow* aWindow in [self orderedWindows])		
		if ([[aWindow windowController] isKindOfClass:[SD8ScriptWindowController class]] &&
            [aWindow.tabGroup.selectedWindow isEqualTo:aWindow])
			[result addObject:aWindow];
	
	return result;
}
1 Like

Thanks Mark. But I believe I already do that in my code, basically (nice way to handle tabbed windows, though – I’ll consider that). I also have specific accessors for the list of “result windows” and “search window”, just like you show above for your “script windows”. And when I explore those specific list accessors, then I do get them all listed with the subclass name.

Yet, when querying “get windows”, all window subclasses appear as just “window id X”, not as e.g. “search window id X” as it’s desired and as SD does it as shown in my screenshot of SD’s Explorer.

Please read again my post just before yours. I had updated it just when you wrote yours. I added some thoughts about using object specifiers. Are you sure you’re not doing that?

Whoops – I think I confused myself here. I had been so sure that I’d see the custom window types when I used the specific window lists, but now I see that it’s not as I thought:

Still, I wonder if I need to implement objectSpecifier and return custom NSUniqueIDSpecifiers to make this work as it does in SD.

Actually, I think I was right. If I implement this in my SearchWindow subclass of NSWindow, then it works:

-(NSScriptObjectSpecifier *)objectSpecifier {
	NSUniqueIDSpecifier *res = (NSUniqueIDSpecifier*) super.objectSpecifier; // this is an assumption that may not always be true!
	NSScriptObjectSpecifier *appSpec = NSApp.objectSpecifier;
	NSScriptClassDescription *appDesc = [NSScriptClassDescription classDescriptionForClass:NSApplication.class]; 
	res = [NSUniqueIDSpecifier.alloc initWithContainerClassDescription:appDesc containerSpecifier:appSpec key:@"searchWindows" uniqueID:res.uniqueID];
	return res;
}

The result:

Yay.

And I didn’t even have to implement the reverse ID Lookup function. I wonder if that’s proper or it’ll bite me in the rear later.

1 Like

I see, yes, the object specifiers returned for a particular class need to properly identify the collection they belong to. Here’s how Script Debugger’s window object specifier is created:

- (NSScriptObjectSpecifier*) objectSpecifier {
	NSString* windowKey;
	NSWindowController* controller = [self windowController];
	NSDocument* document = [controller document];

	if (document && [document isKindOfClass:[ScriptDocument class]])
		windowKey = @"orderedScriptWindows";
	else if ([controller isKindOfClass:[RKDictionaryWindowController class]])
		windowKey = @"orderedDictionaryWindows";
	else if ([controller isKindOfClass:[RKViewerWindowController class]])
		windowKey = @"orderedViewerWindows";
	else if ([controller isKindOfClass:[EventLogWindowController class]])
		windowKey = @"eventLogViewerWindows";
	else
		windowKey = @"orderedWindows";

	NSScriptClassDescription* desc = [[NSScriptSuiteRegistry sharedScriptSuiteRegistry] classDescriptionWithAppleEventCode:cApplication];
	
	return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:desc
														containerSpecifier:nil
																	   key:windowKey
																  uniqueID:[NSNumber numberWithInt:[self uniqueID]]] autorelease];
}
2 Likes

I recommend ID-based object specifiers because they are stable over time, but they lack any useful identifying information for users (i.e. name). Within Script Debugger this isn’t an issue because the explorer shows the name associated with an ID. But in the Script Editor, IDs are an opaque value.

Falling back to name-based object specifiers provides useful identifying information but is less stable (cannot survive object renaming). In the end, its your call as to which is the best for your app.

2 Likes