diff urwid_satext/sat_widgets.py @ 30:1aeb3540aa49

files reorganisation after project separation. new README, and COPYING files
author Goffi <goffi@goffi.org>
date Tue, 28 Dec 2010 11:53:18 +0100
parents frontends/primitivus/custom_widgets.py@654d31983f19
children 9fc778aab7f5
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/urwid_satext/sat_widgets.py	Tue Dec 28 11:53:18 2010 +0100
@@ -0,0 +1,1020 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+Primitivus: a SAT frontend
+Copyright (C) 2009, 2010  Jérôme Poisson (goffi@goffi.org)
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+
+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)"""
+
+    def __init__(self, *args, **kwargs):
+        """Same args than Edit.__init__ with an additional keyword arg 'hidden_char'
+        @param hidden_char: char to show instead of what is actually entered: default '*'
+        """
+        self.hidden_char=kwargs['hidden_char'] if kwargs.has_key('hidden_char') else '*'
+        self.__real_text=''
+        super(Password, self).__init__(*args, **kwargs)
+
+    def set_edit_text(self, text):
+        self.__real_text = text
+        hidden_txt = len(text)*'*'
+        super(Password, self).set_edit_text(hidden_txt)
+
+    def get_edit_text(self):
+        return self.__real_text
+
+    def insert_text(self, text):
+        self._edit_text = self.__real_text
+        super(Password,self).insert_text(text)
+
+    def render(self, size, focus=False):
+        return super(Password, self).render(size, focus)
+
+class AdvancedEdit(urwid.Edit):
+    """Edit box with some custom improvments
+    new chars:
+              - C-a: like 'home'
+              - C-e: like 'end'
+              - C-k: remove everything on the right of the cursor
+              - C-w: remove the word on the back
+    new behaviour: emit a 'click' signal when enter is pressed"""
+    signals = urwid.Edit.signals + ['click']
+
+    def setCompletionMethod(self, callback):
+        """Define method called when completion is asked
+        @callback: method with 2 arguments:
+                    - the text to complete
+                    - if there was already a completion, a dict with
+                        - 'completed':last completion
+                        - 'completion_pos': cursor position where the completion starts 
+                        - 'position': last completion cursor position
+                      this dict must be used (and can be filled) to find next completion)
+                   and which return the full text completed"""
+        self.completion_cb = callback
+        self.completion_data = {}
+
+    def keypress(self, size, key):
+        #TODO: insert mode is not managed yet
+        if key == 'ctrl a':
+            key = 'home'
+        elif key == 'ctrl e':
+            key = 'end'
+        elif key == 'ctrl k':
+            self._delete_highlighted()
+            self.set_edit_text(self.edit_text[:self.edit_pos])
+        elif key == 'ctrl w':    
+            before = self.edit_text[:self.edit_pos]
+            pos = before.rstrip().rfind(" ")+1
+            self.set_edit_text(before[:pos] + self.edit_text[self.edit_pos:])
+            self.set_edit_pos(pos)
+        elif key == 'enter':
+            self._emit('click')
+        elif key == 'shift tab':
+            try:
+                before = self.edit_text[:self.edit_pos]
+                if self.completion_data:
+                    if (not self.completion_data['completed']
+                        or self.completion_data['position'] != self.edit_pos
+                        or not before.endswith(self.completion_data['completed'])):
+                        self.completion_data.clear()
+                    else:
+                        before = before[:-len(self.completion_data['completed'])]
+                complet = self.completion_cb(before, self.completion_data)
+                self.completion_data['completed'] = complet[len(before):]
+                self.set_edit_text(complet+self.edit_text[self.edit_pos:])
+                self.set_edit_pos(len(complet))
+                self.completion_data['position'] = self.edit_pos
+                return
+            except AttributeError:
+                #No completion method defined
+                pass
+        return super(AdvancedEdit, self).keypress(size, key) 
+       
+
+class SurroundedText(urwid.FlowWidget):
+    """Text centered on a repeated character (like a Divider, but with a text in the center)"""
+
+    def __init__(self,text,car=utf8decode('─')):
+        self.text=text
+        self.car=car
+
+    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):
+        (maxcol,) = size
+        middle = (maxcol-len(self.text))/2
+        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']
+    
+    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):
+        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):
+        """/!\ set_text doesn't change self.selected_txt !"""
+        self.default_txt = self.__valid_text(text)
+        self.setState(self.__selected,invisible=True)
+
+    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.getValue())
+        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
+        @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
+        self._invalidate()
+        if not invisible:
+            self._emit("change", self.__selected)
+   
+    def getState(self):
+        return self.__selected
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        if key==' ' or key=='enter':
+            self.setState(not self.__selected)
+        else:
+            return key
+
+    def mouse_event(self, size, event, button, x, y, focus):
+        if urwid.is_mouse_press(event) and button == 1:
+            self.setState(not self.__selected)
+            return True
+        
+        return False
+   
+    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
+        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):
+        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
+        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)
+
+    def getSize(self):
+        """Return representation size of the button"""
+        return self.size
+
+    def get_label(self):
+        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']
+
+    def __init__(self, options, style=[], 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
+        @param style: list of string:
+            - 'single' if only one must be selected
+            - 'no_first_select' nothing selected when list is first displayed 
+            - 'can_select_none' if we can select nothing
+        @param align: alignement of text inside the list
+        @param on_click: method called when click signal is emited
+        @param user_data: data sent to the callback for click signal
+        """
+        self.single = 'single' in style
+        self.no_first_select = 'no_first_select' in style
+        self.can_select_none = 'can_select_none' in style
+        self.align = align
+        self.option_type = option_type
+        self.first_display = True
+        
+        if on_click:
+            urwid.connect_signal(self, 'click', on_click, user_data)
+        
+        if on_change:
+            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)
+        self.changeValues(options)
+
+    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
+                widget.setState(True, invisible=True)
+                return
+            if selected:
+                self.unselectAll(invisible=True)
+                widget.setState(True, invisible=True)
+        self._emit("click")
+
+
+    def unselectAll(self, invisible=False):
+        for widget in self.content:
+            if widget.getState():
+                widget.setState(False, invisible)
+                widget._invalidate()
+
+    def deleteValue(self, value):
+        """Delete the first value equal to the param given"""
+        for widget in self.content:
+            if widget.getValue() == value:
+                self.content.remove(widget)
+                self._emit('change')
+                return
+        raise ValueError("%s ==> %s" %  (str(value),str(self.content)))
+
+    def getSelectedValue(self):
+        """Convenience method to get the value selected as a string in single mode, or None"""
+        values = self.getSelectedValues()
+        return values[0] if values else None
+
+    def getAllValues(self):
+        """Return values of all items"""
+        return [widget.getValue() for widget in self.content]
+
+    def getSelectedValues(self):
+        """Return values of selected items"""
+        result = []
+        for widget in self.content:
+            if widget.getState():
+                result.append(widget.getValue())
+        return result
+
+    def getDisplayWidget(self):
+        return self.list_box
+
+    def changeValues(self, new_values):
+        """Change all value in one shot"""
+        if not self.first_display:
+            old_selected = self.getSelectedValues()
+        widgets = []
+        for option in new_values:
+            widget = self.option_type(option, self.align)
+            if not self.first_display and option in old_selected:
+                widget.setState(True)
+            widgets.append(widget)
+            try:
+                urwid.connect_signal(widget, 'change', self.__onStateChange)
+            except NameError:
+                pass #the widget given doesn't support 'change' signal
+        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 
+
+    def selectValue(self, value):
+        """Select the first item which has the given value"""
+        self.unselectAll()
+        idx = 0
+        for widget in self.content:
+            if widget.getValue() == value:
+                widget.setState(True)
+                self.list_box.set_focus(idx)
+                return
+            idx+=1
+
+class List(urwid.FlowWidget):
+    """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'"""
+    signals = ['click','change']
+
+    def __init__(self, options, style=[], max_height=5, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None):
+        self.genericList = GenericList(options, style, align, option_type, on_click, on_change, user_data)
+        self.max_height = max_height 
+
+    def selectable(self):
+        return True
+
+    def keypress(self, size, key):
+        return self.displayWidget(size,True).keypress(size, key)
+        
+    def unselectAll(self, invisible=False):
+        return self.genericList.unselectAll(invisible)
+    
+    def deleteValue(self, value):
+        return self.genericList.deleteValue(value)
+
+    def getSelectedValue(self):
+        return self.genericList.getSelectedValue()
+
+    def getAllValues(self):
+        return self.genericList.getAllValues()
+
+    def getSelectedValues(self):
+        return self.genericList.getSelectedValues()
+
+    def changeValues(self, new_values):
+        return self.genericList.changeValues(new_values)
+
+    def selectValue(self, value):
+        return self.genericList.selectValue(value)
+
+    def render(self, size, focus=False):
+        return self.displayWidget(size, focus).render(size, focus)
+    
+    def rows(self, size, focus=False):
+        return self.displayWidget(size, focus).rows(size, focus)
+
+    def displayWidget(self, size, focus):
+        list_size = sum([wid.rows(size, focus) for wid in self.genericList.content])
+        height = min(list_size,self.max_height) or 1 
+        return urwid.BoxAdapter(self.genericList, height)
+
+## MISC ##
+
+class NotificationBar(urwid.WidgetWrap):
+    """Bar used to show misc information to user"""
+    signals = ['change']
+
+    def __init__(self):
+        self.waitNotifs = urwid.Text('')
+        self.message = ClickableText('')
+        urwid.connect_signal(self.message, 'click', lambda wid: self.showNext())
+        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(('notifs',"(%i)" % len(self.notifs) if self.notifs else ''))
+        self._emit('change')
+
+    def setProgress(self,percentage):
+        """Define the progression to show on the right side of the bar"""
+        if percentage == None:
+            self.progress.set_text('')
+        else:
+            self.progress.set_text(('notifs','%02i%%' % percentage))
+        self._emit('change')
+
+    def addPopUp(self, pop_up_widget):
+        """Add a popup to the waiting queue"""
+        self.notifs.append(('popup',pop_up_widget))
+        self.__modQueue()
+
+    def addMessage(self, message):
+        "Add a message to the notificatio bar"
+        if not self.message.get_text():
+            self.message.set_text(('notifs',message))
+            self._invalidate()
+            self._emit('change')
+        else:
+            self.notifs.append(('message',message))
+            self.__modQueue()
+
+    def showNext(self):
+        """Show next message if any, else delete current message"""
+        found = None
+        for notif in self.notifs:
+            if notif[0] == "message":
+                found = notif
+                break
+        if found:
+            self.notifs.remove(found)
+            self.message.set_text(('notifs',found[1]))
+            self.__modQueue()
+        else:
+            self.message.set_text('')
+            self._emit('change')
+
+    def getNextPopup(self):
+        """Return next pop-up and remove it from the queue
+        @return: pop-up or None if there is no more in the queue"""
+        ret = None
+        for notif in self.notifs:
+            if notif[0] == 'popup':
+                ret = notif[1]
+                break
+        if ret:
+            self.notifs.remove(notif)
+            self.__modQueue()
+        return ret
+
+    def isQueueEmpty(self):
+        return not bool(self.notifs)
+
+    def canHide(self):
+        """Return True if there is now important information to show"""
+        return self.isQueueEmpty() and not self.message.get_text() and not self.progress.get_text()
+
+
+class MenuBox(urwid.WidgetWrap):
+    """Show menu items of a category in a box"""
+    signals = ['click']
+    
+    def __init__(self,parent,items):
+        self.parent = parent
+        self.selected = None
+        content = urwid.SimpleListWalker([ClickableText(('menuitem',text)) for text in items])
+        for wid in content:
+            urwid.connect_signal(wid, 'click', self.onClick)
+
+        self.listBox = urwid.ListBox(content)
+        menubox = urwid.LineBox(urwid.BoxAdapter(self.listBox,len(items)))
+        urwid.WidgetWrap.__init__(self,menubox)
+
+    def getValue(self):
+        return self.selected
+   
+    def keypress(self, size, key):
+        if key=='up':
+            if self.listBox.get_focus()[1] == 0:
+                self.parent.keypress(size, key)
+        elif key=='left' or key=='right':
+            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.WidgetWrap):
+
+    def __init__(self,loop, x_orig=0):
+        """Menu widget
+        @param loop: main loop of urwid
+        @param x_orig: absolute start of the abscissa
+        """
+        self.loop = loop
+        self.menu_keys = []
+        self.menu = {}
+        self.x_orig = x_orig
+        self.shortcuts = {} #keyboard shortcuts
+        self.save_bottom = None
+        col_rol = ColumnsRoller()
+        urwid.WidgetWrap.__init__(self, urwid.AttrMap(col_rol,'menubar'))
+    
+    def selectable(self):
+        return True
+
+    def getMenuSize(self):
+        """return the current number of categories in this menu"""
+        return len(self.menu_keys)
+   
+    def setOrigX(self, orig_x):
+        self.x_orig = orig_x
+
+    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"""
+        max_len = 0
+        for item in self.menu[menu_key]:
+            if len(item[0]) > max_len:
+                max_len = len(item[0])
+
+        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.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 == 'down':
+            key = 'enter'
+        elif key == 'up':
+            if  self.save_bottom:
+                self.loop.widget = self.save_bottom
+                self.save_bottom = None
+            
+        return self._w.base_widget.keypress(size, key)
+    
+    def checkShortcuts(self, key):
+        for shortcut in self.shortcuts.keys():
+            if key == shortcut:
+                category, item, callback = self.shortcuts[shortcut]
+                callback((category, item))
+        return key
+
+    def addMenu(self, category, item, callback, shortcut=None):
+        """Add a menu item, create the category if new
+        @param category: category of the menu (e.g. File/Edit)
+        @param item: menu item (e.g. new/close/about)
+        @callback: method to call when item is selected"""
+        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 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]
+                break
+        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):
+
+    def __init__(self,menus_list):
+        """Create a MenuRoller
+        @param menus_list: list of tuple with (name, Menu_instance), name can be None
+        """
+        assert (menus_list)
+        self.selected = 0
+        self.name_list = []
+        self.menus = {}
+             
+        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)
+
+    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.widget_list[0] = current_name
+        self.columns.column_types[0]=('fixed', name_len)
+        self.columns.widget_list[1] = current_menu
+
+    def keypress(self, size, key):
+        if key=='up':
+            if self.columns.get_focus_column()==0 and self.selected > 0:
+                self.selected -= 1
+                self.__showSelected()
+        elif key=='down':
+            if self.columns.get_focus_column()==0 and self.selected < len(self.name_list)-1:
+                self.selected += 1
+                self.__showSelected()
+        elif key=='right':
+            if self.columns.get_focus_column()==0 and \
+                (isinstance(self.columns.widget_list[1], urwid.Text) or \
+                self.menus[self.name_list[self.selected]].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:
+            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 checkShortcuts(self, key):
+        for menu in self.name_list:
+            key = self.menus[menu].checkShortcuts(key)
+        return key
+        
+
+## DIALOGS ##
+
+class GenericDialog(urwid.WidgetWrap):
+
+    def __init__(self, widgets_lst, title, style=[], **kwargs):
+        frame_header = urwid.AttrMap(urwid.Text(title,'center'),'title')
+       
+        buttons = None
+
+        if "OK/CANCEL" in style:
+            cancel_arg = [kwargs['cancel_value']] if kwargs.has_key('cancel_value') else []
+            ok_arg = [kwargs['ok_value']] if kwargs.has_key('ok_value') else []
+            buttons = [urwid.Button(_("Cancel"), kwargs['cancel_cb'], *cancel_arg),
+                      urwid.Button(_("Ok"), kwargs['ok_cb'], *ok_arg)]
+        elif "YES/NO" in style:
+            yes_arg = [kwargs['yes_value']] if kwargs.has_key('yes_value') else []
+            no_arg = [kwargs['no_value']] if kwargs.has_key('no_value') else []
+            buttons = [urwid.Button(_("Yes"), kwargs['yes_cb'], *yes_arg),
+                      urwid.Button(_("No"), kwargs['no_cb'], *no_arg)]
+        if "OK" in style:
+            ok_arg = [kwargs['ok_value']] if kwargs.has_key('ok_value') else []
+            buttons = [urwid.Button(_("Ok"), kwargs['ok_cb'], *ok_arg)]
+        if buttons:
+            buttons_flow = urwid.GridFlow(buttons, max([len(button.get_label()) for button in buttons])+4, 1, 1, 'center')
+        body_content = urwid.SimpleListWalker(widgets_lst)
+        frame_body = urwid.ListBox(body_content)
+        frame = FocusFrame(frame_body, frame_header, buttons_flow if buttons else None, 'footer' if buttons else 'body')
+        decorated_frame = urwid.LineBox(frame)
+        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):
+        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):
+        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.FlowWidget):
+    
+    def __init__(self, widget_list = None, focus_column=0):
+        self.widget_list = widget_list or []
+        self.focus_column = focus_column
+        self.__start = 0
+        self.__next = False
+
+    def addWidget(self, widget, width):
+        self.widget_list.append((width,widget))
+        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()
+        except IndexError:
+            return False
+    
+    def keypress(self, size, key):
+        if key=='left':
+            if self.focus_column>0:
+                self.focus_column-=1
+                self._invalidate()
+                return
+        if key=='right':
+            if self.focus_column<len(self.widget_list)-1:
+                self.focus_column+=1
+                self._invalidate()
+                return
+        if self.focus_column<len(self.widget_list):
+            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
+        self.focus_column = idx
+
+    def rows(self,size,focus=False):
+        return 1
+
+    def __calculate_limits(self, size):
+        (maxcol,) = size
+        _prev = _next = False
+        start_wid = 0
+        end_wid = len(self.widget_list)-1
+        
+        total_wid = sum([w[0] for w in self.widget_list])
+        while total_wid > maxcol:
+            if self.focus_column == end_wid:
+                if not _prev:
+                    total_wid+=1
+                    _prev = True
+                total_wid-=self.widget_list[start_wid][0]
+                start_wid+=1
+            else:
+                if not _next:
+                    total_wid+=1
+                    _next = True
+                total_wid-=self.widget_list[end_wid][0]
+                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
+        
+
+    def mouse_event(self, size, event, button, x, y, focus):
+        (maxcol,)=size
+
+        if urwid.is_mouse_press(event) and button == 1:
+            _prev,_next,start_wid,end_wid,cols_left = self.__calculate_limits(size)
+            if x==0 and _prev:
+                self.keypress(size,'left')
+                return True
+            if x==maxcol-1 and _next:
+                self.keypress(size,'right')
+                return True
+            
+            current_pos = 1 if _prev else 0
+            idx = 0
+            while current_pos<x and idx<len(self.widget_list):
+                width,widget = self.widget_list[idx]
+                if x<=current_pos+width:
+                    self.focus_column = idx
+                    self._invalidate()
+                    if not hasattr(widget,'mouse_event'):
+                        return False
+                    return widget.mouse_event((width,0), event, button, 
+                        x-current_pos, 0, focus)
+
+                current_pos+=self.widget_list[idx][0]
+                idx+=1
+        
+        return False
+    
+    def render(self, size, focus=False):
+        if not self.widget_list:
+            return SolidCanvas(" ", size[0], 1)
+
+        _prev,_next,start_wid,end_wid,cols_left = self.__calculate_limits(size)
+        
+        idx=start_wid
+        render = []
+
+        for width,widget in self.widget_list[start_wid:end_wid+1]:
+            _focus = idx == self.focus_column and focus
+            render.append((widget.render((width,),_focus),False,_focus,width))
+            idx+=1
+        if _prev:
+            render.insert(0,(urwid.Text([u"◀"]).render((1,),False),False,False,1))
+        if _next:
+            render.append((urwid.Text([u"▶"],align='right').render((1+cols_left,),False),False,False,1+cols_left))
+        else:
+            render.append((urwid.SolidCanvas(" "*cols_left, size[0], 1),False,False,cols_left))
+
+        return urwid.CanvasJoin(render)
+
+
+class FocusFrame(urwid.Frame):
+    """Frame which manage 'tab' key"""
+
+    def keypress(self, size, key):
+        ret = urwid.Frame.keypress(self, size, key)
+        if not ret:
+            return
+        
+        if key == 'tab':
+            focus_list = ('header','body','footer')
+            focus_idx = focus_list.index(self.focus_part)
+            for i in range(2):
+                focus_idx = (focus_idx + 1) % len(focus_list)
+                focus_name = focus_list[focus_idx]
+                widget = getattr(self,'_'+focus_name)
+                if widget!=None and widget.selectable():
+                    self.set_focus(focus_name)
+
+        return ret 
+
+class TabsContainer(urwid.WidgetWrap):
+    signals = ['click']
+
+    def __init__(self):
+        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"─")]))
+        urwid.WidgetWrap.__init__(self, self.__frame)
+
+    def keypress(self, size, key):
+        if key=='tab':
+            self._w.keypress(size,key)
+            return
+        return self._w.keypress(size,key)
+
+    def __buttonClicked(self, button, invisible=False):
+        """Called when a button on the tab is changed,
+        change the page
+        @param button: button clicked
+        @param invisible: emit signal only if False"""
+        tab_name = button.get_label()
+        for tab in self.tabs:
+            if tab[0] == tab_name:
+                break
+        if tab[0] != tab_name:
+            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')
+
+    def __appendButton(self, name):
+        """Append a button to the frame header,
+        and link it to the page change method"""
+        button = CustomButton(name, self.__buttonClicked, left_border = '', right_border=' | ')
+        self._buttons_cont.addWidget(button, button.getSize())
+        if len(self._buttons_cont.widget_list) == 1:
+            #first button: we set the focus and the body
+            self._buttons_cont.set_focus(0)
+            self.__buttonClicked(button,True)
+
+    def addTab(self,name,content=[]):
+        """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)"""
+        listbox = urwid.ListBox(urwid.SimpleListWalker(content))
+        self.tabs.append([name,listbox])
+        self.__appendButton(name)
+        return listbox
+
+    def addFooter(self, widget):
+        """Add a widget on the bottom of the tab (will be displayed on all pages)
+        @param widget: FlowWidget"""
+        self._w.footer = widget
+    
+
+## DECORATORS ##
+class LabelLine(urwid.LineBox):
+    """Like LineBox, but with a Label centered in the top line"""
+
+    def __init__(self, original_widget, label_widget):
+        urwid.LineBox.__init__(self, original_widget)
+        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."""
+        
+        widgets = [original_widget]
+        if left_char:
+            widgets.insert(0, ('fixed', 1, urwid.SolidFill(left_char)))
+        if right_char:
+            widgets.append(('fixed', 1, urwid.SolidFill(right_char)))
+        columns = urwid.Columns(widgets, box_columns = [0,2], focus_column = 1)
+        urwid.WidgetDecoration.__init__(self, original_widget)
+        urwid.WidgetWrap.__init__(self, columns)
+
+