Lightningbeam/GUI/Generic/GContainers.py

383 lines
13 KiB
Python

#
# Python GUI - Containers - Generic
#
try:
maketrans = str.maketrans
except AttributeError:
from string import maketrans
from GUI.Properties import overridable_property
from GUI.Exceptions import ArgumentError
from GUI.Geometry import pt_in_rect
from GUI import Component
anchor_to_sticky = maketrans("ltrb", "wnes")
class Container(Component):
"""A Container is a Component that can contain other Components.
The sub-components are clipped to the boundary of their container."""
contents = overridable_property('contents',
"List of subcomponents. Do not modify directly.")
content_width = overridable_property('content_width', "Width of the content area.")
content_height = overridable_property('content_height', "Height of the content area.")
content_size = overridable_property('content_size', "Size of the content area.")
auto_layout = overridable_property('auto_layout',
"Automatically adjust layout of subcomponents when resized.")
_auto_layout = True
# _contents [Component]
def __init__(self, **kw):
self._contents = []
Component.__init__(self, **kw)
def destroy(self):
"""Destroy this Container and all of its contents."""
contents = self._contents
while contents:
comp = contents[-1]
comp.destroy()
assert not contents or contents[-1] is not comp, \
"%r failed to remove itself from container on destruction" % comp
Component.destroy(self)
#
# Content area
#
def get_content_width(self):
return self.content_size[0]
def set_content_width(self, w):
self.content_size = w, self.content_height
def get_content_height(self):
return self.content_size[1]
def set_content_height(self, h):
self.content_size = self.content_width, h
get_content_size = Component.get_size
set_content_size = Component.set_size
#
# Subcomponent Management
#
def get_contents(self):
return self._contents
def add(self, comp):
"""Add the given Component as a subcomponent."""
if comp:
if isinstance(comp, Component):
comp.container = self
else:
for item in comp:
self.add(item)
def remove(self, comp):
"""Remove subcomponent, if present."""
if isinstance(comp, Component):
if comp in self._contents:
comp.container = None
else:
for item in comp:
self.remove(item)
def _add(self, comp):
# Called by comp.set_container() to implement subcomponent addition.
self._contents.append(comp)
self._invalidate_tab_chain()
self.added(comp)
def _remove(self, comp):
# Called by comp.set_container() to implement subcomponent removal.
self._contents.remove(comp)
self._invalidate_tab_chain()
self.removed(comp)
def added(self, comp):
"""Called after a subcomponent has been added."""
pass
def removed(self, comp):
"""Called after a subcomponent has been removed."""
pass
#
# The infamous 'place' method and friends.
#
_place_default_spacing = 8
def place(self, item,
left = None, right = None, top = None, bottom = None,
sticky = 'nw', scrolling = '', border = None, anchor = None):
"""Add a component to the frame with positioning,
resizing and scrolling options. See the manual for details."""
self._place([item], left = left, right = right, top = top, bottom = bottom,
sticky = sticky, scrolling = scrolling, border = border, anchor = anchor)
def place_row(self, items,
left = None, right = None, top = None, bottom = None,
sticky = 'nw', scrolling = '', border = None, spacing = None,
anchor = None):
"""Add a row of components to the frame with positioning,
resizing and scrolling options. See the manual for details."""
if left is not None and right is not None:
raise ValueError("Cannot specify both left and right to place_row")
elif left is None and right is not None:
direction = 'left'
items = items[:]
items.reverse()
else:
direction = 'right'
self._place(items, left = left, right = right, top = top, bottom = bottom,
sticky = sticky, scrolling = scrolling, border = border,
direction = direction, spacing = spacing, anchor = anchor)
def place_column(self, items,
left = None, right = None, top = None, bottom = None,
sticky = 'nw', scrolling = '', border = None, spacing = None,
anchor = None):
"""Add a column of components to the frame with positioning,
resizing and scrolling options. See the manual for details."""
if top is not None and bottom is not None:
raise ValueError("Cannot specify both top and bottom to place_column")
elif top is None and bottom is not None:
direction = 'up'
items = items[:]
items.reverse()
else:
direction = 'down'
self._place(items, left = left, right = right, top = top, bottom = bottom,
sticky = sticky, scrolling = scrolling, border = border,
direction = direction, spacing = spacing, anchor = anchor)
def _place(self, items,
left = None,
right = None,
top = None,
bottom = None,
sticky = 'nw',
scrolling = '',
direction = 'right',
spacing = None,
border = None,
anchor = None):
def side(spec, name):
# Process a side specification taking the form of either
# (1) an offset, (2) a reference component, or (3) a
# tuple (component, offset). Returns a tuple (ref, offset)
# where ref is the reference component or None (representing
# the Frame being placed into). Checks that the reference
# component, if any, is directly contained by this Frame.
ref = None
offset = None
if spec is not None:
if isinstance(spec, tuple):
ref, offset = spec
elif isinstance(spec, Component):
ref = spec
offset = 0
elif isinstance(spec, (int, float)):
offset = spec
else:
raise ArgumentError(self, 'place', name, spec)
if ref is self:
ref = None
elif ref:
con = ref.container
#if con is not self and isinstance(con, ScrollFrame):
# ref = con
# con = ref.container
if con is not self:
raise ValueError("Reference component for place() is not"
" directly contained by the frame being placed into.")
return ref, offset
if spacing is None:
spacing = self._place_default_spacing
# Decode the sticky options
if anchor is not None:
sticky = anchor.translate(anchor_to_sticky)
hmove = vmove = hstretch = vstretch = 0
if 'e' in sticky:
if 'w' in sticky:
hstretch = 1
else:
hmove = 1
if 's' in sticky:
if 'n' in sticky:
vstretch = 1
else:
vmove = 1
# Translate the direction argument
try:
dir = {'right':0, 'down':1, 'left':2, 'up':3}[direction]
except KeyError:
raise ArgumentError(self, 'place', 'direction', direction)
# Unpack the side arguments
left_obj, left_off = side(left, 'left')
right_obj, right_off = side(right, 'right')
top_obj, top_off = side(top, 'top')
bottom_obj, bottom_off = side(bottom, 'bottom')
# Process the items
#if not isinstance(items, list):
# items = [items]
for item in items:
x, y = item.position
w, h = item.size
# Calculate left edge position
if left_obj:
l = left_obj.left + left_obj.width + left_off
elif left_off is not None:
if left_off < 0:
l = self.width + left_off
else:
l = left_off
else:
l = None
# Calculate top edge position
if top_obj:
t = top_obj.top + top_obj.height + top_off
elif top_off is not None:
if top_off < 0:
t = self.height + top_off
else:
t = top_off
else:
t = None
# Calculate right edge position
if right_obj:
r = right_obj.left + right_off
elif right_off is not None:
if right_off <= 0:
r = self.width + right_off
else:
r = right_off
else:
r = None
# Calculate bottom edge position
if bottom_obj:
b = bottom_obj.top + bottom_off
elif bottom_off is not None:
if bottom_off <= 0:
b = self.height + bottom_off
else:
b = bottom_off
else:
b = None
# Fill in unspecified positions
if l is None:
if r is not None:
l = r - w
else:
l = x
if r is None:
r = l + w
if t is None:
if b is not None:
t = b - h
else:
t = y
if b is None:
b = t + h
if scrolling:
item.scrolling = scrolling
# Position, resize and add the item
item.bounds = (l, t, r, b)
self.add(item)
# Record resizing and border options
item.hmove = hmove
item.vmove = vmove
item.hstretch = hstretch
item.vstretch = vstretch
if border is not None:
item.border = border
# Step to the next item
if dir == 0:
left_obj = item
left_off = spacing
elif dir == 1:
top_obj = item
top_off = spacing
elif dir == 2:
right_obj = item
right_off = -spacing
else:
bottom_obj = item
bottom_off = -spacing
#
# Resizing
#
def _resized(self, delta):
if self._auto_layout:
self.resized(delta)
def resized(self, delta):
for c in self._contents:
c.container_resized(delta)
def resize(self, auto_layout = False, **kwds):
"""Change the geometry of the component, with control over whether
the layout of subcomponents is updated. The default is not to do so.
Keyword arguments to this method may be any of the properties
affecting position and size (i.e. left, top, right, bottom, x, y,
width, height, position, size, bounds)."""
old_auto_layout = self.auto_layout
try:
self.auto_layout = auto_layout
self.set(**kwds)
finally:
self.auto_layout = old_auto_layout
#
# Tabbing
#
def _build_tab_chain(self, chain):
Component._build_tab_chain(self, chain)
for c in self._contents:
c._build_tab_chain(chain)
#
# Other
#
def shrink_wrap(self, padding = None):
"""Adjust the size of the component so that it neatly encloses its
contents. If padding is specified, it specifies the amount of space
to leave at right and bottom, otherwise the minimum distance from the
left and top sides to the nearest components is used."""
contents = self.contents
if not contents:
return
if padding:
hpad, vpad = padding
else:
hpad = min([item.left for item in contents])
vpad = min([item.top for item in contents])
rights = [item.right for item in contents]
bottoms = [item.bottom for item in contents]
self.resize(size = (max(rights) + hpad, max(bottoms) + vpad))
def broadcast(self, message, *args):
"""Traverse the component hierarchy, calling each component's handler for
the given message, if any."""
Component.broadcast(self, message, *args)
for comp in self._contents:
comp.broadcast(message, *args)