Source code for enaml.session

#------------------------------------------------------------------------------
#  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)