]> git.mar77i.info Git - zenbook_gui/commitdiff
bookpaint mvp
authormar77i <mar77i@protonmail.ch>
Mon, 3 Feb 2025 01:31:11 +0000 (02:31 +0100)
committermar77i <mar77i@protonmail.ch>
Mon, 3 Feb 2025 01:35:12 +0000 (02:35 +0100)
bookpaint/bookpaint.py
bookpaint/draw_ui.py
layout/__init__.py [new file with mode: 0644]
layout/layout.py [new file with mode: 0755]
ui/ui.py
zenbook_conf/zenbook_conf.py

index 50162f5c36cbbef1335bf3b4381701278f69825f..3fd7307a3d3e5ba42e5e7388b7c260c1af4efa14 100644 (file)
@@ -1,12 +1,73 @@
+import sys
+from logging import getLogger
+from pathlib import Path
+
 import pygame
 
-from ui.ui import Button, DrawImage, FPSWidget, Modal, Root
+from ui.ui import Button, Child, FPSWidget, Label, Modal, RepeatButton, Root
+from layout.layout import BarLayout
+
+from .draw_ui import DrawImage
+
+logger = getLogger(__name__)
 
 
 class BookPaintMenu(Modal):
+    PAGE_EXT = ".png"
+    root: "BookPaint"
+
     def __init__(self, root):
         super().__init__(root)
         size = self.surf.get_size()
+        label_bar_layout = BarLayout(size, 1536, 48)
+        button_bar_layout = BarLayout(size, 1664, 48)
+        manager = self.root.book_manager
+        self.page_label = Label(
+            root,
+            label_bar_layout.get_rect((256, 96)),
+            manager.get_page_of_total(),
+        )
+        self.filename_label = Label(
+            root,
+            label_bar_layout.get_rect((256, 96)),
+            manager.file_name.name,
+        )
+        self.nav_children = (
+            Button(
+                root,
+                button_bar_layout.get_rect((192, 96)),
+                "|<",
+                manager.first_page,
+            ),
+            RepeatButton(
+                root,
+                button_bar_layout.get_rect((192, 96)),
+                "<",
+                manager.prev_page,
+            ),
+            RepeatButton(
+                root,
+                button_bar_layout.get_rect((192, 96)),
+                ">",
+                manager.next_page,
+            ),
+            Button(
+                root,
+                button_bar_layout.get_rect((192, 96)),
+                ">|",
+                manager.last_page,
+            ),
+            Button(
+                root,
+                button_bar_layout.get_rect((192, 96)),
+                "new",
+                manager.new_page,
+            ),
+            self.page_label,
+            self.filename_label,
+        )
+        self.hidden = None
+        self.nav_rect = pygame.Rect((672, 1456), (1536, 288))
         root.children.extend(
             (
                 Button(
@@ -21,18 +82,174 @@ class BookPaintMenu(Modal):
                     "_",
                     root.iconify,
                 ),
-                FPSWidget(root),
+                *self.nav_children,
             ),
         )
+        label_bar_layout()
+        button_bar_layout()
 
     def key_escape(self):
         self.deactivate()
 
     KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}}
 
+    def update_nav_bar(self, page_of_total, file_name):
+        self.page_label.value = page_of_total
+        self.filename_label.value = file_name
+        root = self.root
+        self.backsurf.blit(root.draw_image.surface, (0, 0))
+        if self.hidden is not None:
+            return
+        hidden_children = [
+            child
+            for child in root.children
+            if child is not self and child not in self.nav_children
+        ]
+        for child in hidden_children:
+            root.children.remove(child)
+        self.hidden = (self.tintsurf, hidden_children)
+        self.tintsurf = None
+        self.dirty = True
+
     def draw(self):
-        self.root.surf.fill(0x333333)
         super().draw()
+        pygame.draw.rect(self.root.surf, "black", self.nav_rect)
+        pygame.draw.rect(self.root.surf, "white", self.nav_rect, 1)
+
+    def deactivate(self):
+        super().deactivate()
+        self.root.book_paint_menu = None
+
+    def handle_mousemotion(self, ev):
+        if self.hidden is None or self.nav_rect.collidepoint(ev.pos):
+            return
+        self.tintsurf = self.hidden[0]
+        self.root.children.extend(self.hidden[1])
+        self.hidden = None
+        self.dirty = True
+
+
+class BookManager(Child):
+    root: "BookPaint"
+    IMAGE_EXT = ".png"
+
+    @staticmethod
+    def get_book_dir(argv):
+        num_args = len(argv)
+        if num_args > 2:
+            raise ValueError("Too many arguments")
+        elif num_args == 2:
+            book_dir = Path(argv[1])
+            if not book_dir.exists() or not book_dir.is_dir():
+                raise ValueError(f"Path does not exist or is not a dir: {argv[1]}")
+            return book_dir
+        return Path().absolute()
+
+    @classmethod
+    def get_pages(cls, book_dir):
+        pages = set()
+        for file in book_dir.iterdir():
+            if file.suffixes != [cls.IMAGE_EXT]:
+                continue
+            try:
+                page_no = int(file.name[:-len(cls.IMAGE_EXT)], 10)
+            except ValueError:
+                logger.info(f"Skipping {file.name}: failed to parse page nunber")
+                continue
+            pages.add(iter((page_no, file)))
+        return [next(x) for x in sorted(pages, key=next)]
+
+    @property
+    def page_zero(self):
+        return self.book_dir / f"0{self.IMAGE_EXT}"
+
+    def __init__(self, root):
+        super().__init__(root)
+        self.book_dir = self.get_book_dir(sys.argv)
+        self.pages = self.get_pages(self.book_dir)
+        num_pages = len(self.pages)
+        if num_pages > 0:
+            self.current_page = num_pages - 1
+            self.file_name = self.pages[self.current_page]
+        else:
+            self.current_page = 0
+            self.file_name = self.page_zero
+            self.pages.append(self.file_name)
+
+    def maybe_load(self):
+        if self.file_name.exists():
+            return pygame.image.load(self.file_name)
+        return None
+
+    def get_page_of_total(self):
+        return f"{self.current_page + 1} / {len(self.pages)}"
+
+    def get_last_existing_page(self):
+        page_iter = iter(reversed(self.pages))
+        try:
+            page = next(page_iter)
+            while not page.exists():
+                page = next(page_iter)
+        except StopIteration:
+            return self.page_zero
+        return page
+
+    def new_page(self):
+        self.current_page = len(self.pages)
+        last_page = self.get_last_existing_page()
+        self.file_name = (
+            self.book_dir
+            / f"{int(last_page.name[:-len(self.IMAGE_EXT)], 10) + 1}{self.IMAGE_EXT}"
+        )
+        if self.file_name in self.pages:
+            self.current_page = self.pages.index(self.file_name)
+        else:
+            self.pages.append(self.file_name)
+        self.root.draw_image.surface.fill(self.root.background_color)
+        self.dirty = True
+        self.maybe_update_menu()
+
+    def maybe_update_menu(self):
+        menu = self.root.book_paint_menu
+        if menu is not None and menu in self.root.children:
+            menu.update_nav_bar(self.get_page_of_total(), self.file_name.name)
+
+    def prev_page(self):
+        if self.current_page == 0:
+            return
+        self.update_current_page(self.current_page - 1)
+
+    def next_page(self):
+        if self.current_page == len(self.pages) - 1:
+            return
+        self.update_current_page(self.current_page + 1)
+
+    def first_page(self):
+        self.update_current_page(0)
+
+    def last_page(self):
+        if len(self.pages) == 0:
+            return
+        self.update_current_page(len(self.pages) - 1)
+
+    def update_current_page(self, current_page):
+        if self.current_page == current_page:
+            return
+        self.current_page = current_page
+        if self.current_page >= len(self.pages):
+            self.current_page = max(len(self.pages) - 1, 0)
+        if self.current_page == 0:
+            self.file_name = self.page_zero
+        else:
+            self.file_name = self.pages[self.current_page]
+        self.root.draw_image.load_surf(self.maybe_load(), self.root.background_color)
+        self.dirty = True
+        self.maybe_update_menu()
+
+    def save(self):
+        draw_image = self.root.draw_image
+        pygame.image.save(draw_image.surface, self.file_name)
+        draw_image.drawing_dirty = False
 
 
 class BookPaint(Root):
@@ -44,22 +261,21 @@ class BookPaint(Root):
             pygame.display.set_mode(flags=pygame.FULLSCREEN),
             pygame.font.Font(None, size=96),
         )
-        self.children.extend(
-            (
-                DrawImage(
-                    self,
-                    self.surf.get_rect(),
-                    self.BACKGROUND_COLOR,
-                ),
-                FPSWidget(
-                    self,
-                ),
-            )
+        # todo: this should be configurable per book
+        self.background_color = self.BACKGROUND_COLOR
+        self.book_manager = BookManager(self)
+        self.book_paint_menu = None
+        self.draw_image = DrawImage(
+            self,
+            self.surf.get_rect(),
+            self.BACKGROUND_COLOR,
+            self.book_manager.maybe_load(),
         )
+        self.children.extend((self.book_manager, self.draw_image, FPSWidget(self)))
 
     def key_escape(self):
-        if isinstance(self.children[0], DrawImage):
-            BookPaintMenu(self)
+        if self.draw_image in self.children:
+            self.book_paint_menu = BookPaintMenu(self)
 
     def quit(self):
         self.running = False
@@ -67,4 +283,11 @@ class BookPaint(Root):
     def iconify(self):
         pygame.display.iconify()
 
-    KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}}
+    def key_save(self):
+        if self.draw_image in self.children:
+            self.book_manager.save()
+
+    KEY_METHODS = {
+        frozenset(set()): {pygame.K_ESCAPE: key_escape},
+        frozenset({pygame.KMOD_CTRL}): {pygame.K_s: key_save},
+    }
index b73c2590a625d13fba5f756e63da71c18d9d3542..eb19e125a416f089f54e97408f9385204e8befc2 100644 (file)
@@ -1,20 +1,31 @@
 import pygame
 
-from ui.ui import Child
+from ui.ui import Button, Child
 
 
 class DrawImage(Child):
-    def __init__(self, root, rect, background_color=None):
+    def __init__(self, root, rect, background_color=None, load_surf=None):
         super().__init__(root)
         self.pos = rect.topleft
         self.surface = pygame.Surface(rect.size, 0, 24)
-        if background_color is not None:
-            self.surface.fill(background_color)
+        self.load_surf(load_surf, background_color)
         self.last_pos = None
         self.color = "white"
         self.line = pygame.draw.line
+        self.drawing_dirty = False
+
+    def load_surf(self, load_surf, background_color):
+        self.drawing_dirty = False
+        if load_surf is None:
+            self.surface.fill(background_color)
+            return
+        size = self.surface.get_size()
+        if load_surf.get_size() != size:
+            load_surf = pygame.transform.scale(load_surf, size)
+        self.surface.blit(load_surf, (0, 0))
 
     def draw_line(self, pos):
+        self.drawing_dirty = True
         if self.last_pos is not None:
             self.line(self.surface, self.color, self.last_pos, pos)
         else:
@@ -40,3 +51,15 @@ class DrawImage(Child):
 
     def draw(self):
         self.surf.blit(self.surface, self.pos)
+
+
+class ColorButton(Button):
+    def __init__(self, root, rect, color, callback, is_active=False):
+        super().__init__(root, rect, None, callback, is_active)
+        self.color = color
+
+    def draw(self):
+        pygame.draw.rect(self.surf, self.color, self.rect)
+        pygame.draw.rect(
+            self.surf, "gray" if self.pushed else "honeydew4", self.rect, 8
+        )
diff --git a/layout/__init__.py b/layout/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/layout/layout.py b/layout/layout.py
new file mode 100755 (executable)
index 0000000..5b32648
--- /dev/null
@@ -0,0 +1,52 @@
+import pygame
+
+
+class MenuLayout:
+    def __init__(self, size, num_columns, center_at_y, spacer_y):
+        self.size = size
+        self.num_columns = num_columns
+        self.columns = [[] for _ in range(num_columns)]
+        self.center_at_y = center_at_y
+        self.spacer_y = spacer_y
+
+    def get_center_x(self, column):
+        return self.size[0] * (column + 1) // (self.num_columns + 1)
+
+    def get_rect(self, column, rect_size):
+        rect = pygame.Rect(
+            (self.get_center_x(column) - rect_size[0] // 2, 0), rect_size
+        )
+        self.columns[column].append(rect)
+        return rect
+
+    def __call__(self):
+        for column in self.columns:
+            total_height = (len(column) - 1) * self.spacer_y + sum(
+                r.height for r in column
+            )
+            y = 0
+            for rect in column:
+                rect.top = self.center_at_y - total_height // 2 + y
+                y += rect.height + self.spacer_y
+
+
+class BarLayout:
+    def __init__(self, size, center_at_y, spacer_x):
+        self.size = size
+        self.center_at_y = center_at_y
+        self.spacer_x = spacer_x
+        self.row = []
+
+    def get_rect(self, rect_size):
+        rect = pygame.Rect((0, self.center_at_y - rect_size[1] // 2), rect_size)
+        self.row.append(rect)
+        return rect
+
+    def __call__(self):
+        total_width = (len(self.row) - 1) * self.spacer_x + sum(
+            r.width for r in self.row
+        )
+        x = (self.size[0] - total_width) // 2
+        for rect in self.row:
+            rect.left = x
+            x += rect.width + self.spacer_x
index 61dbeb4222963027c84f13e5cfdaa41ed80c0839..14b3948ea3b4258dde736351cf13d705fe34e67d 100644 (file)
--- a/ui/ui.py
+++ b/ui/ui.py
@@ -39,17 +39,9 @@ class Parent(EventMethodDispatcher):
     def __init__(self):
         self.children = []
 
-    def get_event_objects(self):
-        return (self, *self.children)
-
-    def handle_event_for_child(self, child, ev):
-        child.handle_event(ev)
-        return True
-
-    def call_event_handlers(self, ev):
-        for child in self.get_event_objects():
-            if not self.handle_event_for_child(child, ev):
-                break
+    def handle_event(self, ev):
+        for child in (super(), *self.children):
+            child.handle_event(ev)
 
     def update(self):
         for child in self.children:
@@ -116,13 +108,11 @@ class Root(Parent):
 
     KEY_METHODS = {frozenset(set()): {pygame.K_ESCAPE: key_escape}}
 
-    def get_event_objects(self):
-        return filter(None, (self, self.cursor, *self.children))
-
-    def handle_event_for_child(self, child, ev):
-        if child is not None:
+    def handle_event(self, ev):
+        for child in (super(Parent, self), *self.children):
             child.handle_event(ev)
-        return self.running and not self.stop_event
+            if not self.running or self.stop_event:
+                break
 
     def draw(self):
         if hasattr(self, "BACKGROUND_COLOR"):
@@ -138,7 +128,7 @@ class Root(Parent):
         while True:
             for ev in pygame.event.get():
                 self.stop_event = False
-                self.call_event_handlers(ev)
+                self.handle_event(ev)
                 if not self.running:
                     break
             if not self.running:
@@ -352,11 +342,11 @@ class Switch(Child):
     def set_value(self, value):
         if value == self.value:
             return
-        self.value = value
-        if value is None:
+        if None in (value, self.value):
             self.moving_since = time() - self.MOVE_FOR_SEC / 2
         else:
             self.moving_since = time()
+        self.value = value
 
     def handle_mousebuttondown(self, ev):
         if ev.button == 1 and self.rect.collidepoint(ev.pos):
@@ -378,6 +368,13 @@ class Cursor(Child):
         self.key = None
         self.repeat_ts = None
         self.pos = self.pos_from_offset(x_offset)
+        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 pos_from_offset(self, x_offset):
         value = self.text_input.value
@@ -627,9 +624,7 @@ class TextInput(Child):
 
     def blur(self, restore=False):
         if self.cursor is not None:
-            old_value = self.cursor.old_value
-            self.cursor = None
-            self.dirty = True
+            old_value = self.cursor.remove()
             if restore:
                 self.value = old_value
             elif self.value != old_value:
@@ -753,7 +748,8 @@ class Modal(Child):
 
     def draw(self):
         self.surf.blit(self.backsurf, (0, 0))
-        self.surf.blit(self.tintsurf, (0, 0))
+        if self.tintsurf:
+            self.surf.blit(self.tintsurf, (0, 0))
 
     def deactivate(self):
         self.root.children.clear()
index e6f3f49ad7aacbdc714de3b857b2f73495a35c60..ff672925c0013e9389b4f68989e912a029ed1a79 100644 (file)
@@ -3,7 +3,7 @@ from functools import partial
 import pygame
 
 from .bluetooth import BluetoothConf
-from ui.ui import Button, FPSWidget, Icon, IconButton, Switch, UIParent
+from ui.ui import Button, FPSWidget, Icon, IconButton, Root, Switch
 from .shapes import (
     bluetooth,
     laptop_double,
@@ -16,7 +16,7 @@ from .xinput import XinputConf
 from .xrandr import XrandrConf
 
 
-class ZenbookConf(UIParent):
+class ZenbookConf(Root):
     BACKGROUND_COLOR = 0x333333
 
     def __init__(self):