feat: Enhance edit window UI with improved layout and scrolling functionality

This commit is contained in:
William Valentin
2025-07-29 17:28:52 -07:00
parent fd1f9a43c6
commit 30750710b8
2 changed files with 675 additions and 33 deletions

View File

@@ -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>", configure_scroll_region)
canvas.bind("<Configure>", configure_canvas_width)
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
canvas.bind("<Button-4>", lambda e: canvas.yview_scroll(-1, "units")) # Linux
canvas.bind("<Button-5>", 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("<Motion>", update_value_label)
scale.bind("<ButtonRelease-1>", update_value_label)
scale.bind("<KeyRelease>", 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("<MouseWheel>", on_mousewheel)
widget.bind("<Button-4>", lambda e: canvas.yview_scroll(-1, "units"))
widget.bind("<Button-5>", 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,

68
test_edit_window.py Normal file
View File

@@ -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()