omniJS – Item.state always returns State.Unchecked for parent nodes?


#1

OO 5.2 test (v184 r294107)

Item .state returns the expected [ State.Checked | State.Unchecked ] values for leaf rows, but only seems to return State.Unchecked (never State.Mixed or State.Checked) for parent nodes, regardless of their actual state and the state of their descendants.

Not sure if that is by design ?


#2

This is due to how summary values are implemented in OmniOutliner as a non-destructive calculation atop the internal model, which results in a few oddities like this. The underlying parent Item has its own value for the column, and the TreeNode stores a calculated value for display in the view. In the user interface, the parent’s value is not visible or otherwise accessible while a summary is enabled, but when accessing OmniOutliner’s model via a scripting interface some of this becomes apparent.

If you want to get the value for a calculated summary, you can use the valueForColumn() function on the TreeNode instead of the underlying Item.


#3

you can use the valueForColumn() function on the TreeNode instead of the underlying Item

Got it – thanks !


#4
function everyItemWhoseStateMatchesValue(stateValue){
	var matchedItems = new Array()
	mainNode = document.editors[0].rootNode
	mainNode.apply(function(node){
		nodeState = node.valueForColumn(document.outline.statusColumn)
		if (node != mainNode && nodeState == stateValue){
			matchedItems.push(node.object)
		}
	})
	return matchedItems
}

everyItemWhoseStateMatchesValue(State.Mixed)


#5

Build: 5.1.4 (v181.17.12 r297370)

Seems to error in 5.1.4 – Perhaps for a later build ?

Rob


#6

Yup, I’m using later build.


#7

Or, assuming macOS Sierra onwards and taking advantage of filter:

(() => {

    // itemsByState :: State -> [Item]
    function itemsByState(stateValue) {
        const
            e = document.editors[0],
            status = document.outline.statusColumn;

        return e.rootNode.object.descendants.filter(
            row => e.nodeForItem(row).valueForColumn(status) === stateValue
        );
    };

    return itemsByState(State.Mixed);
})();

A couple of notes:

  1. Wrapping omniJS code ‘as a module’ i.e. in (() => { … })() avoids collisions in the global name space, and potential clashes between successive experiments in the console. ( All value names bound inside the module vanish after execution, rather than lingering on undetected).
  2. === is always faster and less accident-prone than ==, which attempts some type conversions, sometimes with unexpected results.
  3. The draft above uses the slightly cleaner ES6 syntax (Sierra onwards – for an El Capitan version we can paste the code into the REPL at https://babeljs.io/repl/, which gives us the draft below)
(function () {

    // itemsByState :: State -> [Item]
    function itemsByState(stateValue) {
        var e = document.editors[0],
            status = document.outline.statusColumn;

        return e.rootNode.object.descendants.filter(function (row) {
            return e.nodeForItem(row).valueForColumn(status) === stateValue;
        });
    };

    return itemsByState(State.Mixed);
})();

#8

To test the value of module wrapping, we can try the following experiment in the Console.

First we define a constant:

const year = 2017;

Then we change our mind and instead type:

const year = 2018;

And we get a syntax error …

Module wrapping makes all declarations temporary (cleared at end of run) and avoids pollutions of the global name space which lead to this kind of clash and error message;

To see:

  1. How cluttered the global namespace already is, and
  2. how the names we bind outside module wrappers linger on, adding to the clutter, and leading to clashes, after run-time:


#9

What am I missing? Why not use rootItem instead of rootNode.object?

function itemsByState(stateValue) {
	var editor = document.editors[0],
	status = document.outline.statusColumn;

	return rootItem.descendants.filter(function(row){
		return editor.nodeForItem(row).valueForColumn(status) === stateValue;
	})
}
itemsByState(State.Mixed)

#10

Even better :-)


#11

Incidentally on ES6 JavaScript vs ES5, my understanding is that 10.12 is now the base macOS platform for OO 5.2,

so perhaps there’s really no longer any need to stick to ES5 idioms, for fear of excluding users, particularly when the ES5 forms are rather more noisy and verbose to read.

I would personally tend, for example, to use the arrow syntax in the filter here:

(() => {
    'use strict';

    // itemsByState :: OO State -> [OO Item]
    function itemsByState(stateValue) {
        const
            editor = document.editors[0],
            status = document.outline.statusColumn;

        return rootItem.descendants.filter(
            row => editor.nodeForItem(row)
            .valueForColumn(status) === stateValue
        );
    }

    return itemsByState(State.Mixed);

})();

and of course const has some advantages over var – it happens to be good for performance, but more importantly I think it’s a useful instrument of clarity to:

  1. make things constant by default,
  2. make things mutable only when you really have to,
  3. check that you do really have to.

Ancient mariners like us are used to keeping mental track of constant changes in the values attached to names, tho they still trip us up, but the advantage for beginners is that you do get a simpler mental model when mutability is reined back to the minimum necessary.

(and of course, given the JS crown jewels of filter, map and reduce, it’s not that often that you genuinely need the additional uncertainty (or slower performance) of a var)