From 6980cfe3d83b7228063ec217821ee70e4fd8a234 Mon Sep 17 00:00:00 2001 From: mar77i Date: Tue, 21 Jan 2025 17:55:14 +0100 Subject: [PATCH] consolidate key methods --- ui/ui.py | 174 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 103 insertions(+), 71 deletions(-) diff --git a/ui/ui.py b/ui/ui.py index c7b391f..3a84628 100644 --- a/ui/ui.py +++ b/ui/ui.py @@ -20,6 +20,30 @@ import pygame class EventMethodDispatcher: + MODS = (pygame.KMOD_CTRL, pygame.KMOD_ALT, pygame.KMOD_META, pygame.KMOD_SHIFT) + KEY_METHODS = {} + + @classmethod + def get_mods(cls, mod): + mods = set() + for mask in cls.MODS: + if mod & mask: + mods.add(mask) + return frozenset(mods) + + def get_key_method(self, key, mod): + method = self.KEY_METHODS.get(self.get_mods(mod), {}).get(key) + if method is not None: + return partial(method, self) + return None + + def handle_keydown(self, ev): + if not self.KEY_METHODS: + return + key_method = self.get_key_method(ev.key, ev.mod) + if key_method is not None: + key_method() + def handle_event(self, ev): method_name = f"handle_{pygame.event.event_name(ev.type).lower()}" if hasattr(self, method_name): @@ -37,7 +61,7 @@ class UIParent(EventMethodDispatcher): self.dirty = False self.clock = pygame.time.Clock() self.children = [] - self.cursor = None + self.cursor: Cursor | None = None def handle_quit(self, _): self.running = False @@ -47,23 +71,26 @@ class UIParent(EventMethodDispatcher): handle_activeevent = handle_windowexposed - def handle_keydown(self, ev): - if ev.key == pygame.K_ESCAPE: - if self.cursor is not None: - self.cursor.remove() - else: - self.running = False + def key_escape(self): + if self.cursor is None: + self.running = False + + KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}} def handle_event(self, ev): - super().handle_event(ev) - if not self.running: - return - for child in self.children: + children = ( + super(), + *((self.cursor,) if self.cursor is not None else ()), + *self.children, + ) + for child in children: child.handle_event(ev) if not self.running: - break + return def update(self): + if self.cursor is not None: + self.cursor.update() for child in self.children: child.update() @@ -109,6 +136,17 @@ class UIChild(EventMethodDispatcher): def surf(self): return self.parent.surf + @property + def cursor(self): + cursor = self.parent.cursor + if cursor is not None and cursor.child is self: + return cursor + return None + + @cursor.setter + def cursor(self, value): + self.parent.cursor = value + def draw(self): pass @@ -327,26 +365,17 @@ class Switch(UIChild): self.moving_since = time() - offset -class Cursor: +class Cursor(EventMethodDispatcher): DELAY_MS = 500 REPEAT_MS = 100 - def __init__(self, child, pos): + def __init__(self, child): self.child = child + self.old_value = child.value self.key_callback = None self.key = None self.repeat_ts = None - self.pos = pos - - MODS = (pygame.KMOD_CTRL, pygame.KMOD_ALT, pygame.KMOD_META, pygame.KMOD_SHIFT) - - @classmethod - def get_mods(cls, mod): - mods = set() - for mask in cls.MODS: - if mod & mask: - mods.add(mask) - return frozenset(mods) + self.pos = len(child.value) @contextmanager def check_dirty(self): @@ -355,23 +384,21 @@ class Cursor: if (self.pos, self.value) != old: self.child.dirty = True - def press(self, key, mod, unicode): - method = self.METHODS_PER_MODS.get(self.get_mods(mod), {}).get(key) - if method is not None: - self.key_callback = partial(method, self) - elif unicode and unicode.isprintable(): - self.key_callback = partial(self.key_printable_unicode, unicode) + 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 = key + self.key = ev.key self.repeat_ts = time() + self.DELAY_MS / 1000 - def release(self): - self.key_callback = None - self.key = None - self.repeat_ts = None + def handle_keyup(self, ev): + if ev.key == self.key: + self.key_release() @property def value(self): @@ -401,7 +428,7 @@ class Cursor: def key_delete(self): value = self.value if self.pos < len(value): - self.value = f"{value[:self.pos]}{value[self.pos + 1:]}" + self.value = "".join((value[:self.pos], value[self.pos + 1:])) def key_prev_word(self): value = self.value @@ -449,21 +476,30 @@ class Cursor: def key_printable_unicode(self, unicode): value = self.value - value = f"{value[:self.pos]}{unicode}{value[self.pos:]}" + len_old_value = len(value) + value = "".join((value[:self.pos], unicode, value[self.pos:])) if self.child.value_filter: try: - value = self.child.value_filter(value) + result = self.child.value_filter(value) except Exception: - value = None - if not isinstance(value, str): + result = None + if isinstance(result, str): + value = result + elif not result: return self.value = value - self.pos += len(unicode) + if len(value) > len_old_value: + self.pos += len(unicode) + + def key_release(self): + self.key_callback = None + self.key = None + self.repeat_ts = None - def remove(self): - self.child.remove_cursor() + def key_blur(self, restore=False): + self.child.blur(restore) - METHODS_PER_MODS = { + KEY_METHODS = { frozenset(set()): { pygame.K_LEFT: key_left, pygame.K_RIGHT: key_right, @@ -471,8 +507,9 @@ class Cursor: pygame.K_END: key_end, pygame.K_BACKSPACE: key_backspace, pygame.K_DELETE: key_delete, - pygame.K_KP_ENTER: remove, - pygame.K_RETURN: remove, + pygame.K_KP_ENTER: key_blur, + pygame.K_RETURN: key_blur, + pygame.K_ESCAPE: partial(key_blur, restore=True), }, frozenset({pygame.KMOD_CTRL}): { pygame.K_LEFT: key_prev_word, @@ -488,7 +525,6 @@ class TextInput(UIChild): self.callback = callback self.value = value self.value_filter = value_filter - self.cursor = None @staticmethod def maybe_scroll_font_surface(font, value_to_cursor, fs, width, height): @@ -541,32 +577,28 @@ class TextInput(UIChild): ) pygame.draw.rect(self.surf, "gray", self.rect, 1) - def update(self): - if self.cursor is not None: - self.cursor.update() - - def remove_cursor(self): - self.callback(self.value) - self.cursor = self.parent.cursor = None + def focus(self): + cursor = self.cursor + if cursor is not None: + if cursor.child is self: + return + cursor.child.blur(True) self.dirty = True + self.cursor = Cursor(self) + + def blur(self, restore=False): + if self.cursor is not None: + old_value = self.cursor.old_value + self.cursor = None + self.dirty = True + if restore: + self.value = old_value + else: + self.callback(self.value) def handle_mousebuttondown(self, ev): if ev.button == 1: if self.rect.collidepoint(ev.pos): - if self.parent.cursor is not None: - self.parent.cursor.remove() - self.cursor = self.parent.cursor = Cursor(self, len(self.value)) - self.dirty = True + self.focus() elif self.cursor is not None: - self.remove_cursor() - - def handle_keydown(self, ev): - if self.cursor is None: - return - self.cursor.press(ev.key, ev.mod, ev.unicode) - - def handle_keyup(self, ev): - if self.cursor is None: - return - if ev.key == self.cursor.key: - self.cursor.release() + self.blur(True) -- 2.51.0