Design and Architecture ======================= .. warning:: This documentation is not current and does not reflect the way Enaml currently works. Enaml is designed with a flexible, open architecture. It is designed to be able to adapt to different UI toolkit backends beyond the currently supported Qt and Wx backends, as well as allowing other key parts of the infrastructure to be replaced. Construction of a View ^^^^^^^^^^^^^^^^^^^^^^ When building a view, you typically will create it via a sequence of commands like:: import enaml with enaml.imports(): from my_enaml_module import MyView view = MyView(model) view.show() The import step parses and compiles the Enaml file, creating a Python module containing view factories which can be used by importing the appropriate name. When called, these views factories expect model objects to be passed via arguments and then use the UI toolkit to construct the actual UI components that will be used in the view. Finally the show() method starts the application mainloop if needed and makes the UI components visible. The enaml.imports() context manager provides an import hook that detects when an ``.enaml`` file is being imported, parses it into an Enaml AST and uses :py:class:`~enaml.parsing.enaml_compiler.EnamlCompiler` to compile it to Enaml bytecode. From the importer's point of view it creates a standard Python module which has one or more :py:class:`EnamlDefinition` objects which create re-usable UI templates. The :py:class:`EnamlDefinition` objects can be used by other Enaml modules which import them, or directly by Python code. Each :py:class:`EnamlDefinition` instance is a namespace which can have additional variable values supplied as arguments when it is called. Calling an :py:class:`EnamlDefinition` object uses the supplied arguments to build its namespace and then executes the Enaml bytecode to construct the UI shell components. The UI shell components are toolkit independent traited classes which expose the functionality of a toolkit widget in a uniform manner. The toolkit specific widget wrapper objects are managed internally by the shell components. Both of these objects are created through the use of special Toolkit objects. Normally the Toolkit object to use is inferred from the user's environment variables, but a particular toolkit can be selected using a context manager:: from enaml.toolkit import wx_toolkit with wx_tookit(): view = MyView(model) view.show() Finally the show() call on the view object recursively creates the underlying gui toolkit widgets by following a formalized set process. This process calls the following methods on each component in the tree in a top-down fashion: * ``create()`` method to create the gui toolkit widget * ``initialize()`` method to set the initial state of the gui toolkit widget * ``bind()`` method to bind event handlers (or the toolkit equivalents) In addition, widgets which participate in constraints-based layout will have methods called to register their constraints with the appropriate constraint solvers, and then solve the layout. Adding New Widgets ^^^^^^^^^^^^^^^^^^ These layers of abstraction and delegation mean that it is fairly simple to add new widget types in custom applications. To create a new widget, one needs to: 1) Optionally, but ideally, define the abstract interface for your widget, which should be at least a subclass of the abstract base class :py:class:`~enaml.widgets.base_component.AbstractTkBaseComponent` but will likely be a subclass of :py:class:`~enaml.widgets.control.AbstractTkControl` or :py:class:`~enaml.widgets.container.AbstractTkContainer`. Since this is an abstract base class, you shouldn't implement any of the functionality in this class. This class provides the generic API the individual toolkit backends will need to implement, and will provide the methods that the Enaml widget will call in order to communicate with the gui toolkit widget. In particular, there needs to be a specially named :py:meth:`shell_*_changed(self, value)` change handler for every dynamic Trait on the Enaml shell version of the Widget. These methods will allow the toolkit widget to appropriately react to changes from the user's code. 2) Create the Enaml shell version of the Widget. This will at least be a subclass of :py:class:`~enaml.widgets.base_component.BaseComponent`, and most likely a subclass of :py:class:`~enaml.widgets.control.Control` or :py:class:`~enaml.widgets.container.AbstractTkContainer`. This class defines the interface that the Enaml markup language sees and can use. There should be, at a minimum, traits corresponding to values that can be read or changed on the widget, as well as methods for all standard actions for which access should be supplied. This class is not abstract, and should provide all the functionality required in a toolkit-independent manner. This must define a trait called :py:attr:`abstract_obj` which is an :py:class:`Instance()` of the implementation interface defined in the previous step. 3) Create a version of the Widget for each backend that you need to support. Each of these will be a subclass of the appropriate backend-specific component, such as :py:class:`~enaml.widgets.wx.wx_base_component.WXBaseComponent` or :py:class:`~enaml.widgets.qt.qt_base_component.QtBaseComponent` as well as subclassing the abstract interface defined in the first step. Once again, these are most likely to be subclasses of the appropriate Control classes. Instances of this class will have a :py:attr:`shell_obj` attribute which provides a reference to the Enaml shell widget instance for that control so that values can be obtained and inspected. This attribute is provided by the base class and will normally not need to be overridden. This class must then, obviously, provide a concrete implemetation of the abstract interface. In particular, it must provide the following methods (even if they are no-ops or implemented in a superclass): :py:meth:`create(self)` This is responsible for creating the underlying toolkit objects or widgets that the Enaml shell widget requires as part of its UI. e.g. create the QPushButton or wx.Button widget. You will almost always have to write this method. :py:meth:`initialize(self)` This is responsible for initializing the state of the toolkit object or objects based on the state of the Enaml shell widget. You will almost always have to write this method. :py:meth:`bind(self)` This is responsible for setting up the initial bindings of toolkit events to handlers on this object. You will almost always have to write this method. If you are writing a composite widget which contains a collection of toolkit widgets, as opposided to a single control-style widget, you may need to override the following: :py:meth:`size_hint(self)` This is responsible for returning a suggested size for the widget in its current state for use by the layout manager. :py:meth:`set_geometry(self, x, y, width, height)` This method is called when the layout system needs to re-position or resize the widget. For a simple single widget control, this would usually just call the appropriate set geometry method on the underlying toolkit widget, but for an Enaml widget composed of multiple toolkit widgets you will need to lay them out relative to each other and the space that they have been provided. :py:meth:`move(self, x, y)` A position-only version of :py:meth:`set_geometry(...)` :py:meth:`resize(self, width, height)` A size-only version of :py:meth:`set_geometry(...)` In addition to these standard methods, you will need to provide implementations for each of the methods you declared in the first step: :py:meth:`shell_*_changed(self, value)` This has to react to a change to the appropriate trait on the Enaml widget and change the appropriate toolkit state. as well as any other methods that may be needed. If you are writing a control, you may need to handle error and exceptions generated by invalid values, either coming in to the widget from the underlying model, or from values entered by the user. The :py:class:`enaml.widgets.control.Control` class provides a standard API for registering these:: :py:attr:`error` This is a boolean trait which is True if an invalid value was entered. :py:attr:`exception` This is a trait which holds the Exception object that caused the error to be flagged. The :py:class:`enaml.widgets.control.Control` class provides two helper contexts which can be used to automatically capture any exceptions:: :py:meth:`capture_exceptions` This will capture any exceptions generated by a block of code in a with statement, and will automatically set or clear the error state appropriately. Because of the way that exceptions work in trait notification handlers, this may fail to capture errors generated by delegation or notification expresssions. :py:meth:`capture_notification_exceptions` This will capture any exceptions including exceptions generated in notification handlers fired by traits in response to changes within code in a with statement. Finally, to assist in debugging and logging, the toolkit object has a :py:attr:`control_exception_handler` callback that can be supplied which will be called with a single argument which is an exception captured by either of the above contexts. .. warning:: These methods are outdated and for the moment is only a placeholder To handle styling :py:meth:`create_style_handler(self)` This is responsible for creating a :py:class:`StyleHandler` instance. You may need to implement a custom subclass of :py:class:`StyleHandler` if your widget has unusual styling needs. If your styling needs are simple, you may be able to define an appropriate :py:attr:`tags` class attribute which maps supported style tags to toolkit-dependent information, and use the default implementation of the method from the toolkit. :py:meth:`initialize_style(self)` This method is responsible for initializing the values on the :py:class:`StyleHandler` class created by the previous method. If your styling needs are simple, you may be able to use the default toolkit implementation of this class. :py:meth:`layout_child_widgets(self)` This method is used by :py:class:`Container` implementations to insert child widgets into the appropriate toolkit-specific layout object, and set the appropriate attributes and properties of this object. Most simple Control subclasses do not need to implement this, since they do not have child widgets. 4) Create the toolkit constructor and add it to the appropriate toolkit object. There are several ways to do this, depending on your goals: * if you are adding a new control type to the main Enaml source, then you can directly create a constructor in the toolkit's ``constructors.py`` module. This module contains a dictionary of constructors and a utility function for building them assuming that you have followed a naming pattern for your classes which is consistent with the rest of the toolkit widgets. Typically this will look something like:: QT_CONSTRUCTORS = dict(( ... constructor('my_new_widget'), )) * if you are adding a new control type that is specific to your code and not part of the main Enaml system, then you will need to manually create an :py:class:`~enaml.toolkit.Constructor` instance and add it to an appropriate toolkit. Building a constructor is simply a matter of creating a new :py:class:`~enaml.toolkit.Constructor` with your Enaml shell class from step (2) and your toolkit backend class from step (3). Typical code for this would look like:: from enaml.toolkit import Constructor def my_new_widget(): from my_widgets.my_new_widgets import MyNewWidget return MyNewWidget def my_new_qt_widget(): from my_widgets.qt.qt_my_new_widgets import QtMyNewWidget ctor = Constructor(my_new_widget, my_new_qt_widget) The items passed to the Constructor are callables which return the appropriate classes, so that importing of the necessary modules can be delayed until the objects actually need to be used. This helps to drastically reduce runtime overhead for simple applications which only use a small portion of a ui toolkit. Once you have the constructor you need to add it to a toolkit. If you want this to be globally available in your process as part of the appropriate toolkit then you need to add it to the toolkit's constructor dictionary before you create any views:: from enaml.widgets.qt.constructors import QT_CONSTRUCTORS QT_CONSTRUCTORS['MyNewWidget'] = ctor Any subsequent calls to :py:func:`~enaml.toolkit.qt_toolkit` will now contain your new widget. Alternatively, you may want to create your own toolkit that is separate from the usual backend toolkit:: from enaml.toolkit import qt_toolkit my_toolkit = qt_toolkit() my_toolkit['MyNewWidget'] = ctor This will create a new toolkit which has all of the widgets in the standard Qt toolkit, but also includes yours. Code can then choose whether to use the standard Qt toolkit or your new toolkit as appropriate. * There is a convienence built into the constructors for the cases where a custom widget is only a simple subclass of an existing shell component. Suppose we wish to create a FloatField which is a simple subclass of Field that hard-codes the converter object to a float converter:: from traits.api import Constant from enaml.converters import FloatConverter from enaml.widgets.field import Field class FloatField(Field): converter = Constant(FloatConverter()) It would be silly to require the definition of a new toolkit implementation class for each backend, since the implementation class doesn't need to change. Instead, we can make sure that our new subclass uses the appropriate implementation but creating a clone of its constructor:: from enaml.toolkits import qt_toolkit def my_float_field(): return FloatField my_toolkit = qt_toolkit() field_constructor = my_toolkit['Field'] my_constructor = field_constructor.clone(my_float_field) my_toolkit['FloatField'] = my_constructor This toolkit will now always be sure to use the proper toolkit widget for the FloatField. Implementing A New Toolkit ^^^^^^^^^^^^^^^^^^^^^^^^^^ Currently, Enaml supports the Qt toolkit and the Wx toolkit (Wx officially on Windows only). The architecture is designed to be as toolkit-independent as possible. To implement a new toolkit, you will need to perform the following steps: 1) Create a constructor dictionary for your toolkit. You should be able to take the ``constructor.py`` module from either the Qt or Wx backends and modify the constructor factory function to import from the correct packages and mangle the class names appropriately. 2) Create a default stylesheet for your toolkit. Initially it may be sufficient to copy the stylesheet for an existing backend, since the stylesheet definitions are toolkit-independent. 3) Create a new toolkit factory for your new backend. This should look something like the current :py:class:`enaml.toolkit.wx_toolkit` or :py:class:`enaml.toolkit.qt_toolkit` factories. This factory should create a Toolkit instance, which is a dictionary subclass whose keys are the available Enaml entity names. Usually this will consist of the toolkit's constructor dictionary from (1) together with the standard ``OPERATORS`` from :py:mod:`enaml.toolkit` and a ``utils`` dictionary. In additon the following attributes need to be supplied with callables:: :py:attr:`create_app` A function that is responsible for obtaining (or creating, if it doesn't yet exist) the main toolkit application object, or otherwise performing whatever initialization is needed to allow widgets to be created. It should not start the main event loop, however. This should return the application object, if appropriate. :py:attr:`start_app` A function that takes an application object returned by :py:attr:`create_app` and starts the main event loop. :py:attr:`style_sheet` The default stylesheet for your toolkit. 4) Write toolkit-specific implementations of each Enaml widget. See the previous section for discussion for the methods that you will need to implement on this class. This is where the bulk of the work will be performed. 5) Write the implementations of auxilliary objects, such as dialog windows. If all of the above steps are performed correctly, you should be able to display any Enaml UI in your new toolkit. Using A Different Notification Model ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Enaml uses Enthought's Traits system by default for handling binding and notification of expressions to model attributes. You may have existing code which uses a different system for reacting to changes within the model, and Enaml can be extended to be able to use these systems as well. This would allow developers to write code which might do things like access a model on a remote machine, or stored in a database. To support this sort of behaviour, you will probably want to have a base class that all model objects with this new reaction mechanism inherit from, or some other simple way that these model instances can be distinguished from regular Python or Traits instances. You may then need to implement subclasses of :py:class:`enaml.expressions.AbstractExpression` that correctly handle the interactions that your notification system supports for its models. These subclasses will need to implement appropriate versions of the :py:meth:`bind` and :py:meth:`eval_expression` methods. For the four basic expression bindings, you will most likely need to create subclasses of :py:class:`enaml.expressions.SimpleExpression`, :py:class:`enaml.expressions.UpdatingExpression`, :py:class:`enaml.expressions.DelegatingExpression`, and :py:class:`enaml.expressions.NotifyingExpression`. When implementing overriden methods, all of these subclasses must check to see whether the model object is of the new model type, and if it is not then they need to fall back to using the standard superclass implementation of the method. If this is not done then expressions involving widget traits will fail to work correctly. :py:class:`~enaml.expressions.SimpleExpression` This class needs to be able to provide a default value for the expression, but does not need to react to changes in the model object or in the Enaml namespace. You may need to override the :py:meth:`eval_expression` handler to compute the default value from the model, but ideally you should be able to use this class unmodified. :py:class:`~enaml.expressions.UpdatingExpression` This class needs to provide a default value for the expression, but also needs to analyze the expression for dependencies and react to changes in the dependency values on the model objects. You may need to override the :py:meth:`eval_expression` handler to as in the :py:class:`~enaml.expressions.DefaultExpression` case, but again hopefully the default will be sufficient. You will also need to override the :py:meth:`bind` method to correctly hook up the expression to its dependencies in your model's notification model. This is likely to require walking the provided expression AST to determine dependencies (the AttributeVisitor class may be useful for this) and you may have to register callbacks on an appropriate object. This callback will probably look something like the :py:meth:`update_object()` method, but may need to perform additional steps depending on your model. :py:class:`~enaml.expressions.NotifyingExpression` This class requires the ability to execute a code expression whenever an Enaml attribute changes. You may need to override the :py:meth:`notify()` method to compute the expression correctly, but ideally you should be able to use this class unmodified. :py:class:`~enaml.expressions.DelegatingExpression` This class requires both the ability to analyze and react to changes in expression dependencies, but also push changes from the Enaml trait which it is connected to onto the designated object. This will require an appropriate :py:meth:`bind()` method similar to the one that the :py:class:`~enaml.expressions.BindingExpression` uses, although the allowable expressions are much simpler for :py:class:`~enaml.expressions.DelegatingExpression`. You will also need to override the implementations of :py:meth:`update_object()` and :py:meth:`update_delegate()` to appropriately change the value on the underlying model. Having written these classes, you will need to define operator factories for each of them and override your toolkit's ``OPERATORS``, for example:: from enaml.operators import operator_factory, OPERATORS OPERATORS['__operator_LessLess__'] = operator_factory(MyUpdatingExpression) If it makes sense for your new expression to use a different operator than the standard four, you can define a different name and then the corresponding operator will be available, for example to enable ``<<<`` as an operator:: OPERATORS['__operator_LessLessLess__'] = operator_factory(MyUpdatingExpression) The above changes will be global in nature. If you want to restrict the modified operators to a subset of code, you can create an instance of at Toolkit object and override the operators in just that instance:: from enaml.operators import operator_factory from enaml.toolkit import qt_toolkit my_toolkit = qt_toolkit() my_toolkit['__operator_LessLess__'] = operator_factory(MyUpdatingExpression) Or for even more fine grained control (and are accepting or horrible, horrible hacks) then you can pass in an operator as a local variable to an EnamlDefinition:: enamldef MainWindow(my_model, __operator_LessLessLess__): Window: PushButton: # The <<< operator is resolved to the 2nd argument # to MainWindow text <<< my_model.foo This could also be a keyword argument if desired, or even a module level python function. That is, operators resolved using the same scope rules as the rest of the Enaml file.