Q: Set stroke color of a set of lines using JavaScript for Automation?

osascript economises on Apple Event traffic and performs less sluggishly if we work with references to sets/collections of objects, batch-reading and batch-setting properties across these sets, rather than fetching and reading/mutating one object at a time.

In Applescript, to batch-read the colors of all lines on a layer, for example, we can write:

tell application "OmniGraffle"
    tell canvas of front window
        tell front layer
            stroke color of lines
        end tell
    end tell
end tell

-- Returns:
-- {{0, 0, 65535}, {0, 0, 65535}, {0, 0, 65535}, {65535, 0, 65535}, {65535, 0, 65535}, {65535, 0, 65535}}

The JXA translation of this for batch-reading properties would be:

Application('OmniGraffle')
.windows.at(0).canvas
.layers.at(0)
.lines.strokeColor();

// Returns:
[[0, 0, 1], [0, 0, 1], [0, 0, 1], [1, 0, 1], [1, 0, 1], [1, 0, 1]]

Even more usefully, osascript allows for batch-setting of a property across a collection. To make all the referenced lines blue, for example, we can write, in AppleScript:

tell application "OmniGraffle"
    tell canvas of front window
        tell front layer
            set (stroke color of lines) to {0, 0, 1}
        end tell
    end tell
end tell

The question is, what is the JavaScript for Automation translation for setting a property like this ?

(across a reference to a filtered collection of objects, without the much slower fetching of all objects and iterating through them, setting properties object by object)

If we try the obvious translation:

Application('OmniGraffle')
.windows.at(0).canvas
.layers.at(0)
.lines.strokeColor = [0, 0, 1];

We get an error, and the properties are not set.

What would be the correct syntax to batch-set the color of all lines on an OmniGraffle 6 layer ?

Or to put it another way, what type of arguments to the .setProperty() method on collections like .lines and .shapes (and on sets filtered by .whose()) allow us to set a property value across those collections ?

(The fetch all and set one by one approach might be:

	lines().forEach(function (x) {
		x.strokeColor = [0, 0, 1];
	});

but this involves a lot more traffic across the automation interface, and will be much slower.

How much slower ?

243 seconds vs 6 seconds, for example.

In a document with 186 lines, I ran a batch-set and a line-by-line version of repeatedly changing the line colors (in AppleScript)

Using a very rough-grained timer, the batch-set version took about 6 seconds each time I ran it on my system.

The line by line version consistently took more than 240 seconds, over 1800 threads, and over a gigabyte of RAM.

Script (comment out one version and test the other)

tell application "OmniGraffle"
    tell canvas of front window
        tell front layer
            set dteBefore to current date
            
            repeat with i from 1 to 1000
                
                -- SLOW VERSION  ( 1 line at a time ) -- 243 seconds
                repeat with oLine in lines
                    set (stroke color of oLine) to {0, 0, 1}
                end repeat
                
                -- FAST VERSION ( batch setting )
                -- set (stroke color of lines) to {0, 0, 1}  -- 6 seconds
                
            end repeat
            
            set dteAfter to current date
            
            return (dteAfter - dteBefore)
        end tell
    end tell
end tell

Emailed this question to Support a week ago, but haven’t heard back. Any thoughts ?

Sorry, sounds like there was a misunderstanding about what you were expecting! Looks like they took your message to be a bug report, not a question, and filed it as such to see if there is anything we can fix in the app to make this work. (They replied that afternoon letting you know they’d filed the issue, hopefully you at least saw that reply?)

That said, it looks to me like this problem is a limitation of JXA, unless there’s just something we’re both missing. I see the same thing when scripting TextEdit:

app = Application("TextEdit")
doc = app.documents.byName("ScriptTest")

// This works fine
doc.text = "Green\nYellow\nRed\n"
doc.text.paragraphs[0].color = 'green'
doc.text.paragraphs[1].color = 'yellow'
doc.text.paragraphs[2].color = 'red'
doc.text.paragraphs.color() // Returns [[0, 1, 0], [1, 1, 0], [1, 0, 0]]

// This fails with Error -10002: Invalid key form.
// doc.text.paragraphs.color = ['green', 'yellow', 'red'];

I tried searching the documentation, rereading the release notes for JXA from 10.10 and 10.11, and reviewing the WWDC 2014 session, but didn’t find any definitive answer about setting a property from JXA for an object specifier representing multiple objects. Maybe it would be worth asking about on Apple’s AppleScript-Users mailing list? I can’t guarantee a response, of course, but some of the developers who work on JXA do seem to answer questions about it there from time to time—and I suspect they’re the only people who are in a position to be able to authoritatively answer this question.

(If it turns out there’s something we could be doing in our implementation to make this work, I’d sure love to hear about it! But since it already works just fine from AppleScript, and fails identically in TextEdit—and since JXA’s Apple Events look just like AppleScript’s Apple Events from the point of view of a scripted app—I suspect this is a language problem where it’s not producing the proper incantation of Apple Events rather than an app implementation problem.)

P.S. — Maybe it would work to write a tiny script library in AppleScript which you call from JXA for just that sort of batch operation?

1 Like

Thanks for looking into that (and I’m sorry that I buried the question too deeply in the discussion for it to be noticed :-)

The fact that the interface has a .setProperty() method on collection specifiers does suggest that batch setting is part of the spec - intended as a pair to .getProperty(), which batch-reads happily, and in parallel to the equivalent operation in AppleScript.

I will see if the mailing list yields anything.

PS I would guess that your code should probably read

doc.text.paragraphs.color = 'red';

rather than trying to assign an array of properties. The pattern in AppleScript is to map a single value across all the objects in the collection.