""" forms.py A variety of specialized subclasses of FormLayout which are good shortcuts for common layout problems. @author: Stephen Theodore @note Did some empirical testing, and it appears that lots of attachments is slower than lots of simple forms. Thus generating a columnLayout containing 1000 buttons, each wrapped in its own formlayout takes .86 seconds on my machine; where one formlayout with 1000 buttons takes 1.89 seconds if the buttons are attached to both sides of form and 1.25 if they are not; a differences of between 50 and 100% The same 1000 buttons with no individual form layouts = .53 seconds For this reason prefer row or column or flowlayouts for repetitive stuff and use forms for the main architecture However, for less complex UI groups of 100-odd sets of 3 controls (300 total) came in under .25s, so it's not a big deal in common cases """ import itertools import maya.cmds as cmds from mGui.core import Nested from mGui.core.layouts import FormLayout from mGui.styles import Bounds def physical_controls(widget): return (i for i in widget.controls if i.CMD not in (cmds.popupMenu, cmds.menuItem, cmds.radioCollection, cmds.iconTextRadioCollection)) class Form(FormLayout): """ A wrapper for FormLayout with convenience methods for attaching controls. Use this when you need precise control over form behavior. Form baseclass is entirely manual - it does no automatic layout behavior. """ def __init__(self, key=None, **kwargs): margin, spacing = None, None if 'margin' in kwargs: margin = kwargs.pop('margin') if 'spacing' in kwargs: spacing = kwargs.pop('spacing') super(Form, self).__init__(key, **kwargs) if margin: self._style['margin'] = margin if spacing: self._style['spacing'] = spacing self.margin = Bounds(*self._style.get('margin', (0, 0))) self.spacing = Bounds(*self._style.get('spacing', (0, 0))) def _fill(self, ctrl, *sides, **kwargs): """ convenience wrapper for tedious formLayout editing """ margin = kwargs.get('margin', None) ct = [ctrl for _ in sides] mr = [margin for _ in sides] if margin is None: mr = [self.margin[_] for _ in sides] self.attachForm = zip(ct, sides, mr) def top(self, ctrl, margin=None): """ Docks 'ctrl' against the top of the form, with the supplied margin on top, left and right """ self._fill(ctrl, 'top', 'left', 'right', margin=margin) def left(self, ctrl, margin=None): """ dock 'ctrl' along the left side of the form with the supplied margin """ self._fill(ctrl, 'top', 'left', 'bottom', margin=margin) def right(self, ctrl, margin=None): """ dock 'ctrl' along the right side of the form with the supplied margin """ self._fill(ctrl, 'top', 'bottom', 'right', margin=margin) def bottom(self, ctrl, margin=None): """ dock 'ctrl' along the bottom of the form with the supplied margin """ self._fill(ctrl, 'bottom', 'left', 'right', margin=margin) def fill(self, ctrl, margin=None): """ docks 'ctrl' into the form filling it completely with suppled margin on all sides """ sides = ['top', 'bottom', 'left', 'right'] self._fill(ctrl, *sides, margin=margin) def snap(self, ctrl1, ctrl2, edge, space=None): """ docs 'ctrl1' to 'ctrl2' along the supplied edge (top, left, etc) with the supplied margin """ if space is None: space = self.spacing[edge] self.attachControl = (ctrl1, edge, space, ctrl2) def form_attachments(self, *sides): """ returns a list of (control, side, spacing) values used by attachForm style commands """ attachments = ([side, self.margin[side]] for side in sides) ctls = itertools.product(physical_controls(self), attachments) return [[a] + b for a, b in ctls] def form_series(self, side): """ returns a series of (control, side, space, control) for use in serial placement """ first, second = itertools.tee(physical_controls(self)) second.next() return [(s, side, self.spacing[side], f) for f, s in itertools.izip(first, second)] def percentage_series(self, side): side2 = {'left': 'right', 'right': 'left', 'top': 'bottom', 'bottom': 'top'}[side] widths = [i.width if hasattr(i, 'width') else 0 for i in physical_controls(self)] total_width = sum(widths) proportions = map(lambda q: q * 100.0 / total_width, widths) p_l = len(proportions) left_edges = [sum(proportions[:r]) for r in range(0, p_l)] right_edges = [sum(proportions[:r]) for r in range(1, p_l + 1)] ap = [] for c, l, r in itertools.izip(physical_controls(self), left_edges, right_edges): ap.append((c, side, self.spacing[side], l)) ap.append((c, side2, self.spacing[side2], r)) return ap def equal_series(self, side): side2 = {'left': 'right', 'right': 'left', 'top': 'bottom', 'bottom': 'top'}[side] widths = [1 for each_item in physical_controls(self)] total_width = sum(widths) proportions = map(lambda q: q * 100.0 / total_width, widths) p_l = len(proportions) left_edges = [sum(proportions[:r]) for r in range(0, p_l)] right_edges = [sum(proportions[:r]) for r in range(1, p_l + 1)] ap = [] for c, l, r in itertools.izip(physical_controls(self), left_edges, right_edges): ap.append((c, side, self.spacing[side], l)) ap.append((c, side2, self.spacing[side2], r)) return ap def dock(self, ctrl, top=None, left=None, right=None, bottom=None): """ docks ctrl into the form. for each of the optional flags (top, bottom, left, & right), the arguments are interpreted as follows: None (default): Ignore this edge Number (eg 10): dock to form with this margin along this edge (ctrl2, number): dock to other control 'ctrl2' along this edge, with supplied margin """ if not hasattr(top, '__iter__'): top = (None, top) if not hasattr(bottom, '__iter__'): bottom = (None, bottom) if not hasattr(left, '__iter__'): left = (None, left) if not hasattr(right, '__iter__'): right = (None, right) ac = lambda edge, other, margin: cmds.formLayout(self.widget, e=True, ac=(ctrl, edge, margin, other)) af = lambda edge, ignore, margin: cmds.formLayout(self.widget, e=True, af=(ctrl, edge, margin)) if top[0]: ac('top', *top) elif top[1]: af('top', *top) if left[0]: ac('left', *left) elif left[1]: af('left', *left) if bottom[0]: ac('bottom', *bottom) elif bottom[1]: af('bottom', *bottom) if right[0]: ac('right', *right) elif right[1]: af('right', *right) def detach(self, *controls): """ call AttachNone on all """ sides = ('top', 'bottom', 'left', 'right') # note the order : it's backwards from the others! cmds.formLayout(self.widget, an=[(ctl, side) for side, ctl in itertools.product(sides, controls)]) class LayoutDialogForm(Form): """ Shim that will create a formLayout wrapper from an existing formLayout. Used with the maya LayoutDialog command, which creates a form for you, so you can still use mGui property access. you should explicitly call forget() on this form when you close it """ def __init__(self, key=None): self.CMD = self.fake_create super(LayoutDialogForm, self).__init__(key=None) self.modal = True self.CMD = cmds.formLayout def __enter__(self): return super(LayoutDialogForm, self).__enter__() def __exit__(self, typ, value, tb): return super(LayoutDialogForm, self).__exit__(typ, value, tb) @staticmethod def fake_create(*args, **kwargs): return cmds.setParent(q=True) def forget(self, *args, **kwargs): for item in self.recurse(): item.forget() super(LayoutDialogForm, self).forget() class FillForm(Form): """ Docks the first child so it fills the entire form with the specified margin """ def layout(self): for item in physical_controls(self): self.fill(item) return len(self.controls) class VerticalForm(Form): """ Lays out children vertically. The first child is attached to the top of the form, all children are attached to the left and right. Note tha the last child will NOT be offset by the margin, since tha form can be added to indefinitely -- if you want a complete margin you should nest this in another form """ def layout(self): if len(self.controls): ctrls = [i for i in physical_controls(self)] af = self.form_attachments('left', 'right') af.append([ctrls[0], 'top', self.margin.top]) ac = self.form_series('top') self.attachForm = af self.attachControl = ac return len(self.controls) class HorizontalForm(Form): """ Lays out children horizontally. The first child is attacked to the left of the form, all children are attached to the top and bottom. Note tha the last child will NOT be offset by the margin, since tha form can be added to indefinitely -- if you want a complete margin you should nest this in another form """ def layout(self): if len(self.controls): ctrls = [i for i in physical_controls(self)] af = self.form_attachments('top', 'bottom') af.append([ctrls[0], 'left', self.margin.left]) ac = self.form_series('left') self.attachForm = af self.attachControl = ac return len(self.controls) class VerticalExpandForm(Form): """ Lays out children vertically. The first child is attached to the top of the form, and the last to the bottom. The last division will expand with the form. """ def layout(self): if len(self.controls): ctrls = [i for i in physical_controls(self)] af = self.form_attachments('left', 'right') af.append([ctrls[0], 'top', self.margin.top]) af.append([ctrls[-1], 'bottom', self.margin.bottom]) ac = self.form_series('top') self.attachForm = af self.attachControl = ac return len(self.controls) class HorizontalExpandForm(Form): """ Lays out children horizontally. The first child is attacked to the left of the form, and the last to the right. The last division will expand with the form. """ def layout(self): if len(self.controls): ctrls = [i for i in physical_controls(self)] af = self.form_attachments('top', 'bottom') af.append([ctrls[0], 'left', self.margin.left]) af.append([ctrls[-1], 'right', self.margin.right]) ac = self.form_series('left') self.attachForm = af self.attachControl = ac return len(self.controls) class HorizontalStretchForm(Form): """ Lays out children horizontally. All children will scale proportionally as the form changes size """ def layout(self): if len(self.controls): af = self.form_attachments('top', 'bottom') ap = self.percentage_series('left') self.attachForm = af self.attachPosition = ap self.attachForm = [self.controls[0], 'left', self.margin.left] self.attachForm = [self.controls[-1], 'right', self.margin.right] return len(self.controls) class VerticalStretchForm(Form): """ Lays out children vertically, with sizes proportional to their original heights """ def layout(self): if len(self.controls): af = self.form_attachments('left', 'right') ap = self.percentage_series('top') self.attachForm = af self.attachPosition = ap self.attachForm = [self.controls[0], 'top', self.margin.top] self.attachForm = [self.controls[-1], 'bottom', self.margin.bottom] return len(self.controls) class VerticalThreePane(Form): """ First child is glued to the top, last child is glued to the bottom, intermediate children are stretched """ def layout(self): if len(self.controls) < 3: raise ValueError("VerticalThreePane requires at least 3 children") af = self.form_attachments('left', 'right') ap = self.percentage_series('top') self.attachForm = af self.attachForm = (self.controls[-1], 'bottom', self.margin.bottom) self.attachPosition = ap[2:-2] self.attachControl = (self.controls[1], 'top', self.spacing.top, self.controls[0]) self.attachControl = (self.controls[-2], 'bottom', self.spacing.bottom, self.controls[-1]) return len(self.controls) class HorizontalThreePane(Form): """ First child is glued to the left, last child is glued to the right, intermediate children are stretched """ def layout(self): if len(self.controls) < 3: raise ValueError("HorizontalThreePane requires at least 3 children") af = self.form_attachments('top', 'bottom') ap = self.percentage_series('left') self.attachForm = af self.attachForm = (self.controls[-1], 'right', self.margin.right) self.attachPosition = ap[2:-2] self.attachControl = (self.controls[1], 'left', self.spacing.left, self.controls[0]) self.attachControl = (self.controls[-2], 'right', self.spacing.right, self.controls[-1]) return len(self.controls) class FooterForm(VerticalForm): """ A vertical layout with two children. The first expands with the container, the second is glued to the bottom """ def layout(self): if len(self.controls) != 2: raise ValueError("FooterForm requires at exactly 2 children") af = self.form_attachments('left', 'right') self.attachForm = af self.attachForm = (self.controls[0], 'top', self.margin.top) self.attachForm = (self.controls[-1], 'bottom', self.margin.bottom) self.attachControl = (self.controls[0], 'bottom', self.spacing.bottom, self.controls[1]) return len(self.controls) class HeaderForm(VerticalForm): """ A vertical layout with two children. The second expands with the container, the first is glued to the top """ def layout(self): if len(self.controls) != 2: raise ValueError("FooterForm requires at exactly 2 children") af = self.form_attachments('left', 'right') self.attachForm = af self.attachForm = (self.controls[0], 'top', self.margin.top) self.attachForm = (self.controls[-1], 'bottom', self.margin.bottom) self.attachControl = (self.controls[1], 'top', self.spacing.bottom, self.controls[0]) return len(self.controls) class NavForm(HorizontalForm): """ A two-pane horizontal form. THe first is fixed to the left size, the second expands with the container """ def layout(self): af = self.form_attachments('top', 'bottom') self.attachForm = af self.attachForm = (self.controls[0], 'left', self.margin.left) self.attachForm = (self.controls[-1], 'right', self.margin.right) self.attachControl = (self.controls[1], 'left', self.spacing.left, self.controls[0]) return len(self.controls) __all__ = ['Form', 'FillForm', 'VerticalForm', 'HorizontalForm', 'VerticalExpandForm', 'HorizontalExpandForm', 'VerticalStretchForm', 'HorizontalStretchForm', 'HorizontalThreePane', 'VerticalThreePane', 'HeaderForm', 'FooterForm', 'NavForm', 'LayoutDialogForm']