comparison 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
comparison
equal deleted inserted replaced
69:b39c81cdd863 70:24d49f1d735f
83 except AttributeError: 83 except AttributeError:
84 #No completion method defined 84 #No completion method defined
85 pass 85 pass
86 return super(AdvancedEdit, self).keypress(size, key) 86 return super(AdvancedEdit, self).keypress(size, key)
87 87
88
88 class Password(AdvancedEdit): 89 class Password(AdvancedEdit):
89 """Edit box which doesn't show what is entered (show '*' or other char instead)""" 90 """Edit box which doesn't show what is entered (show '*' or other char instead)"""
90 91
91 def __init__(self, *args, **kwargs): 92 def __init__(self, *args, **kwargs):
92 """Same args than Edit.__init__ with an additional keyword arg 'hidden_char' 93 """Same args than Edit.__init__ with an additional keyword arg 'hidden_char'
108 self._edit_text = self.__real_text 109 self._edit_text = self.__real_text
109 super(Password,self).insert_text(text) 110 super(Password,self).insert_text(text)
110 111
111 def render(self, size, focus=False): 112 def render(self, size, focus=False):
112 return super(Password, self).render(size, focus) 113 return super(Password, self).render(size, focus)
114
113 115
114 class ModalEdit(AdvancedEdit): 116 class ModalEdit(AdvancedEdit):
115 """AdvancedEdit with vi-like mode management 117 """AdvancedEdit with vi-like mode management
116 - there is a new 'mode' property which can be changed with properties 118 - there is a new 'mode' property which can be changed with properties
117 specified during init 119 specified during init
158 if self._mode == 'NORMAL' and key in self._modes: 160 if self._mode == 'NORMAL' and key in self._modes:
159 self.mode = self._modes[key][0] 161 self.mode = self._modes[key][0]
160 return 162 return
161 return super(ModalEdit, self).keypress(size, key) 163 return super(ModalEdit, self).keypress(size, key)
162 164
165
163 class SurroundedText(urwid.Widget): 166 class SurroundedText(urwid.Widget):
164 """Text centered on a repeated character (like a Divider, but with a text in the center)""" 167 """Text centered on a repeated character (like a Divider, but with a text in the center)"""
165 _sizing = frozenset(['flow']) 168 _sizing = frozenset(['flow'])
166 169
167 def __init__(self,text,car=utf8decode('─')): 170 def __init__(self,text,car=utf8decode('─')):
177 def display_widget(self, size, focus): 180 def display_widget(self, size, focus):
178 (maxcol,) = size 181 (maxcol,) = size
179 middle = (maxcol-len(self.text))/2 182 middle = (maxcol-len(self.text))/2
180 render_text = middle * self.car + self.text + (maxcol - len(self.text) - middle) * self.car 183 render_text = middle * self.car + self.text + (maxcol - len(self.text) - middle) * self.car
181 return urwid.Text(render_text) 184 return urwid.Text(render_text)
185
182 186
183 class SelectableText(urwid.WidgetWrap): 187 class SelectableText(urwid.WidgetWrap):
184 """Text which can be selected with space""" 188 """Text which can be selected with space"""
185 signals = ['change'] 189 signals = ['change']
186 190
292 attr_list[idx] = (attr,attr_len) 296 attr_list[idx] = (attr,attr_len)
293 self._w.base_widget._invalidate() 297 self._w.base_widget._invalidate()
294 self.__was_focused = True #bloody ugly hack :) 298 self.__was_focused = True #bloody ugly hack :)
295 return self._w.render(size, focus) 299 return self._w.render(size, focus)
296 300
301
297 class ClickableText(SelectableText): 302 class ClickableText(SelectableText):
298 signals = SelectableText.signals + ['click'] 303 signals = SelectableText.signals + ['click']
299 304
300 def setState(self, selected, invisible=False): 305 def setState(self, selected, invisible=False):
301 super(ClickableText,self).setState(False,True) 306 super(ClickableText,self).setState(False,True)
302 if not invisible: 307 if not invisible:
303 self._emit('click') 308 self._emit('click')
309
304 310
305 class CustomButton(ClickableText): 311 class CustomButton(ClickableText):
306 312
307 def __init__(self, label, on_press=None, user_data=None, left_border = "[ ", right_border = " ]"): 313 def __init__(self, label, on_press=None, user_data=None, left_border = "[ ", right_border = " ]"):
308 self.label = label 314 self.label = label
322 328
323 def set_label(self, label): 329 def set_label(self, label):
324 self.label = label 330 self.label = label
325 self.set_text([self.left_border, label, self.right_border]) 331 self.set_text([self.left_border, label, self.right_border])
326 332
333
327 class ListOption(unicode): 334 class ListOption(unicode):
328 """ Class similar to unicode, but which make the difference between value and label 335 """ Class similar to unicode, but which make the difference between value and label
329 label is show when use as unicode, the .value attribute contain the actual value 336 label is show when use as unicode, the .value attribute contain the actual value
330 Can be initialised with: 337 Can be initialised with:
331 - basestring (label = value = given string) 338 - basestring (label = value = given string)
332 - a tuple with (value, label) 339 - a tuple with (value, label)
333 XXX: comparaison is made again value, not the label which is the one displayed 340 XXX: comparaison is made against value, not the label which is the one displayed
334 341
335 """ 342 """
336 343
337 def __new__(cls, option): 344 def __new__(cls, option):
338 if (isinstance(option, cls)): 345 if (isinstance(option, cls)):
341 value = label = option 348 value = label = option
342 elif (isinstance(option, tuple) and len(option) == 2): 349 elif (isinstance(option, tuple) and len(option) == 2):
343 value, label = option 350 value, label = option
344 else: 351 else:
345 raise NotImplementedError 352 raise NotImplementedError
346 if not value:
347 raise ValueError("value can't be empty")
348 if not label: 353 if not label:
349 label = value 354 label = value
350 instance = super(ListOption, cls).__new__(cls, label) 355 instance = super(ListOption, cls).__new__(cls, label)
351 instance._value = value 356 instance._value = value
352 return instance 357 return instance
385 390
386 391
387 class GenericList(urwid.WidgetWrap): 392 class GenericList(urwid.WidgetWrap):
388 signals = ['click','change'] 393 signals = ['click','change']
389 394
390 def __init__(self, options, style=[], align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): 395 def __init__(self, options, style=None, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None):
391 """ 396 """
392 Widget managing list of string and their selection 397 Widget managing list of string and their selection
393 @param options: list of strings used for options 398 @param options: list of strings used for options
394 @param style: list of string: 399 @param style: list of string:
395 - 'single' if only one must be selected 400 - 'single' if only one must be selected
397 - 'can_select_none' if we can select nothing 402 - 'can_select_none' if we can select nothing
398 @param align: alignement of text inside the list 403 @param align: alignement of text inside the list
399 @param on_click: method called when click signal is emited 404 @param on_click: method called when click signal is emited
400 @param user_data: data sent to the callback for click signal 405 @param user_data: data sent to the callback for click signal
401 """ 406 """
407 if style is None:
408 style = []
402 self.single = 'single' in style 409 self.single = 'single' in style
403 self.no_first_select = 'no_first_select' in style 410 self.no_first_select = 'no_first_select' in style
404 self.can_select_none = 'can_select_none' in style 411 self.can_select_none = 'can_select_none' in style
405 self.align = align 412 self.align = align
406 self.option_type = option_type 413 self.option_type = option_type
499 widget.setState(True) 506 widget.setState(True)
500 self.list_box.set_focus(idx) 507 self.list_box.set_focus(idx)
501 return 508 return
502 idx+=1 509 idx+=1
503 510
511
504 class List(urwid.Widget): 512 class List(urwid.Widget):
505 """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'""" 513 """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'"""
506 signals = ['click','change'] 514 signals = ['click','change']
507 _sizing = frozenset(['flow']) 515 _sizing = frozenset(['flow'])
508 516
509 def __init__(self, options, style=[], max_height=5, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): 517 def __init__(self, options, style=None, max_height=5, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None):
518 if style is None:
519 style = []
510 self.genericList = GenericList(options, style, align, option_type, on_click, on_change, user_data) 520 self.genericList = GenericList(options, style, align, option_type, on_click, on_change, user_data)
521 urwid.connect_signal(self.genericList, 'change', self._onChange)
522 urwid.connect_signal(self.genericList, 'click', self._onClick)
511 self.max_height = max_height 523 self.max_height = max_height
524
525 def _onChange(self, widget):
526 self._emit('change')
527
528 def _onClick(self, widget):
529 self._emit('click')
512 530
513 def selectable(self): 531 def selectable(self):
514 return True 532 return True
515 533
516 def keypress(self, size, key): 534 def keypress(self, size, key):
545 563
546 def displayWidget(self, size, focus): 564 def displayWidget(self, size, focus):
547 list_size = sum([wid.rows(size, focus) for wid in self.genericList.content]) 565 list_size = sum([wid.rows(size, focus) for wid in self.genericList.content])
548 height = min(list_size,self.max_height) or 1 566 height = min(list_size,self.max_height) or 1
549 return urwid.BoxAdapter(self.genericList, height) 567 return urwid.BoxAdapter(self.genericList, height)
568
550 569
551 ## MISC ## 570 ## MISC ##
552 571
553 class NotificationBar(urwid.WidgetWrap): 572 class NotificationBar(urwid.WidgetWrap):
554 """Bar used to show misc information to user""" 573 """Bar used to show misc information to user"""
662 681
663 def onClick(self, wid): 682 def onClick(self, wid):
664 self.selected = wid.getValue() 683 self.selected = wid.getValue()
665 self._emit('click') 684 self._emit('click')
666 685
686
667 class Menu(urwid.WidgetWrap): 687 class Menu(urwid.WidgetWrap):
668 688
669 def __init__(self,loop, x_orig=0): 689 def __init__(self,loop, x_orig=0):
670 """Menu widget 690 """Menu widget
671 @param loop: main loop of urwid 691 @param loop: main loop of urwid
688 return len(self.menu_keys) 708 return len(self.menu_keys)
689 709
690 def setOrigX(self, orig_x): 710 def setOrigX(self, orig_x):
691 self.x_orig = orig_x 711 self.x_orig = orig_x
692 712
693 def __buildOverlay(self,menu_key,columns): 713 def __buildOverlay(self, menu_key, columns):
694 """Build the overlay menu which show menuitems 714 """Build the overlay menu which show menuitems
695 @param menu_key: name of the category 715 @param menu_key: name of the category
696 @colums: column number where the menubox must be displayed""" 716 @param columns: column number where the menubox must be displayed"""
697 max_len = 0 717 max_len = 0
698 for item in self.menu[menu_key]: 718 for item in self.menu[menu_key]:
699 if len(item[0]) > max_len: 719 if len(item[0]) > max_len:
700 max_len = len(item[0]) 720 max_len = len(item[0])
701 721
825 845
826 ## DIALOGS ## 846 ## DIALOGS ##
827 847
828 class GenericDialog(urwid.WidgetWrap): 848 class GenericDialog(urwid.WidgetWrap):
829 849
830 def __init__(self, widgets_lst, title, style=[], **kwargs): 850 def __init__(self, widgets_lst, title, style=None, **kwargs):
851 if style is None:
852 style = []
831 frame_header = urwid.AttrMap(urwid.Text(title,'center'),'title') 853 frame_header = urwid.AttrMap(urwid.Text(title,'center'),'title')
832 854
833 buttons = None 855 buttons = None
834 856
835 if "OK/CANCEL" in style: 857 if "OK/CANCEL" in style:
852 frame = FocusFrame(frame_body, frame_header, buttons_flow if buttons else None, 'footer' if buttons else 'body') 874 frame = FocusFrame(frame_body, frame_header, buttons_flow if buttons else None, 'footer' if buttons else 'body')
853 decorated_frame = urwid.LineBox(frame) 875 decorated_frame = urwid.LineBox(frame)
854 urwid.WidgetWrap.__init__(self, decorated_frame) 876 urwid.WidgetWrap.__init__(self, decorated_frame)
855 877
856 878
857
858 class InputDialog(GenericDialog): 879 class InputDialog(GenericDialog):
859 """Dialog with an edit box""" 880 """Dialog with an edit box"""
860 881
861 def __init__(self, title, instrucions, style=['OK/CANCEL'], default_txt = '', **kwargs): 882 def __init__(self, title, instrucions, style=None, default_txt = '', **kwargs):
883 if style is None:
884 style = ['OK/CANCEL']
862 instr_wid = urwid.Text(instrucions+':') 885 instr_wid = urwid.Text(instrucions+':')
863 edit_box = AdvancedEdit(edit_text=default_txt) 886 edit_box = AdvancedEdit(edit_text=default_txt)
864 GenericDialog.__init__(self, [instr_wid,edit_box], title, style, ok_value=edit_box, **kwargs) 887 GenericDialog.__init__(self, [instr_wid,edit_box], title, style, ok_value=edit_box, **kwargs)
865 self._w.base_widget.set_focus('body') 888 self._w.base_widget.set_focus('body')
866 889
890
867 class ConfirmDialog(GenericDialog): 891 class ConfirmDialog(GenericDialog):
868 """Dialog with buttons for confirm or cancel an action""" 892 """Dialog with buttons for confirm or cancel an action"""
869 893
870 def __init__(self, title, style=['YES/NO'], **kwargs): 894 def __init__(self, title, style=None, **kwargs):
895 if style is None:
896 style = ['YES/NO']
871 GenericDialog.__init__(self, [], title, style, **kwargs) 897 GenericDialog.__init__(self, [], title, style, **kwargs)
898
872 899
873 class Alert(GenericDialog): 900 class Alert(GenericDialog):
874 """Dialog with just a message and a OK button""" 901 """Dialog with just a message and a OK button"""
875 902
876 def __init__(self, title, message, style=['OK'], **kwargs): 903 def __init__(self, title, message, style=['OK'], **kwargs):
877 GenericDialog.__init__(self, [urwid.Text(message, 'center')], title, style, ok_value=None, **kwargs) 904 GenericDialog.__init__(self, [urwid.Text(message, 'center')], title, style, ok_value=None, **kwargs)
905
878 906
879 ## CONTAINERS ## 907 ## CONTAINERS ##
880 908
881 class ColumnsRoller(urwid.Widget): 909 class ColumnsRoller(urwid.Widget):
882 _sizing = frozenset(['flow']) 910 _sizing = frozenset(['flow'])
1029 widget = getattr(self,'_'+focus_name) 1057 widget = getattr(self,'_'+focus_name)
1030 if widget!=None and widget.selectable(): 1058 if widget!=None and widget.selectable():
1031 self.set_focus(focus_name) 1059 self.set_focus(focus_name)
1032 1060
1033 return ret 1061 return ret
1062
1034 1063
1035 class TabsContainer(urwid.WidgetWrap): 1064 class TabsContainer(urwid.WidgetWrap):
1036 signals = ['click'] 1065 signals = ['click']
1037 1066
1038 def __init__(self): 1067 def __init__(self):
1076 if len(self._buttons_cont.widget_list) == 1: 1105 if len(self._buttons_cont.widget_list) == 1:
1077 #first button: we set the focus and the body 1106 #first button: we set the focus and the body
1078 self._buttons_cont.set_focus(0) 1107 self._buttons_cont.set_focus(0)
1079 self.__buttonClicked(button,True) 1108 self.__buttonClicked(button,True)
1080 1109
1081 def addTab(self,name,content=[]): 1110 def addTab(self,name,content=None):
1082 """Add a page to the container 1111 """Add a page to the container
1083 @param name: name of the page (what appear on the tab) 1112 @param name: name of the page (what appear on the tab)
1084 @param content: content of the page 1113 @param content: content of the page
1085 @return: ListBox (content of the page)""" 1114 @return: ListBox (content of the page)"""
1115 if content is None:
1116 content = []
1086 listbox = urwid.ListBox(urwid.SimpleListWalker(content)) 1117 listbox = urwid.ListBox(urwid.SimpleListWalker(content))
1087 self.tabs.append([name,listbox]) 1118 self.tabs.append([name,listbox])
1088 self.__appendButton(name) 1119 self.__appendButton(name)
1089 return listbox 1120 return listbox
1090 1121
1092 """Add a widget on the bottom of the tab (will be displayed on all pages) 1123 """Add a widget on the bottom of the tab (will be displayed on all pages)
1093 @param widget: FlowWidget""" 1124 @param widget: FlowWidget"""
1094 self._w.footer = widget 1125 self._w.footer = widget
1095 1126
1096 1127
1128 class HighlightColumns(urwid.AttrMap):
1129 """ Decorated columns which highlight all or some columns """
1130
1131 def __init__(self, highlight_cols, highlight_attr, *args, **kwargs):
1132 """ Create the HighlightColumns
1133 @param highlight_cols: tuple of columns to highlight, () to highlight to whole row
1134 @param highlight_attr: name of the attribute to use when focused
1135 other parameter are passed to urwid Columns
1136
1137 """
1138 columns = urwid.Columns(*args, **kwargs)
1139 self.highlight_cols = highlight_cols
1140 self.highlight_attr = highlight_attr
1141 self.has_focus = False
1142 if highlight_cols == ():
1143 super(HighlightColumns, self).__init__(columns, None, highlight_attr)
1144 self.highlight_cols = None
1145 else:
1146 super(HighlightColumns, self).__init__(columns, None)
1147
1148 @property
1149 def options(self):
1150 return self.base_widget.options
1151
1152 @property
1153 def contents(self):
1154 return self.base_widget.contents
1155
1156 @property
1157 def focus_position(self):
1158 return self.base_widget.focus_position
1159
1160 @focus_position.setter
1161 def focus_position(self, value):
1162 self.base_widget.focus_position = value
1163
1164 def addWidget(self, wid, options):
1165 """ Add a widget to the columns
1166 Widget is wrapped with AttrMap, that's why Columns.contents should not be used directly for appending new widgets
1167 @param wid: widget to add
1168 @param options: result of Columns.options(...)
1169
1170 """
1171 wrapper = urwid.AttrMap(wid, None)
1172 self.base_widget.contents.append((wrapper, options))
1173
1174
1175 def render(self, size, focus=False):
1176 if self.highlight_cols and focus != self.has_focus:
1177 self.has_focus = focus
1178 for idx in self.highlight_cols:
1179 wid = self.base_widget.contents[idx][0]
1180 wid.set_attr_map({None: self.highlight_attr if focus else None})
1181
1182 return super(HighlightColumns, self).render(size, focus)
1183
1184
1185 class TableContainer(urwid.WidgetWrap):
1186 """ Widgets are disposed in row and columns """
1187 signals = ['click']
1188
1189 def __init__(self, items=None, columns=None, dividechars=1, row_selectable=False, select_key='enter', options=None):
1190 """ Create a TableContainer
1191 @param items: iterable of widgets to add to this container
1192 @param columns: nb of columns of this table
1193 @param dividechars: same as dividechars param for urwid.Columns
1194 @param row_selectable: if True, row are always selectable, even if they don't contain any selectable widget
1195 @param options: dictionnary with the following keys:
1196 - ADAPT: tuple of columns for which the size must be adapted to its contents,
1197 empty tuple for all columns
1198 - HIGHLIGHT: tuple of columns which must be higlighted on focus,
1199 empty tuple for the whole row
1200 - FOCUS_ATTR: Attribute name to use when focused (see HIGHLIGHT). Default is "table_selected"
1201
1202 """
1203 pile = urwid.Pile([])
1204 super(TableContainer, self).__init__(pile)
1205 if items is None:
1206 items = []
1207 if columns is None: # if columns is None, we suppose only one row is given in items
1208 columns = len(items)
1209 assert columns
1210 self._columns = columns
1211 self._row_selectable = row_selectable
1212 self.select_key = select_key
1213 if options is None:
1214 options = {}
1215 for opt in ['ADAPT', 'HIGHLIGHT']:
1216 if opt in options:
1217 try:
1218 options[opt] = tuple(options[opt])
1219 except TypeError:
1220 warning('[%s] option is not a tuple' % opt)
1221 options[opt] = ()
1222 self._options = options
1223 self._dividechars = dividechars
1224 self._idx = 0
1225 self._longuest = self._columns * [0]
1226 self._next_row_idx = None
1227 for item in items:
1228 self.addWidget(item)
1229
1230 def _getIdealSize(self, widget):
1231 """ return preferred size for widget, or 0 if we can't find it """
1232 try:
1233 return len(widget.text)
1234 except AttributeError:
1235 return 0
1236
1237 def keypress(self, size, key):
1238 if key == self.select_key and self._row_selectable:
1239 self._emit('click')
1240 else:
1241 return super(TableContainer, self).keypress(size, key)
1242
1243
1244 def addWidget(self, widget):
1245 # TODO: use a contents property ?
1246 pile = self._w
1247 col_idx = self._idx % self._columns
1248
1249 options = None
1250
1251 if col_idx == 0:
1252 # we have a new row
1253 columns = HighlightColumns(self._options.get('HIGHLIGHT'), self._options.get('FOCUS_ATTR', 'table_selected'), [], dividechars=self._dividechars)
1254 columns.row_idx = self._next_row_idx
1255 pile.contents.append((columns, pile.options()))
1256 else:
1257 columns = pile.contents[-1][0]
1258
1259 if 'ADAPT' in self._options and (col_idx in self._options['ADAPT']
1260 or self._options['ADAPT'] == ()):
1261 current_len = self._getIdealSize(widget)
1262 longuest = self._longuest[col_idx]
1263 max_len = max(longuest, current_len)
1264 if max_len > longuest:
1265 self._longuest[col_idx] = max_len
1266 for wid,_ in pile.contents[:-1]:
1267 col = wid.base_widget
1268 col.contents[col_idx] = (col.contents[col_idx][0], col.options('given', max_len))
1269 options = columns.options('given', max_len) if max_len else columns.options()
1270
1271 columns.addWidget(widget, options or columns.options())
1272
1273 if self._row_selectable and col_idx == self._columns - 1:
1274 columns.addWidget(urwid.SelectableIcon(''), columns.options('given', 0))
1275
1276 if not columns.selectable() and columns.contents[-1][0].base_widget.selectable():
1277 columns.focus_position = len(columns.contents)-1
1278 self._idx += 1
1279
1280 def setRowIndex(self, idx):
1281 self._next_row_idx = idx
1282
1283 def getSelectedWidgets(self):
1284 columns = self._w.focus
1285 return (wid for wid, _ in columns.contents)
1286
1287 def getSelectedIndex(self):
1288 columns = self._w.focus
1289 return columns.row_idx
1290
1097 ## DECORATORS ## 1291 ## DECORATORS ##
1098 class LabelLine(urwid.LineBox): 1292 class LabelLine(urwid.LineBox):
1099 """Like LineBox, but with a Label centered in the top line""" 1293 """Like LineBox, but with a Label centered in the top line"""
1100 1294
1101 def __init__(self, original_widget, label_widget): 1295 def __init__(self, original_widget, label_widget):
1102 urwid.LineBox.__init__(self, original_widget) 1296 urwid.LineBox.__init__(self, original_widget)
1103 top_columns = self._w.widget_list[0] 1297 top_columns = self._w.widget_list[0]
1104 top_columns.widget_list[1] = label_widget 1298 top_columns.widget_list[1] = label_widget
1299
1105 1300
1106 class VerticalSeparator(urwid.WidgetDecoration, urwid.WidgetWrap): 1301 class VerticalSeparator(urwid.WidgetDecoration, urwid.WidgetWrap):
1107 def __init__(self, original_widget, left_char = u"│", right_char = ''): 1302 def __init__(self, original_widget, left_char = u"│", right_char = ''):
1108 """Draw a separator on left and/or right of original_widget.""" 1303 """Draw a separator on left and/or right of original_widget."""
1109 1304