From 9645ed873c63c6dbc1a5b8f7839ce044b68bcc03 Mon Sep 17 00:00:00 2001 From: mar77i Date: Tue, 11 Feb 2025 02:09:07 +0100 Subject: [PATCH] use focus stack for modal and cursor --- ui/base.py | 75 ++++--- ui/drop_down.py | 30 +-- ui/focus.py | 37 ++++ ui/message_box.py | 12 +- ui/modal.py | 46 ++--- ui/scroll.py | 10 +- ui/spinner.py | 67 +++---- ui/tab_bar.py | 29 +-- ui/text_input.py | 376 +++++++++++++++++------------------ zenbook_conf/zenbook_conf.py | 116 +++++------ 10 files changed, 407 insertions(+), 391 deletions(-) create mode 100644 ui/focus.py diff --git a/ui/base.py b/ui/base.py index fc86575..91d4b5f 100644 --- a/ui/base.py +++ b/ui/base.py @@ -2,6 +2,8 @@ from functools import cached_property, partial import pygame +from .focus import FocusRootMixin + class EventMethodDispatcher: MODS = (pygame.KMOD_CTRL, pygame.KMOD_ALT, pygame.KMOD_META, pygame.KMOD_SHIFT) @@ -29,36 +31,65 @@ class EventMethodDispatcher: if hasattr(self, method_name): getattr(self, method_name)(ev) + def update(self): + pass + + def draw(self): + pass + class Parent(EventMethodDispatcher): - def __init__(self): + root: "Root" + running: bool + stop_event: bool + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) self.children = [] def handle_event(self, ev): - Child.handle_event(self, ev) - self.handle_event_children(ev) - - def handle_event_children(self, ev): - root = self.root - if not root.running or root.stop_event: + super().handle_event(ev) + if not self.running or self.stop_event: return for child in self.children: child.handle_event(ev) - if not root.running or root.stop_event: + if not self.running or self.stop_event: break def update(self): + super().update() for child in self.children: child.update() def draw(self): + super().draw() for child in self.children: child.draw() class Child(EventMethodDispatcher): def __init__(self, parent): - self.parent = parent + self._parent = parent + if parent is None: + return + for parent in (parent, getattr(parent, "parent", None)): + if isinstance(parent, Parent): + parent.children.append(self) + break + + @property + def parent(self): + return self._parent + + @parent.setter + def parent(self, parent): + if self._parent is not None: + self._parent.children.remove(self) + self._parent = parent + if parent is not None: + parent.children.append(self) + if "root" in self.__dict__: + self.__dict__.pop("root") @cached_property def root(self): @@ -88,20 +119,16 @@ class Child(EventMethodDispatcher): parent = parent.root or parent.parent return parent.surf - def draw(self): - pass - - def update(self): - pass - + @property + def running(self): + return self.root.running -class ChildAndParent(Parent, Child): - def __init__(self, parent): - Parent.__init__(self) - Child.__init__(self, parent) + @property + def stop_event(self): + return self.root.stop_event -class Root(Parent): +class Root(FocusRootMixin, Parent): BACKGROUND_COLOR: pygame.Color def __init__(self, surf, font=None): @@ -111,7 +138,6 @@ class Root(Parent): self.dirty = False self.surf = surf self.clock = pygame.time.Clock() - self.cursor = None self.stop_event = False self.root = self @@ -124,7 +150,7 @@ class Root(Parent): handle_activeevent = handle_windowexposed def key_escape(self): - if self.cursor is None: + if self.active: self.running = False KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}} @@ -134,11 +160,6 @@ class Root(Parent): self.surf.fill(self.BACKGROUND_COLOR) super().draw() - def update(self): - super().update() - if self.cursor is not None: - self.cursor.update() - def run(self): while True: for ev in pygame.event.get(): diff --git a/ui/drop_down.py b/ui/drop_down.py index b09ec68..2673d3b 100644 --- a/ui/drop_down.py +++ b/ui/drop_down.py @@ -2,6 +2,7 @@ from functools import partial import pygame +from .base import Parent from .button import Button from .modal import Modal @@ -10,19 +11,15 @@ class DropDownMenu(Modal): def __init__(self, parent, rect, entries, callback): super().__init__(parent) self.callback = callback - self.children.extend( - ( - Button( - self, - pygame.Rect( - (rect.left, rect.bottom + i * rect.height), rect.size, - ), - entry, - partial(self.choose, i), - ) - for i, entry in enumerate(entries) + for i, entry in enumerate(entries): + Button( + self, + pygame.Rect( + (rect.left, rect.bottom + i * rect.height), rect.size, + ), + entry, + partial(self.choose, i), ) - ) self.buttons_rect = pygame.Rect( rect.bottomleft, (rect.width, rect.height * len(entries)) ) @@ -38,7 +35,10 @@ class DropDownMenu(Modal): self.deactivate() -class DropDown(Button): +class DropDown(Parent, Button): def __init__(self, parent, rect, value, entries, callback, is_active=False): - self.dropdown_menu = DropDownMenu(parent, rect, entries, callback) - super().__init__(parent, rect, value, self.dropdown_menu.activate, is_active) + super().__init__(parent, rect, value, self.activate, is_active) + self.dropdown_menu = DropDownMenu(self, rect, entries, callback) + + def activate(self): + self.dropdown_menu.activate() diff --git a/ui/focus.py b/ui/focus.py new file mode 100644 index 0000000..07fe6d1 --- /dev/null +++ b/ui/focus.py @@ -0,0 +1,37 @@ +import pygame + + +class FocusRootMixin: + surf: pygame.Surface + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.focus_stack: list[FocusableMixin | FocusRootMixin] = [self] + + @property + def active(self): + return self.focus_stack[-1] is self + + def handle_event(self, ev): + focused = self.focus_stack[-1] + (super() if focused is self else focused).handle_event(ev) + + +class FocusableMixin: + root: FocusRootMixin + + @property + def active(self): + return self.root.focus_stack[-1] is self + + def activate(self): + focus_stack = self.root.focus_stack + assert self not in focus_stack + focus_stack.append(self) + self.dirty = True + + def deactivate(self): + focus_stack = self.root.focus_stack + assert self.active + focus_stack.pop() + self.dirty = True diff --git a/ui/message_box.py b/ui/message_box.py index 3c43152..5b481b2 100644 --- a/ui/message_box.py +++ b/ui/message_box.py @@ -9,14 +9,14 @@ class MessageBox(Modal): def __init__(self, parent, rect, message): super().__init__(parent) self.rect = rect - self.label = Label(self.parent, pygame.Rect(rect.center, (10, 10)), "") - self.children.extend((self.label, *self.get_buttons())) + self.label = Label(self, pygame.Rect(rect.center, (10, 10)), "") + self.add_buttons() self.message = message - def get_buttons(self): + def add_buttons(self): rect = self.rect - yield Button( - self.parent, + Button( + self, pygame.Rect( (rect.left + rect.width // 3, rect.centery + 64), (rect.width // 3, 128), @@ -26,9 +26,9 @@ class MessageBox(Modal): ) def draw(self): - super().draw() pygame.draw.rect(self.surf, "black", self.rect) pygame.draw.rect(self.surf, "gray", self.rect, 1) + super().draw() @property def message(self): diff --git a/ui/modal.py b/ui/modal.py index 0a1b4ef..f92e3c8 100644 --- a/ui/modal.py +++ b/ui/modal.py @@ -1,15 +1,13 @@ import pygame -from .base import Child +from .base import Child,Parent +from .focus import FocusableMixin -class Modal(Child): +class Modal(FocusableMixin, Parent, Child): def __init__(self, parent): super().__init__(parent) - self.backsurf = None - self.children = [] - self.active = False - self.draw = self._check_active(self.draw) + self.draw = self._check_active(self._wrap_draw(self.draw)) self.update = self._check_active(self.update) self.handle_event = self._check_active(self.handle_event) self.activate = self._check_active(self.activate, False) @@ -22,35 +20,23 @@ class Modal(Child): return None return inner - def draw(self): - self.surf.blit(self.backsurf, (0, 0)) - - @staticmethod - def tinted_copy(surf): - surf = surf.copy() - tintsurf = pygame.Surface(surf.get_size(), pygame.SRCALPHA, 32) - tintsurf.fill(pygame.Color(0x00000080)) - surf.blit(tintsurf, (0, 0)) - return surf - - def swap_children(self): - children = self.children.copy() - self.children.clear() - self.children.extend(self.root.children) - self.root.children.clear() - self.root.children.extend(children) + def _wrap_draw(self, method): + def inner(): + tintsurf = pygame.Surface(self.surf.get_size(), pygame.SRCALPHA, 32) + tintsurf.fill(pygame.Color(0x80)) + self.surf.blit(tintsurf, (0, 0)) + return method() + return inner def activate(self): - self.backsurf = self.tinted_copy(self.surf) - self.swap_children() - self.root.children.insert(0, self) - self.active = True + super().activate() self.root.stop_event = True self.dirty = True def deactivate(self): - self.backsurf = None - self.swap_children() - self.active = False + super().deactivate() self.root.stop_event = True self.dirty = True + + def draw(self): + super().draw() diff --git a/ui/scroll.py b/ui/scroll.py index 22c7614..292b689 100644 --- a/ui/scroll.py +++ b/ui/scroll.py @@ -1,13 +1,13 @@ import pygame -from .base import ChildAndParent +from .base import Child, Parent -class Scroll(ChildAndParent): +class Scroll(Parent, Child): def __init__(self, parent, rect, surf_size): super().__init__(parent) self.rect = rect - self.surf = pygame.Surface(surf_size, pygame.SRCALPHA, 32) + self.__dict__["surf"] = pygame.Surface(surf_size, pygame.SRCALPHA, 32) self.scroll_x = 0 self.scroll_y = 0 @@ -28,7 +28,7 @@ class Scroll(ChildAndParent): pygame.draw.rect(self.parent.surf, "gray", self.rect) self.root.surf.blit(self.get_subsurf(), self.rect.topleft) - def handle_event_children(self, ev): + def handle_event(self, ev): if hasattr(ev, "pos"): if not self.rect.collidepoint(ev.pos): return @@ -42,4 +42,4 @@ class Scroll(ChildAndParent): ) }, ) - super().handle_event_children(ev) + super().handle_event(ev) diff --git a/ui/spinner.py b/ui/spinner.py index 9574001..8154829 100644 --- a/ui/spinner.py +++ b/ui/spinner.py @@ -5,7 +5,7 @@ from time import time import pygame -from .base import ChildAndParent +from .base import Child, Parent from .button import Button from .text_input import TextInput @@ -49,42 +49,38 @@ class RepeatButton(Button): self.dirty = True -class Spinner(ChildAndParent): +class Spinner(Parent, Child): def __init__(self, parent, rect, callback, value=0): super().__init__(parent) self.callback = callback self.value = value button_size = (rect.height // 2, rect.height // 2) - self.children.extend( - ( - TextInput( - self, - pygame.Rect( - rect.topleft, (rect.width - button_size[0], rect.height) - ), - self.call_callback, - str(value), - re.compile(r"[-+]?\d*").fullmatch, - ), - RepeatButton( - self, - pygame.Rect( - (rect.right - button_size[0], rect.top), - button_size, - ), - "^", - partial(self.spin_callback, 1), - ), - RepeatButton( - self, - pygame.Rect( - (rect.right - button_size[0], rect.top + button_size[1]), - button_size, - ), - "v", - partial(self.spin_callback, -1), - ), - ) + self.text_input = TextInput( + self, + pygame.Rect( + rect.topleft, (rect.width - button_size[0], rect.height) + ), + self.call_callback, + str(value), + re.compile(r"[-+]?\d*").fullmatch, + ) + RepeatButton( + self, + pygame.Rect( + (rect.right - button_size[0], rect.top), + button_size, + ), + "^", + partial(self.spin_callback, 1), + ) + RepeatButton( + self, + pygame.Rect( + (rect.right - button_size[0], rect.top + button_size[1]), + button_size, + ), + "v", + partial(self.spin_callback, -1), ) def call_callback(self, value): @@ -96,14 +92,13 @@ class Spinner(ChildAndParent): if int_value != self.value: self.value = int_value self.callback(int_value) - text_input = self.children[0] str_value = str(self.value) - if str_value != text_input.value: - text_input.value = str(self.value) + if str_value != self.text_input.value: + self.text_input.value = str(self.value) self.root.dirty = True def spin_callback(self, value): self.value += value - self.children[0].value = str(self.value) + self.text_input.value = str(self.value) self.callback(self.value) self.root.dirty = True diff --git a/ui/tab_bar.py b/ui/tab_bar.py index 180785c..7ff721d 100644 --- a/ui/tab_bar.py +++ b/ui/tab_bar.py @@ -2,37 +2,41 @@ from functools import partial import pygame -from .base import ChildAndParent +from .base import Child, Parent from .button import Button -class TabBar(ChildAndParent): +class TabBar(Parent, Child): def __init__(self, parent, rect, labels, groups, active): super().__init__(parent) self.labels = labels + # ...why do we have to reparent these items? + for group in groups: + for item in group: + item.parent = self self.groups = groups - num_names = len(groups) + self.active = active + num_labels = len(labels) + self.children.clear() self.buttons = [ Button( self, pygame.Rect( - (rect.left + rect.width * i // num_names, rect.top), - (rect.width // num_names, rect.height), + (rect.left + rect.width * i // num_labels, rect.top), + (rect.width // num_labels, rect.height), ), labels[i], partial(self.update_children, i), - i == active, + False, ) - for i in range(len(groups)) + for i in range(num_labels) ] - self.children.extend(self.buttons) - self.active = active self.children.extend(self.groups[active]) - def update_children(self, name): - if self.active == name: + def update_children(self, i): + if self.active == i: return - self.active = name + self.active = i for i, (button, group) in enumerate(zip(self.buttons, self.groups)): is_group_active = i == self.active if button.is_active != is_group_active: @@ -47,3 +51,4 @@ class TabBar(ChildAndParent): elif is_child_active: self.children.remove(item) self.dirty = True + assert len(self.children) == len(set(self.children)) diff --git a/ui/text_input.py b/ui/text_input.py index 6481b51..ed20171 100644 --- a/ui/text_input.py +++ b/ui/text_input.py @@ -1,84 +1,78 @@ from contextlib import contextmanager +from dataclasses import dataclass from functools import partial from math import floor from time import time +from typing import Optional, Callable import pygame from .base import Child +from .focus import FocusableMixin -class Cursor(Child): +@dataclass +class Cursor: DELAY_MS = 500 REPEAT_MS = 100 - def __init__(self, text_input, x_offset): - super().__init__(text_input.parent) - self.text_input = text_input - self.old_value = text_input.value + old_value: str + pos: Optional[int] = None + key_callback: Optional[Callable] = None + key: Optional[int] = None + repeat_ts: Optional[float] = None + + def press_key(self, key_callback, key): + self.key_callback = key_callback + self.key = key + self.repeat_ts = time() + self.DELAY_MS / 1000 + + def release_key(self): self.key_callback = None self.key = None self.repeat_ts = None - self.pos = self.pos_from_offset(x_offset) - # I would rather have one cursor object on the current focus that activates - # itself by appending self to the current_focus stack. - self.root.children.append(self) - def remove(self): - self.root.children.remove(self) - self.text_input.cursor = None - self.dirty = True - return self.old_value + def repeat_key_callback(self): + repeat_offset = floor((time() - self.repeat_ts) * 1000 / self.REPEAT_MS) + 1 + if repeat_offset < 1: + return + for _ in range(repeat_offset): + self.key_callback() + self.repeat_ts += self.REPEAT_MS * repeat_offset / 1000 - def pos_from_offset(self, x_offset): - value = self.text_input.value - a, a_x = 0, 0 - b, b_x = len(value), self.font.size(value)[0] - if x_offset <= a_x: - return a - elif x_offset >= b_x: - return b - while b - a > 1: - c = a + (b - a) // 2 - c_x = self.font.size(value[:c])[0] - if c_x < x_offset: - a, a_x = c, c_x - else: - b, b_x = c, c_x - if abs(a_x - x_offset) < abs(b_x - x_offset): - return a - return b - @contextmanager - def check_dirty(self): - old = self.pos, self.value - yield - if (self.pos, self.value) != old: - self.text_input.dirty = True +def search_same_isspace_backward(value, pos): + if pos == 0: + return 0 + pos -= 1 + isspace = value[pos].isspace() + while pos > 0: + pos -= 1 + if value[pos].isspace() != isspace: + return pos + 1 + return 0 - def handle_keydown(self, ev): - if (key_method := self.get_key_method(ev.key, ev.mod)) is not None: - self.key_callback = key_method - elif ev.unicode and ev.unicode.isprintable(): - self.key_callback = partial(self.key_printable_unicode, ev.unicode) - else: - return - with self.check_dirty(): - self.key_callback() - self.key = ev.key - self.repeat_ts = time() + self.DELAY_MS / 1000 - def handle_keyup(self, ev): - if ev.key == self.key: - self.key_release() +def search_same_isspace_forward(value, pos): + len_value = len(value) + if pos == len_value: + return len_value + isspace = value[pos].isspace() + pos += 1 + while pos < len_value and value[pos].isspace() == isspace: + pos += 1 + return pos - @property - def value(self): - return self.text_input.value - @value.setter - def value(self, value): - self.text_input.value = value +class TextInput(FocusableMixin, Child): + def __init__(self, parent, rect, callback, value="", value_filter=None): + super().__init__(parent) + self.rect = rect + self.callback = callback + self.value = value + self.value_filter = value_filter + self.offset = 0 + self.cursor = None @staticmethod def maybe_scroll_font_surface(font, value_to_cursor, fs, width, height): @@ -101,14 +95,14 @@ class Cursor(Child): return fs, offset, x - offset def get_font_surface(self): - if self.pos > len(self.value): - self.pos = len(self.value) + if self.cursor.pos > len(self.value): + self.cursor.pos = len(self.value) fs = self.font.render(self.value, True, "gray") - rect = self.text_input.rect + rect = self.rect if self.value: - fs, self.text_input.offset, x = self.maybe_scroll_font_surface( + fs, self.offset, x = self.maybe_scroll_font_surface( self.font, - self.value[:self.pos], + self.value[:self.cursor.pos], fs, rect.width - 24, rect.height, @@ -122,95 +116,127 @@ class Cursor(Child): return fs def update(self): - if self.key_callback is None: + if getattr(self.cursor, "key_callback", None) is None: return - repeat_offset = floor((time() - self.repeat_ts) * 1000 / self.REPEAT_MS) - if repeat_offset < 0: - return - repeat_offset += 1 with self.check_dirty(): - for _ in range(repeat_offset): - self.key_callback() - self.repeat_ts += self.REPEAT_MS * repeat_offset / 1000 + self.cursor.repeat_key_callback() - def key_backspace(self): - if self.pos > 0: - self.pos -= 1 - self.key_delete() + def draw(self): + pygame.draw.rect(self.surf, "black", self.rect) + if self.active: + fs = self.get_font_surface() + else: + fs = self.font.render(self.value, True, "gray") + self.surf.subsurface(self.rect).blit( + fs, (16, (self.rect.height - fs.get_height()) // 2) + ) + pygame.draw.rect(self.surf, "gray", self.rect, 1) - def key_delete(self): + def pos_from_offset(self, x_offset): value = self.value - if self.pos < len(value): - self.value = "".join((value[:self.pos], value[self.pos + 1:])) + if x_offset is None: + return len(value) + a, a_x = 0, 0 + b, b_x = len(value), self.font.size(value)[0] + if x_offset <= a_x: + return a + elif x_offset >= b_x: + return b + while b - a > 1: + c = a + (b - a) // 2 + c_x = self.font.size(value[:c])[0] + if c_x < x_offset: + a, a_x = c, c_x + else: + b, b_x = c, c_x + if abs(a_x - x_offset) < abs(b_x - x_offset): + return a + return b - def key_prev_word(self): - value = self.value - pos = self.pos - for _ in range(2): - if pos == 0: - continue - n = 1 - isspace = value[pos - n].isspace() - while n < pos: - n += 1 - if value[pos - n].isspace() != isspace: - n -= 1 - break - pos -= n - if pos != self.pos: - self.pos = pos - - def key_next_word(self): - value = self.value - value_len = len(value) - pos = self.pos - for _ in range(2): - if pos == value_len: - continue - n = 0 - isspace = value[pos].isspace() - while pos + n < value_len and value[pos + n].isspace() == isspace: - n += 1 - pos += n - if pos != self.pos: - self.pos = pos + def handle_mousebuttondown(self, ev): + if ev.button != 1: + return + if self.rect.collidepoint(ev.pos): + self.activate() + with self.check_dirty(): + self.cursor.pos = self.pos_from_offset( + ev.pos[0] - self.rect.left - 16 + self.offset + ) + elif self.active: + self.deactivate(True) + + @contextmanager + def check_dirty(self): + old = getattr(self.cursor, "pos", -1), self.value + yield + if (getattr(self.cursor, "pos", -2), self.value) != old: + self.dirty = True def key_left(self): - self.pos = max(self.pos - 1, 0) + self.cursor.pos = max(self.cursor.pos - 1, 0) def key_right(self): - self.pos = min(self.pos + 1, len(self.value)) + self.cursor.pos = min(self.cursor.pos + 1, len(self.value)) def key_home(self): - self.pos = 0 + self.cursor.pos = 0 def key_end(self): - self.pos = len(self.value) + self.cursor.pos = len(self.value) - def key_printable_unicode(self, unicode): + def key_backspace(self): + if self.cursor.pos > 0: + self.cursor.pos -= 1 + self.key_delete() + + def key_delete(self): value = self.value - len_old_value = len(value) - value = "".join((value[:self.pos], unicode, value[self.pos:])) - if self.text_input.value_filter: - try: - result = self.text_input.value_filter(value) - except ValueError: - result = None - if isinstance(result, str): - value = result - elif not result: - return + if self.cursor.pos < len(value): + self.value = f"{value[:self.cursor.pos]}{value[self.cursor.pos + 1:]}" + + def filter_value(self, value): + if not self.value_filter: + return value + try: + return self.value_filter(value) + except ValueError: + return None + + def key_printable_unicode(self, unicode): + len_old_value = len(self.value) + value = f"{self.value[:self.cursor.pos]}{unicode}{self.value[self.cursor.pos:]}" + result = self.filter_value(value) + if isinstance(result, str): + value = result + elif not value: + return self.value = value - if len(value) > len_old_value: - self.pos += len(unicode) + len_value = len(self.value) + if len_value > len_old_value: + self.cursor.pos += len_value - len_old_value - def key_release(self): - self.key_callback = None - self.key = None - self.repeat_ts = None + def key_skip_word(self, func): + pos = self.cursor.pos + for _ in range(2): + pos = func(self.value, pos) + if pos != self.cursor.pos: + self.cursor.pos = pos - def key_blur(self, restore=False): - self.text_input.blur(restore) + def activate(self): + if self.active: + return + super().activate() + self.cursor = Cursor(self.value) + + def deactivate(self, restore=False): + if not self.active: + return + if restore: + self.value = self.cursor.old_value + else: + self.callback(self.value) + self.cursor = None + super().deactivate() KEY_METHODS = { frozenset(set()): { @@ -220,74 +246,30 @@ class Cursor(Child): pygame.K_END: key_end, pygame.K_BACKSPACE: key_backspace, pygame.K_DELETE: key_delete, - pygame.K_KP_ENTER: key_blur, - pygame.K_RETURN: key_blur, - pygame.K_ESCAPE: partial(key_blur, restore=True), + pygame.K_KP_ENTER: deactivate, + pygame.K_RETURN: deactivate, + pygame.K_ESCAPE: partial(deactivate, restore=True), }, frozenset({pygame.KMOD_CTRL}): { - pygame.K_LEFT: key_prev_word, - pygame.K_RIGHT: key_next_word, + pygame.K_LEFT: partial(key_skip_word, func=search_same_isspace_backward), + pygame.K_RIGHT: partial(key_skip_word, func=search_same_isspace_forward), }, } - -class TextInput(Child): - def __init__(self, parent, rect, callback, value="", value_filter=None): - super().__init__(parent) - self.rect = rect - self.callback = callback - self.value = value - self.value_filter = value_filter - self.offset = 0 - - @property - def cursor(self): - cursor = self.root.cursor - if cursor is not None and cursor.text_input is self: - return cursor - return None - - @cursor.setter - def cursor(self, value): - self.root.cursor = value - - def draw(self): - pygame.draw.rect(self.surf, "black", self.rect) - if self.cursor is not None: - fs = self.cursor.get_font_surface() + def handle_keydown(self, ev): + if not self.active: + return + if (key_method := self.get_key_method(ev.key, ev.mod)) is not None: + key_callback = key_method + elif ev.unicode and ev.unicode.isprintable(): + key_callback = partial(self.key_printable_unicode, ev.unicode) else: - fs = self.font.render(self.value, True, "gray") - self.surf.subsurface(self.rect).blit( - fs, (16, (self.rect.height - fs.get_height()) // 2) - ) - pygame.draw.rect(self.surf, "gray", self.rect, 1) - - def focus(self, x_offset): - cursor = self.cursor - x_offset = x_offset - self.rect.left - 16 + self.offset - if cursor is not None: - if cursor.text_input is self: - new_pos = cursor.pos_from_offset(x_offset) - if new_pos != cursor.pos: - cursor.pos = new_pos - self.dirty = True - return - cursor.text_input.blur(True) - self.dirty = True - self.cursor = Cursor(self, x_offset) - - def blur(self, restore=False): - if self.cursor is not None: - old_value = self.cursor.remove() - if restore: - self.value = old_value - elif self.value != old_value: - self.callback(self.value) - self.offset = 0 + return + with self.check_dirty(): + key_callback() + if self.active: + self.cursor.press_key(key_callback, ev.key) - def handle_mousebuttondown(self, ev): - if ev.button == 1: - if self.rect.collidepoint(ev.pos): - self.focus(ev.pos[0]) - elif self.cursor is not None: - self.blur(True) + def handle_keyup(self, ev): + if ev.key == getattr(self.cursor, "key", None): + self.cursor.release_key() diff --git a/zenbook_conf/zenbook_conf.py b/zenbook_conf/zenbook_conf.py index b950063..a0ec61a 100644 --- a/zenbook_conf/zenbook_conf.py +++ b/zenbook_conf/zenbook_conf.py @@ -85,70 +85,60 @@ class ZenbookConf(Root): ), self.xrandr_conf.is_active("vertical"), ) - self.children.extend( - ( - FPSWidget(self), - self.single_button, - self.double_button, - self.vertical_button, - bt_switch, - Icon( - self, - bluetooth.fit( - (bt_switch.rect.right + 68, bt_switch.rect.centery - 88), - (8, 8), - ), - ), - touch_switch, - Icon( - self, - touchscreen.fit( - (touch_switch.rect.right + 68, touch_switch.rect.centery - 88), - (8, 8), - ), - ), - stylus_switch, - Icon( - self, - stylus.fit( - ( - stylus_switch.rect.right + 68, - stylus_switch.rect.centery - 88, - ), - (8, 8), - ), - ), - Button( - self, - pygame.Rect((784, touch_switch.rect.top), (384, 128)), - "Re-apply", - partial(self.xinput_conf.reapply_by_type, "touchpad") - ), - Button( - self, - pygame.Rect((784, stylus_switch.rect.top), (384, 128)), - "Re-apply", - partial(self.xinput_conf.reapply_by_type, "stylus") - ), - Button( - self, - pygame.Rect((window_size[0] - 128, 0), (128, 96)), - "×", - self.quit, - ), - Button( - self, - pygame.Rect((window_size[0] - 256, 0), (128, 96)), - "_", - self.iconify, - ), - Button( - self, - pygame.Rect((window_size[0] - 384, 0), (128, 96)), - "»", - self.next_display, + FPSWidget(self) + Icon( + self, + bluetooth.fit( + (bt_switch.rect.right + 68, bt_switch.rect.centery - 88), + (8, 8), + ), + ) + Icon( + self, + touchscreen.fit( + (touch_switch.rect.right + 68, touch_switch.rect.centery - 88), + (8, 8), + ), + ) + Icon( + self, + stylus.fit( + ( + stylus_switch.rect.right + 68, + stylus_switch.rect.centery - 88, ), - ) + (8, 8), + ), + ) + Button( + self, + pygame.Rect((784, touch_switch.rect.top), (384, 128)), + "Re-apply", + partial(self.xinput_conf.reapply_by_type, "touchpad") + ) + Button( + self, + pygame.Rect((784, stylus_switch.rect.top), (384, 128)), + "Re-apply", + partial(self.xinput_conf.reapply_by_type, "stylus") + ) + Button( + self, + pygame.Rect((window_size[0] - 128, 0), (128, 96)), + "×", + self.quit, + ) + Button( + self, + pygame.Rect((window_size[0] - 256, 0), (128, 96)), + "_", + self.iconify, + ) + Button( + self, + pygame.Rect((window_size[0] - 384, 0), (128, 96)), + "»", + self.next_display, ) def get_icon(self): -- 2.51.0