Dynamic Named Styles based on text or saved filter?

Hi,

Is it possible to through the UI or scripting to automatically apply a Named Style to a row if certain conditions are met?

For example use the Red style if the row contains the word “Overdue”. Use Green style if the word “Todo” exists, etc…

Another way could be to tie a saved filter to a named style.

Thx

Via script (which isn’t totally automatic; you have to trigger it by clicking run in Script Editor or placing it in your script menu and choosing it from there and/or attaching it to a keyboard shortcut) you can do something like this:

tell application "OmniOutliner"'s front document
	repeat with r in rows
		if r's name contains "Overdue" then ¬
			add named style ("Red") to r's style's named styles
		if r's name contains "ToDo" then ¬
			add named style ("Green") to r's style's named styles
	end repeat
end tell

If you’re placing Overdue and ToDo in another column you would need to reference that column instead.

For example, if you have a column named “Status” you could do this:

tell application "OmniOutliner"'s front document
	repeat with r in rows
		if r's cell "Status"'s value contains "Overdue" then ¬
			add named style ("Red") to r's style's named styles
		if r's cell "Status"'s value contains "ToDo" then ¬
			add named style ("Green") to r's style's named styles
	end repeat
end tell

SG

With the upcoming built-in JavaScript automation (testable now as discussed in that thread, but lacking documentation) you’ll be able to create an automation handler which notices when a row changes and automatically updates its format based on that row’s content.

For example, to make rows red when they contain the word “red” you’d declare a handler along these lines:

var _ = function(){
	var handler = new PlugIn.Handler(function(sender, item, column, state) {
		console.log("invoke; sender: ", sender);
		console.log("invoke; item: ", item);
		console.log("invoke; column: ", column);

		var outline = item.outline;
		for (idx in outline.columns) {
			var c = outline.columns[idx];
			// console.log("looking at " + c);
			// console.log("type is " + c.type);
			// console.log("vs  " + ColumnType.Text);
			if (c.type == Column.Type.Text) {
				var value = item.valueForColumn(c);
				// console.log("value " + value);
				if (!value) {
					continue;
				}
				var string = value.string;
				// console.log("checking string " + string);
				if (string.indexOf("red") >= 0) {
					item.style.set(Style.Attribute.BackgroundColor, Color.RGB(1.0, 0.5, 0.5, 1.0));
					// console.log("found red");
					return;
				}
			}
		}

		item.style.set(Style.Attribute.BackgroundColor, Color.RGB(1.0, 1.0, 1.0, 1.0));
		
		// var styles = selection.styles;
		// console.log("styles = " + styles);
	});
	
	return handler;
}();
_;

Sorry that we don’t have an actual working sample for you to try out and modify yet. The omni-automation.com website has mostly been focused on OmniGraffle automation (with a goal of having that ready to go when OmniGraffle 3 for iOS ships), but I’ll suggest to @Sal that we include an example along these lines when he shifts his attention to OmniOutliner and automation handlers.

For now, the script @SGIII posted above is a better answer since it’s an actual working solution (which is more useful than a not-yet-working proposal). Just wanted you all to know that you’ll be able to automate even more of this in the future! (As well as other handler-based logic like making a column be the sum of two other columns, etc.)

5 Likes

@kcase

That’s great news!

Examples of how to use will be particularly helpful.

SG

I’m struggling to adapt this script not to change the row, just to look and find a string of words and apply the named style to that string.

But this threat is helpful. Following close.

Thank you

@kcase

I’ll suggest to @Sal that we include an example along these lines when he shifts his attention to OmniOutliner and automation handlers

Pending documentation would it be possible to make available a few worked examples of OmniJS doing something in OmniOutliner?

Briefly experimented with the code above, which would do something really useful (color code according to content). It complains about “missing required user key …”

SG

Here is one sample that shows working with OmniOutliner and text. This looks for named styles in a document of the form “Keyword: Foo”, an then applies those named styles to any matching text.

The main script (written as an action):

var _ = function(){
	
	// Creating the action, a function is required for when the action is invoked.
	var action = new PlugIn.Action(function(selection) {
		var outline = selection.outline;
		var styles = wordToStyle(outline);
		
		// Recursively apply a function to the item tree.
		outline.rootItem.apply(function(item){
			applyStylesToItem(item, styles);
		});
	});
	
	// An optional function to specify when the action is valid, based on the current selection.
	// We operate on the whole document, so this currently disables the action if the document is empty.
	// It could also return false if there are no 'Keyword: ' prefixed named styles.	
	action.validate = function(selection) {
		return selection.outline.rootItem.children.length > 0;
	};
	
	// Some helper functions that are only visible to the action.
	
	// This gets applied to each item via the Item.apply() call above, passing the current item and the table of keyword styles.
	// NOTE: This is very slow, and doesn't remove old keyword ranges if you edit the text.
	function applyStylesToItem(item, styles) {
		var outline = item.outline;
		
		// Look at each Text column in the outline
		for (idx in outline.columns) {
			var c = outline.columns[idx];
			if (c.type == Column.Type.Text) {
				var value = item.valueForColumn(c);
				if (!value) {
					continue;
				}

			    // Consider every word in the text value
				var ranges = value.ranges(TextComponent.Words);
				
				for (rangeIdx in ranges) {
					var range = ranges[rangeIdx];
					var word = value.textInRange(range);
					
					// See if this matches a keyword					
					var style = styles[word.string];
					if (style) {
						// Add the style on this range if it doesn't already have it.
						var wordStyle = value.styleForRange(range);
						if (!wordStyle.influencedBy(style)) {
							wordStyle.addNamedStyle(style);
						}
					}
				}
			}
		}
	}
	
	// Build the table of keyword -> style. For any named style "Keyword: Foo", return an object with a property {"Foo": style}.
	function wordToStyle(outline) {
		var prefix = "Keyword: ";
		var result = new Object();
		
		var namedStyles = outline.namedStyles.all;
		for (idx in namedStyles) {
			var style = namedStyles[idx];
			if (style.name.indexOf(prefix) == 0) {
				var keyword = style.name.substring(prefix.length);
				result[keyword] = style;
			}
		}
		
		return result;
	}
	
	// Finally, return the action.
	return action;
}();
_;

when packaged as a .omnioutlinerjs plugin (here is a copy on Dropbox https://www.dropbox.com/s/9ed76f4int9tjkn/Apply%20Keyword%20Styles.omnioutlinerjs.zip?dl=0 which you can unzip and install in the folder under Automation > Plug-ins), a menu item shows up under Automation and selecting it will apply named styles to matching ranges of text:

This example as written is pretty basic, but hopefully shows off a bit of how to work with OmniOutliner. It’s also worth noting that we’re open to suggestions on additions to make your automation needs easier to accomplish!

5 Likes

@tjw

Thanks! I’ve downloaded and double-clicked and see it is installed in Scripts > Plug-ins. I’ve had a look at the package contents and at omni-automation.com to get a general idea.

What is the next step to actually use the plug-in? Just choosing ‘Apply Keyword Styles’ in the menu “doesn’t do anything” so presumably I’m missing something really basic about how to designate a string to search for and to style, the Horatio in your example.

SG

The action looks for named styles that have the form “Keyword: Foo” and then apply that style to any word “Foo” in the document. It’s kind of subtle in the screenshot, but you can see two named styles in the sidebar that are being picked up by the script.

@tjw

Ah, I see. I added named style with name as shown:

The plugin appears to be installed correctly.

But “nothing happens” in OO 5.0.5 test (v181.17 r288245)

I assume I am still missing something obvious. Advice?

SG

Whoops – I was testing with an internal build. I’ve merged a fix from the development branch to the 5.0.x release branch that is needed for the Item.apply() function to work correctly. This should show up in 5.0.5 builds numbered 288342 or higher. Sorry for the trouble!

@tjw

No trouble. Thanks to your example I have made it up the learning curve a little further, and look forward to trying it out in new builds.

The ability to “color code” (without user intervention) selected words or entire rows containing certain words will be a great thing to have.

SG

Thank you,

These is what I need look for a word and apply a style. I have a huge document, research, memo audios, notes… And sometimes I need to remember some things, OmniOutliner offers “Find string” and “replace” but I don’t need to replace, I just need to apply an specific format to a word.

At this moment is not working for me in the version 5.0.4 (v181.16 r287398).

@shinfu

At first I was puzzled about where/how to install but what worked for me here was to simply double-click the download (after double-clicking the zip). This installed Apply Keyword Styles.omnioutlinerjs in the the bowels of the Library at: Library > Containers > com.omnigroup.OmniOutliner5 > Data > Library > Application Support > PlugIns. Apply Keyword Styles then showed up in my Scripts menu (per the screenshot above).

I understand we need to wait until 5.0.5 build 288342 (or higher) for this to work.

SG

1 Like

Just thought I’d note that r288342 is now available on our public test page:

https://omnistaging.omnigroup.com/omnioutliner/

1 Like

Thanks for sharing useful information …

Thanks to @kcase and @tjw for this working example to help get us started. It requires the user to trigger the script by choosing Apply Keyword Styles from the menu, but is a great start.

SG

I’ve had a certain amount of luck using the Timer to automate tasks…
This one will find all rows that have a checked status and set the Date Completed column.
Once it’s instantiated, it will constantly check the document without having to re-run the “Action”

var _ = (function(){
	var timer = Timer.repeating(1, function(timer) {
			timer.count++;
			//console.log(timer.count);
			var col = document.outline.columns.byTitle('Date Completed');
			var items = document.outline.rootItem.descendants;
			for(i in items) {
				var item = items[i];
				if (item.topic === 'xyzzy') {
					timer.cancel();
				}
				var state = item.state;
				var date = item.valueForColumn(col);
				if (state === State.Unchecked && date) {
					console.log(item.topic + " Date unset");
					item.setValueForColumn(null, col);
				} else if (state == State.Checked && !date) {
					console.log(item.topic + " Date SET");
					item.setValueForColumn(new Date(), col);
				}					
			}
		});
	timer.count = 0;
	var action = new PlugIn.Action(function(selection, sender) {
		var alert = new Alert("Greg's Automation", "Automation started...");
		alert.show(function(){});
	});

	action.validate = function(selection, sender){
		return true;
	};
	
	return action;
})();
_;

// COPY & PASTE into editor app. EDIT & SAVE with “.omnijs” file extension.
/*{
	"type": "action",
	"targets": ["omnioutliner"],
	"author": "Greg Smith",
	"description": "Sets the Done Date when the item is selected.",
	"label": "Greg's Automation",
	"paletteLabel": "Greg's Automation"
}*/

@Sal @kcase @draft8 Was Handler ever implemented as described here, such that it would notice when a row changes and automatically update it. Would like to use it for conditional highlighting, e.g. a row contains a certain word in then turn it a certain color.

@SGIII, this is a first attempt at this issue.

Usage:

To attach the handler to the current Outline, type (in the Console):

const redHandler = PlugIn.find("com.unlocked2412.conditionalFormatting").handler('redHighlighting')

To detach the handler, type (again, in the console):

redHandler.remove()

2 Likes