feat: add status bar to UI for improved user feedback and information display
This commit is contained in:
+73
-26
@@ -108,7 +108,7 @@ class MedTrackerApp:
|
|||||||
self.root.grid_columnconfigure(0, weight=1)
|
self.root.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# Configure main frame grid for scaling
|
# Configure main frame grid for scaling
|
||||||
for i in range(2):
|
for i in range(3): # Changed from 2 to 3 to accommodate status bar
|
||||||
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
|
main_frame.grid_rowconfigure(i, weight=1 if i == 1 else 0)
|
||||||
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
main_frame.grid_columnconfigure(i, weight=3 if i == 1 else 1)
|
||||||
logger.debug("Main frame and root grid configured for scaling.")
|
logger.debug("Main frame and root grid configured for scaling.")
|
||||||
@@ -155,9 +155,15 @@ class MedTrackerApp:
|
|||||||
self.tree: ttk.Treeview = table_ui["tree"]
|
self.tree: ttk.Treeview = table_ui["tree"]
|
||||||
self.tree.bind("<Double-1>", self.handle_double_click)
|
self.tree.bind("<Double-1>", self.handle_double_click)
|
||||||
|
|
||||||
|
# --- Create Status Bar ---
|
||||||
|
self.status_bar = self.ui_manager.create_status_bar(main_frame)
|
||||||
|
|
||||||
# Load data
|
# Load data
|
||||||
self.refresh_data_display()
|
self.refresh_data_display()
|
||||||
|
|
||||||
|
# Initialize status bar with ready message
|
||||||
|
self.ui_manager.update_status("Application ready", "info")
|
||||||
|
|
||||||
def _setup_menu(self) -> None:
|
def _setup_menu(self) -> None:
|
||||||
"""Set up the menu bar."""
|
"""Set up the menu bar."""
|
||||||
menubar = tk.Menu(self.root)
|
menubar = tk.Menu(self.root)
|
||||||
@@ -182,22 +188,29 @@ class MedTrackerApp:
|
|||||||
|
|
||||||
def _open_export_window(self) -> None:
|
def _open_export_window(self) -> None:
|
||||||
"""Open the export window."""
|
"""Open the export window."""
|
||||||
|
self.ui_manager.update_status("Opening export window", "info")
|
||||||
ExportWindow(self.root, self.export_manager)
|
ExportWindow(self.root, self.export_manager)
|
||||||
|
|
||||||
def _open_pathology_manager(self) -> None:
|
def _open_pathology_manager(self) -> None:
|
||||||
"""Open the pathology management window."""
|
"""Open the pathology management window."""
|
||||||
|
self.ui_manager.update_status("Opening pathology manager", "info")
|
||||||
PathologyManagementWindow(
|
PathologyManagementWindow(
|
||||||
self.root, self.pathology_manager, self._refresh_ui_after_config_change
|
self.root, self.pathology_manager, self._refresh_ui_after_config_change
|
||||||
)
|
)
|
||||||
|
|
||||||
def _open_medicine_manager(self) -> None:
|
def _open_medicine_manager(self) -> None:
|
||||||
"""Open the medicine management window."""
|
"""Open the medicine management window."""
|
||||||
|
self.ui_manager.update_status("Opening medicine manager", "info")
|
||||||
MedicineManagementWindow(
|
MedicineManagementWindow(
|
||||||
self.root, self.medicine_manager, self._refresh_ui_after_config_change
|
self.root, self.medicine_manager, self._refresh_ui_after_config_change
|
||||||
)
|
)
|
||||||
|
|
||||||
def _refresh_ui_after_config_change(self) -> None:
|
def _refresh_ui_after_config_change(self) -> None:
|
||||||
"""Refresh UI components after pathology or medicine configuration changes."""
|
"""Refresh UI components after pathology or medicine configuration changes."""
|
||||||
|
self.ui_manager.update_status(
|
||||||
|
"Refreshing UI after configuration change", "info"
|
||||||
|
)
|
||||||
|
|
||||||
# Clear caches in optimized data manager
|
# Clear caches in optimized data manager
|
||||||
if hasattr(self.data_manager, "_invalidate_cache"):
|
if hasattr(self.data_manager, "_invalidate_cache"):
|
||||||
self.data_manager._invalidate_cache()
|
self.data_manager._invalidate_cache()
|
||||||
@@ -238,14 +251,22 @@ class MedTrackerApp:
|
|||||||
# Refresh data display
|
# Refresh data display
|
||||||
self.refresh_data_display()
|
self.refresh_data_display()
|
||||||
|
|
||||||
|
# Update status to show completion
|
||||||
|
self.ui_manager.update_status("UI refreshed successfully", "success")
|
||||||
|
|
||||||
def handle_double_click(self, event: tk.Event) -> None:
|
def handle_double_click(self, event: tk.Event) -> None:
|
||||||
"""Handle double-click event to edit an entry."""
|
"""Handle double-click event to edit an entry."""
|
||||||
logger.debug("Double-click event triggered on treeview.")
|
logger.debug("Double-click event triggered on treeview.")
|
||||||
if len(self.tree.get_children()) > 0:
|
if len(self.tree.get_children()) > 0:
|
||||||
item_id = self.tree.selection()[0]
|
item_id = self.tree.selection()[0]
|
||||||
item_values = self.tree.item(item_id, "values")
|
item_values = self.tree.item(item_id, "values")
|
||||||
|
self.ui_manager.update_status(
|
||||||
|
f"Opening entry for {item_values[0]} for editing", "info"
|
||||||
|
)
|
||||||
logger.debug(f"Editing item_id={item_id}, values={item_values}")
|
logger.debug(f"Editing item_id={item_id}, values={item_values}")
|
||||||
self._create_edit_window(item_id, item_values)
|
self._create_edit_window(item_id, item_values)
|
||||||
|
else:
|
||||||
|
self.ui_manager.update_status("No entries to edit", "warning")
|
||||||
|
|
||||||
def _create_edit_window(self, item_id: str, values: tuple[str, ...]) -> None:
|
def _create_edit_window(self, item_id: str, values: tuple[str, ...]) -> None:
|
||||||
"""Create a new Toplevel window for editing an entry."""
|
"""Create a new Toplevel window for editing an entry."""
|
||||||
@@ -347,8 +368,10 @@ class MedTrackerApp:
|
|||||||
|
|
||||||
values.append(note)
|
values.append(note)
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Saving changes...", "info")
|
||||||
if self.data_manager.update_entry(original_date, values):
|
if self.data_manager.update_entry(original_date, values):
|
||||||
edit_win.destroy()
|
edit_win.destroy()
|
||||||
|
self.ui_manager.update_status("Entry updated successfully!", "success")
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry updated successfully!", parent=self.root
|
"Success", "Entry updated successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
@@ -358,6 +381,7 @@ class MedTrackerApp:
|
|||||||
# Check if it's a duplicate date issue
|
# Check if it's a duplicate date issue
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
if original_date != date and not df.empty and date in df["date"].values:
|
if original_date != date and not df.empty and date in df["date"].values:
|
||||||
|
self.ui_manager.update_status("Duplicate date found", "error")
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Error",
|
"Error",
|
||||||
f"An entry for date '{date}' already exists. "
|
f"An entry for date '{date}' already exists. "
|
||||||
@@ -365,6 +389,7 @@ class MedTrackerApp:
|
|||||||
parent=edit_win,
|
parent=edit_win,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to save changes", "error")
|
||||||
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
messagebox.showerror("Error", "Failed to save changes", parent=edit_win)
|
||||||
|
|
||||||
def handle_window_closing(self) -> None:
|
def handle_window_closing(self) -> None:
|
||||||
@@ -409,10 +434,13 @@ class MedTrackerApp:
|
|||||||
|
|
||||||
# Check if date is empty
|
# Check if date is empty
|
||||||
if not self.date_var.get().strip():
|
if not self.date_var.get().strip():
|
||||||
|
self.ui_manager.update_status("Please enter a date", "error")
|
||||||
messagebox.showerror("Error", "Please enter a date.", parent=self.root)
|
messagebox.showerror("Error", "Please enter a date.", parent=self.root)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Adding new entry...", "info")
|
||||||
if self.data_manager.add_entry(entry):
|
if self.data_manager.add_entry(entry):
|
||||||
|
self.ui_manager.update_status("Entry added successfully!", "success")
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry added successfully!", parent=self.root
|
"Success", "Entry added successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
@@ -422,6 +450,7 @@ class MedTrackerApp:
|
|||||||
# Check if it's a duplicate date by trying to load existing data
|
# Check if it's a duplicate date by trying to load existing data
|
||||||
df = self.data_manager.load_data()
|
df = self.data_manager.load_data()
|
||||||
if not df.empty and self.date_var.get() in df["date"].values:
|
if not df.empty and self.date_var.get() in df["date"].values:
|
||||||
|
self.ui_manager.update_status("Duplicate entry found", "error")
|
||||||
messagebox.showerror(
|
messagebox.showerror(
|
||||||
"Error",
|
"Error",
|
||||||
f"An entry for date '{self.date_var.get()}' already exists. "
|
f"An entry for date '{self.date_var.get()}' already exists. "
|
||||||
@@ -429,6 +458,7 @@ class MedTrackerApp:
|
|||||||
parent=self.root,
|
parent=self.root,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to add entry", "error")
|
||||||
messagebox.showerror("Error", "Failed to add entry", parent=self.root)
|
messagebox.showerror("Error", "Failed to add entry", parent=self.root)
|
||||||
|
|
||||||
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
def _delete_entry(self, edit_win: tk.Toplevel, item_id: str) -> None:
|
||||||
@@ -443,13 +473,16 @@ class MedTrackerApp:
|
|||||||
date: str = self.tree.item(item_id, "values")[0]
|
date: str = self.tree.item(item_id, "values")[0]
|
||||||
logger.debug(f"Deleting entry with date={date}")
|
logger.debug(f"Deleting entry with date={date}")
|
||||||
|
|
||||||
|
self.ui_manager.update_status("Deleting entry...", "info")
|
||||||
if self.data_manager.delete_entry(date):
|
if self.data_manager.delete_entry(date):
|
||||||
edit_win.destroy()
|
edit_win.destroy()
|
||||||
|
self.ui_manager.update_status("Entry deleted successfully!", "success")
|
||||||
messagebox.showinfo(
|
messagebox.showinfo(
|
||||||
"Success", "Entry deleted successfully!", parent=self.root
|
"Success", "Entry deleted successfully!", parent=self.root
|
||||||
)
|
)
|
||||||
self.refresh_data_display()
|
self.refresh_data_display()
|
||||||
else:
|
else:
|
||||||
|
self.ui_manager.update_status("Failed to delete entry", "error")
|
||||||
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
messagebox.showerror("Error", "Failed to delete entry", parent=edit_win)
|
||||||
|
|
||||||
def _clear_entries(self) -> None:
|
def _clear_entries(self) -> None:
|
||||||
@@ -471,38 +504,52 @@ class MedTrackerApp:
|
|||||||
if children:
|
if children:
|
||||||
self.tree.delete(*children)
|
self.tree.delete(*children)
|
||||||
|
|
||||||
# Load data from the CSV file
|
try:
|
||||||
df: pd.DataFrame = self.data_manager.load_data()
|
# Load data from the CSV file
|
||||||
|
df: pd.DataFrame = self.data_manager.load_data()
|
||||||
|
|
||||||
# Update the treeview with the data
|
# Update the treeview with the data
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
# Build display columns dynamically (exclude dose columns for table view)
|
# Build display columns dynamically
|
||||||
display_columns = ["date"]
|
# (exclude dose columns for table view)
|
||||||
|
display_columns = ["date"]
|
||||||
|
|
||||||
# Add pathology columns
|
# Add pathology columns
|
||||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||||
display_columns.append(pathology_key)
|
display_columns.append(pathology_key)
|
||||||
|
|
||||||
# Add medicine columns (without dose columns)
|
# Add medicine columns (without dose columns)
|
||||||
for medicine_key in self.medicine_manager.get_medicine_keys():
|
for medicine_key in self.medicine_manager.get_medicine_keys():
|
||||||
display_columns.append(medicine_key)
|
display_columns.append(medicine_key)
|
||||||
|
|
||||||
display_columns.append("note")
|
display_columns.append("note")
|
||||||
|
|
||||||
# Filter to only the columns we want to display
|
# Filter to only the columns we want to display
|
||||||
if all(col in df.columns for col in display_columns):
|
if all(col in df.columns for col in display_columns):
|
||||||
display_df = df[display_columns]
|
display_df = df[display_columns]
|
||||||
|
else:
|
||||||
|
# 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))
|
||||||
|
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
||||||
|
|
||||||
|
# Update the graph
|
||||||
|
self.graph_manager.update_graph(df)
|
||||||
|
|
||||||
|
# Update status bar with file info
|
||||||
|
entry_count = len(df) if not df.empty else 0
|
||||||
|
self.ui_manager.update_file_info(self.filename, entry_count)
|
||||||
|
if entry_count == 0:
|
||||||
|
self.ui_manager.update_status("No data to display", "warning")
|
||||||
else:
|
else:
|
||||||
# Fallback - just use all columns
|
self.ui_manager.update_status("Data loaded successfully", "success")
|
||||||
display_df = df
|
|
||||||
|
|
||||||
# Batch insert for better performance
|
except Exception as e:
|
||||||
for _index, row in display_df.iterrows():
|
logger.error(f"Error loading data: {e}")
|
||||||
self.tree.insert(parent="", index="end", values=list(row))
|
self.ui_manager.update_status(f"Error loading data: {str(e)}", "error")
|
||||||
logger.debug(f"Loaded {len(display_df)} entries into treeview.")
|
|
||||||
|
|
||||||
# Update the graph
|
|
||||||
self.graph_manager.update_graph(df)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ class UIManager:
|
|||||||
self.medicine_manager = medicine_manager
|
self.medicine_manager = medicine_manager
|
||||||
self.pathology_manager = pathology_manager
|
self.pathology_manager = pathology_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
|
||||||
|
|
||||||
def setup_application_icon(self, img_path: str) -> bool:
|
def setup_application_icon(self, img_path: str) -> bool:
|
||||||
"""Set up the application icon."""
|
"""Set up the application icon."""
|
||||||
try:
|
try:
|
||||||
@@ -297,6 +302,103 @@ class UIManager:
|
|||||||
|
|
||||||
return button_frame
|
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."""
|
||||||
|
# Create the status bar frame
|
||||||
|
self.status_bar = tk.Frame(parent_frame, relief=tk.SUNKEN, bd=1)
|
||||||
|
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
|
||||||
|
parent_frame.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
|
# Create status message label (left side)
|
||||||
|
self.status_label = tk.Label(
|
||||||
|
self.status_bar,
|
||||||
|
text="Ready",
|
||||||
|
anchor=tk.W,
|
||||||
|
font=("TkDefaultFont", 9),
|
||||||
|
padx=10,
|
||||||
|
pady=2,
|
||||||
|
)
|
||||||
|
self.status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
|
||||||
|
# Create file info label (right side)
|
||||||
|
self.file_info_label = tk.Label(
|
||||||
|
self.status_bar,
|
||||||
|
text="",
|
||||||
|
anchor=tk.E,
|
||||||
|
font=("TkDefaultFont", 9),
|
||||||
|
padx=10,
|
||||||
|
pady=2,
|
||||||
|
)
|
||||||
|
self.file_info_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
return self.status_bar
|
||||||
|
|
||||||
|
def update_status(self, message: str, message_type: str = "info") -> None:
|
||||||
|
"""
|
||||||
|
Update the status bar with a message.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to display
|
||||||
|
message_type: Type of message ('info', 'success', 'warning', 'error')
|
||||||
|
"""
|
||||||
|
if not self.status_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Color mapping for different message types
|
||||||
|
colors = {
|
||||||
|
"info": "#000000", # Black
|
||||||
|
"success": "#28A745", # Green
|
||||||
|
"warning": "#FFC107", # Yellow/Orange
|
||||||
|
"error": "#DC3545", # Red
|
||||||
|
}
|
||||||
|
|
||||||
|
color = colors.get(message_type, "#000000")
|
||||||
|
self.status_label.config(text=message, fg=color)
|
||||||
|
|
||||||
|
# Clear the message after 5 seconds for non-info messages
|
||||||
|
if message_type != "info":
|
||||||
|
self.root.after(5000, lambda: self.update_status("Ready", "info"))
|
||||||
|
|
||||||
|
def update_file_info(self, filename: str, entry_count: int = 0) -> None:
|
||||||
|
"""
|
||||||
|
Update the file information in the status bar.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename: Name of the current data file
|
||||||
|
entry_count: Number of entries in the file
|
||||||
|
"""
|
||||||
|
if not self.file_info_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
file_display = os.path.basename(filename) if filename else "No file"
|
||||||
|
info_text = f"{file_display}"
|
||||||
|
if entry_count > 0:
|
||||||
|
info_text += f" ({entry_count} entries)"
|
||||||
|
|
||||||
|
self.file_info_label.config(text=info_text)
|
||||||
|
|
||||||
|
def show_status_message(self, message: str, duration: int = 3000) -> None:
|
||||||
|
"""
|
||||||
|
Show a temporary status message for a specific duration.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
message: The message to display
|
||||||
|
duration: How long to show the message in milliseconds
|
||||||
|
"""
|
||||||
|
if not self.status_label:
|
||||||
|
return
|
||||||
|
|
||||||
|
original_text = self.status_label.cget("text")
|
||||||
|
original_color = self.status_label.cget("fg")
|
||||||
|
|
||||||
|
self.status_label.config(text=message, fg="#2E86AB")
|
||||||
|
self.root.after(
|
||||||
|
duration,
|
||||||
|
lambda: self.status_label.config(text=original_text, fg=original_color),
|
||||||
|
)
|
||||||
|
|
||||||
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:
|
||||||
|
|||||||
Reference in New Issue
Block a user