Enaml Workbench Developer Crash Course¶
This document is a short introduction to the Enaml Workbench plugin framework. It is intended for developers of plugin applications that need to get up and running with the framework in a short amount of time. The Workbench framework is not large, and a good developer can be comfortable with it in an afternoon.
This document covers the concepts, terminology, workflow, and the core plugins and classes of the framework. The accompanying example demonstrates the various parts of the framework with a simple plugin application which allows the user to toggle between a handful of sample views.
Writing large applications is hard. Writing large UI applications is harder. Writing large UI applications which can be safetly extended at runtime by other developers is a recipe for hair loss. There are several difficult issues which must be addressed when developing such applications, some of the most notable are:
- How does user code get dynamically registered and unregistered at runtime?
- Life Cyle
- When and how should user code be loaded and run? How and when and how should it be unloaded and stopped?
- How does the application get started without requiring all user code to be available at startup? How does the application avoid loading external dependencies until they are actually required to do work?
- How can various parts of the application be notified when user code is registered and unregistered?
- User Interfaces
- How can the application be flexible enough to allow user code to add user interface elements to the window at runtime, without clobbering or interfering with the existing user interface elements?
- How can an application be designed in a way where it may be extended to support future use cases which have not yet been conceived?
- Ease of Use
- How can all of these difficult problems be solved in such a way that a good developer can be comfortable developing with the application in an afternoon?
The Enaml Workbench framework attempts to solve these problems by providing a set of low-level components which can be used to develop high-level plugin applications. Think of it as a mini-Eclipse framework for Enaml.
Unlike Eclipse however, the Enaml Workbench framework strives to be compact and efficient. Following the “less is more” mantra, it seeks to provide only the core low-level features required for generic plugin applications. It is intended that the core development team for a large application will build domain specific abstractions on top of the core workbench pieces which will then used to assemble the final application.
Before continuuing with the crash course, the following terminology is introduced and used throughout the rest of the document.
- The core framework object which manages the registration of plugin manifests and the creation of plugin objects. It acts as the central registry and primary communication hub for the various parts of a plugin application.
- Plugin Manifest
- An object which declares a plugin and its public behavior. It does not provide an implementation of that behavior.
- An object which can be dynamically loaded and unloaded from a workbench. It is the implementation of the behavior defined by its plugin manifest. This term is often overload to also indicate the collection of manifest, plugin, extension points, and extensions. That is, ‘plugin’ can refer to the actual plugin instance, or the entire package of related objects written by the developer.
- Extension Point
- A declaration in a plugin manifest which advertises that other plugins may contribute functionality to this plugin through extensions. It defines the interface to which an extension must conform in order to be useful to the plugin which declares the extension point.
- A contribution to the extension point of a plugin. An extension adds functionality and behavior to an existing application by implementing the interface required by a given extension point.
Using the workbench framework is relatively straightforward and has only a few conceptual steps.
- Define the classes which implement your application business logic.
- If your application will create a plugin which contribute extensions to an extension point, define the extension classes and ensure that they implement the interface required by the extension point. The extension classes should interact with the business logic classes to expose their functionality to the rest of the application.
- If your application will create a plugin which defines new extension points, define a Plugin subclasses which will implement the extension point behavior by interacting with the extensions contributed to the extension point by other plugins.
- Create a PluginManifest for each plugin defined by your application. The manifest will declare the extension points provided by the plugin as well as the extensions it contributes to other extension points. If needed, it will supply a factory to create the custom Plugin object.
- Create an instance of Workbench or one of its subclasses.
- Register the plugin manifests required by your application with the workbench. Only the plugins required for startup need to be registered. Additional manifest can be added and removed dynamically at runtime.
- Start the application. How this is done is application dependent.
Points 0 - 3 require the most mental effort. The framework provides a few pre- defined plugins and Workbench subclasses (described later) which make the last few steps of the process more-or-less trivial.
The important takeaway here is that the application business logic should be defined first, and then be bundled up as extensions and extension points to expose that logic to various other parts of the application. This design pattern forces a strong separation between logical components. And while it requires a bit more up-front work, it results in better code reuse and a more maintainable and extensible code base.
This section covers the core classes of the workbench framework.
The Workbench class acts as the fundamental registry and manager object for all the other parts of the plugin framework. As a central hub, it’s usually possible to access any object of interest in the application by starting with a reference to the workbench object.
The core Workbench class can be imported from enaml.workbench.api.
The core Workbench class may be used directly, though application developers will typically create a subclass to register default plugins on startup. A perfect example of this is the UIWorkbench subclass which registers the ‘enaml.workbench.core’ and ‘enaml.workbench.ui’ plugins when started.
The following methods on a Workbench are of particular interest:
- This method is used to register a PluginManifest instance with the workbench. This is the one-and-only way to contribute plugins to an application, whether during initialization or later at runtime.
- This method is used to unregister a plugin manifest which was previously added to the workbench with a call to register. This is the one-and- only way to remove plugins from the workbench application.
- This method is used to query for, and lazily create, the plugin object for a given manifest. The plugin object will be created the first time this method is called. Future calls will return the cached plugin object.
- This method will return the extension point declared by a plugin. The extension point can be queried for contributed extensions at runtime.
The PluginManifest class is used to describe a plugin in terms of its extension points and extensions. It also defines a globally unique identifier for the plugin along with an optional factory function which can be used to create the underlying plugin instance when needed.
The PluginManifest class can be imported from enaml.workbench.api.
The PluginManifest class is a declarative class and defines the following attributes of interest:
- This is a globally unique identifier which identifies both the manifest and the plugin which will be created for it. It should be a string in dot-separated form, typically ‘org.pkg.module.name’. It also serves as the enclosing namespace for the identifiers of its extension points and extensions. The global uniqueness of this identifier is enforced.
- A callable which takes no arguments and returns an instance of Plugin. For most use-cases, this factory can be ignored. The default factory will create an instance of the default Plugin class which is suitable for the frequent case of a plugin providing nothing but extensions to the extension points of other plugins.
Since this class is declarative, children may be defined on it. In particular, a plugin’s extension points and extensions are defined by declaring children of type ExtensionPoint and Extension on the plugin manifest.
The Plugin class is what does the actual work for implementing the behaviors defined by extension points. It acts as a sort of manager, ensuring that the extensions which were contributed to a given extension point are invoked properly and in accordance with interface defined by the extension point.
Well-behaved plugins also react appropriately when extensions are added or removed from one of their extension points at runtime.
The Plugin class can be imported from enaml.workbench.api.
It will be uncommon for most end-user developers to ever need to create a custom plugin class. That job is reserved for core application developers which actually define how the application can be extened. That said, there are two methods on a plugin which will be of interest to developers:
- This method will be called by the workbench after it creates the plugin. The default implementation does nothing and can be ignored by subclasses which do not need life-cycle behavior.
- This method will be called by the workbench when the plugin is removed. The default implementation does nothing and can be ignored by subclasses which do not need life-cycle behavior.
The ExtensionPoint class is used to publicly declare a point to which extensions can be contributed to the plugin. Is is declared as the child of a PluginManifest.
The ExtensionPoint class can be imported from enaml.workbench.api.
The ExtensionPoint class is a declarative class and defines the following attributes of interest:
- The unique identifier for the extension point. It should be simple string with no dots. The fully qualified id of the extension point will be formed by dot-joining the id of the parent plugin manifest with this id.
Declarative children of an extension point do not have any meaning as far as the workbench framework is concerned.
The Extension class is used to pubclicly declare the contribution a plugin provides to the extension point of another plugin. It is declared as the child of a PluginManifest.
The Extension class can be imported from enaml.workbench.api.
The Extension class is a declarative class and defines the following attributes of interest:
- The unique identifier for the extension. It should be simple string with no dots. The fully qualified id of the extension will be formed by dot-joining the id of the parent plugin manifest with this id.
- The fully qualified id of the extension point to which the extension is contributing.
- An optional integer to rank the extension among other extensions contributed to the same extension point. The semantics of how the rank value is used is specified by a given extension point.
- An optional callable which is used to create the implementation object for an extension. The semantics of the call signature and return value are specified by a given extension point.
Declarative children of an Extension are allowed, and their semantic meaning are defined by a given extension point. For example, the extension point ‘enaml.workbench.core.commands’ allows extension commands to be defined as declarative children of the extension.
The section covers the workbench core plugin.
The core plugin is a pre-defined plugin supplied by the workbench framework. It provides non-ui related functionality that is useful across a wide variety of applications. It must be explicitly registered with a workbench in order to be used.
The CoreManifest class can be imported from enaml.workbench.core.api. It is a declarative enamldef and so must be imported from within an Enaml imports context.
The id for the core plugin is ‘enaml.workbench.core’ and it declares the following extension points:
- Extensions to this point may contribute Command objects which can be invoked via the invoke_command method of the CorePlugin instance. Commands can be provided by declaring them as children of the Extension and/or by declaring a factory function which takes the workbench as an argument and returns a list of Command instances.
A Command object is used to declare that a plugin can take some action when invoked by a user. It is declared as the child of an Extension which contributes to the ‘enaml.workbench.core.commands’ extension point.
The Command class can be imported from enaml.workbench.core.api.
The Command class is a declarative class and defines the following attributes of interest:
- The globally unique identifier for the command. This should be a dot-separated string. The global uniqueness is enforced.
- A callable object which implements the command behavior. It must accept a single argument which is an instance of ExecutionEvent.
An ExecutionEvent is an object which is passed to a Command handler when it is invoked by the framework. User code will never directly create an ExecutionEvent.
An ExecutionEvent has the following attributes of interest:
- The Command object which is being invoked.
- A reference to the workbench which owns the command.
- A dictionary of user-supplied parameters to the command.
- The user object which triggered the command.
This section covers the workbench ui plugin.
The ui plugin is a pre-defined plugin supplied by the workbench framework. It provides ui-related functionality which is common to a large swath of UI applications. It must be explicity registered with a workbench in order to be used.
The UIManifest class can be imported from enaml.workbench.ui.api. It is a declarative enamldef and so must be imported from within an Enaml imports context.
The id of the ui plugin is ‘enaml.workbench.ui’ and it declares the following extension points:
- An Extension to this point can be used to provide a custom application object for the workbench. The extension factory should accept no arguments and return an Application instance. The highest ranking extension will be chosen to create the application.
- An Extension to this point can be used to provide a custom main window for the workbench. The extension factory should accept the workbench as an argument and return a WorkbenchWindow instance. The highest ranking extension will be chosen to create the window.
- An Extension to this point can be used to provide a custom window title and icon to the primary workbench window. A Branding object can be declared as the child of the extension, or created by the extension factory function which accepts the workbench as an argument. The highest ranking extension will be chosen to provide the branding.
- Extensions to this point can be used to provide menu items and action items to be added to the primary workbench window menu bar. The extension can declare child MenuItem and ActionItem instances as well as provide a factory function which returns a list of the same.
- Extensions to this point can be used to provide workspaces which can be readily swapped to provide the main content for the workbench window. The extension factory function should accep the workbench as an argument and return an instance of Workspace.
- Extensions to this point can be used to provide the id of a plugin which should be started preemptively on application startup. The extension should declare children of type Autostart. The plugins will be started in order of extension rank. Warning - abusing this facility can cause drastic slowdowns in application startup time. Only use it if you are absolutely sure your plugin must be loaded on startup.
The plugin declares the following extensions:
- This contributes to the ‘enaml.workbench.ui.application_factory’ extension point and provides a default instance of a QtApplication.
- This contributes to the ‘enaml.workbench.ui.window_factory’ extension point and provides a default instance of a WorkbenchWindow.
- This contributes to the ‘enaml.workbench.core.commands’ extension point and provides the default command for the plugin (described later).
The plugin provides the following commands:
- This command will close the primary application window. It takes no parameters.
- This command will close the currently active workspace. It takes no parameters.
- This command will select and activate a new workspace. It takes a single ‘workspace’ parameter which is the fully qualified id of the extension point which contributes the workspace of interest.
The WorkbenchWindow is an enamldef subclass of the Enaml MainWindow widget. It is used by the ui plugin to bind to the internal ui window model which drives the runtime dynamism of the window.
The will be cases where a developer wishes to create a custom workbench window for one reason or another. This can be done subclassing the plain WorkbenchWindow and writing a plugin which contributes a factory to the ‘enaml.workbench.ui.window_factory’ class.
The WorkbenchWindow class can be imported from enaml.workbench.ui.api.
The Branding class is a declarative class which can be used to apply a custom window title and window icon to the primary application window. This is a declarative class which can be defined as the child of an extension, or returned from the factory of an extension which contributes to the ‘enaml.workbench.ui.branding’ extension point.
The Branding class can be imported from enaml.workbench.ui.api.
It has the following attributes of interest:
- The string to use as the primary title of the main window.
- The icon to use for the icon of the main window and taskbar.
The ItemGroup class is a declarative class used to form a logical and visual group of items in a menu. It is declared as a child of a MenuItem and provides a concrete advertisement by the author of a MenuItem that it expects other MenuItem and ActionItem instances to be added to that point in the Menu.
The ItemGroup class can be imported from enaml.workbench.ui.api.
It has the following attributes of interest:
- The identifier of the group within the menu. It must be unique among all other group siblings defined for the menu item.
- Whether or not the items in the group are visible.
- Whether or not the items in the group are enabled.
- Whether or not neighboring checkable action items in the group should behave as exclusive checkable items.
The ActionItem class is used to declare a triggerable item in a menu. It is declared as a child of a plugin Extension object.
The ActionItem class can be imported from enaml.workbench.ui.api.
It has the following attributes of interest:
- A “/” separated path to the location of this item in the menu bar. This path must be unique for the menu bar, and the parent path must exist in the menu bar. The last token in the path is the id of this action item with respect to its siblings. For example, if the path for the item is ‘/foo/bar/baz’, then ‘/foo/bar’ is the path for the parent menu, and ‘baz’ is the id of the action with respect to its siblings. The parent menu need not be defined by the same extension which defines the action. That is, one plugin can contribute an action to a menu defined by another plugin.
- The name of the item group defined by the parent menu to which this action item should be added.
- The id of the sibling item before which this action item should appear. The sibling must exist in the same group as this action item.
- The id of the sibling item after which this action item should appear. This sibling must exist in the same group as this action item.
- The identifier of the Command object which should be invoked when this action item is triggered by the user.
- The dictionary of parameters which should be passed to the command when it is invoked.
- The text to diplay as the label for the action.
- The keyboard shortcut which should be bound to trigger action item.
- Whether or not the action is visible.
- Whether or not the action is enabled.
- Whether or not the action is checkable.
- Whether or not the action is checked.
- The icon to display next to the action.
- The tool tip text to display when the user hovers over the action.
- The text to display in the status bar when the user hovers over the action.
The Workspace class is a declarative class which is used to supply the central window content for a ui workbench application. It contains the attributes and method which are necessary for the ui plugin to be able to dynamically switch workspaces at runtime. The application developer will typically create a custom workspace class for each one of the views that will be shown in the workbench.
The Workspace class is declarative to allow the developer to fully leverage the Enaml language in the course of defining their workspace. It will typically be declared as the child of any object.
The Workspace class can be imported from enaml.workbench.ui.api.
It has the following attributes of interest:
- This is text which will be added to the window title in addition to the title text which is supplied by a branding extension.
- This is an Enaml Container widget which will be used as the primary window content. It should be created during the workspace ‘start’ method and will be destroyed by the framework automatically when the workspace is stopped.
It has the following methods of interest:
- This method is called when the UI plugin starts the workspace. This can be used to load content or any other resource which should exist for the life of the workspace.
- This method is called when the UI plugin closes the workspace. This should be used to release any resources acquired during the lifetime of the workspace. The content Container will be destroyed automatically after this method returns.
The Autostart class is a declarative class which is used to supply the plugin id for a plugin which should be automatically started on application startup.
The Autostart class can be imported from enaml.workbench.ui.api.
It has the following attributes of interest.
- This is the id of the plugin to start on application startup. The manifest for the plugin must be registered before the ui plugin is started.
The UIWorkbench class is a simple sublass of Workbench for creating ui applications. This class will automatically register the pre-defined ‘core’ and ‘ui’ workbench plugins when it is started.
The UIWorkbench class can be imported from enaml.workbench.ui.api.
It has the following methods of interest:
- This method will load the core and ui plugins and start the main application event loop. This is a blocking call which will return when the application event loop exits.