# HG changeset patch # User Goffi # Date 1391090442 -3600 # Node ID 24d49f1d735fdad6c8e76211cb5396d87403736b # Parent b39c81cdd86304fae6e6ada8ea5e29b2dc2d266d added TableContainer + some bug fixes (bad default parameters) diff -r b39c81cdd863 -r 24d49f1d735f urwid_satext/sat_widgets.py --- a/urwid_satext/sat_widgets.py Tue Dec 24 15:09:21 2013 +0100 +++ b/urwid_satext/sat_widgets.py Thu Jan 30 15:00:42 2014 +0100 @@ -85,6 +85,7 @@ pass return super(AdvancedEdit, self).keypress(size, key) + class Password(AdvancedEdit): """Edit box which doesn't show what is entered (show '*' or other char instead)""" @@ -111,6 +112,7 @@ def render(self, size, focus=False): return super(Password, self).render(size, focus) + class ModalEdit(AdvancedEdit): """AdvancedEdit with vi-like mode management - there is a new 'mode' property which can be changed with properties @@ -160,6 +162,7 @@ return return super(ModalEdit, self).keypress(size, key) + class SurroundedText(urwid.Widget): """Text centered on a repeated character (like a Divider, but with a text in the center)""" _sizing = frozenset(['flow']) @@ -180,6 +183,7 @@ render_text = middle * self.car + self.text + (maxcol - len(self.text) - middle) * self.car return urwid.Text(render_text) + class SelectableText(urwid.WidgetWrap): """Text which can be selected with space""" signals = ['change'] @@ -294,6 +298,7 @@ self.__was_focused = True #bloody ugly hack :) return self._w.render(size, focus) + class ClickableText(SelectableText): signals = SelectableText.signals + ['click'] @@ -302,6 +307,7 @@ if not invisible: self._emit('click') + class CustomButton(ClickableText): def __init__(self, label, on_press=None, user_data=None, left_border = "[ ", right_border = " ]"): @@ -324,13 +330,14 @@ self.label = label self.set_text([self.left_border, label, self.right_border]) + class ListOption(unicode): """ Class similar to unicode, but which make the difference between value and label label is show when use as unicode, the .value attribute contain the actual value Can be initialised with: - basestring (label = value = given string) - a tuple with (value, label) - XXX: comparaison is made again value, not the label which is the one displayed + XXX: comparaison is made against value, not the label which is the one displayed """ @@ -343,8 +350,6 @@ value, label = option else: raise NotImplementedError - if not value: - raise ValueError("value can't be empty") if not label: label = value instance = super(ListOption, cls).__new__(cls, label) @@ -387,7 +392,7 @@ class GenericList(urwid.WidgetWrap): signals = ['click','change'] - def __init__(self, options, style=[], align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): + def __init__(self, options, style=None, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): """ Widget managing list of string and their selection @param options: list of strings used for options @@ -399,6 +404,8 @@ @param on_click: method called when click signal is emited @param user_data: data sent to the callback for click signal """ + if style is None: + style = [] self.single = 'single' in style self.no_first_select = 'no_first_select' in style self.can_select_none = 'can_select_none' in style @@ -501,15 +508,26 @@ return idx+=1 + class List(urwid.Widget): """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'""" signals = ['click','change'] _sizing = frozenset(['flow']) - def __init__(self, options, style=[], max_height=5, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): + def __init__(self, options, style=None, max_height=5, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): + if style is None: + style = [] self.genericList = GenericList(options, style, align, option_type, on_click, on_change, user_data) + urwid.connect_signal(self.genericList, 'change', self._onChange) + urwid.connect_signal(self.genericList, 'click', self._onClick) self.max_height = max_height + def _onChange(self, widget): + self._emit('change') + + def _onClick(self, widget): + self._emit('click') + def selectable(self): return True @@ -548,6 +566,7 @@ height = min(list_size,self.max_height) or 1 return urwid.BoxAdapter(self.genericList, height) + ## MISC ## class NotificationBar(urwid.WidgetWrap): @@ -664,6 +683,7 @@ self.selected = wid.getValue() self._emit('click') + class Menu(urwid.WidgetWrap): def __init__(self,loop, x_orig=0): @@ -690,10 +710,10 @@ def setOrigX(self, orig_x): self.x_orig = orig_x - def __buildOverlay(self,menu_key,columns): + def __buildOverlay(self, menu_key, columns): """Build the overlay menu which show menuitems @param menu_key: name of the category - @colums: column number where the menubox must be displayed""" + @param columns: column number where the menubox must be displayed""" max_len = 0 for item in self.menu[menu_key]: if len(item[0]) > max_len: @@ -827,7 +847,9 @@ class GenericDialog(urwid.WidgetWrap): - def __init__(self, widgets_lst, title, style=[], **kwargs): + def __init__(self, widgets_lst, title, style=None, **kwargs): + if style is None: + style = [] frame_header = urwid.AttrMap(urwid.Text(title,'center'),'title') buttons = None @@ -854,28 +876,34 @@ urwid.WidgetWrap.__init__(self, decorated_frame) - class InputDialog(GenericDialog): """Dialog with an edit box""" - def __init__(self, title, instrucions, style=['OK/CANCEL'], default_txt = '', **kwargs): + def __init__(self, title, instrucions, style=None, default_txt = '', **kwargs): + if style is None: + style = ['OK/CANCEL'] instr_wid = urwid.Text(instrucions+':') edit_box = AdvancedEdit(edit_text=default_txt) GenericDialog.__init__(self, [instr_wid,edit_box], title, style, ok_value=edit_box, **kwargs) self._w.base_widget.set_focus('body') + class ConfirmDialog(GenericDialog): """Dialog with buttons for confirm or cancel an action""" - def __init__(self, title, style=['YES/NO'], **kwargs): + def __init__(self, title, style=None, **kwargs): + if style is None: + style = ['YES/NO'] GenericDialog.__init__(self, [], title, style, **kwargs) + class Alert(GenericDialog): """Dialog with just a message and a OK button""" def __init__(self, title, message, style=['OK'], **kwargs): GenericDialog.__init__(self, [urwid.Text(message, 'center')], title, style, ok_value=None, **kwargs) + ## CONTAINERS ## class ColumnsRoller(urwid.Widget): @@ -1032,6 +1060,7 @@ return ret + class TabsContainer(urwid.WidgetWrap): signals = ['click'] @@ -1078,11 +1107,13 @@ self._buttons_cont.set_focus(0) self.__buttonClicked(button,True) - def addTab(self,name,content=[]): + def addTab(self,name,content=None): """Add a page to the container @param name: name of the page (what appear on the tab) @param content: content of the page @return: ListBox (content of the page)""" + if content is None: + content = [] listbox = urwid.ListBox(urwid.SimpleListWalker(content)) self.tabs.append([name,listbox]) self.__appendButton(name) @@ -1094,6 +1125,169 @@ self._w.footer = widget +class HighlightColumns(urwid.AttrMap): + """ Decorated columns which highlight all or some columns """ + + def __init__(self, highlight_cols, highlight_attr, *args, **kwargs): + """ Create the HighlightColumns + @param highlight_cols: tuple of columns to highlight, () to highlight to whole row + @param highlight_attr: name of the attribute to use when focused + other parameter are passed to urwid Columns + + """ + columns = urwid.Columns(*args, **kwargs) + self.highlight_cols = highlight_cols + self.highlight_attr = highlight_attr + self.has_focus = False + if highlight_cols == (): + super(HighlightColumns, self).__init__(columns, None, highlight_attr) + self.highlight_cols = None + else: + super(HighlightColumns, self).__init__(columns, None) + + @property + def options(self): + return self.base_widget.options + + @property + def contents(self): + return self.base_widget.contents + + @property + def focus_position(self): + return self.base_widget.focus_position + + @focus_position.setter + def focus_position(self, value): + self.base_widget.focus_position = value + + def addWidget(self, wid, options): + """ Add a widget to the columns + Widget is wrapped with AttrMap, that's why Columns.contents should not be used directly for appending new widgets + @param wid: widget to add + @param options: result of Columns.options(...) + + """ + wrapper = urwid.AttrMap(wid, None) + self.base_widget.contents.append((wrapper, options)) + + + def render(self, size, focus=False): + if self.highlight_cols and focus != self.has_focus: + self.has_focus = focus + for idx in self.highlight_cols: + wid = self.base_widget.contents[idx][0] + wid.set_attr_map({None: self.highlight_attr if focus else None}) + + return super(HighlightColumns, self).render(size, focus) + + +class TableContainer(urwid.WidgetWrap): + """ Widgets are disposed in row and columns """ + signals = ['click'] + + def __init__(self, items=None, columns=None, dividechars=1, row_selectable=False, select_key='enter', options=None): + """ Create a TableContainer + @param items: iterable of widgets to add to this container + @param columns: nb of columns of this table + @param dividechars: same as dividechars param for urwid.Columns + @param row_selectable: if True, row are always selectable, even if they don't contain any selectable widget + @param options: dictionnary with the following keys: + - ADAPT: tuple of columns for which the size must be adapted to its contents, + empty tuple for all columns + - HIGHLIGHT: tuple of columns which must be higlighted on focus, + empty tuple for the whole row + - FOCUS_ATTR: Attribute name to use when focused (see HIGHLIGHT). Default is "table_selected" + + """ + pile = urwid.Pile([]) + super(TableContainer, self).__init__(pile) + if items is None: + items = [] + if columns is None: # if columns is None, we suppose only one row is given in items + columns = len(items) + assert columns + self._columns = columns + self._row_selectable = row_selectable + self.select_key = select_key + if options is None: + options = {} + for opt in ['ADAPT', 'HIGHLIGHT']: + if opt in options: + try: + options[opt] = tuple(options[opt]) + except TypeError: + warning('[%s] option is not a tuple' % opt) + options[opt] = () + self._options = options + self._dividechars = dividechars + self._idx = 0 + self._longuest = self._columns * [0] + self._next_row_idx = None + for item in items: + self.addWidget(item) + + def _getIdealSize(self, widget): + """ return preferred size for widget, or 0 if we can't find it """ + try: + return len(widget.text) + except AttributeError: + return 0 + + def keypress(self, size, key): + if key == self.select_key and self._row_selectable: + self._emit('click') + else: + return super(TableContainer, self).keypress(size, key) + + + def addWidget(self, widget): + # TODO: use a contents property ? + pile = self._w + col_idx = self._idx % self._columns + + options = None + + if col_idx == 0: + # we have a new row + columns = HighlightColumns(self._options.get('HIGHLIGHT'), self._options.get('FOCUS_ATTR', 'table_selected'), [], dividechars=self._dividechars) + columns.row_idx = self._next_row_idx + pile.contents.append((columns, pile.options())) + else: + columns = pile.contents[-1][0] + + if 'ADAPT' in self._options and (col_idx in self._options['ADAPT'] + or self._options['ADAPT'] == ()): + current_len = self._getIdealSize(widget) + longuest = self._longuest[col_idx] + max_len = max(longuest, current_len) + if max_len > longuest: + self._longuest[col_idx] = max_len + for wid,_ in pile.contents[:-1]: + col = wid.base_widget + col.contents[col_idx] = (col.contents[col_idx][0], col.options('given', max_len)) + options = columns.options('given', max_len) if max_len else columns.options() + + columns.addWidget(widget, options or columns.options()) + + if self._row_selectable and col_idx == self._columns - 1: + columns.addWidget(urwid.SelectableIcon(''), columns.options('given', 0)) + + if not columns.selectable() and columns.contents[-1][0].base_widget.selectable(): + columns.focus_position = len(columns.contents)-1 + self._idx += 1 + + def setRowIndex(self, idx): + self._next_row_idx = idx + + def getSelectedWidgets(self): + columns = self._w.focus + return (wid for wid, _ in columns.contents) + + def getSelectedIndex(self): + columns = self._w.focus + return columns.row_idx + ## DECORATORS ## class LabelLine(urwid.LineBox): """Like LineBox, but with a Label centered in the top line""" @@ -1103,6 +1297,7 @@ top_columns = self._w.widget_list[0] top_columns.widget_list[1] = label_widget + class VerticalSeparator(urwid.WidgetDecoration, urwid.WidgetWrap): def __init__(self, original_widget, left_char = u"│", right_char = ''): """Draw a separator on left and/or right of original_widget."""