Source code for mlib.core.callbacks

__author__ = 'Nathan'
__doc__ =\
"""
Callbacks system

Events are registered "as needed"


Basic examples
--------------
::

	import mlib.core.callbacks as callbacks

	#Define some callback function
	def test():
		print 'YAY!'

	#Register it to the event "SelectionChanged"
	callbacks.addCallback('SelectionChanged', 'test', test)





reload(callbacks)
print '\n'.join(callbacks.getEvents())


def test():
	print 'YAY!'

callbacks.addCallback('SelectionChanged', 'test', test)
callbacks.removeCallback('SelectionChanged', 'test')

rfunc, dfunc = make_user_event_funcs('myEvent')
callbacks.addEvent('myEvent', rfunc, dfunc)

callbacks.addCallback('myEvent', 'test', test)
callbacks.postEvent('myEvent')



"""

import __main__
import logging
import collections
from functools import partial


import maya.cmds as cmds
import maya.OpenMaya as om

log = logging.getLogger(__name__)

#Load in the globals
callbacks = __main__.__dict__.setdefault(__name__+'.registered_callbacks', {})
events = __main__.__dict__.setdefault(__name__+'.supported_events', {})
event_handles = __main__.__dict__.setdefault(__name__+'.event_handles', {})
event_queue = __main__.__dict__.setdefault(__name__+'.event_queue', {})

Callback = collections.namedtuple('Callback', ['event', 'owner', 'function', 'priority', 'immediate'])
Event = collections.namedtuple('Event', ['name', 'register_func', 'deregister_func', 'disable_undo', 'allow_deferred', 'deferred_low_priority', 'builtin'])

[docs]def getEvents(): """ Get a list of event names that are supported :return: Event names :rtype: list """ return sorted(events.keys())
[docs]def addCallback(event, owner, function, priority=None, immediate=None): """ Add a callback to an event :param event: Event to add callback for :type event: str :param owner: Name of callback owner (Typically your script or plugin __name__) :type owner: str :param function: Any callable function to execute when the event occurs. Some events also pass data to the callback function. :type function: function :param priority: (Optional) Callback priority. Callbacks are executed in priority order. :type priority: int :param immediate: Execute this callback immediately on event occurance. default compresses multiple rapid events and delays until next idle. :type immediate: bool """ callback = Callback(event, owner, function, priority, immediate) #Check if event is registered if not event in event_handles: _registerEvent(event) callbacks.setdefault(event, {})[owner] = callback
[docs]def removeCallback(event, owner): """ Remove a callback from an event :param event: Event to remove callback for :type event: str :param owner: Name of callback owner to remove (See :py:func:`.addCallback`) :type owner: str """ event_callbacks = callbacks.get(event, []) if owner in event_callbacks: event_callbacks.pop(owner) #Clean up unused event if not event_callbacks: _deregisterEvent(event) return True return False
[docs]def getCallbacks(event=None): """ Returns a list of all callbacks, sorted by priority :param event: (Optional) event name to filter to :type event: str or None :return: callbacks :rtype: list """ #Sorter priority_sort = lambda callback: (callback.event.lower(), callback.priority, callback.immediate, callback.owner,) if event is not None: return sorted(callbacks.get(event, {}).values(), key=priority_sort, reverse=True) return sorted(sum(callbacks.values()), key=priority_sort, reverse=True)
[docs]def postEvent(event, *args, **kwargs): """ Post an event if possible. Arguments are forwarded to postUserEvent/event_handler :param event: Event to post :type event: str """ if om.MUserEventMessage.isUserEvent(event): om.MUserEventMessage.postUserEvent(event, *args, **kwargs) else: event_handler(event, *args, **kwargs)
[docs]def addEvent(event, register_func, deregister_func, disable_undo=False, allow_deferred=False, deferred_low_priority=False, builtin=False): """ Define a new event for the system. :param event: Event name :type event: str :param register_func: Function to register the event_handler for this event :type register_func: function :param deregister_func: Function to de-register the event_handler for this event (Passed the return from register_func if needed) :type deregister_func: function :param disable_undo: Disable the undo queue when calling callbacks, typically only needed for undo/redo based callbacks :type disable_undo: bool :param allow_deferred: Enable deffered callbacks, best for "rapid" events that do not need return values like SelecitonChanged. Uses evalDeferred. :type allow_deferred: bool :param deferred_low_priority: Use "Low priority" when deferring. Useful for *extremely* spammy events. :type deferred_low_priority: bool :param builtin: Flag as a builtin defined Event (From this callbacks.py module) :type builtin: bool """ if event in getEvents() and not builtin: raise ValueError('Duplicate event name!') events[event] = Event(event, register_func, deregister_func, disable_undo, allow_deferred, deferred_low_priority, builtin)
[docs]def event_handler(event, *args, **kwargs): """ :param event: :param args: :param kwargs: :return: """ print 'Event Handler!', event if event not in events: log.error('Event handler called for event that is not supported! "%s"'%event) return checkFile = ('checkFile' in kwargs) if checkFile: #Dont block file open checks retCode = args[0] mFile = args[1] om.MScriptUtil.setBool(retCode, True) event_callbacks = getCallbacks(event) allow_deferred = events[event].allow_deferred immediate_callbacks = [callback for callback in event_callbacks if callback.immediate or not allow_deferred] if immediate_callbacks: log.debug('Event %s Callbacks: %s'%(event, immediate_callbacks)) add_to_queue = len(immediate_callbacks)<event_callbacks for callback in immediate_callbacks: try: if checkFile: callback.function(mFile, retCode, *[arg for arg in args[2:] if arg is not None]) elif len([v for v in args if v is not None]): callback.function(*args) else: callback.function() except: log.exception('Error handling event "%s" with event handler "%s"\n'%(event, callback.owner)) if add_to_queue: if not event_queue.get(event): event_queue[event] = True cmds.evalDeferred(partial(queued_event_handler, event, *args, **kwargs), lowestPriority=events[event].deferred_low_priority)
[docs]def queued_event_handler(event, *args, **kwargs): event_callbacks = getCallbacks(event) deferred_callbacks = [callback for callback in event_callbacks if not callback.immediate] log.debug('Queued event "%s" Callbacks: %s'%(event, deferred_callbacks)) for callback in deferred_callbacks: if not callback.immediate: try: if len([v for v in args if v is not None]): callback.function(*args) else: callback.function() except: log.exception('Error handling queued event "%s" with event handler "%s"\n'%(event, callback.owner)) #Allow another event to queue event_queue[event] = False
def _registerEvent(event): if not event in events: raise ValueError('Invalid event name: %s, not in supported events list!'%event) if event in event_handles: if not _deregisterEvent(event): raise SystemError('Could not de-register old event handler, unable to re-register event: "%s"'%event) event_handles[event] = events[event].register_func() return True def _deregisterEvent(event): if not event in events: raise ValueError('Invalid event name: %s, not in supported events list!'%event) if not event in event_handles: return True handle = event_handles[event] try: if handle is not None: events[event].deregister_func(handle) else: events[event].deregister_func() del event_handles[event] return True except: log.exception('Error deregistering event "%s":\n'%event) return False
[docs]def make_user_event_funcs(event): """ :param event: :return: """ def rfunc(event): om.MUserEventMessage.registerUserEvent(event) om.MUserEventMessage.addUserEventCallback(event, partial(event_handler, event)) def dfunc(event, id): om.MMessage.removeCallback(id) om.MUserEventMessage.deregisterUserEvent(event) return partial(rfunc, event), partial(dfunc, event)
[docs]def add_default_events(): _temp = [] #Add builtin events om.MEventMessage.getEventNames(_temp) for mevent in sorted(_temp): rfunc = partial(om.MEventMessage.addEventCallback, mevent, partial(event_handler, mevent)) dfunc = om.MMessage.removeCallback addEvent(mevent, rfunc, dfunc, disable_undo=False, allow_deferred=True, builtin=True) #Add scene events scene_events = [ 'AfterSave', 'BeforeSave', 'BeforeOpen', 'AfterOpen', 'BeforeImport', 'AfterImport', 'BeforeLoadReference', 'AfterLoadReference', 'BeforeLoadReferenceAndRecordEdits', 'AfterLoadReferenceAndRecordEdits', 'BeforeUnloadReference', 'AfterUnloadReference', 'BeforeRemoveReference', 'AfterRemoveReference', 'BeforeCreateReference', 'AfterCreateReference', 'BeforeCreateReferenceAndRecordEdits', 'AfterCreateReferenceAndRecordEdits', 'BeforeNew', 'AfterNew', 'SceneUpdate', 'MayaExiting', ] for mevent in scene_events: event_id = getattr(om.MSceneMessage, 'k%s'%mevent) rfunc = partial(om.MSceneMessage.addCallback, event_id, partial(event_handler, mevent)) dfunc = om.MMessage.removeCallback disable_undo = mevent in ['Undo', 'Redo'] addEvent(mevent, rfunc, dfunc, disable_undo=disable_undo, allow_deferred=True, builtin=True) #Add scene check events scene_check_events = [ ('BeforeNewCheck', False), ('BeforeOpenCheck', True), ('BeforeSaveCheck', False), ('BeforeImportCheck', True), ('BeforeExportCheck', True), ('BeforeReferenceCheck', False), ('BeforeLoadReferenceCheck', True), ('BeforeCreateReferenceCheck', True), ] for mevent, checkFile in scene_check_events: event_id = getattr(om.MSceneMessage, 'k%s'%mevent) if checkFile: rfunc = partial(om.MSceneMessage.addCheckFileCallback, event_id, partial(event_handler, mevent, checkFile=True)) else: rfunc = partial(om.MSceneMessage.addCheckCallback, event_id, partial(event_handler, mevent)) dfunc = om.MMessage.removeCallback addEvent(mevent, rfunc, dfunc, disable_undo=False, allow_deferred=False, builtin=True)
add_default_events()