From: mar77i Date: Mon, 3 Feb 2025 01:31:11 +0000 (+0100) Subject: bookpaint mvp X-Git-Url: https://git.mar77i.info/?a=commitdiff_plain;h=ed81d6fe335a0531e3eacb6f3b90167b2d9c8bb8;p=zenbook_gui bookpaint mvp --- diff --git a/bookpaint/bookpaint.py b/bookpaint/bookpaint.py index 50162f5..3fd7307 100644 --- a/bookpaint/bookpaint.py +++ b/bookpaint/bookpaint.py @@ -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}, + } diff --git a/bookpaint/draw_ui.py b/bookpaint/draw_ui.py index b73c259..eb19e12 100644 --- a/bookpaint/draw_ui.py +++ b/bookpaint/draw_ui.py @@ -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 index 0000000..e69de29 diff --git a/layout/layout.py b/layout/layout.py new file mode 100755 index 0000000..5b32648 --- /dev/null +++ b/layout/layout.py @@ -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 diff --git a/ui/ui.py b/ui/ui.py index 61dbeb4..14b3948 100644 --- 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() diff --git a/zenbook_conf/zenbook_conf.py b/zenbook_conf/zenbook_conf.py index e6f3f49..ff67292 100644 --- a/zenbook_conf/zenbook_conf.py +++ b/zenbook_conf/zenbook_conf.py @@ -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):