From 30750710b8ddc98cf82b2da879a514a9e9892245 Mon Sep 17 00:00:00 2001 From: William Valentin Date: Tue, 29 Jul 2025 17:28:52 -0700 Subject: [PATCH] feat: Enhance edit window UI with improved layout and scrolling functionality --- src/ui_manager.py | 640 +++++++++++++++++++++++++++++++++++++++++--- test_edit_window.py | 68 +++++ 2 files changed, 675 insertions(+), 33 deletions(-) create mode 100644 test_edit_window.py diff --git a/src/ui_manager.py b/src/ui_manager.py index 36f5ab3..6b0f8b8 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -277,14 +277,50 @@ class UIManager: def create_edit_window( self, values: tuple[str, ...], callbacks: dict[str, Callable] ) -> tk.Toplevel: - """Create a new window for editing an entry.""" + """Create a new window for editing an entry with improved UI.""" edit_win: tk.Toplevel = tk.Toplevel(master=self.root) edit_win.title("Edit Entry") edit_win.transient(self.root) # Make window modal - edit_win.minsize(400, 300) + edit_win.minsize(600, 700) + edit_win.geometry("800x800") - # Configure grid columns to expand properly - edit_win.grid_columnconfigure(1, weight=1) + # Create scrollable container + canvas = tk.Canvas(edit_win, highlightthickness=0) + scrollbar = ttk.Scrollbar(edit_win, orient="vertical", command=canvas.yview) + canvas.configure(yscrollcommand=scrollbar.set) + + # Configure main container with padding inside the canvas + main_container = ttk.Frame(canvas, padding="20") + + # Pack canvas and scrollbar + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") + + # Create window in canvas for the main container + canvas_window = canvas.create_window((0, 0), window=main_container, anchor="nw") + + # Configure grid for main container + main_container.grid_columnconfigure(0, weight=1) + + # Configure scrolling + def configure_scroll_region(event=None): + canvas.configure(scrollregion=canvas.bbox("all")) + + def configure_canvas_width(event=None): + canvas_width = canvas.winfo_width() + canvas.itemconfig(canvas_window, width=canvas_width) + + def on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "units") + + main_container.bind("", configure_scroll_region) + canvas.bind("", configure_canvas_width) + 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 + + # Bind mouse wheel to main container and its children for better scrolling + self._bind_mousewheel_to_widget_tree(main_container, canvas) # Unpack values - handle both old and new CSV formats if len(values) == 10: @@ -361,21 +397,20 @@ class UIManager: note, ) = values_list[:16] - # Create variables and fields - vars_dict = self._create_edit_fields(edit_win, date, dep, anx, slp, app) - - # Medicine checkboxes - current_row = 6 # After the 5 fields (date, dep, anx, slp, app) - med_vars = self._create_medicine_checkboxes( - edit_win, current_row, bup, hydro, gaba, prop, quet - ) - vars_dict.update(med_vars) - - # Dose information display (editable) - current_row += 1 - dose_vars = self._add_dose_display_to_edit( - edit_win, - current_row, + # Create improved UI sections + vars_dict = self._create_improved_edit_ui( + main_container, + date, + dep, + anx, + slp, + app, + bup, + hydro, + gaba, + prop, + quet, + note, { "bupropion": bup_doses, "hydroxyzine": hydro_doses, @@ -384,29 +419,568 @@ class UIManager: "quetiapine": quet_doses, }, ) - vars_dict.update(dose_vars) - # Note field - current_row += 2 # Account for dose display - vars_dict["note"] = tk.StringVar(value=str(note)) - ttk.Label(edit_win, text="Note:").grid( - row=current_row, column=0, sticky="w", padx=5, pady=2 - ) - ttk.Entry(edit_win, textvariable=vars_dict["note"]).grid( - row=current_row, column=1, sticky="ew", padx=5, pady=2 - ) + # Add action buttons + self._add_improved_edit_buttons(main_container, vars_dict, callbacks, edit_win) - # Buttons - current_row += 1 - self._add_edit_window_buttons(edit_win, current_row, vars_dict, callbacks) + # Update scroll region after adding all content + edit_win.update_idletasks() + canvas.configure(scrollregion=canvas.bbox("all")) # Make window modal - edit_win.update_idletasks() edit_win.focus_set() edit_win.grab_set() return edit_win + def _create_improved_edit_ui( + self, + parent: ttk.Frame, + date: str, + dep: int, + anx: int, + slp: int, + app: int, + bup: int, + hydro: int, + gaba: int, + prop: int, + quet: int, + note: str, + dose_data: dict[str, str], + ) -> dict[str, Any]: + """Create improved UI layout for edit window with better organization.""" + vars_dict = {} + row = 0 + + # Header with entry date + header_frame = ttk.Frame(parent) + header_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20)) + header_frame.grid_columnconfigure(1, weight=1) + + ttk.Label( + header_frame, text="Editing Entry for:", font=("TkDefaultFont", 12, "bold") + ).grid(row=0, column=0, sticky="w") + + vars_dict["date"] = tk.StringVar(value=str(date)) + date_entry = ttk.Entry( + header_frame, + textvariable=vars_dict["date"], + font=("TkDefaultFont", 12), + width=15, + ) + date_entry.grid(row=0, column=1, sticky="w", padx=(10, 0)) + + row += 1 + + # Symptoms section + symptoms_frame = ttk.LabelFrame( + parent, text="Daily Symptoms (0-10 scale)", padding="15" + ) + symptoms_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) + symptoms_frame.grid_columnconfigure(1, weight=1) + + # Create symptom scales with better layout + symptoms = [ + ("Depression", "depression", dep), + ("Anxiety", "anxiety", anx), + ("Sleep Quality", "sleep", slp), + ("Appetite", "appetite", app), + ] + + for i, (label, key, value) in enumerate(symptoms): + self._create_improved_symptom_scale( + symptoms_frame, i, label, key, value, vars_dict + ) + + row += 1 + + # Medications section + meds_frame = ttk.LabelFrame(parent, text="Medications Taken", padding="15") + meds_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) + meds_frame.grid_columnconfigure(0, weight=1) + + # Create medicine checkboxes with better styling + med_vars = self._create_improved_medicine_section( + meds_frame, bup, hydro, gaba, prop, quet + ) + vars_dict.update(med_vars) + + row += 1 + + # Dose tracking section + dose_frame = ttk.LabelFrame(parent, text="Dose Tracking", padding="15") + dose_frame.grid(row=row, column=0, sticky="ew", pady=(0, 15)) + dose_frame.grid_columnconfigure(0, weight=1) + + dose_vars = self._create_improved_dose_tracking(dose_frame, dose_data) + vars_dict.update(dose_vars) + + row += 1 + + # Notes section + notes_frame = ttk.LabelFrame(parent, text="Notes", padding="15") + notes_frame.grid(row=row, column=0, sticky="ew", pady=(0, 20)) + notes_frame.grid_columnconfigure(0, weight=1) + + vars_dict["note"] = tk.StringVar(value=str(note)) + note_text = tk.Text( + notes_frame, height=4, wrap=tk.WORD, font=("TkDefaultFont", 10) + ) + note_text.grid(row=0, column=0, sticky="ew") + note_text.insert(1.0, str(note)) + vars_dict["note_text"] = note_text + + # Add scrollbar for notes + note_scroll = ttk.Scrollbar( + notes_frame, orient="vertical", command=note_text.yview + ) + note_scroll.grid(row=0, column=1, sticky="ns") + note_text.configure(yscrollcommand=note_scroll.set) + + return vars_dict + + def _create_improved_symptom_scale( + self, + parent: ttk.Frame, + row: int, + label: str, + key: str, + value: int, + vars_dict: dict[str, Any], + ) -> None: + """Create an improved symptom scale with better visual feedback.""" + # Ensure value is properly converted + try: + value = int(float(value)) if value not in ["", None] else 0 + except (ValueError, TypeError): + value = 0 + + vars_dict[key] = tk.IntVar(value=value) + + # Label + ttk.Label(parent, text=f"{label}:", font=("TkDefaultFont", 10, "bold")).grid( + row=row, column=0, sticky="w", pady=8 + ) + + # Scale container + scale_container = ttk.Frame(parent) + scale_container.grid(row=row, column=1, sticky="ew", padx=(20, 0), pady=8) + scale_container.grid_columnconfigure(0, weight=1) + + # Scale with value labels + scale_frame = ttk.Frame(scale_container) + scale_frame.grid(row=0, column=0, sticky="ew") + scale_frame.grid_columnconfigure(1, weight=1) + + # Current value display + value_label = ttk.Label( + scale_frame, + text=str(value), + font=("TkDefaultFont", 12, "bold"), + foreground="#2E86AB", + width=3, + ) + value_label.grid(row=0, column=0, padx=(0, 10)) + + # Scale widget + scale = ttk.Scale( + scale_frame, + from_=0, + to=10, + variable=vars_dict[key], + orient=tk.HORIZONTAL, + length=300, + ) + scale.grid(row=0, column=1, sticky="ew") + + # Scale labels (0, 5, 10) + labels_frame = ttk.Frame(scale_container) + labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0)) + + ttk.Label(labels_frame, text="0", font=("TkDefaultFont", 8)).grid( + row=0, column=0, sticky="w" + ) + labels_frame.grid_columnconfigure(1, weight=1) + ttk.Label(labels_frame, text="5", font=("TkDefaultFont", 8)).grid( + row=0, column=1 + ) + ttk.Label(labels_frame, text="10", font=("TkDefaultFont", 8)).grid( + row=0, column=2, sticky="e" + ) + + # Update label when scale changes + def update_value_label(event=None): + current_val = vars_dict[key].get() + value_label.configure(text=str(current_val)) + # Change color based on value + if current_val <= 3: + value_label.configure(foreground="#28A745") # Green for low/good + elif current_val <= 6: + value_label.configure(foreground="#FFC107") # Yellow for medium + else: + value_label.configure(foreground="#DC3545") # Red for high/bad + + scale.bind("", update_value_label) + scale.bind("", update_value_label) + scale.bind("", update_value_label) + update_value_label() # Set initial color + + def _create_improved_medicine_section( + self, parent: ttk.Frame, bup: int, hydro: int, gaba: int, prop: int, quet: int + ) -> dict[str, tk.IntVar]: + """Create improved medicine checkboxes with better layout.""" + vars_dict = {} + + # Create a grid layout for medicines + medicines = [ + ("bupropion", bup, "Bupropion", "150/300 mg", "#E8F4FD"), + ("hydroxyzine", hydro, "Hydroxyzine", "25 mg", "#FFF2E8"), + ("gabapentin", gaba, "Gabapentin", "100 mg", "#F0F8E8"), + ("propranolol", prop, "Propranolol", "10 mg", "#FCE8F3"), + ("quetiapine", quet, "Quetiapine", "25 mg", "#E8F0FF"), + ] + + # Create medicine cards in a 2-column layout + for i, (key, value, name, dose, _bg_color) in enumerate(medicines): + row = i // 2 + col = i % 2 + + # Medicine card frame + med_card = ttk.Frame(parent, relief="solid", borderwidth=1) + med_card.grid(row=row, column=col, sticky="ew", padx=5, pady=5) + parent.grid_columnconfigure(col, weight=1) + + vars_dict[key] = tk.IntVar(value=int(value)) + + # Checkbox with medicine name + check_frame = ttk.Frame(med_card) + check_frame.pack(fill="x", padx=10, pady=8) + + checkbox = ttk.Checkbutton( + check_frame, + text=f"{name} ({dose})", + variable=vars_dict[key], + style="Medicine.TCheckbutton", + ) + checkbox.pack(anchor="w") + + return vars_dict + + def _create_improved_dose_tracking( + self, parent: ttk.Frame, dose_data: dict[str, str] + ) -> dict[str, Any]: + """Create improved dose tracking interface.""" + vars_dict = {} + + # Create notebook for organized dose tracking + notebook = ttk.Notebook(parent) + notebook.pack(fill="both", expand=True) + + medicines = [ + ("bupropion", "Bupropion"), + ("hydroxyzine", "Hydroxyzine"), + ("gabapentin", "Gabapentin"), + ("propranolol", "Propranolol"), + ("quetiapine", "Quetiapine"), + ] + + for med_key, med_name in medicines: + # Create tab for each medicine + tab_frame = ttk.Frame(notebook) + notebook.add(tab_frame, text=med_name) + + # Configure tab layout + tab_frame.grid_columnconfigure(0, weight=1) + + # Quick dose entry section + entry_frame = ttk.LabelFrame(tab_frame, text="Add New Dose", padding="10") + entry_frame.grid(row=0, column=0, sticky="ew", padx=10, pady=5) + entry_frame.grid_columnconfigure(1, weight=1) + + ttk.Label(entry_frame, text="Dose amount:").grid( + row=0, column=0, sticky="w" + ) + + dose_entry_var = tk.StringVar() + vars_dict[f"{med_key}_entry_var"] = dose_entry_var + + dose_entry = ttk.Entry(entry_frame, textvariable=dose_entry_var, width=15) + dose_entry.grid(row=0, column=1, sticky="w", padx=(10, 10)) + + # Quick dose buttons + quick_frame = ttk.Frame(entry_frame) + quick_frame.grid(row=0, column=2, sticky="w") + + # Common dose amounts (customize per medicine) + quick_doses = self._get_quick_doses(med_key) + for i, dose in enumerate(quick_doses): + ttk.Button( + quick_frame, + text=dose, + width=8, + command=lambda d=dose, var=dose_entry_var: var.set(d), + ).grid(row=0, column=i, padx=2) + + # Take dose button + def create_take_dose_command(med_name, entry_var, med_key): + def take_dose(): + self._take_dose_improved(med_name, entry_var, med_key, vars_dict) + + return take_dose + + take_button = ttk.Button( + entry_frame, + text=f"Take {med_name}", + style="Accent.TButton", + command=create_take_dose_command(med_name, dose_entry_var, med_key), + ) + take_button.grid(row=1, column=0, columnspan=3, pady=(10, 0), sticky="ew") + + # Dose history section + history_frame = ttk.LabelFrame( + tab_frame, text="Today's Doses", padding="10" + ) + history_frame.grid(row=1, column=0, sticky="ew", padx=10, pady=5) + history_frame.grid_columnconfigure(0, weight=1) + + # Dose history display with fixed height to prevent excessive expansion + dose_text = tk.Text( + history_frame, + height=4, # Reduced height to fit better in scrollable window + wrap=tk.WORD, + font=("Consolas", 10), + state="normal", + ) + dose_text.grid(row=0, column=0, sticky="ew") + + # Populate with existing doses + doses_str = dose_data.get(med_key, "") + self._populate_dose_history(dose_text, doses_str) + + vars_dict[f"{med_key}_doses_text"] = dose_text + + # Scrollbar for dose history + dose_scroll = ttk.Scrollbar( + history_frame, orient="vertical", command=dose_text.yview + ) + dose_scroll.grid(row=0, column=1, sticky="ns") + dose_text.configure(yscrollcommand=dose_scroll.set) + + return vars_dict + + def _get_quick_doses(self, medicine_key: str) -> list[str]: + """Get common dose amounts for quick selection.""" + dose_map = { + "bupropion": ["150", "300"], + "hydroxyzine": ["25", "50"], + "gabapentin": ["100", "300", "600"], + "propranolol": ["10", "20", "40"], + "quetiapine": ["25", "50", "100"], + } + return dose_map.get(medicine_key, ["25", "50"]) + + def _populate_dose_history(self, text_widget: tk.Text, doses_str: str) -> None: + """Populate dose history text widget with formatted dose data.""" + text_widget.delete(1.0, tk.END) + + if not doses_str or str(doses_str) == "nan": + text_widget.insert(1.0, "No doses recorded today") + text_widget.configure(state="disabled") + return + + doses_str = str(doses_str) + formatted_doses = [] + + for dose_entry in doses_str.split("|"): + if ":" in dose_entry: + timestamp, dose = dose_entry.split(":", 1) + try: + dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") + time_str = dt.strftime("%I:%M %p") + formatted_doses.append(f"• {time_str} - {dose}") + except ValueError: + formatted_doses.append(f"• {dose_entry}") + + if formatted_doses: + text_widget.insert(1.0, "\n".join(formatted_doses)) + else: + text_widget.insert(1.0, "No doses recorded today") + + def _take_dose_improved( + self, + med_name: str, + entry_var: tk.StringVar, + med_key: str, + vars_dict: dict[str, Any], + ) -> None: + """Handle taking a dose with improved feedback.""" + dose = entry_var.get().strip() + + if not dose: + messagebox.showerror("Error", f"Please enter a dose amount for {med_name}") + return + + # Get current time + now = datetime.now() + time_str = now.strftime("%I:%M %p") + + # Update dose history + dose_text_widget = vars_dict.get(f"{med_key}_doses_text") + if dose_text_widget: + current_content = dose_text_widget.get(1.0, tk.END).strip() + + new_dose_line = f"• {time_str} - {dose}" + + if current_content == "No doses recorded today" or not current_content: + dose_text_widget.delete(1.0, tk.END) + dose_text_widget.insert(1.0, new_dose_line) + else: + dose_text_widget.insert(tk.END, f"\n{new_dose_line}") + + # Clear entry + entry_var.set("") + + # Success feedback + messagebox.showinfo( + "Dose Recorded", f"{med_name} dose of {dose} recorded at {time_str}" + ) + + def _add_improved_edit_buttons( + self, + parent: ttk.Frame, + vars_dict: dict[str, Any], + callbacks: dict[str, Callable], + edit_win: tk.Toplevel, + ) -> None: + """Add improved action buttons to edit window.""" + button_frame = ttk.Frame(parent) + button_frame.grid(row=999, column=0, sticky="ew", pady=(20, 0)) + button_frame.grid_columnconfigure((0, 1, 2), weight=1) + + # Save button + def save_with_improved_data(): + # Get note text from Text widget + note_text_widget = vars_dict.get("note_text") + note_content = "" + if note_text_widget: + note_content = note_text_widget.get(1.0, tk.END).strip() + + # Extract dose data + dose_data = {} + medicine_list = [ + "bupropion", + "hydroxyzine", + "gabapentin", + "propranolol", + "quetiapine", + ] + for medicine in medicine_list: + dose_text_key = f"{medicine}_doses_text" + if dose_text_key in vars_dict: + dose_text_widget = vars_dict[dose_text_key] + raw_text = dose_text_widget.get(1.0, tk.END).strip() + dose_data[medicine] = self._parse_improved_dose_text( + raw_text, vars_dict["date"].get() + ) + else: + dose_data[medicine] = "" + + callbacks["save"]( + edit_win, + vars_dict["date"].get(), + vars_dict["depression"].get(), + vars_dict["anxiety"].get(), + vars_dict["sleep"].get(), + vars_dict["appetite"].get(), + vars_dict["bupropion"].get(), + vars_dict["hydroxyzine"].get(), + vars_dict["gabapentin"].get(), + vars_dict["propranolol"].get(), + vars_dict["quetiapine"].get(), + note_content, + dose_data, + ) + + save_btn = ttk.Button( + button_frame, + text="💾 Save Changes", + style="Accent.TButton", + command=save_with_improved_data, + ) + save_btn.grid(row=0, column=0, sticky="ew", padx=(0, 5)) + + # Cancel button + cancel_btn = ttk.Button( + button_frame, text="❌ Cancel", command=edit_win.destroy + ) + cancel_btn.grid(row=0, column=1, sticky="ew", padx=5) + + # Delete button + delete_btn = ttk.Button( + button_frame, + text="🗑️ Delete Entry", + style="Danger.TButton", + command=lambda: callbacks["delete"](edit_win), + ) + delete_btn.grid(row=0, column=2, sticky="ew", padx=(5, 0)) + + def _parse_improved_dose_text(self, text: str, date: str) -> str: + """Parse improved dose text format back to CSV format.""" + if not text or "No doses recorded" in text: + return "" + + lines = text.strip().split("\n") + dose_entries = [] + + for line in lines: + line = line.strip() + if line.startswith("•") and " - " in line: + try: + # Remove bullet point and split + content = line[1:].strip() # Remove • + time_part, dose_part = content.split(" - ", 1) + + # Parse time (could be 12-hour format) + try: + time_obj = datetime.strptime(time_part.strip(), "%I:%M %p") + except ValueError: + # Try 24-hour format + time_obj = datetime.strptime(time_part.strip(), "%H:%M") + + # Create full timestamp + today = datetime.strptime(date, "%m/%d/%Y") + full_timestamp = today.replace( + hour=time_obj.hour, minute=time_obj.minute, second=0 + ) + + timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S") + dose_entries.append(f"{timestamp_str}:{dose_part.strip()}") + + except (ValueError, IndexError): + continue + + return "|".join(dose_entries) + + def _bind_mousewheel_to_widget_tree( + self, widget: tk.Widget, canvas: tk.Canvas + ) -> None: + """Recursively bind mouse wheel events to all widgets in the tree.""" + + def on_mousewheel(event): + canvas.yview_scroll(int(-1 * (event.delta / 120)), "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")) + + # 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) + def _create_edit_fields( self, parent: tk.Toplevel, diff --git a/test_edit_window.py b/test_edit_window.py new file mode 100644 index 0000000..2e8dda9 --- /dev/null +++ b/test_edit_window.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Test script to demonstrate the improved edit window.""" + +import sys +import tkinter as tk +from pathlib import Path + +# Add src directory to path +sys.path.insert(0, str(Path(__file__).parent / "src")) + +from src.logger import logger +from src.ui_manager import UIManager + + +def test_edit_window(): + """Test the improved edit window.""" + root = tk.Tk() + root.title("Edit Window Test") + root.geometry("400x300") + + ui_manager = UIManager(root, logger) + + # Sample data for testing (16 fields format) + test_values = ( + "12/25/2024", # date + 7, # depression + 5, # anxiety + 6, # sleep + 4, # appetite + 1, # bupropion + "09:00:00:150|18:00:00:150", # bupropion_doses + 1, # hydroxyzine + "21:30:00:25", # hydroxyzine_doses + 0, # gabapentin + "", # gabapentin_doses + 1, # propranolol + "07:00:00:10|14:00:00:10", # propranolol_doses + 0, # quetiapine + "", # quetiapine_doses + # Had a good day overall, feeling better with new medication routine + "Had a good day overall, feeling better with the new medication routine.", + ) + + # Mock callbacks + def save_callback(win, *args): + print("Save called with args:", args) + win.destroy() + + def delete_callback(win): + print("Delete called") + win.destroy() + + callbacks = {"save": save_callback, "delete": delete_callback} + + # Create the improved edit window + edit_win = ui_manager.create_edit_window(test_values, callbacks) + + # Center the edit window + edit_win.update_idletasks() + x = (edit_win.winfo_screenwidth() // 2) - (edit_win.winfo_width() // 2) + y = (edit_win.winfo_screenheight() // 2) - (edit_win.winfo_height() // 2) + edit_win.geometry(f"+{x}+{y}") + + root.mainloop() + + +if __name__ == "__main__": + test_edit_window()