This document describes the process of creating an Envisage plugin that works with a Workbench-based application.
There are several questions to consider when designing a plugin:
“What your plugin does” refers to the basic functionality that your plugin adds to the application.
Very often, you want to take some pre-existing chunk of functionality (module, library, etc.) and make it available within the Envisage application. Assuming the library has a well-defined API, you do not need to alter it in any way. You only create the plugin code to wrap it for Envisage.
Sometimes, however, you are designing a new chunk of functionality just for the Envisage application. In this case, step back and think about how you would design the core functionality of the plugin, independent of Envisage. It is important to keep this separate from the plugin machinery that make that functionality work within Envisage.
In either case, we will refer to the code that does this core functionality as “the library”.
Suppose you have a library that implements a game of Tetris. You want to add this game to an Envisage application that lets users pick a game to play from a catalog of available games. You have a Tetris class that looks something like this:
class Tetris(HasTraits):
# Basic colors
background_color = Color
foreground_color = Color
# Shapes to use in the game
shapes = List(IShape)
...
In the following sections, we’ll look at ways that this library can be integrated into the application by a plugin.
There are two ways that a plugin can provide functionality to other plugins.
Suppose that the games application defines an “IGame” interface that it uses to control games, with methods for starting, stopping, displaying scores, and so on. In this case, you offer the Tetris library as a service that implements the “IGame” interface used by the application. (You might need to create an adapter for your Tetris class to the IGame interface, but we’ll ignore that.)
You can manually register a service, but a simple way to do it is to contribute to the ‘envisage.services_offers’ extension point of the Core plugin. This ensures that the plugin is registered, and is created when it is needed.
You also want to allow users to contribute their own Tetris shapes, so you define an extension point for shapes. We’ll leave aside the question of how users actually define their shapes. The point is that the catalog of shapes is extensible. (You would probably also contribute some basic shapes from your plugin, so that users don’t need to contribute any.)
Other plugins may provide extension points that are useful to your plugin. If so, you can contribute to those extension points. Essentially, your plugin passes one or more objects to the plugin whose extension point you are contributing to, and that plugin “does the right thing” with those items.
Continuing the example of the Tetris game, the application might keep a list of available games, and present the list for the user to select from. Thus, it might have a ‘games’ extension point, to which you can contribute an object containing information about your Tetris game, such as name, description, icon, and entry point.
More concretely, you want to specify default colors for the foreground and background colors of the game, but allow users to change them and save their changes across sessions. For specifying default preference values and saving changed values, you can contribute to the ‘envisage.preferences’ extension point offered by the Envisage core plugin; for a UI to change preferences, you can contribute to the ‘envisage.ui.workbench.preferences_pages’ extension point offered by the Workbench plugin. (A game application probably wouldn’t use the Workbench plugin, but we’ll assume it does to avoid using a fictional plugin.)
A contribution to the preferences extension point must be a URL of a preferences file (readable by ConfigObj). A plugin typically has only one preferences file, even if it has many categories of preferences.
A contribution to the preference_pages extension point must be a callable that returns an object that implements the apptools.preferences.ui.api.IPreferencesPage interface. Such an object typically has a Traits UI view that can be used in the Preferences dialog box to set the values of the preference attributes. A plugin may have multiple preferences pages, depending on how it groups the items to be configured.
There are two strategies for defining a callable that returns an object:
In either case, the contribution is a trait attribute on the plugin object.
Your plugin may need to use the API of some other plugin. In Envisage, you use the other plugin’s API via a service, rather than directly. This allows for the plugin offering the service to be replaced with another one, transparently to your plugin. There may be multiple plugins offering a particular type of service, but a client plugin uses only one instance of a service at any given time.
The service you use may be your own. For the Tetris game, the object that you contribute to the application’s ‘games’ extension point needs to be able to start the game. However, to reduce memory overhead, you don’t want the Tetris library to be imported until the user actually chooses to play Tetris. Using the service offered by the Tetris plugin is a way to accomplish that.
The complete plugin for the Tetris game might look like this:
class TetrisPlugin(Plugin):
""" Plugin to make the Tetris library available in Envisage.
"""
##### IPlugin Interface ################################################
### Extension points offered by the plugin
# Shapes to be used in the game
shape = ExtensionPoint(List(IShape), id='acme.tetris.shapes')
### Contributions to extension points
my_shapes = List(contributes_to='acme.tetris.shapes')
def _my_shapes_default(self):
""" Trait initializer for 'my_shapes' contribution to this plugin's
own 'shapes' extension point.
"""
return [ Shape1(), Shape2(), Shape3() ]
games = List(contributes_to='acme.game_player.game_infos'
def _games_default(self):
""" Trait initializer for 'games' contribution to the application
plugin's 'games' extension point.
"""
return [ GameInfo(name='Tetris', icon='tetris.png',
description='Classic shape-fitting puzzle game',
entry_point=self._start_game) ]
preferences = List(contributes_to='envisage.preferences')
def _preferences_default(self):
""" Trait initializer for 'preferences' contribution. """
return ['pkgfile://acme.tetris.plugin/preferences.ini']
preferences_pages = List(contributes_to=
'envisage.ui.workbench.preferences_pages')
def _preferences_pages_default(self):
""" Trait initializer for 'preferences_pages' contribution. """
from acme.tetris.plugin.preferences_pages import \
TetrisPreferencesPages
return [ TetrisPreferencesPages ]
services_offers = List(contributes_to='envisages.service_offers')
def _service_offers_default(self):
""" Trait initializer for 'service_offers' contribution. """
return [ ServiceOffer(protocol=IGame,
factory=self._create_tetris_service,
properties={'name':'tetris'}) ]
#### Private interface #################################################
def _create_tetris_service(self, **properties):
""" Factory method for the Tetris service. """
tetris = Tetris() # This creates the non-Envisage library object.
# Hook up the extension point contributions to the library object trait.
bind_extension_point(tetris, 'shapes', 'acme.tetris.shapes')
# Hook up the preferences to the library object traits.
bind_preference(tetris, 'background_color',
'acme.tetris.background_color')
bind_preference(tetris, 'foreground_color',
'acme.tetris.foreground_color')
return tetris
def _start_game(self):
""" Starts a Tetris game. """
game = self.application.get_service(IGame, "name == 'tetris'")
game.start()