2 # libtui - TUI framework using ncurses
5 # I sware, this is the last time I ever touch
6 # any sort of the GUI messes.
11 import curses.panel as cpanel
12 import curses.textpad as textpad
15 def __invoke_fn(obj, fn, *args):
19 _id = obj._id if obj else "<root>"
20 raise Exception(_id, str(fn), args)
22 def resize_safe(obj, co, y, x):
23 __invoke_fn(obj, co.resize, y, x)
25 def move_safe(obj, co, y, x):
26 __invoke_fn(obj, co.move, y, x)
28 def addstr_safe(obj, co, y, x, str, *args):
29 __invoke_fn(obj, co.addstr, y, x, str, *args)
32 def __init__(self, v) -> None:
40 black = _TuiColor(curses.COLOR_BLACK)
41 red = _TuiColor(curses.COLOR_RED)
42 green = _TuiColor(curses.COLOR_GREEN)
43 yellow = _TuiColor(curses.COLOR_YELLOW)
44 blue = _TuiColor(curses.COLOR_BLUE)
45 magenta = _TuiColor(curses.COLOR_MAGENTA)
46 cyan = _TuiColor(curses.COLOR_CYAN)
47 white = _TuiColor(curses.COLOR_WHITE)
74 E_M_FOCUS = 0b10000000
77 return (t & EventType.E_M_FOCUS)
80 return t & ~EventType.E_M_FOCUS
83 return (t & ~EventType.E_M_FOCUS) == EventType.E_KEY
86 RelSize = re.compile(r"(?P<mult>[0-9]+(?:\.[0-9]+)?)?\*(?P<add>[+-][0-9]+)?")
88 class BoundExpression:
89 def __init__(self, expr = None):
96 def set_pair(self, mult, add):
101 self._mult = expr._mult
102 self._add = expr._add
104 def update(self, expr):
105 if isinstance(expr, int):
108 m = Matchers.RelSize.match(expr)
112 mult = 1 if not g["mult"] else float(g["mult"])
113 add = 0 if not g["add"] else int(g["add"])
117 self.set_pair(0, int(expr))
119 def calc(self, ref_val):
120 return int(self._mult * ref_val + self._add)
123 return self._mult == 0
126 return self._mult == 0 and self._add == 0
128 def scale_mult(self, scalar):
133 def normalise(*exprs):
134 v = BoundExpression()
137 return [e.scale_mult(1 / v._mult) for e in exprs]
139 def __add__(self, b):
140 v = BoundExpression()
146 def __sub__(self, b):
147 v = BoundExpression()
153 def __iadd__(self, b):
154 self._mult += b._mult
158 def __isub__(self, b):
159 self._mult -= b._mult
163 def __rmul__(self, scalar):
164 v = BoundExpression()
170 def __truediv__(self, scalar):
171 v = BoundExpression()
173 v._mult /= float(scalar)
179 self.__x = BoundExpression()
180 self.__y = BoundExpression()
188 def resolve(self, ref_w, ref_h):
189 return (self.__y.calc(ref_h), self.__x.calc(ref_w))
191 def set(self, dyn_bound):
192 self.__x.set(dyn_bound.dyn_x())
193 self.__y.set(dyn_bound.dyn_y())
196 def __init__(self) -> None:
200 def shrinkX(self, dx):
202 def shrinkY(self, dy):
215 def update(self, dynsz, ref_bound):
216 y, x = dynsz.resolve(ref_bound.x(), ref_bound.y())
220 def reset(self, x, y):
221 self.__x, self.__y = x, y
229 def yx(self, scale = 1):
230 return int(self.__y * scale), int(self.__x * scale)
232 class TuiStackWindow:
233 def __init__(self, obj) -> None:
235 self.__win = curses.newwin(0, 0)
236 self.__pan = cpanel.new_panel(self.__win)
239 def resize(self, h, w):
240 resize_safe(self.__obj, self.__win, h, w)
242 def relocate(self, y, x):
243 move_safe(self.__obj, self.__pan, y, x)
245 def set_geometric(self, h, w, y, x):
246 resize_safe(self.__obj, self.__win, h, w)
247 move_safe(self.__obj, self.__pan, y, x)
249 def set_background(self, color_scope):
250 self.__win.bkgd(' ', curses.color_pair(color_scope))
261 def send_front(self):
268 def __init__(self) -> None:
269 self._local_pos = DynamicBound()
271 self._dyn_size = DynamicBound()
273 self._margin = (0, 0, 0, 0)
274 self._padding = (0, 0, 0, 0)
275 self._align = Alignment.TOP | Alignment.LEFT
277 def set_local_pos(self, x, y):
278 self._local_pos.dyn_x().update(x)
279 self._local_pos.dyn_y().update(y)
281 def set_alignment(self, align):
284 def set_size(self, w = None, h = None):
286 self._dyn_size.dyn_x().update(w)
288 self._dyn_size.dyn_y().update(h)
290 def set_margin(self, top, right, bottom, left):
291 self._margin = (top, right, bottom, left)
293 def set_padding(self, top, right, bottom, left):
294 self._padding = (top, right, bottom, left)
297 self._pos.reset(0, 0)
298 self._size.reset(0, 0)
300 def deduce_spatial(self, constrain):
302 self.__satisfy_bound(constrain)
303 self.__satisfy_alignment(constrain)
304 self.__satisfy_margin(constrain)
305 self.__satisfy_padding(constrain)
307 self.__to_corner_pos(constrain)
309 def __satisfy_alignment(self, constrain):
310 local_pos = self._local_pos
311 cbound = constrain._size
315 ry, rx = local_pos.resolve(cx, cy)
316 ay, ax = size.yx(0.5)
318 if self._align & Alignment.CENTER:
322 if self._align & Alignment.BOT:
323 ay = min(cy - ay, cy - 1)
325 elif self._align & Alignment.TOP:
328 if self._align & Alignment.RIGHT:
331 elif self._align & Alignment.LEFT:
334 self._pos.reset(ax + rx, ay + ry)
336 def __satisfy_margin(self, constrain):
337 tm, lm, bm, rm = self._margin
339 self._pos.growX(rm - lm)
340 self._pos.growY(bm - tm)
342 def __satisfy_padding(self, constrain):
343 csize = constrain._size
345 h, w = self._size.yx(0.5)
346 y, x = self._pos.yx()
348 tp, lp, bp, rp = self._padding
350 if not (tp or lp or bp or rp):
353 dtp = min(y - h, tp) - tp
354 dbp = min(ch - (y + h), bp) - bp
356 dlp = min(x - w, lp) - lp
357 drp = min(cw - (x + w), rp) - rp
359 self._size.growX(drp + dlp)
360 self._size.growY(dtp + dbp)
362 def __satisfy_bound(self, constrain):
363 self._size.update(self._dyn_size, constrain._size)
365 def __to_corner_pos(self, constrain):
366 h, w = self._size.yx(0.5)
367 g_pos = constrain._pos
372 self._pos.growX(g_pos.x())
373 self._pos.growY(g_pos.y())
376 class TuiObject(SpatialObject):
377 def __init__(self, context, id):
380 self._context = context
383 self._focused = False
385 def set_parent(self, parent):
386 self._parent = parent
390 return self._parent.canvas()
391 return (self, self._context.window())
397 return self._context.session()
402 def on_destory(self):
410 self.deduce_spatial(self._parent)
415 def on_event(self, ev_type, ev_arg):
418 def on_focused(self):
421 def on_focus_lost(self):
422 self._focused = False
424 def set_visbility(self, visible):
425 self._visible = visible
434 class TuiWidget(TuiObject):
435 def __init__(self, context, id):
436 super().__init__(context, id)
441 co, _ = self.canvas()
447 class TuiContainerObject(TuiObject):
448 def __init__(self, context, id):
449 super().__init__(context, id)
452 def add(self, child):
453 child.set_parent(self)
454 self._children.append(child)
457 return self._children
461 for child in self._children:
464 def on_destory(self):
466 for child in self._children:
471 for child in self._children:
476 for child in self._children:
481 for child in self._children:
484 def on_event(self, ev_type, ev_arg):
485 super().on_event(ev_type, ev_arg)
486 for child in self._children:
487 child.on_event(ev_type, ev_arg)
490 class TuiScrollable(TuiObject):
491 def __init__(self, context, id):
492 super().__init__(context, id)
493 self.__spos = Bound()
495 self.__pad = curses.newpad(1, 1)
496 self.__pad.bkgd(' ', curses.color_pair(ColorScope.PANEL))
497 self.__pad_panel = cpanel.new_panel(self.__pad)
498 self.__content = None
501 return (self, self.__pad)
503 def set_content(self, content):
504 self.__content = content
505 self.__content.set_parent(self)
507 def set_scrollY(self, y):
508 self.__spos.resetY(y)
510 def set_scrollX(self, x):
511 self.__spos.resetX(x)
513 def reached_last(self):
514 off = self.__spos.y() + self._size.y()
515 return off >= self.__content._size.y()
517 def reached_top(self):
518 return self.__spos.y() < self._size.y()
523 if not self.__content:
526 self.__content.on_layout()
528 h, w = self._size.yx()
529 ch, cw = self.__content._size.yx()
530 sh, sw = max(ch, h), max(cw, w)
532 self.__spos.resetX(min(self.__spos.x(), max(cw, w) - w))
533 self.__spos.resetY(min(self.__spos.y(), max(ch, h) - h))
535 resize_safe(self, self.__pad, sh, sw)
538 if not self.__content:
543 self.__pad_panel.top()
544 self.__content.on_draw()
546 wminy, wminx = self._pos.yx()
547 wmaxy, wmaxx = self._size.yx()
548 wmaxy, wmaxx = wmaxy + wminy, wmaxx + wminx
549 self.__pad.touchwin()
550 self.__pad.refresh(*self.__spos.yx(),
551 wminy, wminx, wmaxy - 1, wmaxx - 1)
554 class Layout(TuiContainerObject):
555 class Cell(TuiObject):
556 def __init__(self, context):
557 super().__init__(context, "cell")
560 def set_obj(self, obj):
562 self.__obj.set_parent(self)
566 self.__obj.on_create()
568 def on_destory(self):
570 self.__obj.on_destory()
579 self.__obj.do_layout()
585 def on_event(self, ev_type, ev_arg):
587 self.__obj.on_event(ev_type, ev_arg)
589 def __init__(self, context, id, ratios):
590 super().__init__(context, id)
592 rs = [BoundExpression(r) for r in ratios.split(',')]
593 self._rs = BoundExpression.normalise(*rs)
595 for _ in range(len(self._rs)):
596 cell = Layout.Cell(self._context)
599 self._adjust_to_fit()
601 def _adjust_to_fit(self):
604 def add(self, child):
605 raise RuntimeError("invalid operation")
607 def set_cell(self, i, obj):
608 if i > len(self._children):
609 raise ValueError(f"cell #{i} out of bound")
611 self._children[i].set_obj(obj)
614 class FlexLinearLayout(Layout):
617 def __init__(self, context, id, ratios):
618 self.__horizontal = False
620 super().__init__(context, id, ratios)
622 def orientation(self, orient):
623 self.__horizontal = orient == FlexLinearLayout.LANDSCAPE
629 def _adjust_to_fit(self):
630 sum_abs = BoundExpression()
639 for i, r in enumerate(self._rs):
641 self._rs[i] -= sum_abs
643 def __apply_ratio(self):
644 if self.__horizontal:
645 self.__adjust_horizontal()
647 self.__adjust_vertical()
649 def __adjust_horizontal(self):
650 acc = BoundExpression()
651 for r, cell in zip(self._rs, self.children()):
652 cell._dyn_size.dyn_y().set_pair(1, 0)
653 cell._dyn_size.dyn_x().set(r)
655 cell.set_alignment(Alignment.LEFT)
656 cell._local_pos.dyn_y().set_pair(0, 0)
657 cell._local_pos.dyn_x().set(acc)
661 def __adjust_vertical(self):
662 acc = BoundExpression()
663 for r, cell in zip(self._rs, self.children()):
664 cell._dyn_size.dyn_x().set_pair(1, 0)
665 cell._dyn_size.dyn_y().set(r)
667 cell.set_alignment(Alignment.TOP | Alignment.CENTER)
668 cell._local_pos.dyn_x().set_pair(0, 0)
669 cell._local_pos.dyn_y().set(acc)
674 class TuiPanel(TuiContainerObject):
675 def __init__(self, context, id):
676 super().__init__(context, id)
678 self.__use_border = False
679 self.__use_shadow = False
680 self.__shadow_param = (0, 0)
682 self.__swin = TuiStackWindow(self)
683 self.__shad = TuiStackWindow(self)
685 self.__swin.set_background(ColorScope.PANEL)
686 self.__shad.set_background(ColorScope.SHADOW)
689 return (self, self.__swin.window())
691 def drop_shadow(self, off_y, off_x):
692 self.__shadow_param = (off_y, off_x)
693 self.__use_shadow = not (off_y == off_x and off_y == 0)
695 def border(self, _b):
696 self.__use_border = _b
698 def bkgd_override(self, scope):
699 self.__swin.set_background(scope)
706 h, w = self._size.y(), self._size.x()
707 y, x = self._pos.y(), self._pos.x()
708 self.__swin.set_geometric(h, w, y, x)
710 if self.__use_shadow:
711 sy, sx = self.__shadow_param
712 self.__shad.set_geometric(h, w, y + sy, x + sx)
714 def on_destory(self):
720 win = self.__swin.window()
723 if self.__use_border:
726 if self.__use_shadow:
732 self.__swin.send_front()
738 class TuiLabel(TuiWidget):
739 def __init__(self, context, id):
740 super().__init__(context, id)
741 self._text = "TuiLabel"
744 self.__auto_fit = True
747 self.__highlight = False
748 self.__color_scope = -1
750 def __try_fit_text(self, txt):
752 self._dyn_size.dyn_x().set_pair(0, len(txt))
753 self._dyn_size.dyn_y().set_pair(0, 1)
755 def __pad_text(self):
756 for i, t in enumerate(self._wrapped):
757 self._wrapped[i] = str.rjust(t, self._size.x())
759 def set_text(self, text):
761 self.__try_fit_text(text)
763 def override_color(self, color = -1):
764 self.__color_scope = color
766 def auto_fit(self, _b):
769 def truncate(self, _b):
772 def hightlight(self, _b):
773 self.__highlight = _b
775 def pad_around(self, _b):
782 self.__try_fit_text(txt)
786 if len(txt) <= self._size.x():
787 self._wrapped = [txt]
792 txt = txt[:self._size.x() - 1]
793 self._wrapped = [txt]
797 self._wrapped = textwrap.wrap(txt, self._size.x())
801 _, win = self.canvas()
802 y, x = self._pos.yx()
804 if self.__color_scope != -1:
805 color = curses.color_pair(self.__color_scope)
806 elif self.__highlight:
807 color = curses.color_pair(ColorScope.TEXT_HI)
809 color = curses.color_pair(ColorScope.TEXT)
811 for i, t in enumerate(self._wrapped):
812 addstr_safe(self, win, y + i, x, t, color)
815 class TuiTextBlock(TuiWidget):
816 def __init__(self, context, id):
817 super().__init__(context, id)
820 self.__fit_to_height = False
822 def set_text(self, text):
823 text = textwrap.dedent(text)
824 self.__lines = text.split('\n')
825 if self.__fit_to_height:
826 self._dyn_size.dyn_y().set_pair(0, 0)
828 def height_auto_fit(self, yes):
829 self.__fit_to_height = yes
834 self.__wrapped.clear()
835 for t in self.__lines:
837 self.__wrapped.append(t)
839 wrap = textwrap.wrap(t, self._size.x())
840 self.__wrapped += wrap
842 if self._dyn_size.dyn_y().nullity():
843 h = len(self.__wrapped)
844 self._dyn_size.dyn_y().set_pair(0, h)
850 _, win = self.canvas()
851 y, x = self._pos.yx()
853 color = curses.color_pair(ColorScope.TEXT)
854 for i, t in enumerate(self.__wrapped):
855 addstr_safe(self, win, y + i, x, t, color)
858 class TuiTextBox(TuiWidget):
859 def __init__(self, context, id):
860 super().__init__(context, id)
861 self.__box = TuiStackWindow(self)
862 self.__box.set_background(ColorScope.PANEL)
863 self.__textb = textpad.Textbox(self.__box.window(), True)
864 self.__textb.stripspaces = True
866 self.__scheduled_edit = False
868 self._context.focus_group().register(self, 0)
870 def __validate(self, x):
871 if x == 10 or x == 9:
878 co, _ = self.canvas()
879 h, w = self._size.yx()
880 y, x = self._pos.yx()
881 cy, cx = co._pos.yx()
882 y, x = y + cy, x + cx
885 self.__box.set_geometric(1, w - 1, y + h // 2, x + 1)
889 self.__box.send_front()
891 _, cwin = self.canvas()
893 h, w = self._size.yx()
894 y, x = self._pos.yx()
895 textpad.rectangle(cwin, y, x, y + h - 1, x+w)
897 win = self.__box.window()
901 self.__str = self.__textb.edit(lambda x: self.__validate(x))
902 self.session().schedule(EventType.E_CHFOCUS)
903 self.__scheduled_edit = False
908 def on_focused(self):
909 self.__box.set_background(ColorScope.BOX)
910 if not self.__scheduled_edit:
911 # edit will block, defer to next update cycle
912 self.session().schedule_task(self.__edit)
913 self.__scheduled_edit = True
915 def on_focus_lost(self):
916 self.__box.set_background(ColorScope.PANEL)
919 class SimpleList(TuiWidget):
921 def __init__(self) -> None:
925 def on_selected(self):
927 def on_key_pressed(self, key):
930 def __init__(self, context, id):
931 super().__init__(context, id)
934 self.__on_sel_confirm_cb = None
935 self.__on_sel_change_cb = None
937 self._context.focus_group().register(self)
939 def set_onselected_cb(self, cb):
940 self.__on_sel_confirm_cb = cb
942 def set_onselection_change_cb(self, cb):
943 self.__on_sel_change_cb = cb
946 return len(self.__items)
949 return self.__selected
951 def add_item(self, item):
952 self.__items.append(item)
959 self.__selected = min(self.__selected, len(self.__items))
960 self._size.resetY(len(self.__items) + 1)
963 _, win = self.canvas()
966 for i, item in enumerate(self.__items):
967 color = curses.color_pair(ColorScope.TEXT)
968 if i == self.__selected:
970 color = curses.color_pair(ColorScope.SELECT)
972 color = curses.color_pair(ColorScope.BOX)
974 txt = str.ljust(item.get_text(), w)
976 addstr_safe(self, win, i, 0, txt, color)
978 def on_event(self, ev_type, ev_arg):
979 if not EventType.key_press(ev_type):
982 if len(self.__items) == 0:
985 sel = self.__items[self.__selected]
990 if self.__on_sel_confirm_cb:
991 self.__on_sel_confirm_cb(self, self.__selected, sel)
994 sel.on_key_pressed(ev_arg)
996 if (ev_arg != curses.KEY_DOWN and
997 ev_arg != curses.KEY_UP):
1000 prev = self.__selected
1001 if ev_arg == curses.KEY_DOWN:
1002 self.__selected += 1
1004 self.__selected -= 1
1006 self.__selected = max(self.__selected, 0)
1007 self.__selected = self.__selected % len(self.__items)
1009 if self.__on_sel_change_cb:
1010 self.__on_sel_change_cb(self, prev, self.__selected)
1013 class TuiButton(TuiLabel):
1014 def __init__(self, context, id):
1015 super().__init__(context, id)
1016 self.__onclick = None
1018 context.focus_group().register(self)
1020 def set_text(self, text):
1021 return super().set_text(f"<{text}>")
1023 def set_click_callback(self, cb):
1026 def hightlight(self, _b):
1027 raise NotImplemented()
1030 _, win = self.canvas()
1031 y, x = self._pos.yx()
1034 color = curses.color_pair(ColorScope.SELECT)
1036 color = curses.color_pair(ColorScope.TEXT)
1038 addstr_safe(self, win, y, x, self._wrapped[0], color)
1040 def on_event(self, ev_type, ev_arg):
1041 if not EventType.focused_only(ev_type):
1043 if not EventType.key_press(ev_type):
1046 if ev_arg == ord('\n') and self.__onclick:
1047 self.__onclick(self)
1051 def __init__(self) -> None:
1052 self.stdsc = curses.initscr()
1053 curses.start_color()
1058 self.__context_stack = []
1059 self.__sched_events = []
1061 ws = self.window_size()
1062 self.__win = curses.newwin(*ws)
1063 self.__winbg = curses.newwin(*ws)
1064 self.__panbg = cpanel.new_panel(self.__winbg)
1066 self.__winbg.bkgd(' ', curses.color_pair(ColorScope.WIN))
1068 self.__win.timeout(50)
1069 self.__win.keypad(True)
1071 def window_size(self):
1072 return self.stdsc.getmaxyx()
1074 def set_color(self, scope, fg, bg):
1075 curses.init_pair(scope, int(fg), int(bg))
1077 def schedule_redraw(self):
1078 self.schedule(EventType.E_REDRAW)
1080 def schedule_task(self, task):
1081 self.schedule(EventType.E_REDRAW)
1082 self.schedule(EventType.E_TASK, task)
1084 def schedule(self, event, arg = None):
1085 if len(self.__sched_events) > 0:
1086 if self.__sched_events[-1] == event:
1089 self.__sched_events.append((event, arg))
1091 def push_context(self, tuictx):
1093 self.__context_stack.append(tuictx)
1094 self.schedule(EventType.E_REDRAW)
1096 curses.curs_set(self.active().curser_mode())
1098 def pop_context(self):
1099 if len(self.__context_stack) == 1:
1102 ctx = self.__context_stack.pop()
1104 self.schedule(EventType.E_REDRAW)
1106 curses.curs_set(self.active().curser_mode())
1109 return self.__context_stack[-1]
1111 def event_loop(self):
1112 if len(self.__context_stack) == 0:
1113 raise RuntimeError("no tui context to display")
1116 key = self.__win.getch()
1118 self.schedule(EventType.E_KEY, key)
1120 if len(self.__sched_events) == 0:
1123 evt, arg = self.__sched_events.pop(0)
1124 if evt == EventType.E_REDRAW:
1126 elif evt == EventType.E_QUIT:
1127 self.__notify_quit()
1130 self.active().dispatch_event(evt, arg)
1132 def __notify_quit(self):
1133 while len(self.__context_stack) == 0:
1134 ctx = self.__context_stack.pop()
1135 ctx.dispatch_event(EventType.E_QUIT, None)
1141 self.active().redraw(self.__win)
1143 self.__panbg.bottom()
1144 self.__win.touchwin()
1145 self.__winbg.touchwin()
1147 self.__win.refresh()
1148 self.__winbg.refresh()
1150 cpanel.update_panels()
1153 class TuiFocusGroup:
1154 def __init__(self) -> None:
1158 self.__focused = None
1160 def register(self, tui_obj, pos=-1):
1162 self.__grp.append((self.__id, tui_obj))
1164 self.__grp.insert(pos, (self.__id, tui_obj))
1166 return self.__id - 1
1168 def navigate_focus(self, dir = 1):
1169 self.__sel = (self.__sel + dir) % len(self.__grp)
1170 f = None if not len(self.__grp) else self.__grp[self.__sel][1]
1171 if f and f != self.__focused:
1173 self.__focused.on_focus_lost()
1178 return self.__focused
1180 class TuiContext(TuiObject):
1181 def __init__(self, session: TuiSession):
1182 super().__init__(self, "context")
1185 self.__session = session
1189 y, x = self.__session.window_size()
1190 self._size.reset(x, y)
1191 self.set_parent(None)
1193 self.__focus_group = TuiFocusGroup()
1194 self.__curser_mode = 0
1196 def set_curser_mode(self, mode):
1197 self.__curser_mode = mode
1199 def curser_mode(self):
1200 return self.__curser_mode
1202 def set_root(self, root):
1204 self.__root.set_parent(self)
1206 def set_state(self, obj):
1213 self.__root.on_create()
1215 def on_destory(self):
1216 self.__root.on_destory()
1219 return (self, self.__win)
1222 return self.__session
1224 def dispatch_event(self, evt, arg):
1225 if evt == EventType.E_REDRAW:
1226 self.__focus_group.navigate_focus(0)
1227 elif evt == EventType.E_CHFOCUS:
1228 self.__focus_group.navigate_focus(1)
1229 self.__session.schedule(EventType.E_REDRAW)
1231 elif evt == EventType.E_TASK:
1233 elif evt == EventType.E_QUIT:
1234 self.__root.on_quit()
1235 elif evt == EventType.E_KEY:
1236 self._handle_key_event(arg)
1238 self.__root.on_event(evt, arg)
1240 focused = self.__focus_group.focused()
1242 focused.on_event(evt | EventType.E_M_FOCUS, arg)
1244 def redraw(self, win):
1249 def on_layout(self):
1250 self.__root.on_layout()
1253 self.__root.on_draw()
1255 def focus_group(self):
1256 return self.__focus_group
1258 def _handle_key_event(self, key):
1259 if key == ord('\t') or key == curses.KEY_RIGHT:
1260 self.__focus_group.navigate_focus()
1261 elif key == curses.KEY_LEFT:
1262 self.__focus_group.navigate_focus(-1)
1264 self.__root.on_event(EventType.E_KEY, key)
1266 if self.__focus_group.focused():
1267 self.__session.schedule(EventType.E_REDRAW)