Chimera Developer's Guide


      Chimera Overview
      1.1   Plugin
      1.2   Service
      1.3   Registry
      1.4   Resource
      1.5   View
      1.6   Menubar
      1.7   Toolbar
      Anatomy of a Plugin
      2.1   The Basic Skeleton
      2.2   Services and Resources
      2.3   Adding a User Interface
      The Registry
      3.1   Creating Nodes
      3.2   Subscribing to Nodes
      3.3   Core API
      3.4   Convenience API
      3.5   Examples
      Useful Core Services
      4.1   file browser
      4.2   help
      4.3   swing worker

1 Chimera Overview

Chimera is a highly dynamic application development framework. An application built with the Chimera framework, such as ObjectDevel, is really just a collection of plugins. Some of the plugins, such as the console, are core plugins that are part of chimera.jar itself, others such as the editor are part of the application itself. There is nothing special about core plugins, other than they are distributed as part of the Chimera framework (aka chimera.jar ).

(Plugin_Architecture.png)

1.1 Plugin

A plugin is basically just a container for a group of related resources. It's purpose is to manage Resources and provide Services. Since a plugin can be activated on demand, and become inactive when it is no longer needed, the Plugin base class helps the developer manage the lifecycle of the plugin.

1.2 Service

A service is a named API exported by a plugin. A plugin can provide zero or more services. By allowing a plugin to provide an API to other plugins, the system can be designed to have a higher degree of code-reuse; also because it encourages code-reuse, it is easier to design the system of smaller plugins which do one thing and one thing only. The named service concept was developed to reduce coupling. When some code (another plugin, a test script, etc.) uses a service, it only cares that the service is implemented according to it's contract (it's API), and not which plugin implements the API. Because of this it is possible to move which plugin implements which service, split one plugin into multiple plugins, or even overload the service by loading a plugin which has an enhanced re-implementation of the API.

The lifecycle management provided by plugin extends to services too... if a service provided by an inactive plugin is accessed, the framework ensures that the plugin becomes active before the user can call any method of the service. For this reason, a plugin can use a managed resource (see below) to initialize variables used by the service implementation, etc.

1.3 Registry

A single instance of the registry is the repository of registered plugins. It is how other parts of the system access services. But it is also a general purpose data sharing system, with the option of making that data persistent. The registry is convered in greater detail in a later section.

1.4 Resource

A resource is what the plugin knows how to manage. A resource object implements install() and uninstall() methods. When the plugin becomes active, it will automatically install() all it's resources. Likewise, when a plugin becomes inactive, it will automatically uninstall() all of it's resources. Note that these methods should only be called by the plugin base class itself.

The plugin base class provides subclasses of Resource for certain specialized applications:

The plugin implementation may also create it's own subclasses of Resource... for example a common pattern is an anonymous subclass of Resource which handles initializing the plugin's state variables when the plugin becomes active, and cleaning up when the plugin becomes inactive.

A resource can either be managed, or unmanaged, which determines how the plugin manages the resource. When a managed resource is added to a plugin, it does not cause the plugin to change state (ie. transition from inactive to active or visa versa), because the plugin manages the resource. On the other hand, an adding an unmanaged resource to a plugin will cause it to become active (and install() all managed resources), and removing the last unmanaged resource will cause the plugin to become inactive.

Point of interest: the resource mechanism is used to cause a plugin to become active if a service provided by the plugin is accessed, and allow the plugin to become inactive when the service is no longer used.

1.5 View

A view is a window created by a plugin. One or more views are grouped into "docks". The user can re-arrange the association between views and docks to their liking, but by default all the views for a given plugin are displayed in the same dock. Plugins (or any code, really) can directly create a dialog, but the general guideline is to use dialogs for temporary windows (for example, a FileChooser), and view's for longer lived screens.

1.6 Menubar

Chimera also provides a way of registering entries in a menubar. If you wanted to create a menubar entry File -> Test -> Print Hello, which when selected prints "hello" to the console window:

// this file saved as menubar-example.os

pkg.util.addMenuBarItem( "File/Test", "Print Hello", function(evt) {
  pkg.output.writeln("hello");
} );
      

The evt is the java.awt.event.ActionEvent event that is generated when the menubar entry is selected

1.7 Toolbar

A toolbar can be created by a plugin. Toolbars are displayed at the top of the screen, and can have buttons, status labels, etc. In general toolbars should be used to provide easy access to frequently used features. For example:

// this file saved as toolbar-example.os

var myToolBar = new swing.JToolBar();

myToolBar.add(
  new pkg.util.ActionAdapter( "Button 1", null, null, function(evt) {

      pkg.output.writeln("someone pressed button 1!!");

    } ) );

myToolBar.add(
  new pkg.util.ActionAdapter( "Button 2", null, null, function(evt) {

      pkg.output.writeln("someone pressed button 2!!");

    } ) );

windowManager.addToolBar(myToolBar);
       

Like with menubar handler functions, the evt is the java.awt.event.ActionEvent in response to the button press. The second argument to pkg.util.ActionAdapter is the optional tool-tip, which is a string that is displayed to the user if they mouse-over the button. The third argument is the optional icon... if you have a small .gif image, you can use it in your button by putting it in the img directory, and then using pkg.util.getImageIcon(path) to load it.

2 Anatomy of a Plugin

This section covers the basics parts of a plugin written in ObjectScript. The New Plugin Wizard in ObjectDevel (menubar File -> New -> From Wizard -> Plugin) is also available to generate skeleton code for your new plugin.

2.1 The Basic Skeleton

Any script plugin starts with the same basic framework:

// this file saved as plugin-1.os

/**
 * My First Plugin(TM)
 */
registry.register( new (function() extends ti.chimera.Plugin( main, "My First Plugin" ) {

})() );
      

Basically what this does is create an instance of a plugin and register it with the registry. The class implementing the plugin does not have to be declared inline with it's construction, as is shown in this example, but since we never create more than one instance of any given plugin it is the common practice to declare the class inline. Of course this plugin doesn't do anything interesting.

2.2 Services and Resources

In order to do something interesting, you will need to create resources. Also, most plugins need to provide a service, if for no other reason to activate the plugin itself. (There are a few places where plugins activate themselves when they are loaded, but in the spirit of being able to only load plugins for features the user wants to use, self activation is the exception rather than the rule.)

// this file saved as plugin-2.os

/**
 * My First Plugin(TM)
 */
registry.register( new (function() extends ti.chimera.Plugin( main, "My First Plugin" ) {

  /**
   * Keep a counter of the number of times someone has called our
   * service's <code>incrementCounter()</code> method.
   */
  var cnt;

  /**
   * Initialize/cleanup.  Create a managed resource to handled initializing
   * state variables when we start, and cleanup when we are done.
   */
  addResource( new (function() extends ti.chimera.Resource(true) {

    public function install()
    {
      cnt = 0;
    }

    public function uninstall()
    {
      pkg.output.writeln("cnt=" + cnt);
      cnt = null;
    }

  })() );

  /**
   * The service which provides the <code>incrementCounter()</code>
   * method.
   */
  registerServiceFactory( function() {

    return new (function() extends ti.chimera.Service("my counter") {

      public function incrementCounter()
      {
        cnt++;
      }

    })();

  } );

})() );
      

In this example a managed resource is used to initialize the cnt variable when the plugin becomes active, and cleanup when the plugin becomes inactive. It is a good idea to use a managed resource for this purpose, since the plugin can transition from inactive to active and back many times over the lifecycle of the application.

The other aspect of this simple example is a service called "my counter" which has the incrementCounter() method. The purpose of registering the service as a function that returns a service instance (aka a "service factory") is that the lifecycle of the service object itself is tracked by the framework. When the service object is garbage collected because no other code still holds a reference to it, the plugin can become inactive (iff it has no other unmanaged resources). You'll see the "factory" pattern show up again, in places where we need to defer creation of some object.

To try this out, from the script console import the file and access the service. Note that services["my counter"] is just shorthand for registry.getService("my counter").

os> import "MyFirstPlugin.os"
os> var s = services["my counter"]
os> s.incrementCounter();
os> s.incrementCounter();
os> s.incrementCounter();
os> s = null
cnt=3
os> 
        

Depending on the size of your heap, you may need to trigger garbage collection in order for the service object to be garbage collection, and your plugin to become inactive again. To do this call java.lang.System.gc() to tell the Java VM to perform garbage collection.

2.3 Adding a User Interface

While not all plugins have a user interface, many do. So we are going to add a view to this simple example, but using a ViewResource and a subclass of View. There are two different constructors for ViewResource which are interesting for different uses. If you want to create a unmanaged ViewResource, you can simply use the constructor that simply takes a View instance as a constructor. But if you want a managed ViewResource, you will have to implement a factory function. The reason for this is that view's that create a complex screen with many buttons, etc., will use RAM regardless of whether it is visible or not, so we want to defer creating the view itself until it is time to display that view.

In this example, we will use a managed view which will display the current value of the cnt variable. When the plugin becomes inactive, the view will automatically be closed.

The View subclass itself must just implement a single method, getComponent, which returns the component which makes up the view displayed to the user... usually this will be a javax.swing.JPanel which is a container for all the buttons, text fields, labels, etc. which make up the plugin.

// this file saved as plugin-3.os

/**
 * My First Plugin(TM)
 */
registry.register( new (function() extends ti.chimera.Plugin( main, "My First Plugin" ) {

  /**
   * Keep a counter of the number of times someone has called our
   * service's incrementCounter() method.
   */
  var cnt;
  var textField;

  /**
   * Initialize/cleanup.  Create a managed resource to handled initializing
   * state variables when we start, and cleanup when we are done.
   */
  addResource( new (function() extends ti.chimera.Resource(true) {

    public function install()
    {
      cnt = 0;
      textField = new swing.JTextField(5);
    }

    public function uninstall()
    {
      pkg.output.writeln("cnt=" + cnt);
      cnt = null;
      textField = null;
    }

  })() );

  /**
   * A managed view for displaying the current value of the counter.
   */
  addResource( new ViewResource( function() {

    return new (function() extends ti.chimera.View( main, "My View", null ) {

      var panel = new swing.JPanel();
      panel.add( new swing.JLabel("Current counter value:") );
      panel.add(textField);

      /**
       * Get the component that makes up this view.  The view must always 
       * implement this method.
       * 
       * @return any swing component will do
       */
      public function getComponent()
      {
        return panel;
      }

    })();

  }, true ) );

  /**
   * The service which provides the incrementCounter()
   * method.
   */
  registerServiceFactory( function() {

    return new (function() extends ti.chimera.Service("my counter") {

      public function incrementCounter()
      {
        cnt++;

        /* Note: setText() is thread safe, although many swing methods are not.
         * For swing methods that are not thread-safe, swing provides the
         * SwingUtilities.invokeLater() method, which takes a function to
         * invoke from the context of the swing AWT thread, which is the thread
         * context that is safe to call any swing method from.  For example if
         * setText() was not thread-safe we could do:
         *   
         *   swing.SwingUtilities.invokeLater( function() {
         *     textField.setText( cnt.castToString() );
         *   } );
         *   
         * This is useful to keep in mind for code that can be called from 
         * any thread context.
         */
        textField.setText( cnt.castToString() );
      }

    })();

  } );

})() );
      

3 The Registry

Originally the registry was just the data structure that kept track of all the plugins and services in the system. It has since been refactoried into a general purpose mechanism for storing and sharing data between different parts of the system. That data can optionally be persistant, meaning that the most recent value is preserved when the user exits and restarts the application, which makes the registry handy for user configurable options.

The data in the registry is stored in a tree-like structure, and indexed via a "/" separated path, for example /Preferences/Console/Font, or /Dialogs/File Browser/bounds. The convention is to store user configurable preferences under /Preferences. Also, if the data is specific to a certain plugin, it is suggested to use the plugin name as part of the path to avoid namespace conflicts. Branch nodes, or directories, are simply tree node's whose data is a table mapping child name to child node.

The registry uses a publish-subscribe model for access values stored in the registry... in other words, if for example you write a plugin that wants to make use of /Preferences/Console/Font, you would subscribe to that path, providing a subscriber function, and the registry would publish the most recent value to you by calling that subscriber function. Any subsequent changes to the value of that path you have subscribed to would result in the most recent value being published to you again, until you unsubscribe from that path. Your subscriber function may be as simple as a single line, like console.setFont(newValue);, and since the most current value is published to you every time the value changes, if the user changes the console font in the preferences dialog, the change will immediately take effect with no extra work required by you.

Since branch nodes are just regular data nodes (containing a table mapping child name to child node), they follow the same publish-subscribe model as regular leaf nodes. This means that it is possible to subscribe to directories, and receive notification of new nodes being linked in to that directory, or existing nodes being unlinked from that directory. The Convenience API builds upon this to provide a way to subscribe to the creation or deletion of a node from a specified path.

3.1 Creating Nodes

Creating a node involves two parts: creating the node object itself, and then link()ing it in to some path in the registry. The ti.chimera.registry.Node constructor takes the following parameters:

  1. Object val - the initial value of the node... this is the value that is published to subscribers.
  2. NodeContract contract - the node contract, or null for none. Any value that is assigned to this node, including the initial value, must meet the contract. An example of a type of contract would be an instance of ti.chimera.registry.TypeNodeContract, which ensures that the value assigned to this node has the correct type. Some default contracts are provided for common cases, for example ti.chimera.registry.NodeContract.STRING_CONTRACT ensures that the value assigned to that node is a string. In addition to ensuring that no invalid value is assigned to this node, the node contract is also used by the registry browser plugin (the preferences view is just a registry browser view opened to the root path of /Preferences) to determine what sort of user interface to construct to let the user see and modify the value of a node... for example if the contract is a ti.chimera.pref.StringChoiceNodeContract, a user interface that is a JList is constructed to let the user choose between the values that the contract would accept.
  3. String comment - since the registry could be used as a mechanism for implementing a public interface between two components, there needs to be a way to document that interface. That is what this field is used for. Since it is displayed in the registry browser, it is also a good way to indicate to the user what a certain preference is used for. HTML markup tags should be used as necessary.

To create a value that is stored persistently, use instead a ti.chimera.registry.PersistentNode, whose constructor takes the same arguments to the constructor as Node. When the PersistentNode is linked into the tree, if there is a previously stored value for a node at that path which still meets the current node contract, it will override the initial value. If the previously stored value does not meet the node contract, it will be discarded, which protects against problems with an old cache.jar when the user upgrades to a newer version of the application.

To add the newly created node (or even a node that already exists elsewhere in the tree) to the registry, use the link() method, which takes the following parameters:

  1. Node node - the node to link in
  2. String path - the path, where to link the node in to. If necessary, parent directories are automatically created for you. If a node is already linked in at this path, this will throw an exception.

To remove a node from the tree, use the unlink() method, which takes as a parameter the path to the node to unlink. To test for the existance of a node, use the exists() method

3.2 Subscribing to Nodes

TBD

3.3 Core API

TBD

3.4 Convenience API

TBD

3.5 Examples

To create a persistent node, and link it into the tree;

// this file saved as registry-1.os

registry.link(
  new ti.chimera.registry.PersistentNode(
    new java.awt.Font( "Monospaced", java.awt.Font.PLAIN, 12 ), // default/initial value
    new ti.chimera.pref.FontNodeContract(                      // only allow monospace fonts
      ti.chimera.pref.FontNodeContract.MONOSPACE ),
    "the font for editor windows" ),                            // descriptive comment
  "/Preferences/Editor/Font"
);
      

and to subscribe to a node in the tree:

// this file saved as registry-2.os

registry.subscribeToValue(
  "/Preferences/Editor/Font",
  null,
  function( node, font ) {
    editorBuffer.setFont(font);
    glyphGutter.setFont(font);
  }
);
      

This sort of over-simplifies the subscription, since a well behaved plugin will unsubscribe from the value when it no longer needs it, for example when the view is closed. A utility function, pkg.registry.subscribeToValue, is provided for plugins implemented in script, to simplify this. The first argument to this function is a plugin or view object, and the remaining three are the same as the registry's subscribeToValue. In the case that the first argument is a plugin, a resource is created which subscribes/unsubscribes the subscriber, so when the plugin is active, the subscriber will get values published to it, but when the plugin becomes inactive the subscriber will be automatically unsubscribed. If the first argument is a view, a close-runnable is created to unsubscribe the subscriber when the view is closed.

// this file saved as registry-3.os

pkg.registry.subscribeToValue(
  this,                            // the view or plugin object
  "/Preferences/Editor/Font",
  null,
  function( node, font ) {
    editorBuffer.setFont(font);
    glyphGutter.setFont(font);
  }
);
      

4 Useful Core Services

This section introduces a few services which are implemented by "core" plugins that come with Chimera.

4.1 file browser

The file browser plugin displays to the user a view of all the files in the system (currently as a JTree, but that is an implementation detail). As a plugin writer, if you have a plugin that needs to deal with files, you can take advantage of the "file browser" service in a couple of different ways:

For example, the following code is used by the console plugin so that the user can (a) click and drag a file to the console to import it, or (b) select "run" from the files action menu (right click menu) to import it:

// this file saved as fb-3.os

function doImport(file)
{
  if( file.exists() )
    console.paste("import \"" + file.getPath() + "\";\n");
}

services["file browser"].
  addAction( "os", new pkg.util.ActionAdapter( "run", null, null, function(evt) {
    doImport( pkg.util.getFile( evt.getSource() ) );
  } ) );

services["file browser"].addDragAndDropSupport( "os", console, doImport  );
      

4.2 help

The "help" service enables a plugin to register a help page, which the user can browse from the help system. That help page can be regular HTML.

// this file saved as help-1.os

const var HELP_PATH = "Plugins/Console";

// to register help:
services["help"].registerHelp(
  HELP_PATH,
  "ConsolePluginHelp.html",
  "Help for the Console Plugin"
);

// to display help (for example, from ActionListener)
services["help"].displayHelp(HELP_PATH);
      

4.3 swing worker

In situations where you want to perform a long, or potentially blocking, operation, you should use the "swing worker" service to ensure that the operation is performed from a safe thread context, ie. a thread that is not an event dispatch thread, such as the AWT-event-thread.

// this file saved as sw-1.os

services["swing worker"].run( function() {

  // ... some length operation ...

}, "Some Length Operation" );
      

If the current thread is not an event thread, the runnable is run from the context of the current thread, otherwise a new thread is created to perform the operation in, and the current thread is blocked in a safe mannar, for example by waiting-for and dispatching events.


Last modified: Sun Aug 3 17:36:29 PDT 2003