How To Create a Plugin for an Envisage Application

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 does it do?

  • What functionality does it offer to other plugins?

  • What does it add to what other plugins already do?

  • What does it need from other plugins?

What Does Your Plugin Do?

“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”.

Example 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.

What Does Your Plugin Offer to Other Plugins?

There are two ways that a plugin can provide functionality to other plugins.

  • Offering services: Your plugin may provide an API, the core functionality of your library, as a service. Yours might be the only provider of that type of service in an application, or it might be one of many.

  • Defining extension points: Your plugin may offer one or more extension points, which means that your plugin will do something specific with contributions to that extension point. Contributions to an extension point must be instances of a type or interface that you specify. An extension point is a trait attribute whose type is ExtensionPoint, which consists of a List of some other type, plus an ID. In other words, an extension point is a list, used by your plugin, that is populated by plugins.

Examples of Offering 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.)

What Does Your Plugin Add to Other Plugins?

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.

Examples of Contributing to Extension Points

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:

  • Subclass from a class that implements the interface, in this case apptools.preferences.ui.api.PreferencesPage.

  • Define a factory function that returns an appropriate object. This strategy is not needed for preferences pages, but can be helpful when the object being returned contains a reference to a service.

In either case, the contribution is a trait attribute on the plugin object.

What Does Your Plugin Need from Other Plugins?

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.

Example of Using a Service

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.

Complete Example

The complete plugin for the Tetris game might look like this:

class TetrisPlugin(Plugin):
    """ Plugin to make the Tetris library available in Envisage.
    """

    ### 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()