-
Notifications
You must be signed in to change notification settings - Fork 23
Events and callbacks
In conventional Maya GUI, widgets provide one or more callbacks: a button, for example, provides a 'command' callback which will invoke a function or other callable object when the button is pressed:
def test_function(*_):
print "pressed"
b = cmds.button('example', command=test_function)
This works, but it's often troublesome to manage: it encourages you to interleave your functional code and your gui layout, since it's awkward to assign the callback functions after the gui has been declared. More importantly it forces you to create a complex function if a UI action has to trigger multiple behaviors: for example, clicking a button might do something to the scene, highlight an item elsewhere in the UI, and update the title of the currently open window -- but you'll have to shoehorn these three, quite distinct jobs into a single callback function.
To make it easier to work with callbacks, mGui uses an event based paradigm: each callback provided by Maya is a callable object. You can add functions to the object using the +=
operator. This is the mGui equivalent to the button example above:
def test(*a, **kw):
print "pressed"
b = Button()
b.command += test
Since b's command
is an Event object you can add more functions using the same syntax. This will add a second function to the above button:
def add_cube(*_, **kw):
cmds.polyCube()
b.command += add_cube
Now pressing the button will both print the message and also create a polyCube in the scene. You can remove functions the same way:
b.command -= test # removes the printout, but the polyCube will still be created
This makes it easier to keep different aspects of your tool (GUI updates, scene operations and so on) separated out cleanly.
You probably noticed that the test function included two extra arguments. The first (*a
in the example) passes along any arguments from the Maya gui widget, since some Maya widgets provide arguments when manipulated. The keyword argument is added by mGui automatically. It adds an extremely useful function to all callbacks: it remembers which widget fired the event and passes it to the callback function as the keyword argument 'sender':
def who_sent_me(*_, *kw):
print kw['sender'], "was clicked"
b.command += who_sent_me
will print something like |window23|columnLayout5|button123 was clicked'. Combined with the fact that every mGui widget includes a
tag` field for storing data it becomes very easy to react to specific combinations of gui and data without a lot of work to match them up. Here's a really simple example that will select scene objects from a window:
def select_clicked(*_, **kw):
cmds.select(kw['sender'].tag)
with Window() as w:
with ColumnLayout() as c:
for item in cmds.ls(type='transform')[:10]:
b = Button(label = "select " + item, tag=item)
b.command += select_clicked
w.show()
In more conventional Maya programming you'd need to create a separate callable object or function for each item in the scene, or try to pack the information into some kind of dictionary to retrieve it later. sender
and tag
make this kind of task much simpler.
You can also customize the data that gets sent along when an event argument fires. Each event contains a field called data
, which is the dictionary that will be passed as a keyword argument set when the Event fires. The 'sender' key is added when the event fires, but any other key-value pairs in data
will be passed along. So you could customize the behavior of an event by adding more metadata:
def what_is(*_, **kw):
print kw['sender'].tag, 'is a', kw['type']
with Window() as w:
with ColumnLayout() as c:
for item in cmds.ls(type='transform')[:10]:
b = Button(label = "select " + item, tag=item)
b.command += select_clicked
b.command.data['type'] = cmds.nodeType(item)
w.show()
mGui uses the same event strategy for scriptJobs. Not only is this a lot less cumbersome, it also allows you to add multiple behaviors to the same scriptJob trigger using the +=
syntax. This makes it easy to add or remove functionality without having to juggle too many independent scriptJobs. The scriptJobs module provides pre-made classes for all of Maya's scriptJob types, so instead of
cmds.scriptJob( e = ('selectionChanged', myFunctionOne))
cmds.scriptJob( e = ('selectionChanged', myFunctionTwo))
in mGUi you'd invoke
sj = scriptJobs.SelectionChanged()
sj += myFunctionOne
sj += myFunctionTwo
the scriptJobEvents can start or stop their own scriptJobs:
sj.start()
and test if the scriptJob is currently running:
if not sj.running:
print "warning: SelectionChanged script job is inactive"
Event
objects are just conventional python classes. They can be a useful way to break up complex jobs into smaller pieces, particularly when you want to split out the display and processing of information. Here's a scene-analysis class that uses Events to report what it's doing during a long running operation -- some other piece of code can handle these events so that you can log to the listener, display a window, or write files to disk as appropriate without touching the actual functional code:
class SlowSceneAnalyzer(object):
def __init___(self):
self.found_transform = Event()
self.found_light = Event()
self.completed = Event()
def process():
for item in cmds.ls(type='transform'):
do_some_long_operation(item)
self.found_transform(item) # Events are callables, so this 'fires' the event with item as an argument
for item in cmds.ls(type='light'):
set_some_stuff(item)
self.found_light(item) # Events are callables, so this 'fires' the event with item as an argument
self.completed()
By using events you could run this in a gui window which updated a progress bar when lights or transforms were met with -- or run the same code in a standalone while just printing to the console
def print_item(*arg, **kw):
print 'processed', arg
def all_done(*_, **kw):
print "-" * 72
print done
ssa = SlowSceneAnalyzer()
ssa.found_transform += print_item
ssa.found_light += print_item
ssa.completed += done
sss.process()
This is a good way to keep your display code from getting entangled with your functional code, and it makes it much easier to maintain gui and headless versions of the same tools side-by-side without a lot of if-checks.