From aad02f0d367da1fb51576b45ebb8c9b7ade00c5a Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 29 Jul 2025 17:42:38 -0700 Subject: [PATCH] feat: Improve canvas scrolling functionality with enhanced mouse wheel event handling --- src/ui_manager.py | 142 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 119 insertions(+), 23 deletions(-) diff --git a/src/ui_manager.py b/src/ui_manager.py index 6b0f8b8..1b904d8 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -70,18 +70,6 @@ class UIManager: input_frame = ttk.Frame(canvas) input_frame.grid_columnconfigure(1, weight=1) - # Configure canvas scrolling - def configure_scroll_region(event=None): - canvas.configure(scrollregion=canvas.bbox("all")) - - def on_mousewheel(event): - canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") - - input_frame.bind("", configure_scroll_region) - canvas.bind("", on_mousewheel) # Windows/Linux - canvas.bind("", lambda e: canvas.yview_scroll(-1, "units")) # Linux - canvas.bind("", lambda e: canvas.yview_scroll(1, "units")) # Linux - # Place canvas and scrollbar in the container canvas.grid(row=0, column=0, sticky="nsew") scrollbar.grid(row=0, column=1, sticky="ns") @@ -94,8 +82,53 @@ class UIManager: canvas_width = canvas.winfo_width() canvas.itemconfig(canvas_window, width=canvas_width) + # Configure canvas scrolling + def configure_scroll_region(event=None): + canvas.configure(scrollregion=canvas.bbox("all")) + + def on_mousewheel(event): + # Check if canvas is scrollable before scrolling + if canvas.cget("scrollregion"): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def on_mousewheel_linux_up(event): + # Linux mouse wheel up + if canvas.cget("scrollregion"): + canvas.yview_scroll(-1, "units") + + def on_mousewheel_linux_down(event): + # Linux mouse wheel down + if canvas.cget("scrollregion"): + canvas.yview_scroll(1, "units") + + input_frame.bind("", configure_scroll_region) canvas.bind("", configure_canvas_width) + # Bind mouse wheel events to canvas and main container + canvas.bind("", on_mousewheel) # Windows/Linux + canvas.bind("", on_mousewheel_linux_up) # Linux + canvas.bind("", on_mousewheel_linux_down) # Linux + main_container.bind("", on_mousewheel) # Windows/Linux + main_container.bind("", on_mousewheel_linux_up) # Linux + main_container.bind("", on_mousewheel_linux_down) # Linux + + # Bind mouse wheel to input frame and its children for better scrolling + self._bind_mousewheel_to_widget_tree(input_frame, canvas) + + # Set focus to canvas to ensure it receives scroll events + canvas.focus_set() + + # Add mouse enter/leave events to manage focus for scrolling + def on_mouse_enter(event): + canvas.focus_set() + + def on_mouse_leave(event): + # Don't change focus when leaving to avoid disrupting user interaction + pass + + main_container.bind("", on_mouse_enter) + canvas.bind("", on_mouse_enter) + # Create variables for symptoms symptom_vars: dict[str, tk.IntVar] = { "depression": tk.IntVar(value=0), @@ -168,6 +201,11 @@ class UIManager: # Set default date to today date_var.set(datetime.now().strftime("%m/%d/%Y")) + # Ensure mouse wheel binding is applied to all newly created widgets + main_container.update_idletasks() + canvas.configure(scrollregion=canvas.bbox("all")) + self._bind_mousewheel_to_widget_tree(input_frame, canvas) + # Return all UI elements and variables return { "frame": main_container, @@ -311,17 +349,48 @@ class UIManager: canvas.itemconfig(canvas_window, width=canvas_width) def on_mousewheel(event): - canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + # Check if canvas is scrollable before scrolling + if canvas.cget("scrollregion"): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def on_mousewheel_linux_up(event): + # Linux mouse wheel up + if canvas.cget("scrollregion"): + canvas.yview_scroll(-1, "units") + + def on_mousewheel_linux_down(event): + # Linux mouse wheel down + if canvas.cget("scrollregion"): + canvas.yview_scroll(1, "units") main_container.bind("", configure_scroll_region) canvas.bind("", configure_canvas_width) + + # Bind mouse wheel events to canvas and edit window canvas.bind("", on_mousewheel) # Windows/Linux - canvas.bind("", lambda e: canvas.yview_scroll(-1, "units")) # Linux - canvas.bind("", lambda e: canvas.yview_scroll(1, "units")) # Linux + canvas.bind("", on_mousewheel_linux_up) # Linux + canvas.bind("", on_mousewheel_linux_down) # Linux + edit_win.bind("", on_mousewheel) # Windows/Linux + edit_win.bind("", on_mousewheel_linux_up) # Linux + edit_win.bind("", on_mousewheel_linux_down) # Linux # Bind mouse wheel to main container and its children for better scrolling self._bind_mousewheel_to_widget_tree(main_container, canvas) + # Set focus to canvas to ensure it receives scroll events + canvas.focus_set() + + # Add mouse enter/leave events to manage focus for scrolling + def on_mouse_enter(event): + canvas.focus_set() + + def on_mouse_leave(event): + # Don't change focus when leaving to avoid disrupting user interaction + pass + + edit_win.bind("", on_mouse_enter) + canvas.bind("", on_mouse_enter) + # Unpack values - handle both old and new CSV formats if len(values) == 10: # Old format: date, dep, anx, slp, app, bup, hydro, gaba, prop, note @@ -427,6 +496,9 @@ class UIManager: edit_win.update_idletasks() canvas.configure(scrollregion=canvas.bbox("all")) + # Ensure mouse wheel binding is applied to all newly created widgets + self._bind_mousewheel_to_widget_tree(main_container, canvas) + # Make window modal edit_win.focus_set() edit_win.grab_set() @@ -968,18 +1040,42 @@ class UIManager: """Recursively bind mouse wheel events to all widgets in the tree.""" def on_mousewheel(event): - canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + # Check if canvas is scrollable before scrolling + if canvas.cget("scrollregion"): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + def on_mousewheel_linux_up(event): + if canvas.cget("scrollregion"): + canvas.yview_scroll(-1, "units") + + def on_mousewheel_linux_down(event): + if canvas.cget("scrollregion"): + canvas.yview_scroll(1, "units") # Bind to the widget itself - widget.bind("", on_mousewheel) - widget.bind("", lambda e: canvas.yview_scroll(-1, "units")) - widget.bind("", lambda e: canvas.yview_scroll(1, "units")) + try: + widget.bind("", on_mousewheel) + widget.bind("", on_mousewheel_linux_up) + widget.bind("", on_mousewheel_linux_down) + except tk.TclError: + # Some widgets might not support binding + pass # Recursively bind to all children - for child in widget.winfo_children(): - # Skip certain widgets that have their own scrolling behavior - if not isinstance(child, tk.Text | tk.Listbox | tk.Canvas): - self._bind_mousewheel_to_widget_tree(child, canvas) + try: + for child in widget.winfo_children(): + # Skip widgets that have their own scrolling behavior or are problematic + skip_types = (tk.Text, tk.Listbox, tk.Canvas, ttk.Notebook) + if not isinstance(child, skip_types): + self._bind_mousewheel_to_widget_tree(child, canvas) + elif isinstance(child, ttk.Notebook): + # For notebooks, bind to their tab frames + for tab_id in child.tabs(): + tab_widget = child.nametowidget(tab_id) + self._bind_mousewheel_to_widget_tree(tab_widget, canvas) + except tk.TclError: + # Handle potential errors when accessing children + pass def _create_edit_fields( self,