1

Is there a way in JXA to get multiple properties from multiple objects with a single call?

For example, I want to get name and enabled property from menu items which can be done for each individual property as follows:

Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.name()
Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.enabled()

but is it possible to get them with a single function call? Something like:

Application("System Events").processes.byName('Finder').menuBars[0].menuBarItems.select('name', 'enabled')

I know, that I can iterate through the menuBarItems and collect properties from .properties() method, but this approach is too slow, that's why I'm looking for other options.

UPDATE

I'm looking for better performance, not for nicer syntax, i.e. I want properties to be retrieved in a single call to System Events.

Vader
  • 3,675
  • 23
  • 40
  • 1
    Nope, while both Apple event IPC and SQL are query-based, there’s no direct equivalent to SQL’s `SELECT a,b,c,… FROM…`. A single Apple event can get a single property from multiple objects, but not multiple properties at once. (You can always get the `properties` property from multiple objects, though that may have its own performance overheads.) [This](https://bitbucket.org/hhas/nodeautomation/src/default/doc-src/13-performance-issues.md) may help clarify (caveat various AE capabilities that work correctly in AppleScript and nodeautomation but are broken in JXA). – foo Jul 26 '19 at 18:17
  • @foo I also came to a conclusion that it is not possible, but thought that maybe I'm missing something as docs are far from prefect. Thank you for confirming that – Vader Jul 28 '19 at 19:17
  • There’s no technical reason it *couldn’t* be done, but AppleScript can’t express it so apps don’t provide it. Best you can do is to `get name of `, then `get enabled of `, then zip those lists together, but both AS and JXA are a pain for that. e.g. In Python, I’d do `from appscript import *; ref = app('System Events').processes['Finder'].menu_bars[1].menu_bar_items; zip(ref.name(), ref.enabled())` which gives `[('Apple', True), ('Finder', True), ('File', True), ('Edit', True),…]` in just two Apple events. But I think CJK’s C API solution will prove faster. – foo Jul 28 '19 at 19:58
  • The other caveat, of course, is any code that manipulates GUI controls directly is inevitably knotty, brittle, not too portable, and an ever-increasing PITA where security is concerned. You never say what the problem you’re trying to solve is, but if it’s something that can be done without resorting to GUI Scripting via System Events or lower-level Accessibility APIs, then it’s almost certainly best to do it that way instead. – foo Jul 28 '19 at 20:05
  • @foo that's UI testing and I agree it is PITA :) – Vader Jul 28 '19 at 20:20
  • @foo in manual AppleScript I can use `get {name, url} from currentTab` and I'll get those properties in one pass, not two. If I do those two separately using `Objective-C` it takes exactly twice as long. – Lucas van Dongen Apr 19 '20 at 15:18
  • Old post is old, but anyway… Cutesy AppleScript syntax aside, that’s still sending two separate `get` events (`get name of tab…` and `get url of tab…`) one after the other. It would be great if multiple properties could be specified in a single reference, c.f. SQL `SELECT name, url FROM…` and I can’t think of any technical reason it couldn’t be (plus it’d be faster, more convenient, and avoid potential race issues), but as neither AppleScript nor existing “AppleScriptable” apps do so it’s really academic. If ObjC is twice as slow, that’s because ScriptingBridge is rubbish. – foo Apr 19 '20 at 17:50

1 Answers1

3

I'd probably do it like this:

sys = Application('com.apple.systemevents');
FinderProc = sys.processes['Finder'];
FinderMenuBarItems = FinderProc.menuBars[0].menuBarItems();


Array.from(FinderMenuBarItems,x=>[x.name(),x.enabled()]);

By first converting the object to an array, this allows one to map each element and retrieve the desired properties for all in one go. The code is split over several lines for ease of reading.

EDIT: added on 2019-07-27

Following on from your comment regarding Objective-C implementation, I had a bit of time today to write a JSObjc script. It does the same thing as the vanilla JXA version above, and, yes, it clearly makes multiple function calls, which is necessary. But it's performing these functions at a lower level than System Events (which isn't involved at all here), so hopefully you'll find it more performant.

ObjC.import('ApplicationServices');
ObjC.import('CoreFoundation');
ObjC.import('Foundation');
ObjC.import('AppKit');

var err = {
    '-25211':'APIDisabled',
    '-25206':'ActionUnsupported',
    '-25205':'AttributeUnsupported',
    '-25204':'CannotComplete',
    '-25200':'Failure',
    '-25201':'IllegalArgument',
    '-25202':'InvalidUIElement',
    '-25203':'InvalidUIElementObserver',
    '-25212':'NoValue',
    '-25214':'NotEnoughPrecision',
    '-25208':'NotImplemented',
    '-25209':'NotificationAlreadyRegistered',
    '-25210':'NotificationNotRegistered',
    '-25207':'NotificationUnsupported',
    '-25213':'ParameterizedAttributeUnsupported',
         '0':'Success' 
};

var unwrap = ObjC.deepUnwrap.bind(ObjC);
var bind = ObjC.bindFunction.bind(ObjC);

bind('CFMakeCollectable', [ 'id', [ 'void *' ] ]);
Ref.prototype.nsObject = function() {
    return unwrap($.CFMakeCollectable(this[0]));
}

function getAttrValue(AXUIElement, AXAttrName) {
    var e;
    var _AXAttrValue = Ref();

    e = $.AXUIElementCopyAttributeValue(AXUIElement,
                                        AXAttrName,
                                        _AXAttrValue);
    if (err[e]!='Success') return err[e];

    return _AXAttrValue.nsObject();
}

function getAttrValues(AXUIElement, AXAttrNames){
    var e;
    var _AXAttrValues = Ref();

    e = $.AXUIElementCopyMultipleAttributeValues(AXUIElement,
                                                 AXAttrNames,
                                                 0,
                                                 _AXAttrValues);
    if (err[e]!='Success') return err[e];

    return _AXAttrValues.nsObject();
}

function getAttrNames(AXUIElement) {
    var e;
    var _AXAttrNames = Ref();

    e = $.AXUIElementCopyAttributeNames(AXUIElement, _AXAttrNames);
    if (err[e]!='Success') return err[e];

    return _AXAttrNames.nsObject();
}


(() => {
    const pid_1        = $.NSWorkspace.sharedWorkspace
                                      .frontmostApplication
                                      .processIdentifier;   
    const appElement   = $.AXUIElementCreateApplication(pid_1);
    const menuBar      = getAttrValue(appElement,"AXMenuBar");
    const menuBarItems = getAttrValue(menuBar, "AXChildren");

    return menuBarItems.map(x => {
        return getAttrValues(x, ["AXTitle", "AXEnabled"]);
    });
})();
CJK
  • 5,732
  • 1
  • 8
  • 26
  • But this still makes separate calls to `name` and `enabled`. My goal is not to have better syntax, but to reduce number of calls to `System Events` for performance reasons – Vader Jul 26 '19 at 12:08
  • This was not at all clear from your question. Your comment here is very well articulated, and ought to have been the wording you chose for your main question. My feeling on the problem as I now understand it is that it would not be possible to reduce how many calls are made. JavaScript `objects` are passed by reference, so will necessarily get evaluated with each retrieval of a `property`. If performance is your concern, you might be better using an Objective-C framework to make the API call at a lower level. – CJK Jul 26 '19 at 12:21
  • Ok, I updated the question to make it clearer. As about Objective-C, I'd be happy to use it, but seems like there is no way to access external application from it without using the scripting bridge, which will lead to the same performance issues. – Vader Jul 26 '19 at 12:31
  • I've added a JSObjC version of the script as a proof-of-concept that you will hopefully find useful. I agree that it would be a fruitless endeavour if one wanted to used Objective-C merely to then use the scripting bridge to execute an AppleScript and call out to _System Events_. Thankfully, that's not necessary. – CJK Jul 27 '19 at 13:18
  • Thank you! This seems to be much faster comparing to pure JXA version. – Vader Jul 28 '19 at 19:14