widgets.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. #
  2. # Copyright (c) Daniel Sheffield 2021
  3. #
  4. # All rights reserved
  5. #
  6. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  7. from decimal import Decimal
  8. from typing import Callable, Iterable, Union
  9. import urwid
  10. from urwid import numedit
  11. from urwid.wimp import PopUpLauncher
  12. class AutoCompleteEdit(urwid.Edit):
  13. signals = [ *urwid.Edit.signals, 'apply', 'open' ]
  14. def __init__(self, name: str, *args: Iterable, **kwargs):
  15. if isinstance(name, tuple):
  16. pallete, title = name
  17. self.name = title
  18. title = title.title()
  19. passthrough = (pallete, u' ' if title.lower() == 'unit' else u'')
  20. else:
  21. self.name = name
  22. title = name.title()
  23. passthrough = u' ' if name.lower() == 'unit' else u''
  24. super().__init__(passthrough, *args, **kwargs)
  25. def keypress(self, size, key):
  26. if key == 'enter':
  27. self._emit('apply', self.name)
  28. return
  29. elif key == 'delete':
  30. self.set_edit_text('')
  31. return
  32. return super().keypress(size, key)
  33. class AutoCompleteFloatEdit(numedit.FloatEdit):
  34. signals = [ *urwid.Edit.signals, 'apply', 'open' ]
  35. def __init__(self, name: str, *args: Iterable, **kwargs: dict):
  36. self.last_val = None
  37. self.op = '='
  38. self.pallete = None
  39. if isinstance(name, tuple):
  40. self.pallete, self.name = name
  41. title = self.name.title()
  42. passthrough = (self.pallete, f'{self.op} ')
  43. else:
  44. self.name = name
  45. title = name.title()
  46. passthrough = title
  47. super().__init__(passthrough, *args, **kwargs)
  48. def update_caption(self):
  49. if self.pallete is not None:
  50. self.set_caption((self.pallete, f'{self.op} '))
  51. else:
  52. self.set_caption(f'{self.op} ')
  53. def set_op(self, op):
  54. self.op = op
  55. self.last_val = self.value()
  56. self.set_edit_text('')
  57. self.update_caption()
  58. def calc(self):
  59. x = self.last_val
  60. op = self.op
  61. if op in ('+', '-',):
  62. y = self.value() or Decimal(0.0)
  63. if op == '+':
  64. z = x + y
  65. else:
  66. z = x - y
  67. elif op in ('*', '/'):
  68. y = self.value() or Decimal(1.0)
  69. if op == '*':
  70. z = x * y
  71. else:
  72. z = x / y
  73. else:
  74. y = self.value() or Decimal(0.0)
  75. z = y
  76. self.op = '='
  77. self.update_caption()
  78. self.set_edit_text(f'{z:.2f}')
  79. def keypress(self, size, key):
  80. if isinstance(key, tuple):
  81. return
  82. ops = ('+', '-', '*', '/',)
  83. if key in ops:
  84. if self.op in ops:
  85. self.calc()
  86. self.set_op(key)
  87. return
  88. elif key == 'enter':
  89. if self.get_edit_text() == '' or self.value() == Decimal(0.0):
  90. self._emit('open', self.name)
  91. return
  92. self.calc()
  93. return
  94. elif key == '=':
  95. self.calc()
  96. return
  97. elif key == 'delete':
  98. self.set_edit_text('')
  99. self.op = '='
  100. self.update_caption()
  101. return
  102. return super().keypress(size, key)
  103. class NoTabCheckBox(urwid.CheckBox):
  104. def keypress(self, size, key):
  105. if not isinstance(key, tuple) and key == 'tab':
  106. return
  107. else:
  108. return super().keypress(size, key)
  109. class FocusWidget(urwid.WidgetPlaceholder):
  110. def __init__(self, widget, initial_focus, skip_focus):
  111. super().__init__(widget)
  112. self._initial_focus = tuple([ i for i in initial_focus ])
  113. self._skip_focus = tuple([ i for i in skip_focus ])
  114. @property
  115. def skip_focus(self):
  116. return self._skip_focus
  117. @property
  118. def initial_focus(self):
  119. return list(self._initial_focus)
  120. @property
  121. def container(self):
  122. return self.original_widget.original_widget.original_widget
  123. def _set_focus_path(self, path):
  124. try:
  125. self.container.set_focus_path(path)
  126. return
  127. except IndexError:
  128. pass
  129. if path[-1] == 0 and len(path) > 1:
  130. self._set_focus_path(path[:-1])
  131. return
  132. raise IndexError
  133. def iter_focus_paths(self):
  134. self._set_focus_path(self.initial_focus)
  135. while True:
  136. path = self.container.get_focus_path()
  137. yield path
  138. self.advance_focus()
  139. path = self.container.get_focus_path()
  140. while len(path) < len(self.initial_focus):
  141. path.extend([0])
  142. if path == self.initial_focus:
  143. return
  144. def advance_focus(self, reverse=False):
  145. path = self.container.get_focus_path()
  146. if reverse:
  147. paths = [ i for i in self.iter_focus_paths() ]
  148. zipped_paths = zip(paths, [
  149. *paths[1:], paths[0]
  150. ])
  151. prev_path = map(lambda x: x[0], filter(
  152. lambda x: x[1] == path,
  153. zipped_paths
  154. ))
  155. p = next(prev_path)
  156. self._set_focus_path(p)
  157. return
  158. _iter = [ i for i in enumerate(path) ][::-1]
  159. for idx, part in _iter:
  160. p = [ i for i in path ]
  161. if reverse:
  162. p[idx] -= 1
  163. else:
  164. p[idx] += 1
  165. try:
  166. self._set_focus_path(p)
  167. if p in self.skip_focus:
  168. self.advance_focus(reverse=reverse)
  169. return
  170. except IndexError:
  171. path[idx] = 0
  172. while len(path) < len(self.initial_focus):
  173. path.extend([0])
  174. self._set_focus_path(self.initial_focus)
  175. class AutoCompletePopUp(PopUpLauncher):
  176. def __init__(self,
  177. widget: Union[AutoCompleteEdit, AutoCompleteFloatEdit],
  178. apply_choice_cb: Callable[[str, str], None]
  179. ):
  180. super().__init__(widget)
  181. self.apply_choice_cb = apply_choice_cb
  182. urwid.connect_signal(self._original_widget, 'open', lambda _, options: self._open_pop_up(options))
  183. def _open_pop_up(self, options):
  184. self.options = options
  185. self.open_pop_up()
  186. def create_pop_up(self):
  187. pop_up = SuggestionPopup(
  188. self._original_widget.name, self.options,
  189. self.apply_choice_cb,
  190. )
  191. urwid.connect_signal(pop_up, 'close',
  192. lambda _: self.close_pop_up())
  193. return pop_up
  194. def get_pop_up_parameters(self):
  195. return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height': 10}
  196. class SuggestionPopup(urwid.WidgetWrap):
  197. signals = ['close']
  198. def __init__(self,
  199. name: str,
  200. options: Iterable,
  201. apply_cb: Callable[[str, str], None]
  202. ):
  203. self.apply_cb = lambda _, v: apply_cb(name, v)
  204. body = []
  205. for c in options:
  206. button = urwid.Button(c)
  207. urwid.connect_signal(button, 'click', self.apply_cb, c)
  208. urwid.connect_signal(button, 'click', lambda _: self._emit("close"))
  209. body.append(urwid.AttrMap(button, None, focus_map='reversed'))
  210. walker = urwid.SimpleFocusListWalker(body, wrap_around=False)
  211. listbox = urwid.ListBox(walker)
  212. super().__init__(urwid.AttrWrap(listbox, 'banner'))
  213. def keypress(self, size, key):
  214. if key == 'esc':
  215. self._emit("close")
  216. return
  217. if key == 'tab':
  218. return
  219. return super().keypress(size, key)