diff --git a/frontend/__main__.py b/frontend/__main__.py index ab3e1c1..668f50f 100644 --- a/frontend/__main__.py +++ b/frontend/__main__.py @@ -9,8 +9,9 @@ import pandas as pd from python_toolkit.serial_reader import SerialReader from python_toolkit.serial_mock import SerialMock from python_toolkit.gui.connect import ConnectFrame + from .config import DEFAULT_CALIB_WEIGHT, DEFAULT_CALIB, DISPLAY_TYPES, MOV_AVG_DEFAULTS -from .views import CombinedView +from .views import MainView class WeightApp(tk.Tk): def __init__(self, weight_reader: SerialReader): @@ -55,6 +56,9 @@ class WeightApp(tk.Tk): # Checkboxes for multiple view selection self.view_vars = {} for display_type in DISPLAY_TYPES: + if display_type == DISPLAY_TYPES.TIMER: + continue + var = tk.BooleanVar() if display_type == DISPLAY_TYPES.NUMBER: var.set(True) # Default to NUMBER view @@ -127,14 +131,11 @@ class WeightApp(tk.Tk): selected_types |= display_type # Combine using bitwise OR # Remove existing views - if self.view is not None: - self.view.update_views(selected_types) - else: - self.view = CombinedView(self, - tare_command=self.weight_reader.tare, - calibrate_command=self.calibrate) - - self.update_weight_display() + if self.view is None: + self.view = MainView(self, + tare_command=self.weight_reader.tare, + calibrate_command=self.calibrate) + self.view.update_views(selected_types) def update_weight_display(self): diff --git a/frontend/views/__init__.py b/frontend/views/__init__.py index d800f9a..2ef4c3e 100644 --- a/frontend/views/__init__.py +++ b/frontend/views/__init__.py @@ -1,4 +1,4 @@ from .number import NumberView from .circle import CircleView from .timer import TimerView -from .combined_view import CombinedView \ No newline at end of file +from .main_view import MainView \ No newline at end of file diff --git a/frontend/views/circle.py b/frontend/views/circle.py index 59f915e..52264b5 100644 --- a/frontend/views/circle.py +++ b/frontend/views/circle.py @@ -6,7 +6,7 @@ from .base import View class CircleView(View): - def __init__(self, parent, size, center, radius_offset=10, **kwargs): + def __init__(self, parent, size, center, radius_offset=11, **kwargs): self.target_radius = min(center) - radius_offset super().__init__(parent, size, center, **kwargs) diff --git a/frontend/views/combined_view.py b/frontend/views/combined_view.py deleted file mode 100644 index a0ff510..0000000 --- a/frontend/views/combined_view.py +++ /dev/null @@ -1,81 +0,0 @@ -import io -import tkinter as tk -from tkinter import Frame, Canvas, ttk, PhotoImage - -from PIL import Image, ImageChops - -from ..config import DISPLAY_TYPES -from . import NumberView, CircleView, TimerView - -class CombinedView(tk.Frame): - def __init__(self, parent, - tare_command=None, calibrate_command=None, - **kwargs): - super().__init__(parent, **kwargs) - self.views = [] - self.timer_view = None # Timer view is always active - self.tare_command = tare_command - self.calibrate_command = calibrate_command - - self.actions = Frame(self) - self.actions.pack() - self.calibrate_button = ttk.Button(self.actions, text="Calibrate", command=calibrate_command) - self.calibrate_button.pack() - self.tare_button = ttk.Button(self.actions, text="Tare", command=tare_command) - self.tare_button.pack() - - self.im_size = (168, 144) - self.center = (168 // 2, 144 // 2) - - # Create timer view that's always active - self.timer_view = TimerView(self.actions, self.im_size, self.center) - - self.canvas = Canvas(self, width=168, height=144, background='white', - highlightthickness=1, highlightbackground="black") - self.canvas.pack() - - - def update_views(self, selected_types: DISPLAY_TYPES): - # Clear only the selectable views, not the timer - for v in self.views: - if v.ui is not None: - v.ui.destroy() - self.views.clear() - - if selected_types & DISPLAY_TYPES.NUMBER: - number_view = NumberView(self.actions, self.im_size, self.center) - self.views.append(number_view) - - if selected_types & DISPLAY_TYPES.CIRCLE: - circle_view = CircleView(self.actions, self.im_size, self.center) - self.views.append(circle_view) - - - def refresh(self, weight: float): - ims = [] - - # Always include timer view - if self.timer_view: - timer_im = self.timer_view.update_weight(weight) - ims.append(timer_im) - - # Add other selected views - for view in self.views: - im = view.update_weight(weight) - ims.append(im) - - self.canvas.delete("all") - # Combine images by logical_and - if ims: - combined_im = ims[0] - for im in ims[1:]: - combined_im = ImageChops.invert(ImageChops.logical_xor(combined_im, im)) - - # Convert PIL image to bytes - buffer = io.BytesIO() - combined_im.save(buffer, format='PNG') - buffer.seek(0) - - # Load into PhotoImage and display on canvas - self.photo = PhotoImage(data=buffer.getvalue()) - self.canvas.create_image(0, 0, anchor="nw", image=self.photo) \ No newline at end of file diff --git a/frontend/views/main_view.py b/frontend/views/main_view.py new file mode 100644 index 0000000..2ef929f --- /dev/null +++ b/frontend/views/main_view.py @@ -0,0 +1,166 @@ +import io +import tkinter as tk +from tkinter import Frame, Canvas, ttk, PhotoImage + +from PIL import Image, ImageChops + +from ..config import DISPLAY_TYPES +from . import NumberView, CircleView, TimerView + +class MainView(tk.Frame): + def __init__(self, parent, + tare_command=None, + calibrate_command=None, + **kwargs): + super().__init__(parent, **kwargs) + self.views = [] + self.timer_view = None # Timer view is always active + self.tare_command = tare_command + self.calibrate_command = calibrate_command + + # Button press detection variables + self.left_press_start = None + self.right_press_start = None + self.long_press_threshold = 1000 # 1 second in milliseconds + self.left_press_job = None + self.right_press_job = None + + self.actions = Frame(self) + self.actions.pack() + self.calibrate_button = ttk.Button(self.actions, text="Calibrate", command=calibrate_command) + self.calibrate_button.pack() + + # self.tare_button = ttk.Button(self.actions, text="Tare", command=tare_command) + # self.tare_button.pack() + + self.im_size = (168, 144) + self.center = (168 // 2, 144 // 2) + + # Create timer view that's always active + self.timer_view = TimerView(self.actions, self.im_size, self.center) + + self.canvas = Canvas(self, width=168, height=144, background='white', + highlightthickness=1, highlightbackground="black") + self.canvas.pack() + + self.device_frame = Frame(self.actions) + self.left_button = ttk.Button(self.device_frame, text="Left") + # Bind mouse events for press detection + self.left_button.bind("", self._left_button_press_start) + self.left_button.bind("", self._left_button_press_end) + self.left_button.pack(side="left") + + self.right_button = ttk.Button(self.device_frame, text="Right") + # Bind mouse events for press detection + self.right_button.bind("", self._right_button_press_start) + self.right_button.bind("", self._right_button_press_end) + self.right_button.pack(side="right") + self.device_frame.pack() + + + ################ VIEW MANAGEMENT ################ + + def update_views(self, selected_types: DISPLAY_TYPES): + # Clear only the selectable views, not the timer + for v in self.views: + if v.ui is not None: + v.ui.destroy() + self.views.clear() + + if selected_types & DISPLAY_TYPES.NUMBER: + number_view = NumberView(self.actions, self.im_size, self.center) + self.views.append(number_view) + + if selected_types & DISPLAY_TYPES.CIRCLE: + circle_view = CircleView(self.actions, self.im_size, self.center) + self.views.append(circle_view) + + + def refresh(self, weight: float): + ims = [] + + # Always include timer view + if self.timer_view: + timer_im = self.timer_view.update_weight(weight) + ims.append(timer_im) + + # Add other selected views + for view in self.views: + im = view.update_weight(weight) + ims.append(im) + + self.canvas.delete("all") + # Combine images by logical_and + if ims: + combined_im = ims[0] + for im in ims[1:]: + combined_im = ImageChops.invert(ImageChops.logical_xor(combined_im, im)) + + # Convert PIL image to bytes + buffer = io.BytesIO() + combined_im.save(buffer, format='PNG') + buffer.seek(0) + + # Load into PhotoImage and display on canvas + self.photo = PhotoImage(data=buffer.getvalue()) + self.canvas.create_image(0, 0, anchor="nw", image=self.photo) + + + + + + ########### BUTTON PRESS HANDLING ########### + + def _left_button_press_start(self, event): + """Handle left button press start""" + self.left_press_start = self.after_idle(lambda: None) # Get current time reference + # Schedule long press detection + self.left_press_job = self.after(self.long_press_threshold, self._left_long_press_detected) + + def _left_button_press_end(self, event): + """Handle left button press end""" + if self.left_press_job: + self.after_cancel(self.left_press_job) + self.left_press_job = None + # If we get here, it was a short press + self.left_button_press() + self.left_press_start = None + + def _left_long_press_detected(self): + """Called when long press threshold is reached for left button""" + self.left_press_job = None + self.left_button_long_press() + + def _right_button_press_start(self, event): + """Handle right button press start""" + self.right_press_start = self.after_idle(lambda: None) # Get current time reference + # Schedule long press detection + self.right_press_job = self.after(self.long_press_threshold, self._right_long_press_detected) + + def _right_button_press_end(self, event): + """Handle right button press end""" + if self.right_press_job: + self.after_cancel(self.right_press_job) + self.right_press_job = None + # If we get here, it was a short press + self.right_button_press() + self.right_press_start = None + + def _right_long_press_detected(self): + """Called when long press threshold is reached for right button""" + self.right_press_job = None + self.right_button_long_press() + + def left_button_press(self): + self.tare_command() + + def right_button_press(self): + self.timer_view.toggle_timer() + + def left_button_long_press(self): + """Handle long press on left button""" + print("Left button long press") # Replace with your logic + pass + + def right_button_long_press(self): + self.timer_view.reset_timer() \ No newline at end of file diff --git a/frontend/views/timer.py b/frontend/views/timer.py index 98e46c3..255476a 100644 --- a/frontend/views/timer.py +++ b/frontend/views/timer.py @@ -27,12 +27,6 @@ class TimerView(View): self.goal_entry.insert(0, "0") self.goal_entry.pack() - # Timer buttons - self.start_stop_button = ttk.Button(self.ui, text="Start", command=self.toggle_timer) - self.start_stop_button.pack(side=tk.LEFT, padx=2) - - self.reset_button = ttk.Button(self.ui, text="Reset", command=self.reset_timer) - self.reset_button.pack(side=tk.LEFT, padx=2) def toggle_timer(self): if self.is_running: @@ -107,20 +101,3 @@ class TimerView(View): pass return im - - def _draw_progress_arc(self, draw, progress): - """Draw a progress arc around the outer circle""" - if progress <= 0: - return - - # Draw filled arc by drawing multiple lines from center to circumference - center_x, center_y = self.center - # Start from top (270 degrees) and go clockwise - start_angle = 270 - num_steps = max(1, int(360 * progress)) - - for i in range(num_steps): - angle = math.radians(start_angle + i) - end_x = center_x + self.outer_radius * math.cos(angle) - end_y = center_y + self.outer_radius * math.sin(angle) - draw.line([(center_x, center_y), (end_x, end_y)], fill='black', width=1) \ No newline at end of file