Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 179 additions & 34 deletions src/accessiweather/ui/dialogs/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import json
import logging
import webbrowser
from contextlib import suppress
from pathlib import Path
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -99,12 +100,23 @@ def _on_ok(self, event):
class SettingsDialogSimple(wx.Dialog):
"""Comprehensive settings dialog — thin coordinator over per-tab modules."""

_TAB_DEFINITIONS = [
("general", "General"),
("display", "Display"),
("notifications", "Alerts"),
("audio", "Audio"),
("data_sources", "Data Sources"),
("ai", "AI"),
("updates", "Updates"),
("advanced", "Advanced"),
]

def __init__(self, parent, app: AccessiWeatherApp):
"""Initialize the settings dialog."""
super().__init__(
parent,
title="Settings",
size=(600, 550),
size=(760, 640),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
)
self.app = app
Expand All @@ -118,6 +130,11 @@ def __init__(self, parent, app: AccessiWeatherApp):
self._load_settings()
self._setup_accessibility()

@classmethod
def get_tab_definitions(cls) -> list[tuple[str, str]]:
"""Return notebook tab keys and visible labels in display order."""
return list(cls._TAB_DEFINITIONS)

# ------------------------------------------------------------------
# Delegation helpers for backward compatibility
# ------------------------------------------------------------------
Expand All @@ -140,6 +157,98 @@ def _build_default_source_settings_states() -> dict:
# UI creation
# ------------------------------------------------------------------

@staticmethod
def _wrap_static_text(control: wx.Window, width: int = 620) -> wx.Window:
"""Wrap static text where supported to keep copy readable."""
if hasattr(control, "Wrap"):
with suppress(Exception):
control.Wrap(width)
return control

def add_help_text(
self,
parent: wx.Window,
parent_sizer: wx.Sizer,
text: str,
*,
left: int = 10,
bottom: int = 8,
) -> wx.StaticText:
"""Add wrapped helper text to a sizer."""
control = wx.StaticText(parent, label=text)
self._wrap_static_text(control)
parent_sizer.Add(control, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, left)
return control

def add_labeled_row(
self,
parent: wx.Window,
parent_sizer: wx.Sizer,
label: str,
control: wx.Window,
*,
expand_control: bool = False,
bottom: int = 8,
) -> wx.BoxSizer:
"""Add a consistent label/control row to a sizer."""
row = wx.BoxSizer(wx.HORIZONTAL)
row.Add(
wx.StaticText(parent, label=label),
0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT,
10,
)
row.Add(control, 1 if expand_control else 0, wx.EXPAND if expand_control else 0)
parent_sizer.Add(row, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, bottom)
return row

def add_labeled_control_row(
self,
parent: wx.Window,
parent_sizer: wx.Sizer,
label: str,
control_factory,
*,
expand_control: bool = False,
bottom: int = 8,
) -> wx.Window:
"""Create the visible label before the control for wx/NVDA association stability."""
row = wx.BoxSizer(wx.HORIZONTAL)
row.Add(
wx.StaticText(parent, label=label),
0,
wx.ALIGN_CENTER_VERTICAL | wx.RIGHT,
10,
)
control = control_factory(parent)
row.Add(control, 1 if expand_control else 0, wx.EXPAND if expand_control else 0)
parent_sizer.Add(row, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, bottom)
return control

def create_section(
self,
parent: wx.Window,
parent_sizer: wx.Sizer,
title: str,
description: str | None = None,
) -> wx.BoxSizer:
"""
Create a titled settings section.

We intentionally avoid StaticBoxSizer here because screen readers can
announce the group label as part of the first interactive control in the
section. The description parameter is accepted for call-site readability,
but we do not auto-render it above the first interactive control.
"""
del description
heading = wx.StaticText(parent, label=title)
self._wrap_static_text(heading)
parent_sizer.Add(heading, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.EXPAND, 5)

section = wx.BoxSizer(wx.VERTICAL)
parent_sizer.Add(section, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 5)
return section

def _create_ui(self):
"""Create the dialog UI using per-tab modules."""
from .settings_tabs import (
Expand All @@ -154,32 +263,54 @@ def _create_ui(self):
)

main_sizer = wx.BoxSizer(wx.VERTICAL)
intro = wx.StaticText(
self,
label=(
"Review preferences by category. Changes are saved when you choose Save Settings."
),
)
self._wrap_static_text(intro)
main_sizer.Add(intro, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.EXPAND, 10)
sub_intro = wx.StaticText(
self,
label=(
"Everyday preferences appear first. Maintenance, backup, and reset "
"tools are grouped on the Advanced tab."
),
)
self._wrap_static_text(sub_intro)
main_sizer.Add(sub_intro, 0, wx.LEFT | wx.RIGHT | wx.TOP | wx.EXPAND, 10)
self.notebook = wx.Notebook(self)
tab_classes = {
"general": GeneralTab,
"display": DisplayTab,
"notifications": NotificationsTab,
"audio": AudioTab,
"data_sources": DataSourcesTab,
"ai": AITab,
"updates": UpdatesTab,
"advanced": AdvancedTab,
}

self._tab_objects = [
GeneralTab(self),
DisplayTab(self),
DataSourcesTab(self),
NotificationsTab(self),
AudioTab(self),
UpdatesTab(self),
AITab(self),
AdvancedTab(self),
]
# Keep named references for methods that need specific tabs
self._audio_tab = self._tab_objects[4]
self._display_tab = self._tab_objects[1]
self._data_sources_tab = self._tab_objects[2]
self._tab_objects = []
self._tab_objects_by_key = {}
for tab_key, page_label in self.get_tab_definitions():
tab = tab_classes[tab_key](self)
self._tab_objects.append(tab)
self._tab_objects_by_key[tab_key] = tab
tab.create(page_label=page_label)

for tab in self._tab_objects:
tab.create()
# Keep named references for methods that need specific tabs
self._audio_tab = self._tab_objects_by_key["audio"]
self._display_tab = self._tab_objects_by_key["display"]
self._data_sources_tab = self._tab_objects_by_key["data_sources"]

main_sizer.Add(self.notebook, 1, wx.EXPAND | wx.ALL, 10)

button_sizer = wx.BoxSizer(wx.HORIZONTAL)
button_sizer.AddStretchSpacer()

ok_btn = wx.Button(self, wx.ID_OK, "OK")
ok_btn = wx.Button(self, wx.ID_OK, "Save Settings")
ok_btn.Bind(wx.EVT_BUTTON, self._on_ok)
cancel_btn = wx.Button(self, wx.ID_CANCEL, "Cancel")

Expand All @@ -188,6 +319,10 @@ def _create_ui(self):
main_sizer.Add(button_sizer, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 10)

self.SetSizer(main_sizer)
self.SetMinSize((700, 580))
self.SetAffirmativeId(wx.ID_OK)
self.SetEscapeId(wx.ID_CANCEL)
ok_btn.SetDefault()

# ------------------------------------------------------------------
# Load / Save / Accessibility (delegate to tab objects)
Expand Down Expand Up @@ -333,9 +468,11 @@ def _run_event_sounds_dialog(self) -> dict[str, bool] | None:
scroll_sizer = wx.BoxSizer(wx.VERTICAL)

for section_title, description, event_keys in self._get_event_sound_sections():
section = wx.StaticBoxSizer(wx.VERTICAL, scroll, section_title)
section = wx.BoxSizer(wx.VERTICAL)
heading = wx.StaticText(scroll, label=section_title)
self._wrap_static_text(heading, width=380)
section.Add(
wx.StaticText(scroll, label=description),
heading,
0,
wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND,
5,
Expand All @@ -345,6 +482,14 @@ def _run_event_sounds_dialog(self) -> dict[str, bool] | None:
checkbox.SetValue(state_map.get(event_key, True))
dialog_controls[event_key] = checkbox
section.Add(checkbox, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM, 5)
description_text = wx.StaticText(scroll, label=description)
self._wrap_static_text(description_text, width=380)
section.Add(
description_text,
0,
wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND,
5,
)
scroll_sizer.Add(section, 0, wx.ALL | wx.EXPAND, 5)

scroll.SetSizer(scroll_sizer)
Expand Down Expand Up @@ -411,23 +556,23 @@ def _run_source_settings_dialog(self) -> dict | None:
10,
)

cc_sizer.Add(
wx.StaticText(cc_panel, label="NWS station selection strategy:"),
0,
wx.LEFT | wx.RIGHT,
10,
)
strategy_ctrl = wx.Choice(
strategy_ctrl = self.add_labeled_control_row(
cc_panel,
choices=[
"Hybrid default (recommended: fresh + major station with distance guardrail)",
"Nearest station (pure distance)",
"Major airport preferred (within radius, else nearest)",
"Freshest observation (among nearest stations)",
],
cc_sizer,
"NWS station selection strategy:",
lambda parent: wx.Choice(
parent,
choices=[
"Hybrid default (recommended: fresh + major station with distance guardrail)",
"Nearest station (pure distance)",
"Major airport preferred (within radius, else nearest)",
"Freshest observation (among nearest stations)",
],
),
expand_control=True,
bottom=10,
)
strategy_ctrl.SetSelection(state.get("station_selection_strategy", 0))
cc_sizer.Add(strategy_ctrl, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 10)

cc_panel.SetSizer(cc_sizer)
notebook.AddPage(cc_panel, "Current Conditions")
Expand Down
Loading
Loading