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
This commit is contained in:
Goffi 2010-08-19 21:11:22 +08:00
commit 827bc0bd3c
6 changed files with 161 additions and 101 deletions

View file

@ -95,7 +95,8 @@ class ContactList(urwid.WidgetWrap, QuickContactList):
nick = self.CM.getAttr(jid,'nick')
display = nick or name or jid.node or jid.short
header = '(*) ' if contact in self.alert_jid else ''
widget = custom_widgets.SelectableText(display, selected = contact==self.selected, header=header, data=contact)
widget = custom_widgets.SelectableText(display, selected = contact==self.selected, header=header)
widget.data = contact
if contact in self.alert_jid:
widget.setAttribute('default','alert')
content.append(widget)

View file

@ -21,6 +21,7 @@ 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)"""
@ -128,43 +129,81 @@ class SurroundedText(urwid.FlowWidget):
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 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 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 setState(self, selected, invisible=False):
"""Change state
@ -172,6 +211,8 @@ class SelectableText(urwid.FlowWidget):
@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 @@ class SelectableText(urwid.FlowWidget):
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 @@ class CustomButton(ClickableText):
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 @@ class NotificationBar(urwid.WidgetWrap):
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 @@ class NotificationBar(urwid.WidgetWrap):
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 @@ class NotificationBar(urwid.WidgetWrap):
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 @@ class NotificationBar(urwid.WidgetWrap):
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 @@ class MenuBox(urwid.WidgetWrap):
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 @@ class MenuBox(urwid.WidgetWrap):
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 @@ class Menu(urwid.FlowWidget):
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 @@ class Menu(urwid.FlowWidget):
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 @@ class Menu(urwid.FlowWidget):
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 @@ class ColumnsRoller(urwid.FlowWidget):
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 @@ class ColumnsRoller(urwid.FlowWidget):
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 @@ class ColumnsRoller(urwid.FlowWidget):
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 @@ class TabsContainer(urwid.WidgetWrap):
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 @@ class TabsContainer(urwid.WidgetWrap):
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')

View file

@ -106,13 +106,13 @@ class FilesViewer(urwid.WidgetWrap):
directories.sort()
files.sort()
if os.path.abspath(path)!='/' and os.path.abspath(path) != '//':
previous_wid = custom_widgets.ClickableText('..',default_attr='directory')
previous_wid = custom_widgets.ClickableText(('directory','..'))
urwid.connect_signal(previous_wid,'click',self.onPreviousDir)
self.files_list.append(previous_wid)
for directory in directories:
if directory.startswith('.') and not self.show_hidden:
continue
dir_wid = custom_widgets.ClickableText(directory,default_attr='directory')
dir_wid = custom_widgets.ClickableText(('directory',directory))
urwid.connect_signal(dir_wid,'click',self.onDirClick)
self.files_list.append(dir_wid)
self.files_list.append(urwid.AttrMap(urwid.Divider('-'),'separator'))

View file

@ -45,6 +45,7 @@ logging.basicConfig(level=logging.CRITICAL, #TODO: configure it to put messages
const_APP_NAME = "Primitivus"
const_PALETTE = [('title', 'black', 'light gray', 'standout,underline'),
('title_focus', 'white,bold', 'light gray', 'standout,underline'),
('selected', 'default', 'dark red'),
('selected_focus', 'default,bold', 'dark red'),
('default', 'default', 'default'),
@ -55,6 +56,7 @@ const_PALETTE = [('title', 'black', 'light gray', 'standout,underline'),
('my_nick', 'dark red,bold', 'default'),
('other_nick', 'dark cyan,bold', 'default'),
('menubar', 'light gray,bold', 'dark red'),
('menubar_focus', 'light gray,bold', 'dark green'),
('selected_menu', 'light gray,bold', 'dark green'),
('menuitem', 'light gray,bold', 'dark red'),
('menuitem_focus', 'light gray,bold', 'dark green'),

View file

@ -232,7 +232,7 @@ class XMLUI(urwid.WidgetWrap):
ctrl = self.ctrl_list[ctrl_name]
if isinstance(ctrl['control'], custom_widgets.List):
data.append((ctrl_name, ctrl['control'].getSelectedValue()))
if isinstance(ctrl['control'], urwid.CheckBox):
elif isinstance(ctrl['control'], urwid.CheckBox):
data.append((ctrl_name, "true" if ctrl['control'].get_state() else "false"))
else:
data.append((ctrl_name, ctrl['control'].get_edit_text()))

View file

@ -55,8 +55,7 @@ class DBusBridgeFrontend(BridgeFrontend):
return self.db_req_iface.getProfileName(profile_key)
def getProfilesList(self):
return self.db_req_iface.getProfilesList()
return self.db_req_iface.getProfilesList()
def createProfile(self, name):
return self.db_req_iface.createProfile(name)