]> git.mar77i.info Git - zenbook_gui/commitdiff
consolidate key methods
authormar77i <mar77i@protonmail.ch>
Tue, 21 Jan 2025 16:55:14 +0000 (17:55 +0100)
committermar77i <mar77i@protonmail.ch>
Tue, 21 Jan 2025 16:55:14 +0000 (17:55 +0100)
ui/ui.py

index c7b391fe225a8b70fdbb2a728e1e5a2d89b7da2f..3a846280102386ebd27deb561e944cec7c42b3d8 100644 (file)
--- 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)