diff --git a/frontend/config.py b/frontend/config.py index 1c6ad01..e54b6d1 100644 --- a/frontend/config.py +++ b/frontend/config.py @@ -20,4 +20,5 @@ class DISPLAY_MODES(Enum): SETTINGS = 2 RECIPE_SELECTION = 3 EDIT_RECIPE = 4 - DO_RECIPE = 5 \ No newline at end of file + DO_RECIPE = 5 + EDIT_STEP = 6 \ No newline at end of file diff --git a/frontend/views/buttons_manager.py b/frontend/views/buttons_manager.py index 0399b58..4a76a33 100644 --- a/frontend/views/buttons_manager.py +++ b/frontend/views/buttons_manager.py @@ -9,7 +9,7 @@ class ButtonsManager(View): def __init__(self, parent, im_size, center, curr_view): self.current_view: ButtonInterface = curr_view - self.long_press_threshold = 1000 # milliseconds + self.long_press_threshold = 500 # milliseconds self.left_press_start = None self.right_press_start = None self.both_press_start = None diff --git a/frontend/views/confirm.py b/frontend/views/confirm.py index 348c4be..902883b 100644 --- a/frontend/views/confirm.py +++ b/frontend/views/confirm.py @@ -1,5 +1,5 @@ from .base import View -from ..button_interface import ButtonInterface +from .button_interface import ButtonInterface class ConfirmView(View, ButtonInterface): def __init__(self, parent, im_size, center, diff --git a/frontend/views/main_view.py b/frontend/views/main_view.py index 07ad2a8..f6b6d3f 100644 --- a/frontend/views/main_view.py +++ b/frontend/views/main_view.py @@ -11,7 +11,7 @@ from . import NumberView, CircleView, TimerView from .button_interface import ButtonInterface from .buttons_manager import ButtonsManager -from .recipes import RecipeSelection, RecipeManager, EditRecipe, Recipe +from .recipes import RecipeSelection, RecipeManager, EditRecipe, Recipe, EditStep class MainView(tk.Frame, ButtonInterface): def __init__(self, parent, @@ -92,10 +92,21 @@ class MainView(tk.Frame, ButtonInterface): def enter_edit_recipe(self, recipe: Recipe = None): self.curr_mode = DISPLAY_MODES.EDIT_RECIPE self.buttons.current_view = EditRecipe(self, - self.im_size, self.center, - recipe=recipe, - recipe_manager=self.recipes_manager, - deactivate_command=self.enter_recipe_selection) + self.im_size, self.center, + recipe=recipe, + recipe_manager=self.recipes_manager, + edit_step_command=self.enter_edit_step, + deactivate_command=self.enter_recipe_selection) + self.refresh(0.0) + + def enter_edit_step(self, recipe: Recipe, step_idx: int): + self.curr_mode = DISPLAY_MODES.EDIT_STEP + self.buttons.current_view = EditStep(self, + self.im_size, self.center, + recipe=recipe, + step_index=step_idx, + recipe_manager=self.recipes_manager, + deactivate_command=lambda: self.enter_edit_recipe(recipe)) self.refresh(0.0) ################ VIEW MANAGEMENT ################ diff --git a/frontend/views/recipes/__init__.py b/frontend/views/recipes/__init__.py index c6555ba..b105765 100644 --- a/frontend/views/recipes/__init__.py +++ b/frontend/views/recipes/__init__.py @@ -1,4 +1,5 @@ from .recipe_selection import RecipeSelection from .recipe_manager import RecipeManager from .edit_recipe import EditRecipe +from .edit_step import EditStep from .recipe import Recipe \ No newline at end of file diff --git a/frontend/views/recipes/edit_recipe.py b/frontend/views/recipes/edit_recipe.py index 3365536..dc5c34a 100644 --- a/frontend/views/recipes/edit_recipe.py +++ b/frontend/views/recipes/edit_recipe.py @@ -5,16 +5,18 @@ from ..base import View from ..button_interface import ButtonInterface from .recipe_manager import RecipeManager -from .recipe import Recipe +from .recipe import Recipe, Step class EditRecipe(View, ButtonInterface): def __init__(self, parent, im_size, center, recipe_manager: RecipeManager, + edit_step_command, deactivate_command, recipe: Recipe = None): self.deactivate_command = deactivate_command self.recipe_manager = recipe_manager self.recipe = recipe + self.edit_step_command = edit_step_command if recipe is None: self.recipe = Recipe("New", []) @@ -24,7 +26,7 @@ class EditRecipe(View, ButtonInterface): super().__init__(parent, im_size, center) def _get_visual_steps(self): - steps = self.recipe.steps + ['+'] + steps = self.recipe.steps + ['+', 'BACK'] if len(steps) < 4: return steps, 0 @@ -60,13 +62,20 @@ class EditRecipe(View, ButtonInterface): offset = 15 for i in range(0, 90, r // 2): draw.circle((x + i, y_pos), r, fill='black') - if str(step) != '+': + + + if str(step) == '+': + draw.text((x, y_pos - 5), '+', fill='white') + elif str(step) == 'BACK': + draw.regular_polygon((x + 5, y_pos, 5), 3, fill='white', rotation=90) + else: step.step_type.render(draw, (x, y_pos - 5), fill='white') draw.text((x + 30, y_pos - 5), step.value_str, fill='white') - else: - draw.text((x, y_pos - 5), '+', fill='white') + elif str(step) == '+': draw.text((x, y_pos - 5), '+', fill='black') + elif str(step) == 'BACK': + draw.regular_polygon((x + 5, y_pos, 5), 3, fill='black', rotation=90) else: step.step_type.render(draw, (x, y_pos - 5), fill='black') draw.text((x + 30, y_pos - 5), step.value_str, fill='black') @@ -74,31 +83,39 @@ class EditRecipe(View, ButtonInterface): return im def left_press(self): - # edit entry - pass + self.selected_field = (self.selected_field - 1) % (len(self.recipe.steps) + 3) def left_long_press(self): + if self.selected_field == len(self.recipe.steps) + 2: + # back + self.deactivate_command() + elif self.selected_field == len(self.recipe.steps) + 1: + # add step + pass + else: + # edit name + self.edit_step_command(self.recipe, self.selected_field) + + def right_press(self): + self.selected_field = (self.selected_field + 1) % (len(self.recipe.steps) + 3) + + def right_long_press(self): # save self.recipe_manager.add_recipe(self.recipe) self.deactivate_command() - def right_press(self): - self.selected_field += 1 - if self.selected_field > len(self.recipe.steps) + 1: - self.selected_field = 0 - def has_button(self) -> Tuple[bool, bool, bool, bool]: return True, True, True, True def render_left_press(self, draw, x, y): - draw.regular_polygon((x, y, 5), 3, fill='black', rotation=270) + draw.regular_polygon((x, y+2, 5), 3, fill='black') def render_left_long_press(self, draw, x, y): - draw.text((x - 3, y - 5), 'Save', fill='black') + draw.text((x, y-5), 'Enter', fill='black') def render_right_press(self, draw, x, y): - draw.regular_polygon((x, y + 6, 5), 3, fill='black', rotation=180) + draw.regular_polygon((x, y+4, 5), 3, fill='black', rotation=180) def render_right_long_press(self, draw, x, y): - draw.text((x - 30, y - 5), 'Cancel', fill='black') \ No newline at end of file + draw.text((x - 20, y-5), 'Save', fill='black') \ No newline at end of file diff --git a/frontend/views/recipes/edit_step.py b/frontend/views/recipes/edit_step.py new file mode 100644 index 0000000..8f7222b --- /dev/null +++ b/frontend/views/recipes/edit_step.py @@ -0,0 +1,141 @@ +from typing import Tuple +from PIL import ImageDraw, Image +from time import time + +from MorseCodePy import decode + +from ..base import View +from ..button_interface import ButtonInterface + +from .recipe_manager import RecipeManager +from .recipe import Recipe, Step, StepType + +class EditStep(View, ButtonInterface): + def __init__(self, parent, im_size, center, + recipe: Recipe, + step_index: int, + recipe_manager: RecipeManager, + deactivate_command): + self.deactivate_command = deactivate_command + self.recipe_manager = recipe_manager + self.recipe = recipe + self.step_index = step_index + if step_index == 0: + self.step = recipe.name + + self.confirm_view = False + self.edit_step = 0 # 0: type, 1: step/name value + self.new_type = '' + self.new_value = '' + self.value_cursor = 0 + self.value_cursor_pulse = 0.0 + self.morse_buffer = '' + self.morse_code_language = 'english' + if isinstance(self.step, Step) and \ + self.step.step_type in [StepType.START_TIME, StepType.WEIGH]: + self.morse_code_language = 'numbers' + + self.last_input = None + self.letter_timeout = 2.0 # seconds + + super().__init__(parent, im_size, center) + + def update_weight(self, weight: float) -> Image.Image: + im = self.bkg_im.copy() + draw = ImageDraw.Draw(im) + + x = 40 + + if self.last_input is not None: + if time() - self.last_input > self.letter_timeout: + if len(self.morse_buffer) < 10: + self.value_cursor += 1 + self.new_value += decode(self.morse_buffer, language=self.morse_code_language).upper() + else: + self.value_cursor -= 1 + self.new_value = self.new_value[:-1] + # process morse buffer + self.last_input = None + self.morse_buffer = '' + + if isinstance(self.step, str): + draw.text((x, 10), "Name:", fill='black') + draw.text((x, 30), self.new_value, fill='black') + if self.value_cursor_pulse > 1.0: + draw.rectangle((x + self.value_cursor * 8, 28, x + self.value_cursor * 8 + 8, 40), fill='black') + if self.value_cursor_pulse > 2.0: + self.value_cursor_pulse = 0.0 + self.value_cursor_pulse += 0.1 + + draw.line((x, 45, x + 80, 45), fill='black') + elif self.edit_step == 0: + pass + else: + pass + + # visual_steps, start = self._get_visual_steps() + + # for idx, step in enumerate(visual_steps): + # y_pos = 60 + idx * 20 + # if start + idx + 1 == self.selected_field: + # r = 10 + # offset = 15 + # for i in range(0, 90, r // 2): + # draw.circle((x + i, y_pos), r, fill='black') + + + # if str(step) == '+': + # draw.text((x, y_pos - 5), '+', fill='white') + # elif str(step) == 'BACK': + # draw.regular_polygon((x + 5, y_pos, 5), 3, fill='white', rotation=90) + # else: + # step.step_type.render(draw, (x, y_pos - 5), fill='white') + # draw.text((x + 30, y_pos - 5), step.value_str, fill='white') + + # elif str(step) == '+': + # draw.text((x, y_pos - 5), '+', fill='black') + # elif str(step) == 'BACK': + # draw.regular_polygon((x + 5, y_pos, 5), 3, fill='black', rotation=90) + # else: + # step.step_type.render(draw, (x, y_pos - 5), fill='black') + # draw.text((x + 30, y_pos - 5), step.value_str, fill='black') + + return im + + def left_press(self): + self.selected_field = (self.selected_field - 1) % (len(self.recipe.steps) + 3) + + def left_long_press(self): + if self.selected_field == len(self.recipe.steps) + 2: + # back + self.deactivate_command() + elif self.selected_field == len(self.recipe.steps) + 1: + # add step + pass + elif self.selected_field == 0: + # edit name + pass + else: + # edit entry + pass + + def right_press(self): + self.last_input = time() + self.morse_buffer += '.' + + def right_long_press(self): + self.last_input = time() + self.morse_buffer += '-' + + def has_button(self) -> Tuple[bool, bool, bool, bool]: + return True, True, True, False + + + def render_left_press(self, draw, x, y): + draw.regular_polygon((x, y+2, 5), 3, fill='black') + + def render_left_long_press(self, draw, x, y): + draw.text((x, y-5), 'Next', fill='black') + + def render_right_press(self, draw, x, y): + draw.text((x - 30, y), 'Morse', fill='black') \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2b5784f..228d298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ description = "Add your description here" readme = "README.md" requires-python = ">=3.13" dependencies = [ + "morsecodepy>=4.2", "pandas>=2.3.3", "pillow>=11.3.0", "pyserial>=3.5", diff --git a/uv.lock b/uv.lock index ef38ad1..7448532 100644 --- a/uv.lock +++ b/uv.lock @@ -7,6 +7,7 @@ name = "frontend-dev" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "morsecodepy" }, { name = "pandas" }, { name = "pillow" }, { name = "pyserial" }, @@ -15,12 +16,19 @@ dependencies = [ [package.metadata] requires-dist = [ + { name = "morsecodepy", specifier = ">=4.2" }, { name = "pandas", specifier = ">=2.3.3" }, { name = "pillow", specifier = ">=11.3.0" }, { name = "pyserial", specifier = ">=3.5" }, { name = "python-toolkit", git = "https://git.magnuss.link/JannTer/python-toolkit" }, ] +[[package]] +name = "morsecodepy" +version = "4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/93/00b15782b16c869beff6c8efc685a518a2f972eee3c0fb6e2a4e2f3c3c39/morsecodepy-4.2.tar.gz", hash = "sha256:f2e85f9ba065feb48b9a08a42974e6a76d7cc8ca879caad10a4d34f9db01c244", size = 8579, upload-time = "2025-09-30T19:18:14.291Z" } + [[package]] name = "numpy" version = "2.3.4"