1
0
Fork 0
menu/menu.py

274 lines
9.8 KiB
Python

#!/usr/bin/env python3
import math
import argparse
import json
from enum import Enum, auto
from time import sleep, strftime
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 Screensaver(IBoard):
def handle_event(self, _):
pass
class ClockScreensaver(Screensaver):
def __init__(self):
self.font = pygame.freetype.SysFont(name="Sans", size=20)
def draw(self, screen: pygame.Surface):
text = strftime("%H:%M:%S")
screen.fill("black")
pygame.Rect(0, 0, screen.get_width(), screen.get_height())
text_rect = self.font.get_rect(text)
text_pos = pygame.Rect((screen.get_width() - text_rect.width)/2,
(screen.get_height() - text_rect.height)/2,
text_rect.width, text_rect.height)
self.font.render_to(screen, text_pos, text, fgcolor="pink")
class Button:
PRESS_OFFSET = 2
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 get_position(self) -> pygame.Rect:
if pygame.mouse.get_pressed(3)[0] and self.pos_is_inside(pygame.mouse.get_pos()):
return pygame.Rect(self.position.left + self.PRESS_OFFSET, self.position.top + self.PRESS_OFFSET,
self.position.width, self.position.height)
return self.position
def get_text_pos(self) -> pygame.Rect:
if pygame.mouse.get_pressed(3)[0] and self.pos_is_inside(pygame.mouse.get_pos()):
return pygame.Rect(self.text_pos.left + self.PRESS_OFFSET, self.text_pos.top + self.PRESS_OFFSET,
self.text_pos.width, self.text_pos.height)
return self.text_pos
def draw(self, screen: pygame.Surface):
pygame.draw.rect(screen, "black", self.get_position(), 0)
self.font.render_to(screen, self.get_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:
FPS = 30
TICKS_UNTIL_SCREENSAVER = 1 * FPS
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()
self.screensaver = ClockScreensaver()
self.screensaver_ticks = self.TICKS_UNTIL_SCREENSAVER
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
if self.screensaver_ticks != 0:
self.board.handle_event(event)
self.screensaver_ticks = self.TICKS_UNTIL_SCREENSAVER
if self.screensaver_ticks == 0:
self.screensaver.draw(self.screen)
else:
self.board.draw(self.screen)
self.screensaver_ticks -= 1
pygame.display.flip()
self.clock.tick(self.FPS)
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()