Advanced Example

An advanced example of Enaml templates.

This example shows how Enaml templates can be used to automatically generate the body of a form. Template specialization is used to select the proper control for a model attribute at runtime. Template recursion is then used to unroll a list of these controls into the body of a form.

Tip

To see this example in action, download it from advanced and run:

$ enaml-run advanced.enaml

Screenshot

../_images/ex_advanced.png

Example Enaml Code

#------------------------------------------------------------------------------
# Copyright (c) 2013, Nucleic Development Team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
#------------------------------------------------------------------------------
""" An advanced example of Enaml templates.

This example shows how Enaml templates can be used to automatically
generate the body of a form. Template specialization is used to select
the proper control for a model attribute at runtime. Template recursion
is then used to unroll a list of these controls into the body of a form.

<< autodoc-me >>
"""
from __future__ import print_function
from atom.api import Atom, Bool, Enum, Event, Float, Int, Str

from enaml.core.api import DynamicTemplate
from enaml.stdlib.fields import FloatField
from enaml.widgets.api import (
    CheckBox, Container, Field, Form, GroupBox, Label, ObjectCombo, PushButton,
    SpinBox, Window,
)


#------------------------------------------------------------------------------
# "Libaray" Definitions
#------------------------------------------------------------------------------
# The templates and enamldefs defined in this section are ones which can
# be written once and then used as a library. They are more-or-less fully
# generic and will work for a large swath of models.

template FormControl(Attr, MemberType):
    """ A template which generates a control for an AutoForm.

    This default specialization displays read-only text for the value.

    Parameters
    ----------
    Attr : str
        The name of the attribute on 'model' being accessed.

    MemberType : type
        The type of the member being accessed.

    """
    Field:
        read_only = True
        text << str(getattr(model, Attr))


template FormControl(Attr, MemberType: Int):
    """ A form control template specialization for Int members.

    This control uses a spin box to represent the value.

    """
    SpinBox:
        value := getattr(model, Attr)


template FormControl(Attr, MemberType: Str):
    """ A form control template specialization for Str members.

    This control uses a Field to represent the value.

    """
    Field:
        text := getattr(model, Attr)


template FormControl(Attr, MemberType: Float):
    """ A form control template specialization for Float members.

    This control uses a FloatField to represent the value.

    """
    FloatField:
        value := getattr(model, Attr)


template FormControl(Attr, MemberType: Bool):
    """ A form control template specialization for Bool members.

    This control uses a CheckBox to represent the value.

    """
    CheckBox:
        checked := getattr(model, Attr)


template FormControl(Attr, MemberType: Event):
    """ A form control template specialization for Event members.

    This control uses a PushButton to represent the value.

    """
    const ButtonText = Attr[0].upper() + Attr[1:].lower()
    PushButton:
        text = ButtonText
        clicked :: getattr(model, Attr)()


def enum_labels(model, attr):
    """ Return the list of enum labels for the given model and attr.

    """
    items = getattr(type(model), attr).items
    return sorted(items)


template FormControl(Attr, MemberType: Enum):
    """ A form control template specialization for Enum members.

    This control uses an ObjectCombo to represent the value.

    """
    ObjectCombo:
        items = enum_labels(model, Attr)
        selected := getattr(model, Attr)


template FormItem(Attr, MemberType):
    """ A template which generates a pair of items for an AutoForm.

    Parameters
    ----------
    Attr : str
        The name of the attribute on 'model' being accessed.

    MemberType : type
        The type of the member being accessed.

    """
    const LabelText = Attr[0].upper() + Attr[1:].lower()
    Label:
        text = LabelText
    FormControl(Attr, MemberType):
        pass


def form_spec(obtype):
    """ Generate a form specification for an atom type.

    Parameters
    ----------
    obtype : type
        The Atom subclass of interest.

    Returns
    -------
    result : tuple
        A tuple of 2-tuples of (attr, member_type) for all non-private
        members of the class.

    """
    items = []
    for name, member in obtype.members().items():
        if not name.startswith('_'):
            items.append((name, type(member)))
    items.sort()
    return tuple(items)


template ForEach(Spec, Item):
    """ A templated loop which maps a template over a sequence.

    Parameters
    ----------
    Spec : tuple
        A tuple of tuples which are the values to map over the item.

    Item : template
        A template which accepts *values from inner tuples of the spec.

    """
    ForEach(Spec[:-1], Item):
        pass
    Item(*Spec[-1]):
        pass


template ForEach(Spec: (), Item):
    """ The terminating condition for the templated loop.

    """
    pass


template AutoFormBody(ModelType):
    """ A template which builds the body for an AutoForm.

    Parameters
    ----------
    ModelType : type
        The type of the model. This should be an Atom subclass.

    """
    const Spec: tuple = form_spec(ModelType)
    ForEach(Spec, FormItem):
        pass


template AutoFormBody(ModelType: type(None)):
    """ A template specialization for null models.

    """
    pass


enamldef AutoForm(Form):
    """ A Form which automatically generates its body from a model.

    """
    attr model: Atom
    DynamicTemplate:
        base = AutoFormBody
        args = (type(model),)


#------------------------------------------------------------------------------
# Main Models and Views
#------------------------------------------------------------------------------
class FooModel(Atom):
    spam = Int(34)
    ham = Int(42)
    first = Str('first')
    last = Str('last')
    owner = Str('owner')
    time = Float(42.56)
    click = Bool()
    clack = Bool()


class BarModel(Atom):
    name = Str('name')
    trigger = Event()
    choices = Enum('first', 'second', 'third')
    def _observe_trigger(self, change):
        print('I was triggered')


enamldef Main(Window):
    title = 'Advanced Templates'
    attr foo_model = FooModel()
    attr bar_model = BarModel()
    Container:
        GroupBox:
            title = 'Foo Model'
            flat = True
            AutoForm:
                padding = 0
                model = foo_model
        GroupBox:
            title = 'Bar Model'
            flat = True
            AutoForm:
                padding = 0
                model = bar_model