diff urwid_satext/sat_widgets.py @ 107:ed2675f92f7c

menus management improvment
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:08:35 +0100
parents b2fee87c1d5a
children 5bb3b7e25bf6
line wrap: on
line diff
--- a/urwid_satext/sat_widgets.py	Fri Sep 19 18:23:33 2014 +0200
+++ b/urwid_satext/sat_widgets.py	Wed Dec 10 19:08:35 2014 +0100
@@ -22,6 +22,10 @@
 import encodings
 utf8decode = lambda s: encodings.codecs.utf_8_decode(s)[0]
 
+import uuid
+
+import collections
+
 from urwid.util import is_mouse_press #XXX: is_mouse_press is not included in urwid in 1.0.0
 from .keys import action_key_map as a_key
 
@@ -215,8 +219,8 @@
         @param selected: is the text selected ?
         """
         self.focus_attr = focus_attr
-        self.__selected = False
-        self.__was_focused = False
+        self._selected = False
+        self._was_focused = False
         self.header = header
         self.text = text
         urwid.WidgetWrap.__init__(self, urwid.Text("",align=align))
@@ -242,7 +246,7 @@
     def set_text(self, text):
         """/!\ set_text doesn't change self.selected_txt !"""
         self.text = text
-        self.setState(self.__selected,invisible=True)
+        self.setState(self._selected,invisible=True)
 
     def setSelectedText(self, text=None):
         """Text to display when selected
@@ -250,12 +254,12 @@
         if text == None:
             text = ('selected',self.getValue())
         self.selected_txt = text
-        if self.__selected:
-            self.setState(self.__selected)
+        if self._selected:
+            self.setState(self._selected)
 
-    def __set_txt(self):
+    def _set_txt(self):
         txt_list = [self.header]
-        txt = self.selected_txt if self.__selected else self.text
+        txt = self.selected_txt if self._selected else self.text
         if isinstance(txt,list):
             txt_list.extend(txt)
         else:
@@ -265,31 +269,33 @@
 
     def setState(self, selected, invisible=False):
         """Change state
+
         @param selected: boolean state value
-        @param invisible: don't emit change signal if True"""
-        assert(type(selected)==bool)
-        self.__selected=selected
-        self.__set_txt()
-        self.__was_focused = False
+        @param invisible: don't emit change signal if True
+        """
+        assert type(selected)==bool
+        self._selected=selected
+        self._set_txt()
+        self._was_focused = False
         self._invalidate()
         if not invisible:
-            self._emit("change", self.__selected)
+            self._emit("change", self._selected)
 
     def getState(self):
-        return self.__selected
+        return self._selected
 
     def selectable(self):
         return True
 
     def keypress(self, size, key):
         if key in (a_key['TEXT_SELECT'], a_key['TEXT_SELECT2']):
-            self.setState(not self.__selected)
+            self.setState(not self._selected)
         else:
             return key
 
     def mouse_event(self, size, event, button, x, y, focus):
         if is_mouse_press(event) and button == 1:
-            self.setState(not self.__selected)
+            self.setState(not self._selected)
             return True
 
         return False
@@ -297,11 +303,11 @@
     def render(self, size, focus=False):
         attr_list = self._w.base_widget._attrib
         if not focus:
-            if self.__was_focused:
-                self.__set_txt()
-                self.__was_focused = False
+            if self._was_focused:
+                self._set_txt()
+                self._was_focused = False
         else:
-            if not self.__was_focused:
+            if not self._was_focused:
                 if not attr_list:
                     attr_list.append((self.focus_attr,len(self._w.base_widget.text)))
                 else:
@@ -315,7 +321,7 @@
                                 attr+="_focus"
                                 attr_list[idx] = (attr,attr_len)
                 self._w.base_widget._invalidate()
-                self.__was_focused = True #bloody ugly hack :)
+                self._was_focused = True #bloody ugly hack :)
         return self._w.render(size, focus)
 
 
@@ -337,11 +343,11 @@
 
 class CustomButton(ClickableText):
 
-    def __init__(self, label, on_press=None, user_data=None, left_border = "[ ", right_border = " ]"):
+    def __init__(self, label, on_press=None, user_data=None, left_border="[ ", right_border=" ]", align="left"):
         self.label = label
         self.left_border = left_border
         self.right_border = right_border
-        super(CustomButton, self).__init__([left_border, label, right_border])
+        super(CustomButton, self).__init__([left_border, label, right_border], align=align)
         self.size = len(self.get_text())
         if on_press:
             urwid.connect_signal(self, 'click', on_press, user_data)
@@ -359,13 +365,14 @@
 
 
 class ListOption(unicode):
-    """ Class similar to unicode, but which make the difference between value and label
+    """Unicode which manage label and value
+
+    This 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 against value, not the label which is the one displayed
-
     """
 
     def __new__(cls, option):
@@ -446,12 +453,12 @@
         return super(UnselectableListBox, self).render(size, focus)
 
 
-class GenericList(urwid.WidgetWrap):
+class GenericList(urwid.ListBox):
     signals = ['click','change']
 
     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
+        """Widget managing list of string and their selection
+
         @param options: list of strings used for options
         @param style: list of string:
             - 'single' if only one must be selected
@@ -477,11 +484,10 @@
             urwid.connect_signal(self, 'change', on_change, user_data)
 
         self.content = urwid.SimpleListWalker([])
-        self.list_box = urwid.ListBox(self.content)
-        urwid.WidgetWrap.__init__(self, self.list_box)
+        super(GenericList, self).__init__(self.content)
         self.changeValues(options)
 
-    def __onStateChange(self, widget, selected):
+    def _onStateChange(self, widget, selected):
         if self.single:
             if not selected and not self.can_select_none:
                 #if in single mode, it's forbidden to unselect a value
@@ -492,7 +498,7 @@
                 widget.setState(True, invisible=True)
         self._emit("change")
 
-    def __onClick(self, widget):
+    def _onClick(self, widget):
         self._emit("click", widget)
 
     def unselectAll(self, invisible=False):
@@ -527,9 +533,6 @@
                 result.append(widget.getValue())
         return result
 
-    def getDisplayWidget(self):
-        return self.list_box
-
     def changeValues(self, new_values):
         """Change all values in one shot"""
         new_values = ListOption.fromOptions(new_values)
@@ -541,7 +544,7 @@
             if not self.first_display and option in old_selected:
                 widget.setState(True)
             widgets.append(widget)
-            for signal, callback in (('change', self.__onStateChange), ('click', self.__onClick)):
+            for signal, callback in (('change', self._onStateChange), ('click', self._onClick)):
                 try:
                     urwid.connect_signal(widget, signal, callback)
                 except NameError:
@@ -549,8 +552,6 @@
         self.content[:] = widgets
         if self.first_display and self.single and new_values and not self.no_first_select:
             self.content[0].setState(True)
-        display_widget = self.getDisplayWidget()
-        self._set_w(display_widget)
         self._emit('change')
         self.first_display = False
 
@@ -558,8 +559,10 @@
         """Select the first item which has the given value.
 
         @param value
-        @param move_focus (boolean): True to move the focus on the selected value,
-                                     False to leave the focus position unchanged.
+        @param move_focus (bool):
+            - True to move the focus on the selected value,
+            - False to leave the focus position unchanged.
+
         """
         self.unselectAll()
         idx = 0
@@ -567,7 +570,7 @@
             if widget.getValue() == value:
                 widget.setState(True)
                 if move_focus:
-                    self.list_box.focus_position = idx
+                    self.focus_position = idx
                 return
             idx+=1
 
@@ -589,7 +592,7 @@
                 if widget.getValue() == value:
                     widget.setState(True)
                     if move_focus:
-                        self.list_box.focus_position = idx
+                        self.focus_position = idx
                 idx += 1
 
 
@@ -868,75 +871,122 @@
         self.__buildOverlay(button.get_label(),
                             self.x_orig + self._w.base_widget.getStartCol(button))
 
+MenuItem = collections.namedtuple('MenuItem', ('name', 'widget'))
 
 class MenuRoller(urwid.WidgetWrap):
 
-    def __init__(self,menus_list):
+    def __init__(self, menus_list):
         """Create a MenuRoller
-        @param menus_list: list of tuple with (name, Menu_instance), name can be None
+
+        @param menus_list: list of tuples which can be either:
+            - (name, Menu instance)
+            - (name, Menu instance, id)
         """
-        assert (menus_list)
-        self.selected = 0
-        self.name_list = []
-        self.menus = {}
+        assert menus_list
+        self.selected = None
+        self.menu_items = collections.OrderedDict()
 
         self.columns = urwid.Columns([urwid.Text(''),urwid.Text('')])
         urwid.WidgetWrap.__init__(self, self.columns)
 
         for menu_tuple in menus_list:
-            name,menu = menu_tuple
-            self.addMenu(name, menu)
+            try:
+                name, menu, id_ = menu_tuple
+            except ValueError:
+                name, menu = menu_tuple
+                id_ = None
+            self.addMenu(name, menu, id_)
 
     def _showSelected(self):
         """show menu selected"""
-        name_txt = u'\u21c9 '+self.name_list[self.selected]+u' \u21c7 '
-        current_name = ClickableText(name_txt)
-        name_len = len(name_txt)
-        current_menu = self.menus[self.name_list[self.selected]]
-        current_menu.setOrigX(name_len)
-        self.columns.contents[0] = (current_name, ('given', name_len, False))
-        self.columns.contents[1] = (current_menu, ('weight', 1, False))
+        if self.selected is None:
+            self.columns.contents[0] = (urwid.Text(''), ('given', 0, False))
+            self.columns.contents[1] = (urwid.Text(''), ('weight', 1, False))
+        else:
+            menu_item = self.menu_items[self.selected]
+            name_txt = u'\u21c9 ' + menu_item.name + u' \u21c7 '
+            current_name = ClickableText(name_txt)
+            name_len = len(name_txt)
+            current_menu = menu_item.widget
+            current_menu.setOrigX(name_len)
+            self.columns.contents[0] = (current_name, ('given', name_len, False))
+            self.columns.contents[1] = (current_menu, ('weight', 1, False))
 
     def keypress(self, size, key):
+        menu_ids = self.menu_items.keys()
+        try:
+            idx = menu_ids.index(self.selected)
+        except ValueError:
+            return super(MenuRoller, self).keypress(size, key)
+
         if key==a_key['MENU_ROLLER_UP']:
             if self.columns.get_focus_column()==0:
-                if self.selected > 0:
-                    self.selected -= 1
+                if idx > 0:
+                    self.selected = menu_ids[idx-1]
                     self._showSelected()
                 return
         elif key==a_key['MENU_ROLLER_DOWN']:
             if self.columns.get_focus_column()==0:
-                if self.selected < len(self.name_list)-1:
-                    self.selected += 1
+                if idx < len(menu_ids)-1:
+                    self.selected = menu_ids[idx+1]
                     self._showSelected()
                 return
         elif key==a_key['MENU_ROLLER_RIGHT']:
             if self.columns.get_focus_column()==0 and \
                 (isinstance(self.columns.contents[1][0], urwid.Text) or \
-                self.menus[self.name_list[self.selected]].getMenuSize()==0):
+                self.menu_items[self.selected].widget.getMenuSize()==0):
                 return #if we have no menu or the menu is empty, we don't go the right column
 
         return super(MenuRoller, self).keypress(size, key)
 
-    def addMenu(self, name_param, menu):
-        name = name_param or ''
-        if name not in self.name_list:
-            self.name_list.append(name)
-        self.menus[name] = menu
-        if self.name_list[self.selected] == name:
+    def addMenu(self, name, widget, menu_id=None):
+        """Add a menu
+
+        @param name: name of the menu to add, it name already exists, menu is not added
+        @param widget: instance of Menu
+        @param menu_id: id to use of this menu, or None to generate
+        @return: menu_id
+        """
+        names = {menu_item.name: id_ for id_, menu_item in self.menu_items.iteritems()}
+
+        if name not in names:
+            id_ = menu_id or str(uuid.uuid4())
+            if id_ in self.menu_items:
+                raise ValueError('Conflict: the id [{}] is already used'.format(id_))
+            self.menu_items[id_] = MenuItem(name, widget)
+        else:
+            id_ = names[name]
+            menu_item = self.menu_items[id_]
+            if menu_item.widget is not widget:
+                raise ValueError("The menu with id [{}] exists and doesn't contain the given instance. Use replaceMenu if you want to change the menu.".format(id_))
+        if self.selected is None:
+            self.selected = id_
+            self._showSelected()
+        return id_
+
+    def replaceMenu(self, name, widget, menu_id):
+        """Add a menu or replace it if the id already exists
+
+        @param name: name of the menu to add, it name already exists, menu is not added
+        @param widget: instance of Menu
+        @param menu_id: id or the menu
+        """
+        assert menu_id is not None
+        if menu_id in self.menu_items:
+            del self.menu_items[menu_id]
+        self.addMenu(name, widget, menu_id)
+        if self.selected == menu_id:
             self._showSelected() #if we are on the menu, we update it
 
-    def removeMenu(self, name):
-        if name in self.name_list:
-            self.name_list.remove(name)
-        if name in self.menus.keys():
-            del self.menus[name]
-        self.selected = 0
-        self._showSelected()
+    def removeMenu(self, menu_id):
+        del self.menu_items[menu_id]
+        if self.selected == menu_id:
+            self.selected = self.menu_items.items[0] if self.menu_items else None
+            self._showSelected()
 
     def checkShortcuts(self, key):
-        for menu in self.name_list:
-            key = self.menus[menu].checkShortcuts(key)
+        for menu_item in self.menu_items.values():
+            key = menu_item.widget.checkShortcuts(key)
         return key