1
0
Fork 0
menu/menu.py

229 lines
7.9 KiB
Python

#!/usr/bin/env python3
import math
import argparse
import json
from enum import Enum, auto
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
import requests
class ButtonDef(TypedDict):
text: str
cbk: Callable[[], None]
class ActionType(Enum):
MSG = auto() # Shows a message for about 5 seconds
GET = auto() # Performs a HTTP GET
class Action(TypedDict):
label: str
action_type: ActionType
param: 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[Action], 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)), 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 show_message_handler(self, action: Action) -> 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(), action['param'], bg_color=self.bg_color)
return impl
def get_handler(self, action: Action) -> Callable[[], None]:
def impl():
previous_board = self.board
def thr_fun():
def end_thr():
self.board = previous_board
ret = requests.get(action['param'])
print(f"GET {action['param']}: {ret}")
self.task_q.put(end_thr)
process_thr = Thread(target=thr_fun)
process_thr.start()
self.board = MessageBoard(self.get_screen_rect(), f"GETting {action['param']}", bg_color=self.bg_color)
return impl
def button_press_handler(self, action: Action) -> Callable[[], None]:
if action['action_type'] == ActionType.MSG:
return self.show_message_handler(action)
elif action['action_type'] == ActionType.GET:
return self.get_handler(action)
raise NotImplementedError(action['action_type'])
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[Action]:
url_defs = []
for d in data['urls']:
url_defs.append(Action(label=d['label'],
action_type=ActionType[d['action_type']],
param=d['action_param']))
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[Action] = []
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()