1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
|
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.
Concepts
--------
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:
Registration
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?
Dependencies
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?
Notifications
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?
Flexibility
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.
Terminology
-----------
Before continuuing with the crash course, the following terminology is
introduced and used throughout the rest of the document.
Workbench
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.
Plugin
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.
Extension
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.
Workflow
--------
Using the workbench framework is relatively straightforward and has only
a few conceptual steps.
0. Define the classes which implement your application business logic.
1. 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.
2. 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.
3. 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.
4. Create an instance of Workbench or one of its subclasses.
5. 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.
6. 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.
Core Classes
------------
This section covers the core classes of the workbench framework.
Workbench
~~~~~~~~~
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:
register
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.
unregister
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.
get_plugin
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.
get_extension_point
This method will return the extension point declared by a plugin. The
extension point can be queried for contributed extensions at runtime.
PluginManifest
~~~~~~~~~~~~~~
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:
id
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.
factory
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.
Plugin
~~~~~~
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 extended. That said, there
are two methods on a plugin which will be of interest to developers:
start
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.
stop
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.
ExtensionPoint
~~~~~~~~~~~~~~
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:
id
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.
Extension
~~~~~~~~~
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:
id
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.
point
The fully qualified id of the extension point to which the extension
is contributing.
rank
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.
factory
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.
Core Plugin
-----------
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:
'commands'
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.
Command
~~~~~~~
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:
id
The globally unique identifier for the command. This should be a
dot-separated string. The global uniqueness is enforced.
handler
A callable object which implements the command behavior. It must
accept a single argument which is an instance of `ExecutionEvent`.
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:
command
The Command object which is being invoked.
workbench
A reference to the workbench which owns the command.
parameters
A dictionary of user-supplied parameters to the command.
trigger
The user object which triggered the command.
UI Plugin
---------
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:
'application_factory'
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.
'window_factory'
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.
'branding'
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.
'actions'
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.
'workspaces'
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.
'autostart'
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:
'default_application_factory'
This contributes to the 'enaml.workbench.ui.application_factory'
extension point and provides a default instance of a QtApplication.
'default_window_factory'
This contributes to the 'enaml.workbench.ui.window_factory' extension
point and provides a default instance of a WorkbenchWindow.
'default_commands'
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:
'enaml.workbench.ui.close_window'
This command will close the primary application window. It takes
no parameters.
'enaml.workbench.ui.close_workspace'
This command will close the currently active workspace. It takes
no parameters.
'enaml.workbench.ui.select_workspace'
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.
WorkbenchWindow
~~~~~~~~~~~~~~~
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`.
Branding
~~~~~~~~
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:
title
The string to use as the primary title of the main window.
icon
The icon to use for the icon of the main window and taskbar.
MenuItem
~~~~~~~~
The MenuItem class is a declarative class which can be used to declare a
menu in the primary window menu bar.
The MenuItem class can be imported from `enaml.workbench.ui.api`.
It has the following attributes of interest:
path
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
menu 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 menu with respect to its siblings.
*The parent menu need not be defined by the same extension which
defines the menu. That is, one plugin can contribute a sub-menu to
a menu defined by another plugin.*
group
The name of the item group defined by the parent menu to which this
menu item should be added. For a top-level menu item, the empty group
is automatically implied.
before
The id of the sibling item before which this menu item should appear.
The sibling must exist in the same group as this menu item.
after
The id of the sibling item after which this menu item should appear.
This sibling must exist in the same group as this menu item.
label
The text to diplay as the label for the menu.
visible
Whether or not the menu is visible.
enabled
Whether or not the menu is enabled.
A MenuItem can define conceptual groups in which other plugins may contribute
other menu items and action items. A group is defined by declaring a child
ItemGroup object on the menu item. The group will appear on screen in the
order in which they were declared. There is an implicit group with an empty
identifier into which all unclassified items are added. The implicit group
will always appear visually last on the screen.
ItemGroup
~~~~~~~~~
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:
id
The identifier of the group within the menu. It must be unique among
all other group siblings defined for the menu item.
visible
Whether or not the items in the group are visible.
enabled
Whether or not the items in the group are enabled.
exclusive
Whether or not neighboring checkable action items in the group
should behave as exclusive checkable items.
ActionItem
~~~~~~~~~~
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:
path
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.*
group
The name of the item group defined by the parent menu to which this
action item should be added.
before
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.
after
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.
command
The identifier of the Command object which should be invoked when
this action item is triggered by the user.
parameters
The dictionary of parameters which should be passed to the command
when it is invoked.
label
The text to diplay as the label for the action.
shortcut
The keyboard shortcut which should be bound to trigger action item.
visible
Whether or not the action is visible.
enabled
Whether or not the action is enabled.
checkable
Whether or not the action is checkable.
checked
Whether or not the action is checked.
icon
The icon to display next to the action.
tool_tip
The tool tip text to display when the user hovers over the action.
status_tip
The text to display in the status bar when the user hovers over the
action.
Workspace
~~~~~~~~~
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:
window_title
This is text which will be added to the window title *in addition*
to the title text which is supplied by a branding extension.
content
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:
start
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.
stop
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.
Autostart
~~~~~~~~~
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.
plugin_id
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.
UI Workbench
------------
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:
run
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.
|