Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 85e30671d4 | |||
| b259837af4 | |||
| aad02f0d36 | |||
| 30750710b8 | |||
| fd1f9a43c6 |
@@ -14,6 +14,8 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Fetch full history for release notes generation
|
||||||
|
|
||||||
- name: Install Docker
|
- name: Install Docker
|
||||||
run: curl -fsSL https://get.docker.com | sh
|
run: curl -fsSL https://get.docker.com | sh
|
||||||
@@ -55,3 +57,49 @@ jobs:
|
|||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
cache-from: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache
|
cache-from: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache
|
||||||
cache-to: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache,mode=max
|
cache-to: type=registry,ref=gitea-http.taildb3494.ts.net/will/thechart:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Generate release notes
|
||||||
|
id: release_notes
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
|
run: |
|
||||||
|
# Get the current tag
|
||||||
|
CURRENT_TAG=${GITEA_REF#refs/tags/}
|
||||||
|
|
||||||
|
# Get the previous tag
|
||||||
|
PREVIOUS_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Generate release notes from commits
|
||||||
|
if [ -n "$PREVIOUS_TAG" ]; then
|
||||||
|
echo "## Changes from $PREVIOUS_TAG to $CURRENT_TAG" > release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..$CURRENT_TAG >> release_notes.md
|
||||||
|
else
|
||||||
|
echo "## Initial Release $CURRENT_TAG" > release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
git log --pretty=format:"- %s (%h)" >> release_notes.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Add Docker image information
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
echo "## Docker Images" >> release_notes.md
|
||||||
|
echo "" >> release_notes.md
|
||||||
|
echo "This release includes multi-platform Docker images:" >> release_notes.md
|
||||||
|
echo "- \`gitea-http.taildb3494.ts.net/will/thechart:$CURRENT_TAG\`" >> release_notes.md
|
||||||
|
echo "- \`gitea-http.taildb3494.ts.net/will/thechart:latest\`" >> release_notes.md
|
||||||
|
|
||||||
|
# Output the release notes content for use in next step
|
||||||
|
echo "release_notes<<EOF" >> $GITEA_OUTPUT
|
||||||
|
cat release_notes.md >> $GITEA_OUTPUT
|
||||||
|
echo "EOF" >> $GITEA_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
if: startsWith(gitea.ref, 'refs/tags/')
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ gitea.ref_name }}
|
||||||
|
release_name: Release ${{ gitea.ref_name }}
|
||||||
|
body: ${{ steps.release_notes.outputs.release_notes }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
|||||||
+850
-47
@@ -70,18 +70,6 @@ class UIManager:
|
|||||||
input_frame = ttk.Frame(canvas)
|
input_frame = ttk.Frame(canvas)
|
||||||
input_frame.grid_columnconfigure(1, weight=1)
|
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>", configure_scroll_region)
|
|
||||||
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
|
|
||||||
|
|
||||||
# Place canvas and scrollbar in the container
|
# Place canvas and scrollbar in the container
|
||||||
canvas.grid(row=0, column=0, sticky="nsew")
|
canvas.grid(row=0, column=0, sticky="nsew")
|
||||||
scrollbar.grid(row=0, column=1, sticky="ns")
|
scrollbar.grid(row=0, column=1, sticky="ns")
|
||||||
@@ -94,8 +82,53 @@ class UIManager:
|
|||||||
canvas_width = canvas.winfo_width()
|
canvas_width = canvas.winfo_width()
|
||||||
canvas.itemconfig(canvas_window, width=canvas_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>", configure_scroll_region)
|
||||||
canvas.bind("<Configure>", configure_canvas_width)
|
canvas.bind("<Configure>", configure_canvas_width)
|
||||||
|
|
||||||
|
# Bind mouse wheel events to canvas and main container
|
||||||
|
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
||||||
|
canvas.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
||||||
|
canvas.bind("<Button-5>", on_mousewheel_linux_down) # Linux
|
||||||
|
main_container.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
||||||
|
main_container.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
||||||
|
main_container.bind("<Button-5>", 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("<Enter>", on_mouse_enter)
|
||||||
|
canvas.bind("<Enter>", on_mouse_enter)
|
||||||
|
|
||||||
# Create variables for symptoms
|
# Create variables for symptoms
|
||||||
symptom_vars: dict[str, tk.IntVar] = {
|
symptom_vars: dict[str, tk.IntVar] = {
|
||||||
"depression": tk.IntVar(value=0),
|
"depression": tk.IntVar(value=0),
|
||||||
@@ -168,6 +201,11 @@ class UIManager:
|
|||||||
# Set default date to today
|
# Set default date to today
|
||||||
date_var.set(datetime.now().strftime("%m/%d/%Y"))
|
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 all UI elements and variables
|
||||||
return {
|
return {
|
||||||
"frame": main_container,
|
"frame": main_container,
|
||||||
@@ -277,14 +315,81 @@ class UIManager:
|
|||||||
def create_edit_window(
|
def create_edit_window(
|
||||||
self, values: tuple[str, ...], callbacks: dict[str, Callable]
|
self, values: tuple[str, ...], callbacks: dict[str, Callable]
|
||||||
) -> tk.Toplevel:
|
) -> 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: tk.Toplevel = tk.Toplevel(master=self.root)
|
||||||
edit_win.title("Edit Entry")
|
edit_win.title("Edit Entry")
|
||||||
edit_win.transient(self.root) # Make window modal
|
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
|
# Create scrollable container
|
||||||
edit_win.grid_columnconfigure(1, weight=1)
|
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):
|
||||||
|
# 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>", configure_scroll_region)
|
||||||
|
canvas.bind("<Configure>", configure_canvas_width)
|
||||||
|
|
||||||
|
# Bind mouse wheel events to canvas and edit window
|
||||||
|
canvas.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
||||||
|
canvas.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
||||||
|
canvas.bind("<Button-5>", on_mousewheel_linux_down) # Linux
|
||||||
|
edit_win.bind("<MouseWheel>", on_mousewheel) # Windows/Linux
|
||||||
|
edit_win.bind("<Button-4>", on_mousewheel_linux_up) # Linux
|
||||||
|
edit_win.bind("<Button-5>", 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("<Enter>", on_mouse_enter)
|
||||||
|
canvas.bind("<Enter>", on_mouse_enter)
|
||||||
|
|
||||||
# Unpack values - handle both old and new CSV formats
|
# Unpack values - handle both old and new CSV formats
|
||||||
if len(values) == 10:
|
if len(values) == 10:
|
||||||
@@ -361,21 +466,20 @@ class UIManager:
|
|||||||
note,
|
note,
|
||||||
) = values_list[:16]
|
) = values_list[:16]
|
||||||
|
|
||||||
# Create variables and fields
|
# Create improved UI sections
|
||||||
vars_dict = self._create_edit_fields(edit_win, date, dep, anx, slp, app)
|
vars_dict = self._create_improved_edit_ui(
|
||||||
|
main_container,
|
||||||
# Medicine checkboxes
|
date,
|
||||||
current_row = 6 # After the 5 fields (date, dep, anx, slp, app)
|
dep,
|
||||||
med_vars = self._create_medicine_checkboxes(
|
anx,
|
||||||
edit_win, current_row, bup, hydro, gaba, prop, quet
|
slp,
|
||||||
)
|
app,
|
||||||
vars_dict.update(med_vars)
|
bup,
|
||||||
|
hydro,
|
||||||
# Dose information display (editable)
|
gaba,
|
||||||
current_row += 1
|
prop,
|
||||||
dose_vars = self._add_dose_display_to_edit(
|
quet,
|
||||||
edit_win,
|
note,
|
||||||
current_row,
|
|
||||||
{
|
{
|
||||||
"bupropion": bup_doses,
|
"bupropion": bup_doses,
|
||||||
"hydroxyzine": hydro_doses,
|
"hydroxyzine": hydro_doses,
|
||||||
@@ -384,29 +488,708 @@ class UIManager:
|
|||||||
"quetiapine": quet_doses,
|
"quetiapine": quet_doses,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
vars_dict.update(dose_vars)
|
|
||||||
|
|
||||||
# Note field
|
# Add action buttons
|
||||||
current_row += 2 # Account for dose display
|
self._add_improved_edit_buttons(main_container, vars_dict, callbacks, edit_win)
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# Buttons
|
# Update scroll region after adding all content
|
||||||
current_row += 1
|
edit_win.update_idletasks()
|
||||||
self._add_edit_window_buttons(edit_win, current_row, vars_dict, callbacks)
|
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
|
# Make window modal
|
||||||
edit_win.update_idletasks()
|
|
||||||
edit_win.focus_set()
|
edit_win.focus_set()
|
||||||
edit_win.grab_set()
|
edit_win.grab_set()
|
||||||
|
|
||||||
return edit_win
|
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", # Start enabled
|
||||||
|
)
|
||||||
|
dose_text.grid(row=0, column=0, sticky="ew")
|
||||||
|
|
||||||
|
# Store raw dose string in a variable
|
||||||
|
doses_str = dose_data.get(med_key, "")
|
||||||
|
dose_str_var = tk.StringVar(value=doses_str)
|
||||||
|
vars_dict[f"{med_key}_doses_str"] = dose_str_var
|
||||||
|
|
||||||
|
# Populate with existing doses
|
||||||
|
self._populate_dose_history(dose_text, dose_str_var.get())
|
||||||
|
|
||||||
|
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.configure(state="normal")
|
||||||
|
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")
|
||||||
|
# Keep text widget enabled for editing
|
||||||
|
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:
|
||||||
|
# Handle cases where the timestamp might be malformed
|
||||||
|
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")
|
||||||
|
|
||||||
|
# Always keep text widget enabled for user editing
|
||||||
|
|
||||||
|
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 and state management."""
|
||||||
|
dose = entry_var.get().strip()
|
||||||
|
|
||||||
|
# Get the dose text widget - this is what the save function reads from
|
||||||
|
dose_text_widget = vars_dict.get(f"{med_key}_doses_text")
|
||||||
|
if not dose_text_widget:
|
||||||
|
self.logger.error(f"Dose text widget not found for {med_key}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find the parent edit window
|
||||||
|
parent_window = dose_text_widget.winfo_toplevel()
|
||||||
|
|
||||||
|
if not dose:
|
||||||
|
messagebox.showerror(
|
||||||
|
"Error",
|
||||||
|
f"Please enter a dose amount for {med_name}",
|
||||||
|
parent=parent_window,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get current time and timestamp
|
||||||
|
now = datetime.now()
|
||||||
|
time_str = now.strftime("%I:%M %p")
|
||||||
|
|
||||||
|
# Ensure text widget is enabled
|
||||||
|
dose_text_widget.configure(state="normal")
|
||||||
|
|
||||||
|
# Get current content from the text widget
|
||||||
|
current_content = dose_text_widget.get(1.0, tk.END).strip()
|
||||||
|
self.logger.debug(f"Current content before adding dose: '{current_content}'")
|
||||||
|
|
||||||
|
# Create new dose entry in the display format
|
||||||
|
new_dose_line = f"• {time_str} - {dose}"
|
||||||
|
self.logger.debug(f"New dose line: '{new_dose_line}'")
|
||||||
|
|
||||||
|
# Add the new dose to the text widget
|
||||||
|
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)
|
||||||
|
self.logger.debug("Added first dose")
|
||||||
|
else:
|
||||||
|
# Append to existing content with proper formatting
|
||||||
|
updated_content = current_content + f"\n{new_dose_line}"
|
||||||
|
self.logger.debug(f"Updated content: '{updated_content}'")
|
||||||
|
dose_text_widget.delete(1.0, tk.END)
|
||||||
|
dose_text_widget.insert(1.0, updated_content)
|
||||||
|
self.logger.debug("Added subsequent dose")
|
||||||
|
|
||||||
|
# Verify what's actually in the widget after insertion
|
||||||
|
final_content = dose_text_widget.get(1.0, tk.END).strip()
|
||||||
|
self.logger.debug(f"Final content in widget: '{final_content}'")
|
||||||
|
|
||||||
|
# Clear entry field
|
||||||
|
entry_var.set("")
|
||||||
|
|
||||||
|
# Success feedback
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Dose Recorded",
|
||||||
|
f"{med_name} dose of {dose} recorded at {time_str}",
|
||||||
|
parent=parent_window,
|
||||||
|
)
|
||||||
|
|
||||||
|
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():
|
||||||
|
self.logger.debug("=== SAVE FUNCTION CALLED ===")
|
||||||
|
|
||||||
|
# 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 from the editable text widgets
|
||||||
|
dose_data = {}
|
||||||
|
medicine_list = [
|
||||||
|
"bupropion",
|
||||||
|
"hydroxyzine",
|
||||||
|
"gabapentin",
|
||||||
|
"propranolol",
|
||||||
|
"quetiapine",
|
||||||
|
]
|
||||||
|
for medicine in medicine_list:
|
||||||
|
dose_text_key = f"{medicine}_doses_text"
|
||||||
|
self.logger.debug(f"Processing {medicine}...")
|
||||||
|
|
||||||
|
if dose_text_key in vars_dict and isinstance(
|
||||||
|
vars_dict[dose_text_key], tk.Text
|
||||||
|
):
|
||||||
|
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
|
||||||
|
self.logger.debug(f"Raw text for {medicine}: '{raw_text}'")
|
||||||
|
|
||||||
|
parsed_dose = self._parse_dose_history_for_saving(
|
||||||
|
raw_text, vars_dict["date"].get()
|
||||||
|
)
|
||||||
|
dose_data[medicine] = parsed_dose
|
||||||
|
self.logger.debug(f"Parsed dose for {medicine}: '{parsed_dose}'")
|
||||||
|
else:
|
||||||
|
self.logger.debug(f"No text widget found for {medicine}")
|
||||||
|
dose_data[medicine] = ""
|
||||||
|
|
||||||
|
self.logger.debug(f"Final dose_data: {dose_data}")
|
||||||
|
|
||||||
|
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_dose_history_for_saving(self, text: str, date_str: str) -> str:
|
||||||
|
"""
|
||||||
|
Parse the user-edited dose history back into the storable format,
|
||||||
|
supporting add/delete/edit.
|
||||||
|
"""
|
||||||
|
self.logger.debug("=== PARSING DOSE HISTORY ===")
|
||||||
|
self.logger.debug(f"Input text: '{text}'")
|
||||||
|
self.logger.debug(f"Date string: '{date_str}'")
|
||||||
|
|
||||||
|
if not text or "No doses recorded" in text:
|
||||||
|
self.logger.debug("No doses to parse, returning empty string")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
lines = text.strip().split("\n")
|
||||||
|
self.logger.debug(f"Split into {len(lines)} lines: {lines}")
|
||||||
|
dose_entries = []
|
||||||
|
|
||||||
|
for line_num, line in enumerate(lines):
|
||||||
|
line = line.strip()
|
||||||
|
self.logger.debug(f"Processing line {line_num}: '{line}'")
|
||||||
|
if not line or line.lower().startswith("no doses recorded"):
|
||||||
|
self.logger.debug("Empty or placeholder line, skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle bullet point format: "• HH:MM AM/PM - dose"
|
||||||
|
if line.startswith("•") and " - " in line:
|
||||||
|
try:
|
||||||
|
content = line.lstrip("• ").strip()
|
||||||
|
self.logger.debug(f"Bullet point content: '{content}'")
|
||||||
|
time_part, dose_part = content.split(" - ", 1)
|
||||||
|
self.logger.debug(
|
||||||
|
f"Time part: '{time_part}', Dose part: '{dose_part}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Try parsing as 12-hour (with AM/PM)
|
||||||
|
try:
|
||||||
|
time_obj = datetime.strptime(time_part.strip(), "%I:%M %p")
|
||||||
|
except ValueError:
|
||||||
|
# Try 24-hour format fallback
|
||||||
|
time_obj = datetime.strptime(time_part.strip(), "%H:%M")
|
||||||
|
|
||||||
|
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
|
||||||
|
full_timestamp = entry_date.replace(
|
||||||
|
hour=time_obj.hour,
|
||||||
|
minute=time_obj.minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0,
|
||||||
|
)
|
||||||
|
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
dose_entry = f"{timestamp_str}:{dose_part.strip()}"
|
||||||
|
dose_entries.append(dose_entry)
|
||||||
|
self.logger.debug(f"Added dose entry: '{dose_entry}'")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Could not parse dose line: '{line}'. Error: {e}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle simple format: "HH:MM dose" or "HH:MM: dose"
|
||||||
|
elif ":" in line and not line.startswith("•"):
|
||||||
|
try:
|
||||||
|
# Try to parse as "HH:MM dose" or "HH:MM: dose"
|
||||||
|
if " " in line:
|
||||||
|
time_part, dose_part = line.split(" ", 1)
|
||||||
|
time_part = time_part.rstrip(":")
|
||||||
|
# Try 24-hour format first
|
||||||
|
try:
|
||||||
|
time_obj = datetime.strptime(time_part, "%H:%M")
|
||||||
|
except ValueError:
|
||||||
|
# Try 12-hour format
|
||||||
|
time_obj = datetime.strptime(time_part, "%I:%M")
|
||||||
|
entry_date = datetime.strptime(date_str, "%m/%d/%Y")
|
||||||
|
full_timestamp = entry_date.replace(
|
||||||
|
hour=time_obj.hour,
|
||||||
|
minute=time_obj.minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0,
|
||||||
|
)
|
||||||
|
timestamp_str = full_timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
dose_entries.append(f"{timestamp_str}:{dose_part.strip()}")
|
||||||
|
self.logger.debug(
|
||||||
|
"Added simple dose entry: '%s:%s'",
|
||||||
|
timestamp_str,
|
||||||
|
dose_part.strip(),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(
|
||||||
|
f"Could not parse simple dose line: '{line}'. Error: {e}"
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If user just types a dose (no time), store as-is with no timestamp
|
||||||
|
elif line:
|
||||||
|
self.logger.debug(f"Line with no time, storing as-is: '{line}'")
|
||||||
|
dose_entries.append(line)
|
||||||
|
|
||||||
|
result = "|".join(dose_entries)
|
||||||
|
self.logger.debug(f"Final parsed result: '{result}'")
|
||||||
|
return result
|
||||||
|
|
||||||
|
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):
|
||||||
|
# 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
|
||||||
|
try:
|
||||||
|
widget.bind("<MouseWheel>", on_mousewheel)
|
||||||
|
widget.bind("<Button-4>", on_mousewheel_linux_up)
|
||||||
|
widget.bind("<Button-5>", on_mousewheel_linux_down)
|
||||||
|
except tk.TclError:
|
||||||
|
# Some widgets might not support binding
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Recursively bind to all children
|
||||||
|
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(
|
def _create_edit_fields(
|
||||||
self,
|
self,
|
||||||
parent: tk.Toplevel,
|
parent: tk.Toplevel,
|
||||||
@@ -548,6 +1331,7 @@ class UIManager:
|
|||||||
|
|
||||||
# Save button - create a custom callback to handle dose data
|
# Save button - create a custom callback to handle dose data
|
||||||
def save_with_doses():
|
def save_with_doses():
|
||||||
|
self.logger.debug("save_with_doses called")
|
||||||
# Extract dose data from the text widgets
|
# Extract dose data from the text widgets
|
||||||
dose_data = {}
|
dose_data = {}
|
||||||
|
|
||||||
@@ -559,15 +1343,24 @@ class UIManager:
|
|||||||
"quetiapine",
|
"quetiapine",
|
||||||
]:
|
]:
|
||||||
dose_text_key = f"{medicine}_doses_text"
|
dose_text_key = f"{medicine}_doses_text"
|
||||||
|
self.logger.debug(f"Looking for key: {dose_text_key}")
|
||||||
|
|
||||||
if dose_text_key in vars_dict and isinstance(
|
if dose_text_key in vars_dict and isinstance(
|
||||||
vars_dict[dose_text_key], tk.Text
|
vars_dict[dose_text_key], tk.Text
|
||||||
):
|
):
|
||||||
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
|
raw_text = vars_dict[dose_text_key].get(1.0, tk.END).strip()
|
||||||
dose_data[medicine] = self._parse_dose_text(
|
self.logger.debug(f"Raw text for {medicine}: '{raw_text}'")
|
||||||
|
dose_data[medicine] = self._parse_dose_history_for_saving(
|
||||||
raw_text, vars_dict["date"].get()
|
raw_text, vars_dict["date"].get()
|
||||||
)
|
)
|
||||||
|
self.logger.debug(
|
||||||
|
f"Parsed dose data for {medicine}: '{dose_data[medicine]}'"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
|
self.logger.debug(
|
||||||
|
f"Key {dose_text_key} not found in vars_dict or not a Text "
|
||||||
|
"widget"
|
||||||
|
)
|
||||||
dose_data[medicine] = ""
|
dose_data[medicine] = ""
|
||||||
|
|
||||||
callbacks["save"](
|
callbacks["save"](
|
||||||
@@ -783,7 +1576,14 @@ class UIManager:
|
|||||||
|
|
||||||
def _parse_dose_text(self, text: str, date: str) -> str:
|
def _parse_dose_text(self, text: str, date: str) -> str:
|
||||||
"""Parse dose text from edit window back to CSV format."""
|
"""Parse dose text from edit window back to CSV format."""
|
||||||
|
self.logger.debug(
|
||||||
|
f"_parse_dose_text called with text: '{text}' and date: '{date}'"
|
||||||
|
)
|
||||||
|
|
||||||
if not text or text == "No doses recorded":
|
if not text or text == "No doses recorded":
|
||||||
|
self.logger.debug(
|
||||||
|
"Text is empty or 'No doses recorded', returning empty string"
|
||||||
|
)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
lines = text.strip().split("\n")
|
lines = text.strip().split("\n")
|
||||||
@@ -829,6 +1629,9 @@ class UIManager:
|
|||||||
dose_entries.append(f"{timestamp_str}:{dose_part}")
|
dose_entries.append(f"{timestamp_str}:{dose_part}")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# If parsing fails, skip this line
|
# If parsing fails, skip this line
|
||||||
|
self.logger.debug(f"Failed to parse line: '{line}'")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return "|".join(dose_entries)
|
result = "|".join(dose_entries)
|
||||||
|
self.logger.debug(f"_parse_dose_text returning: '{result}'")
|
||||||
|
return result
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script to verify mouse wheel scrolling works in both the new entry window
|
||||||
|
and edit window of TheChart application.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
|
||||||
|
def test_scrolling():
|
||||||
|
"""Test both new entry and edit window scrolling."""
|
||||||
|
print("Testing mouse wheel scrolling functionality...")
|
||||||
|
|
||||||
|
# Create test root window
|
||||||
|
root = tk.Tk()
|
||||||
|
root.title("Scrolling Test")
|
||||||
|
root.geometry("800x600")
|
||||||
|
|
||||||
|
# Create logger
|
||||||
|
logger = logging.getLogger("test")
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
# Create UI manager
|
||||||
|
ui_manager = UIManager(root, logger)
|
||||||
|
|
||||||
|
# Create main frame
|
||||||
|
main_frame = tk.Frame(root)
|
||||||
|
main_frame.pack(fill="both", expand=True)
|
||||||
|
main_frame.grid_rowconfigure(0, weight=1)
|
||||||
|
main_frame.grid_rowconfigure(1, weight=1)
|
||||||
|
main_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
main_frame.grid_columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# Test 1: Create input frame (new entry window)
|
||||||
|
print("✓ Creating new entry input frame with mouse wheel scrolling...")
|
||||||
|
ui_manager.create_input_frame(main_frame)
|
||||||
|
|
||||||
|
# Test 2: Create edit window
|
||||||
|
def test_edit_window():
|
||||||
|
print("✓ Creating edit window with mouse wheel scrolling...")
|
||||||
|
# Sample data for edit window
|
||||||
|
test_values = (
|
||||||
|
"01/15/2025", # date
|
||||||
|
"3", # depression
|
||||||
|
"5", # anxiety
|
||||||
|
"7", # sleep
|
||||||
|
"4", # appetite
|
||||||
|
"1", # bupropion
|
||||||
|
"09:00: 150", # bup_doses
|
||||||
|
"0", # hydroxyzine
|
||||||
|
"", # hydro_doses
|
||||||
|
"1", # gabapentin
|
||||||
|
"20:00: 100", # gaba_doses
|
||||||
|
"0", # propranolol
|
||||||
|
"", # prop_doses
|
||||||
|
"0", # quetiapine
|
||||||
|
"", # quet_doses
|
||||||
|
"Test note", # note
|
||||||
|
)
|
||||||
|
|
||||||
|
callbacks = {
|
||||||
|
"save": lambda *args: print("Save callback called"),
|
||||||
|
"delete": lambda *args: print("Delete callback called"),
|
||||||
|
}
|
||||||
|
|
||||||
|
edit_window = ui_manager.create_edit_window(test_values, callbacks)
|
||||||
|
return edit_window
|
||||||
|
|
||||||
|
# Add test button
|
||||||
|
test_button = tk.Button(
|
||||||
|
main_frame,
|
||||||
|
text="Test Edit Window Scrolling",
|
||||||
|
command=test_edit_window,
|
||||||
|
font=("TkDefaultFont", 12),
|
||||||
|
bg="#4CAF50",
|
||||||
|
fg="white",
|
||||||
|
padx=20,
|
||||||
|
pady=10,
|
||||||
|
)
|
||||||
|
test_button.grid(row=2, column=0, columnspan=2, pady=20)
|
||||||
|
|
||||||
|
# Add instructions
|
||||||
|
instructions = tk.Label(
|
||||||
|
main_frame,
|
||||||
|
text="Instructions:\n\n"
|
||||||
|
"1. Use mouse wheel anywhere in the 'New Entry' section to test scrolling\n"
|
||||||
|
"2. Click 'Test Edit Window Scrolling' button\n"
|
||||||
|
"3. Use mouse wheel anywhere in the edit window to test scrolling\n"
|
||||||
|
"4. Both windows should scroll smoothly with mouse wheel\n\n"
|
||||||
|
"✓ Mouse wheel scrolling has been enhanced for both windows!",
|
||||||
|
font=("TkDefaultFont", 10),
|
||||||
|
justify="left",
|
||||||
|
bg="#E8F5E8",
|
||||||
|
padx=20,
|
||||||
|
pady=15,
|
||||||
|
)
|
||||||
|
instructions.grid(row=3, column=0, columnspan=2, padx=20, pady=10, sticky="ew")
|
||||||
|
|
||||||
|
print("✓ Test setup complete!")
|
||||||
|
print("\nMouse wheel scrolling features implemented:")
|
||||||
|
print(" • Recursive binding to all child widgets")
|
||||||
|
print(" • Platform-specific event handling (Windows/Linux)")
|
||||||
|
print(" • Focus management for consistent scrolling")
|
||||||
|
print(" • Works anywhere within the scrollable areas")
|
||||||
|
print("\nTest the scrolling by moving your mouse wheel over any part of the")
|
||||||
|
print("'New Entry' section or the edit window when opened.")
|
||||||
|
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_scrolling()
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import datetime
|
||||||
|
import tkinter as tk
|
||||||
|
from src.ui_manager import UIManager
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def root_window():
|
||||||
|
root = tk.Tk()
|
||||||
|
yield root
|
||||||
|
root.destroy()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def ui_manager(root_window):
|
||||||
|
class DummyLogger:
|
||||||
|
def debug(self, *a, **k): pass
|
||||||
|
def warning(self, *a, **k): pass
|
||||||
|
def error(self, *a, **k): pass
|
||||||
|
return UIManager(root_window, DummyLogger())
|
||||||
|
|
||||||
|
def test_parse_dose_history_for_saving_bullet_and_delete(ui_manager):
|
||||||
|
# Simulate user editing: add, delete, and custom lines
|
||||||
|
date_str = "07/30/2025"
|
||||||
|
# User deletes one line, adds a custom one
|
||||||
|
text = """
|
||||||
|
• 09:00 AM - 150mg
|
||||||
|
• 06:00 PM - 150mg
|
||||||
|
Custom note
|
||||||
|
""".strip()
|
||||||
|
result = ui_manager._parse_dose_history_for_saving(text, date_str)
|
||||||
|
# Should parse both bullets and keep the custom line
|
||||||
|
assert "2025-07-30 09:00:00:150mg" in result
|
||||||
|
assert "2025-07-30 18:00:00:150mg" in result
|
||||||
|
assert "Custom note" in result
|
||||||
|
# If user deletes all, should return empty string
|
||||||
|
assert ui_manager._parse_dose_history_for_saving("", date_str) == ""
|
||||||
|
assert ui_manager._parse_dose_history_for_saving("No doses recorded today", date_str) == ""
|
||||||
|
|
||||||
|
def test_parse_dose_history_for_saving_simple_time(ui_manager):
|
||||||
|
date_str = "07/30/2025"
|
||||||
|
text = "09:00 150mg\n18:00 150mg"
|
||||||
|
result = ui_manager._parse_dose_history_for_saving(text, date_str)
|
||||||
|
assert "2025-07-30 09:00:00:150mg" in result
|
||||||
|
assert "2025-07-30 18:00:00:150mg" in result
|
||||||
|
|
||||||
|
def test_parse_dose_history_for_saving_mixed(ui_manager):
|
||||||
|
date_str = "07/30/2025"
|
||||||
|
text = "• 09:00 AM - 150mg\n18:00 150mg\nJust a note"
|
||||||
|
result = ui_manager._parse_dose_history_for_saving(text, date_str)
|
||||||
|
assert "2025-07-30 09:00:00:150mg" in result
|
||||||
|
assert "2025-07-30 18:00:00:150mg" in result
|
||||||
|
assert "Just a note" in result
|
||||||
Reference in New Issue
Block a user