diff --git a/drinks_touch/elements/base_elm.py b/drinks_touch/elements/base_elm.py index e61f9fd3..be7cf6f2 100644 --- a/drinks_touch/elements/base_elm.py +++ b/drinks_touch/elements/base_elm.py @@ -1,3 +1,4 @@ +from __future__ import annotations import pygame from pygame import Surface @@ -17,6 +18,7 @@ def __init__( ): if pos is None: pos = (0, 0) + self.ts = 0 self.pos = pos self._height = height self._width = width @@ -77,7 +79,10 @@ def screen_pos(self): def box(self): return self.screen_pos + (self.width, self.height) - def event(self, event, pos=None): + def event(self, event: pygame.event.Event, pos=None) -> "BaseElm" | None: + """ + Returns the element that consumed the event + """ if pos is None and hasattr(event, "pos"): pos = event.pos if pos is None: @@ -89,24 +94,31 @@ def event(self, event, pos=None): ) for child in self.children: - if hasattr(event, "consumed") and event.consumed: - return - child.event(event, transformed_pos) + if consumed_by := child.event(event, transformed_pos): + return consumed_by if event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: if collides: self.focus = True - event.consumed = True + return self else: self.focus = False elif event.type == pygame.MOUSEBUTTONUP and event.button == 1: - if self.focus and collides: + had_focus = self.focus + self.focus = False + if had_focus and collides: if hasattr(self, "on_click"): self.on_click(*transformed_pos) - event.consumed = True - self.focus = False + return self + + def key_event(self, event: pygame.event.Event): + """ + Called when a keyboard key was pressed and the element is active + (active means: was clicked on before and therefore the ScreenManager + has set this element as active) + """ @property def visible(self): @@ -120,9 +132,9 @@ def collides_with(self, pos: tuple[int, int]) -> bool: and pygame.Rect(self.box).collidepoint(pos[0], pos[1]) ) - def render(self, *args, **kwargs) -> Surface: + def render(self, *args, **kwargs) -> Surface | None: surface = pygame.font.SysFont("monospace", 25).render( - "Return surface in render()", 1, (255, 0, 255) + "Return surface or None in render()", 1, (255, 0, 255) ) return surface diff --git a/drinks_touch/elements/elm_list.py b/drinks_touch/elements/elm_list.py deleted file mode 100644 index 12957bfc..00000000 --- a/drinks_touch/elements/elm_list.py +++ /dev/null @@ -1,42 +0,0 @@ -import pygame - -from .base_elm import BaseElm - - -class ElmList(BaseElm): - def __init__( - self, - children: list["BaseElm"] | None = None, - height=None, - width=None, - pos=(0, 0), - **kwargs - ): - super().__init__(children, pos, height, width) - self.pos = pos - self.elm_margin = kwargs.get("elm_margin", 5) - self.max_elm_count = kwargs.get("max_elm_count", 10) - self.elm_size = kwargs.get("elm_size", 25) - - self.__next_elm_post = self.pos[1] - self.elements = [] - - def add_elm(self, elm): - elm.pos = (self.pos[0], self.__next_elm_post) - self.elements.append(elm) - self.__update_elements() - - def __update_elements(self): - if len(self.elements) > self.max_elm_count: - self.elements.pop(0) - for e in self.elements: - e.pos = (self.pos[0], e.pos[1] - (e.height + self.elm_margin)) - else: - last_elm = self.elements[-1] - last_elm_pos = last_elm.pos[1] - self.__next_elm_post = last_elm_pos + last_elm.height + self.elm_margin - - def render(self) -> pygame.Surface: - for e in self.elements: - e.render() - return super().render() diff --git a/drinks_touch/elements/input_field.py b/drinks_touch/elements/input_field.py new file mode 100644 index 00000000..e7705b7c --- /dev/null +++ b/drinks_touch/elements/input_field.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +import enum +import logging + +import pygame + +from config import Color, Font +from elements.base_elm import BaseElm +from screens.screen_manager import ScreenManager + + +logger = logging.getLogger(__name__) + + +class InputType(enum.Enum): + TEXT = "text" + NUMBER = "number" + POSITIVE_NUMBER = "positive_number" + + +NUMERIC = list(range(pygame.K_0, pygame.K_9 + 1)) +ALPHABET = list(range(pygame.K_a, pygame.K_z + 1)) + [ + 223, # ß + 228, # ä + 246, # ö + 252, # ü +] +ALPHA_NUMERIC = NUMERIC + ALPHABET + + +class InputField(BaseElm): + + def __init__(self, *args, width, height, input_type=InputType.TEXT, **kwargs): + if input_type in [InputType.NUMBER, InputType.POSITIVE_NUMBER]: + self.valid_chars = NUMERIC + [ + pygame.K_PERIOD, + pygame.K_COMMA, + ] + if input_type == InputType.NUMBER: + self.valid_chars.append(pygame.K_MINUS) + self.max_decimal_places = kwargs.pop("max_decimal_places", None) + elif input_type == InputType.TEXT: + self.valid_chars = ALPHA_NUMERIC + [ + pygame.K_SPACE, + pygame.K_COMMA, + pygame.K_PERIOD, + pygame.K_MINUS, + pygame.K_UNDERSCORE, + ] + + super().__init__(*args, **kwargs, width=width, height=height) + self.input_type = input_type + self.text = "" + + def render(self, *args, **kwargs): + is_active = ScreenManager.get_instance().active_object is self + surface = pygame.Surface((self.width, self.height), pygame.SRCALPHA) + pygame.draw.rect( + surface, Color.NAVBAR_BACKGROUND.value, (0, 0, self.width, self.height) + ) + pygame.draw.rect(surface, Color.BLACK.value, (0, 0, self.width, self.height), 1) + pygame.draw.line( + surface, + Color.PRIMARY.value, + (0, self.height - 1), + (self.width, self.height - 1), + ) + font = pygame.font.Font(Font.SANS_SERIF.value, self.height - 10) + text_surface = font.render(self.text, 1, Color.PRIMARY.value) + surface.blit(text_surface, (5, 0)) + if is_active: + pygame.draw.rect( + surface, Color.PRIMARY.value, (0, 0, self.width, self.height), 1 + ) + if self.ts // 1 % 2 == 0: + x = text_surface.get_width() + 5 + pygame.draw.line( + surface, Color.PRIMARY.value, (x, 5), (x, self.height - 5), 2 + ) + return surface + + def key_event(self, event: pygame.event.Event): + if event.key == pygame.K_BACKSPACE: + self.text = self.text[:-1] + elif event.key in self.valid_chars: + char = event.unicode + if self.input_type in (InputType.NUMBER, InputType.POSITIVE_NUMBER): + if event.key == pygame.K_MINUS and self.text: + # only allow minus at the start + return + if event.key in (pygame.K_PERIOD, pygame.K_COMMA): + # only allow one decimal point, and convert comma to period + if "." in self.text: + return + char = "." + if "." in self.text and self.max_decimal_places is not None: + before_comma, _, after_comma = self.text.partition(".") + if len(after_comma) >= self.max_decimal_places: + return + self.text += char + else: + logger.info(f"Invalid key: {event.key}") diff --git a/drinks_touch/elements/progress.py b/drinks_touch/elements/progress.py index 0f6bd2fe..c20575df 100644 --- a/drinks_touch/elements/progress.py +++ b/drinks_touch/elements/progress.py @@ -55,7 +55,7 @@ def __default_tick(self, old_value, dt): else: return old_value - def render(self, dt) -> pygame.Surface: + def render(self, dt, *args, **kwargs) -> pygame.Surface: if self.tick is not None: self.value = self.tick(self.value, dt) diff --git a/drinks_touch/elements/spacer.py b/drinks_touch/elements/spacer.py new file mode 100644 index 00000000..a046d076 --- /dev/null +++ b/drinks_touch/elements/spacer.py @@ -0,0 +1,14 @@ +from elements.base_elm import BaseElm + + +class Spacer(BaseElm): + """ + Doesn't render anything, just takes up space. + Use "width" and "height" to set the size of the spacer. + """ + + def __init__(self, *args, width=0, height=0, **kwargs): + super().__init__(*args, **kwargs, width=width, height=height) + + def render(self, *args, **kwargs): + return None diff --git a/drinks_touch/elements/vbox.py b/drinks_touch/elements/vbox.py index ed8e3034..f3c9b5ca 100644 --- a/drinks_touch/elements/vbox.py +++ b/drinks_touch/elements/vbox.py @@ -32,7 +32,8 @@ def render(self, *args, **kwargs) -> pygame.Surface: for element in self.children: element.pos = (self.padding_left, y) element_surface = element.render(*args, **kwargs) - surface.blit(element_surface, element.pos) + if element_surface: + surface.blit(element_surface, element.pos) y += element.height + self.gap if self.focus: pygame.draw.rect( diff --git a/drinks_touch/game.py b/drinks_touch/game.py index 104d5140..5651b221 100755 --- a/drinks_touch/game.py +++ b/drinks_touch/game.py @@ -15,7 +15,7 @@ from database.storage import init_db, Session from drinks.drinks_manager import DrinksManager from notifications.notification import send_low_balances, send_summaries -from overlays import MouseOverlay +from overlays import MouseOverlay, BaseOverlay from screens.screen_manager import ScreenManager from screens.tasks_screen import TasksScreen from stats.stats import run as stats_send @@ -45,7 +45,7 @@ set_library_log_detail_level(EXTENDED) event_queue = queue.Queue() -overlays = [] +overlays: list[BaseOverlay] = [] screen_manager: ScreenManager | None = None @@ -124,6 +124,8 @@ def main(argv): if env.is_pi(): os.system("rsync -a sounds/ pi@pixelfun:sounds/ &") + pygame.key.set_repeat(500, 50) + t = 0 done = False diff --git a/drinks_touch/screens/profile.py b/drinks_touch/screens/profile.py index c9bdca99..b3f096fa 100644 --- a/drinks_touch/screens/profile.py +++ b/drinks_touch/screens/profile.py @@ -17,6 +17,7 @@ from .id_card_screen import IDCardScreen from .screen import Screen from .screen_manager import ScreenManager +from .transfer_balance_screen import TransferBalanceScreen class ProfileScreen(Screen): @@ -85,10 +86,10 @@ def on_start(self, *args, **kwargs): padding=20, ), Button( - text="Guthaben übertragen", + text="Guthaben übertragen (WIP)", on_click=functools.partial( - self.alert, - "Nicht implementiert", + self.goto, + TransferBalanceScreen(self.account), ), padding=20, ), diff --git a/drinks_touch/screens/screen.py b/drinks_touch/screens/screen.py index 869ebf4b..bd7d68b9 100644 --- a/drinks_touch/screens/screen.py +++ b/drinks_touch/screens/screen.py @@ -75,15 +75,14 @@ def render_alert(self) -> pygame.Surface: ) return alert - def event(self, event): + def event(self, event) -> BaseElm | None: if self._alert: if event.type == pygame.MOUSEBUTTONUP: self._alert = None return for obj in self.objects[::-1]: - if getattr(event, "consumed", False): - break - obj.event(event) + if consumed_by := obj.event(event): + return consumed_by @staticmethod def back(): diff --git a/drinks_touch/screens/screen_manager.py b/drinks_touch/screens/screen_manager.py index 3ba98d98..5b058c3c 100644 --- a/drinks_touch/screens/screen_manager.py +++ b/drinks_touch/screens/screen_manager.py @@ -16,6 +16,7 @@ class ScreenManager: MENU_BAR_HEIGHT = 65 def __init__(self): + self.ts = 0 self.current_screen = None self.surface = get_screen_surface() self.screen_history: list[Screen] = [] @@ -35,6 +36,7 @@ def __init__(self): ), self.timeout_widget, ] + self.active_object = None def set_idle_timeout(self, timeout: int): """ @@ -88,6 +90,9 @@ def nav_bar_visible(self): return len(self.screen_history) > 1 def render(self, dt): + self.ts += dt + if self.active_object: + self.active_object.ts += dt self.surface.fill(Color.BACKGROUND.value) current_screen = self.get_active() surface, debug_surface = current_screen.render(dt) @@ -120,7 +125,14 @@ def events(self, events): if pygame.mouse.get_pressed()[0]: self.set_idle_timeout(0) for event in events: - if event.type == pygame.MOUSEBUTTONUP and event.button == 1: + event_consumed = False + if event.type == pygame.MOUSEBUTTONDOWN: + ScreenManager.get_instance().active_object = None + if ( + event.type == pygame.MOUSEBUTTONUP + and event.button == 1 + or event.type == pygame.KEYDOWN + ): self.set_idle_timeout(screen.idle_timeout) if self.nav_bar_visible and hasattr(event, "pos"): transformed_pos = ( @@ -128,13 +140,18 @@ def events(self, events): event.pos[1] - self.surface.get_height() + self.MENU_BAR_HEIGHT, ) for obj in self.objects: - if getattr(event, "consumed", False): + if obj.event(event, transformed_pos): + event_consumed = True continue - obj.event(event, transformed_pos) - if getattr(event, "consumed", False): + if event_consumed: continue - screen.event(event) - # screen.events(events) + if active_object := screen.event(event): + active_object.ts = 0 + ScreenManager.get_instance().active_object = active_object + if event.type == pygame.KEYDOWN and ( + active_object := ScreenManager.get_instance().active_object + ): + active_object.key_event(event) @staticmethod def get_instance() -> "ScreenManager": diff --git a/drinks_touch/screens/transfer_balance_screen.py b/drinks_touch/screens/transfer_balance_screen.py new file mode 100644 index 00000000..4af0144e --- /dev/null +++ b/drinks_touch/screens/transfer_balance_screen.py @@ -0,0 +1,68 @@ +import config +from elements import Label +from elements.input_field import InputField, InputType +from elements.spacer import Spacer +from elements.vbox import VBox +from screens.screen import Screen + + +class TransferBalanceScreen(Screen): + + idle_timeout = 60 + + def __init__(self, account): + super().__init__() + self.account = account + + def on_start(self, *args, **kwargs): + self.objects = [ + Label( + text=self.account.name, + pos=(5, 5), + ), + VBox( + [ + Label( + text="Wie viel Euro möchtest du übertragen?", + size=20, + ), + InputField( + input_type=InputType.POSITIVE_NUMBER, + max_decimal_places=2, + width=config.SCREEN_WIDTH - 10, + height=50, + ), + Spacer(height=20), + Label( + text="An wen möchtest du Guthaben übertragen?", + size=20, + ), + InputField( + width=config.SCREEN_WIDTH - 10, + height=50, + ), + Spacer(height=40), + Label( + text="Work in progress. Es fehlt:", + size=15, + ), + Label( + text="- On-Screen-Keyboard", + size=15, + ), + Label( + text=" Theoretisch kannst du eine Tastatur anschließen :)", + size=15, + ), + Label( + text="- Auto-complete des Namens", + size=15, + ), + Label( + text="- Nächster Screen zum bestätigen", + size=15, + ), + ], + pos=(5, 100), + ), + ]