feat: Implement search filter persistence and UI synchronization
This commit is contained in:
+65
-5
@@ -374,14 +374,31 @@ 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
|
||||
self.refresh_data_display()
|
||||
# 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
|
||||
self.ui_manager.update_status("Application ready", "info")
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user