Add theme management and settings functionality
Build and Push Docker Image / build-and-push (push) Has been cancelled

- Introduced `ThemeManager` to handle application themes using `ttkthemes`.
- Added `SettingsWindow` for user preferences including theme selection and UI settings.
- Integrated theme selection into the main application with a menu for quick access.
- Enhanced UI components with custom styles based on the selected theme.
- Implemented tooltips for better user guidance across various UI elements.
- Updated dependencies to include `ttkthemes` for improved visual appeal.
This commit is contained in:
William Valentin
2025-08-05 11:58:25 -07:00
parent 86606d56b6
commit c3c88c63d2
14 changed files with 1287 additions and 70 deletions
+66 -6
View File
@@ -17,6 +17,8 @@ from medicine_management_window import MedicineManagementWindow
from medicine_manager import MedicineManager
from pathology_management_window import PathologyManagementWindow
from pathology_manager import PathologyManager
from settings_window import SettingsWindow
from theme_manager import ThemeManager
from ui_manager import UIManager
@@ -44,6 +46,9 @@ class MedTrackerApp:
logger.info(f"Log level: {LOG_LEVEL}")
# Initialize theme manager first
self.theme_manager: ThemeManager = ThemeManager(self.root, logger)
if LOG_LEVEL == "DEBUG":
logger.debug(f"Script name: {sys.argv[0]}")
logger.debug(f"Logs path: {LOG_PATH}")
@@ -54,7 +59,11 @@ class MedTrackerApp:
self.medicine_manager: MedicineManager = MedicineManager(logger=logger)
self.pathology_manager: PathologyManager = PathologyManager(logger=logger)
self.ui_manager: UIManager = UIManager(
root, logger, self.medicine_manager, self.pathology_manager
root,
logger,
self.medicine_manager,
self.pathology_manager,
self.theme_manager,
)
self.data_manager: DataManager = DataManager(
self.filename, logger, self.medicine_manager, self.pathology_manager
@@ -103,7 +112,7 @@ class MedTrackerApp:
import tkinter.ttk as ttk
# --- Main Frame ---
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10")
main_frame: ttk.Frame = ttk.Frame(self.root, padding="10", style="Card.TFrame")
main_frame.grid(row=0, column=0, sticky="nsew")
# Configure root window grid
@@ -206,9 +215,36 @@ class MedTrackerApp:
label="Refresh Data", command=self.refresh_data_display, accelerator="F5"
)
# Theme menu
theme_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Theme", menu=theme_menu)
# Add quick theme options
available_themes = self.theme_manager.get_available_themes()
current_theme = self.theme_manager.get_current_theme()
for theme in available_themes:
theme_menu.add_radiobutton(
label=theme.title(),
command=lambda t=theme: self._change_theme(t),
value=theme == current_theme,
)
theme_menu.add_separator()
theme_menu.add_command(
label="More Settings...",
command=self._open_settings_window,
)
# Help menu
help_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Help", menu=help_menu)
help_menu.add_command(
label="Settings...",
command=self._open_settings_window,
accelerator="F2",
)
help_menu.add_separator()
help_menu.add_command(
label="Keyboard Shortcuts",
command=self._show_keyboard_shortcuts,
@@ -237,6 +273,7 @@ class MedTrackerApp:
self.root.bind("<Delete>", lambda e: self._delete_selected_entry())
self.root.bind("<Escape>", lambda e: self._clear_selection())
self.root.bind("<F1>", lambda e: self._show_keyboard_shortcuts())
self.root.bind("<F2>", lambda e: self._open_settings_window())
# Make the window focusable so it can receive key events
self.root.focus_set()
@@ -276,10 +313,24 @@ Table Operations:
• Double-click: Edit entry
Help:
• F1: Show this help dialog"""
• F1: Show this help dialog
• F2: Open settings window"""
messagebox.showinfo("Keyboard Shortcuts", shortcuts_text, parent=self.root)
def _change_theme(self, theme_name: str) -> None:
"""Change the application theme."""
if self.theme_manager.apply_theme(theme_name):
self.ui_manager.update_status(
f"Theme changed to: {theme_name.title()}", "info"
)
# Refresh the menu to update radio button selection
self._setup_menu()
else:
self.ui_manager.update_status(
f"Failed to apply theme: {theme_name}", "error"
)
def _show_about_dialog(self) -> None:
"""Show about dialog."""
about_text = """TheChart - Medication Tracker
@@ -315,6 +366,11 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
self.root, self.medicine_manager, self._refresh_ui_after_config_change
)
def _open_settings_window(self) -> None:
"""Open the settings window."""
self.ui_manager.update_status("Opening settings window", "info")
SettingsWindow(self.root, self.theme_manager, self.ui_manager)
def _refresh_ui_after_config_change(self) -> None:
"""Refresh UI components after pathology or medicine configuration changes."""
self.ui_manager.update_status(
@@ -678,9 +734,13 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
# Fallback - just use all columns
display_df = df
# Batch insert for better performance
for _index, row in display_df.iterrows():
self.tree.insert(parent="", index="end", values=list(row))
# Batch insert for better performance with alternating row colors
for index, row in display_df.iterrows():
# Add alternating row tags for better visibility
tag = "evenrow" if index % 2 == 0 else "oddrow"
self.tree.insert(
parent="", index="end", values=list(row), tags=(tag,)
)
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
# Update the graph
+324
View File
@@ -0,0 +1,324 @@
"""Settings window for TheChart application."""
import tkinter as tk
from tkinter import messagebox, ttk
class SettingsWindow:
"""Settings window for application preferences."""
def __init__(self, parent: tk.Tk, theme_manager, ui_manager) -> None:
self.parent = parent
self.theme_manager = theme_manager
self.ui_manager = ui_manager
# Create window
self.window = tk.Toplevel(parent)
self.window.title("Settings - TheChart")
self.window.geometry("500x400")
self.window.resizable(False, False)
# Make window modal
self.window.transient(parent)
self.window.grab_set()
# Center the window
self._center_window()
# Setup UI
self._setup_ui()
# Set initial values
self._load_current_settings()
def _center_window(self) -> None:
"""Center the settings window on the parent."""
self.window.update_idletasks()
# Get window dimensions
window_width = self.window.winfo_reqwidth()
window_height = self.window.winfo_reqheight()
# Get parent window position and size
parent_x = self.parent.winfo_x()
parent_y = self.parent.winfo_y()
parent_width = self.parent.winfo_width()
parent_height = self.parent.winfo_height()
# Calculate centered position
x = parent_x + (parent_width // 2) - (window_width // 2)
y = parent_y + (parent_height // 2) - (window_height // 2)
self.window.geometry(f"{window_width}x{window_height}+{x}+{y}")
def _setup_ui(self) -> None:
"""Setup the settings UI."""
# Main container
main_frame = ttk.Frame(self.window, padding="20", style="Card.TFrame")
main_frame.pack(fill="both", expand=True)
# Title
title_label = ttk.Label(
main_frame,
text="Application Settings",
font=("TkDefaultFont", 16, "bold"),
)
title_label.pack(pady=(0, 20))
# Create notebook for different setting categories
notebook = ttk.Notebook(main_frame, style="Modern.TNotebook")
notebook.pack(fill="both", expand=True, pady=(0, 20))
# Theme settings tab
self._create_theme_tab(notebook)
# UI settings tab
self._create_ui_tab(notebook)
# About tab
self._create_about_tab(notebook)
# Button frame
button_frame = ttk.Frame(main_frame)
button_frame.pack(fill="x", pady=(10, 0))
# Buttons
ttk.Button(
button_frame,
text="Apply",
command=self._apply_settings,
style="Action.TButton",
).pack(side="right", padx=(5, 0))
ttk.Button(
button_frame,
text="Cancel",
command=self._cancel,
style="Action.TButton",
).pack(side="right")
ttk.Button(
button_frame,
text="OK",
command=self._ok,
style="Action.TButton",
).pack(side="right", padx=(0, 5))
def _create_theme_tab(self, notebook: ttk.Notebook) -> None:
"""Create the theme settings tab."""
theme_frame = ttk.Frame(notebook, style="Card.TFrame")
notebook.add(theme_frame, text="Theme")
# Theme selection
theme_label_frame = ttk.LabelFrame(
theme_frame, text="Theme Selection", style="Card.TLabelframe"
)
theme_label_frame.pack(fill="x", padx=10, pady=10)
ttk.Label(
theme_label_frame,
text="Choose your preferred theme:",
font=("TkDefaultFont", 10),
).pack(anchor="w", padx=10, pady=(10, 5))
# Theme radio buttons
self.theme_var = tk.StringVar()
themes = self.theme_manager.get_available_themes()
theme_buttons_frame = ttk.Frame(theme_label_frame)
theme_buttons_frame.pack(fill="x", padx=10, pady=(0, 10))
# Create radio buttons in a grid
for i, theme in enumerate(themes):
row = i // 3
col = i % 3
ttk.Radiobutton(
theme_buttons_frame,
text=theme.title(),
variable=self.theme_var,
value=theme,
style="Modern.TCheckbutton",
).grid(row=row, column=col, sticky="w", padx=5, pady=2)
# Theme preview info
preview_frame = ttk.LabelFrame(
theme_frame, text="Theme Preview", style="Card.TLabelframe"
)
preview_frame.pack(fill="both", expand=True, padx=10, pady=(0, 10))
preview_text = tk.Text(
preview_frame,
height=6,
wrap="word",
font=("TkDefaultFont", 9),
state="disabled",
)
preview_text.pack(fill="both", expand=True, padx=10, pady=10)
# Theme change callback
def on_theme_change():
selected_theme = self.theme_var.get()
preview_text.config(state="normal")
preview_text.delete("1.0", "end")
preview_text.insert(
"1.0",
f"Selected theme: {selected_theme.title()}\\n\\n"
"Theme changes will be applied when you click 'Apply' or 'OK'. "
"The new theme will affect all windows and UI elements "
"in the application.",
)
preview_text.config(state="disabled")
self.theme_var.trace("w", lambda *args: on_theme_change())
def _create_ui_tab(self, notebook: ttk.Notebook) -> None:
"""Create the UI settings tab."""
ui_frame = ttk.Frame(notebook, style="Card.TFrame")
notebook.add(ui_frame, text="Interface")
# Font settings
font_frame = ttk.LabelFrame(
ui_frame, text="Font Settings", style="Card.TLabelframe"
)
font_frame.pack(fill="x", padx=10, pady=10)
ttk.Label(
font_frame,
text="Font size adjustments (requires restart):",
font=("TkDefaultFont", 10),
).pack(anchor="w", padx=10, pady=10)
# Font size scale
self.font_scale_var = tk.DoubleVar(value=1.0)
font_scale = ttk.Scale(
font_frame,
from_=0.8,
to=1.5,
variable=self.font_scale_var,
orient="horizontal",
style="Modern.Horizontal.TScale",
)
font_scale.pack(fill="x", padx=10, pady=(0, 10))
# Scale labels
scale_labels_frame = ttk.Frame(font_frame)
scale_labels_frame.pack(fill="x", padx=10, pady=(0, 10))
ttk.Label(scale_labels_frame, text="Small").pack(side="left")
ttk.Label(scale_labels_frame, text="Large").pack(side="right")
ttk.Label(scale_labels_frame, text="Normal").pack()
# Window settings
window_frame = ttk.LabelFrame(
ui_frame, text="Window Settings", style="Card.TLabelframe"
)
window_frame.pack(fill="x", padx=10, pady=(0, 10))
# Remember window size
self.remember_size_var = tk.BooleanVar(value=True)
ttk.Checkbutton(
window_frame,
text="Remember window size and position",
variable=self.remember_size_var,
style="Modern.TCheckbutton",
).pack(anchor="w", padx=10, pady=10)
# Always on top
self.always_on_top_var = tk.BooleanVar(value=False)
ttk.Checkbutton(
window_frame,
text="Keep window always on top",
variable=self.always_on_top_var,
style="Modern.TCheckbutton",
).pack(anchor="w", padx=10, pady=(0, 10))
def _create_about_tab(self, notebook: ttk.Notebook) -> None:
"""Create the about tab."""
about_frame = ttk.Frame(notebook, style="Card.TFrame")
notebook.add(about_frame, text="About")
# App info
info_frame = ttk.LabelFrame(
about_frame, text="Application Information", style="Card.TLabelframe"
)
info_frame.pack(fill="both", expand=True, padx=10, pady=10)
about_text = tk.Text(
info_frame,
wrap="word",
font=("TkDefaultFont", 10),
state="disabled",
bg=self.theme_manager.get_theme_colors()["bg"],
fg=self.theme_manager.get_theme_colors()["fg"],
)
about_text.pack(fill="both", expand=True, padx=10, pady=10)
about_content = """TheChart - Medication Tracker
Version: 1.9.5
Built with: Python, Tkinter, ttkthemes
Features:
• Modern themed interface with multiple themes
• Medication and pathology tracking
• Visual graphs and charts
• Data export capabilities
• Keyboard shortcuts for efficiency
• Customizable UI settings
This application helps you track your daily medications and health
conditions with an intuitive, modern interface.
Enhanced with ttkthemes for better visual appeal and user experience."""
about_text.config(state="normal")
about_text.insert("1.0", about_content)
about_text.config(state="disabled")
def _load_current_settings(self) -> None:
"""Load current application settings."""
# Set current theme
current_theme = self.theme_manager.get_current_theme()
self.theme_var.set(current_theme)
# Trigger theme change to update preview
if hasattr(self, "theme_var"):
self.theme_var.set(current_theme)
def _apply_settings(self) -> None:
"""Apply the selected settings."""
# Apply theme if changed
selected_theme = self.theme_var.get()
current_theme = self.theme_manager.get_current_theme()
if selected_theme != current_theme:
if self.theme_manager.apply_theme(selected_theme):
self.ui_manager.update_status(
f"Theme changed to: {selected_theme.title()}", "info"
)
else:
messagebox.showerror(
"Error",
f"Failed to apply theme: {selected_theme}",
parent=self.window,
)
return
# Apply other settings (font size, window settings, etc.)
# These would typically be saved to a config file
messagebox.showinfo(
"Settings Applied",
"Settings have been applied successfully!",
parent=self.window,
)
def _ok(self) -> None:
"""Apply settings and close window."""
self._apply_settings()
self.window.destroy()
def _cancel(self) -> None:
"""Close window without applying settings."""
self.window.destroy()
+298
View File
@@ -0,0 +1,298 @@
"""Theme manager for the application using ttkthemes."""
import logging
import tkinter as tk
from tkinter import ttk
from ttkthemes import ThemedStyle
class ThemeManager:
"""Manages application themes and styling."""
def __init__(self, root: tk.Tk, logger: logging.Logger) -> None:
self.root = root
self.logger = logger
self.style: ThemedStyle | None = None
self.current_theme: str = "arc" # Default theme
# Available themes - these are some of the best looking ones
self.available_themes = [
"arc",
"equilux",
"adapta",
"yaru",
"ubuntu",
"plastik",
"breeze",
"elegance",
]
self.initialize_theme()
def initialize_theme(self) -> None:
"""Initialize the themed style."""
try:
self.style = ThemedStyle(self.root)
self.apply_theme(self.current_theme)
self._configure_custom_styles()
self.logger.info(
f"Theme manager initialized with theme: {self.current_theme}"
)
except Exception as e:
self.logger.error(f"Failed to initialize theme manager: {e}")
# Fallback to default ttk styling
self.style = ttk.Style()
def apply_theme(self, theme_name: str) -> bool:
"""Apply a specific theme."""
try:
if self.style and theme_name in self.get_available_themes():
self.style.set_theme(theme_name)
self.current_theme = theme_name
self._configure_custom_styles()
self.logger.info(f"Applied theme: {theme_name}")
return True
else:
self.logger.warning(f"Theme '{theme_name}' not available")
return False
except Exception as e:
self.logger.error(f"Failed to apply theme '{theme_name}': {e}")
return False
def get_available_themes(self) -> list[str]:
"""Get list of available themes."""
if self.style:
try:
# Get all available themes from ttkthemes
all_themes = self.style.theme_names()
# Filter to only include our curated list
return [theme for theme in self.available_themes if theme in all_themes]
except Exception as e:
self.logger.error(f"Failed to get available themes: {e}")
return self.available_themes
return self.available_themes
def get_current_theme(self) -> str:
"""Get the currently active theme."""
return self.current_theme
def _configure_custom_styles(self) -> None:
"""Configure custom styles for better appearance."""
if not self.style:
return
try:
# Get current theme colors for consistent styling
colors = self.get_theme_colors()
# Configure frame styles with better padding and borders
self.style.configure(
"Card.TFrame",
relief="flat",
borderwidth=0,
background=colors["bg"],
)
# Configure label frame styles with modern appearance
self.style.configure(
"Card.TLabelframe",
relief="solid",
borderwidth=1,
background=colors["bg"],
foreground=colors["fg"],
padding=(10, 5, 10, 10),
)
self.style.configure(
"Card.TLabelframe.Label",
background=colors["bg"],
foreground=colors["fg"],
font=("TkDefaultFont", 10, "bold"),
)
# Configure button styles for better appearance
self.style.configure(
"Action.TButton",
padding=(15, 8),
font=("TkDefaultFont", 9, "normal"),
)
# Configure entry styles with modern look
self.style.configure(
"Modern.TEntry",
padding=(8, 5),
borderwidth=1,
relief="solid",
)
# Configure scale styles for pathology inputs
self.style.configure(
"Modern.Horizontal.TScale",
borderwidth=0,
background=colors["bg"],
troughcolor="#e0e0e0",
lightcolor=colors["select_bg"],
darkcolor=colors["select_bg"],
focuscolor=colors["select_bg"],
)
# Configure treeview for better data display
self.style.configure(
"Modern.Treeview",
rowheight=28,
borderwidth=1,
relief="solid",
background=colors["bg"],
foreground=colors["fg"],
fieldbackground=colors["bg"],
selectbackground=colors["select_bg"],
selectforeground=colors["select_fg"],
)
self.style.configure(
"Modern.Treeview.Heading",
padding=(8, 6),
relief="flat",
borderwidth=1,
background=colors["select_bg"],
foreground=colors["select_fg"],
font=("TkDefaultFont", 9, "bold"),
)
# Configure comprehensive row selection colors for better visibility
self.style.map(
"Modern.Treeview",
background=[
("selected", colors["select_bg"]),
("active", colors["select_bg"]),
("focus", colors["select_bg"]),
("", colors["bg"]),
],
foreground=[
("selected", colors["select_fg"]),
("active", colors["select_fg"]),
("focus", colors["select_fg"]),
("", colors["fg"]),
],
selectbackground=[
("focus", colors["select_bg"]),
("", colors["select_bg"]),
],
selectforeground=[
("focus", colors["select_fg"]),
("", colors["select_fg"]),
],
)
# Configure notebook tabs with modern styling
self.style.configure(
"Modern.TNotebook.Tab",
padding=(15, 8),
borderwidth=1,
relief="flat",
)
self.style.map(
"Modern.TNotebook.Tab",
background=[("selected", colors["select_bg"])],
foreground=[("selected", colors["select_fg"])],
)
# Configure checkbutton for medicine selection
self.style.configure(
"Modern.TCheckbutton",
padding=(8, 4),
background=colors["bg"],
foreground=colors["fg"],
focuscolor=colors["select_bg"],
)
self.logger.debug("Enhanced custom styles configured")
except Exception as e:
self.logger.error(f"Failed to configure custom styles: {e}")
def configure_widget_style(self, widget: tk.Widget, style_name: str) -> None:
"""Apply a specific style to a widget."""
try:
if hasattr(widget, "configure") and self.style:
widget.configure(style=style_name)
except Exception as e:
self.logger.error(f"Failed to configure widget style '{style_name}': {e}")
def get_theme_colors(self) -> dict[str, str]:
"""Get current theme colors for custom widgets."""
if not self.style:
return {
"bg": "#ffffff",
"fg": "#000000",
"select_bg": "#3584e4",
"select_fg": "#ffffff",
"alt_bg": "#f5f5f5",
}
try:
# Get colors from current theme
bg = self.style.lookup("TFrame", "background") or "#ffffff"
fg = self.style.lookup("TLabel", "foreground") or "#000000"
# Try to get better selection colors from different widget states
select_bg = (
self.style.lookup("TButton", "background", ["pressed"])
or self.style.lookup("TButton", "background", ["active"])
or self.style.lookup("Treeview", "selectbackground")
or "#0078d4" # Modern blue fallback
)
select_fg = (
self.style.lookup("TButton", "foreground", ["pressed"])
or self.style.lookup("TButton", "foreground", ["active"])
or self.style.lookup("Treeview", "selectforeground")
or "#ffffff" # White fallback
)
# Ensure contrast - if selection colors are too similar to background,
# use fallbacks
if select_bg == bg or select_bg.lower() == bg.lower():
select_bg = "#0078d4" if bg != "#0078d4" else "#0066cc"
if select_fg == fg or select_fg.lower() == fg.lower():
select_fg = "#ffffff" if fg != "#ffffff" else "#000000"
# Calculate alternating row color
if bg.startswith("#"):
try:
rgb = tuple(int(bg[i : i + 2], 16) for i in (1, 3, 5))
if sum(rgb) > 384: # Light theme
alt_bg = (
f"#{max(0, rgb[0] - 10):02x}"
f"{max(0, rgb[1] - 10):02x}"
f"{max(0, rgb[2] - 10):02x}"
)
else: # Dark theme
alt_bg = (
f"#{min(255, rgb[0] + 10):02x}"
f"{min(255, rgb[1] + 10):02x}"
f"{min(255, rgb[2] + 10):02x}"
)
except ValueError:
alt_bg = "#f5f5f5"
else:
alt_bg = "#f5f5f5"
return {
"bg": bg,
"fg": fg,
"select_bg": select_bg,
"select_fg": select_fg,
"alt_bg": alt_bg, # Add alternating background color
}
except Exception as e:
self.logger.error(f"Failed to get theme colors: {e}")
return {
"bg": "#ffffff",
"fg": "#000000",
"select_bg": "#3584e4",
"select_fg": "#ffffff",
"alt_bg": "#f5f5f5",
}
+163
View File
@@ -0,0 +1,163 @@
"""Tooltip system for enhanced user experience."""
import tkinter as tk
class ToolTip:
"""Create a tooltip for a given widget."""
def __init__(
self,
widget: tk.Widget,
text: str,
delay: int = 500,
wrap_length: int = 250,
) -> None:
self.widget = widget
self.text = text
self.delay = delay
self.wrap_length = wrap_length
self.tooltip: tk.Toplevel | None = None
self.id_after: str | None = None
# Bind events
self.widget.bind("<Enter>", self._on_enter)
self.widget.bind("<Leave>", self._on_leave)
self.widget.bind("<ButtonPress>", self._on_leave)
def _on_enter(self, event: tk.Event | None = None) -> None:
"""Mouse entered widget - schedule tooltip."""
self._cancel_scheduled()
self.id_after = self.widget.after(self.delay, self._show_tooltip)
def _on_leave(self, event: tk.Event | None = None) -> None:
"""Mouse left widget - hide tooltip."""
self._cancel_scheduled()
self._hide_tooltip()
def _cancel_scheduled(self) -> None:
"""Cancel any scheduled tooltip."""
if self.id_after:
self.widget.after_cancel(self.id_after)
self.id_after = None
def _show_tooltip(self) -> None:
"""Display the tooltip."""
if self.tooltip:
return
# Get widget position
x = self.widget.winfo_rootx() + 25
y = self.widget.winfo_rooty() + 25
# Create tooltip window
self.tooltip = tk.Toplevel(self.widget)
self.tooltip.wm_overrideredirect(True)
self.tooltip.wm_geometry(f"+{x}+{y}")
# Create tooltip content
label = tk.Label(
self.tooltip,
text=self.text,
justify="left",
background="#ffffe0",
foreground="#000000",
relief="solid",
borderwidth=1,
font=("TkDefaultFont", "9", "normal"),
wraplength=self.wrap_length,
padx=8,
pady=6,
)
label.pack()
# Make sure tooltip appears above other windows
self.tooltip.lift()
def _hide_tooltip(self) -> None:
"""Hide the tooltip."""
if self.tooltip:
self.tooltip.destroy()
self.tooltip = None
def update_text(self, new_text: str) -> None:
"""Update the tooltip text."""
self.text = new_text
class TooltipManager:
"""Manages tooltips for UI elements."""
def __init__(self, theme_manager) -> None:
self.theme_manager = theme_manager
self.tooltips: list[ToolTip] = []
def add_tooltip(
self,
widget: tk.Widget,
text: str,
delay: int = 500,
wrap_length: int = 250,
) -> ToolTip:
"""Add a tooltip to a widget."""
tooltip = ToolTip(widget, text, delay, wrap_length)
self.tooltips.append(tooltip)
return tooltip
def add_scale_tooltip(self, scale_widget: tk.Widget, pathology_name: str) -> None:
"""Add a specialized tooltip for pathology scales."""
text = (
f"Adjust your {pathology_name} level\\n"
"• Drag the slider to set your current level\\n"
"• Higher values typically indicate worse symptoms\\n"
"• Use the full range for accurate tracking"
)
self.add_tooltip(scale_widget, text, delay=800)
def add_medicine_tooltip(self, widget: tk.Widget, medicine_name: str) -> None:
"""Add a specialized tooltip for medicine checkboxes."""
text = (
f"Mark if you took {medicine_name} today\\n"
"• Check the box when you've taken this medication\\n"
"• This helps track your medication adherence\\n"
"• You can add dose details when editing entries"
)
self.add_tooltip(widget, text, delay=600)
def add_button_tooltip(self, widget: tk.Widget, action: str) -> None:
"""Add a tooltip for action buttons."""
tooltips_map = {
"save": (
"Save your current entry (Ctrl+S)\\nThis will add a new daily record"
),
"export": (
"Export your data to various formats\\n"
"Supports CSV, PDF, and image exports"
),
"refresh": (
"Reload data from file (F5)\\nUpdates the display with latest changes"
),
"settings": (
"Open application settings (F2)\\nCustomize themes and preferences"
),
"quit": (
"Exit the application (Ctrl+Q)\\nYour data will be automatically saved"
),
}
text = tooltips_map.get(action, f"Perform {action} action")
self.add_tooltip(widget, text, delay=400)
def add_menu_tooltip(self, widget: tk.Widget, menu_type: str) -> None:
"""Add tooltips for menu items."""
tooltips_map = {
"theme": (
"Quick theme selection\\nClick to instantly change the app's appearance"
),
"file": "File operations\\nExport data and manage files",
"tools": ("Data management tools\\nConfigure medicines and pathologies"),
"help": ("Get help and information\\nKeyboard shortcuts and about dialog"),
}
text = tooltips_map.get(menu_type, "Menu options")
self.add_tooltip(widget, text, delay=600)
+98 -16
View File
@@ -11,6 +11,7 @@ from PIL import Image, ImageTk
from medicine_manager import MedicineManager
from pathology_manager import PathologyManager
from tooltip_system import TooltipManager
class UIManager:
@@ -22,17 +23,22 @@ class UIManager:
logger: logging.Logger,
medicine_manager: MedicineManager,
pathology_manager: PathologyManager,
theme_manager, # Import would create circular dependency
) -> None:
self.root: tk.Tk = root
self.logger: logging.Logger = logger
self.medicine_manager = medicine_manager
self.pathology_manager = pathology_manager
self.theme_manager = theme_manager
# Status bar attributes
self.status_bar: tk.Frame | None = None
self.status_label: tk.Label | None = None
self.file_info_label: tk.Label | None = None
# Initialize tooltip manager
self.tooltip_manager = TooltipManager(theme_manager)
def setup_application_icon(self, img_path: str) -> bool:
"""Set up the application icon."""
try:
@@ -70,13 +76,20 @@ class UIManager:
def create_input_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
"""Create and configure the input frame with all widgets."""
# Create main container for the scrollable input frame
main_container = ttk.LabelFrame(parent_frame, text="New Entry")
main_container = ttk.LabelFrame(
parent_frame, text="New Entry", style="Card.TLabelframe"
)
main_container.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
main_container.grid_rowconfigure(0, weight=1)
main_container.grid_columnconfigure(0, weight=1)
# Create canvas and scrollbar for scrolling
canvas = tk.Canvas(main_container, highlightthickness=0)
theme_colors = self.theme_manager.get_theme_colors()
canvas = tk.Canvas(
main_container,
highlightthickness=0,
bg=theme_colors["bg"],
)
scrollbar = ttk.Scrollbar(
main_container, orient="vertical", command=canvas.yview
)
@@ -164,7 +177,9 @@ class UIManager:
ttk.Label(input_frame, text="Treatment:").grid(
row=medicine_row, column=0, sticky="w", padx=5, pady=2
)
medicine_frame = ttk.LabelFrame(input_frame, text="Medicine")
medicine_frame = ttk.LabelFrame(
input_frame, text="Medicine", style="Card.TLabelframe"
)
medicine_frame.grid(row=medicine_row, column=1, padx=0, pady=10, sticky="nsew")
medicine_frame.grid_columnconfigure(0, weight=1)
@@ -178,11 +193,19 @@ class UIManager:
text = f"{medicine.display_name} {medicine.dosage_info}"
medicine_vars[medicine_key] = (var, text)
for idx, (_med_name, (var, text)) in enumerate(medicine_vars.items()):
for idx, (med_key, (var, text)) in enumerate(medicine_vars.items()):
# Just checkbox for medicine taken
ttk.Checkbutton(medicine_frame, text=text, variable=var).grid(
row=idx, column=0, sticky="w", padx=5, pady=2
checkbox = ttk.Checkbutton(
medicine_frame, text=text, variable=var, style="Modern.TCheckbutton"
)
checkbox.grid(row=idx, column=0, sticky="w", padx=5, pady=2)
# Add tooltip for medicine checkbox
medicine = self.medicine_manager.get_medicine(med_key)
if medicine:
self.tooltip_manager.add_medicine_tooltip(
checkbox, medicine.display_name
)
# Note and Date fields - adjust row numbers
note_row = medicine_row + 1
@@ -194,16 +217,19 @@ class UIManager:
ttk.Label(input_frame, text="Note:").grid(
row=note_row, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(input_frame, textvariable=note_var).grid(
ttk.Entry(input_frame, textvariable=note_var, style="Modern.TEntry").grid(
row=note_row, column=1, sticky="ew", padx=5, pady=2
)
ttk.Label(input_frame, text="Date (mm/dd/yyyy):").grid(
row=date_row, column=0, sticky="w", padx=5, pady=2
)
ttk.Entry(input_frame, textvariable=date_var, justify="center").grid(
row=date_row, column=1, sticky="ew", padx=5, pady=2
)
ttk.Entry(
input_frame,
textvariable=date_var,
justify="center",
style="Modern.TEntry",
).grid(row=date_row, column=1, sticky="ew", padx=5, pady=2)
# Set default date to today
date_var.set(datetime.now().strftime("%m/%d/%Y"))
@@ -225,7 +251,7 @@ class UIManager:
def create_table_frame(self, parent_frame: ttk.Frame) -> dict[str, Any]:
"""Create and configure the table frame with a treeview."""
table_frame: ttk.LabelFrame = ttk.LabelFrame(
parent_frame, text="Log (Double-click to edit)"
parent_frame, text="Log (Double-click to edit)", style="Card.TLabelframe"
)
table_frame.grid(row=1, column=1, padx=10, pady=10, sticky="nsew")
@@ -258,7 +284,34 @@ class UIManager:
col_labels.append("Note")
col_settings.append(("Note", 300, "w"))
tree: ttk.Treeview = ttk.Treeview(table_frame, columns=columns, show="headings")
tree: ttk.Treeview = ttk.Treeview(
table_frame, columns=columns, show="headings", style="Modern.Treeview"
)
# Configure treeview selection behavior
tree.configure(selectmode="browse") # Single selection mode
# Configure row tags for alternating colors
theme_colors = self.theme_manager.get_theme_colors()
tree.tag_configure("evenrow", background=theme_colors["bg"])
tree.tag_configure("oddrow", background=theme_colors["alt_bg"])
# Configure selection highlighting
tree.tag_configure(
"selected",
background=theme_colors["select_bg"],
foreground=theme_colors["select_fg"],
)
# Bind selection events to ensure proper highlighting
def on_selection_change(event):
"""Handle treeview selection changes to ensure proper highlighting."""
selection = tree.selection()
if selection:
# Force focus to ensure selection is visible
tree.focus(selection[0])
tree.bind("<<TreeviewSelect>>", on_selection_change)
for col, label in zip(columns, col_labels, strict=False):
tree.heading(col, text=label)
@@ -277,7 +330,9 @@ class UIManager:
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
"""Create and configure the graph frame."""
graph_frame: ttk.LabelFrame = ttk.LabelFrame(parent_frame, text="Evolution")
graph_frame: ttk.LabelFrame = ttk.LabelFrame(
parent_frame, text="Evolution", style="Card.TLabelframe"
)
graph_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=10, sticky="nsew")
return graph_frame
@@ -289,23 +344,40 @@ class UIManager:
button_frame.grid(row=7, column=0, columnspan=2, pady=10)
for btn_config in buttons_config:
ttk.Button(
button = ttk.Button(
button_frame,
text=btn_config["text"],
command=btn_config["command"],
).pack(
style="Action.TButton",
)
button.pack(
side="left",
padx=5,
fill=btn_config.get("fill", None),
expand=btn_config.get("expand", False),
)
# Add tooltips based on button text
button_text = btn_config["text"].lower()
if "add" in button_text or "save" in button_text:
self.tooltip_manager.add_button_tooltip(button, "save")
elif "quit" in button_text or "exit" in button_text:
self.tooltip_manager.add_button_tooltip(button, "quit")
return button_frame
def create_status_bar(self, parent_frame: tk.Widget) -> tk.Frame:
"""Create and configure the status bar at the bottom of the application."""
# Get theme colors for consistent styling
theme_colors = self.theme_manager.get_theme_colors()
# Create the status bar frame
self.status_bar = tk.Frame(parent_frame, relief=tk.SUNKEN, bd=1)
self.status_bar = tk.Frame(
parent_frame,
relief=tk.SUNKEN,
bd=1,
bg=theme_colors["bg"],
)
self.status_bar.grid(row=2, column=0, columnspan=2, sticky="ew", padx=5, pady=2)
# Configure the parent to make the status bar stretch
@@ -319,6 +391,8 @@ class UIManager:
font=("TkDefaultFont", 9),
padx=10,
pady=2,
bg=theme_colors["bg"],
fg=theme_colors["fg"],
)
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
@@ -330,6 +404,8 @@ class UIManager:
font=("TkDefaultFont", 9),
padx=10,
pady=2,
bg=theme_colors["bg"],
fg=theme_colors["fg"],
)
self.file_info_label.pack(side=tk.RIGHT)
@@ -793,9 +869,15 @@ class UIManager:
variable=vars_dict[key],
orient=tk.HORIZONTAL,
length=250,
style="Modern.Horizontal.TScale",
)
scale.grid(row=0, column=1, sticky="ew")
# Add tooltip for the scale
pathology = self.pathology_manager.get_pathology(key)
if pathology:
self.tooltip_manager.add_scale_tooltip(scale, pathology.display_name)
# Scale labels
labels_frame = ttk.Frame(scale_container)
labels_frame.grid(row=1, column=0, sticky="ew", pady=(5, 0))