.. _extensibility: =============== Extensibility =============== .. index:: Envisage 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 :ref:`introduction`, some familiarity with the Envisage plugin framework is assumed. For more information about Envisage, the reader is referred to the :doc:`../envisage_core_documentation/index`. .. _tasks-plugin: The Tasks Plugin ================ In creating an extensible Tasks application, we imagine two primary extensibility use cases: 1. **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. 2. **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: .. index:: TaskFactory 1. ``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. .. index:: TaskExtension 2. ``envisage.ui.tasks.task_extensions``: A list of ``TaskExtension`` instances. A ``TaskExtension`` 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 :ref:`extending-a-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 :ref:`creating-a-preferences-dialog`. .. _tasks-applications: .. index:: application, TasksApplication 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, ), ] .. index:: application; layout, TaskWindowLayout 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. .. index:: application; state restoration 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 :ref:`global-task-extensions` that the Tasks plugins provides a few built-in extensions for implementing common behaviors. .. _creating-a-preferences-dialog: Creating a Preferences Dialog ============================= .. index:: preferences 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. 1. ``envisage.preferences``: A list of locators for default preferences files (INI files). This extension point is at the model level in the preferences system. .. index:: preferences; category 2. ``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. .. index:: preferences; pane 3. ``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 a ``PreferencesHelper``. A preferences pane has a name and an ID, as well as a ``category`` 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-a-task: 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: 1. ``task_id``: The ID of the task to extend. 2. ``actions``: A list of ``SchemaAddition`` objects. 3. ``dock_pane_factories``: A list of callables for creating dock panes. .. index:: SchemaAddition 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. .. index:: path 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``. .. index:: before, after 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: Global Task Extensions ======================= .. index:: TaskExtension; global 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``. .. rubric:: Footnotes .. [1] In this section, we shall be referencing--often with considerable simplification--the Attractors example code in the EnvisagePlugins package, available :github-demo:`online ` and in the ETS distribution. .. [2] Although they are expanded into Pyface action items, schemas belong to a distinct API. It is beyond the scope of this document to describe the Pyface action API. For more complete documentation, the reader is referred to the `Pyface documentation `_. .. [3] Tasks differs from the Workbench in this regard.