diff urwid_satext/sat_widgets.py @ 70:24d49f1d735f

added TableContainer + some bug fixes (bad default parameters)
author Goffi <goffi@goffi.org>
date Thu, 30 Jan 2014 15:00:42 +0100
parents b39c81cdd863
children eddb8369ba06
line wrap: on
line diff
--- 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."""