Yikes! I’m looking for you to tell me the features I can take away :). That’s my goal right now… make it as minimal as possible, without messing up the foundation too much. Then can incrementally add features over time as needs arise.
I’m going to add a conceptual overview to the Bike User’s guide and will link to that from the scripting dictionary. I think some concepts such as “hoisted”, “focused” are better described there where I can have more space and pictures.
Bike’s data model is very simple. Pretty much:
Row
content: SingleLineOfText
children: [Row]
That’s unlike some outliners that distinguish between a “heading name” and then attached content. In Bike every line of visible text is a row, there’s no separate attached content.
So you are right that the text this is “content”, but from a user’s perspective many rows have a name (Todos, January, House, etc), and I think it’s nice to use AppleScripts build in “name” support to access these rows by name.
For other rows name doesn’t make as much sense, but I still think it’s worth using name for the above reason. The data model isn’t going to change to allow for a separate name field, so that’s why I’m using name to access content as plain text right now.
In future (as rich text and maybe row types are added) then I will add a new content field of type rich text. So name will continue to be a plain text view of the content, content will become a richer view.
I do plan to add rich text support, but that will be added after the 1.0 release.
I’m not sure about interactive elements, but I do plan to support for alternative row types in future such as “heading”, “separator”, etc. Again these are future things after 1.0 release.
I think this should be possible, but I don’t have time to work on it right now. The solution will be to create a stylesheet that makes the HTML list look like Bike, and a JavaScript that will collapse, expand those rows. With those files hosted on a CDN then I would just add those links to Bike documents. Free licenses to anyone (and there friends!) who makes that
I will add some conceptual documentation, but for now consider this outline:
Yard work
Garden
Weeding
Planting
Rake Leaves
In this example outline rows of document is everything you see. On the other hand child rows of document is only “Yard work”. Or if you are accessing the “Yard work” row then rows are the four rows listed under it, and child rows are “Garden” and “Rake Leaves”.
child rows are the direct content of the container (container is document, or another row)
rows are the entire recursive content of the container
Please see my latest discussion with @hhas01 for why I think these distinct collections are both needed. Let me know if there’s a better way.
“Hoisted” is when you don’t want to see your entire outline, it’s a view operation that doesn’t effect the model. For example if you hoist “Garden” then your view view will only show “Weeding” and “Planting”.
In your script you are comparing x (which is a row count based number) with count of child rows. I don’t think this generally makes sense to do.
I’m not quite sure what you are trying to do. Here’s a similar script that collects the names of all rows that have children:
tell application "Bike"
tell document 1
set rowText to {}
repeat with x from 1 to count of rows
if ((count of child rows of row x) > 0) then set the end of rowText to name of row x
end repeat
end tell
end tell
You’re asking us how to implement a good idiomatic AEOM. That’s what I’ve given you. If you want to bend it into some weird mess then you certainly do so, though honestly I don’t see what it gains you over doing it the right way.
If you want any given row object to be able to reference all of the nodes in its subtree, putting an entire contents property onto every row object is the way to go. (And also onto document if the root node is invisible, with its children visible as row elements of the document.) This design is simple, powerful, and uniform; everything users love in a good AEOM. e.g. See the entire contents property of Finder’s container class. The only issue is implementing that property within the CS framework, given CS is dumb as rocks and as flexible too.
OTOH, if you only needed to provide direct access to all nodes from the document object, you could define row as elements of document and have that provide a flat view of the entire outline. You can then expose the outline’s top (root) node as an outline property for those who want to traverse the hierarchy themselves, with each node providing access to its immediate children only. e.g. Adobe Illustrator does this for a document’s page item elements. This is not as intuitive to use as entire contents though, and it’s less powerful, of course. So I would still recommend the first way.
…
hello
one
a
two
three
Your mock code for this is not valid AppleScript. At any rate, assuming the “hello” node is the document’s root node presented as an outline property which cannot be created or deleted, the user would create new nodes using commands such as these:
make new row at (beginning of rows of outline) with properties {name: "one"}
make new row at (end of rows of outline) with properties {name: "three"}
make new row at (after row "one" of outline) with properties {name: "two"}
make new row at (end of rows of row "one" of outline) with properties {name: "a"}
I forget the extent to which CS allows users to omit parts of an insertion location specifier and infers them automatically from the rest of the reference. For instance (and continuing to assume your AEOM has an explicit root property named outline), the user might shorten the end of rows of outline reference to end of outline, rows of outline, or even just outline, expecting your app to take care of the details for them.
For instance, if the end of portion (insertion location) is omitted, the command might default to whatever’s the most logical insertion point, typically beginning or end. e.g. With Finder, you don’t need to specify beginning/end/before/after when making/moving elements, just the container into which they will go. However, since your outliner’s row elements are highly order-sensitive, you may prefer to insist the user always specifies an insertion point (beginning/end of rows, before/after row) and throw an error if not. A bit bureaucratic, but leaves nothing to assumption or guesswork.
At any rate just go with whatever behavior the standard framework provides, matching if you can the same default behaviors the user experiences in the GUI.
Replying to parts I’m not sure about, I’m applying quite a bit of this feedback, thanks.
I considered this, but I think it would have performance problems. For example here’s a “Cleanup” script that I wrote:
tell front document of application "Bike"
set savedSelection to selection row
collapse at every child row with completely
select at savedSelection
end tell
With the collapse command its instant and easy to collapse everything. In the above case I can collapse everything and it’s just a single command that’s sent from AppleScript to my app, very fast.
If collapsed becomes a state attached to row then it becomes a lot more expensive. For example if the outline has thousands of rows then they all need to get shipped to AppleScript layer, mutated, shipped back to Bike, and then apply changes to model.
Well, I was going to keep them all :). I feel like they are all different and important views of the selection. I did originally have them as properties of a more generic “selection” object, but wasn’t sure how that really improved things, and it made it more verbose to access the selection.
I agree if I’m just selecting rows than a property or direct parameter is simpler. Why I like using a separate command that with document as direct parameter is that it doesn’t lock me in to selecting row types. I can add other at types in the future. For example I was going to support selecting by line/column range and I still might in the future. That could be added easily to the current selection command without breaking anything.
I think they can still be useful for performance and working with other applications. The intention isn’t so much to export to and import from files or documents. Instead the idea is to be able to quickly turn a part of my outline into a standard format that I can share with another other app.
Outlines can be big and have a variety of content. Maybe I want a script that quickly turns my “top 10” section into a public web page. I can do that with a quick export and post the string to the web. On the other hand duplicating those items to a new document, saving as a new format, etc is just much harder.
Generally, all of Bike’s import/export formats are open HTML/OPML/Plain Text and so likely to be useful to / or available from other apps. Allowing quick export of a chunk of your outline to another location in a standard format is pretty useful I think.
I agree it’s weird, but that’s a type that I’m importing from the standard cocoa scripting terminology through this line in my .sdef:
The CocoaStandard.sdef linked above is handling all the document load/save format behavior. I’m not sure what it’s doing, but I will probably just use it’s standard behavior without any changes.
Remember: AEOM is a very high-level end-user-oriented ViewController abstraction layer, with a particular focus on multi-object queries. The UI it presents to the user may track the Model implementation very closely (thin abstraction), or it could be wildly different that does a ton of work to present the raw program data in a way that the user finds logical and intuitive (thick abstraction).
For instance, the standard Text Suite has character, word, and paragraph classes, but these bear zero resemblance to how the backing store is actually implemented (e.g. one big linear char buffer, or maybe an NSMutableAttributedString if you’re really pushing the boat out). These so-called “classes” are just thick abstractions for building queries that will search through all those chars to find the portions to manipulate. Yes it’s a chore to implement such a thick abstraction—even harder to make it rock-solid reliable as fast as well. But compare it to “automation” APIs that require the user to wrangle a raw “cursor” and manipulate that buffer via the same low-level [Obj]C[++] Model API that the app’s GUI uses internally, and you’ll appreciate the huge difference in end-user UX that the high-level abstraction provides.
BTW, don’t know if you already know of it, but if you don’t:
Technical Note TN2106: Scripting Interface Guidelines. Basically to AEOM what Apple’s HIG was to GUI (at least back when Apple still cared about consistency, legibility, etc, etc). Worth a skim.
Anyway, getting back to implementation…
There is nothing to say that under the hood your AEOM can’t recognize set collapsed of entire contents of someRow to true as a single collapse operation performed on an entire subtree and optimize that to the appropriate Model method call. You don’t say what you’re using for a backing store or what sort of internal methods your Model presents to your graphical and scripting ViewControllers for their use.
Of course, if you leave CS to do things its own way, it will insist on iterating every single object (real or virtual) and thereby taking forever, because CS really is thick as mince. (This is also why CS’s standard Text Suite is complete garbage for both reliability and performance. To call CS’s implementation “naive” is…well, naive.) The only question for you is whether it is worth overriding the standard GetCommand, SetCommand classes to be considerably less dumb. Or it might be that internally you could implement an BKEntireContents class that dumb old CS thinks is a single object, essentially a facade/shim for whatever fast set operations magic your app uses internally. I can’t tell you how ’cos the CS framework is not something I’ve used (I develop automation workflows and languages, not GUI apps). I just know it’s “adequate, just about”, as long as you don’t ask its crappy Text Suite to do any heavy lifting or don’t do anything that involves operating on lots of “objects” at once where it really starts to drop the ball (both for reliability and performance).
Well, no. The fact you have so much overlapping behavior in 3 adjacent properties is an obvious design/code smell. Figure out what it is you want to present to the user, in terms of what does the user expect to do with it, and implement that. That’s just good UI/UX design, no matter the medium (GUI, web-based protocol, Apple events, etc).
You should be able to achieve everything with a single property on application and or document classes:
selection : reference
What that selection actually is can be resolved during Get/Set operations. e.g.
get selection of document 1
can return a single multi-object reference, a list of single-object references, or a string. For my money, it is best to return a single multi-object reference if the selection is contiguous, as that’s vastly more efficient if the user is just going to pass it back to the app in another command, e.g.:
tell document 1
set selectedRows to selection
move selectedRows to end
set collapsed of selectedRows to true
end tell
or, if it’s just the one command actually:
tell document 1
move selection to end
end tell
The user can also change the selection by setting the property to a new reference:
tell document 1
set selection to rows 3 thru -8
end tell
And if the user does want a list of single-object references:
tell document 1
get every row of selection -- returns a list of `row` references
end tell
or a list of name properties:
get name of every row of selection -- returns a list of strings
Query-oriented UI FTW.
OTOH, if your app allows non-contiguous selections then you really have no choice but for get selection to return a list of single-object references. That said, you should still type it as reference so that users can write stuff like:
tell document 1
tell selection
move to end
set collapsed to true
end tell
end tell
It only resolves to a list when a get is performed. Even then, you still want to support query-oriented behavior up to the point where it’s finally crunched down to a list so that, e.g.:
get name of every row of selection
will still return a list of strings. (If the selection property is typed as a list, the user can’t do that; they have to get that list and then iterate it themselves. Tedious and slow both to code and to run.)
Now, when it comes to getting the selected rows’ text as a single contiguous string, best way to do that is by requesting the type in the get command:
get selection as string
In addition to being a built-in AS operator, as is also an optional parameter to get. It’s not normally shown in dictionaries, but will arrive in the core/getd AppleEvent record as the property keyAERequestedType. (I don’t recall if CS’s standard NSGetCommand implementation does anything with this parameter, never mind it doing something logical and helpful. You’ll have to dig into the docs to find out.)
…
Like I said at the beginning, implementing a JavaScriptCore API will likely be quicker and easier than implementing an AEOM UI; though perhaps I should have qualified that as “a good AEOM UI”, which allows the possibility of implementing a not-so-good one quite a bit quicker.
–
p.s. I will also leave this here:
It’s a paper by one of AppleScript’s original designers that sheds more light on how Apple event IPC and the AEOM works and why it was designed to work that way. Had Apple mgt not pissed off Cook and Harris right as AS 1.1 was going out the door, they might not have walked out immediately after it, taking all their institutional knowledge with them. And perhaps today we’d have top-notch comprehensive developer documentation, a high-quality app framework for implementing AEOMs that isn’t full of bugs, holes, and slooooow, and a few million enthusiastic app scripters across a half-dozen popular languages to make that app-side development all worthwhile. (Anyway, AppleScript’s authors are dead now, and AppleScript probably not far behind them, so caveat emptor, E&OE, YMMV, perfect is the enemy of good, etc, etc.)
It’s not brilliant, but you might find an explanation of how AEOM behaves from the client side, written with a programmer audience (Python3) in mind, a bit easier to grasp. It could probably have done with 8-foot neon flashing “It’s RPC plus Queries, not OOP!” text to really get the point across, but I guess I ran out of 8-foot neon flashing pixels that day so you’ll just have to take that as implicit. HTH
I’ve just posted an updated Bike with an updated scripting dictionary:
Lets say it was done medium quick
It’s not perfect, but I think much improved from what I started with. Most importantly I got rid of child row concept, and I think it’s much improved for that. Here’s what things look like with updated dictionary:
I wasn’t able to apply the expand/collapse/selection suggestions. I generally agree they are good, but it’s also not strait easy path for Cocoa Scripting and I have a bunch of other things I need to work on first. If needed I think I can come back to these with better implementations and just mark the existing properties as hidden.
Lots of future todo’s for Bike. I will be adding a lower level JavaScriptCore API among many other things. Anyway, thanks again for all the feedback.
collapse v : Collapse rows.
collapse document
at list of row : The rows to collapse.
[completely boolean]
expand v : Expand rows.
expand document
at list of row : The rows to collapse.
[completely boolean]
A document reference should not be passed as the direct parameter. The document object is already specified within every row X of document Y reference the user passes. Make the direct parameter list of row and remove the at parameter.
As an aesthetic improvement to these commands I would also rename the completely parameter. It’s a PITA to think of good names for Boolean parameters as AS automatically reformats a command such as:
expand aRow with completely
expand aRow without completely
Have a think about it. If you can replace that adverb with a suitable noun, code will read better. (The tidy choice would be “with[out] recursion”, but that’s far too programmer-y jargon.) Perhaps:
Verbose, but reads better in AppleScript’s weird pidgin.
You do still need add a read-only Boolean collapsed† property to the row class. This allows users to get a row’s current state.
You don’t need to worry about this property’s performance as the really important task—changing the expanded/collapsed state across multiple rows—is done by your fast commands. But it is important that users can observe a document’s current state and not be forced to blindly assume it.
–
† While you could call the property expanded and flip the boolean around, I think of false as signifying the resting state and true indicating an active state. As an outline user I would naturally assume all rows to be visible by default and must actively hide those rows I don’t want to see. Again, it’s a case of thinking “what is the user’s natural expectation” and shaping the UI to satisfy their prediction of what it is called and how it behaves.
Bike – MacInTouch https://www.macintouch.com/post/24139/bike/
Bike is a new outliner app for the Mac from Jesse Grosjean (Hog Bay Software) with a simple, friendly design built on a foundation of open file formats with support for large documents.