Building long-wished-for custom features using Omni Automation

Earlier today, a customer mentioned to me some of the features they’ve long wished were in OmniOutliner. With their permission, here’s their list:

  • strikethrough - ability to add strikethrough shortcut in the toolbar
  • ability to multiply and divide
  • ability to add/ multiply/ divide and add check-box to a subset of an OmniOutliner document (for example, I may want to add check box to a particular sub-section and do some math on another sub-section)
  • keyboard shortcut to move child nodes from one section to another (for example, I have two parent nodes: doing and done. I have an item called “task 1” under doing; once I’ve completed the task, I would like to be able to click a button or keyboard shortcut and send it to the done node.)

It’s a great list! I can certainly see use cases for each of these things.

Part of the reason we’ve put such an emphasis on Omni Automation is to make it possible for people to extend the app to implement features like these, so I thought I’d see how far that automation currently gets us!

Going through these requests one by one:

  • strikethrough - ability to add strikethrough shortcut in the toolbar

You can create an Omni Automation script which adds strikethrough which you can add to your toolbar. There’s some generic boilerplate (that the app can build for you when you use the “Add Action” item in the Automation Console toolbar), but the code at the heart of that script would be:

let editor = document.editors[0];
let selection = editor.selection;
let style = selection.styles[0];

style.set(Style.Attribute.StrikethroughStyle, UnderlineStyle.Single);
  • ability to multiply and divide

I took this to mean something like “Column C = Column A * Column B”. If we wanted to provide this as a built-in feature, the design issue here would be to figure out what this would look like in a general-purpose outlining interface. But if we focus on implementing the functionality rather than designing an interface, this can be easily expressed today in an Omni Automation script, like so:

let columnA = columns.byTitle("A");
let columnB = columns.byTitle("B");
let columnC = columns.byTitle("C");
let editor = document.editors[0];
let nodes = editor.selectedNodes;

nodes.forEach(node => {
    let result = node.valueForColumn(columnA).multiply(node.valueForColumn(columnB));
    node.setValueForColumn(result, columnC);
});

This script can be assigned to a keyboard shortcut or toolbar item so that it’s never more than a keystroke or mouse click away.

  • ability to add/ multiply/ divide and add check-box to a subset of an OmniOutliner document (for example, I may want to add check box to a particular sub-section and do some math on another sub-section)

Unfortunately, this request goes beyond something a script can do, because it changes the shape of the document in a way that OmniOutliner doesn’t yet support. We refer to this internally as “multi-schema support,” where some portions of an outline would have different columns than other portions of the outline.

The tricky part of implementing this is not really the data model—we solved that while building OmniFocus. What’s tricky about this is figuring out how to clearly present this to the user, both for constructing such an outline and for using it.

  • keyboard shortcut to move child nodes from one section to another (for example, I have two parent nodes: doing and done. I have an item called “task 1” under doing; once I’ve completed the task, I would like to be able to click a button or keyboard shortcut and send it to the done node.)

I’m not sure how we would present this functionality in a general outlining interface, but the logic as described here is fairly simple—which once again makes it a good candidate for a custom script. (In fact, the Kinkless GTD scripts which inspired OmniFocus did something a lot like this.)

A script which takes the currently selected items and moves them into the “Done” section looks like this:

let topItems = rootItem.children;
let doneParent = topItems.find(item => item.topic == "Done");
let editor = document.editors[0];
let selection = editor.selection;

selection.outline.moveItems(selection.items, doneParent.end);

I hope the above examples make it clear why we’ve invested so much attention in Omni Automation! Building this level of automation and making it work across all Mac, iPad, and iPhone was a big undertaking—but the benefit of all this is that it makes it possible for users to customize our apps to meet their exact needs (rather than having us keep attempting to provide one-size-fits-all solutions that don’t quite meet anyone’s exact needs).

As we saw above, there are gaps: we were able to write simple scripts to implement 3 of these 4 requests, but we couldn’t write a script to make the app do something it truly doesn’t support (in this case, different sets of columns in different rows). But if you’re trying to make it easier to do something the app does already do, Omni Automation can help by making that work much easier.

(Each of the above scripts were tested in the Omni Automation console in OmniOutliner. It should be easy to adapt each of the above code samples for use in plug-in actions.)

Enjoy!

4 Likes

Great ideas Ken! As a follow-up, example plug-ins for applying and removing strikethrough have been added to the OmniOutliner section of the Omni Automation website:
https://omni-automation.com/omnioutliner/style.html#strikethrough

And here’s interesting tidbit: because plug-ins pass the selection object and its properties to the script automatically, the basic code is reduced to two lines!

var style = selection.styles[0]
style.set(Style.Attribute.StrikethroughStyle, UnderlineStyle.Single)

Also, here’s a script from the OmniOutliner section of the Omni Automaton website that sums a column of numbers:
(bottom of page)
https://omni-automation.com/omnioutliner/column.html

var column = columns.byTitle('Q4')
if (column && column.type === Column.Type.Number){
	var cellValues = rootItem.children.map(item => {
		return item.valueForColumn(column)
	})
	var total = Decimal.zero
	cellValues.forEach(value => total = total.add(value))
	console.log(Number(total.toString()))
}

Or, for a direct reduction of all the separate numbers in a list to a single total:

(() => {
    // Adding 'use strict' allows for
    // more helpful messages from JavaScript.
    'use strict';

    let q4 = columns.byTitle('Q4');

    let summation = (q4 && q4.type === Column.Type.Number) ? (
        rootItem.children.reduce(
            (runningTotal, item) => {

                // Any numeric value in this cell ?
                let value = item.valueForColumn(q4);

                // No change if the cell is empty, but
                return null === value ? (
                    runningTotal

                // otherwise the running total increases.
                ) : runningTotal.add(value);
            },

            // Starting value for the running Total.
            Decimal.zero

        ).toString()
    ) : 'Q4 column not found in front document.';

    return isNaN(summation) ? (
        summation          // Message string, or
    ) : Number(summation); // Numeric value.
})();

This is very interesting, thanks. I noticed, however, that adding plugins seems to decrease performance with really long outlines which is usually the case of my files. Is this to be expected?

Also, please consider some of the other requests in the forum that cannot be achieved via scripting, such as optionally displaying child nodes when applying a filter.

As in the Schachtürke ?

Forgive me, but I’m not sure of what you’re asking. Please explain. Cheers.

Oh! “Automaton” instead of “Automation.” LOL! I’m running on automatic sometimes too. ;-)

2 Likes