class WidgetContainer(Widget): def __init__(self, widget_list): self.__super.__init__() self._widget_list = MonitoredList([]) self._set_widget_list(widget_list) self._widget_list.set_modified_callback(self._invalidate) def _get_widget_list(self): return self._widget_list def _set_widget_list(self, widget_list): """ widget_list -- iterable containing widgets Copy the values from widget_list into self.widget_list """ widget_list = property(_get_widget_list, _set_widget_list) def __getitem__(self, index): """ Return the base widget of the widget at self.widget_list[index]. """ w = self._widget_list[index] if hasattr(w, 'base_widget'): w = w.base_widget return w def __len__(self): return len(self._widget_list) def __iter__(self): i = 0 try: while True: v = self[i] yield v i += 1 except IndexError: return def __contains__(self, value): for v in self: if v == value: return True return False def __reversed__(self): for i in reversed(range(len(self))): yield self[i] def index(self, value): for i, v in enumerate(self): if v == value: return i raise ValueError def count(self, value): return sum(1 for v in self if v == value)
class WidgetContainer(Widget): def __init__(self, widget_list): self.__super.__init__() self._widget_list = MonitoredList([]) self._set_widget_list(widget_list) self._widget_list.set_modified_callback(self._invalidate) def _get_widget_list(self): return self._widget_list def _set_widget_list(self, widget_list): """ widget_list -- iterable containing widgets Copy the values from widget_list into self.widget_list """ widget_list = property(_get_widget_list, _set_widget_list) def __getitem__(self, index): """ Return the base widget of the widget at self.widget_list[index]. """ w = self._widget_list[index] if hasattr(w, "base_widget"): w = w.base_widget return w def __len__(self): return len(self._widget_list) def __iter__(self): i = 0 try: while True: v = self[i] yield v i += 1 except IndexError: return def __contains__(self, value): for v in self: if v == value: return True return False def __reversed__(self): for i in reversed(range(len(self))): yield self[i] def index(self, value): for i, v in enumerate(self): if v == value: return i raise ValueError def count(self, value): return sum(1 for v in self if v == value)
def __init__(self, widget_list, dividechars=0, focus_column=None, min_width=1, box_columns=None): """ widget_list -- list of flow widgets or list of box widgets dividechars -- blank characters between columns focus_column -- index into widget_list of column in focus, if None the first selectable widget will be chosen. min_width -- minimum width for each column which is not designated as flow widget in widget_list. box_columns -- a list of column indexes containing box widgets whose maxrow is set to the maximum of the rows required by columns not listed in box_columns. widget_list may also contain tuples such as: ('flow', widget) always treat widget as a flow widget ('fixed', width, widget) give this column a fixed width ('weight', weight, widget) give this column a relative weight widgets not in a tuple are the same as ('weight', 1, widget) box_columns is ignored when this widget is being used as a box widget because in that case all columns are treated as box widgets. """ self.__super.__init__() self.widget_list = MonitoredList(widget_list) self.column_types = [] self.has_flow_type = False for i in range(len(widget_list)): w = widget_list[i] if type(w) != tuple: self.column_types.append(('weight',1)) elif w[0] == 'flow': f, widget = w self.widget_list[i] = widget self.column_types.append((f,None)) self.has_flow_type = True w = widget elif w[0] in ('fixed', 'weight'): f,width,widget = w self.widget_list[i] = widget self.column_types.append((f,width)) w = widget else: raise ColumnsError, "widget list item invalid: %r" % (w,) if focus_column is None and w.selectable(): focus_column = i self.widget_list.set_modified_callback(self._invalidate) self.dividechars = dividechars if focus_column is None: focus_column = 0 self.focus_col = focus_column self.pref_col = None self.min_width = min_width self.box_columns = box_columns self._cache_maxcol = None
def __init__(self, contents): """ contents -- list to copy into this object Changes made to this object (when it is treated as a list) are detected automatically and will cause ListBox objects using this list walker to be updated. """ if not type(contents) == list and not hasattr(contents, '__getitem__'): raise ListWalkerError, "SimpleListWalker expecting list like object, got: %r"%(contents,) MonitoredList.__init__(self, contents) self.focus = 0
def __init__(self, widget_list, focus_item=None): """ widget_list -- list of widgets focus_item -- widget or integer index, if None the first selectable widget will be chosen. widget_list may also contain tuples such as: ('flow', widget) always treat widget as a flow widget ('fixed', height, widget) give this box widget a fixed height ('weight', weight, widget) if the pile is treated as a box widget then treat widget as a box widget with a height based on its relative weight value, otherwise treat widget as a flow widget widgets not in a tuple are the same as ('weight', 1, widget) If the pile is treated as a box widget there must be at least one 'weight' tuple in widget_list. """ self.__super.__init__() self.widget_list = MonitoredList(widget_list) self.item_types = [] for i in range(len(widget_list)): w = widget_list[i] if type(w) != tuple: self.item_types.append(('weight',1)) elif w[0] == 'flow': f, widget = w self.widget_list[i] = widget self.item_types.append((f,None)) w = widget elif w[0] in ('fixed', 'weight'): f, height, widget = w self.widget_list[i] = widget self.item_types.append((f,height)) w = widget else: raise PileError, "widget list item invalid %r" % (w,) if focus_item is None and w.selectable(): focus_item = i self.widget_list.set_modified_callback(self._invalidate) if focus_item is None: focus_item = 0 if self.widget_list: self.set_focus(focus_item) else: self.focus_item=None self.pref_col = 0
class Pile(Widget): # either FlowWidget or BoxWidget def __init__(self, widget_list, focus_item=None): """ widget_list -- list of widgets focus_item -- widget or integer index, if None the first selectable widget will be chosen. widget_list may also contain tuples such as: ('flow', widget) always treat widget as a flow widget ('fixed', height, widget) give this box widget a fixed height ('weight', weight, widget) if the pile is treated as a box widget then treat widget as a box widget with a height based on its relative weight value, otherwise treat widget as a flow widget widgets not in a tuple are the same as ('weight', 1, widget) If the pile is treated as a box widget there must be at least one 'weight' tuple in widget_list. """ self.__super.__init__() self.widget_list = MonitoredList(widget_list) self.item_types = [] for i in range(len(widget_list)): w = widget_list[i] if type(w) != tuple: self.item_types.append(('weight',1)) elif w[0] == 'flow': f, widget = w self.widget_list[i] = widget self.item_types.append((f,None)) w = widget elif w[0] in ('fixed', 'weight'): f, height, widget = w self.widget_list[i] = widget self.item_types.append((f,height)) w = widget else: raise PileError, "widget list item invalid %r" % (w,) if focus_item is None and w.selectable(): focus_item = i self.widget_list.set_modified_callback(self._invalidate) if focus_item is None: focus_item = 0 if self.widget_list: self.set_focus(focus_item) else: self.focus_item=None self.pref_col = 0 @property def contents(self): for i, w in enumerate(self.widget_list): try: yield w, self.item_types[i] except IndexError: yield w, ('weight', 1) def _get_item_types(self, i): try: return self.item_types[i] except IndexError: return 'weight', 1 def selectable(self): """Return True if the focus item is selectable.""" return self.focus_item is not None and self.focus_item.selectable() def set_focus(self, item): """Set the item in focus. item -- widget or integer index""" if type(item) == int: assert item>=0 and item<len(self.widget_list) self.focus_item = self.widget_list[item] else: assert item in self.widget_list self.focus_item = item self._invalidate() def get_focus(self): """Return the widget in focus.""" return self.focus_item def get_pref_col(self, size): """Return the preferred column for the cursor, or None.""" if not self.selectable(): return None self._update_pref_col_from_focus(size) return self.pref_col def get_item_size(self, size, i, focus, item_rows=None): """ Return a size appropriate for passing to self.widget_list[i] """ maxcol = size[0] f, height = self._get_item_types(i) if f=='fixed': return (maxcol, height) elif f=='weight' and len(size)==2: if not item_rows: item_rows = self.get_item_rows(size, focus) return (maxcol, item_rows[i]) else: return (maxcol,) def get_item_rows(self, size, focus): """ Return a list of the number of rows used by each widget in self.item_list. """ remaining = None maxcol = size[0] if len(size)==2: remaining = size[1] l = [] if remaining is None: # pile is a flow widget for w, (f, height) in self.contents: if f == 'fixed': l.append(height) else: l.append(w.rows((maxcol,), focus=focus and self.focus_item == w)) return l # pile is a box widget # do an extra pass to calculate rows for each widget wtotal = 0 for w, (f, height) in self.contents: if f == 'flow': rows = w.rows((maxcol,), focus=focus and self.focus_item == w ) l.append(rows) remaining -= rows elif f == 'fixed': l.append(height) remaining -= height else: l.append(None) wtotal += height if wtotal == 0: raise PileError, "No weighted widgets found for Pile treated as a box widget" if remaining < 0: remaining = 0 for i, (w, (f, height)) in enumerate(self.contents): li = l[i] if li is None: rows = int(float(remaining)*height /wtotal+0.5) l[i] = rows remaining -= rows wtotal -= height return l def render(self, size, focus=False): """ Render all widgets in self.widget_list and return the results stacked one on top of the next. """ maxcol = size[0] item_rows = None combinelist = [] for i, (w, (f, height)) in enumerate(self.contents): item_focus = self.focus_item == w canv = None if f == 'fixed': canv = w.render( (maxcol, height), focus=focus and item_focus) elif f == 'flow' or len(size)==1: canv = w.render( (maxcol,), focus=focus and item_focus) else: if item_rows is None: item_rows = self.get_item_rows(size, focus) rows = item_rows[i] if rows>0: canv = w.render( (maxcol, rows), focus=focus and item_focus ) if canv: combinelist.append((canv, i, item_focus)) if not combinelist: return SolidCanvas(" ", size[0], (size[1:]+(0,))[0]) out = CanvasCombine(combinelist) if len(size)==2 and size[1] < out.rows(): # flow/fixed widgets rendered too large out = CompositeCanvas(out) out.pad_trim_top_bottom(0, size[1] - out.rows()) return out def get_cursor_coords(self, size): """Return the cursor coordinates of the focus widget.""" if not self.focus_item.selectable(): return None if not hasattr(self.focus_item,'get_cursor_coords'): return None i = self.widget_list.index(self.focus_item) f, height = self._get_item_types(i) item_rows = None maxcol = size[0] if f == 'fixed' or (f=='weight' and len(size)==2): if f == 'fixed': maxrow = height else: if item_rows is None: item_rows = self.get_item_rows(size, focus=True) maxrow = item_rows[i] coords = self.focus_item.get_cursor_coords( (maxcol,maxrow)) else: coords = self.focus_item.get_cursor_coords((maxcol,)) if coords is None: return None x,y = coords if i > 0: if item_rows is None: item_rows = self.get_item_rows(size, focus=True) for r in item_rows[:i]: y += r return x, y def rows(self, size, focus=False ): """Return the number of rows required for this widget.""" return sum(self.get_item_rows(size, focus)) def keypress(self, size, key ): """Pass the keypress to the widget in focus. Unhandled 'up' and 'down' keys may cause a focus change.""" item_rows = None if len(size)==2: item_rows = self.get_item_rows( size, focus=True ) i = self.widget_list.index(self.focus_item) f, height = self._get_item_types(i) if self.focus_item.selectable(): tsize = self.get_item_size(size,i,True,item_rows) key = self.focus_item.keypress( tsize, key ) if self._command_map[key] not in ('cursor up', 'cursor down'): return key if self._command_map[key] == 'cursor up': candidates = range(i-1, -1, -1) # count backwards to 0 else: # self._command_map[key] == 'cursor down' candidates = range(i+1, len(self.widget_list)) if not item_rows: item_rows = self.get_item_rows( size, focus=True ) for j in candidates: if not self.widget_list[j].selectable(): continue self._update_pref_col_from_focus(size) self.set_focus(j) if not hasattr(self.focus_item,'move_cursor_to_coords'): return f, height = self._get_item_types(i) rows = item_rows[j] if self._command_map[key] == 'cursor up': rowlist = range(rows-1, -1, -1) else: # self._command_map[key] == 'cursor down' rowlist = range(rows) for row in rowlist: tsize=self.get_item_size(size,j,True,item_rows) if self.focus_item.move_cursor_to_coords( tsize,self.pref_col,row): break return # nothing to select return key def _update_pref_col_from_focus(self, size ): """Update self.pref_col from the focus widget.""" widget = self.focus_item if not hasattr(widget,'get_pref_col'): return i = self.widget_list.index(widget) tsize = self.get_item_size(size,i,True) pref_col = widget.get_pref_col(tsize) if pref_col is not None: self.pref_col = pref_col def move_cursor_to_coords(self, size, col, row): """Capture pref col and set new focus.""" self.pref_col = col #FIXME guessing focus==True focus=True wrow = 0 item_rows = self.get_item_rows(size,focus) for r,w in zip(item_rows, self.widget_list): if wrow+r > row: break wrow += r if not w.selectable(): return False if hasattr(w,'move_cursor_to_coords'): i = self.widget_list.index(w) tsize = self.get_item_size(size, i, focus, item_rows) rval = w.move_cursor_to_coords(tsize,col,row-wrow) if rval is False: return False self.set_focus(w) return True def mouse_event(self, size, event, button, col, row, focus): """ Pass the event to the contained widget. May change focus on button 1 press. """ wrow = 0 item_rows = self.get_item_rows(size,focus) for r,w in zip(item_rows, self.widget_list): if wrow+r > row: break wrow += r focus = focus and self.focus_item == w if is_mouse_press(event) and button==1: if w.selectable(): self.set_focus(w) if not hasattr(w,'mouse_event'): return False i = self.widget_list.index(w) tsize = self.get_item_size(size, i, focus, item_rows) return w.mouse_event(tsize, event, button, col, row-wrow, focus)
def __init__(self, widget_list): self.__super.__init__() self._widget_list = MonitoredList([]) self._set_widget_list(widget_list) self._widget_list.set_modified_callback(self._invalidate)
class Columns(Widget): # either FlowWidget or BoxWidget def __init__(self, widget_list, dividechars=0, focus_column=None, min_width=1, box_columns=None): """ widget_list -- list of flow widgets or list of box widgets dividechars -- blank characters between columns focus_column -- index into widget_list of column in focus, if None the first selectable widget will be chosen. min_width -- minimum width for each column which is not designated as flow widget in widget_list. box_columns -- a list of column indexes containing box widgets whose maxrow is set to the maximum of the rows required by columns not listed in box_columns. widget_list may also contain tuples such as: ('flow', widget) always treat widget as a flow widget ('fixed', width, widget) give this column a fixed width ('weight', weight, widget) give this column a relative weight widgets not in a tuple are the same as ('weight', 1, widget) box_columns is ignored when this widget is being used as a box widget because in that case all columns are treated as box widgets. """ self.__super.__init__() self.widget_list = MonitoredList(widget_list) self.column_types = [] self.has_flow_type = False for i in range(len(widget_list)): w = widget_list[i] if type(w) != tuple: self.column_types.append(('weight',1)) elif w[0] == 'flow': f, widget = w self.widget_list[i] = widget self.column_types.append((f,None)) self.has_flow_type = True w = widget elif w[0] in ('fixed', 'weight'): f,width,widget = w self.widget_list[i] = widget self.column_types.append((f,width)) w = widget else: raise ColumnsError, "widget list item invalid: %r" % (w,) if focus_column is None and w.selectable(): focus_column = i self.widget_list.set_modified_callback(self._invalidate) self.dividechars = dividechars if focus_column is None: focus_column = 0 self.focus_col = focus_column self.pref_col = None self.min_width = min_width self.box_columns = box_columns self._cache_maxcol = None @property def contents(self): for i, w in enumerate(self.widget_list): try: yield w, self.column_types[i] except IndexError: yield w, ('weight', 1) def _invalidate(self): self._cache_maxcol = None self.__super._invalidate() def set_focus_column( self, num ): """Set the column in focus by its index in self.widget_list.""" self.focus_col = num self._invalidate() def get_focus_column( self ): """Return the focus column index.""" return self.focus_col def set_focus(self, item): """Set the item in focus. item -- widget or integer index""" if type(item) == int: assert item>=0 and item<len(self.widget_list) position = item else: position = self.widget_list.index(item) self.focus_col = position self._invalidate() def get_focus(self): """Return the widget in focus.""" return self.widget_list[self.focus_col] def column_widths(self, size, focus=False): """Return a list of column widths. size -- (maxcol,) if self.widget_list contains flow widgets or (maxcol, maxrow) if it contains box widgets. """ maxcol = size[0] if maxcol == self._cache_maxcol and not self.has_flow_type: return self._cache_column_widths widths=[] weighted = [] shared = maxcol + self.dividechars for i, (w, (t, width)) in enumerate(self.contents): if t == 'fixed': static_w = width elif t == 'flow': static_w = w.pack((maxcol,), focus)[0] else: static_w = self.min_width if shared < static_w + self.dividechars: break widths.append(static_w) shared -= static_w + self.dividechars if t not in ('fixed', 'flow'): weighted.append((width,i)) if shared: # divide up the remaining space between weighted cols weighted.sort() wtotal = sum([weight for weight,i in weighted]) grow = shared + len(weighted)*self.min_width for weight, i in weighted: width = int(float(grow) * weight / wtotal + 0.5) width = max(self.min_width, width) widths[i] = width grow -= width wtotal -= weight self._cache_maxcol = maxcol self._cache_column_widths = widths return widths def render(self, size, focus=False): """Render columns and return canvas. size -- (maxcol,) if self.widget_list contains flow widgets or (maxcol, maxrow) if it contains box widgets. """ widths = self.column_widths(size, focus) if not widths: return SolidCanvas(" ", size[0], (size[1:]+(1,))[0]) box_maxrow = None if len(size)==1 and self.box_columns: box_maxrow = 1 # two-pass mode to determine maxrow for box columns for i in range(len(widths)): if i in self.box_columns: continue mc = widths[i] w = self.widget_list[i] rows = w.rows( (mc,), focus = focus and self.focus_col == i ) box_maxrow = max(box_maxrow, rows) l = [] for i in range(len(widths)): mc = widths[i] # if the widget has a width of 0, hide it if mc <= 0: continue w = self.widget_list[i] if box_maxrow and i in self.box_columns: sub_size = (mc, box_maxrow) else: sub_size = (mc,) + size[1:] canv = w.render(sub_size, focus = focus and self.focus_col == i) if i < len(widths)-1: mc += self.dividechars l.append((canv, i, self.focus_col == i, mc)) canv = CanvasJoin(l) if canv.cols() < size[0]: canv.pad_trim_left_right(0, size[0]-canv.cols()) return canv def get_cursor_coords(self, size): """Return the cursor coordinates from the focus widget.""" w = self.widget_list[self.focus_col] if not w.selectable(): return None if not hasattr(w, 'get_cursor_coords'): return None widths = self.column_widths( size ) if len(widths) < self.focus_col+1: return None colw = widths[self.focus_col] coords = w.get_cursor_coords( (colw,)+size[1:] ) if coords is None: return None x,y = coords x += self.focus_col * self.dividechars x += sum( widths[:self.focus_col] ) return x, y def move_cursor_to_coords(self, size, col, row): """Choose a selectable column to focus based on the coords.""" widths = self.column_widths(size) best = None x = 0 for i in range(len(widths)): w = self.widget_list[i] end = x + widths[i] if w.selectable(): # sometimes, col == 'left' - that doesn't seem like its handled here, does it? # assert isinstance(x, int) and isinstance(col, int), (x, col) if x > col and best is None: # no other choice best = i, x, end break if x > col and col-best[2] < x-col: # choose one on left break best = i, x, end if col < end: # choose this one break x = end + self.dividechars if best is None: return False i, x, end = best w = self.widget_list[i] if hasattr(w,'move_cursor_to_coords'): if type(col)==int: move_x = min(max(0,col-x),end-x-1) else: move_x = col rval = w.move_cursor_to_coords((end-x,)+size[1:], move_x, row) if rval is False: return False self.focus_col = i self.pref_col = col self._invalidate() return True def mouse_event(self, size, event, button, col, row, focus): """ Send event to appropriate column. May change focus on button 1 press. """ widths = self.column_widths(size) x = 0 for i in range(len(widths)): if col < x: return False w = self.widget_list[i] end = x + widths[i] if col >= end: x = end + self.dividechars continue focus = focus and self.focus_col == i if is_mouse_press(event) and button == 1: if w.selectable(): self.set_focus(w) if not hasattr(w,'mouse_event'): return False return w.mouse_event((end-x,)+size[1:], event, button, col - x, row, focus) return False def get_pref_col(self, size): """Return the pref col from the column in focus.""" maxcol = size[0] widths = self.column_widths( (maxcol,) ) w = self.widget_list[self.focus_col] if len(widths) < self.focus_col+1: return 0 col = None if hasattr(w,'get_pref_col'): col = w.get_pref_col((widths[self.focus_col],)+size[1:]) if type(col)==int: col += self.focus_col * self.dividechars col += sum( widths[:self.focus_col] ) if col is None: col = self.pref_col if col is None and w.selectable(): col = widths[self.focus_col] // 2 col += self.focus_col * self.dividechars col += sum( widths[:self.focus_col] ) return col def rows(self, size, focus=0 ): """Return the number of rows required by the columns. Only makes sense if self.widget_list contains flow widgets.""" widths = self.column_widths(size, focus) rows = 1 for i in range(len(widths)): if self.box_columns and i in self.box_columns: continue mc = widths[i] w = self.widget_list[i] rows = max( rows, w.rows( (mc,), focus = focus and self.focus_col == i ) ) return rows def keypress(self, size, key): """Pass keypress to the focus column. size -- (maxcol,) if self.widget_list contains flow widgets or (maxcol, maxrow) if it contains box widgets. """ if self.focus_col is None: return key widths = self.column_widths( size ) if self.focus_col < 0 or self.focus_col >= len(widths): return key i = self.focus_col mc = widths[i] w = self.widget_list[i] if self._command_map[key] not in ('cursor up', 'cursor down', 'cursor page up', 'cursor page down'): self.pref_col = None key = w.keypress( (mc,)+size[1:], key ) if self._command_map[key] not in ('cursor left', 'cursor right'): return key if self._command_map[key] == 'cursor left': candidates = range(i-1, -1, -1) # count backwards to 0 else: # key == 'right' candidates = range(i+1, len(widths)) for j in candidates: if not self.widget_list[j].selectable(): continue self.set_focus_column( j ) return return key def selectable(self): """Return the selectable value of the focus column.""" return self.widget_list[self.focus_col].selectable()