Source code for enaml.core.messenger
#------------------------------------------------------------------------------
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#------------------------------------------------------------------------------
from traits.api import Instance, Uninitialized
from enaml.utils import LoopbackGuard
from .declarative import Declarative
from .object import Object
class PublishAttributeNotifier(object):
""" A lightweight trait change notifier used by Messenger.
"""
def __call__(self, obj, name, old, new):
""" Called by traits to dispatch the notifier.
"""
if old is not Uninitialized and name not in obj.loopback_guard:
obj.send_action('set_' + name, {name: new})
def equals(self, other):
""" Compares this notifier against another for equality.
"""
return False
# Only a single instance of PublishAttributeNotifier is needed.
PublishAttributeNotifier = PublishAttributeNotifier()
[docs]class ChildrenChangedTask(object):
""" A task for posting a children changed event to a client.
Instances of this class can be posted to the `batch_action_task`
method of Object to send a 'children_changed' action to a client
object. This task will ensure that new children are initialized
and activated using session object of the provided parent.
"""
[docs] def __init__(self, parent, event):
""" Initialize a ChildrenChangedTask.
Parameters
----------
parent : Object
The object to which the children event was posted.
event : ChildrenEvent
The children event posted to the parent.
"""
self._parent = parent
self._event = event
[docs] def __call__(self):
""" Create the content dictionary for the task.
This method will also initialize and activate any new objects
which were added to the parent.
"""
event = self._event
content = {}
new_set = set(event.new)
old_set = set(event.old)
added = new_set - old_set
removed = old_set - new_set
for obj in added:
if obj.is_inactive:
obj.initialize()
content['order'] = [
c.object_id for c in event.new if isinstance(c, Messenger)
]
content['removed'] = [
c.object_id for c in removed if isinstance(c, Messenger)
]
content['added'] = [
c.snapshot() for c in added if isinstance(c, Messenger)
]
session = self._parent.session
for obj in added:
if obj.is_initialized:
obj.activate(session)
return content
[docs]class Messenger(Declarative):
""" A base class for creating messaging enabled Enaml objects.
This is a Declarative subclass which provides convenient APIs for
sharing state between a server-side Enaml object and a client-side
implementation of that object.
"""
#: A loopback guard which can be used to prevent a notification
#: cycle when setting attributes from within an action handler.
loopback_guard = Instance(LoopbackGuard, ())
#--------------------------------------------------------------------------
# Lifetime API
#--------------------------------------------------------------------------
[docs] def post_initialize(self):
""" A reimplemented post initialization method.
This method calls the `bind` method after calling the superclass
class version.
"""
super(Messenger, self).post_initialize()
self.bind()
[docs] def bind(self):
""" Called during initialization pass to bind change handlers.
This method is called at the end of widget initialization to
provide a subclass the opportunity to setup any required change
notification handlers for publishing their state to the client.
"""
pass
#--------------------------------------------------------------------------
# Snapshot API
#--------------------------------------------------------------------------
[docs] def snapshot(self):
""" Get a dictionary representation of the widget tree.
This method can be called to get a dictionary representation of
the current state of the widget tree which can be used by client
side implementation to construct their own implementation tree.
Returns
-------
result : dict
A serializable dictionary representation of the widget tree
from this widget down.
"""
snap = {}
snap['object_id'] = self.object_id
snap['name'] = self.name
snap['class'] = self.class_name()
snap['bases'] = self.base_names()
snap['children'] = [c.snapshot() for c in self.snap_children()]
return snap
[docs] def snap_children(self):
""" Get an iterable of children to include in the snapshot.
The default implementation returns the list of children which
are instances of Messenger. Subclasses may reimplement this
method if more control is needed.
Returns
-------
result : list
The list of children which are instances of Messenger.
"""
return [c for c in self.children if isinstance(c, Messenger)]
[docs] def class_name(self):
""" Get the name of the class for this instance.
Returns
-------
result : str
The name of the class of this instance.
"""
return type(self).__name__
[docs] def base_names(self):
""" Get the list of base class names for this instance.
Returns
-------
result : list
The list of string names for the base classes of this
instance. The list starts with the parent class of this
instance and terminates with Object.
"""
names = []
for base in type(self).mro()[1:]:
names.append(base.__name__)
if base is Object:
break
return names
#--------------------------------------------------------------------------
# Messaging Support
#--------------------------------------------------------------------------
[docs] def set_guarded(self, **attrs):
""" Set attribute values from within a loopback guard.
This is a convenience method provided for subclasses to set the
values of attributes from within a loopback guard. This prevents
the change from being published back to client and reduces the
chances of getting hung in a feedback loop.
Parameters
----------
**attrs
The attributes to set on the object from within a loopback
guard context.
"""
with self.loopback_guard(*attrs):
for name, value in attrs.iteritems():
setattr(self, name, value)
[docs] def publish_attributes(self, *attrs):
""" A convenience method provided for subclasses to publish
attribute changes as actions to the client.
The action name is created by prefixing 'set_' to the name of
the changed attribute. This method is suitable for most cases
of simple attribute publishing. More complex cases will need
to implement their own dispatching handlers. The handler for
the changes will only send the action message if the attribute
name is not held by the loopback guard.
Parameters
----------
*attrs
The string names of the attributes to publish to the client.
The values of these attributes should be JSON serializable.
More complex values should use their own dispatch handlers.
"""
for attr in attrs:
self.add_notifier(attr, PublishAttributeNotifier)
[docs] def children_event(self, event):
""" Handle a `ChildrenEvent` for the widget.
If the widget state is 'active', a `children_changed` action
will be sent to the client widget. The action is sent before
the superclass' handler is called, and will therefore precede
the trait change notification.
"""
super(Messenger, self).children_event(event)
# Children events are fired all the time during initialization,
# so only batch the children task if the widget is activated.
# The children may not be fully instantiated when this event is
# fired, and they may still be executing their constructor. The
# batched task allows the children to finish initializing before
# their snapshot is taken.
if self.is_active:
task = ChildrenChangedTask(self, event)
self.batch_action_task('children_changed', task)