feat: Implement search filter persistence and UI synchronization

This commit is contained in:
William Valentin
2025-08-08 11:54:43 -07:00
parent 61c8c72cf7
commit b039447a1f
3 changed files with 132 additions and 5 deletions
+64 -4
View File
@@ -374,13 +374,30 @@ class MedTrackerApp:
self.pathology_manager,
logger,
)
# Initially hidden - can be toggled with Ctrl+F
self.search_filter_visible = False
# Restore prior visibility state from preferences
self.search_filter_visible = bool(get_pref("search_panel_visible", False))
if self.search_filter_visible:
self.search_filter_widget.show()
# --- Create Status Bar ---
self.status_bar = self.ui_manager.create_status_bar(main_frame)
# Load data
# Load data, optionally restoring saved filters and syncing the UI
saved_summary = get_pref("last_filter_state", None)
has_saved_filters = bool(
isinstance(saved_summary, dict) and saved_summary.get("has_filters")
)
if has_saved_filters:
# Force one-time restoration in refresh and reflect in the UI if visible
try:
self.refresh_data_display(apply_filters=True)
if self.search_filter_visible and hasattr(
self.search_filter_widget, "sync_ui_from_filter"
):
self.search_filter_widget.sync_ui_from_filter()
except Exception:
self.refresh_data_display()
else:
self.refresh_data_display()
# Initialize status bar with ready message
@@ -1088,10 +1105,14 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
self.search_filter_widget.hide()
self.search_filter_visible = False
self.ui_manager.update_status("Search panel hidden", "info")
set_pref("search_panel_visible", False)
save_preferences()
else:
self.search_filter_widget.show()
self.search_filter_visible = True
self.ui_manager.update_status("Search panel shown", "info")
set_pref("search_panel_visible", True)
save_preferences()
def _on_filter_update(self) -> None:
"""Handle filter updates from the search widget."""
@@ -1104,6 +1125,12 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
with contextlib.suppress(Exception):
self.root.after_cancel(self._filter_debounce_id) # type: ignore[attr-defined]
# Persist filters to preferences for next run
try:
set_pref("last_filter_state", self.data_filter.get_filter_summary())
save_preferences()
except Exception:
pass
# Schedule refresh after short delay
self._filter_debounce_id = self.root.after( # type: ignore[attr-defined]
250, lambda: self.refresh_data_display(apply_filters=True)
@@ -1333,6 +1360,32 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
logger.debug("Loading data from CSV.")
try:
# One-time restoration of last filter state (best-effort)
if apply_filters and not hasattr(self, "_restored_filters_once"):
import contextlib
self._restored_filters_once = True # type: ignore[attr-defined]
summary = get_pref("last_filter_state", None)
if isinstance(summary, dict) and summary.get("has_filters"):
self.data_filter.set_search_term(summary.get("search_term", ""))
date_rng = summary.get("filters", {}).get("date_range") or {}
self.data_filter.set_date_range_filter(
date_rng.get("start") or None, date_rng.get("end") or None
)
meds = summary.get("filters", {}).get("medicines") or {}
for key in meds.get("taken", []) or []:
self.data_filter.set_medicine_filter(key, True)
for key in meds.get("not_taken", []) or []:
self.data_filter.set_medicine_filter(key, False)
paths = summary.get("filters", {}).get("pathologies") or {}
for key, _range_text in paths.items():
with contextlib.suppress(Exception):
parts = str(_range_text).split("-")
mn = parts[0].strip()
mx = parts[1].strip() if len(parts) > 1 else ""
mn_i = int(mn) if mn and mn.lower() != "any" else None
mx_i = int(mx) if mx and mx.lower() != "any" else None
self.data_filter.set_pathology_range_filter(key, mn_i, mx_i)
# Load data from the CSV file once
# Use cached graph-ready data for plotting & base data for table
df_full: pd.DataFrame = self.data_manager.load_data()
@@ -1340,7 +1393,8 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
original_df = df.copy() # Keep a copy for graph updates
# Apply filters if requested and filters are active
if apply_filters and self.data_filter.get_filter_summary()["has_filters"]:
filter_summary = self.data_filter.get_filter_summary()
if apply_filters and filter_summary["has_filters"]:
df = self.data_filter.apply_filters(df)
self.current_filtered_data = df
else:
@@ -1376,6 +1430,12 @@ Use Ctrl+S to save entries and Ctrl+Q to quit."""
else:
self.ui_manager.update_file_info(self.filename, displayed_entries)
# Update tiny filter activity hint
import contextlib
with contextlib.suppress(Exception):
self.ui_manager.set_filter_hint(bool(filter_summary["has_filters"]))
if displayed_entries == 0:
status_msg = (
"No data matches filters" if apply_filters else "No data to display"
+42
View File
@@ -442,6 +442,48 @@ class SearchFilterWidget:
"""Get the main widget for embedding in UI (may be None until shown)."""
return self.frame
def sync_ui_from_filter(self) -> None:
"""Synchronize the UI controls with the current DataFilter state.
Best-effort: silently ignores keys not present in the UI (e.g., when
managers have changed). Does not trigger an immediate callback; traces
may schedule a debounced update which is acceptable.
"""
# Search term
import contextlib
with contextlib.suppress(Exception):
self.search_var.set(self.data_filter.search_term or "")
# Date range
with contextlib.suppress(Exception):
date_filter = self.data_filter.active_filters.get("date_range", {})
self.start_date_var.set(date_filter.get("start", "") or "")
self.end_date_var.set(date_filter.get("end", "") or "")
# Medicine filters
with contextlib.suppress(Exception):
meds = self.data_filter.active_filters.get("medicines", {})
for key, var in self.medicine_vars.items():
if key in meds:
var.set("taken" if meds[key] else "not taken")
else:
var.set("any")
# Pathology ranges
with contextlib.suppress(Exception):
paths = self.data_filter.active_filters.get("pathologies", {})
for key, rng in paths.items():
if key in self.pathology_min_vars:
mn = rng.get("min")
self.pathology_min_vars[key].set("" if mn is None else str(mn))
if key in self.pathology_max_vars:
mx = rng.get("max")
self.pathology_max_vars[key].set("" if mx is None else str(mx))
# Update status text
self._update_status()
def show(self) -> None:
"""Show the search filter widget and configure the parent row."""
if not self._ui_initialized:
+25
View File
@@ -686,6 +686,19 @@ class UIManager:
# Pack after file_info so it appears to the left of it
self.last_backup_label.pack(side=tk.RIGHT)
# Tiny filter activity hint (right side, left of backup info)
self.filter_hint_label = tk.Label(
self.status_bar,
text="",
anchor=tk.E,
font=("TkDefaultFont", 9),
padx=8,
pady=2,
bg=theme_colors["bg"],
fg="#6c757d",
)
self.filter_hint_label.pack(side=tk.RIGHT)
return self.status_bar
def update_last_backup(self, when_text: str) -> None:
@@ -815,6 +828,18 @@ class UIManager:
# Non-fatal UI convenience; ignore errors
pass
def set_filter_hint(self, active: bool, text: str | None = None) -> None:
"""Show or hide a small status hint when filters are active.
Args:
active: Whether filters are currently active
text: Optional custom hint text (defaults to 'Filters active')
"""
if not self.filter_hint_label:
return
hint_text = (text or "Filters active") if active else ""
self.filter_hint_label.config(text=hint_text)
def create_edit_window(
self, values: tuple[str, ...], callbacks: dict[str, Callable]
) -> tk.Toplevel: