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
A plugin is basically just a container for a group of related resources.
It's purpose is to manage Resource
s and provide
Service
s. 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.
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.
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.
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:
ViewResource
- handles displaying/hiding a viewToolBarResource
- display/hide a toolbarRegistrySubscriberResource
- handles subscribing/unsubscribing
from registry nodesMenuBarItemResource
- add/remove menubar entries
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.
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.
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
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.
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.
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.
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.
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() ); } })(); } ); })() );
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.
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:
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:
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
TBD
TBD
TBD
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); } );
This section introduces a few services which are implemented by "core" plugins that come with Chimera.
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:
// this file saved as fb-1.os services["file browser"].addDragAndDropSupport( extension, component, fxn );
extension
is the file extension (ie "os" or "html"),
component
is the swing component that is the drop target,
and fxn
is a function that takes the file object for the
file being dropped as its argument.
// this file saved as fb-2.os services["file browser"].addAction( extension, action ); services["file browser"].addDefaultAction( extension, action );
action
is the
javax.swing.Action.
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 );
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);
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.