feat: Enhance UI feedback and improve data filtering logic
This commit is contained in:
+31
-14
@@ -1017,9 +1017,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
self._mark_data_modified() # Mark for auto-save
|
||||
edit_win.destroy()
|
||||
self.ui_manager.update_status("Entry updated successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
"Success", "Entry updated successfully!", parent=self.root
|
||||
)
|
||||
if hasattr(self.ui_manager, "show_toast"):
|
||||
self.ui_manager.show_toast("Entry updated", 1500)
|
||||
self._clear_entries()
|
||||
self.refresh_data_display()
|
||||
new_date = values[0]
|
||||
@@ -1170,11 +1169,23 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
return
|
||||
entry_data["note"] = validated_note
|
||||
|
||||
# Check entry completeness
|
||||
is_complete, missing_fields = InputValidator.validate_entry_completeness(
|
||||
entry_data
|
||||
# Check entry completeness: require date and at least one of
|
||||
# (any pathology score > 0) or (any medicine taken == 1)
|
||||
missing_fields: list[str] = []
|
||||
if not entry_data.get("date"):
|
||||
missing_fields.append("Date")
|
||||
|
||||
has_pathology = any(
|
||||
entry_data.get(k, 0) > 0
|
||||
for k in self.pathology_manager.get_pathology_keys()
|
||||
)
|
||||
if not is_complete:
|
||||
has_medicine = any(
|
||||
entry_data.get(k, 0) == 1 for k in self.medicine_manager.get_medicine_keys()
|
||||
)
|
||||
if not (has_pathology or has_medicine):
|
||||
missing_fields.append("At least one pathology score or medicine entry")
|
||||
|
||||
if missing_fields:
|
||||
missing_msg = "Missing required data:\n" + "\n".join(
|
||||
f"• {field}" for field in missing_fields
|
||||
)
|
||||
@@ -1224,9 +1235,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
if self.data_manager.add_entry(entry):
|
||||
self._mark_data_modified() # Mark for auto-save
|
||||
self.ui_manager.update_status("Entry added successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
"Success", "Entry added successfully!", parent=self.root
|
||||
)
|
||||
if hasattr(self.ui_manager, "show_toast"):
|
||||
self.ui_manager.show_toast("Entry added", 1500)
|
||||
self._clear_entries()
|
||||
self.refresh_data_display()
|
||||
added_date = entry[0]
|
||||
@@ -1278,9 +1288,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
self._mark_data_modified() # Mark for auto-save
|
||||
edit_win.destroy()
|
||||
self.ui_manager.update_status("Entry deleted successfully!", "success")
|
||||
messagebox.showinfo(
|
||||
"Success", "Entry deleted successfully!", parent=self.root
|
||||
)
|
||||
if hasattr(self.ui_manager, "show_toast"):
|
||||
self.ui_manager.show_toast("Entry deleted", 1500)
|
||||
self.refresh_data_display()
|
||||
if deleted_row:
|
||||
|
||||
@@ -1308,7 +1317,11 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
def _clear_entries(self) -> None:
|
||||
"""Clear all input fields."""
|
||||
logger.debug("Clearing input fields.")
|
||||
self.date_var.set("")
|
||||
# Keep date practical: default to today's date after clear
|
||||
try:
|
||||
self.date_var.set(datetime.now().strftime("%m/%d/%Y"))
|
||||
except Exception:
|
||||
self.date_var.set("")
|
||||
for key in self.pathology_vars:
|
||||
self.pathology_vars[key].set(0)
|
||||
for key in self.medicine_vars:
|
||||
@@ -1336,6 +1349,10 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
|
||||
# Use efficient tree update to reduce flickering
|
||||
self._update_tree_efficiently(df)
|
||||
|
||||
# Reapply last sort state if any
|
||||
if hasattr(self.ui_manager, "reapply_last_sort"):
|
||||
self.ui_manager.reapply_last_sort(self.tree)
|
||||
|
||||
# Update the graph (always use unfiltered data for complete picture)
|
||||
# Graph gets preprocessed, use dedicated cached transformation
|
||||
if hasattr(self.data_manager, "get_graph_ready_data"):
|
||||
|
||||
+13
-5
@@ -192,11 +192,19 @@ class DataFilter:
|
||||
for medicine_key, should_be_taken in medicine_filters.items():
|
||||
if medicine_key in df.columns:
|
||||
col = df[medicine_key]
|
||||
# Medicine columns in tests contain empty string when not taken
|
||||
if should_be_taken:
|
||||
mask &= col.astype(str).str.len() > 0
|
||||
else:
|
||||
mask &= col.astype(str).str.len() == 0
|
||||
# Prefer numeric/boolean interpretation when possible (0/1)
|
||||
try:
|
||||
numeric = pd.to_numeric(col, errors="coerce")
|
||||
if should_be_taken:
|
||||
mask &= numeric.fillna(0) == 1
|
||||
else:
|
||||
mask &= numeric.fillna(0) == 0
|
||||
except Exception:
|
||||
# Fallback to truthy string length semantics
|
||||
if should_be_taken:
|
||||
mask &= col.astype(str).str.len() > 0
|
||||
else:
|
||||
mask &= col.astype(str).str.len() == 0
|
||||
|
||||
return df[mask]
|
||||
|
||||
|
||||
+22
-19
@@ -38,33 +38,29 @@ class SearchFilterWidget:
|
||||
self.pathology_manager = pathology_manager
|
||||
self.logger = logger
|
||||
|
||||
# Initialize visibility state
|
||||
# Visibility and UI init state
|
||||
self.is_visible = False
|
||||
|
||||
self.search_history = SearchHistory()
|
||||
self._ui_initialized = False
|
||||
self.frame: ttk.LabelFrame | None = None
|
||||
|
||||
# Debouncing mechanism to reduce filter update frequency
|
||||
self._update_timer = None
|
||||
self._debounce_delay = 300 # milliseconds
|
||||
self._update_timer: str | None = None
|
||||
self._debounce_delay = 450 # milliseconds
|
||||
|
||||
# UI state variables
|
||||
# History and UI state variables
|
||||
self.search_history = SearchHistory()
|
||||
self.search_var = tk.StringVar()
|
||||
self.start_date_var = tk.StringVar()
|
||||
self.end_date_var = tk.StringVar()
|
||||
|
||||
# Medicine filter variables
|
||||
self.medicine_vars = {}
|
||||
|
||||
# Pathology filter variables
|
||||
self.pathology_min_vars = {}
|
||||
self.pathology_max_vars = {}
|
||||
|
||||
self._setup_ui()
|
||||
self._bind_events()
|
||||
# Medicine and pathology filter variables
|
||||
self.medicine_vars: dict[str, tk.StringVar] = {}
|
||||
self.pathology_min_vars: dict[str, tk.StringVar] = {}
|
||||
self.pathology_max_vars: dict[str, tk.StringVar] = {}
|
||||
|
||||
def _setup_ui(self) -> None:
|
||||
"""Set up the search and filter UI."""
|
||||
# Main container - remove height limit to allow full horizontal stretch
|
||||
# Main container
|
||||
self.frame = ttk.LabelFrame(self.parent, text="Search & Filter", padding="5")
|
||||
|
||||
# Create main content frame without scrolling - use horizontal layout
|
||||
@@ -442,12 +438,17 @@ class SearchFilterWidget:
|
||||
|
||||
self.status_label.config(text=status_text)
|
||||
|
||||
def get_widget(self) -> ttk.LabelFrame:
|
||||
"""Get the main widget for embedding in UI."""
|
||||
def get_widget(self) -> ttk.LabelFrame | None:
|
||||
"""Get the main widget for embedding in UI (may be None until shown)."""
|
||||
return self.frame
|
||||
|
||||
def show(self) -> None:
|
||||
"""Show the search filter widget and configure the parent row."""
|
||||
if not self._ui_initialized:
|
||||
self._setup_ui()
|
||||
self._bind_events()
|
||||
self._ui_initialized = True
|
||||
assert self.frame is not None
|
||||
self.frame.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=5, pady=2)
|
||||
# Configure the parent grid row for horizontal layout (smaller minsize)
|
||||
if hasattr(self.parent, "grid_rowconfigure"):
|
||||
@@ -457,6 +458,8 @@ class SearchFilterWidget:
|
||||
|
||||
def hide(self) -> None:
|
||||
"""Hide the search filter widget and reset the parent row."""
|
||||
if not self.frame:
|
||||
return
|
||||
self.frame.grid_remove()
|
||||
# Reset the parent grid row to not allocate space when hidden
|
||||
if hasattr(self.parent, "grid_rowconfigure"):
|
||||
@@ -466,7 +469,7 @@ class SearchFilterWidget:
|
||||
|
||||
def toggle(self) -> None:
|
||||
"""Toggle visibility of the search and filter widget."""
|
||||
if self.frame.winfo_viewable():
|
||||
if self.frame and self.frame.winfo_viewable():
|
||||
self.hide()
|
||||
else:
|
||||
self.show()
|
||||
|
||||
+71
-4
@@ -392,10 +392,15 @@ class UIManager:
|
||||
|
||||
# Column sort state tracking
|
||||
self._tree_sort_directions: dict[str, bool] = {}
|
||||
self._last_sorted_column: str | None = None
|
||||
self._last_sorted_ascending: bool | None = None
|
||||
|
||||
def make_sort_callback(col_name: str):
|
||||
def _callback():
|
||||
self.sort_tree_column(tree, col_name)
|
||||
# Remember last sort state
|
||||
self._last_sorted_column = col_name
|
||||
self._last_sorted_ascending = self._tree_sort_directions.get(col_name)
|
||||
|
||||
return _callback
|
||||
|
||||
@@ -407,10 +412,12 @@ class UIManager:
|
||||
|
||||
tree.pack(side="left", fill="both", expand=True)
|
||||
|
||||
# Add scrollbar with optimized scroll handling
|
||||
scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
|
||||
tree.configure(yscrollcommand=scrollbar.set)
|
||||
scrollbar.pack(side="right", fill="y")
|
||||
# Add scrollbars with optimized scroll handling
|
||||
vscroll = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
|
||||
hscroll = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
|
||||
tree.configure(yscrollcommand=vscroll.set, xscrollcommand=hscroll.set)
|
||||
vscroll.pack(side="right", fill="y")
|
||||
hscroll.pack(side="bottom", fill="x")
|
||||
|
||||
# Optimize tree scrolling performance
|
||||
self._optimize_tree_scrolling(tree)
|
||||
@@ -452,6 +459,50 @@ class UIManager:
|
||||
# Update heading arrow (basic glyph)
|
||||
direction_glyph = "▲" if ascending else "▼"
|
||||
tree.heading(column, text=f"{column} {direction_glyph}")
|
||||
# Re-apply alternating row tags after sort
|
||||
self.normalize_tree_stripes(tree)
|
||||
|
||||
def _sort_tree_column_direction(
|
||||
self, tree: ttk.Treeview, column: str, ascending: bool
|
||||
) -> None:
|
||||
"""Sort a treeview column in a specific direction without toggling state."""
|
||||
data = []
|
||||
for item in tree.get_children(""):
|
||||
values = tree.item(item, "values")
|
||||
try:
|
||||
col_index = tree["columns"].index(column)
|
||||
except ValueError:
|
||||
continue
|
||||
data.append((values[col_index], item, values))
|
||||
|
||||
def try_cast(v: Any):
|
||||
for caster in (int, float):
|
||||
try:
|
||||
return caster(v)
|
||||
except Exception:
|
||||
continue
|
||||
return str(v)
|
||||
|
||||
data.sort(key=lambda tup: try_cast(tup[0]), reverse=not ascending)
|
||||
|
||||
for index, (_value, item, _vals) in enumerate(data):
|
||||
tree.move(item, "", index)
|
||||
|
||||
direction_glyph = "▲" if ascending else "▼"
|
||||
tree.heading(column, text=f"{column} {direction_glyph}")
|
||||
# Re-apply alternating row tags after sort
|
||||
self.normalize_tree_stripes(tree)
|
||||
|
||||
def reapply_last_sort(self, tree: ttk.Treeview) -> None:
|
||||
"""Reapply the last known sort to the tree after data refresh."""
|
||||
if not self._last_sorted_column or self._last_sorted_ascending is None:
|
||||
return
|
||||
import contextlib
|
||||
|
||||
with contextlib.suppress(Exception):
|
||||
self._sort_tree_column_direction(
|
||||
tree, self._last_sorted_column, bool(self._last_sorted_ascending)
|
||||
)
|
||||
|
||||
def diff_update_tree(self, tree: ttk.Treeview, df: pd.DataFrame) -> None:
|
||||
"""Apply minimal changes to treeview vs full rebuild.
|
||||
@@ -518,6 +569,22 @@ class UIManager:
|
||||
tag = "evenrow" if idx % 2 == 0 else "oddrow"
|
||||
tree.insert("", "end", values=list(row), tags=(tag,))
|
||||
|
||||
# Ensure alternating stripes are normalized after updates
|
||||
self.normalize_tree_stripes(tree)
|
||||
|
||||
def normalize_tree_stripes(self, tree: ttk.Treeview) -> None:
|
||||
"""Normalize alternating row tags based on current visual order.
|
||||
|
||||
Keeps even/odd striping consistent after inserts, deletes, and sorts.
|
||||
"""
|
||||
try:
|
||||
for idx, item in enumerate(tree.get_children("")):
|
||||
tag = "evenrow" if idx % 2 == 0 else "oddrow"
|
||||
tree.item(item, tags=(tag,))
|
||||
except Exception:
|
||||
# Best-effort visual enhancement; ignore errors
|
||||
pass
|
||||
|
||||
def create_graph_frame(self, parent_frame: ttk.Frame) -> ttk.LabelFrame:
|
||||
"""Create and configure the graph frame."""
|
||||
graph_frame: ttk.LabelFrame = ttk.LabelFrame(
|
||||
|
||||
Reference in New Issue
Block a user