383 lines
13 KiB
Python
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)
|