]> git.mar77i.info Git - zenbook_gui/commitdiff
make that keyboard pretty great
authormar77i <mar77i@protonmail.ch>
Wed, 17 Sep 2025 20:15:38 +0000 (22:15 +0200)
committermar77i <mar77i@protonmail.ch>
Wed, 17 Sep 2025 20:15:38 +0000 (22:15 +0200)
12 files changed:
keyboard/clock.py [new file with mode: 0644]
keyboard/draggable.py [new file with mode: 0644]
keyboard/key.py
keyboard/keyboard.py
keyboard/layout.py [new file with mode: 0644]
keyboard/mousepad.py [new file with mode: 0644]
keyboard/settings.py
launch.py
memory.py
ui/__init__.py
ui/fps_widget.py
ui/modal.py

diff --git a/keyboard/clock.py b/keyboard/clock.py
new file mode 100644 (file)
index 0000000..31c6a2b
--- /dev/null
@@ -0,0 +1,126 @@
+from datetime import datetime
+from math import cos, sin, tau
+
+import pygame
+
+from ui import Child
+
+
+class ClockWidget(Child):
+    CLOCK_RADIUS = 3 / 8
+    MARKINGS = (3 / 4, 15 / 16)
+    HANDS = (
+        (-1 / 16, 3 / 16),  # fullday
+        (-1 / 16, 7 / 16),  # hour
+        (-1 / 16, 3 / 4),   # minute
+        (-1 / 8, 13 / 16),  # second
+    )
+    WIDTHS = (
+        0.0325,   # fullday
+        0.02625,  # hour
+        0.0175,   # minute
+        0.005,    # second
+    )
+
+    def __init__(self, parent, rect):
+        super().__init__(parent)
+        self.rect = rect
+        min_rect_size = min(rect.size)
+        self.radius = min_rect_size * self.CLOCK_RADIUS
+        self.widths = tuple(w * min_rect_size for w in self.WIDTHS)
+        self.state = (0, 0, 0, 0, 0, 0)
+
+    def update(self):
+        now = datetime.now()
+        state = (
+            now.strftime("%b"),
+            now.day,
+            now.hour,
+            now.minute,
+            now.second,
+            now.microsecond,
+        )
+        if state != self.state:
+            self.state = state
+            self.dirty = True
+
+    def draw(self):
+        month, day, hour, minute, second, microsecond = self.state
+        second = second + microsecond / 1000000
+        fractional_minute = minute + second / 60
+        fractional_hour = hour + fractional_minute / 60
+        self.draw_markings()
+        self.draw_date(month, "gold")
+        self.draw_date(str(day), "gold", True)
+        self.draw_hand_polygon(
+            fractional_hour * tau / 24, self.HANDS[0], self.widths[0], "purple"
+        )
+        if fractional_hour >= 13:
+            fractional_hour -= 12
+        self.draw_hand_polygon(
+            fractional_hour * tau / 12, self.HANDS[1], self.widths[1], "lightgreen"
+        )
+        self.draw_hand_polygon(
+            fractional_minute * tau / 60, self.HANDS[2], self.widths[2], "darkcyan"
+        )
+        self.draw_hand_polygon(second * tau / 60, self.HANDS[3], self.widths[3], "blue")
+
+    def draw_markings(self):
+        center = self.rect.center
+        radius = self.radius
+        markings = tuple(m * radius for m in self.MARKINGS)
+        for i in range(12):
+            angle = tau * i / 12
+            fractions = cos(angle), -sin(angle)
+            pygame.draw.line(
+                self.surf,
+                "green",
+                tuple(c + (f * markings[0]) for c, f in zip(center, fractions)),
+                tuple(c + (f * radius) for c, f in zip(center, fractions)),
+            )
+            for j in range(i * 5 + 1, (i + 1) * 5):
+                angle = tau * j / 60
+                fractions = cos(angle), -sin(angle)
+                pygame.draw.line(
+                    self.surf,
+                    "yellow",
+                    tuple(c + (f * markings[1]) for c, f in zip(center, fractions)),
+                    tuple(c + (f * radius) for c, f in zip(center, fractions)),
+                )
+        pygame.draw.circle(self.surf, "green", center, radius, 1)
+
+    def draw_date(self, string, color, right=False):
+        fs = self.font.render(string, True, color)
+        if right:
+            x_offset = self.MARKINGS[0] * self.radius - fs.get_width()
+        else:
+            x_offset = -self.MARKINGS[0] * self.radius
+        self.surf.blit(
+            fs,
+            (
+                self.rect.centerx + x_offset,
+                self.rect.centery - fs.get_height() / 2,
+            ),
+        )
+
+    def draw_hand_polygon(self, angle, interval, width, color):
+        # fix origin and direction
+        angle = tau / 4 - angle
+        center = self.rect.center
+        radius = self.radius
+        fractions = cos(angle), -sin(angle)
+        points = (
+            tuple(c + (f * interval[0] * radius) for c, f in zip(center, fractions)),
+            tuple(c + (f * interval[1] * radius) for c, f in zip(center, fractions)),
+        )
+        offset = (cos(angle + tau / 4) * width / 2, -sin(angle + tau / 4) * width / 2)
+        pygame.draw.polygon(
+            self.surf,
+            color,
+            [
+                (points[0][0] - offset[0], points[0][1] - offset[1]),
+                (points[0][0] + offset[0], points[0][1] + offset[1]),
+                (points[1][0] + offset[0], points[1][1] + offset[1]),
+                (points[1][0] - offset[0], points[1][1] - offset[1]),
+            ]
+        )
diff --git a/keyboard/draggable.py b/keyboard/draggable.py
new file mode 100644 (file)
index 0000000..462aec5
--- /dev/null
@@ -0,0 +1,98 @@
+import pygame
+
+from ui import Child
+from ui.multitouch import MultitouchHandler
+
+
+class DraggableButton(Child):
+    def __init__(self, parent, rect, surf, dests, callback, highlight=False):
+        super().__init__(parent)
+        assert rect.size == surf.get_size()
+        self.rect = rect
+        self.surface = surf
+        self.dests = dests
+        self.callback = callback
+        self.highlight = highlight
+        self.pushed = None
+        self.drag_pos = None
+        self.drag_rel = None
+
+    def draw(self):
+        if not self.pushed:
+            value_color = "lime" if self.highlight else "gray"
+            colors = ("black", value_color, value_color)
+        else:
+            colors = ("darkgray", "lightgray", "black")
+        pygame.draw.rect(self.surf, colors[0], self.rect)
+        pygame.draw.rect(self.surf, colors[1], self.rect, 8)
+        self.surf.blit(self.surface, self.rect.topleft)
+        if self.drag_pos is not None:
+            for dest in self.dests:
+                width = 6
+                if dest.collidepoint(self.drag_pos):
+                    dest = dest.inflate((128, 128))
+                    width *= 2
+                pygame.draw.rect(self.surf, "red", dest, width)
+            self.surf.blit(
+                self.surface,
+                (
+                    self.drag_pos[0] - self.drag_rel[0],
+                    self.drag_pos[1] - self.drag_rel[1],
+                ),
+            )
+
+    def toggle(self, pos=None, pushed=None):
+        if pos is None:
+            self.drag_pos = None
+            self.drag_rel = None
+        else:
+            if self.drag_rel is None:
+                self.drag_rel = (pos[0] - self.rect.left, pos[1] - self.rect.top)
+            self.drag_pos = pos
+        self.pushed = pushed
+        self.dirty = True
+
+    def handle_mousebuttondown(self, ev):
+        if ev.button == 1 and self.rect.collidepoint(ev.pos):
+            self.toggle(ev.pos, True)
+
+    def handle_mousemotion(self, ev):
+        if self.pushed is not True:
+            return
+        if not ev.buttons[0]:
+            self.toggle()
+        else:
+            self.drag_pos = ev.pos
+
+    def handle_mousebuttonup(self, ev):
+        if ev.button != 1 or self.pushed is not True:
+            return
+        self.drop(ev.pos)
+        self.toggle()
+
+    def drop(self, pos):
+        for i, dest in enumerate(self.dests):
+            if dest.collidepoint(pos):
+                self.rect, self.dests[i] = dest, self.rect
+                self.callback()
+                break
+
+    def handle_fingerdown(self, event):
+        pos = MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+        if self.pushed is None and self.rect.collidepoint(pos):
+            self.toggle(pos, (event.touch_id, event.finger_id))
+
+    def handle_fingermotion(self, event):
+        if self.pushed != (event.touch_id, event.finger_id):
+            return
+        self.drag_pos = MultitouchHandler.map_coords(
+            (event.x, event.y), self.surf.get_size()
+        )
+
+    def handle_fingerup(self, event):
+        if self.pushed != (event.touch_id, event.finger_id):
+            return
+        self.drop(
+            MultitouchHandler.map_coords((event.x, event.y), self.surf.get_size())
+        )
+        self.toggle()
index 1e00c125f17185440544212ec951fa9e099460a1..6a63df84b7420a208b7b5ccd02aa3305ec45fd6e 100644 (file)
@@ -177,7 +177,7 @@ class KeyButton(Button):
             self.press()
 
     def handle_mousemotion(self, event):
-        if event.buttons[0] and not self.rect.collidepoint(event.pos):
+        if not event.buttons[0] or not self.rect.collidepoint(event.pos):
             self.release()
 
     def handle_mousebuttonup(self, event):
@@ -220,7 +220,7 @@ def get_key_size(size, x_sums_iter, x_sums_len):
     return (size[0] / x_sum, size[1] / x_sums_len)
 
 
-def get_keyboard_keys(rect, parent, callback, keys):
+def get_keyboard_keys(parent, rect, callback, keys):
     result = []
     x_sums = [0]
     y_spans = []
index 74af025966ce1afb06e19d22b95f54bf9d25f7a1..1ca36f3fec4393e009e3f22462c0439f6257ed84 100644 (file)
@@ -1,14 +1,16 @@
 #!/usr/bin/env python3
 
 from pynput.keyboard import Controller as KeyboardController, Key, KeyCode
-from pynput._util.xorg_keysyms import KEYPAD_KEYS
+from pynput.mouse import Controller as MouseController, Button
 
 from launch import pygame
 from multitouch import MultitouchHandler
 from ui import BaseRoot, FPSWidget
 
-from .key import CURSOR_KEYS, KEYBOARD, KEYPAD_KEYS, get_keyboard_keys
-from .settings import Settings
+from .clock import ClockWidget
+from .key import KEYBOARD, get_keyboard_keys
+from .mousepad import MousePadWidget
+from .settings import SettingsModal
 
 # add a launcher symbol
 
@@ -21,44 +23,85 @@ class Root(BaseRoot):
             pygame.display.set_mode((0, 0), pygame.NOFRAME, display=1),
             pygame.font.SysFont("DejaVu Sans", size=64),
         )
-        size = self.surf.get_size()
-        widget_size = (size[0] // 3, size[1] // 2)
-        get_keyboard_keys(
-            pygame.Rect((0, 0), (size[0], widget_size[1])), self, self.key_cb, KEYBOARD
-        )
-        get_keyboard_keys(
-            pygame.Rect((0, widget_size[1]), widget_size),
-            self,
-            self.key_cb,
-            CURSOR_KEYS,
-        )
-        get_keyboard_keys(
-            pygame.Rect(widget_size, widget_size), self, self.key_cb, KEYPAD_KEYS
-        )
         self.pushed = set()
         self.mt = MultitouchHandler(
             MultitouchHandler.find_device(name="ELAN9009:00 04F3:425A")
         )
         self.keyboard = KeyboardController()
+        self.mouse = MouseController()
+        self.mouse_fingers = {}
         self.display = self.keyboard._display
         self.led_mask = self.display.get_keyboard_control().led_mask
         self.muted = False
         FPSWidget(self)
-        self.settings = Settings(
+        size = self.surf.get_size()
+        self.settings_modal = SettingsModal(
             self,
             pygame.Rect((size[0] // 4, size[1] // 4), (size[0] // 2, size[1] // 2)),
         )
+        self.keys_index = len(self.children)
+        self.setup_widgets()
+
+    def setup_widgets(self):
+        while len(self.children) > self.keys_index:
+            self.children.pop()
+        size = self.surf.get_size()
+        half_height = size[1] // 2
+        for row in range(2):
+            widget_top = half_height * row
+            if row == self.settings_modal.layout_modal.keyboard_row:
+                get_keyboard_keys(
+                    self,
+                    pygame.Rect((widget_top, 0), (size[0], half_height)),
+                    self.key_cb,
+                    KEYBOARD,
+                )
+                continue
+            widget_row = self.settings_modal.layout_modal.widget_row
+            widget_width = size[0] // (3 if len(widget_row) == 3 else 2)
+            x = 0 if len(widget_row) > 0 else size[0] // 4
+            for keyset in widget_row:
+                rect = pygame.Rect((x, half_height), (widget_width, half_height))
+                if keyset is MousePadWidget:
+                    keyset(self, rect, self.click_cb, self.move_cb)
+                elif keyset is ClockWidget:
+                    keyset(self, rect)
+                else:
+                    get_keyboard_keys(self, rect, self.key_cb, keyset)
+                x += widget_width
 
     def key_cb(self, key, release=False):
         if key == "Settings":
-            self.settings.activate()
+            self.settings_modal.activate()
         if isinstance(key, (Key, KeyCode)):
-            (self.keyboard.press, self.keyboard.release)[release](key)
-            if release and key in self.pushed:
-                self.pushed.remove(key)
-            elif not release:
+            if release:
+                self.keyboard.release(key)
+                if key in self.pushed:
+                    self.pushed.remove(key)
+            else:
+                self.keyboard.press(key)
                 self.pushed.add(key)
 
+    def click_cb(self, button, *args):
+        if len(args) > 2:
+            raise TypeError(f"Too many arguments to click_callback: {args}")
+        elif len(args) == 2:
+            n, release = args
+        else:
+            n, release = None, *args
+        if release:
+            if button in self.pushed:
+                self.mouse.release(button)
+                self.pushed.remove(button)
+        elif n is not None:
+            self.mouse.click(button, n)
+        else:
+            self.mouse.press(button)
+            self.pushed.add(button)
+
+    def move_cb(self, pos, scroll=False):
+        (self.mouse.scroll if scroll else self.mouse.move)(*pos)
+
     def handle_events(self):
         self.mt.handle_events()
         super().handle_events()
@@ -69,6 +112,8 @@ class Root(BaseRoot):
         if led_mask != self.led_mask:
             self.led_mask = led_mask
             self.dirty = True
+#        if self.mouse_frame:
+#            self.move_mouse()
 
     def run(self):
         try:
@@ -76,4 +121,4 @@ class Root(BaseRoot):
                 super().run()
         finally:
             for key in self.pushed:
-                self.keyboard.release(key)
+                (self.mouse if isinstance(key, Button) else self.keyboard).release(key)
diff --git a/keyboard/layout.py b/keyboard/layout.py
new file mode 100644 (file)
index 0000000..bb9aa15
--- /dev/null
@@ -0,0 +1,212 @@
+import pygame
+
+from ui import Child, QuittableModal, Rect
+
+from .clock import ClockWidget
+from .draggable import DraggableButton
+from .key import apply_y_spans, get_key_size
+from .mousepad import MousePadWidget
+from .touchbutton import TouchButton
+from .key import KEYBOARD, KEYPAD_KEYS, CURSOR_KEYS
+
+
+
+
+class LayoutWidget(Child):
+    padding = 8
+
+    @classmethod
+    def get_keyboard_keys(cls, surf, size, keys):
+        result = []
+        x_sums = [0]
+        y_spans = []
+        for key in keys:
+            if key == "\n":
+                x_sums.append(0)
+                apply_y_spans(x_sums, y_spans)
+            elif len(key) > 2:
+                width = key[2].get("x_hint", 1)
+                x_sums[-1] += width
+                y_hint = key[2].get("y_hint", 1)
+                if y_hint > 1:
+                    y_spans.append([width, y_hint - 1])
+            else:
+                x_sums[-1] += 1
+        apply_y_spans(x_sums, y_spans)
+        assert all(item[1] <= 0 for item in y_spans)
+        x, y = (0, 0)
+        x_sums_iter = iter(x_sums)
+        x_sums_len = len(x_sums)
+        key_size = get_key_size(size, x_sums_iter, x_sums_len)
+        for key in keys:
+            if key == "\n":
+                x, y = 0, y + key_size[1]
+                key_size = get_key_size(size, x_sums_iter, x_sums_len)
+                continue
+            key_width, key_height = key_size
+            if key != (None, None):
+                if len(key) > 2:
+                    key_width *= key[2].get("x_hint", 1)
+                    key_height *= key[2].get("y_hint", 1)
+                rect = pygame.Rect(
+                    (x + cls.padding / 2, y + cls.padding / 2),
+                    (key_width - cls.padding, key_height - cls.padding),
+                )
+                pygame.draw.rect(surf, "gray", rect, 1)
+                pygame.draw.ellipse(
+                    surf,
+                    "0x333333",
+                    pygame.Rect(
+                        (
+                            rect.centerx - key_size[0] / 6,
+                            rect.centery - key_size[1] / 4,
+                        ),
+                        (key_size[0] / 3, key_size[1] / 2),
+                    )
+                )
+            x += key_width
+        return result
+
+    def get_widget_row_rects(self, num_widgets):
+        if self.parent.keyboard_row == 0:
+            top = self.rect.centery
+        else:
+            top = self.rect.top
+        size = (self.rect.width / 3, self.rect.height / 2)
+        rects = []
+        if num_widgets == 1:
+            rects.append(
+                pygame.Rect(
+                    (self.rect.left + size[0], top),
+                    size,
+                )
+            )
+        elif num_widgets == 2:
+            rects.append(
+                pygame.Rect(
+                    (self.rect.left + self.rect.width / 4 - size[0] / 2, top),
+                    size,
+                )
+            )
+            rects.append(
+                pygame.Rect(
+                    (self.rect.left + self.rect.width * 3 / 4 - size[0] / 2, top),
+                    size,
+                )
+            )
+        elif num_widgets == 3:
+            for i in range(3):
+                rects.append(
+                    pygame.Rect(
+                        (self.rect.left + i * size[0], top),
+                        size,
+                    )
+                )
+        return rects
+
+    def get_widget_row_rect_dests(self, len_widget_row, keyset, source_rect):
+        if keyset in self.parent.widget_row:
+            keypad_dests = self.get_widget_row_rects(len_widget_row)
+            keypad_idx = self.parent.widget_row.index(KEYPAD_KEYS)
+            keypad_dests[keypad_idx], source_rect = source_rect, keypad_dests[keypad_idx]
+        elif len_widget_row < 3:
+            keypad_dests = self.get_widget_row_rects(len_widget_row + 1)
+        else:
+            keypad_dests = []
+        return source_rect, keypad_dests
+
+    def __init__(self, parent, rect):
+        super().__init__(parent)
+        self.rect = rect
+        keyboard_size = (rect.width, rect.height / 2)
+        self.keyboard_rects = [
+            pygame.Rect(rect.topleft, keyboard_size),
+            pygame.Rect((rect.left, rect.centery), keyboard_size),
+        ]
+        widget_size = (rect.width / 3, keyboard_size[1])
+        self.widget_rects = [
+            [
+                pygame.Rect(
+                    (rect.left + rect.width / 4 - widget_size[0] / 2, rect.top),
+                    widget_size,
+                ),
+                pygame.Rect(
+                    (rect.left + rect.width * 3 / 4 - widget_size[0] / 2, rect.top),
+                    widget_size,
+                )
+            ],
+            [
+                pygame.Rect((rect.left, rect.top), widget_size),
+                pygame.Rect((rect.left + widget_size[0], rect.top), widget_size),
+                pygame.Rect((rect.left + 2 * widget_size[1], rect.top), widget_size),
+            ],
+        ]
+        surf = pygame.Surface(keyboard_size)
+        surf.fill("black")
+        self.get_keyboard_keys(surf, keyboard_size, KEYBOARD)
+        self.keyboard = DraggableButton(
+            self.parent,
+            self.keyboard_rects[self.parent.keyboard_row],
+            surf,
+            [self.keyboard_rects[not self.parent.keyboard_row]],
+            self.keyboard_callback,
+        )
+        #self.keypad_rect = pygame.Rect(
+        #    (
+        #        self.parent.rect.left + (
+        #            self.rect.left - self.parent.rect.left
+        #        ) / 2 - self.rect.width / 6,
+        #        self.parent.rect.top + 96,
+        #    ),
+        #    (self.rect.width / 3, self.rect.height / 2),
+        #)
+        #surf = pygame.Surface(self.keypad_rect.size)
+        #surf.fill("black")
+        #self.get_keyboard_keys(surf, self.keypad_rect.size, KEYPAD_KEYS)
+        #keypad_rect, keypad_dests = self.get_widget_row_rect_dests(
+        #    len(parent.widget_row), KEYPAD_KEYS, self.keypad_rect
+        #)
+        #self.keypad = DraggableButton(
+        #    self.parent,
+        #    keypad_rect,
+        #    surf,
+        #    keypad_dests,
+        #    self.keypad_callback,
+        #)
+
+    def keyboard_callback(self):
+        self.parent.keyboard_row = int(
+            (self.keyboard.rect.top - self.rect.top) * 2 / self.rect.height
+        )
+        # move the visual widget row out of the way here
+
+    def keypad_callback(self):
+        if KEYPAD_KEYS in self.parent.widget_row:
+            self.parent.widget_row.remove(KEYPAD_KEYS)
+        if self.keypad.rect != self.keypad_rect:
+            if len(self.parent.widget_row) == 0:
+                self.parent.widget_row.append(KEYPAD_KEYS)
+                return
+            # insert KEYPAD_KEYS to widget_row at the right index
+            # rearrange the visual widget row
+
+    def draw(self):
+        pygame.draw.rect(self.surf, "gray", self.rect.inflate((-2, -2)), 1)
+
+
+class LayoutModal(QuittableModal):
+    def __init__(self, parent):
+        super().__init__(parent)
+        size = self.surf.get_size()
+        self.rect = pygame.Rect(150, 150, size[0] - 300, size[1] - 300)
+        Rect(self, self.rect, "black", "gray")
+        TouchButton(
+            self,
+            pygame.Rect((self.rect.centerx - 128, self.rect.bottom - 128), (256, 128)),
+            "Close",
+            self.deactivate,
+        )
+        self.keyboard_row = 0
+        self.widget_row = [CURSOR_KEYS, KEYPAD_KEYS, ClockWidget]
+        half_size = (size[0] / 2, size[1] / 2)
+        LayoutWidget(self, pygame.Rect((half_size[0] / 2, half_size[1] / 2), half_size))
diff --git a/keyboard/mousepad.py b/keyboard/mousepad.py
new file mode 100644 (file)
index 0000000..050c3dc
--- /dev/null
@@ -0,0 +1,83 @@
+from functools import partial
+
+import pygame
+from pynput.mouse import Button
+
+from ui import Child
+from ui.multitouch import MultitouchHandler
+
+from .touchbutton import TouchButton
+
+
+BUTTONS = (
+    ("Dbl←", (Button.left, 2)),
+    ("←", (Button.left,)),
+    ("↓", (Button.middle,)),
+    ("→", (Button.right,)),
+)
+
+
+class MousePadWidget(Child):
+    def __init__(self, parent, rect, click_cb, move_cb):
+        super().__init__(parent)
+        self.rect = rect
+        button_size = (rect.width / len(BUTTONS), rect.height / 6)
+        for i, (value, args) in enumerate(BUTTONS):
+            TouchButton(
+                parent,
+                pygame.Rect(
+                    (
+                        rect.left + i * button_size[0],
+                        rect.top + rect.height - button_size[1],
+                    ),
+                    button_size,
+                ),
+                value,
+                partial(click_cb, *args)
+            )
+        MousePadArea(
+            parent,
+            pygame.Rect(rect.topleft, (rect.width, rect.height - button_size[1])),
+            move_cb,
+        )
+
+
+class MousePadArea(Child):
+    def __init__(self, parent, rect, move_cb):
+        super().__init__(parent)
+        self.rect = rect
+        self.move_cb = move_cb
+        self.fingers = {}
+
+    def draw(self):
+        pygame.draw.rect(self.surf, "yellow", self.rect, 1)
+
+    def handle_fingerdown(self, event):
+        pos = (event.x, event.y)
+        if self.rect.collidepoint(
+            MultitouchHandler.map_coords(pos, self.surf.get_size())
+        ):
+            self.fingers[(event.touch_id, event.finger_id)] = [pos]
+
+    def handle_fingermotion(self, event):
+        key = (event.touch_id, event.finger_id)
+        if key not in self.fingers:
+            return
+        pos = (event.x, event.y)
+        if self.rect.collidepoint(
+            MultitouchHandler.map_coords(pos, self.surf.get_size())
+        ):
+            self.fingers[key].append(pos)
+        else:
+            self.fingers.pop(key)
+
+    def handle_fingerup(self, event):
+        key = (event.touch_id, event.finger_id)
+        if key not in self.fingers:
+            return
+        pos = (event.x, event.y)
+        if self.rect.collidepoint(
+            MultitouchHandler.map_coords(pos, self.surf.get_size())
+        ):
+            pass
+        self.fingers.pop(key)
index f88f23eaaf0e5dce4ecb15c09a1cd6071fcc7e85..430c438d7190832d73e4bf823cb5dbb68d54aeab 100644 (file)
@@ -1,12 +1,14 @@
 import os
 import sys
+from functools import partial
 from pathlib import Path
 
 import pygame
 
-from ui import Modal, Rect
+from ui import QuittableModal, Rect
 
 from .touchbutton import TouchButton
+from .layout import LayoutModal
 
 # what settings do we need?
 # - configure widgets
@@ -17,35 +19,32 @@ from .touchbutton import TouchButton
 #   - three available widgets: touchpad mouse, number keypad, cursor and navkeys
 
 
-class Settings(Modal):
+class SettingsModal(QuittableModal):
     def __init__(self, parent, rect):
         super().__init__(parent)
         self.rect = rect
-        Rect(self, self.rect, "black", "gray")
+        Rect(self, rect, "black", "gray")
         width = rect.width * 2 / 3
         height = rect.height / 7
         left = rect.left + (rect.width - width) / 2 + 8
         y = rect.top + height + 8
         rect_size = (width - 16, height - 16)
-        self.buttons = []
+        self.layout_modal = LayoutModal(parent)
         for args in (
-            ("Mute", self.mute, self.root.muted),
-            ("Layout...", self.layout),
+            ("Mute", None, self.root.muted),
+            ("Layout...", self.layout_modal.activate),
             ("Restart", self.restart),
             ("Exit", self.root.handle_quit),
             ("Back", self.deactivate),
         ):
-            self.buttons.append(
-                TouchButton(self, pygame.Rect((left, y), rect_size), *args)
-            )
+            button = TouchButton(self, pygame.Rect((left, y), rect_size), *args)
+            if args[0] == "Mute":
+                button.callback = partial(self.mute, button)
             y += height
 
-    def mute(self):
+    def mute(self, button):
         self.root.muted ^= True
-        self.buttons[0].highlight = self.root.muted
-
-    def layout(self):
-        print("stub layout")
+        button.highlight = self.root.muted
 
     def restart(self):
         executable = Path(sys.executable).name
index 95d345de129ce5fbf34786c7696c7fcdd2668a08..ae75ddbfd00e5e9c505c7c48d624c6a28f51484e 100755 (executable)
--- a/launch.py
+++ b/launch.py
@@ -15,6 +15,7 @@ from time import time
 from traceback import format_exception
 
 executable = Path(sys.executable).name
+pkgs = ("pygame", "evdev", "pynput")
 
 
 def tty_connected():
@@ -73,9 +74,9 @@ def run_cmd(cmd, expect_returncode=0):
     raise ChildProcessError(os.linesep.join(lines))
 
 
-def check_has_pygame():
-    return b"No module named pygame.__main__;" in run_cmd(
-        (executable, "-m", "pygame"), 1
+def check_has_pkg(pkg):
+    return f"No module named {pkg}.__main__;".encode() in run_cmd(
+        (executable, "-m", pkg), 1
     ).stderr
 
 
@@ -121,10 +122,13 @@ def setup_venv():
             raise SystemError("'venv' is not a directory.")
         run_cmd((sys.executable, "-m", "venv", str(venv_dir)))
     os.environ.update(get_venv_environ(venv_dir))
-    has_pygame = venv_dir_is_dir and check_has_pygame()
+    has_pkgs = (
+        venv_dir_is_dir
+        and all(check_has_pkg(pkg) for pkg in pkgs)
+    )
     if get_ipv4_is_connected():
         yesterday = time() - 86400
-        if not has_pygame or all(
+        if not has_pkgs or all(
             p.stat().st_mtime < yesterday for p in walk_outer(venv_dir)
         ):
             run_cmd(
@@ -135,34 +139,35 @@ def setup_venv():
                     "install",
                     "-qU",
                     "pip",
-                    "pygame",
-                    "pynput",
-                    "evdev",
+                    *pkgs
                 )
             )
             venv_dir.touch(0o755, True)
-    elif not has_pygame:
+    elif not has_pkgs:
         raise ConnectionError("Internet needed to install requirement: pygame")
 
 
 def pre_run():
-    pg = "pygame"
-    if pg in sys.modules:
-        return sys.modules[pg]
-    try:
-        with redirect_stdout(None):
-            pygame = import_module(pg)
-    except ImportError:
-        with exception_wrapper():
-            setup_venv()
-            os.execl(
-                sys.executable,
-                executable,
-                *sys.orig_argv[sys.orig_argv[0] == executable:],
-            )
-        exit(1)
-    pygame.init()
-    return pygame
+    modules = []
+    for pkg in pkgs:
+        if pkg in sys.modules:
+            modules.append(sys.modules[pkg])
+            continue
+        try:
+            with redirect_stdout(None):
+                modules.append(import_module(pkg))
+        except ImportError:
+            with exception_wrapper():
+                setup_venv()
+                os.execl(
+                    sys.executable,
+                    executable,
+                    *sys.orig_argv[sys.orig_argv[0] == executable:],
+                )
+            exit(1)
+        if pkg == "pygame":
+            modules[-1].init()
+    return modules
 
 
 def resolve_symlink(path):
@@ -205,7 +210,7 @@ def main():
         try_run(".".join((module_name, module_name)))
 
 
-pygame = pre_run()
+pygame, evdev, pynput = pre_run()
 
 if __name__ == "__main__":
     main()
index 1d5ba179478a327ddb63f6d90bccd29fce86ed6f..39523142ec1efaa58811b7c04859a21862b1a54d 100755 (executable)
--- a/memory.py
+++ b/memory.py
@@ -5,14 +5,7 @@ from pathlib import Path
 from secrets import choice
 
 from launch import pygame
-from ui import BaseRoot, Button, Child, Label, Modal, TextInput
-
-
-class QuittableModal(Modal):
-    def handle_quit(self, _=None):
-        self.parent.handle_quit()
-
-    key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}}
+from ui import BaseRoot, Button, Child, Label, QuittableModal, TextInput
 
 
 class NameMenu(QuittableModal):
index e422676577565c1eff673f8de05cbc9844cb1fcf..cb4cbfff9f2b7bdb2e0d19a97bacb779644e4ea9 100644 (file)
@@ -8,7 +8,7 @@ from .icon import Icon
 from .icon_button import IconButton
 from .label import Label
 from .message_box import MessageBox
-from .modal import Modal
+from .modal import Modal, QuittableModal
 from .parent import Parent
 from .rect import Rect
 from .root import BaseRoot
@@ -34,6 +34,7 @@ __all__ = [
     "MessageBox",
     "Modal",
     "Parent",
+    "QuittableModal",
     "Rect",
     "RepeatButton",
     "Scroll",
index 25530adc014953b92461e237c4543cbe39e53779..a33bbc7430cbfb655c11a520efe303addd3b6ef5 100644 (file)
@@ -6,7 +6,7 @@ class FPSWidget(Child):
 
     def __init__(self, parent):
         super().__init__(parent)
-        self.current_fps = None
+        self.current_fps = 0
 
     def update(self):
         new_fps = int(self.root.clock.get_fps())
index 4639ab3caa869c98d548207934a9d41c15fcf753..2a29cf1690be153d09a17b93bc273b5527d60c68 100644 (file)
@@ -33,3 +33,10 @@ class Modal(Focusable, Parent, Child):
     def draw(self):
         self.draw_modal()
         super().draw()
+
+
+class QuittableModal(Modal):
+    def handle_quit(self, _=None):
+        self.parent.handle_quit()
+
+    key_methods = {frozenset(): {pygame.K_ESCAPE: handle_quit}}