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):
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
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()
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
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):
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):
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
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,
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,
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):
)
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)