# # 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)