1
0
Fork 0
menu/menu.py

298 lines
12 KiB
Python
Raw Normal View History

2024-09-08 11:43:21 +00:00
#!/usr/bin/env python3
2024-09-08 14:44:47 +00:00
import math
2024-09-08 15:50:38 +00:00
import argparse
import json
from enum import Enum, auto
from time import sleep
from threading import Thread
from queue import Queue, Empty
from typing import Tuple, Callable, TypedDict, List, Dict, Optional
2024-09-08 11:43:21 +00:00
import pygame
2024-09-08 14:44:47 +00:00
import pygame.freetype
2024-09-08 17:26:29 +00:00
import requests
2024-09-08 14:44:47 +00:00
from iboard import IBoard
from screensavers import ClockScreensaver
2024-09-08 14:44:47 +00:00
class ButtonDef(TypedDict):
text: str
2024-09-08 14:50:18 +00:00
cbk: Callable[[], None]
2024-09-08 11:43:21 +00:00
class ActionType(Enum):
MSG = auto() # Shows a message for about 5 seconds
GET = auto() # Performs a HTTP GET
QUIT = auto() # Quits an application
class Action(TypedDict):
2024-09-08 15:50:38 +00:00
label: str
action_type: ActionType
action_param: Optional[str]
action_message: Optional[str]
2024-09-08 15:50:38 +00:00
2024-09-08 11:43:21 +00:00
class Button:
2024-09-09 17:28:55 +00:00
PRESS_OFFSET = 2
2024-09-10 15:10:06 +00:00
HOVER_ANIMATION_STEPS = 5
2024-09-09 17:28:55 +00:00
2024-09-10 16:22:13 +00:00
def __init__(self, position: pygame.Rect, text: str, theme: Dict, cbk: Callable[[], None]):
2024-09-08 11:43:21 +00:00
self.position: pygame.Rect
self.set_position(position)
2024-09-08 14:44:47 +00:00
self.text = text
2024-09-10 16:22:13 +00:00
self.theme = theme
2024-09-08 11:43:21 +00:00
self.cbk = cbk
2024-09-08 14:44:47 +00:00
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)
2024-09-10 16:22:13 +00:00
self.bg_color = pygame.Color(self.theme["btn_bg_color"])
self.hover_bg_color = pygame.Color(self.theme["btn_hover_color"])
2024-09-10 15:10:06 +00:00
self.hover_animation_step = 0
def get_bg_color(self):
if self.pos_is_inside(pygame.mouse.get_pos()):
self.hover_animation_step = min(self.hover_animation_step + 1, self.HOVER_ANIMATION_STEPS)
else:
self.hover_animation_step = max(self.hover_animation_step - 1, 0)
if self.hover_animation_step == self.HOVER_ANIMATION_STEPS:
return self.hover_bg_color
if self.hover_animation_step == 0:
return self.bg_color
new_color = tuple(map(lambda v: v[0] + (((v[1]-v[0])/self.HOVER_ANIMATION_STEPS)*self.hover_animation_step),
zip(self.bg_color, self.hover_bg_color)))
return new_color
def needs_redrawing(self) -> bool:
return self.hover_animation_step == self.HOVER_ANIMATION_STEPS or self.hover_animation_step == 0
2024-09-08 11:43:21 +00:00
def set_position(self, position: pygame.Rect):
self.position = position
2024-09-09 17:28:55 +00:00
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
2024-09-08 11:43:21 +00:00
def draw(self, screen: pygame.Surface):
2024-09-10 15:10:06 +00:00
pygame.draw.rect(screen, self.get_bg_color(), self.get_position(), 0)
2024-09-10 16:22:13 +00:00
self.font.render_to(screen, self.get_text_pos(), self.text, fgcolor=self.theme["btn_text_color"])
2024-09-08 11:43:21 +00:00
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):
2024-09-10 16:22:13 +00:00
def __init__(self, screen_rect: pygame.Rect, text: str, theme: Dict):
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)
2024-09-10 16:22:13 +00:00
self.theme = theme
self.was_drawn_already = False
def handle_event(self, _):
pass
def draw(self, screen: pygame.Surface, force_redraw: bool):
if not self.was_drawn_already or force_redraw:
2024-09-10 16:22:13 +00:00
screen.fill(self.theme["message_bg_color"])
self.font.render_to(screen, self.text_pos, self.text, fgcolor=self.theme["message_text_color"])
self.was_drawn_already = True
2024-09-08 11:43:21 +00:00
class MenuBoard(IBoard):
2024-09-08 14:44:47 +00:00
BUTTON_MARGINS = 10
2024-09-10 16:22:13 +00:00
def __init__(self, screen_rect: pygame.Rect, buttons: List[ButtonDef], theme: Dict):
2024-09-08 15:50:38 +00:00
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
2024-09-08 14:44:47 +00:00
button_positions = self.generate_button_positions(screen_rect)
2024-09-10 16:22:13 +00:00
self.buttons = list(map(lambda d: Button(next(button_positions), d['text'], theme, d['cbk']), buttons))
self.theme = theme
2024-09-08 14:44:47 +00:00
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)
2024-09-08 11:43:21 +00:00
def handle_event(self, event: pygame.event.Event):
for b in self.buttons:
b.handle_event(event)
def draw(self, screen: pygame.Surface, force_redraw: bool):
2024-09-10 15:10:06 +00:00
if force_redraw or any(map(Button.needs_redrawing, self.buttons)):
2024-09-10 16:22:13 +00:00
screen.fill(self.theme["menu_bg_color"])
for b in self.buttons:
b.draw(screen)
2024-09-08 11:43:21 +00:00
class App:
2024-09-09 17:56:38 +00:00
FPS = 30
2024-09-10 16:22:13 +00:00
def __init__(self, actions: List[Action], theme: Dict, fullscreen: bool, screensaver_delay: int):
2024-09-08 11:43:21 +00:00
pygame.init()
info = pygame.display.Info()
flags = 0
if fullscreen:
flags = pygame.FULLSCREEN
self.screen = pygame.display.set_mode((info.current_w, info.current_h), flags=flags)
2024-09-08 11:43:21 +00:00
self.clock = pygame.time.Clock()
self.running = False
2024-09-10 16:22:13 +00:00
self.theme = theme
buttons = list(map(lambda u: ButtonDef(text=u['label'], cbk=self.button_press_handler(u)), actions))
2024-09-10 16:22:13 +00:00
self.board: IBoard = MenuBoard(self.get_screen_rect(), buttons, theme)
self.task_q = Queue()
2024-09-09 17:56:38 +00:00
self.screensaver = ClockScreensaver()
self.TICKS_UNTIL_SCREENSAVER = screensaver_delay * self.FPS
2024-09-09 17:56:38 +00:00
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
2024-09-08 15:20:47 +00:00
def thr_fun():
def end_thr():
self.board = previous_board
2024-09-08 15:20:47 +00:00
try:
sleep_time = int(action['action_param'])
sleep(sleep_time)
except Exception as e:
def f(ex):
def x():
self.board = MessageBoard(self.get_screen_rect(), f"Exception caught: {ex}",
2024-09-10 16:22:13 +00:00
theme=self.theme)
return x
self.task_q.put(f(e))
sleep(5)
finally:
self.task_q.put(end_thr)
2024-09-08 15:20:47 +00:00
process_thr = Thread(target=thr_fun)
process_thr.start()
2024-09-10 16:22:13 +00:00
self.board = MessageBoard(self.get_screen_rect(), action['action_message'], theme=self.theme)
return impl
2024-09-08 11:43:21 +00:00
2024-09-08 17:26:29 +00:00
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['action_param'])
print(f"GET {action['action_param']}: {ret}")
2024-09-08 17:26:29 +00:00
self.task_q.put(end_thr)
process_thr = Thread(target=thr_fun)
process_thr.start()
2024-09-10 16:22:13 +00:00
self.board = MessageBoard(self.get_screen_rect(), action['action_message'], theme=self.theme)
2024-09-08 17:26:29 +00:00
return impl
def button_press_handler(self, action: Action) -> Callable[[], None]:
if action['action_type'] == ActionType.MSG:
return self.show_message_handler(action)
2024-09-08 17:26:29 +00:00
elif action['action_type'] == ActionType.GET:
return self.get_handler(action)
elif action['action_type'] == ActionType.QUIT:
return self.quit
raise NotImplementedError(action['action_type'])
2024-09-08 11:43:21 +00:00
def loop(self):
self.running = True
needs_redrawing = True
2024-09-08 11:43:21 +00:00
while self.running:
try:
self.task_q.get_nowait()()
except Empty:
pass
2024-09-08 11:43:21 +00:00
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
2024-09-09 17:56:38 +00:00
if self.screensaver_ticks != 0:
self.board.handle_event(event)
self.screensaver_ticks = self.TICKS_UNTIL_SCREENSAVER
needs_redrawing = True
2024-09-08 11:43:21 +00:00
2024-09-09 17:56:38 +00:00
if self.screensaver_ticks == 0:
self.screensaver.draw(self.screen, False)
2024-09-09 17:56:38 +00:00
else:
self.board.draw(self.screen, needs_redrawing)
2024-09-09 17:56:38 +00:00
self.screensaver_ticks -= 1
needs_redrawing = False
2024-09-08 11:43:21 +00:00
pygame.display.flip()
2024-09-09 17:56:38 +00:00
self.clock.tick(self.FPS)
2024-09-08 11:43:21 +00:00
pygame.quit()
2024-09-08 14:50:18 +00:00
def quit(self):
2024-09-10 16:22:13 +00:00
self.board = MessageBoard(self.get_screen_rect(), "Exiting...", theme=self.theme)
2024-09-08 14:50:18 +00:00
self.running = False
2024-09-08 11:43:21 +00:00
def get_url_defs(config_data: dict) -> List[Action]:
2024-09-08 15:50:38 +00:00
url_defs = []
for d in data['actions']:
url_defs.append(Action(label=d['label'],
action_type=ActionType[d['action_type']],
action_param=d.get('action_param'),
action_message=d.get('action_message')))
2024-09-08 15:50:38 +00:00
return url_defs
2024-09-08 11:43:21 +00:00
if __name__ == '__main__':
2024-09-08 15:50:38 +00:00
parser = argparse.ArgumentParser()
parser.add_argument("config_path", help="Path to the config file.", type=str)
parser.add_argument("--no-fullscreen", help="Fullscreen", action='store_true')
parser.add_argument("--screensaver-delay", help="Screensaver delay, in seconds. Default=30",
type=int, default=30)
2024-09-08 15:50:38 +00:00
args = parser.parse_args()
2024-09-08 16:02:13 +00:00
url_defs: List[Action] = []
2024-09-08 16:02:13 +00:00
with open(args.config_path, "rb") as f:
data = json.load(f)
url_defs = get_url_defs(data)
2024-09-10 16:22:13 +00:00
app = App(url_defs, theme=data["theme"], fullscreen=not args.no_fullscreen,
screensaver_delay=args.screensaver_delay)
2024-09-08 11:43:21 +00:00
app.loop()