#------------------------------------------------------------------------------
# Copyright (c) 2012, Enthought, Inc.
# All rights reserved.
#------------------------------------------------------------------------------
import logging
from traits.api import HasTraits, Instance, List, Str, ReadOnly, Enum, Property
from enaml.widgets.window import Window
from .application import deferred_call
from .resource_manager import ResourceManager
from .signaling import Signal
from .socket_interface import ActionSocketInterface
from .utils import make_dispatcher
logger = logging.getLogger(__name__)
#: The set of actions which should be batched and sent to the client as
#: a single message. This allows a client to perform intelligent message
#: handling when dealing with messages that may affect the widget tree.
BATCH_ACTIONS = set(['destroy', 'children_changed', 'relayout'])
#: The dispatch function for action dispatching on the session.
dispatch_action = make_dispatcher('on_action_', logger)
[docs]class DeferredMessageBatch(object):
""" A class which aggregates batch messages.
Each time a message is added to this object, its tick count is
incremented and a tick down event is posted to the event queue.
When the object receives the tick down event, it decrements its
tick count, and if it's zero, fires the `triggered` signal.
This allows a consumer of the batch to continually add messages and
have the `triggered` signal fired only when the event queue is fully
drained of relevant messages.
"""
#: A signal emitted when the tick count of the batch reaches zero
#: and the owner of the batch should consume the messages.
triggered = Signal()
[docs] def __init__(self):
""" Initialize a DeferredMessageBatch.
"""
self._messages = []
self._tick = 0
#--------------------------------------------------------------------------
# Private API
#--------------------------------------------------------------------------
def _tick_down(self):
""" A private handler method which ticks down the batch.
The tick down events are called in a deferred fashion to allow
for the aggregation of batch events. When the tick reaches
zero, the `triggered` signal will be emitted.
"""
self._tick -= 1
if self._tick == 0:
self.triggered.emit()
else:
deferred_call(self._tick_down)
#--------------------------------------------------------------------------
# Public API
#--------------------------------------------------------------------------
[docs] def release(self):
""" Release the messages that were added to the batch.
Returns
-------
result : list
The list of messages added to the batch.
"""
messages = self._messages
self._messages = []
return messages
[docs] def add_message(self, message):
""" Add a message to the batch.
This will cause the batch to tick up and then start the tick
down process if necessary.
Parameters
----------
message : object
The message object to add to the batch.
"""
self._messages.append(message)
if self._tick == 0:
deferred_call(self._tick_down)
self._tick += 1
[docs]class URLReply(object):
""" A reply object for sending a loaded resource to a client session.
"""
__slots__ = ('_session', '_req_id', '_url')
[docs] def __init__(self, session, req_id, url):
""" Initialize a URLReply.
Parameters
----------
session : Session
The session object for which the image is being loaded.
req_id : str
The identifier that was sent with the originating request.
This identifier will be included in the response.
url : str
The url that was sent with the originating request. This
url will be included in the response.
"""
self._session = session
self._req_id = req_id
self._url = url
[docs] def __call__(self, resource):
""" Send the reply to the client session.
Parameters
----------
resource : Resource
The loaded resource object, or None if the resource failed
to load.
"""
reply = {'id': self._req_id, 'url': self._url}
if resource is None:
reply['status'] = 'fail'
else:
reply['status'] = 'ok'
reply['resource'] = resource.snapshot()
session = self._session
session.send(session.session_id, 'url_reply', reply)
[docs]class Session(HasTraits):
""" An object representing the session between a client and its
Enaml objects.
The session object is what ensures that each client has their own
individual instances of objects, so that the only state that is
shared between simultaneously existing clients is that which is
explicitly provided by the developer.
"""
#: The string identifier for this session. This is provided by
#: the application when the session is opened. The value should
#: not be manipulated by user code.
session_id = ReadOnly
#: The top level windows which are managed by this session. This
#: should be populated by user code during the `on_open` method.
windows = List(Window)
#: The widget implementation groups which should be used by the
#: widgets in this session. Widget groups are an advanced feature
#: which allow the developer to selectively expose toolkit specific
#: implementations of Enaml widgets. All standard Enaml widgets are
#: available in the 'default' group. This value will rarely need to
#: be changed by the user.
widget_groups = List(Str, ['default'])
#: A resource manager used for loading resources for the session.
resource_manager = Instance(ResourceManager, ())
#: The socket used by this session for communication. This is
#: provided by the Application when the session is activated.
#: The value should not normally be manipulated by user code.
socket = Instance(ActionSocketInterface)
#: The current state of the session. This value is changed by the
#: by the application as it drives the session through its lifetime.
#: This should not be manipulated directly by user code.
state = Enum(
'inactive', 'opening', 'opened', 'activating', 'active', 'closing',
'closed',
)
#: A read-only property which is True if the session is inactive.
is_inactive = Property(fget=lambda self: self.state == 'inactive')
#: A read-only property which is True if the session is opening.
is_opening = Property(fget=lambda self: self.state == 'opening')
#: A read-only property which is True if the session is opened.
is_opened = Property(fget=lambda self: self.state == 'opened')
#: A read-only property which is True if the session is activating.
is_activating = Property(fget=lambda self: self.state == 'activating')
#: A read-only property which is True if the session is active.
is_active = Property(fget=lambda self: self.state == 'active')
#: A read-only property which is True if the session is closing.
is_closing = Property(fget=lambda self: self.state == 'closing')
#: A read-only property which is True if the session is closed.
is_closed = Property(fget=lambda self: self.state == 'closed')
#: A private dictionary of objects registered with this session.
#: This value should not be manipulated by user code.
_registered_objects = Instance(dict, ())
#: The private deferred message batch used for collapsing layout
#: related messages into a single batch to send to the client
#: session for more efficient handling.
_batch = Instance(DeferredMessageBatch)
def __batch_default(self):
batch = DeferredMessageBatch()
batch.triggered.connect(self._on_batch_triggered)
return batch
#--------------------------------------------------------------------------
# Class API
#--------------------------------------------------------------------------
@classmethod
[docs] def factory(cls, name='', description='', *args, **kwargs):
""" Get a SessionFactory instance for this Session class.
Parameters
----------
name : str, optional
The name to use for the session instances. The default uses
the class name.
description : str, optional
A human friendly description of the session. The default uses
the class docstring.
*args, **kwargs
Any positional and keyword arguments to pass to the session
when it is instantiated.
"""
from enaml.session_factory import SessionFactory
if not name:
name = cls.__name__
if not description:
description = cls.__doc__
return SessionFactory(name, description, cls, *args, **kwargs)
#--------------------------------------------------------------------------
# Private API
#--------------------------------------------------------------------------
def _on_batch_triggered(self):
""" A signal handler for the `triggered` signal on the deferred
message batch.
"""
content = {'batch': self._batch.release()}
self.send(self.session_id, 'message_batch', content)
#--------------------------------------------------------------------------
# Abstract API
#--------------------------------------------------------------------------
[docs] def on_open(self):
""" Called by the application when the session is opened.
This method must be implemented in a subclass and is called to
create the Enaml objects for the session. This method will only
be called once during the session lifetime. User code should
create their windows and assign them to the list of `windows`
before the method returns.
"""
raise NotImplementedError
[docs] def on_close(self):
""" Called by the application when the session is closed.
This method may be optionally implemented by subclasses so that
they can perform custom cleaup. After this method returns, the
session should be considered invalid. This method is only called
once during the session lifetime.
"""
pass
#--------------------------------------------------------------------------
# Public API
#--------------------------------------------------------------------------
[docs] def open(self, session_id):
""" Called by the application to open the session.
This method will call the `on_open` abstract method which must
be implemented by subclasses. The method should never be called
by user code.
Parameters
----------
session_id : str
The unique identifier to use for this session.
"""
self.session_id = session_id
self.state = 'opening'
self.on_open()
for window in self.windows:
window.initialize()
self.state = 'opened'
[docs] def activate(self, socket):
""" Called by the application to activate the session and its
windows.
This method will be called by the Application once during the
session lifetime. Once this method returns, the session and
its objects will be ready to send and receive messages. This
should never be called by user code.
Parameters
----------
socket : ActionSocketInterface
A concrete implementation of ActionSocketInterface to use
for messaging by this session.
"""
self.state = 'activating'
for window in self.windows:
window.activate(self)
self.socket = socket
socket.on_message(self.on_message)
self.state = 'active'
[docs] def close(self):
""" Called by the application when the session is closed.
This method will call the `on_close` method which can optionally
be implemented by subclasses. The method should never be called
by user code.
"""
self.send(self.session_id, 'close', {})
self.state = 'closing'
self.on_close()
for window in self.windows:
window.destroy()
self.windows = []
self._registered_objects = {}
self.socket.on_message(None)
self.socket = None
self.state = 'closed'
[docs] def snapshot(self):
""" Get a snapshot of the windows of this session.
Returns
-------
result : list
A list of snapshots representing the current windows for
this session.
"""
return [window.snapshot() for window in self.windows]
[docs] def register(self, obj):
""" Register an object with the session.
This method is called by an Object when it is activated by a
Session. It should never be called by user code.
Parameters
----------
obj : Object
The object to register with the session.
"""
self._registered_objects[obj.object_id] = obj
[docs] def unregister(self, obj):
""" Unregister an object from the session.
This method is called by an Object when it is being destroyed.
It should never be called by user code.
Parameters
----------
obj : Object
The object to unregister from the session.
"""
self._registered_objects.pop(obj.object_id, None)
#--------------------------------------------------------------------------
# Messaging API
#--------------------------------------------------------------------------
[docs] def send(self, object_id, action, content):
""" Send a message to a client object.
This method is called by the `Object` instances owned by this
session to send messages to their client implementations.
Parameters
----------
object_id : str
The object id of the client object.
action : str
The action that should be performed by the object.
content : dict
The content dictionary for the action.
"""
if self.is_active:
if action in BATCH_ACTIONS:
self._batch.add_message((object_id, action, content))
else:
self.socket.send(object_id, action, content)
[docs] def on_message(self, object_id, action, content):
""" Receive a message sent to an object owned by this session.
This is a handler method registered as the callback for the
action socket. The message will be routed to the appropriate
`Object` instance.
Parameters
----------
object_id : str
The object id of the target object.
action : str
The action that should be performed by the object.
content : dict
The content dictionary for the action.
"""
if self.is_active:
if object_id == self.session_id:
dispatch_action(self, action, content)
else:
try:
obj = self._registered_objects[object_id]
except KeyError:
msg = "Invalid object id sent to Session: %s:%s"
logger.warn(msg % (object_id, action))
return
else:
obj.receive_action(action, content)
#--------------------------------------------------------------------------
# Action Handlers
#--------------------------------------------------------------------------
[docs] def on_action_url_request(self, content):
""" Handle the 'url_request' action from the client session.
"""
url = content['url']
metadata = content['metadata']
reply = URLReply(self, content['id'], url)
self.resource_manager.load(url, metadata, reply)