Created
March 26, 2012 17:00
-
-
Save rkern/2206505 to your computer and use it in GitHub Desktop.
Layout debugging tools
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from collections import defaultdict | |
from enable.api import (AbstractOverlay, ColorTrait, | |
Component as EnableComponent, Container as EnableContainer, LineStyle, | |
transparent_color) | |
from kiva.constants import FILL, STROKE | |
from traits.api import (Any, Bool, Float, HasTraits, Instance, List, Property, | |
NO_COMPARE, on_trait_change) | |
from casuarius import medium | |
from enaml.components.constraints_widget import ConstraintsWidget | |
from enaml.components.container import Container | |
from enaml.core.trait_types import EnamlEvent | |
from enaml.item_models.abstract_item_model import (ALIGN_LEFT, ALIGN_RIGHT, | |
AbstractTableModel) | |
from enaml.layout.constraints_layout import ConstraintsLayout | |
class Coords(HasTraits): | |
top = Float() | |
left = Float() | |
width = Float() | |
height = Float() | |
v_center = Property() | |
_v_center = Any() | |
def _get_v_center(self): | |
if self._v_center is None: | |
return self.top + 0.5 * self.height | |
else: | |
return self._v_center | |
def _set_v_center(self, value): | |
self._v_center = value | |
h_center = Property() | |
_h_center = Any() | |
def _get_h_center(self): | |
if self._h_center is None: | |
return self.left + 0.5 * self.width | |
else: | |
return self._h_center | |
def _set_h_center(self, value): | |
self._h_center = value | |
class Box(EnableComponent): | |
enaml = Instance(ConstraintsWidget) | |
highlighted = Bool(False) | |
normal_fill_color = ColorTrait('transparent') | |
highlight_fill_color = ColorTrait('red') | |
fill_color = Property(depends_on=['highlighted', 'normal_fill_color', | |
'highlight_border_color']) | |
def _get_fill_color(self): | |
if self.highlighted: | |
return self.highlight_fill_color_ | |
else: | |
return self.normal_fill_color_ | |
normal_border_color = ColorTrait('lightgray') | |
highlight_border_color = ColorTrait('black') | |
border_color = Property(depends_on=['highlighted', 'normal_border_color', | |
'highlight_border_color']) | |
def _get_border_color(self): | |
if self.highlighted: | |
return self.highlight_border_color_ | |
else: | |
return self.normal_border_color_ | |
coords = Instance(Coords, args=()) | |
def update_from_enaml(self): | |
""" Update the geometry from the Enaml component. | |
""" | |
if self.enaml.abstract_obj.widget is not None: | |
coords = self.coords | |
coords.left = self.enaml.left.value | |
coords.top = self.enaml.top.value | |
coords.width = self.enaml.width.value | |
coords.height = self.enaml.height.value | |
self.position = [coords.left, self.container.height - coords.top - coords.height] | |
self.bounds = [coords.width, coords.height] | |
coords.v_center = self.enaml.v_center.value | |
coords.h_center = self.enaml.h_center.value | |
if hasattr(self.enaml, 'midline'): | |
coords.midline = self.enaml.midline.value | |
if hasattr(self.enaml, 'margin_top'): | |
coords.margin_top = self.enaml.margin_top.value | |
coords.margin_left = self.enaml.margin_left.value | |
coords.margin_right = self.enaml.margin_right.value | |
coords.margin_bottom = self.enaml.margin_bottom.value | |
def _draw_mainlayer(self, gc, view_bounds=None, mode="default"): | |
""" Draw the box background in a specified graphics context. | |
""" | |
# Set up all the control variables for quick access: | |
coords = self.coords | |
x, y = coords.left, coords.top | |
dx, dy = coords.width, coords.height | |
with gc: | |
gc.translate_ctm(0.0, self.container.height) | |
gc.scale_ctm(1.0, -1.0) | |
# Fill the background region (if required); | |
color = self.fill_color | |
if color is not transparent_color: | |
gc.set_fill_color(color) | |
gc.draw_rect((x, y, dx, dy), FILL) | |
# Draw the border (if required): | |
border_color = self.border_color | |
if border_color is not transparent_color: | |
gc.set_stroke_color(border_color) | |
gc.set_line_width(1) | |
gc.draw_rect((x, y, dx, dy), STROKE) | |
# Draw the center lines, margins and midlines only if highlighted. | |
if self.highlighted and border_color is not transparent_color: | |
gc.set_stroke_color(border_color) | |
gc.set_alpha(0.5) | |
gc.set_line_width(1) | |
gc.begin_path() | |
gc.move_to(x, coords.v_center) | |
gc.line_to(x+dx, coords.v_center) | |
gc.move_to(coords.h_center, y) | |
gc.line_to(coords.h_center, y+dy) | |
if hasattr(coords, 'midline'): | |
gc.move_to(coords.midline, y) | |
gc.line_to(coords.midline, y+dy) | |
gc.stroke_path() | |
if hasattr(coords, 'margin_top'): | |
gc.draw_rect((x-coords.margin_left, y-coords.margin_top, | |
dx+coords.margin_left+coords.margin_right, | |
dy+coords.margin_top+coords.margin_bottom), STROKE) | |
class ViewOutlines(EnableContainer): | |
padding_left = 0 | |
padding_right = 0 | |
padding_top = 0 | |
padding_bottom = 0 | |
@classmethod | |
def fromcomponents(cls, components): | |
children = [] | |
for component in components: | |
box = Box(enaml=component) | |
children.append(box) | |
self = cls(*children) | |
return self | |
def update_from_enaml(self): | |
for box in self.components: | |
box.update_from_enaml() | |
self.request_redraw() | |
def highlight(self, enamls): | |
for box in self.components: | |
box.highlighted = (box.enaml in enamls) | |
self.request_redraw() | |
def _bounds_changed(self, old, new): | |
super(ViewOutlines, self)._bounds_changed(old, new) | |
class ComponentModel(AbstractTableModel): | |
def __init__(self, components): | |
self.components = components | |
self.columns = [ | |
('Name', 'name'), | |
('ID', 'id'), | |
('Left', 'left'), | |
('Top', 'top'), | |
('Width', 'width'), | |
('Height', 'height'), | |
] | |
self._data = [] | |
self.update() | |
def column_count(self, parent=None): | |
return len(self.columns) | |
def row_count(self, parent=None): | |
return len(self.components) | |
def data(self, index): | |
return self._data[index.row][index.column] | |
def alignment(self, index): | |
if index.column < 2: | |
return ALIGN_LEFT | |
else: | |
return ALIGN_RIGHT | |
def horizontal_header_data(self, section): | |
return self.columns[section][0] | |
def update(self): | |
""" Redraw everything. | |
""" | |
self.begin_reset_model() | |
self._data = [] | |
for component in self.components: | |
self._data.append(( | |
self._get_name(component), | |
self._get_id(component), | |
self._get_left(component), | |
self._get_top(component), | |
self._get_width(component), | |
self._get_height(component), | |
)) | |
self.end_reset_model() | |
def _get_name(self, component): | |
# FIXME: if we can get the depth in the tree, it would be nice to indent | |
# this. | |
return type(component).__name__ | |
def _get_id(self, component): | |
return u'{0:x}'.format(id(component)) | |
def _get_top(self, component): | |
return component.layout_geometry()[1] | |
def _get_left(self, component): | |
return component.layout_geometry()[0] | |
def _get_width(self, component): | |
return component.layout_geometry()[2] | |
def _get_height(self, component): | |
return component.layout_geometry()[3] | |
class ConstraintsModel(AbstractTableModel): | |
def __init__(self, layout_mgr): | |
self.layout_mgr = layout_mgr | |
self.layout_mgr.on_trait_change(self.update, 'current_constraints') | |
self.layout_mgr.on_trait_change(self.update, 'layout_event') | |
self._data = [] | |
self._filter_ids = () | |
self.filtered_constraints = [] | |
self.update() | |
def row_count(self, parent=None): | |
if parent is not None: | |
return 0 | |
else: | |
return len(self._data) | |
def column_count(self, parent=None): | |
return 4 | |
def horizontal_header_data(self, section): | |
return ('Constraint', 'Error', 'Strength', 'Weight')[section] | |
def data(self, index): | |
return self._data[index.row][index.column] | |
def alignment(self, index): | |
return ALIGN_LEFT | |
def update(self): | |
self.begin_reset_model() | |
if self._filter_ids: | |
self.filtered_constraints = [] | |
for cn in self.layout_mgr.current_constraints: | |
for term in cn.lhs.terms + cn.rhs.terms: | |
if term.var.name.endswith(self._filter_ids): | |
self.filtered_constraints.append(cn) | |
break | |
else: | |
self.filtered_constraints = self.layout_mgr.current_constraints | |
self._data = [ | |
(unicode(cn), unicode(cn.error), unicode(cn.strength.name), unicode(cn.weight)) | |
for cn in self.filtered_constraints | |
] | |
self.end_reset_model() | |
def filter(self, components): | |
self._filter_ids = tuple('_{0:x}'.format(id(c)) for c in components) | |
self.update() | |
class DebugLayout(HasTraits, ConstraintsLayout): | |
current_constraints = List() | |
layout_event = EnamlEvent() | |
def initialize(self, constraints): | |
self.current_constraints = constraints | |
super(DebugLayout, self).initialize(constraints) | |
def layout(self, cb, width, height, size, strength=medium, weight=1.0): | |
def f(): | |
cb() | |
self.layout_event() | |
super(DebugLayout, self).layout(f, width, height, size, strength, weight) | |
class DebugContainer(Container): | |
def transfer_layout_ownership(self, owner): | |
return False | |
class ConstraintsOverlay(AbstractOverlay): | |
layout_mgr = Instance(DebugLayout) | |
selected_constraints = List(comparison_mode=NO_COMPARE) | |
boxes = Any() | |
term_color = ColorTrait('lightblue') | |
term_line_style = LineStyle('solid') | |
@on_trait_change('layout_mgr:layout_event') | |
def update_from_enaml(self): | |
""" Update the constraints from Enaml. | |
""" | |
self.boxes = defaultdict(Coords) | |
if self.layout_mgr is not None: | |
for constraint in self.layout_mgr.current_constraints: | |
for expr in (constraint.lhs, constraint.rhs): | |
for term in expr.terms: | |
name, attr = self.split_var_name(term.var.name) | |
setattr(self.boxes[name], attr, term.var.value) | |
self.request_redraw() | |
def _selected_constraints_changed(self): | |
self.request_redraw() | |
def split_var_name(self, var_name): | |
attr, class_name, hexid = var_name.rsplit('_', 2) | |
name = '{}_{}'.format(class_name, hexid) | |
return name, attr | |
def overlay(self, other_component, gc, view_bounds=None, mode="normal"): | |
""" Draws this component overlaid on another component. | |
""" | |
with gc: | |
gc.translate_ctm(0.0, other_component.height) | |
gc.scale_ctm(1.0, -1.0) | |
gc.set_stroke_color(self.term_color_) | |
gc.set_line_dash(self.term_line_style_) | |
gc.set_line_width(3) | |
term_attrs = set() | |
for constraint in self.selected_constraints: | |
for expr in (constraint.lhs, constraint.rhs): | |
for term in expr.terms: | |
term_attrs.add(self.split_var_name(term.var.name)) | |
for name, attr in sorted(term_attrs): | |
box = self.boxes[name] | |
if attr == 'top': | |
self.hline(gc, box.left, box.top, box.width) | |
elif attr == 'left': | |
self.vline(gc, box.left, box.top, box.height) | |
elif attr == 'width': | |
self.hline(gc, box.left, box.v_center, box.width) | |
elif attr == 'height': | |
self.vline(gc, box.h_center, box.top, box.height) | |
elif attr == 'midline': | |
self.vline(gc, box.midline, box.top, box.height) | |
elif attr == 'margin_top': | |
self.vline(gc, box.h_center, box.top, -box.margin_top) | |
elif attr == 'margin_bottom': | |
self.vline(gc, box.h_center, box.top+box.height, box.margin_bottom) | |
elif attr == 'margin_left': | |
self.vline(gc, box.left, box.v_center, -box.margin_left) | |
elif attr == 'margin_right': | |
self.hline(gc, box.left+box.width, box.v_center, box.margin_right) | |
elif attr == 'bottom': | |
self.hline(gc, box.left, box.bottom, box.right - box.left) | |
elif attr == 'right': | |
self.vline(gc, box.right, box.top, box.bottom - box.top) | |
gc.stroke_path() | |
def vline(self, gc, x, y0, length): | |
""" Draw a vertical line. | |
""" | |
gc.move_to(x, y0) | |
gc.line_to(x, y0+length) | |
def hline(self, gc, x0, y, length): | |
""" Draw a horizontal line. | |
""" | |
gc.move_to(x0, y) | |
gc.line_to(x0+length, y) | |
def debugize_container(container): | |
""" Install the debugging hooks for a given Container. | |
""" | |
assert type(container) is Container | |
container.__class__ = DebugContainer | |
container._layout_owner = None | |
container.add_trait('layout_manager', Instance(DebugLayout, args=())) | |
container.hug = ('weak', 'weak') | |
container.initialize_layout() | |
def traverse_layout(root): | |
""" Traverse the laid out components starting with the root container. | |
""" | |
yield root | |
for child in root.constraints_children: | |
if isinstance(child, Container) and child.transfer_layout_ownership(root): | |
for c in traverse_layout(child): | |
yield c | |
elif isinstance(child, ConstraintsWidget): | |
yield child |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#------------------------------------------------------------------------------ | |
# Copyright (c) 2011, Enthought, Inc. | |
# All rights reserved. | |
#------------------------------------------------------------------------------ | |
""" Command-line tool to debug Enaml layouts. | |
""" | |
import optparse | |
import sys | |
import types | |
from enaml import imports, default_toolkit, wx_toolkit, qt_toolkit | |
from enaml.core.parser import parse | |
from enaml.core.enaml_compiler import EnamlCompiler | |
toolkits = { | |
'default': default_toolkit, 'wx': wx_toolkit, 'qt': qt_toolkit, | |
} | |
def main(): | |
usage = 'usage: %prog [options] enaml_file' | |
parser = optparse.OptionParser(usage=usage, description=__doc__) | |
parser.add_option('-c', '--component', default='Main', | |
help='The component to view') | |
parser.add_option('-t', '--toolkit', default='default', | |
choices=['default', 'wx', 'qt'], | |
help='The toolkit backend to use') | |
options, args = parser.parse_args() | |
if len(args) == 0: | |
print 'No .enaml file specified' | |
sys.exit() | |
elif len(args) > 1: | |
print 'Too many files specified' | |
sys.exit() | |
else: | |
enaml_file = args[0] | |
with open(enaml_file) as f: | |
enaml_code = f.read() | |
# Parse and compile the Enaml source into a code object | |
ast = parse(enaml_code, filename=enaml_file) | |
code = EnamlCompiler.compile(ast, enaml_file) | |
# Create a proper module in which to execute the compiled code so | |
# that exceptions get reported with better meaning | |
module = types.ModuleType('__main__') | |
module.__file__ = enaml_file | |
ns = module.__dict__ | |
with imports(): | |
exec code in ns | |
from debug_ui import DebugLayoutUI | |
with toolkits[options.toolkit](): | |
requested = options.component | |
if requested in ns: | |
component = ns[requested] | |
window = DebugLayoutUI(root=component().children[0]) | |
window.show() | |
else: | |
msg = "Could not find component '%s'" % options.component | |
print msg | |
if __name__ == '__main__': | |
main() | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from debug_layout import ViewOutlines, ComponentModel, ConstraintsModel, DebugLayout, ConstraintsOverlay, traverse_layout, debugize_container | |
enamldef ComponentTable(MainWindow): | |
id: main | |
attr components | |
attr selection_obj | |
attr item_model : ComponentModel = ComponentModel(components) | |
title = 'View Components' | |
Container: | |
constraints = [ | |
vbox(tv), | |
width >= 400, | |
height >= 250, | |
] | |
TableView: | |
id: tv | |
hug = ('ignore', 'ignore') | |
item_model = main.item_model | |
initialized :: | |
self.toolkit_widget.resizeColumnsToContents() | |
RowSelectionModel: | |
selection_mode = 'extended' | |
selected_rows := main.selection_obj.selected_component_indices | |
enamldef ConstraintsTable(MainWindow): | |
id: main | |
attr layout_mgr | |
attr selection_obj | |
attr item_model : ConstraintsModel = ConstraintsModel(layout_mgr) | |
title = 'View Constraints' | |
Container: | |
constraints = [ | |
vbox(tv), | |
width >= 800, | |
height >= 600, | |
] | |
TableView: | |
id: tv | |
hug = ('ignore', 'ignore') | |
item_model = main.item_model | |
initialized :: | |
self.toolkit_widget.resizeColumnsToContents() | |
RowSelectionModel: | |
selection_mode = 'extended' | |
selected_rows := main.selection_obj.selected_constraint_indices | |
enamldef DebugLayoutUI(MainWindow): | |
id: main | |
attr root | |
attr components : list = list(traverse_layout(root)) | |
attr view_outlines : ViewOutlines = ViewOutlines.fromcomponents(components) | |
attr component_table : ComponentTable = ComponentTable(components=components) | |
attr constraints_table : ConstraintsTable = ConstraintsTable() | |
attr constraints_overlay : ConstraintsOverlay | |
attr selected_component_indices : list = [] | |
attr selected_components : list = [] | |
attr selected_constraint_indices : list = [] | |
attr selected_constraints : list = [] | |
title = 'Debug Layout' | |
initialized :: | |
debugize_container(self.root) | |
self.component_table.selection_obj = self | |
self.constraints_table.selection_obj = self | |
self.constraints_table.layout_mgr = self.root.layout_manager | |
self.root.layout_manager.on_trait_change(self.view_outlines.update_from_enaml, 'layout_event') | |
self.root.layout_manager.on_trait_change(self.component_table.item_model.update, 'layout_event') | |
self.constraints_overlay = ConstraintsOverlay(component=self.view_outlines, layout_mgr=self.root.layout_manager) | |
self.view_outlines.overlays.append(self.constraints_overlay) | |
selected_component_indices :: | |
self.selected_components = [self.components[i] for i in self.selected_component_indices] | |
selected_components :: | |
self.view_outlines.highlight(self.selected_components) | |
self.constraints_table.item_model.filter(self.selected_components) | |
selected_constraint_indices :: | |
self.selected_constraints = [self.constraints_table.item_model.filtered_constraints[i] for i in self.selected_constraint_indices] | |
selected_constraints :: | |
self.constraints_overlay.selected_constraints = self.selected_constraints | |
Container: | |
constraints = [ | |
vbox(hbox(components_button, constraints_button, _space_), side_by_side), | |
] | |
PushButton: | |
id: components_button | |
text = u"Components" | |
clicked :: | |
main.component_table.show() | |
PushButton: | |
id: constraints_button | |
text = u"Constraints" | |
clicked :: | |
main.constraints_table.show() | |
Container: | |
id: side_by_side | |
constraints = [ | |
horizontal(left, _space_==0, root, _space_==0, enable_view, _space_==0, right), | |
vertical(top, _space_==0, enable_view, _space_==0, bottom), | |
vertical(top, _space_==0, root, _space_==0, bottom), | |
root.width == enable_view.width, | |
root.height == enable_view.height, | |
] | |
Include: | |
components = [root] | |
EnableCanvas: | |
id: enable_view | |
component = view_outlines |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment