Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
512 lines (396 sloc) 17.7 KB
import maya.cmds as cmds
import inspect
from collections import OrderedDict
from mGui.bindings import BindableObject, BindingContext
from mGui.properties import CtlProperty, CallbackProperty
from mGui.scriptJobs import ScriptJobCallbackProperty
from mGui.styles import Styled
from weakref import ref
"""
# MGui.Core
A system for defining proxies that make it easier to work with maya GUI controls.
Proxies are created using the ControlMeta metaclass, which maps existing maya
GUI commands so that they look like proper object-oriented properties:
example = Button('buttontest')
example.width = 120
example.backgroundColor = (1,0,0)
creates a 120 pixel wide red button. The properties work both ways, so you can
query them:
print example.width
# 120
## Base Classes
This module defines the base classes Control and Layout, which are used by all
wrapper classes. They use the same property-wrapping strategy but Layouts also work as
context managers, allowing them to call SetParent() when needed and also to
maintain links to child control wrappers:
with mGui.core.layouts.ColumnLayout('main'):
for n in range (5):
with mGui.core.Layout.RowLayout('row_%i' % n, nc = 2):
mGui.core.controls.Text('t_%i', label = 'label)
mGui.core.controls.CheckBox('b_%i')
mGui.core.controls.Button('btn', label = 'big button')
would create a columnLayout with 4 rows with a text and a checkbox, followed by a button.
@note: in actual practice, its easier to import mGui.gui which contains all of
the contents of core.layouts and core.controls
@Note: One minor drawback to using Metaclasses is that Maya's reload mechanism
does not preserve the types created by a metaclass when you call reload(module).
This manifests as a TypeError in code that calls super() -- in this case,
typically in __init__ methods. This problem is ONLY related to reload - if you
restart Maya rather than using reload() it will disappear.
"""
# use this for condtional checks if there are version differences
MAYA_VERSION = cmds.about(version=True).split(' ')[0]
REGISTRY = {}
class ControlMeta(type):
"""
Metaclass which creates CtlProperty and CallbackProperty objects for Control classes
"""
def __new__(mcs, name, parents, kwargs):
maya_cmd = kwargs.get('CMD', None)
_READ_ONLY = kwargs.get('_READ_ONLY', [])
_ATTRIBS = kwargs.get('_ATTRIBS', [])
_CALLBACKS = kwargs.get('_CALLBACKS', [])
if not kwargs.get('CMD'):
maya_cmd = parents[0].CMD
_overridden = ('parent',)
for item in _READ_ONLY:
if item not in _overridden:
kwargs[item] = CtlProperty(item, maya_cmd, writeable=False)
for item in _ATTRIBS:
if item not in _overridden:
kwargs[item] = CtlProperty(item, maya_cmd)
for item in _CALLBACKS:
kwargs[item] = CallbackProperty(item)
kwargs['__bases__'] = parents
completed_type = super(ControlMeta, mcs).__new__(mcs, name, parents, kwargs)
# note sometimes more than one subclass uses the same maya command
# that makes the registry unable to disambiguate them. The correct
# response for gui.wrap will need to be the first class defined
# for this to work properly. If we run into future isses we may need to make
# this an explicit class attribute instead
if maya_cmd and not REGISTRY.get(maya_cmd.__name__):
REGISTRY[maya_cmd.__name__] = completed_type
return completed_type
class Control(Styled, BindableObject):
"""
Base class for all mGui controls. Provides the necessary frameworks for
CtlProperty and CallbackProperty access to the underlying widget.
Control inherits from bindings.BindableObject, so it supports binding
operators. All controls will have _bind_src and _bind_tgt fields, and
if a derived control class indicates default(s) they will be used.
Control inherits from styles.Styled, so it supports styling.
"""
CMD = cmds.control
_ATTRIBS = ['annotation', 'backgroundColor', 'defineTemplate', 'docTag', 'enable', 'enableBackground', 'exists',
'fullPathName', 'height', 'manage', 'noBackground', 'numberOfPopupMenus', 'parent', 'popupMenuArray',
'preventOverride', 'useTemplate', 'visible', 'visibleChangeCommand', 'width']
_CALLBACKS = ['dragCallback', 'dropCallback', 'visibleChangeCommand']
_READ_ONLY = ['isObscured', 'popupMenuArray', 'numberOfPopupMenus']
ADD_TO_LAYOUT = True
__metaclass__ = ControlMeta
onDeleted = ScriptJobCallbackProperty('onDeleted', 'uiDeleted')
def __init__(self, key=None, **kwargs):
# apply Styled, and filter out any CSS tags
super(Control, self).__init__(kwargs)
# arbitrary tag data. Use with care to avoid memory leaks
self.tag = kwargs.pop('tag', None)
maya_kwargs = self.format_maya_arguments(**kwargs)
# widget holds the actual maya gui string
self.widget = self.CMD(key, **maya_kwargs)
# key is our internal name
self.key = self.widget.split("|")[-1]
# Event objects
self.callbacks = {}
# a weak reference to our parent, will be added when
# this widget is added to a control
self._parent = None
# add us to the current layout under our own key name
if self.ADD_TO_LAYOUT:
Layout.add_current(self)
self.onDeleted += self.forget
def register_callback(self, callback_name, event):
"""
when a callback property is first accessed this creates an Event named <callback_name> for the specified
callback and hooks it to the gui widget's callback function
"""
kwargs = {'e': True, callback_name: event}
self.CMD(self.widget, **kwargs)
def __nonzero__(self):
return self.exists
def __repr__(self):
if self:
return self.widget
else:
return "<deleted UI element %s>" % self.__class__
def __str__(self):
return self.widget
def __iter__(self):
yield self
@classmethod
def wrap(cls, control_name, key=None):
def _spoof_create(*_, **__):
return control_name
try:
cache_CMD = cls.CMD
cls.CMD = _spoof_create
# allow wrapping of abstract types, but make sure derived types are correct
if cls.__name__ not in ('Control', 'Layout', 'Nested', 'Panel', 'MenuItem', 'PopupMenu'):
if not cmds.objectTypeUI(control_name, isType = cache_CMD.__name__):
raise RuntimeError( "{} is not an instance of {}".format(control_name, cache_CMD.__name__))
return cls(key=control_name)
finally:
cls.CMD = cache_CMD
def forget(self, *args, **kwargs):
self.callbacks.clear()
self.tag = None
@classmethod
def delete(cls, instance):
cmds.deleteUI(instance.widget)
@property
def parent(self):
"""
the mGui parent of this object. This will be None if:
* this object has no parent (eg, a top level window)
* this object's parent has fallen out of scope
* this layout context for this object has not yet closed.
"""
if self._parent is None:
return None
return self._parent()
class Nested(Control):
"""
Base class for all the nested context-manager classes which automatically parent themselves
Every NestedObject creates an ScriptJobCallbackProperty attached to a uiDeleted scriptJob,
so it's possible to use standard event mechanisms to react to, eg, a window closing. The
scriptJob will be started with default arguments the first time you attempt to add a handler
to it.
"""
ACTIVE_LAYOUT = None
def __init__(self, key=None, **kwargs):
self.controls = []
self.named_children = OrderedDict()
self.ignore_exceptions = False
self.modal = False
super(Nested, self).__init__(key, **kwargs)
def __enter__(self):
self.__cache_layout = Nested.ACTIVE_LAYOUT
if self.__cache_layout is not None:
self.modal = self.modal or self.__cache_layout.modal
Nested.ACTIVE_LAYOUT = self
return self
def __exit__(self, typ, value, tb):
# by default, allow inner exceptions to propagate up
# you can turn this off in production by
# setting ignore_exceptions to true
# if this is suppresed you should expect misleading
# error messages if child controls error out; parent controls
# may get fewer controls than they expect, but the real
# problem is in the suppressed exception
if typ and not self.ignore_exceptions:
return False
# look into the local namespace for Control-derived
# objects with named vars. If they are children of the context manager
# that is closing, add them with variable name as a key
# this supports a more natural, keyless idiom (see 'add')
owning_scope = inspect.currentframe().f_back
if owning_scope.f_locals.get('mGui_expand_stack'):
owning_scope = owning_scope.f_back
for key, value in owning_scope.f_locals.items():
if value in self:
self.add(value, key)
# restore the layout level
Nested.ACTIVE_LAYOUT = self.__cache_layout
self.__cache_layout = None
self.layout()
# restore gui parenting
cmds.setParent(Nested.ACTIVE_LAYOUT)
def layout(self):
"""
this is called at the end of a context, it can be used to (for example) perform attachments
in a formLayout. Override in derived classes for different behaviors.
"""
return len(self.controls)
def add(self, control, key=None):
"""
Add the supplied control (an mGui object) to the both the Controls list and the _named_children dictionary
in this item. If the control has a unique key.
named_children allows for dot notation access:
with gui.ColumnLayout('items') as items:
first = gui.Button(label = 'a button')
second = gui.Button( label = 'another button')
print items.first.label
# a button
named_children will contain a reference to the key name (the optional first argument) of any mGui control. It
will also close over any local variable names in a layout context. Thus
with Window() as outer:
with ColumnLayout() as column:
Button('first')
second = Button()
produces both
outer.column.first
and
outer.column.second
the first one via explicit naming and the second via closure.
Controls contains all of the *physical* widgets under this object (layouts, controls, etc).
Non-physical entities -- such as a RadioButtonCollection -- are available with dot notation but *not*
in the Controls field. This allows the layout() functions in various layouts to rely on the presence of
controls that can be manipulated.
"""
# @ this is a change in behavior from mGui 1
# we now overwrite existing children instead of excepting
# we also DON'T explicitly check to ensure that <control> is a kind of this widget
control_key = key or control.key
self.named_children[control_key] = control
if control not in self.controls:
self.controls.append(control)
control._parent = ref(self)
def replace(self, key, control):
"""
replace the control at <key> with the supplied control, and redo the layout for this item.
@note this will only work if the existing item has a key
"""
original = self.named_children.get(key)
if original:
self.controls.remove(original)
Control.delete(original)
self.add(control, key)
self.layout()
def remove(self, control):
"""
remove <control> from my children
"""
if control not in self.controls:
raise KeyError('{0} is not a child of {1}'.format(control, self))
self.controls.remove(control)
control.delete(control)
for key, ctrl in self.named_children.items():
if ctrl == control:
self.named_children.pop(key)
def clear(self):
delenda = self.controls[:]
for d in delenda:
self.remove(d)
self.named_children.clear()
def __setattr__(self, key, value):
if isinstance(value, Control) and not key.startswith("_"):
self.add(value, key=key)
else:
super(Nested, self).__setattr__(key, value)
def __getattr__(self, item):
if item in self.named_children:
return self.named_children[item]
super(Nested, self).__getattribute__(item)
def __iter__(self):
for sub in self.controls:
yield sub
yield self
def __contains__(self, item):
return item in self.controls
def recurse(self):
for item in self.controls:
if hasattr(item, 'recurse'):
for grandchild in item.recurse():
yield grandchild
yield item
def find(self, mGuiType):
"""
return any items of the type <mGuiType> from this items child controls
"""
return [i for i in self.controls if isinstance(i, mGuiType)]
# note: both of these explicitly use Nested instead of cls
# so that there is only one global layout stack...
@classmethod
def add_current(cls, control):
active = Nested.current()
if active:
Nested.ACTIVE_LAYOUT.add(control)
@classmethod
def current(cls):
"""
return the active layout if it exists
"""
if Nested.ACTIVE_LAYOUT:
return Nested.ACTIVE_LAYOUT
def forget(self, *args, **kwargs):
super(Nested, self).forget()
if self.controls:
self.controls = []
if self.named_children:
self.named_children = {}
if Nested.ACTIVE_LAYOUT == self:
Nested.ACTIVE_LAYOUT = None
def as_parent(self):
try:
cmds.setParent(self)
except RuntimeError as e:
cmds.setParent(self, menu=True)
return self
# IMPORTANT NOTE
# this intentionally duplicates redundant property names from Control.
# That forces the metaclass to F-define the CtlProperties using cmds.layout
# instead of cmds.control. In Maya 2014, using cmds.control to query a layout fails,
# even for flags they have in common
class Layout(Nested):
CMD = cmds.layout
_ATTRIBS = ['annotation', 'backgroundColor', 'defineTemplate', 'docTag', 'dragCallback', 'dropCallback', 'enable',
'enableBackground', 'exists', 'fullPathName', 'height', 'manage', 'noBackground', 'numberOfPopupMenus',
'parent', 'popupMenuArray', 'preventOverride', 'useTemplate', 'visible', 'visibleChangeCommand',
'width']
_CALLBACKS = ['dragCallback', 'dropCallback', 'visibleChangeCommand']
_READ_ONLY = ['isObscured', 'popupMenuArray', 'numberOfPopupMenus', 'childArray', 'numberOfChildren']
class Window(Nested):
"""
Window inherits from bindings.BindableObject, so it supports binding
operators. All controls will have _bind_src and _bind_tgt fields, and
if a derived control class indicates default(s) they will be used.
Window inherits from styles.Styled, so it supports styling.
@note:
in Maya 2011 (only, AFAIK) a sizeable = False parameter mis-behaves when
the window contains a TabLayout which in turn contains a FormLayout. If
you have to have to use a TabLayout in 2011, don't use any derivative
of forms, or set 'sizeable' to TRUE. Bug is not present in 2014 +, not sure
about 2012 or 2013
"""
ACTIVE_WINDOWS = []
CMD = cmds.window
_ATTRIBS = ["backgroundColor", "defineTemplate", "docTag", "exists", "height", "iconify", "iconName", "leftEdge",
"menuBarVisible", "menuIndex", "mainMenuBar", "minimizeButton", "maximizeButton", "resizeToFitChildren",
"sizeable", "title", "titleBar", "titleBarMenu", "topEdge", "toolbox", "topLeftCorner", "useTemplate",
"visible", "width", "widthHeight"]
_CALLBACKS = ["minimizeCommand", "restoreCommand"]
_READ_ONLY = ["numberOfMenus", "menuArray", "menuBar", "retain"]
def __init__(self, key=None, **kwargs):
super(Window, self).__init__(key, **kwargs)
Window.ACTIVE_WINDOWS.append(self)
def forget(self, *args, **kwargs):
super(Window, self).forget()
if self in Window.ACTIVE_WINDOWS:
Window.ACTIVE_WINDOWS.remove(self)
def show(self):
cmds.showWindow(self.widget)
def hide(self):
self.visible = False
@classmethod
def current(cls):
if Window.ACTIVE_WINDOWS:
return Window.ACTIVE_WINDOWS[-1]
return None
class BindingWindow(Window):
"""
A Window with a built in BindingContext
"""
def __init__(self, key=None, **kwargs):
super(BindingWindow, self).__init__(key, **kwargs)
self.bindingContext = BindingContext()
def __enter__(self):
self.bindingContext.__enter__()
return super(BindingWindow, self).__enter__()
def __exit__(self, typ, value, traceback):
self.bindingContext.__exit__(None, None, None)
mGui_expand_stack = True
super(BindingWindow, self).__exit__(typ, value, traceback)
def update_bindings(self):
self.bindingContext.update(True)
def forget(self, *args, **kwargs):
super(BindingWindow, self).forget()
self.bindingContext = None