omniJS - plugin actions unavailable to console and external scripts

UPDATE: solved and understood now, and it seems that there will soon be a new example on the website

According to the omni-automation.com website:

PlugIn actions are available to be called by other actions and external Omni Automation scripts.

This would be very useful, but is currently prevented by what looks like a bug in the selection argument passed to plugin actions.

The selection argument is typically needed by code called by both the .validate() and .perform() methods of a PlugIn.action.

When code is evaluated within a plugin, the selection argument is passed correctly both to the perform method and to the validate method.

When, however, code is called from:

  1. Other scripts
  2. The console

The validate method is still passed a well-formed Selection object,
but the perform method is only getting an undefined value, which makes code choke on lack of access, through the selection, to canvas, view, graphics, etc …

Sample test code defining an action and its validation

var _ = (function () {
    var cycleTidy = new PlugIn.Action(function (selection) {
        console.log("Plugin action selection argument: " + typeof selection)
        this.tidyTreeLib.tidyLayout(
            selection, ['Top', 'Left', 'Bottom', 'Right']
        );
    });

    return (
        cycleTidy.validate = function (selection) {
            console.log("Validation fn selection argument: " + typeof selection)
            return selection.canvas !== null;
        },
        cycleTidy
    );
})();
_;

When this is called by the plugin itself, the selection argument of both methods is passed a javascript object

But if we try to call the action from an external script, or from the console, we hit a bug - the perform method is passed an undefined value instead of the selection object, and our code quickly chokes …

(Note that the validate() method does seem to be passed an object when called from the terminal or an external script, so perhaps there is hope of a fix)

OG7.4 build

OmniGraffle 7.4 test (v179.5 r291208 built Jun 29 2017)

( It looks like the same problem may be arising on iOS with OG3 test builds, but the OG3 omniJS console.log() is losing messages when script is run from a URL, so its a little harder to check )

(This is the first glitch for which I haven’t found any workaround – but that could just be summer heat …)

Thanks – I’ll take a look. Do you have an example of how you are calling the action from (for example), the console?

If I do something like PlugIn.all[0].actions[0].perform(), then the receiving plug-in action does get undefined as its first argument (since I didn’t pass any along as the argument to perform). But, maybe you have a different pattern that fails that I’m not thinking of.

OK, the issue is real, but there is an obvious workaround for the moment - a mutation hack with a more global variable, allowing .validate() to administer first aid to .perform()

In the console here:

p = PlugIn.find('com.complexpoint.OmniGraffle')
a = p.action('cycle')
a.perform()

-->  Plugin action (selection) argument: undefined

And as a workaround, I can get .perform() to steal the argument passed to .validate():

var _ = (function () {
    var oSeln;  // MUTATION HACK - this can be set by .validate(), and then
                // borrowed by .perform() if necessary,
                // i.e. if the bug has passed an undefined to .perform()

    var cycleTidy = new PlugIn.Action(function (selection) {
        console.log("Plugin action (selection) argument: " + typeof selection)
        this.tidyTreeLib.tidyLayout(
            (selection || oSeln), ['Top', 'Left', 'Bottom', 'Right']
        );
    });

    return (
        cycleTidy.validate = function (selection) {
            console.log("Validation fn (selection) argument: " + typeof selection);
            oSeln = selection;
            return selection.canvas !== null;
        },
        cycleTidy
    );
})();
_;

Or perhaps just belt and braces - bind our own local name to the selection object, in case the execution context hasn’t done that. (The plugin context does take care of it, but the console and URL-execution picture seems mixed)

var _ = (function () {
    // Explicitly bind a local name to the selection as a fall-back
    // in case the execution context doesn't pass values to these functions

    var oSeln = document.windows[0].selection;

    var cycleTidy = new PlugIn.Action(function (selection) {
        //console.log("Plugin action (selection) argument: " + typeof selection)
        this.tidyTreeLib.tidyLayout(
            (selection || oSeln), ['Top', 'Left', 'Bottom', 'Right']
        );
    });

    return (
        cycleTidy.validate = function (selection) {
            //console.log("Validation fn (selection) argument: " + typeof selection);
            return (selection || oSeln).canvas !== null;
        },
        cycleTidy
    );
})();
_;

The sample code shown at:

https://omni-automation.com/actions/index.html

var _ = function(){
	var action = new PlugIn.Action(function(selection){
		// action code
	});

	// result of validation routine determines if action menu item is enabled. Return true to enable, false to disable.
	action.validate = function(selection){
		return true
	};

	return action;
}();
_;

Appears to indicate that selection is an argument supplied by default (rather than manually by the scripter)

That assumption seems to work smoothly in the case of plugin execution, and half-work in the case of console and URL execution.

‘Half-work’ in the sense that it works for .validate(), but fails for .perform()

If we look at the code example at the foot of https://omni-automation.com/actions/index.html

we see .validate.call() followed by .perform.call()

neither with a user-supplied argument – compounding the impression that selection is intended to be an assumed default argument that is supplied to the called function.

This does seem to be borne out by the behaviour of .validate() even in a console or URL context, and by the behaviour of both when called without explicit arguments within plugins.

Ah, thanks! This will work if the validate() or perform() function doesn’t actually look at the selection object, but that is going to be relatively rare. I’ll ping Sal about updating the documentation.

The other thing that would be useful out be to add a Editor.selection accessor for cases where you don’t already know the selection (but if you have having one action call another, you could pass the selection along).

Well, it works even if those functions actually do look at the selection object (when code is executed in a plugin)

Are we perhaps missing something that the plugin context does which the console/url contexts don’t do ?

See this code for example - these actions do look at the selection object successfully, and no argument is passed by the user to validate() or to perform()

OK, I get it – the plugin menu and toolbar button (and other event-handling) mechanisms take care automatically of supplying an argument to those functions, whereas the user would need to take of that outside the plugin menu and toolbar context.

So I can get to this simplified calling of Plugin actions from outside:

by slightly rewriting the action-defining closure:

var _ = (function () {

    var // The activeSeln() function below is available within the closure for
        // calls to .validate() and .perform()
        // sparing the scripter from manually passing a selection
        // argument to these methods when calling an action from an external
        // script.
        // (Plugin event-handlers supply a selection reference automatically
        //  when an action is called from a plugin menu or toolbar )

        activeSeln = function () {
            return document.windows[0].selection;
        },

        cycleTidy = new PlugIn.Action(function (selection) {
            var oSeln = (selection || activeSeln());
            this.tidyTreeLib.tidyLayout(
                oSeln,
                ['Top', 'Left', 'Bottom', 'Right']
            );
            return oSeln.canvas.layoutInfo.direction;
        });

    return (
        cycleTidy.validate = function (selection) {
            return (selection || activeSeln())
                .canvas !== null;
        },
        cycleTidy
    );
})();
_;

Here are templates for creating OmniGraffle and OmniOutliner actions that can be called “internally” and “externally”. Note that since OmniOutliner currently doesn’t offer the ability to create a selection object via scripting, I’m using selectedNodes of the Editor class to generate an array of selected items.

// Template for “externally callable” OmniGraffle action
var _ = function(){
	var action = new PlugIn.Action(function(selection){
		if (typeof selection == 'undefined'){selection = document.windows[0].selection}
		// action code
	});

	action.validate = function(selection){
		if (typeof selection == 'undefined'){selection = document.windows[0].selection}
		// check for selected graphics
		if (selection.graphics.length > 0){return true} else {return false}
	};

	return action;
}();
_;

// Template for “externally callable” OmniOutliner action
var _ = function(){
	var action = new PlugIn.Action(function(selection){
		if (typeof selection == 'undefined'){
			// convert nodes into items
			nodes = document.editors[0].selectedNodes
			selectedItems = nodes.map(function(node){return node.object})
		} else {
			selectedItems = selection.items
		}
		// action code
	});

	action.validate = function(selection){
		// check for selected items
		if (typeof selection == 'undefined'){
			if(document.editors[0].selectedNodes.length > 0){return true}else{return false}
		} else {
			if(selection.items.length > 0){return true} else {return false}
		}
	};

	return action;
}();
_;

And a function for calling an action externally. If has a lot of error checking and can be slimmed down, but you get the idea:

function performPlugInAction(PlugInID, actionName){
	var aPlugin = PlugIn.find(PlugInID)
	if (aPlugin == null){throw new Error("Plugin is not installed.")}
	var actionNames = aPlugin.actions.map(function(action){
		return action.name
	})
	if(actionNames.indexOf(actionName) == -1){
		throw new Error("Action “" + actionName + "” is not in the PlugIn.")
	} else {
		if(aPlugin.action(actionName).validate()){
			aPlugin.action(actionName).perform()
		} else {
			throw new Error("The action “" + actionName + "” is not valid to run.")
		}
	}
}
performPlugInAction('com.NyhthawkProductions.OmniGraffle', 'changeColorOfSelectedGraphics')

I’ve updated the website to include action templates for both OmniGraffle and OmniOutliner that are callable from external scripts. Thanks to draft8 for the excellent sleuthing!

https://omni-automation.com/actions/index.html

1 Like