# # Copyright (c) Daniel Sheffield 2021 # # All rights reserved # # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY from decimal import Decimal from typing import Callable, Iterable, Union import urwid from urwid import numedit from urwid.wimp import PopUpLauncher class AutoCompleteEdit(urwid.Edit): signals = [ *urwid.Edit.signals, 'apply', 'open' ] def __init__(self, name: str, *args: Iterable, **kwargs): if isinstance(name, tuple): pallete, title = name self.name = title title = title.title() passthrough = (pallete, u' ' if title.lower() == 'unit' else u'') else: self.name = name title = name.title() passthrough = u' ' if name.lower() == 'unit' else u'' super().__init__(passthrough, *args, **kwargs) def keypress(self, size, key): if key == 'enter': self._emit('apply', self.name) return elif key == 'delete': self.set_edit_text('') return return super().keypress(size, key) class AutoCompleteFloatEdit(numedit.FloatEdit): signals = [ *urwid.Edit.signals, 'apply', 'open' ] def __init__(self, name: str, *args: Iterable, **kwargs: dict): self.last_val = None self.op = '=' self.pallete = None if isinstance(name, tuple): self.pallete, self.name = name title = self.name.title() passthrough = (self.pallete, f'{self.op} ') else: self.name = name title = name.title() passthrough = title super().__init__(passthrough, *args, **kwargs) def update_caption(self): if self.pallete is not None: self.set_caption((self.pallete, f'{self.op} ')) else: self.set_caption(f'{self.op} ') def set_op(self, op): self.op = op self.last_val = self.value() self.set_edit_text('') self.update_caption() def calc(self): x = self.last_val op = self.op if op in ('+', '-',): y = self.value() or Decimal(0.0) if op == '+': z = x + y else: z = x - y elif op in ('*', '/'): y = self.value() or Decimal(1.0) if op == '*': z = x * y else: z = x / y else: y = self.value() or Decimal(0.0) z = y self.op = '=' self.update_caption() self.set_edit_text(f'{z:.2f}') def keypress(self, size, key): if isinstance(key, tuple): return ops = ('+', '-', '*', '/',) if key in ops: if self.op in ops: self.calc() self.set_op(key) return elif key == 'enter': if self.get_edit_text() == '' or self.value() == Decimal(0.0): self._emit('open', self.name) return self.calc() return elif key == '=': self.calc() return elif key == 'delete': self.set_edit_text('') self.op = '=' self.update_caption() return return super().keypress(size, key) class NoTabCheckBox(urwid.CheckBox): def keypress(self, size, key): if not isinstance(key, tuple) and key == 'tab': return else: return super().keypress(size, key) class FocusWidget(urwid.WidgetPlaceholder): def __init__(self, widget, initial_focus, skip_focus): super().__init__(widget) self._initial_focus = tuple([ i for i in initial_focus ]) self._skip_focus = tuple([ i for i in skip_focus ]) @property def skip_focus(self): return self._skip_focus @property def initial_focus(self): return list(self._initial_focus) @property def container(self): return self.original_widget.original_widget.original_widget def _set_focus_path(self, path): try: self.container.set_focus_path(path) return except IndexError: pass if path[-1] == 0 and len(path) > 1: self._set_focus_path(path[:-1]) return raise IndexError def iter_focus_paths(self): self._set_focus_path(self.initial_focus) while True: path = self.container.get_focus_path() yield path self.advance_focus() path = self.container.get_focus_path() while len(path) < len(self.initial_focus): path.extend([0]) if path == self.initial_focus: return def advance_focus(self, reverse=False): path = self.container.get_focus_path() if reverse: paths = [ i for i in self.iter_focus_paths() ] zipped_paths = zip(paths, [ *paths[1:], paths[0] ]) prev_path = map(lambda x: x[0], filter( lambda x: x[1] == path, zipped_paths )) p = next(prev_path) self._set_focus_path(p) return _iter = [ i for i in enumerate(path) ][::-1] for idx, part in _iter: p = [ i for i in path ] if reverse: p[idx] -= 1 else: p[idx] += 1 try: self._set_focus_path(p) if p in self.skip_focus: self.advance_focus(reverse=reverse) return except IndexError: path[idx] = 0 while len(path) < len(self.initial_focus): path.extend([0]) self._set_focus_path(self.initial_focus) class AutoCompletePopUp(PopUpLauncher): def __init__(self, widget: Union[AutoCompleteEdit, AutoCompleteFloatEdit], apply_choice_cb: Callable[[str, str], None] ): super().__init__(widget) self.apply_choice_cb = apply_choice_cb urwid.connect_signal(self._original_widget, 'open', lambda _, options: self._open_pop_up(options)) def _open_pop_up(self, options): self.options = options self.open_pop_up() def create_pop_up(self): pop_up = SuggestionPopup( self._original_widget.name, self.options, self.apply_choice_cb, ) urwid.connect_signal(pop_up, 'close', lambda _: self.close_pop_up()) return pop_up def get_pop_up_parameters(self): return {'left':0, 'top':1, 'overlay_width':32, 'overlay_height': 10} class SuggestionPopup(urwid.WidgetWrap): signals = ['close'] def __init__(self, name: str, options: Iterable, apply_cb: Callable[[str, str], None] ): self.apply_cb = lambda _, v: apply_cb(name, v) body = [] for c in options: button = urwid.Button(c) urwid.connect_signal(button, 'click', self.apply_cb, c) urwid.connect_signal(button, 'click', lambda _: self._emit("close")) body.append(urwid.AttrMap(button, None, focus_map='reversed')) walker = urwid.SimpleFocusListWalker(body, wrap_around=False) listbox = urwid.ListBox(walker) super().__init__(urwid.AttrWrap(listbox, 'banner')) def keypress(self, size, key): if key == 'esc': self._emit("close") return if key == 'tab': return return super().keypress(size, key)