diff frontends/primitivus/custom_widgets.py @ 192:879beacb8e16

Primitivus: major changes in SelectableText, menu can now be used with mouse, TabsContainer show wich tab is selected - SelectableText is a WidgetWrap of urwid.Text, and manage attributes and complexe combination like urwid.Text (list of tuples (attribute,text)) - Menu now use a ColumnsRoller, and can't anymore be displayed on 2 rows, avoiding potential bug when display MenuBox - Mouse can be used in menu. Right click on MenuBox make it disappear - Tab container now display which tab is selected by putting a 'title' attribute on the corresponding button /!\ as SelectableText is heavily used, there can be regressions
author Goffi <goffi@goffi.org>
date Thu, 19 Aug 2010 21:11:22 +0800
parents 33e618d385cf
children 92e4ddd580ae
line wrap: on
line diff
--- a/frontends/primitivus/custom_widgets.py	Wed Aug 18 22:34:35 2010 +0800
+++ b/frontends/primitivus/custom_widgets.py	Thu Aug 19 21:11:22 2010 +0800
@@ -21,6 +21,7 @@
 
 import urwid
 from urwid.escape import utf8decode
+from logging import debug, info, warning, error
 
 class Password(urwid.Edit):
     """Edit box which doesn't show what is entered (show '*' or other char instead)"""
@@ -128,43 +129,81 @@
         render_text = middle * self.car + self.text + (maxcol - len(self.text) - middle) * self.car
         return urwid.Text(render_text)
 
-class SelectableText(urwid.FlowWidget):
+class SelectableText(urwid.WidgetWrap):
     """Text which can be selected with space"""
     signals = ['change']
     
-    def __init__(self, text, align='left', header='', select_attr=None, default_attr=None, selected = False, data=None):
-        self.text=unicode(text)
-        self.header=header
-        if data:
-            self.data=data
-        if select_attr:
-            self.selected = select_attr
-        if default_attr:
-            self.default = default_attr
-        self.align = align
-        self.__selected=selected
+    def __init__(self, text, align='left', header='', focus_attr='default_focus', selected_text=None, selected=False, data=None):
+        """@param text: same as urwid.Text's text parameter
+        @param align: same as urwid.Text's align parameter
+        @select_attr: attrbute to use when selected
+        @param selected: is the text selected ?"""
+        self.focus_attr = focus_attr
+        self.__selected = False
+        self.__was_focused = False
+        self.header = self.__valid_text(header)
+        self.default_txt = self.__valid_text(text)
+        urwid.WidgetWrap.__init__(self, urwid.Text("",align=align))
+        self.setSelectedText(selected_text)
+        self.setState(selected)
+
+    def __valid_text(self, text):
+        """Tmp method needed until dbus and urwid are more friends"""
+        if isinstance(text,basestring):
+            return unicode(text)
+        elif isinstance(text,tuple):
+            return (unicode(text[0]),text[1])
+        elif isinstance(text,list):
+            for idx in range(len(text)):
+                elem = text[idx]
+                if isinstance(elem,basestring):
+                    text[idx] = unicode(elem)
+                if isinstance(elem,tuple):
+                    text[idx] = (unicode(elem[0]),elem[1])
+        else:
+            warning (_('WARNING: unknown text type'))
+        return text
 
     def getValue(self):
-        return self.text
+        if isinstance(self.default_txt,basestring):
+            return self.default_txt
+        list_attr = self.default_txt if isinstance(self.default_txt, list) else [self.default_txt]
+        txt = ""
+        for attr in list_attr:
+            if isinstance(attr,tuple):
+                txt+=attr[1]
+            else:
+                txt+=attr
+        return txt
 
     def get_text(self):
         """for compatibility with urwid.Text"""
         return self.getValue()
 
     def set_text(self, text):
-        self.text=unicode(text)
-        self._invalidate()
+        """/!\ set_text doesn't change self.selected_txt !"""
+        self.default_txt = self.__valid_text(text)
+        self.setState(self.__selected,invisible=True)
 
-    def setAttribute(self, name, value):
-        """Change attribut used for rendering widget
-        @param name: one of
-            -default: when not selected
-            -selected: when selected
-        @param value: name of the attribute
-            /!\ the attribute name followed by _focus is used when widget has focus"""
-        assert name in ['default', 'selected']
-        self.__setattr__(name,value)
-        self._invalidate()
+    def setSelectedText(self, text=None):
+        """Text to display when selected
+        @text: text as in urwid.Text or None for default value"""
+        if text == None:
+            text = ('selected',self.default_txt)
+        self.selected_txt = self.__valid_text(text)
+        if self.__selected:
+            self.setState(self.__selected)
+
+
+    def __set_txt(self):
+        txt_list = [self.header]
+        txt = self.selected_txt if self.__selected else self.default_txt
+        if isinstance(txt,list):
+            txt_list.extend(txt)
+        else:
+            txt_list.append(txt)
+        self._w.base_widget.set_text(txt_list)
+
     
     def setState(self, selected, invisible=False):
         """Change state
@@ -172,6 +211,8 @@
         @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)
@@ -195,39 +236,46 @@
         
         return False
    
-    def rows(self,size,focus=False):
-        return self.display_widget(size, focus).rows(size, focus)
-
     def render(self, size, focus=False):
-        return self.display_widget(size, focus).render(size, focus)
-
-    def display_widget(self, size, focus):
-        try:
-            select_attr = self.selected
-        except AttributeError:
-            select_attr = 'selected'
-        try:
-            default_attr = self.default
-        except AttributeError:
-            default_attr = 'default'
-        attr = select_attr if self.__selected else default_attr
-        if focus:
-            attr+="_focus"
-        return urwid.Text((attr,self.header+self.text), align=self.align)
+        attr_list = self._w.base_widget._attrib
+        if not focus:
+            if self.__was_focused:
+                self.__set_txt()
+                self.__was_focused = False
+        else:
+            if not self.__was_focused:
+                if not attr_list:
+                    attr_list.append((self.focus_attr,len(self._w.base_widget.text)))
+                else:
+                    for idx in range(len(attr_list)):
+                        attr,attr_len = attr_list[idx]
+                        if attr == None:
+                            attr = self.focus_attr
+                            attr_list[idx] = (attr,attr_len)
+                        else:
+                            if not attr.endswith('_focus'):
+                                attr+="_focus"
+                                attr_list[idx] = (attr,attr_len)
+                self._w.base_widget._invalidate()
+                self.__was_focused = True #bloody ugly hack :)
+        return self._w.render(size, focus)
 
 class ClickableText(SelectableText):
     signals = SelectableText.signals + ['click']
     
     def setState(self, selected, invisible=False):
-        self._emit('click')
+        super(ClickableText,self).setState(False,True)
+        if not invisible:
+            self._emit('click')
 
 class CustomButton(ClickableText):
 
     def __init__(self, label, on_press=None, user_data=None, left_border = "[ ", right_border = " ]"):
         self.label = label
-        render_txt = "%s%s%s" % (left_border, label, right_border)
-        self.size = len(render_txt)
-        super(CustomButton, self).__init__(render_txt) 
+        self.left_border = left_border
+        self.right_border = right_border
+        super(CustomButton, self).__init__([left_border, label, right_border]) 
+        self.size = len(self.get_text())
         if on_press:
             urwid.connect_signal(self, 'click', on_press, user_data)
 
@@ -236,7 +284,11 @@
         return self.size
 
     def get_label(self):
-        return self.label
+        return self.label[1] if isinstance(self.label,tuple) else self.label
+
+    def set_label(self, label):
+        self.label = label
+        self.set_text([self.left_border, label, self.right_border])
 
 class GenericList(urwid.WidgetWrap):
     signals = ['click','change']
@@ -404,16 +456,16 @@
 
     def __init__(self):
         self.waitNotifs = urwid.Text('')
-        self.message = ClickableText('', default_attr='notifs')
+        self.message = ClickableText('')
         urwid.connect_signal(self.message, 'click', lambda wid: self.showNext())
-        self.progress = ClickableText('', default_attr='notifs')
+        self.progress = ClickableText('')
         self.columns = urwid.Columns([('fixed',6,self.waitNotifs),self.message,('fixed',4,self.progress)])
         urwid.WidgetWrap.__init__(self, urwid.AttrMap(self.columns,'notifs'))
         self.notifs = []
     
     def __modQueue(self):
         """must be called each time the notifications queue is changed"""
-        self.waitNotifs.set_text("(%i)" % len(self.notifs) if self.notifs else '')
+        self.waitNotifs.set_text(('notifs',"(%i)" % len(self.notifs) if self.notifs else ''))
         self._emit('change')
 
     def setProgress(self,percentage):
@@ -421,7 +473,7 @@
         if percentage == None:
             self.progress.set_text('')
         else:
-            self.progress.set_text('%02i%%' % percentage)
+            self.progress.set_text(('notifs','%02i%%' % percentage))
         self._emit('change')
 
     def addPopUp(self, pop_up_widget):
@@ -432,7 +484,7 @@
     def addMessage(self, message):
         "Add a message to the notificatio bar"
         if not self.message.get_text():
-            self.message.set_text(message)
+            self.message.set_text(('notifs',message))
             self._invalidate()
             self._emit('change')
         else:
@@ -448,7 +500,7 @@
                 break
         if found:
             self.notifs.remove(found)
-            self.message.set_text(found[1])
+            self.message.set_text(('notifs',found[1]))
             self.__modQueue()
         else:
             self.message.set_text('')
@@ -482,7 +534,7 @@
     def __init__(self,parent,items):
         self.parent = parent
         self.selected = None
-        content = urwid.SimpleListWalker([ClickableText(text,default_attr='menuitem') for text in items])
+        content = urwid.SimpleListWalker([ClickableText(('menuitem',text)) for text in items])
         for wid in content:
             urwid.connect_signal(wid, 'click', self.onClick)
 
@@ -501,27 +553,33 @@
             self.parent.keypress(size,'up')
             self.parent.keypress(size,key)
         return super(MenuBox,self).keypress(size,key)
+    
+    def mouse_event(self, size, event, button, x, y, focus):
+        if button == 3:
+            self.parent.keypress(size,'up')
+            return True
+        return super(MenuBox,self).mouse_event(size, event, button, x, y, focus)
 
     def onClick(self, wid):
         self.selected = wid.getValue()
         self._emit('click')
 
-class Menu(urwid.FlowWidget):
+class Menu(urwid.WidgetWrap):
 
     def __init__(self,loop, x_orig=0):
         """Menu widget
         @param loop: main loop of urwid
         @param x_orig: absolute start of the abscissa
         """
-        super(Menu, self).__init__()
         self.loop = loop
         self.menu_keys = []
         self.menu = {}
         self.x_orig = x_orig
         self.shortcuts = {} #keyboard shortcuts
-        self.focus_menu = 0
         self.save_bottom = None
-
+        col_rol = ColumnsRoller()
+        urwid.WidgetWrap.__init__(self, urwid.AttrMap(col_rol,'menubar'))
+    
     def selectable(self):
         return True
 
@@ -543,29 +601,20 @@
 
         self.save_bottom = self.loop.widget
         menu_box = MenuBox(self,[item[0] for item in self.menu[menu_key]])
-        urwid.connect_signal(menu_box, 'click', self.onClick)
+        urwid.connect_signal(menu_box, 'click', self.onItemClick)
         
         self.loop.widget = urwid.Overlay(urwid.AttrMap(menu_box,'menubar'),self.save_bottom,('fixed left', columns),max_len+2,('fixed top',1),None) 
 
     def keypress(self, size, key):
-        if key == 'right' and self.focus_menu < len(self.menu)-1:
-            self.focus_menu += 1
-            self._invalidate()
-        elif key == 'left' and self.focus_menu > 0:
-            self.focus_menu -= 1
-            self._invalidate()
-            return
-        elif key == 'down':
-            if self.menu_keys and not self.save_bottom:
-                column = sum([len(menu)+4 for menu in self.menu_keys[0:self.focus_menu]],self.focus_menu+self.x_orig)
-                self.__buildOverlay(self.menu_keys[self.focus_menu],column)
+        if key == 'down':
+            key = 'enter'
         elif key == 'up':
             if  self.save_bottom:
                 self.loop.widget = self.save_bottom
                 self.save_bottom = None
             
-        return key
-
+        return self._w.base_widget.keypress(size, key)
+    
     def checkShortcuts(self, key):
         for shortcut in self.shortcuts.keys():
             if key == shortcut:
@@ -581,32 +630,19 @@
         if not category in self.menu.keys():
             self.menu_keys.append(category)
             self.menu[category] = []
+            button = CustomButton(('menubar',category), self.onCategoryClick,
+                                   left_border = ('menubar',"[ "),
+                                   right_border = ('menubar'," ]"))
+            self._w.base_widget.addWidget(button,button.getSize())
         self.menu[category].append((item, callback))
         if shortcut:
             assert(shortcut not in self.shortcuts.keys())
             self.shortcuts[shortcut] = (category, item, callback)
 
-    def rows(self,size,focus=False):
-        return self.display_widget(size, focus).rows(size, focus)
-
-    def render(self, size, focus=False):
-        return self.display_widget(size, focus).render(size, focus)
-
-    def display_widget(self, size, focus):
-        render_txt = []
-        idx = 0
-        for menu in self.menu_keys:
-            if focus and idx == self.focus_menu:
-                render_txt.append(('selected_menu', '[ %s ]' % menu)) 
-                render_txt.append(' ') 
-            else:
-                render_txt.append('[ %s ] ' % menu) 
-            idx += 1
-        return urwid.AttrMap(urwid.Text(render_txt), 'menubar')
-
-    def onClick(self, widget):
-        category = self.menu_keys[self.focus_menu]
+    def onItemClick(self, widget):
+        category = self._w.base_widget.getSelected().get_label()
         item = widget.getValue()
+        callback = None
         for menu_item in self.menu[category]:
             if item == menu_item[0]:
                 callback = menu_item[1]
@@ -614,6 +650,11 @@
         if callback:
             self.keypress(None,'up')
             callback((category, item))
+    
+    def onCategoryClick(self, button):
+        self.__buildOverlay(button.get_label(),
+                            self.x_orig + self._w.base_widget.getStartCol(button)) 
+        
 
 class MenuRoller(urwid.WidgetWrap):
 
@@ -751,6 +792,15 @@
         if len(self.widget_list) == 1:
             self.set_focus(0)
 
+    def getStartCol(self, widget):
+        """Return the column of the left corner of the widget"""
+        start_col = 0
+        for wid in self.widget_list[self.__start:]:
+            if wid[1] == widget:
+                return start_col
+            start_col+=wid[0]
+        return None
+
     def selectable(self):
         try:
             return self.widget_list[self.focus_column][1].selectable()
@@ -772,6 +822,10 @@
             return self.widget_list[self.focus_column][1].keypress(size,key)
         return key
 
+    def getSelected(self):
+        """Return selected widget"""
+        return self.widget_list[self.focus_column][1]
+
     def set_focus(self, idx):
         if idx>len(self.widget_list)-1:
             idx = len(self.widget_list)-1
@@ -802,7 +856,7 @@
                 end_wid-=1
         
         cols_left = maxcol - total_wid
-
+        self.__start = start_wid #we need to keep it for getStartCol
         return _prev,_next,start_wid,end_wid,cols_left
         
 
@@ -882,7 +936,7 @@
     signals = ['click']
 
     def __init__(self):
-        #self._current_tab = 0
+        self._current_tab = None
         self._buttons_cont = ColumnsRoller()
         self.tabs = []
         self.__frame = FocusFrame(urwid.Filler(urwid.Text('')),urwid.Pile([self._buttons_cont,urwid.Divider(u"─")]))
@@ -907,6 +961,10 @@
             error(_("INTERNAL ERROR: Tab not found"))
             assert(False)
         self.__frame.body = tab[1]
+        button.set_label(('title',button.get_label()))
+        if self._current_tab:
+            self._current_tab.set_label(self._current_tab.get_label())
+        self._current_tab = button
         if not invisible:
             self._emit('click')