comparison urwid_satext/sat_widgets.py @ 107:ed2675f92f7c

menus management improvment
author Goffi <goffi@goffi.org>
date Wed, 10 Dec 2014 19:08:35 +0100
parents b2fee87c1d5a
children 5bb3b7e25bf6
comparison
equal deleted inserted replaced
106:f083eca93047 107:ed2675f92f7c
19 19
20 import urwid 20 import urwid
21 import logging as log 21 import logging as log
22 import encodings 22 import encodings
23 utf8decode = lambda s: encodings.codecs.utf_8_decode(s)[0] 23 utf8decode = lambda s: encodings.codecs.utf_8_decode(s)[0]
24
25 import uuid
26
27 import collections
24 28
25 from urwid.util import is_mouse_press #XXX: is_mouse_press is not included in urwid in 1.0.0 29 from urwid.util import is_mouse_press #XXX: is_mouse_press is not included in urwid in 1.0.0
26 from .keys import action_key_map as a_key 30 from .keys import action_key_map as a_key
27 31
28 FOCUS_KEYS = (a_key['FOCUS_SWITCH'], a_key['FOCUS_UP'], a_key['FOCUS_DOWN']) 32 FOCUS_KEYS = (a_key['FOCUS_SWITCH'], a_key['FOCUS_UP'], a_key['FOCUS_DOWN'])
213 @param align: same as urwid.Text's align parameter 217 @param align: same as urwid.Text's align parameter
214 @select_attr: attrbute to use when selected 218 @select_attr: attrbute to use when selected
215 @param selected: is the text selected ? 219 @param selected: is the text selected ?
216 """ 220 """
217 self.focus_attr = focus_attr 221 self.focus_attr = focus_attr
218 self.__selected = False 222 self._selected = False
219 self.__was_focused = False 223 self._was_focused = False
220 self.header = header 224 self.header = header
221 self.text = text 225 self.text = text
222 urwid.WidgetWrap.__init__(self, urwid.Text("",align=align)) 226 urwid.WidgetWrap.__init__(self, urwid.Text("",align=align))
223 self.setSelectedText(selected_text) 227 self.setSelectedText(selected_text)
224 self.setState(selected) 228 self.setState(selected)
240 return self.getValue() 244 return self.getValue()
241 245
242 def set_text(self, text): 246 def set_text(self, text):
243 """/!\ set_text doesn't change self.selected_txt !""" 247 """/!\ set_text doesn't change self.selected_txt !"""
244 self.text = text 248 self.text = text
245 self.setState(self.__selected,invisible=True) 249 self.setState(self._selected,invisible=True)
246 250
247 def setSelectedText(self, text=None): 251 def setSelectedText(self, text=None):
248 """Text to display when selected 252 """Text to display when selected
249 @text: text as in urwid.Text or None for default value""" 253 @text: text as in urwid.Text or None for default value"""
250 if text == None: 254 if text == None:
251 text = ('selected',self.getValue()) 255 text = ('selected',self.getValue())
252 self.selected_txt = text 256 self.selected_txt = text
253 if self.__selected: 257 if self._selected:
254 self.setState(self.__selected) 258 self.setState(self._selected)
255 259
256 def __set_txt(self): 260 def _set_txt(self):
257 txt_list = [self.header] 261 txt_list = [self.header]
258 txt = self.selected_txt if self.__selected else self.text 262 txt = self.selected_txt if self._selected else self.text
259 if isinstance(txt,list): 263 if isinstance(txt,list):
260 txt_list.extend(txt) 264 txt_list.extend(txt)
261 else: 265 else:
262 txt_list.append(txt) 266 txt_list.append(txt)
263 self._w.base_widget.set_text(txt_list) 267 self._w.base_widget.set_text(txt_list)
264 268
265 269
266 def setState(self, selected, invisible=False): 270 def setState(self, selected, invisible=False):
267 """Change state 271 """Change state
272
268 @param selected: boolean state value 273 @param selected: boolean state value
269 @param invisible: don't emit change signal if True""" 274 @param invisible: don't emit change signal if True
270 assert(type(selected)==bool) 275 """
271 self.__selected=selected 276 assert type(selected)==bool
272 self.__set_txt() 277 self._selected=selected
273 self.__was_focused = False 278 self._set_txt()
279 self._was_focused = False
274 self._invalidate() 280 self._invalidate()
275 if not invisible: 281 if not invisible:
276 self._emit("change", self.__selected) 282 self._emit("change", self._selected)
277 283
278 def getState(self): 284 def getState(self):
279 return self.__selected 285 return self._selected
280 286
281 def selectable(self): 287 def selectable(self):
282 return True 288 return True
283 289
284 def keypress(self, size, key): 290 def keypress(self, size, key):
285 if key in (a_key['TEXT_SELECT'], a_key['TEXT_SELECT2']): 291 if key in (a_key['TEXT_SELECT'], a_key['TEXT_SELECT2']):
286 self.setState(not self.__selected) 292 self.setState(not self._selected)
287 else: 293 else:
288 return key 294 return key
289 295
290 def mouse_event(self, size, event, button, x, y, focus): 296 def mouse_event(self, size, event, button, x, y, focus):
291 if is_mouse_press(event) and button == 1: 297 if is_mouse_press(event) and button == 1:
292 self.setState(not self.__selected) 298 self.setState(not self._selected)
293 return True 299 return True
294 300
295 return False 301 return False
296 302
297 def render(self, size, focus=False): 303 def render(self, size, focus=False):
298 attr_list = self._w.base_widget._attrib 304 attr_list = self._w.base_widget._attrib
299 if not focus: 305 if not focus:
300 if self.__was_focused: 306 if self._was_focused:
301 self.__set_txt() 307 self._set_txt()
302 self.__was_focused = False 308 self._was_focused = False
303 else: 309 else:
304 if not self.__was_focused: 310 if not self._was_focused:
305 if not attr_list: 311 if not attr_list:
306 attr_list.append((self.focus_attr,len(self._w.base_widget.text))) 312 attr_list.append((self.focus_attr,len(self._w.base_widget.text)))
307 else: 313 else:
308 for idx in range(len(attr_list)): 314 for idx in range(len(attr_list)):
309 attr,attr_len = attr_list[idx] 315 attr,attr_len = attr_list[idx]
313 else: 319 else:
314 if not attr.endswith('_focus'): 320 if not attr.endswith('_focus'):
315 attr+="_focus" 321 attr+="_focus"
316 attr_list[idx] = (attr,attr_len) 322 attr_list[idx] = (attr,attr_len)
317 self._w.base_widget._invalidate() 323 self._w.base_widget._invalidate()
318 self.__was_focused = True #bloody ugly hack :) 324 self._was_focused = True #bloody ugly hack :)
319 return self._w.render(size, focus) 325 return self._w.render(size, focus)
320 326
321 327
322 class SelectableText(AlwaysSelectableText): 328 class SelectableText(AlwaysSelectableText):
323 """Like AlwaysSelectableText but not selectable when text is empty""" 329 """Like AlwaysSelectableText but not selectable when text is empty"""
335 self._emit('click') 341 self._emit('click')
336 342
337 343
338 class CustomButton(ClickableText): 344 class CustomButton(ClickableText):
339 345
340 def __init__(self, label, on_press=None, user_data=None, left_border = "[ ", right_border = " ]"): 346 def __init__(self, label, on_press=None, user_data=None, left_border="[ ", right_border=" ]", align="left"):
341 self.label = label 347 self.label = label
342 self.left_border = left_border 348 self.left_border = left_border
343 self.right_border = right_border 349 self.right_border = right_border
344 super(CustomButton, self).__init__([left_border, label, right_border]) 350 super(CustomButton, self).__init__([left_border, label, right_border], align=align)
345 self.size = len(self.get_text()) 351 self.size = len(self.get_text())
346 if on_press: 352 if on_press:
347 urwid.connect_signal(self, 'click', on_press, user_data) 353 urwid.connect_signal(self, 'click', on_press, user_data)
348 354
349 def getSize(self): 355 def getSize(self):
357 self.label = label 363 self.label = label
358 self.set_text([self.left_border, label, self.right_border]) 364 self.set_text([self.left_border, label, self.right_border])
359 365
360 366
361 class ListOption(unicode): 367 class ListOption(unicode):
362 """ Class similar to unicode, but which make the difference between value and label 368 """Unicode which manage label and value
369
370 This class similar to unicode, but which make the difference between value and label
363 label is show when use as unicode, the .value attribute contain the actual value 371 label is show when use as unicode, the .value attribute contain the actual value
364 Can be initialised with: 372 Can be initialised with:
365 - basestring (label = value = given string) 373 - basestring (label = value = given string)
366 - a tuple with (value, label) 374 - a tuple with (value, label)
367 XXX: comparaison is made against value, not the label which is the one displayed 375 XXX: comparaison is made against value, not the label which is the one displayed
368
369 """ 376 """
370 377
371 def __new__(cls, option): 378 def __new__(cls, option):
372 if (isinstance(option, cls)): 379 if (isinstance(option, cls)):
373 return option 380 return option
444 self.__size_cache = size 451 self.__size_cache = size
445 self.__focus_cache = focus 452 self.__focus_cache = focus
446 return super(UnselectableListBox, self).render(size, focus) 453 return super(UnselectableListBox, self).render(size, focus)
447 454
448 455
449 class GenericList(urwid.WidgetWrap): 456 class GenericList(urwid.ListBox):
450 signals = ['click','change'] 457 signals = ['click','change']
451 458
452 def __init__(self, options, style=None, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None): 459 def __init__(self, options, style=None, align='left', option_type = SelectableText, on_click=None, on_change=None, user_data=None):
453 """ 460 """Widget managing list of string and their selection
454 Widget managing list of string and their selection 461
455 @param options: list of strings used for options 462 @param options: list of strings used for options
456 @param style: list of string: 463 @param style: list of string:
457 - 'single' if only one must be selected 464 - 'single' if only one must be selected
458 - 'no_first_select' nothing selected when list is first displayed 465 - 'no_first_select' nothing selected when list is first displayed
459 - 'can_select_none' if we can select nothing 466 - 'can_select_none' if we can select nothing
475 482
476 if on_change: 483 if on_change:
477 urwid.connect_signal(self, 'change', on_change, user_data) 484 urwid.connect_signal(self, 'change', on_change, user_data)
478 485
479 self.content = urwid.SimpleListWalker([]) 486 self.content = urwid.SimpleListWalker([])
480 self.list_box = urwid.ListBox(self.content) 487 super(GenericList, self).__init__(self.content)
481 urwid.WidgetWrap.__init__(self, self.list_box)
482 self.changeValues(options) 488 self.changeValues(options)
483 489
484 def __onStateChange(self, widget, selected): 490 def _onStateChange(self, widget, selected):
485 if self.single: 491 if self.single:
486 if not selected and not self.can_select_none: 492 if not selected and not self.can_select_none:
487 #if in single mode, it's forbidden to unselect a value 493 #if in single mode, it's forbidden to unselect a value
488 widget.setState(True, invisible=True) 494 widget.setState(True, invisible=True)
489 return 495 return
490 if selected: 496 if selected:
491 self.unselectAll(invisible=True) 497 self.unselectAll(invisible=True)
492 widget.setState(True, invisible=True) 498 widget.setState(True, invisible=True)
493 self._emit("change") 499 self._emit("change")
494 500
495 def __onClick(self, widget): 501 def _onClick(self, widget):
496 self._emit("click", widget) 502 self._emit("click", widget)
497 503
498 def unselectAll(self, invisible=False): 504 def unselectAll(self, invisible=False):
499 for widget in self.content: 505 for widget in self.content:
500 if widget.getState(): 506 if widget.getState():
524 result = [] 530 result = []
525 for widget in self.content: 531 for widget in self.content:
526 if widget.getState(): 532 if widget.getState():
527 result.append(widget.getValue()) 533 result.append(widget.getValue())
528 return result 534 return result
529
530 def getDisplayWidget(self):
531 return self.list_box
532 535
533 def changeValues(self, new_values): 536 def changeValues(self, new_values):
534 """Change all values in one shot""" 537 """Change all values in one shot"""
535 new_values = ListOption.fromOptions(new_values) 538 new_values = ListOption.fromOptions(new_values)
536 if not self.first_display: 539 if not self.first_display:
539 for option in new_values: 542 for option in new_values:
540 widget = self.option_type(option, self.align) 543 widget = self.option_type(option, self.align)
541 if not self.first_display and option in old_selected: 544 if not self.first_display and option in old_selected:
542 widget.setState(True) 545 widget.setState(True)
543 widgets.append(widget) 546 widgets.append(widget)
544 for signal, callback in (('change', self.__onStateChange), ('click', self.__onClick)): 547 for signal, callback in (('change', self._onStateChange), ('click', self._onClick)):
545 try: 548 try:
546 urwid.connect_signal(widget, signal, callback) 549 urwid.connect_signal(widget, signal, callback)
547 except NameError: 550 except NameError:
548 pass #the widget given doesn't support the signal 551 pass #the widget given doesn't support the signal
549 self.content[:] = widgets 552 self.content[:] = widgets
550 if self.first_display and self.single and new_values and not self.no_first_select: 553 if self.first_display and self.single and new_values and not self.no_first_select:
551 self.content[0].setState(True) 554 self.content[0].setState(True)
552 display_widget = self.getDisplayWidget()
553 self._set_w(display_widget)
554 self._emit('change') 555 self._emit('change')
555 self.first_display = False 556 self.first_display = False
556 557
557 def selectValue(self, value, move_focus=True): 558 def selectValue(self, value, move_focus=True):
558 """Select the first item which has the given value. 559 """Select the first item which has the given value.
559 560
560 @param value 561 @param value
561 @param move_focus (boolean): True to move the focus on the selected value, 562 @param move_focus (bool):
562 False to leave the focus position unchanged. 563 - True to move the focus on the selected value,
564 - False to leave the focus position unchanged.
565
563 """ 566 """
564 self.unselectAll() 567 self.unselectAll()
565 idx = 0 568 idx = 0
566 for widget in self.content: 569 for widget in self.content:
567 if widget.getValue() == value: 570 if widget.getValue() == value:
568 widget.setState(True) 571 widget.setState(True)
569 if move_focus: 572 if move_focus:
570 self.list_box.focus_position = idx 573 self.focus_position = idx
571 return 574 return
572 idx+=1 575 idx+=1
573 576
574 def selectValues(self, values, move_focus=True): 577 def selectValues(self, values, move_focus=True):
575 """Select all the given values. 578 """Select all the given values.
587 idx = 0 590 idx = 0
588 for widget in self.content: 591 for widget in self.content:
589 if widget.getValue() == value: 592 if widget.getValue() == value:
590 widget.setState(True) 593 widget.setState(True)
591 if move_focus: 594 if move_focus:
592 self.list_box.focus_position = idx 595 self.focus_position = idx
593 idx += 1 596 idx += 1
594 597
595 598
596 class List(urwid.Widget): 599 class List(urwid.Widget):
597 """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'""" 600 """FlowWidget list, same arguments as GenericList, with an additional one 'max_height'"""
866 869
867 def onCategoryClick(self, button): 870 def onCategoryClick(self, button):
868 self.__buildOverlay(button.get_label(), 871 self.__buildOverlay(button.get_label(),
869 self.x_orig + self._w.base_widget.getStartCol(button)) 872 self.x_orig + self._w.base_widget.getStartCol(button))
870 873
874 MenuItem = collections.namedtuple('MenuItem', ('name', 'widget'))
871 875
872 class MenuRoller(urwid.WidgetWrap): 876 class MenuRoller(urwid.WidgetWrap):
873 877
874 def __init__(self,menus_list): 878 def __init__(self, menus_list):
875 """Create a MenuRoller 879 """Create a MenuRoller
876 @param menus_list: list of tuple with (name, Menu_instance), name can be None 880
881 @param menus_list: list of tuples which can be either:
882 - (name, Menu instance)
883 - (name, Menu instance, id)
877 """ 884 """
878 assert (menus_list) 885 assert menus_list
879 self.selected = 0 886 self.selected = None
880 self.name_list = [] 887 self.menu_items = collections.OrderedDict()
881 self.menus = {}
882 888
883 self.columns = urwid.Columns([urwid.Text(''),urwid.Text('')]) 889 self.columns = urwid.Columns([urwid.Text(''),urwid.Text('')])
884 urwid.WidgetWrap.__init__(self, self.columns) 890 urwid.WidgetWrap.__init__(self, self.columns)
885 891
886 for menu_tuple in menus_list: 892 for menu_tuple in menus_list:
887 name,menu = menu_tuple 893 try:
888 self.addMenu(name, menu) 894 name, menu, id_ = menu_tuple
895 except ValueError:
896 name, menu = menu_tuple
897 id_ = None
898 self.addMenu(name, menu, id_)
889 899
890 def _showSelected(self): 900 def _showSelected(self):
891 """show menu selected""" 901 """show menu selected"""
892 name_txt = u'\u21c9 '+self.name_list[self.selected]+u' \u21c7 ' 902 if self.selected is None:
893 current_name = ClickableText(name_txt) 903 self.columns.contents[0] = (urwid.Text(''), ('given', 0, False))
894 name_len = len(name_txt) 904 self.columns.contents[1] = (urwid.Text(''), ('weight', 1, False))
895 current_menu = self.menus[self.name_list[self.selected]] 905 else:
896 current_menu.setOrigX(name_len) 906 menu_item = self.menu_items[self.selected]
897 self.columns.contents[0] = (current_name, ('given', name_len, False)) 907 name_txt = u'\u21c9 ' + menu_item.name + u' \u21c7 '
898 self.columns.contents[1] = (current_menu, ('weight', 1, False)) 908 current_name = ClickableText(name_txt)
909 name_len = len(name_txt)
910 current_menu = menu_item.widget
911 current_menu.setOrigX(name_len)
912 self.columns.contents[0] = (current_name, ('given', name_len, False))
913 self.columns.contents[1] = (current_menu, ('weight', 1, False))
899 914
900 def keypress(self, size, key): 915 def keypress(self, size, key):
916 menu_ids = self.menu_items.keys()
917 try:
918 idx = menu_ids.index(self.selected)
919 except ValueError:
920 return super(MenuRoller, self).keypress(size, key)
921
901 if key==a_key['MENU_ROLLER_UP']: 922 if key==a_key['MENU_ROLLER_UP']:
902 if self.columns.get_focus_column()==0: 923 if self.columns.get_focus_column()==0:
903 if self.selected > 0: 924 if idx > 0:
904 self.selected -= 1 925 self.selected = menu_ids[idx-1]
905 self._showSelected() 926 self._showSelected()
906 return 927 return
907 elif key==a_key['MENU_ROLLER_DOWN']: 928 elif key==a_key['MENU_ROLLER_DOWN']:
908 if self.columns.get_focus_column()==0: 929 if self.columns.get_focus_column()==0:
909 if self.selected < len(self.name_list)-1: 930 if idx < len(menu_ids)-1:
910 self.selected += 1 931 self.selected = menu_ids[idx+1]
911 self._showSelected() 932 self._showSelected()
912 return 933 return
913 elif key==a_key['MENU_ROLLER_RIGHT']: 934 elif key==a_key['MENU_ROLLER_RIGHT']:
914 if self.columns.get_focus_column()==0 and \ 935 if self.columns.get_focus_column()==0 and \
915 (isinstance(self.columns.contents[1][0], urwid.Text) or \ 936 (isinstance(self.columns.contents[1][0], urwid.Text) or \
916 self.menus[self.name_list[self.selected]].getMenuSize()==0): 937 self.menu_items[self.selected].widget.getMenuSize()==0):
917 return #if we have no menu or the menu is empty, we don't go the right column 938 return #if we have no menu or the menu is empty, we don't go the right column
918 939
919 return super(MenuRoller, self).keypress(size, key) 940 return super(MenuRoller, self).keypress(size, key)
920 941
921 def addMenu(self, name_param, menu): 942 def addMenu(self, name, widget, menu_id=None):
922 name = name_param or '' 943 """Add a menu
923 if name not in self.name_list: 944
924 self.name_list.append(name) 945 @param name: name of the menu to add, it name already exists, menu is not added
925 self.menus[name] = menu 946 @param widget: instance of Menu
926 if self.name_list[self.selected] == name: 947 @param menu_id: id to use of this menu, or None to generate
948 @return: menu_id
949 """
950 names = {menu_item.name: id_ for id_, menu_item in self.menu_items.iteritems()}
951
952 if name not in names:
953 id_ = menu_id or str(uuid.uuid4())
954 if id_ in self.menu_items:
955 raise ValueError('Conflict: the id [{}] is already used'.format(id_))
956 self.menu_items[id_] = MenuItem(name, widget)
957 else:
958 id_ = names[name]
959 menu_item = self.menu_items[id_]
960 if menu_item.widget is not widget:
961 raise ValueError("The menu with id [{}] exists and doesn't contain the given instance. Use replaceMenu if you want to change the menu.".format(id_))
962 if self.selected is None:
963 self.selected = id_
964 self._showSelected()
965 return id_
966
967 def replaceMenu(self, name, widget, menu_id):
968 """Add a menu or replace it if the id already exists
969
970 @param name: name of the menu to add, it name already exists, menu is not added
971 @param widget: instance of Menu
972 @param menu_id: id or the menu
973 """
974 assert menu_id is not None
975 if menu_id in self.menu_items:
976 del self.menu_items[menu_id]
977 self.addMenu(name, widget, menu_id)
978 if self.selected == menu_id:
927 self._showSelected() #if we are on the menu, we update it 979 self._showSelected() #if we are on the menu, we update it
928 980
929 def removeMenu(self, name): 981 def removeMenu(self, menu_id):
930 if name in self.name_list: 982 del self.menu_items[menu_id]
931 self.name_list.remove(name) 983 if self.selected == menu_id:
932 if name in self.menus.keys(): 984 self.selected = self.menu_items.items[0] if self.menu_items else None
933 del self.menus[name] 985 self._showSelected()
934 self.selected = 0
935 self._showSelected()
936 986
937 def checkShortcuts(self, key): 987 def checkShortcuts(self, key):
938 for menu in self.name_list: 988 for menu_item in self.menu_items.values():
939 key = self.menus[menu].checkShortcuts(key) 989 key = menu_item.widget.checkShortcuts(key)
940 return key 990 return key
941 991
942 992
943 ## DIALOGS ## 993 ## DIALOGS ##
944 994