# # libtui - TUI framework using ncurses # (c) 2024 Lunaixsky # # I sware, this is the last time I ever touch # any sort of the GUI messes. # import curses import re import curses.panel as cpanel import curses.textpad as textpad import textwrap def __invoke_fn(obj, fn, *args): try: fn(*args) except: _id = obj._id if obj else "" raise Exception(_id, str(fn), args) def resize_safe(obj, co, y, x): __invoke_fn(obj, co.resize, y, x) def move_safe(obj, co, y, x): __invoke_fn(obj, co.move, y, x) def addstr_safe(obj, co, y, x, str, *args): __invoke_fn(obj, co.addstr, y, x, str, *args) class _TuiColor: def __init__(self, v) -> None: self.__v = v def __int__(self): return self.__v def bright(self): return self.__v + 8 class TuiColor: black = _TuiColor(curses.COLOR_BLACK) red = _TuiColor(curses.COLOR_RED) green = _TuiColor(curses.COLOR_GREEN) yellow = _TuiColor(curses.COLOR_YELLOW) blue = _TuiColor(curses.COLOR_BLUE) magenta = _TuiColor(curses.COLOR_MAGENTA) cyan = _TuiColor(curses.COLOR_CYAN) white = _TuiColor(curses.COLOR_WHITE) class Alignment: LEFT = 0b000001 RIGHT = 0b000010 CENTER = 0b000100 TOP = 0b001000 BOT = 0b010000 ABS = 0b000000 REL = 0b100000 class ColorScope: WIN = 1 PANEL = 2 TEXT = 3 TEXT_HI = 4 SHADOW = 5 SELECT = 6 HINT = 7 BOX = 8 class EventType: E_KEY = 0 E_REDRAW = 1 E_QUIT = 2 E_TASK = 3 E_CHFOCUS = 4 E_M_FOCUS = 0b10000000 def focused_only(t): return (t & EventType.E_M_FOCUS) def value(t): return t & ~EventType.E_M_FOCUS def key_press(t): return (t & ~EventType.E_M_FOCUS) == EventType.E_KEY class Matchers: RelSize = re.compile(r"(?P[0-9]+(?:\.[0-9]+)?)?\*(?P[+-][0-9]+)?") class BoundExpression: def __init__(self, expr = None): self._mult = 0 self._add = 0 if expr: self.update(expr) def set_pair(self, mult, add): self._mult = mult self._add = add def set(self, expr): self._mult = expr._mult self._add = expr._add def update(self, expr): if isinstance(expr, int): m = None else: m = Matchers.RelSize.match(expr) if m: g = m.groupdict() mult = 1 if not g["mult"] else float(g["mult"]) add = 0 if not g["add"] else int(g["add"]) self._mult = mult self._add = add else: self.set_pair(0, int(expr)) def calc(self, ref_val): return int(self._mult * ref_val + self._add) def absolute(self): return self._mult == 0 def nullity(self): return self._mult == 0 and self._add == 0 def scale_mult(self, scalar): self._mult *= scalar return self @staticmethod def normalise(*exprs): v = BoundExpression() for e in exprs: v += e return [e.scale_mult(1 / v._mult) for e in exprs] def __add__(self, b): v = BoundExpression() v.set(self) v._mult += b._mult v._add += b._add return v def __sub__(self, b): v = BoundExpression() v.set(self) v._mult -= b._mult v._add -= b._add return v def __iadd__(self, b): self._mult += b._mult self._add += b._add return self def __isub__(self, b): self._mult -= b._mult self._add -= b._add return self def __rmul__(self, scalar): v = BoundExpression() v.set(self) v._mult *= scalar v._add *= scalar return v def __truediv__(self, scalar): v = BoundExpression() v.set(self) v._mult /= float(scalar) v._add /= scalar return v class DynamicBound: def __init__(self): self.__x = BoundExpression() self.__y = BoundExpression() def dyn_x(self): return self.__x def dyn_y(self): return self.__y def resolve(self, ref_w, ref_h): return (self.__y.calc(ref_h), self.__x.calc(ref_w)) def set(self, dyn_bound): self.__x.set(dyn_bound.dyn_x()) self.__y.set(dyn_bound.dyn_y()) class Bound: def __init__(self) -> None: self.__x = 0 self.__y = 0 def shrinkX(self, dx): self.__x -= dx def shrinkY(self, dy): self.__y -= dy def growX(self, dx): self.__x += dx def growY(self, dy): self.__y += dy def resetX(self, x): self.__x = x def resetY(self, y): self.__y = y def update(self, dynsz, ref_bound): y, x = dynsz.resolve(ref_bound.x(), ref_bound.y()) self.__x = x self.__y = y def reset(self, x, y): self.__x, self.__y = x, y def x(self): return self.__x def y(self): return self.__y def yx(self, scale = 1): return int(self.__y * scale), int(self.__x * scale) class TuiStackWindow: def __init__(self, obj) -> None: self.__obj = obj self.__win = curses.newwin(0, 0) self.__pan = cpanel.new_panel(self.__win) self.__pan.hide() def resize(self, h, w): resize_safe(self.__obj, self.__win, h, w) def relocate(self, y, x): move_safe(self.__obj, self.__pan, y, x) def set_geometric(self, h, w, y, x): resize_safe(self.__obj, self.__win, h, w) move_safe(self.__obj, self.__pan, y, x) def set_background(self, color_scope): self.__win.bkgd(' ', curses.color_pair(color_scope)) def show(self): self.__pan.show() def hide(self): self.__pan.hide() def send_back(self): self.__pan.bottom() def send_front(self): self.__pan.top() def window(self): return self.__win class SpatialObject: def __init__(self) -> None: self._local_pos = DynamicBound() self._pos = Bound() self._dyn_size = DynamicBound() self._size = Bound() self._margin = (0, 0, 0, 0) self._padding = (0, 0, 0, 0) self._align = Alignment.TOP | Alignment.LEFT def set_local_pos(self, x, y): self._local_pos.dyn_x().update(x) self._local_pos.dyn_y().update(y) def set_alignment(self, align): self._align = align def set_size(self, w = None, h = None): if w: self._dyn_size.dyn_x().update(w) if h: self._dyn_size.dyn_y().update(h) def set_margin(self, top, right, bottom, left): self._margin = (top, right, bottom, left) def set_padding(self, top, right, bottom, left): self._padding = (top, right, bottom, left) def reset(self): self._pos.reset(0, 0) self._size.reset(0, 0) def deduce_spatial(self, constrain): self.reset() self.__satisfy_bound(constrain) self.__satisfy_alignment(constrain) self.__satisfy_margin(constrain) self.__satisfy_padding(constrain) self.__to_corner_pos(constrain) def __satisfy_alignment(self, constrain): local_pos = self._local_pos cbound = constrain._size size = self._size cy, cx = cbound.yx() ry, rx = local_pos.resolve(cx, cy) ay, ax = size.yx(0.5) if self._align & Alignment.CENTER: ax = cx // 2 ay = cy // 2 if self._align & Alignment.BOT: ay = min(cy - ay, cy - 1) ry = -ry elif self._align & Alignment.TOP: ay = size.y() // 2 if self._align & Alignment.RIGHT: ax = cx - ax rx = -rx elif self._align & Alignment.LEFT: ax = size.x() // 2 self._pos.reset(ax + rx, ay + ry) def __satisfy_margin(self, constrain): tm, lm, bm, rm = self._margin self._pos.growX(rm - lm) self._pos.growY(bm - tm) def __satisfy_padding(self, constrain): csize = constrain._size ch, cw = csize.yx() h, w = self._size.yx(0.5) y, x = self._pos.yx() tp, lp, bp, rp = self._padding if not (tp or lp or bp or rp): return dtp = min(y - h, tp) - tp dbp = min(ch - (y + h), bp) - bp dlp = min(x - w, lp) - lp drp = min(cw - (x + w), rp) - rp self._size.growX(drp + dlp) self._size.growY(dtp + dbp) def __satisfy_bound(self, constrain): self._size.update(self._dyn_size, constrain._size) def __to_corner_pos(self, constrain): h, w = self._size.yx(0.5) g_pos = constrain._pos self._pos.shrinkX(w) self._pos.shrinkY(h) self._pos.growX(g_pos.x()) self._pos.growY(g_pos.y()) class TuiObject(SpatialObject): def __init__(self, context, id): super().__init__() self._id = id self._context = context self._parent = None self._visible = True self._focused = False def set_parent(self, parent): self._parent = parent def canvas(self): if self._parent: return self._parent.canvas() return (self, self._context.window()) def context(self): return self._context def session(self): return self._context.session() def on_create(self): pass def on_destory(self): pass def on_quit(self): pass def on_layout(self): if self._parent: self.deduce_spatial(self._parent) def on_draw(self): pass def on_event(self, ev_type, ev_arg): pass def on_focused(self): self._focused = True def on_focus_lost(self): self._focused = False def set_visbility(self, visible): self._visible = visible def do_draw(self): if self._visible: self.on_draw() def do_layout(self): self.on_layout() class TuiWidget(TuiObject): def __init__(self, context, id): super().__init__(context, id) def on_layout(self): super().on_layout() co, _ = self.canvas() y, x = co._pos.yx() self._pos.shrinkX(x) self._pos.shrinkY(y) class TuiContainerObject(TuiObject): def __init__(self, context, id): super().__init__(context, id) self._children = [] def add(self, child): child.set_parent(self) self._children.append(child) def children(self): return self._children def on_create(self): super().on_create() for child in self._children: child.on_create() def on_destory(self): super().on_destory() for child in self._children: child.on_destory() def on_quit(self): super().on_quit() for child in self._children: child.on_quit() def on_layout(self): super().on_layout() for child in self._children: child.do_layout() def on_draw(self): super().on_draw() for child in self._children: child.do_draw() def on_event(self, ev_type, ev_arg): super().on_event(ev_type, ev_arg) for child in self._children: child.on_event(ev_type, ev_arg) class TuiScrollable(TuiObject): def __init__(self, context, id): super().__init__(context, id) self.__spos = Bound() self.__pad = curses.newpad(1, 1) self.__pad.bkgd(' ', curses.color_pair(ColorScope.PANEL)) self.__pad_panel = cpanel.new_panel(self.__pad) self.__content = None def canvas(self): return (self, self.__pad) def set_content(self, content): self.__content = content self.__content.set_parent(self) def set_scrollY(self, y): self.__spos.resetY(y) def set_scrollX(self, x): self.__spos.resetX(x) def reached_last(self): off = self.__spos.y() + self._size.y() return off >= self.__content._size.y() def reached_top(self): return self.__spos.y() < self._size.y() def on_layout(self): super().on_layout() if not self.__content: return self.__content.on_layout() h, w = self._size.yx() ch, cw = self.__content._size.yx() sh, sw = max(ch, h), max(cw, w) self.__spos.resetX(min(self.__spos.x(), max(cw, w) - w)) self.__spos.resetY(min(self.__spos.y(), max(ch, h) - h)) resize_safe(self, self.__pad, sh, sw) def on_draw(self): if not self.__content: return self.__pad.erase() self.__pad_panel.top() self.__content.on_draw() wminy, wminx = self._pos.yx() wmaxy, wmaxx = self._size.yx() wmaxy, wmaxx = wmaxy + wminy, wmaxx + wminx self.__pad.touchwin() self.__pad.refresh(*self.__spos.yx(), wminy, wminx, wmaxy - 1, wmaxx - 1) class Layout(TuiContainerObject): class Cell(TuiObject): def __init__(self, context): super().__init__(context, "cell") self.__obj = None def set_obj(self, obj): self.__obj = obj self.__obj.set_parent(self) def on_create(self): if self.__obj: self.__obj.on_create() def on_destory(self): if self.__obj: self.__obj.on_destory() def on_quit(self): if self.__obj: self.__obj.on_quit() def on_layout(self): super().on_layout() if self.__obj: self.__obj.do_layout() def on_draw(self): if self.__obj: self.__obj.do_draw() def on_event(self, ev_type, ev_arg): if self.__obj: self.__obj.on_event(ev_type, ev_arg) def __init__(self, context, id, ratios): super().__init__(context, id) rs = [BoundExpression(r) for r in ratios.split(',')] self._rs = BoundExpression.normalise(*rs) for _ in range(len(self._rs)): cell = Layout.Cell(self._context) super().add(cell) self._adjust_to_fit() def _adjust_to_fit(self): pass def add(self, child): raise RuntimeError("invalid operation") def set_cell(self, i, obj): if i > len(self._children): raise ValueError(f"cell #{i} out of bound") self._children[i].set_obj(obj) class FlexLinearLayout(Layout): LANDSCAPE = 0 PORTRAIT = 1 def __init__(self, context, id, ratios): self.__horizontal = False super().__init__(context, id, ratios) def orientation(self, orient): self.__horizontal = orient == FlexLinearLayout.LANDSCAPE def on_layout(self): self.__apply_ratio() super().on_layout() def _adjust_to_fit(self): sum_abs = BoundExpression() i = 0 for r in self._rs: if r.absolute(): sum_abs += r else: i += 1 sum_abs /= i for i, r in enumerate(self._rs): if not r.absolute(): self._rs[i] -= sum_abs def __apply_ratio(self): if self.__horizontal: self.__adjust_horizontal() else: self.__adjust_vertical() def __adjust_horizontal(self): acc = BoundExpression() for r, cell in zip(self._rs, self.children()): cell._dyn_size.dyn_y().set_pair(1, 0) cell._dyn_size.dyn_x().set(r) cell.set_alignment(Alignment.LEFT) cell._local_pos.dyn_y().set_pair(0, 0) cell._local_pos.dyn_x().set(acc) acc += r def __adjust_vertical(self): acc = BoundExpression() for r, cell in zip(self._rs, self.children()): cell._dyn_size.dyn_x().set_pair(1, 0) cell._dyn_size.dyn_y().set(r) cell.set_alignment(Alignment.TOP | Alignment.CENTER) cell._local_pos.dyn_x().set_pair(0, 0) cell._local_pos.dyn_y().set(acc) acc += r class TuiPanel(TuiContainerObject): def __init__(self, context, id): super().__init__(context, id) self.__use_border = False self.__use_shadow = False self.__shadow_param = (0, 0) self.__swin = TuiStackWindow(self) self.__shad = TuiStackWindow(self) self.__swin.set_background(ColorScope.PANEL) self.__shad.set_background(ColorScope.SHADOW) def canvas(self): return (self, self.__swin.window()) def drop_shadow(self, off_y, off_x): self.__shadow_param = (off_y, off_x) self.__use_shadow = not (off_y == off_x and off_y == 0) def border(self, _b): self.__use_border = _b def bkgd_override(self, scope): self.__swin.set_background(scope) def on_layout(self): super().on_layout() self.__swin.hide() h, w = self._size.y(), self._size.x() y, x = self._pos.y(), self._pos.x() self.__swin.set_geometric(h, w, y, x) if self.__use_shadow: sy, sx = self.__shadow_param self.__shad.set_geometric(h, w, y + sy, x + sx) def on_destory(self): super().on_destory() self.__swin.hide() self.__shad.hide() def on_draw(self): win = self.__swin.window() win.erase() if self.__use_border: win.border() if self.__use_shadow: self.__shad.show() else: self.__shad.hide() self.__swin.show() self.__swin.send_front() super().on_draw() win.touchwin() class TuiLabel(TuiWidget): def __init__(self, context, id): super().__init__(context, id) self._text = "TuiLabel" self._wrapped = [] self.__auto_fit = True self.__trunc = False self.__dopad = False self.__highlight = False self.__color_scope = -1 def __try_fit_text(self, txt): if self.__auto_fit: self._dyn_size.dyn_x().set_pair(0, len(txt)) self._dyn_size.dyn_y().set_pair(0, 1) def __pad_text(self): for i, t in enumerate(self._wrapped): self._wrapped[i] = str.rjust(t, self._size.x()) def set_text(self, text): self._text = text self.__try_fit_text(text) def override_color(self, color = -1): self.__color_scope = color def auto_fit(self, _b): self.__auto_fit = _b def truncate(self, _b): self.__trunc = _b def hightlight(self, _b): self.__highlight = _b def pad_around(self, _b): self.__dopad = _b def on_layout(self): txt = self._text if self.__dopad: txt = f" {txt} " self.__try_fit_text(txt) super().on_layout() if len(txt) <= self._size.x(): self._wrapped = [txt] self.__pad_text() return if not self.__trunc: txt = txt[:self._size.x() - 1] self._wrapped = [txt] self.__pad_text() return self._wrapped = textwrap.wrap(txt, self._size.x()) self.__pad_text() def on_draw(self): _, win = self.canvas() y, x = self._pos.yx() if self.__color_scope != -1: color = curses.color_pair(self.__color_scope) elif self.__highlight: color = curses.color_pair(ColorScope.TEXT_HI) else: color = curses.color_pair(ColorScope.TEXT) for i, t in enumerate(self._wrapped): addstr_safe(self, win, y + i, x, t, color) class TuiTextBlock(TuiWidget): def __init__(self, context, id): super().__init__(context, id) self.__lines = [] self.__wrapped = [] self.__fit_to_height = False def set_text(self, text): text = textwrap.dedent(text) self.__lines = text.split('\n') if self.__fit_to_height: self._dyn_size.dyn_y().set_pair(0, 0) def height_auto_fit(self, yes): self.__fit_to_height = yes def on_layout(self): super().on_layout() self.__wrapped.clear() for t in self.__lines: if not t: self.__wrapped.append(t) continue wrap = textwrap.wrap(t, self._size.x()) self.__wrapped += wrap if self._dyn_size.dyn_y().nullity(): h = len(self.__wrapped) self._dyn_size.dyn_y().set_pair(0, h) # redo layouting super().on_layout() def on_draw(self): _, win = self.canvas() y, x = self._pos.yx() color = curses.color_pair(ColorScope.TEXT) for i, t in enumerate(self.__wrapped): addstr_safe(self, win, y + i, x, t, color) class TuiTextBox(TuiWidget): def __init__(self, context, id): super().__init__(context, id) self.__box = TuiStackWindow(self) self.__box.set_background(ColorScope.PANEL) self.__textb = textpad.Textbox(self.__box.window(), True) self.__textb.stripspaces = True self.__str = "" self.__scheduled_edit = False self._context.focus_group().register(self, 0) def __validate(self, x): if x == 10 or x == 9: return 7 return x def on_layout(self): super().on_layout() co, _ = self.canvas() h, w = self._size.yx() y, x = self._pos.yx() cy, cx = co._pos.yx() y, x = y + cy, x + cx self.__box.hide() self.__box.set_geometric(1, w - 1, y + h // 2, x + 1) def on_draw(self): self.__box.show() self.__box.send_front() _, cwin = self.canvas() h, w = self._size.yx() y, x = self._pos.yx() textpad.rectangle(cwin, y, x, y + h - 1, x+w) win = self.__box.window() win.touchwin() def __edit(self): self.__str = self.__textb.edit(lambda x: self.__validate(x)) self.session().schedule(EventType.E_CHFOCUS) self.__scheduled_edit = False def get_text(self): return self.__str def on_focused(self): self.__box.set_background(ColorScope.BOX) if not self.__scheduled_edit: # edit will block, defer to next update cycle self.session().schedule_task(self.__edit) self.__scheduled_edit = True def on_focus_lost(self): self.__box.set_background(ColorScope.PANEL) class SimpleList(TuiWidget): class Item: def __init__(self) -> None: pass def get_text(self): return "list_item" def on_selected(self): pass def on_key_pressed(self, key): pass def __init__(self, context, id): super().__init__(context, id) self.__items = [] self.__selected = 0 self.__on_sel_confirm_cb = None self.__on_sel_change_cb = None self._context.focus_group().register(self) def set_onselected_cb(self, cb): self.__on_sel_confirm_cb = cb def set_onselection_change_cb(self, cb): self.__on_sel_change_cb = cb def count(self): return len(self.__items) def index(self): return self.__selected def add_item(self, item): self.__items.append(item) def clear(self): self.__items.clear() def on_layout(self): super().on_layout() self.__selected = min(self.__selected, len(self.__items)) self._size.resetY(len(self.__items) + 1) def on_draw(self): _, win = self.canvas() w = self._size.x() for i, item in enumerate(self.__items): color = curses.color_pair(ColorScope.TEXT) if i == self.__selected: if self._focused: color = curses.color_pair(ColorScope.SELECT) else: color = curses.color_pair(ColorScope.BOX) txt = str.ljust(item.get_text(), w) txt = txt[:w] addstr_safe(self, win, i, 0, txt, color) def on_event(self, ev_type, ev_arg): if not EventType.key_press(ev_type): return if len(self.__items) == 0: return sel = self.__items[self.__selected] if ev_arg == 10: sel.on_selected() if self.__on_sel_confirm_cb: self.__on_sel_confirm_cb(self, self.__selected, sel) return sel.on_key_pressed(ev_arg) if (ev_arg != curses.KEY_DOWN and ev_arg != curses.KEY_UP): return prev = self.__selected if ev_arg == curses.KEY_DOWN: self.__selected += 1 else: self.__selected -= 1 self.__selected = max(self.__selected, 0) self.__selected = self.__selected % len(self.__items) if self.__on_sel_change_cb: self.__on_sel_change_cb(self, prev, self.__selected) class TuiButton(TuiLabel): def __init__(self, context, id): super().__init__(context, id) self.__onclick = None context.focus_group().register(self) def set_text(self, text): return super().set_text(f"<{text}>") def set_click_callback(self, cb): self.__onclick = cb def hightlight(self, _b): raise NotImplemented() def on_draw(self): _, win = self.canvas() y, x = self._pos.yx() if self._focused: color = curses.color_pair(ColorScope.SELECT) else: color = curses.color_pair(ColorScope.TEXT) addstr_safe(self, win, y, x, self._wrapped[0], color) def on_event(self, ev_type, ev_arg): if not EventType.focused_only(ev_type): return if not EventType.key_press(ev_type): return if ev_arg == ord('\n') and self.__onclick: self.__onclick(self) class TuiSession: def __init__(self) -> None: self.stdsc = curses.initscr() curses.start_color() curses.noecho() curses.cbreak() self.__context_stack = [] self.__sched_events = [] ws = self.window_size() self.__win = curses.newwin(*ws) self.__winbg = curses.newwin(*ws) self.__panbg = cpanel.new_panel(self.__winbg) self.__winbg.bkgd(' ', curses.color_pair(ColorScope.WIN)) self.__win.timeout(50) self.__win.keypad(True) def window_size(self): return self.stdsc.getmaxyx() def set_color(self, scope, fg, bg): curses.init_pair(scope, int(fg), int(bg)) def schedule_redraw(self): self.schedule(EventType.E_REDRAW) def schedule_task(self, task): self.schedule(EventType.E_REDRAW) self.schedule(EventType.E_TASK, task) def schedule(self, event, arg = None): if len(self.__sched_events) > 0: if self.__sched_events[-1] == event: return self.__sched_events.append((event, arg)) def push_context(self, tuictx): tuictx.prepare() self.__context_stack.append(tuictx) self.schedule(EventType.E_REDRAW) curses.curs_set(self.active().curser_mode()) def pop_context(self): if len(self.__context_stack) == 1: return ctx = self.__context_stack.pop() ctx.on_destory() self.schedule(EventType.E_REDRAW) curses.curs_set(self.active().curser_mode()) def active(self): return self.__context_stack[-1] def event_loop(self): if len(self.__context_stack) == 0: raise RuntimeError("no tui context to display") while True: key = self.__win.getch() if key != -1: self.schedule(EventType.E_KEY, key) if len(self.__sched_events) == 0: continue evt, arg = self.__sched_events.pop(0) if evt == EventType.E_REDRAW: self.__redraw() elif evt == EventType.E_QUIT: self.__notify_quit() break self.active().dispatch_event(evt, arg) def __notify_quit(self): while len(self.__context_stack) == 0: ctx = self.__context_stack.pop() ctx.dispatch_event(EventType.E_QUIT, None) def __redraw(self): self.stdsc.erase() self.__win.erase() self.active().redraw(self.__win) self.__panbg.bottom() self.__win.touchwin() self.__winbg.touchwin() self.__win.refresh() self.__winbg.refresh() cpanel.update_panels() curses.doupdate() class TuiFocusGroup: def __init__(self) -> None: self.__grp = [] self.__id = 0 self.__sel = 0 self.__focused = None def register(self, tui_obj, pos=-1): if pos == -1: self.__grp.append((self.__id, tui_obj)) else: self.__grp.insert(pos, (self.__id, tui_obj)) self.__id += 1 return self.__id - 1 def navigate_focus(self, dir = 1): self.__sel = (self.__sel + dir) % len(self.__grp) f = None if not len(self.__grp) else self.__grp[self.__sel][1] if f and f != self.__focused: if self.__focused: self.__focused.on_focus_lost() f.on_focused() self.__focused = f def focused(self): return self.__focused class TuiContext(TuiObject): def __init__(self, session: TuiSession): super().__init__(self, "context") self.__root = None self.__sobj = None self.__session = session self.__win = None y, x = self.__session.window_size() self._size.reset(x, y) self.set_parent(None) self.__focus_group = TuiFocusGroup() self.__curser_mode = 0 def set_curser_mode(self, mode): self.__curser_mode = mode def curser_mode(self): return self.__curser_mode def set_root(self, root): self.__root = root self.__root.set_parent(self) def set_state(self, obj): self.__sobj = obj def state(self): return self.__sobj def prepare(self): self.__root.on_create() def on_destory(self): self.__root.on_destory() def canvas(self): return (self, self.__win) def session(self): return self.__session def dispatch_event(self, evt, arg): if evt == EventType.E_REDRAW: self.__focus_group.navigate_focus(0) elif evt == EventType.E_CHFOCUS: self.__focus_group.navigate_focus(1) self.__session.schedule(EventType.E_REDRAW) return elif evt == EventType.E_TASK: arg() elif evt == EventType.E_QUIT: self.__root.on_quit() elif evt == EventType.E_KEY: self._handle_key_event(arg) else: self.__root.on_event(evt, arg) focused = self.__focus_group.focused() if focused: focused.on_event(evt | EventType.E_M_FOCUS, arg) def redraw(self, win): self.__win = win self.on_layout() self.on_draw() def on_layout(self): self.__root.on_layout() def on_draw(self): self.__root.on_draw() def focus_group(self): return self.__focus_group def _handle_key_event(self, key): if key == ord('\t') or key == curses.KEY_RIGHT: self.__focus_group.navigate_focus() elif key == curses.KEY_LEFT: self.__focus_group.navigate_focus(-1) else: self.__root.on_event(EventType.E_KEY, key) if self.__focus_group.focused(): self.__session.schedule(EventType.E_REDRAW)