Mercurial > urwid-satext
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 |