1
0
Fork 0
menu/menu.py

195 lines
6.7 KiB
Python

#!/usr/bin/env python3
import math
import argparse
import json
from time import sleep
from threading import Thread
from queue import Queue, Empty
from abc import abstractmethod
from typing import Tuple, Callable, TypedDict, List
import pygame
import pygame.freetype
class ButtonDef(TypedDict):
text: str
cbk: Callable[[], None]
class UrlDef(TypedDict):
label: str
url: str
class IBoard:
@abstractmethod
def draw(self, screen: pygame.Surface):
raise NotImplementedError()
@abstractmethod
def handle_event(self, event: pygame.event.Event):
raise NotImplementedError()
class Button:
def __init__(self, position: pygame.Rect, text: str, cbk: Callable[[], None]):
self.position: pygame.Rect
self.set_position(position)
self.text = text
self.cbk = cbk
self.font = pygame.freetype.SysFont(name="Sans", size=20)
text_rect = self.font.get_rect(self.text)
self.text_pos = pygame.Rect((self.position.left + (self.position.width - text_rect.width)/2),
(self.position.top + (self.position.height - text_rect.height)/2),
text_rect.width, text_rect.height)
def set_position(self, position: pygame.Rect):
self.position = position
def draw(self, screen: pygame.Surface):
pygame.draw.rect(screen, "black", self.position, 0)
self.font.render_to(screen, self.text_pos, self.text, fgcolor="pink")
def pos_is_inside(self, pos: Tuple) -> bool:
return (self.position.left < pos[0] and (self.position.left + self.position.width) > pos[0]) and \
(self.position.top < pos[1] and (self.position.top + self.position.height) > pos[1])
def handle_event(self, event: pygame.event.Event):
if event.type == pygame.MOUSEBUTTONUP:
if event.button == 1 and self.pos_is_inside(event.pos):
self.cbk()
class MessageBoard(IBoard):
def __init__(self, screen_rect: pygame.Rect, text: str, bg_color: str):
self.text = text
self.font = pygame.freetype.SysFont(name="Sans", size=30)
text_rect = self.font.get_rect(self.text)
self.text_pos = pygame.Rect((screen_rect.left + (screen_rect.width - text_rect.width)/2),
(screen_rect.top + (screen_rect.height - text_rect.height)/2),
text_rect.width, text_rect.height)
self.bg_color = bg_color
def handle_event(self, _):
pass
def draw(self, screen: pygame.Surface):
screen.fill(self.bg_color)
self.font.render_to(screen, self.text_pos, self.text, fgcolor="black")
class MenuBoard(IBoard):
BUTTON_MARGINS = 10
def __init__(self, screen_rect: pygame.Rect, buttons: List[ButtonDef], bg_color: str):
self.rows = math.floor(math.sqrt(len(buttons)))
self.cols = math.ceil(math.sqrt(len(buttons)))
if (self.rows * self.cols) < len(buttons):
# extra row if buttons don't fit
self.rows += 1
button_positions = self.generate_button_positions(screen_rect)
self.buttons = list(map(lambda d: Button(next(button_positions), d['text'], d['cbk']), buttons))
self.bg_color = bg_color
def generate_button_positions(self, screen_rect: pygame.Rect):
current_button_row = 0
current_button_col = 0
button_width = math.floor((screen_rect.width - (self.cols + 1) * self.BUTTON_MARGINS) / self.cols)
button_height = math.floor((screen_rect.height - (self.rows + 1) * self.BUTTON_MARGINS) / self.rows)
while (current_button_row * current_button_col) < (self.cols * self.rows):
top = self.BUTTON_MARGINS + (current_button_row * (button_height + self.BUTTON_MARGINS))
left = self.BUTTON_MARGINS + (current_button_col * (button_width + self.BUTTON_MARGINS))
current_button_col += 1
if current_button_col >= self.cols:
current_button_col = 0
current_button_row += 1
yield pygame.Rect(left, top, button_width, button_height)
def handle_event(self, event: pygame.event.Event):
for b in self.buttons:
b.handle_event(event)
def draw(self, screen: pygame.Surface):
screen.fill(self.bg_color)
for b in self.buttons:
b.draw(screen)
class App:
def __init__(self, urls: List[UrlDef], bg_color: str):
pygame.init()
info = pygame.display.Info()
self.screen = pygame.display.set_mode((info.current_w, info.current_h), flags=pygame.FULLSCREEN)
self.clock = pygame.time.Clock()
self.running = False
self.bg_color = bg_color
buttons = list(map(lambda u: ButtonDef(text=u['label'], cbk=self.button_press_handler(u['url'])), urls))
buttons.append(ButtonDef(text="Exit", cbk=self.quit))
self.board: IBoard = MenuBoard(self.get_screen_rect(), buttons, bg_color)
self.task_q = Queue()
def get_screen_rect(self) -> pygame.Rect:
return pygame.Rect(0, 0, self.screen.get_width(), self.screen.get_height())
def button_press_handler(self, url: str) -> Callable[[], None]:
def impl():
previous_board = self.board
def thr_fun():
def end_thr():
self.board = previous_board
sleep(5)
self.task_q.put(end_thr)
process_thr = Thread(target=thr_fun)
process_thr.start()
self.board = MessageBoard(self.get_screen_rect(), f"Fetching: {url}", bg_color=self.bg_color)
return impl
def loop(self):
self.running = True
while self.running:
try:
self.task_q.get_nowait()()
except Empty:
pass
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
self.board.handle_event(event)
self.board.draw(self.screen)
pygame.display.flip()
self.clock.tick(30)
pygame.quit()
def quit(self):
self.board = MessageBoard(self.get_screen_rect(), "Exiting...", bg_color=self.bg_color)
self.running = False
def get_url_defs(config_data: dict) -> List[UrlDef]:
url_defs = []
for d in data['urls']:
url_defs.append(UrlDef(label=d['label'], url=d['url']))
return url_defs
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("config_path", help="Path to the config file.", type=str)
args = parser.parse_args()
url_defs: List[UrlDef] = []
with open(args.config_path, "rb") as f:
data = json.load(f)
url_defs = get_url_defs(data)
app = App(url_defs, bg_color=data["bg_color"])
app.loop()