Wednesday, October 24, 2012

Extending Orion's Settings page with plugin settings

Things are pretty wild in the Orion world right now. We're coming up fast on our 1.0 release (**balloons fall from ceiling**). So given how busy we all are, this seems like a great time to stop fixing important last-minute bugs and instead write a leisurely blog post.

Here I'll explain a new feature that landed in Orion 1.0M1: plugin settings. Plugin settings provide a way for your Orion plugin to contribute settings to Orion's Settings page. You tell Orion about your setting, and it generates a user interface for manipulating your setting's value. That user interface lives on Orion's Settings page. To receive updates about the value of a setting, your plugin registers a service that will be invoked by the Orion platform to deliver notifications.

To show how this looks from a user's point of view, check out this predefined setting that ships with Orion 1.0:

JSLint validation (click for larger).

This setting lets you customize the options passed to the JSLint validator that checks your JavaScript files for problems. You can turn off annoying validation behavior globally by putting your preferred flags in here — for example, filling in eqeqeq:false will tell JSLint to tolerate JavaScript's type-coercing == and != operators without complaining*.

Naturally, this setting is contributed by Orion's JSLint Plugin itself, which ships bundled with Orion. This is where things get a little more interesting: if you went ahead and hacked your Orion environment to remove the JSLint Plugin, this setting would disappear from the UI as well. Makes sense, right? This also emphasizes a fact that I think will become increasingly important as Orion matures: plugins are not one-trick ponies (or one-service ponies, to be exact). Rather, they are ponies that can include a whole package of related functionality supporting a given task or feature set.

A plugin.

OK, back to settings. Here's the second setting I mentioned: the New Tab/Same Tab option.

New Tab/Same Tab setting
New Tab/Same Tab (click for larger).

This thing controls whether the links in the Navigator (and a few other places) open in a new browser tab. You may recall a similar option from Orion 0.5, but back then it was implemented in a rather ad hoc way, and now it's a plugin setting, which is much cooler. Note how this setting gets a drop-down menu: this is how our generated UI presents a setting whose value is restricted to an enumeration of discrete options.

Show me the code, already

Note: I've omitted a few lines of boilerplate plugin code in these examples. If you're unfamiliar with writing Orion plugins, check out the Simple Plugin Example from the Orion Developer Guide.

So how do you, the plugin author — (if you're not a plugin author, pretend you are) — how do you write one of these plugin settings? Like much of Orion, the heavy lifting happens declaratively through service properties. To contribute a setting, we register a service with some properties telling the Orion Settings page about it. Here's what the code for that service registration looks like:

pluginProvider.registerService("orion.core.setting", {}, { settings: [{ pid: "nav.config", name: "Navigation", category: "general", properties: [ { id: "links.newtab", name: "Links", type: "boolean", defaultValue: false, options: [ {value: true, label: "Open in new tab"}, {value: false, label: "Open in same tab"} ] }] }] });
Service registration for the "New Tab" setting.

You should be able to infer the meaning of most of the fields here by comparing the code to the picture above. But do note that:

  • A single service registration can contribute many settings. (Here, ours contributes just one, 'nav.config').
  • A single setting can contain several properties. (Here, ours has just one, 'links.newtab').
  • The identifier for a setting is known as a PID.

For more details about this service API, see its entry in the Orion developer guide.

Staying up to date

Shoving your setting into the UI is all well and good, but it's not very useful if nobody cares when its value gets changed. For a setting to be useful, it has to affect the world somehow. That's where the orion.cm.managedservice extension point comes in. Your plugin implements one of these Managed Services in order to receive updates from Orion about the setting.

The API for receiving an update is very simple. It looks like this:

updated: function(properties)
Interface of orion.cm.managedservice.

That's it: just one method with the signature updated(properties). The framework calls this method to notify your service of a setting change, passing the values of all your setting's properties in the properties object. Like all service calls in Orion, this happens asynchronously. In addition to that method, your service registration must have a property named 'pid', giving the PID (that is, the identifier) of the setting that it's interested in. Here's what a minimal ManagedService implementation looks like:

pluginProvider.registerService("orion.cm.managedservice", { updated: function(properties) { // The "nav.config" setting was updated } }, { pid: "nav.config" });
Minimal ManagedService for the "nav.config" PID.

However, the real power of this stuff comes in when your Managed Service plays several roles. In Orion's service framework, a single service can be registered under several service names — or in other words, a single service may implement several extension points. Think of orion.cm.managedservice, then, as an optional interface that can be implemented by an existing service to make it configurable by the framework.

For example, in the case of the "JSLint validation options" setting I mentioned earlier, we initially started out with just a validation service — ie. a service with the name "orion.edit.validator". When I wanted to make it configurable via the setting, I added the service name "orion.cm.managedservice", along with the pid property and the updated() method to fulfill the ManagedService contract. The final result ends up looking like this:

var myOptions; pluginProvider.registerService(["orion.cm.managedservice", "orion.edit.validator"], { updated: function(properties) { myOptions = properties.validationOptions; }, checkSyntax: function(title, contents) { // Validate using our options return JSLINT(contents, myOptions); } }, { pid: "jslint.config", contentType: ["application/javascript"] // This is for orion.edit.validator });
A configurable JSLint validator (simplified for clarity).

As an added bonus, the framework guarantees that, when your service is "managed" (ie. when it implements orion.cm.managedservice), its updated() method will be called to deliver the setting's value before any of the service's other methods are called. So in the code above, our validator knows that myOptions will be set before checkSyntax is invoked to validate a file. This is particularly nice because most Orion plugins are activated lazily (that is, only when one of their services needs to be called), and plugin authors would otherwise have deal with the possibility that one of their service methods might be invoked before the service had received its configuration. As is, however, the framework saves you from having to worry about that.

Under the hood

(This section talks about internal details, which aren't necessary to use the Settings API. Skip it if you're not interested.)

If you've worked with OSGi frameworks before, you may be familiar with the OSGi concepts of managed services and metatypes. Orion has both Managed Services and Metatypes, which are similar to OSGi's (albeit greatly simplified), and they form the low-level building blocks upon which the Orion Settings API is implemented.

Orion manages Managed Services using a service called ConfigAdmin (another borrowed OSGi-ism). The ConfigAdmin is provided by the Orion platform (platform being a convenient word to describe the low-level plugin and service plumbing). The ConfigAdmin starts up very early in the page's lifecycle, and from then on, monitors the registration of Managed Services and calls their updated() methods when necessary.

A Metatype describes the shape of an object: that is, what properties it can have, their data types, and default values. If this sounds similar to what goes into a setting, it is: in fact the set of properties within a single setting actually comprise a Metatype. Every setting defined through the orion.core.setting API is internally translated into a Metatype definition that describes the setting's properties. A setting, then, is basically just a thin layer on top of a Metatype, combining the Metatype with a PID, and some additional information telling the Settings page how to categorize the setting (the category field).

Far from being just an identifier for settings, a PID provides the crucial linkage between a Managed Service and Metatypes. A Metatype can be "designated" (associated) to a PID. This association informs the framework of the data shape (properties) that a Managed Service having that same PID expects to be configured with.

In summary, Managed Services are the services that expect to be configured with properties, Metatypes describe what properties they receive, the PID is what connects them, and Settings provide a convenient intersection between all three concepts.

For more details on these APIs, see the links below.

More reading

  • Plugging into the Settings page from the Orion Developer guide — Explains the orion.core.setting service in detail.
  • Configuration services from the Orion Developer guide — details on the nuts and bolts of the Managed Service, Metatype, and ConfigAdmin APIs, with code examples.
  • orion.settings.Setting JSDoc — Explains how extension data from 'orion.core.settings' is represented as JavaScript objects.
  • MetaTypeRegisty JSDoc — Provides an API for querying Metatype information from the service registry.

* The eqeqeq:false option has been renamed to eqeq:true in more recent versions of JSLint.