diff --git a/tailor/cassowary/__init__.py b/tailor/cassowary/__init__.py index 3073d1583..ef1c5db2c 100644 --- a/tailor/cassowary/__init__.py +++ b/tailor/cassowary/__init__.py @@ -1,2 +1,9 @@ # Implementation of the Cassowary algorithm # http://www.cs.washington.edu/research/constraints/cassowary/ + +from .constraint import Equation, Inequality, StayConstraint, EditConstraint +from .expression import Expression +from .simplex_solver import SimplexSolver +from .strength import REQUIRED, STRONG, MEDIUM, WEAK +from .variable import Variable +from .utils import approx_equal diff --git a/tailor/cassowary/constraint.py b/tailor/cassowary/constraint.py index d6f015be7..39ce12ba7 100644 --- a/tailor/cassowary/constraint.py +++ b/tailor/cassowary/constraint.py @@ -70,7 +70,7 @@ class Inequality(LinearConstraint): if operator == self.LEQ: self.expression.add_expression(param1, -1.0) elif operator == self.GEQ: - self.expression.multply(-1) + self.expression.multiply(-1) self.expression.add_expression(param1, 1.0) else: raise InternalError("Invalid operator in Inequality constructor") @@ -84,7 +84,15 @@ class Inequality(LinearConstraint): else: raise InternalError("Invalid operator in Inequality constructor") - # elif isinstance(param2, (float, int)): + elif isinstance(param2, (float, int)): + super(Inequality, self).__init__(param1.clone(), strength=strength, weight=weight) + if operator == self.LEQ: + self.expression.multiply(-1.0) + self.expression.add_expression(Expression(constant=param2), 1.0) + elif operator == self.GEQ: + self.expression.add_expression(Expression(constant=param2), -1.0) + else: + raise InternalError("Invalid operator in Inequality constructor") else: raise InternalError("Invalid parameters to Inequality constructor") @@ -142,7 +150,9 @@ class Equation(LinearConstraint): elif isinstance(param2, Variable): super(Equation, self).__init__(param1.clone(), strength=strength, weight=weight) self.expression.add_variable(param2, -1.0) - # elif isinstance(param2, (float, int)): + elif isinstance(param2, (float, int)): + super(Equation, self).__init__(param1.clone(), strength=strength, weight=weight) + self.expression.add_expression(Expression(constant=param2), -1.0) else: raise InternalError("Invalid parameters to Equation constructor") @@ -153,9 +163,10 @@ class Equation(LinearConstraint): elif isinstance(param2, Variable): super(Equation, self).__init__(Expression(variable=param2), strength=strength, weight=weight) elif isinstance(param2, (float, int)): - super(Equation, self).__init__(Expression(value=param2), strength=strength, weight=weight) + super(Equation, self).__init__(Expression(constant=param2), strength=strength, weight=weight) self.expression.add_variable(param1, -1.0) else: raise InternalError("Invalid parameters to Equation constructor") + else: raise InternalError("Invalid parameters to Equation constructor") diff --git a/tailor/cassowary/error.py b/tailor/cassowary/error.py index fa5a71cdf..0e242c0ba 100644 --- a/tailor/cassowary/error.py +++ b/tailor/cassowary/error.py @@ -7,18 +7,5 @@ class ConstraintNotFound(Exception): pass -class NonExpression(Exception): - pass - - -class NotEnoughStays(Exception): - pass - - class RequiredFailure(Exception): pass - - -class TooDifficult(Exception): - pass - diff --git a/tailor/cassowary/expression.py b/tailor/cassowary/expression.py index fc9d5b44f..029617083 100644 --- a/tailor/cassowary/expression.py +++ b/tailor/cassowary/expression.py @@ -1,5 +1,5 @@ from .variable import Variable, AbstractVariable -from .error import NonExpression, InternalError +from .error import InternalError from .utils import approx_equal @@ -43,13 +43,13 @@ class Expression(object): elif x.is_constant: result = self * x.constant else: - raise NonExpression() + raise TypeError('Cannot multiply expression by non-constant') return result def __div__(self, x): if isinstance(x, (float, int)): if approx_equal(x, 0): - raise NonExpression() + raise ZeroDivisionError() result = Expression(constant=self.constant / x) for clv, value in self.terms.items(): result.set_variable(clv, value / x) @@ -57,7 +57,7 @@ class Expression(object): if x.is_constant: result = self / x.constant else: - raise NonExpression() + raise TypeError('Cannot divide expression by non-constant') return result def __add__(self, x): @@ -65,12 +65,20 @@ class Expression(object): return self.clone().add_expression(x, 1.0) elif isinstance(x, Variable): return self.clone().add_variable(x, 1.0) + elif isinstance(x, (int, float)): + return self.clone().add_expression(Expression(constant=x), 1.0) + else: + raise TypeError('Cannot add object of type %s to expression' % type(x)) def __sub__(self, x): if isinstance(x, Expression): return self.clone().add_expression(x, -1.0) elif isinstance(x, Variable): return self.clone().add_variable(x, -1.0) + elif isinstance(x, (int, float)): + return self.clone().add_expression(Expression(constant=x), -1.0) + else: + raise TypeError('Cannot subtract object of type %s from expression' % type(x)) def add_expression(self, expr, n, subject=None, solver=None): if isinstance(expr, AbstractVariable): diff --git a/tailor/cassowary/simplex_solver.py b/tailor/cassowary/simplex_solver.py index c78ce00f3..68de99f39 100644 --- a/tailor/cassowary/simplex_solver.py +++ b/tailor/cassowary/simplex_solver.py @@ -12,8 +12,7 @@ class SimplexSolver(Tableau): def __init__(self): super(SimplexSolver, self).__init__() - self.stay_minus_error_vars = [] - self.stay_plus_error_vars = [] + self.stay_error_vars = [] self.error_vars = {} self.marker_vars = {} @@ -34,8 +33,7 @@ class SimplexSolver(Tableau): def __repr__(self): parts = [] - parts.append('stay_plus_error_vars: %s' % self.stay_plus_error_vars) - parts.append('stay_minus_error_vars: %s' % self.stay_minus_error_vars) + parts.append('stay_error_vars: %s' % self.stay_error_vars) parts.append('edit_var_map: %s' % self.edit_var_map) return super(SimplexSolver, self).__repr__() + '\n' + '\n'.join(parts) @@ -96,7 +94,7 @@ class SimplexSolver(Tableau): z_row = self.rows[self.objective] # print "z_row", z_row sw_coeff = cn.strength * cn.weight - if sw_coeff == 0: + # if sw_coeff == 0: # print "cn ==", cn # print "adding ", eplus, "and", eminus, "with sw_coeff", sw_coeff z_row.set_variable(eplus, sw_coeff) @@ -108,8 +106,7 @@ class SimplexSolver(Tableau): self.insert_error_var(cn, eplus) if cn.is_stay_constraint: - self.stay_plus_error_vars.append(eplus) - self.stay_minus_error_vars.append(eminus) + self.stay_error_vars.append((eplus, eminus)) elif cn.is_edit_constraint: prev_edit_constant = cn.expression.constant @@ -136,6 +133,8 @@ class SimplexSolver(Tableau): self.optimize(self.objective) self.set_external_variables() + return cn + def add_constraint_no_exception(self, cn): try: self.add_constraint(cn) @@ -174,7 +173,7 @@ class SimplexSolver(Tableau): assert len(self.edit_var_map) == n except ConstraintNotFound: - raise InternalError('Constraint not found') + raise InternalError('Constraint not found during internal removal') def add_point_stays(self, points): # print "add_point_stays", points @@ -201,8 +200,10 @@ class SimplexSolver(Tableau): for cv in e_vars: try: z_row.add_expression(self.rows[cv], -cn.weight * cn.strength, self.objective, self) + # print 'add expression', self.rows[cv] except KeyError: z_row.add_variable(cv, -cn.weight * cn.strength, self.objective, self) + # print 'add variable', cv try: marker = self.marker_vars.pop(cn) @@ -216,59 +217,94 @@ class SimplexSolver(Tableau): exit_var = None min_ratio = 0.0 for v in col: + # print 'check var', v if v.is_restricted: + # print 'var', v, ' is restricted' expr = self.rows[v] coeff = expr.coefficient_for(marker) # print "Marker", marker, "'s coefficient in", expr, "is", coeff if coeff < 0: r = -expr.constant / coeff if exit_var is None or r < min_ratio: # EXTRA BITS IN JS? + # print 'set exit var = ',v,r min_ratio = r exit_var = v if exit_var is None: # print "exit_var is still None" for v in col: + # print 'check var', v if v.is_restricted: + # print 'var', v, ' is restricted' expr = self.rows[v] coeff = expr.coefficient_for(marker) + # print "Marker", marker, "'s coefficient in", expr, "is", coeff r = expr.constant / coeff if exit_var is None or r < min_ratio: + # print 'set exit var = ',v,r min_ratio = r exit_var = v if exit_var is None: + # print "exit_var is still None (again)" if len(col) == 0: + # print 'remove column',marker self.remove_column(marker) else: exit_var = [v for v in col if v != self.objective][-1] # ?? + # print 'set exit var', exit_var if exit_var is not None: + # print 'Pivot', marker, exit_var, self.pivot(marker, exit_var) - if not self.rows.get(marker): + if self.rows.get(marker): + # print 'remove row', marker expr = self.remove_row(marker) if e_vars: + # print 'e_vars exist' for v in e_vars: if v != marker: + # print 'remove column',v self.remove_column(v) if cn.is_stay_constraint: if e_vars: - for p_evar, m_evar in zip(self.stay_plus_error_vars): - e_vars.remove(p_evar) - e_vars.remove(m_evar) + # for p_evar, m_evar in self.stay_error_vars: + remaining = [] + while self.stay_error_vars: + p_evar, m_evar = self.stay_error_vars.pop() + found = False + try: + # print 'stay constraint - remove plus evar', p_evar + e_vars.remove(p_evar) + found = True + except KeyError: + pass + try: + # print 'stay constraint - remove minus evar', m_evar + e_vars.remove(m_evar) + found = True + except KeyError: + pass + if not found: + remaining.append((p_evar, m_evar)) + self.stay_error_vars = remaining + elif cn.is_edit_constraint: assert e_vars is not None + # print 'edit constraint - remove column', self.edit_var_map[cn.variable].edit_minus self.remove_column(self.edit_var_map[cn.variable].edit_minus) del self.edit_var_map[cn.variable] if e_vars: for e_var in e_vars: + # print 'Remove error var', e_var del self.error_vars[e_var] if self.auto_solve: + # print 'final auto solve' self.optimize(self.objective) self.set_external_variables() @@ -510,7 +546,7 @@ class SimplexSolver(Tableau): exit_var = v if min_ratio == float('inf'): - raise InternalError('Objective function is unbounded in optimize') + raise RequiredFailure('Objective function is unbounded') self.pivot(entry_var, exit_var) @@ -530,7 +566,7 @@ class SimplexSolver(Tableau): def reset_stay_constants(self): # print "reset_stay_constants" - for p_var, m_var in zip(self.stay_plus_error_vars, self.stay_minus_error_vars): + for p_var, m_var in self.stay_error_vars: expr = self.rows.get(p_var) if expr is None: expr = self.rows.get(m_var) diff --git a/tailor/cassowary/utils.py b/tailor/cassowary/utils.py index 7972d0251..da7d3bd9d 100644 --- a/tailor/cassowary/utils.py +++ b/tailor/cassowary/utils.py @@ -2,4 +2,4 @@ EPSILON = 1e-8 def approx_equal(a, b): - return abs(a - b) < EPSILON \ No newline at end of file + return abs(a - b) < EPSILON diff --git a/tailor/cocoa/widgets/container.py b/tailor/cocoa/widgets/container.py index 14d2db1f2..6dbb0de6c 100644 --- a/tailor/cocoa/widgets/container.py +++ b/tailor/cocoa/widgets/container.py @@ -29,13 +29,18 @@ class Container(Widget): def __init__(self): super(Container, self).__init__() self._impl = NSView.alloc().init() - self.constraints = [] + self.children = [] + self.constraints = {} def add(self, widget): + self.children.append(widget) self._impl.addSubview_(widget._impl) def constrain(self, constraint): "Add the given constraint to the widget." + if constraint in self.constraints: + return + widget = constraint.attr.widget._impl identifier = constraint.attr.identifier @@ -62,4 +67,4 @@ class Container(Widget): self._impl.addConstraint_(constraint._impl) - self.constraints.append(constraint) + self.constraints[constraint] = constraint._impl diff --git a/tailor/constraint.py b/tailor/constraint.py index 7d117c3ea..e62d4e78a 100644 --- a/tailor/constraint.py +++ b/tailor/constraint.py @@ -71,29 +71,6 @@ class Attribute(object): self.multiplier = float(multiplier) self.constant = float(constant) - @property - def update_strategy(self): - update_strategies = { - Attribute.LEFT: Value.MINIMUM, - Attribute.RIGHT: Value.MAXIMUM, - Attribute.TOP: Value.MINIMUM, - Attribute.BOTTOM: Value.MAXIMUM, - Attribute.WIDTH: Value.MAXIMUM, - Attribute.HEIGHT: Value.MAXIMUM, - Attribute.CENTER_X: Value.AVERAGE, - Attribute.CENTER_Y: Value.AVERAGE, - # Attribute.BASELINE: Value.AVERAGE, - } - - if True: # Check for RTL - update_strategies[Attribute.LEADING] = Value.MINIMUM - update_strategies[Attribute.TRAILING] = Value.MAXIMUM - else: - update_strategies[Attribute.LEADING] = Value.MAXIMUM - update_strategies[Attribute.TRAILING] = Value.MINIMUM - - return update_strategies[self.identifier] - @property def identifier_label(self): return { diff --git a/tailor/gtk/widgets/base.py b/tailor/gtk/widgets/base.py index a0f8630e4..776a51dc9 100644 --- a/tailor/gtk/widgets/base.py +++ b/tailor/gtk/widgets/base.py @@ -1,4 +1,6 @@ -from tailor.widget import WidgetBase +from tailor.cassowary import Expression +from tailor.constraint import Attribute +from tailor.widget import WidgetBase, BoundingBox def wrapped_handler(widget, handler): @@ -9,11 +11,40 @@ def wrapped_handler(widget, handler): class Widget(WidgetBase): + def __init__(self): + super(Widget, self).__init__() + self._bounding_box = BoundingBox() + self._expand_horizontal = True + self._expand_vertical = True + + def _expression(self, identifier): + if identifier == Attribute.LEFT: + return Expression(variable=self._bounding_box.x) + elif identifier == Attribute.RIGHT: + return Expression(variable=self._bounding_box.x) + Expression(variable=self._bounding_box.width) + elif identifier == Attribute.TOP: + return Expression(variable=self._bounding_box.y) + elif identifier == Attribute.BOTTOM: + return Expression(variable=self._bounding_box.y) + Expression(variable=self._bounding_box.height) + elif identifier == Attribute.LEADING: + return Expression(variable=self._bounding_box.x) + elif identifier == Attribute.TRAILING: + return Expression(variable=self._bounding_box.x) + Expression(variable=self._bounding_box.width) + elif identifier == Attribute.WIDTH: + return Expression(variable=self._bounding_box.width) + elif identifier == Attribute.HEIGHT: + return Expression(variable=self._bounding_box.height) + elif identifier == Attribute.CENTER_X: + return Expression(variable=self._bounding_box.x) + (Expression(variable=self._bounding_box.width) / 2) + elif identifier == Attribute.CENTER_Y: + return Expression(variable=self._bounding_box.y) + (Expression(variable=self._bounding_box.height) / 2) + # elif identifier == self.BASELINE: + # return ... @property - def width_hints(self): + def _width_hint(self): return self._impl.get_preferred_width() @property - def height_hints(self): - return self._impl.get_preferred_width() + def _height_hint(self): + return self._impl.get_preferred_height() diff --git a/tailor/gtk/widgets/button.py b/tailor/gtk/widgets/button.py index 3be18ad48..c98008f4e 100644 --- a/tailor/gtk/widgets/button.py +++ b/tailor/gtk/widgets/button.py @@ -13,6 +13,9 @@ def wrapped_handler(widget, handler): class Button(Widget): def __init__(self, label, on_press=None): super(Button, self).__init__() + # Buttons have a fixed drawn height. If their space allocation is + # greater than what is provided, center the button vertically. + self._expand_vertical = False self.on_press = on_press diff --git a/tailor/gtk/widgets/container.py b/tailor/gtk/widgets/container.py index 539333129..4e42c034a 100644 --- a/tailor/gtk/widgets/container.py +++ b/tailor/gtk/widgets/container.py @@ -1,65 +1,218 @@ from gi.repository import Gtk, cairo -from tailor.constraint import LayoutManager +from tailor.constraint import Constraint +from tailor.cassowary import SimplexSolver, StayConstraint, WEAK, STRONG, REQUIRED, Equation, Inequality, Expression, approx_equal from tailor.gtk.widgets.base import Widget +class LayoutManager(SimplexSolver): + def __init__(self, bounding_box): + super(LayoutManager, self).__init__() + self.bounding_box = bounding_box + + self.children = {} + + # Enforce a hard constraint that the container starts at 0,0 + self.add_constraint(StayConstraint(self.bounding_box.x, strength=REQUIRED)) + self.add_constraint(StayConstraint(self.bounding_box.y, strength=REQUIRED)) + + # # Add a weak constraint for the bounds of the container. + self.width_constraint = StayConstraint(self.bounding_box.width, strength=WEAK) + self.height_constraint = StayConstraint(self.bounding_box.height, strength=WEAK) + + self.add_constraint(self.width_constraint) + self.add_constraint(self.height_constraint) + + def add_constraint(self, constraint): + print constraint + return super(LayoutManager, self).add_constraint(constraint) + + def add_widget(self, widget): + constraints = set() + + min_width, preferred_width = widget._width_hint + min_height, preferred_height = widget._height_hint + + print min_width, preferred_width + print min_height, preferred_height + # REQUIRED: Widget width must exceed minimum. + constraints.add(self.add_constraint( + Inequality( + Expression(variable=widget._bounding_box.width), + Inequality.GEQ, + min_width, + ) + )) + + # REQUIRED: Widget height must exceed minimum + constraints.add(self.add_constraint( + Inequality( + Expression(variable=widget._bounding_box.height), + Inequality.GEQ, + min_height, + ) + )) + + # STRONG: Adhere to preferred widget width + constraints.add(self.add_constraint( + Equation( + Expression(variable=widget._bounding_box.width), + preferred_width, + strength=STRONG + ) + )) + + # STRONG: Try to adhere to preferred widget height + constraints.add(self.add_constraint( + Equation( + Expression(variable=widget._bounding_box.height), + preferred_height, + strength=STRONG + ) + )) + + print constraints + self.children[widget] = constraints + + def enforce(self, width, height): + self.remove_constraint(self.width_constraint) + self.remove_constraint(self.height_constraint) + + self.bounding_box.width.value = width + self.bounding_box.height.value = height + + self.width_constraint = StayConstraint(self.bounding_box.width, strength=REQUIRED) + self.height_constraint = StayConstraint(self.bounding_box.height, strength=REQUIRED) + + self.add_constraint(self.width_constraint) + self.add_constraint(self.height_constraint) + + def relax(self): + self.remove_constraint(self.width_constraint) + self.remove_constraint(self.height_constraint) + + self.bounding_box.width.value = 0 + self.bounding_box.height.value = 0 + + self.width_constraint = StayConstraint(self.bounding_box.width, strength=WEAK) + self.height_constraint = StayConstraint(self.bounding_box.height, strength=WEAK) + + self.add_constraint(self.width_constraint) + self.add_constraint(self.height_constraint) + + class TContainer(Gtk.Fixed): def __init__(self, layout_manager): super(TContainer, self).__init__() - self.children = [] self.layout_manager = layout_manager def do_get_preferred_width(self): # Calculate the minimum and natural width of the container. - print "PREFERRED WIDTH" - return 290, 370 - # hint = self.layout_manager.layout(None, None)[self.container] - # return hint.right.vmin - hint.left.vmin, hint.right.vpref - hint.left.vpref, + width = self.layout_manager.bounding_box.width.value + # print "PREFERRED WIDTH", width + return width, width def do_get_preferred_height(self): # Calculate the minimum and natural height of the container. - print "PREFERRED HEIGHT" - return 100, 150 - # hint = self.layout_manager.layout(None, None)[self.container] - # return hint.bottom.vmin - hint.top.vmin, hint.bottom.vpref - hint.top.vpref, + height = self.layout_manager.bounding_box.height.value + # print "PREFERRED HEIGHT", height + return height, height def do_size_allocate(self, allocation): print "Size allocate", allocation.width, 'x', allocation.height, ' @ ', allocation.x, 'x', allocation.y - hints = self.layout_manager.layout(allocation.width, allocation.height) - for widget, hint in hints.items(): - if widget == self.layout_manager.container: - print 'LAYOUT CONTAINER' - self.set_allocation(allocation) - elif not widget._impl.get_visible(): + self.set_allocation(allocation) + + # Temporarily enforce a size requirement based on the allocation + self.layout_manager.enforce(allocation.width, allocation.height) + + for widget in self.layout_manager.children: + print widget, widget._bounding_box + if not widget._impl.get_visible(): print "CHILD NOT VISIBLE" else: print "CHILD", widget + min_width, preferred_width = widget._width_hint + min_height, preferred_height = widget._height_hint + + x_pos = widget._bounding_box.x.value + if widget._expand_horizontal: + width = widget._bounding_box.width.value + else: + x_pos = x_pos + ((widget._bounding_box.width.value - preferred_width) / 2.0) + width = preferred_width + + y_pos = widget._bounding_box.y.value + if widget._expand_vertical: + height = widget._bounding_box.height.value + else: + y_pos = y_pos + ((widget._bounding_box.height.value - preferred_height) / 2.0) + height = preferred_height + child_allocation = cairo.RectangleInt() - child_allocation.x = hint.left.vpref - child_allocation.y = hint.top.vpref - child_allocation.width = hint.right.vpref - hint.left.vpref - child_allocation.height = hint.bottom.vpref - hint.top.vpref + child_allocation.x = x_pos + child_allocation.y = y_pos + child_allocation.width = width + child_allocation.height = height widget._impl.size_allocate(child_allocation) + # Restore the unbounded allocation + self.layout_manager.relax() + class Container(Widget): + _RELATION = { + Constraint.LTE: Inequality.LEQ, + Constraint.GTE: Inequality.GEQ + } + def __init__(self): super(Container, self).__init__() - self._layout_manager = LayoutManager(self) + self._layout_manager = LayoutManager(self._bounding_box) self._impl = TContainer(self._layout_manager) - self._children = [] - self._constraints = [] def add(self, widget): - self._layout_manager.reset() - self._children.append(widget) self._impl.add(widget._impl) + self._layout_manager.add_widget(widget) def constrain(self, constraint): "Add the given constraint to the widget." - self._layout_manager.reset() - self._constraints.append(constraint) + widget = constraint.attr.widget + identifier = constraint.attr.identifier + + if constraint.related_attr: + related_widget = constraint.related_attr.widget + related_identifier = constraint.related_attr.identifier + + expr1 = widget._expression(identifier) + if not approx_equal(constraint.attr.multiplier, 1.0): + expr1 = expr1 * constraint.attr.multiplier + if not approx_equal(constraint.attr.constant, 0.0): + expr1 = expr1 + constraint.attr.constant + + expr2 = related_widget._expression(related_identifier) + if not approx_equal(constraint.related_attr.multiplier, 1.0): + expr2 = expr2 * constraint.related_attr.multiplier + if not approx_equal(constraint.related_attr.constant, 0.0): + expr2 = expr2 + constraint.related_attr.constant + + print 'E1', expr1 + print 'E2', expr2 + + if constraint.relation == Constraint.EQUAL: + self._layout_manager.add_constraint(Equation(expr1, expr2)) + else: + self._layout_manager.add_constraint(Inequality(expr1, self._RELATION[constraint.relation], expr2)) + else: + expr = widget._expression(identifier) + if not approx_equal(constraint.attr.multiplier, 1.0): + expr = expr * constraint.attr.multiplier + + print 'E', expr + + if constraint.relation == Constraint.EQUAL: + self._layout_manager.add_constraint(Equation(expr, constraint.attr.constant)) + else: + self._layout_manager.add_constraint(Equation(expr, self._RELATION[constraint.relation], constraint.attr.constant)) diff --git a/tailor/widget.py b/tailor/widget.py index 18ec547fb..58b515e2d 100644 --- a/tailor/widget.py +++ b/tailor/widget.py @@ -1,6 +1,18 @@ +from tailor.cassowary import Variable from tailor.constraint import Attribute +class BoundingBox(object): + def __init__(self): + self.x = Variable('x', 0.0) + self.y = Variable('y', 0.0) + self.width = Variable('width', 0.0) + self.height = Variable('height', 0.0) + + def __repr__(self): + return u'%sx%s @ %s,%s' % (self.width.value, self.height.value, self.x.value, self.y.value) + + class WidgetBase(object): def __init__(self): self.LEFT = Attribute(self, Attribute.LEFT)