+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(
"_",
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):
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
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},
+ }
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:
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
+ )
--- /dev/null
+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
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:
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"):
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:
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):
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
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:
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()