First things first: Remember that your Apple Event Object Model is a relationship-based graph, not an Object-Oriented DOM. If you try to think about it in OO terms, you will only hurt yourself. If you use OmniOutliner as a design reference then look at its AppleScript dictionary, not at its JavaScript API. Omni’s JS APIs use OO DOM design patterns so are not applicable here. (I can’t comment on the design or quality of OmniOutliner’s AS dictionary as I don’t use it myself, but ISTR Omni being better than average at implementing AppleScript support in general.)
…
Child rows will appear as row
elements of the row
class. Remember: element names are fixed, being the singular/plural form of the class name. Therefore you cannot model both the immediate children and all descendents as elements of row.
class:
row -- a single entry in an outline document
elements:
row -- this row's immediate children
properties:
container : reference -- this row's parent row
entire contents : reference -- all rows contained by this row
See Finder’s container
class for reference. In terms of relationships, a row object has:
- a one-to-many relationship with its child rows
- a one-to-one relationship with its parent row†
- a one-to-many relationship with all its descendents
BTW, I can’t tell you the [Cocoa app] design pattern for implementing the entire contents
property in CocoaScripting because I don’t know it myself. (Frankly CS is a boat anchor.) Mark has spelunked the CS framework in the past, trying to push its naive behavior beyond its implicit assumptions and built-in limitations; perhaps he can advise.
All I can say is that is how the app’s AEOM should look and feel from the user’s POV. Actually implementing that behavior using CS, and getting it 100% correct, is mostly left to the poor developer.
It may be that CS forces you to implement the entire contents
property as a list of single-object references (which CS-based apps often do), as opposed to a single multi-object reference. That UX sucks: client-side iteration forces the scripter to write a bunch of boilerplate repeat loops and drops script performance in a hole. While sometimes that’s unavoidable (e.g. setting the name
property of each element to a unique string), when the same command is being applied to multiple objects it’s best for the application to handle the details.
For instance, let’s mock a delete
command which says to the app “recursively delete all rows that don’t have any content”.
tell application "Bike"
set theRow to …
delete every row of entire contents of theRow whose note = "" and rows = {}
end tell
(Assume note
is a property which contains the row’s user-supplied text.)
BTW, AppleScript syntax is weird and allows where
clauses to appear out-of-line, so what that command is really saying is this:
delete (rows where its note = "" and its rows = {}) of entire contents of theRow
Very powerful query, much like an SQL DELETE from <some container> WHERE <some test>
.
Of course, an RDBMS only has to operate on a flat unordered set of records, whereas you may be implementing your AEOM over ordered NSArray<BKRow>
collections, so ensuring your app performs each deletion in the appropriate order is left to you‡.
BTW, this is probably the point that you as developer decide to implement entire contents
the lame dumb way (list of single-object references) purely for sake of getting your app on sale this side of Xmas, and let the scripter eat the full cost of using it. Implementing a second-rate AEOM is a lot quicker, cheaper, and easier than implementing a top-quality one, and most users will just be happy to get one at all. But that’s the tradeoffs you make as a businessperson who needs to sell product to justify the hours spent developing it.
–
† One caveat is how the root row
object represents its parent. Assuming the root row
object is represented as a property (i.e. one-to-one relationship) of a document
object, then the root row’s container
should be a one-to-one relationship to that document, whereas every child row’s container will be a one-to-one relationshipt to its parent row.
BTW, you might feel tempted to make the root row’s container missing value
, which as a programmer would probably be your first instinct as that’s how your underlying OO Model would no doubt expose a BKRow
instance’s container were you implementing a container
property for ObjC/Swift use:
@property (readonly) BKRow * _Nonnull container;
var container: BKRow? { get }
But again look at your UI/UX from the user’s perspective and observe the principle of least surprise. In the GUI, which is what scripters will naturally use as their reference, all rows are contained by something; the only difference between a root row and every other row is the container’s type: a document instead of a row. The downside is you may have to write a bit more CS code to represent this more sophisticated abstraction to the user (since CS, despite being a ViewController framework, has some painfully tight coupling to the app’s Model); or you can punt it for sake of getting the product out the door and most customers will be psyched just that you bothered at all.
‡ CS-based apps are often good at effecting move/duplicate/delete commands in unsafe/incorrect order, causing bits of the user’s data to end up in the wrong position or even vanish entirely; a smorgasbord of off-by-one related errors. e.g. Try delete (every word where it contains "e")
on a TextEdit document for hilarious results. So the poor script ends up having to do all the iteration itself anyway, just to work around the app’s and/or CS’s implementation bugs.