Extensibility¶
The foregoing sections have described those elements of the Tasks framework that
belong to the Pyface project; as such, our imports have been from the
pyface.tasks
package. We now discuss how Tasks can be used in
conjunction with Envisage to build extensible applications. Accordingly, our
imports in this section will be from the envisage.ui.tasks
package.
As remarked in the Introduction, some familiarity with the Envisage plugin framework is assumed. For more information about Envisage, the reader is referred to the Envisage Core Documentation.
The Tasks Plugin¶
In creating an extensible Tasks application, we imagine two primary extensibility use cases:
Contributing to an existing task:
In this case, the user wishes to add some combination of dock panes, menu items, and tool bar buttons to an existing task. In other words, the user would like to extend a task’s functionality without changing its fundamental purpose.
Contributing a new task:
Alternatively, the user may find that the none of the existing tasks are suitable for the functonality that he or she wishes to implement. Thus the user decides to contribute an entirely new task.
The Tasks plugin has an extension point corresponding to each of these two use cases. These extensions points are:
envisage.ui.tasks.tasks
:A list of
TaskFactory
instances.TaskFactory
is a lightweight class for associating a task factory with a name and an ID. We shall see an example of its use in the following subsection.
envisage.ui.tasks.task_extensions
:A list of
TaskExtension
instances. ATaskExtension
is a bundle of menu bar, tool bar, and dock pane additions to an existing task. This class is discussed in detail in the subsection on Extending an Existing Task.
The Tasks plugin also provides two extensions points that permit the creation of extensible preferences dialogs. We defer discussion of this functionality to the subsection on Creating a Preferences Dialog.
Creating a Tasks Application¶
Let us imagine that we are building a (slightly whimsical) application for visualizing strange attractors, in two and three dimensions [1]. We take for granted the existence of tasks for performing each of these two kinds of visualization.
Like any Envisage application, our application will contain a Plugin
instance to expose functionality to other plugins. In this case, we will
contribute our two tasks to the Tasks plugin. Because the Tasks plugin is
responsible for creating tasks and the windows that contain them, it expects to
receive factories for creating Task
instances rather than the instances
themselves. The TaskFactory
class fulfills this role.
With this in mind, we can define a Plugin
for our application:
class AttractorsPlugin(Plugin):
#### 'IPlugin' interface ##############################################
# The plugin's unique identifier.
id = 'example.attractors'
# The plugin's name (suitable for displaying to the user).
name = 'Attractors'
#### Contributions to extension points made by this plugin ############
tasks = List(contributes_to='envisage.ui.tasks.tasks')
def _tasks_default(self):
return [
TaskFactory(
id='example.attractors.task_2d',
name='2D Visualization',
factory=Visualize2dTask,
),
TaskFactory(
id='example.attractors.task_3d',
name='3D Visualization',
factory=Visualize3dTask,
),
]
Having contributed tasks to the Tasks plugin, we must now specify how the tasks
shall be added to windows to constitute our application. We call this
specification the application-level layout to distinguish it from the
lower-level layout attached to a task. Concretely, an application-level layout
consists of a set of TaskWindowLayout
objects, each of which indicates which
tasks are attached to the window, which task is active in the window, and,
optionally, the size and position of the window.
The default application-level layout is defined inside our application class,
which must inherit TasksApplication
:
class AttractorsApplication(TasksApplication):
#### 'IApplication' interface #########################################
# The application's globally unique identifier.
id = 'example.attractors'
# The application's user-visible name.
name = 'Attractors'
#### 'TasksApplication' interface #####################################
# The default application-level layout for the application.
default_layout = [
TaskWindowLayout(
'example.attractors.task_2d',
'example.attractors.task_3d',
size=(800, 600),
),
]
Observe that each of the IDs specified in the layout must correspond to the ID
of a TaskFactory
that has been contributed to the Tasks plugin. Also note
that the TaskWindowLayout
class has an active_task
attribute; by
omitting it, we indicate that the first task in the task list is to be active by
default.
By default, the Tasks framework will restore application-level layout when the
application is restarted. That is, the set of windows and tasks attached to
those windows that is extant when the application exits will be restored when
the application is started again. If, however, the
always_use_default_layout
attribute of the application is set, the default
application layout will be applied when the application is restarted. Tasks
will still attempt to restore as much user interface state as possible,
including window positions and task layouts. This setting is particularly
useful for multi-window applications.
Apart from this functionality, the Tasks plugin provides no additional default behavior for managing tasks and their windows, permitting users to switch tasks within a window, etc. This is to be expected, as these behaviors are fundamentally application-specific. That said, we shall see in Global Task Extensions that the Tasks plugins provides a few built-in extensions for implementing common behaviors.
Creating a Preferences Dialog¶
There are three extensions points associated with preferences. One of these extension points is built into the Envisage core plugin, while the other two belong to the Tasks plugin. Let us survey each of them in turn.
envisage.preferences
:A list of locators for default preferences files (INI files). This extension point is at the model level in the preferences system.
envisage.ui.tasks.preferences_categories
:A list of
PreferencesCategory
instances. Preference categories have name and ID attributes. To each category with a given name corresponds a tab with that name in the preferences dialog, unless there is only a single category, in which the case the tab bar will not be shown.
envisage.ui.tasks.preferences_panes
:A list of
PreferencesPane
instances. A preferences pane defines a set of user interface elements for changing application preferences via a model object called aPreferencesHelper
. A preferences pane has a name and an ID, as well as acategory
attribute for specifying the ID of the category to which it belongs. Preferences panes are stacked vertically among the other panes in their category. By default, the category of a pane is “General”. As a convenience, if a category with the specified ID does not exist, it will be created automatically.
Note that both preference panes and categories have before
and after
attributes for specifying their order, if this is necessary. See the next
subsection for more information about this idiom.
We shall now expand the example from the previous subsection by adding a preferences dialog for changing the default task and the application-level state restoration behavior. By doing so, we shall see concretely how to use the preferences system in Tasks, as well as reinforce our knowledge about application-level layout.
We begin by defining “preferences.ini”, our default preferences file:
[example.attractors]
default_task = example.attractors.task_2d
always_use_default_layout = False
and contributing it to the Envisage core plugin:
class AttractorsPlugin(Plugin):
[ ... ]
preferences = List(contributes_to='envisage.preferences')
def _preferences_default(self):
return ['pkgfile://example.attractors/preferences.ini']
This construction assumes that the attractors example is in Python’s path (in
the example.attractors
package). Alternatively, we could have used the
“file://” prefix in conjunction with an absolute path on the local filesystem.
We can now define two classes: a preferences helper and preferences pane. The preferences helper is a model-level class that makes accessing the keys in the preferences file convenient and type safe. The preferences pane, introduced above, exposes a Traits UI view for this helper object:
from envisage.ui.tasks.api import PreferencesPane, TaskFactory
from apptools.preferences.api import PreferencesHelper
class AttractorsPreferences(PreferencesHelper):
#### 'PreferencesHelper' interface ####################################
# The path to the preference node that contains the preferences.
# Notice that this corresponds to the section header in our preferences
# file above.
preferences_path = 'example.attractors'
#### Preferences ######################################################
default_task = Str
always_use_default_layout = Bool
class AttractorsPreferencesPane(PreferencesPane):
#### 'PreferencesPane' interface ######################################
# The factory to use for creating the preferences model object.
model_factory = AttractorsPreferences
#### 'AttractorsPreferencesPane' interface ############################
task_map = Dict(Str, Str)
# Notice that the default context for trait names is that of the model
# object, and that we must prefix names for this object with 'handler.'.
view = View(
Group(
Item('always_use_default_layout'),
Item(
'default_task',
editor=EnumEditor(name='handler.task_map'),
enabled_when='always_use_default_layout',
),
label='Application startup',
),
resizable=True,
)
def _task_map_default(self):
return dict(
(factory.id, factory.name)
for factory in self.dialog.application.task_factories
)
Finally, we modify our application to make use of this new functionality:
class AttractorsApplication(TasksApplication):
[ ... ]
#### 'TasksApplication' interface #####################################
default_layout = List(TaskWindowLayout)
always_use_default_layout = Property(Bool)
#### 'AttractorsApplication' interface ################################
preferences_helper = Instance(AttractorsPreferences)
def _default_layout_default(self):
active_task = self.preferences_helper.default_task
tasks = [factory.id for factory in self.task_factories]
return [
TaskWindowLayout(
*tasks,
active_task=active_task,
size=(800, 600),
),
]
def _get_always_use_default_layout(self):
return self.preferences_helper.always_use_default_layout
def _preferences_helper_default(self):
return AttractorsPreferences(preferences=self.preferences)
and contribute the preferences pane to the Tasks plugin:
class AttractorsPlugin(Plugin):
[ ... ]
preferences_panes = List(
contributes_to='envisage.ui.tasks.preferences_panes'
)
def _preferences_panes_default(self):
return [AttractorsPreferencesPane]
Extending an Existing Task¶
Contributions are made to an existing task via the TaskExtension
class,
which was briefly introduced above. TaskExtension
is a simple class with
three attributes:
task_id
: The ID of the task to extend.actions
: A list ofSchemaAddition
objects.dock_pane_factories
: A list of callables for creating dock panes.
The second attributes requires further discussion. In the previous section, we
remarked that a task’s menu and tool bars are defined using schemas; the
SchemaAddition
class provides a mechanism for inserting new items into these
schemas.
A schema implicitly defines a path for each of its elements. For example, in the schema:
SMenuBar(
SMenu(
SGroup(
[ ... ],
id='SaveGroup',
),
[ ... ],
id='File',
name='&File,
),
SMenu(
[ ... ],
id='Edit',
name='&Edit',
),
)
the edit menu has the path “MenuBar/Edit”. Likewise, the save group in the file menu has the path “MenuBar/File/SaveGroup”. We might define an addition for this menu as follows:
SchemaAddition(
factory=MyContributedGroup,
path='MenuBar/File',
)
where factory
is a callable that produces either a schema or an object from
the Pyface action API [2]. A schema addition that produces a schema can in turn
be extended by another schema addition. If it produces a Pyface object, it
cannot be further extended. In this case we have opted for latter, using a
custom subclass of Group
.
The group created from the schema addition above would be inserted at the bottom
of the file menu. The SchemaAddition
class provides two further attributes
for specifying with greater precision the location of the insertion: before
and after
. Setting one of these attributes to the ID of a schema with the
same path ensures that the insertion will be made before or after, respectively,
that schema. For example, in the expanded addition:
SchemaAddition(
factory=MyContributedGroup,
before='SaveGroup',
path='MenuBar/File',
)
the created group would be inserted before the save group. If both before
and after
are set, Tasks will attempt to honor both of them [3]. In the
event that Tasks cannot, the menu order is undefined (although the insertions
are guaranteed to made) and an error is logged.
Global Task Extensions¶
When creating an application with several tasks it is frequently the case that certain menu bar or tool bar actions should be present in all tasks. Such actions might include an “Exit” item in the “File” menu or an “About” item in the “Help” menu. One can, of course, include these items in the schemas of each task; indeed, if the actions require task-specific behavior, this is the only reasonable approach to take. But for actions that are truly global in nature Tasks provides an alternative that may be more convenient.
To create a TaskExtension
that applies to all tasks, simply omit the
task_id
attribute. Tasks itself contributes a global task extension with the
following menu items:
A group of actions in the menu with ID “View” for toggling the visibility of dock panes (see
pyface.tasks.action.api.DockPaneToggleGroup
)A “Preferences” action in the menu with ID “Edit”, if the application has any preferences panes
An “Exit” action in the menu with ID “File”
The user is free to supplement these items by contributing additional global
task extensions. For example, to provide a simple mechanism for changing tasks,
one might add include the built-in task switching group in the “View” menu,
either at the toplevel or as a sub-menu (see
pyface.tasks.action.api.TaskToggleGroup
). For switching between
windows, Tasks includes the TaskWindowToggleGroup
. This class, as well as
several other menu-related conveniences, can be found in
envisage.ui.tasks.action.api
.
Footnotes