diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d3c8be2..60a16a91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ All notable changes to this project will be documented in this file. - NOAA Weather Radio now opens independently of saved weather locations and keeps playing after you close the player until you press Stop or exit the app. - The NOAA Weather Radio Station Finder now has clearer modes for searching all stations, browsing by full state or territory name, and finding nearby stations by coordinates. - NOAA Weather Radio stations can now be saved as favorites, with a Favorites finder mode and optional nearby-station lookup from your saved AccessiWeather locations. +- Saved locations can now be reordered manually from the Location menu, and that custom order is available as a saved-location display mode. ### Fixed - Screen reader announcements work again in installed builds — the Weather Assistant now auto-reads replies, and other spoken announcements are back. The installer was shipping without the speech library's native component, so announcements were silently skipped. diff --git a/src/accessiweather/config/config_manager.py b/src/accessiweather/config/config_manager.py index 6b16f260..7fc9cf18 100644 --- a/src/accessiweather/config/config_manager.py +++ b/src/accessiweather/config/config_manager.py @@ -319,6 +319,10 @@ def remove_location(self, name: str) -> bool: """Remove a location.""" return self._locations.remove_location(name) + def reorder_locations(self, ordered_names: list[str]) -> bool: + """Persist a new saved-location order.""" + return self._locations.reorder_locations(ordered_names) + def set_current_location(self, name: str) -> bool: """Set the current location.""" return self._locations.set_current_location(name) diff --git a/src/accessiweather/config/locations.py b/src/accessiweather/config/locations.py index f1860575..2b7a8094 100644 --- a/src/accessiweather/config/locations.py +++ b/src/accessiweather/config/locations.py @@ -6,7 +6,6 @@ from math import isclose from typing import TYPE_CHECKING -from ..location_sorting import location_name_sort_key from ..models import Location if TYPE_CHECKING: @@ -85,7 +84,6 @@ def add_location( marine_mode=marine_mode, ) config.locations.append(new_location) - self._sort_locations(config) if config.current_location is None: config.current_location = new_location @@ -138,7 +136,6 @@ async def add_location_with_enrichment( self.logger.debug("Zone enrichment raised unexpectedly for %s: %s", name, exc) config.locations.append(new_location) - self._sort_locations(config) if config.current_location is None: config.current_location = new_location @@ -155,10 +152,6 @@ def _get_zone_enrichment_service(self) -> ZoneEnrichmentService: self._zone_enrichment_service = ZoneEnrichmentService() return self._zone_enrichment_service - def _sort_locations(self, config) -> None: - """Keep saved locations in alphabetical order.""" - config.locations.sort(key=location_name_sort_key) - def update_zone_metadata(self, location_name: str, fields: dict[str, str]) -> bool: """ Apply zone-metadata updates to an existing location and persist. @@ -275,9 +268,6 @@ def update_location_details( if current_matches_location: config.current_location = location - if new_name != name: - self._sort_locations(config) - self.logger.info( "Updated location details for %s%s%s", name, @@ -296,7 +286,6 @@ def remove_location(self, name: str) -> bool: for index, location in enumerate(config.locations): if location.name == name: config.locations.pop(index) - self._sort_locations(config) if config.current_location and config.current_location.name == name: config.current_location = None @@ -312,6 +301,37 @@ def remove_location(self, name: str) -> bool: self.logger.warning(f"Location {name} not found") return False + def reorder_locations(self, ordered_names: list[str]) -> bool: + """Persist a new saved-location order by location name.""" + config = self._manager.get_config() + visible_locations = [ + location for location in config.locations if location.name != "Nationwide" + ] + current_names = [location.name for location in visible_locations] + + if ordered_names == current_names: + return True + + if len(ordered_names) != len(current_names) or set(ordered_names) != set(current_names): + self.logger.warning("Rejected saved-location reorder with mismatched names") + return False + + locations_by_name = {location.name: location for location in visible_locations} + reordered_locations = [locations_by_name[name] for name in ordered_names] + hidden_locations = [ + location for location in config.locations if location.name == "Nationwide" + ] + config.locations = reordered_locations + hidden_locations + + if ( + config.current_location is not None + and config.current_location.name in locations_by_name + ): + config.current_location = locations_by_name[config.current_location.name] + + self.logger.info("Reordered saved locations: %s", ordered_names) + return self._manager.save_config() + def set_current_location(self, name: str) -> bool: """Set the current location by name.""" config = self._manager.get_config() @@ -334,14 +354,11 @@ def get_current_location(self) -> Location | None: def get_all_locations(self) -> list[Location]: """Return configured locations without synthetic nationwide entries.""" - return sorted( - [ - location - for location in self._manager.get_config().locations.copy() - if location.name != "Nationwide" - ], - key=location_name_sort_key, - ) + return [ + location + for location in self._manager.get_config().locations.copy() + if location.name != "Nationwide" + ] def get_location_names(self) -> list[str]: """Return the list of configured location names.""" diff --git a/src/accessiweather/location_sorting.py b/src/accessiweather/location_sorting.py index 94784bed..21a4bae2 100644 --- a/src/accessiweather/location_sorting.py +++ b/src/accessiweather/location_sorting.py @@ -10,11 +10,14 @@ from .models import Location LOCATION_SORT_ALPHABETICAL = "alphabetical" +LOCATION_SORT_MANUAL = "manual" LOCATION_SORT_NEAREST_CURRENT = "nearest_current" def normalize_location_sort_order(value: object) -> str: """Return a supported location sort order.""" + if value == LOCATION_SORT_MANUAL: + return LOCATION_SORT_MANUAL if value == LOCATION_SORT_NEAREST_CURRENT: return LOCATION_SORT_NEAREST_CURRENT return LOCATION_SORT_ALPHABETICAL @@ -32,8 +35,13 @@ def sort_locations_for_display( anchor: Location | None = None, ) -> list[Location]: """Sort saved locations for user-facing lists.""" - sorted_locations = sorted(locations, key=location_name_sort_key) - if normalize_location_sort_order(sort_order) != LOCATION_SORT_NEAREST_CURRENT or anchor is None: + base_locations = list(locations) + normalized_sort_order = normalize_location_sort_order(sort_order) + if normalized_sort_order == LOCATION_SORT_MANUAL: + return base_locations + + sorted_locations = sorted(base_locations, key=location_name_sort_key) + if normalized_sort_order != LOCATION_SORT_NEAREST_CURRENT or anchor is None: return sorted_locations return sorted( diff --git a/src/accessiweather/models/config_app.py b/src/accessiweather/models/config_app.py index 53ee0b70..0d4d8f5e 100644 --- a/src/accessiweather/models/config_app.py +++ b/src/accessiweather/models/config_app.py @@ -4,7 +4,6 @@ from dataclasses import dataclass -from ..location_sorting import location_name_sort_key from .config_settings import AppSettings from .weather import Location @@ -37,7 +36,7 @@ def to_dict(self) -> dict: **({"fire_zone_id": loc.fire_zone_id} if loc.fire_zone_id else {}), **({"radar_station": loc.radar_station} if loc.radar_station else {}), } - for loc in sorted(self.locations, key=location_name_sort_key) + for loc in self.locations if loc.name != _LEGACY_NATIONWIDE_LOCATION_NAME ], "current_location": { diff --git a/src/accessiweather/models/config_settings.py b/src/accessiweather/models/config_settings.py index 3419679d..859b6744 100644 --- a/src/accessiweather/models/config_settings.py +++ b/src/accessiweather/models/config_settings.py @@ -85,7 +85,7 @@ class AppSettings(AppSettingsValidationMixin, AppSettingsSerializationMixin): # bottom button panel. Takes effect on app restart. location_buttons_on_top: bool = False # Saved location ordering in user-facing lists. - location_sort_order: str = "alphabetical" # "alphabetical" | "nearest_current" + location_sort_order: str = "alphabetical" # "alphabetical" | "manual" | "nearest_current" # Date format preset for rendered dates date_format: str = "iso" # "iso" | "us_short" | "us_long" | "eu" # Taskbar icon text options diff --git a/src/accessiweather/models/config_validation.py b/src/accessiweather/models/config_validation.py index dfcfd34a..4aad30b7 100644 --- a/src/accessiweather/models/config_validation.py +++ b/src/accessiweather/models/config_validation.py @@ -222,7 +222,7 @@ def validate_on_access(self, setting_name: str) -> bool: setattr(settings, setting_name, "separate") elif setting_name == "location_sort_order": - valid_orders = {"alphabetical", "nearest_current"} + valid_orders = {"alphabetical", "manual", "nearest_current"} if value not in valid_orders: setattr(settings, setting_name, "alphabetical") diff --git a/src/accessiweather/ui/dialogs/__init__.py b/src/accessiweather/ui/dialogs/__init__.py index df5b3fb4..b487ec36 100644 --- a/src/accessiweather/ui/dialogs/__init__.py +++ b/src/accessiweather/ui/dialogs/__init__.py @@ -8,7 +8,11 @@ from .discussion_dialog import show_discussion_dialog from .explanation_dialog import show_explanation_dialog from .forecast_products_dialog import show_forecast_products_dialog -from .location_dialog import show_add_location_dialog, show_edit_location_dialog +from .location_dialog import ( + show_add_location_dialog, + show_edit_location_dialog, + show_reorder_locations_dialog, +) from .national_products_dialog import NationalProductsDialog, show_national_products_dialog from .noaa_radio_dialog import NOAARadioDialog, show_noaa_radio_dialog from .precipitation_timeline_dialog import show_precipitation_timeline_dialog @@ -22,6 +26,7 @@ __all__ = [ "show_add_location_dialog", "show_edit_location_dialog", + "show_reorder_locations_dialog", "show_advanced_text_product_dialog", "show_air_quality_dialog", "show_alert_dialog", @@ -55,6 +60,7 @@ "show_national_products_dialog": ".national_products_dialog", "show_add_location_dialog": ".location_dialog", "show_edit_location_dialog": ".location_dialog", + "show_reorder_locations_dialog": ".location_dialog", "NOAARadioDialog": ".noaa_radio_dialog", "show_noaa_radio_dialog": ".noaa_radio_dialog", "show_precipitation_timeline_dialog": ".precipitation_timeline_dialog", diff --git a/src/accessiweather/ui/dialogs/location_dialog.py b/src/accessiweather/ui/dialogs/location_dialog.py index 9b2b4306..4948d111 100644 --- a/src/accessiweather/ui/dialogs/location_dialog.py +++ b/src/accessiweather/ui/dialogs/location_dialog.py @@ -97,6 +97,34 @@ def show_edit_location_dialog( return None +def show_reorder_locations_dialog(parent, app: AccessiWeatherApp) -> list[str] | None: + """ + Show a dialog that lets users reorder saved locations manually. + + Returns the ordered location names on OK, or ``None`` if the user cancelled. + """ + try: + dlg = ReorderLocationsDialog(parent, app) + result = dlg.ShowModal() + + if result == wx.ID_OK: + ordered_names = dlg.get_result() + dlg.Destroy() + return ordered_names + + dlg.Destroy() + return None + + except Exception as e: + logger.error(f"Failed to show reorder locations dialog: {e}") + wx.MessageBox( + f"Failed to open reorder locations dialog: {e}", + "Error", + wx.OK | wx.ICON_ERROR, + ) + return None + + _ZONE_NOT_RESOLVED_MSG = "Not yet resolved - will populate after next weather refresh" @@ -472,6 +500,110 @@ def get_result(self) -> EditLocationResult: ) +class ReorderLocationsDialog(wx.Dialog): + """Dialog for choosing a custom saved-location order.""" + + def __init__(self, parent, app: AccessiWeatherApp): + """Initialize the reorder dialog and load the current saved-location order.""" + super().__init__( + parent, + title="Reorder Saved Locations", + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER, + ) + self.app = app + config_manager = getattr(app, "config_manager", None) + self._ordered_names = ( + config_manager.get_location_names() + if config_manager is not None and hasattr(config_manager, "get_location_names") + else [] + ) + + panel = wx.Panel(self) + root = wx.BoxSizer(wx.VERTICAL) + root.Add( + wx.StaticText( + panel, + label=( + "Choose the saved-location order you want. " + "Saving here switches Saved location order to Manual (custom)." + ), + ), + 0, + wx.ALL | wx.EXPAND, + 10, + ) + + content = wx.BoxSizer(wx.HORIZONTAL) + self._locations_list = wx.ListBox(panel, choices=self._ordered_names, style=wx.LB_SINGLE) + self._locations_list.SetName("Saved locations order") + content.Add(self._locations_list, 1, wx.ALL | wx.EXPAND, 10) + + button_column = wx.BoxSizer(wx.VERTICAL) + self._move_up_button = wx.Button(panel, label="Move Up") + self._move_up_button.SetName("Move selected saved location up") + self._move_up_button.Bind(wx.EVT_BUTTON, lambda event: self._move_selection(-1)) + button_column.Add(self._move_up_button, 0, wx.BOTTOM | wx.EXPAND, 8) + + self._move_down_button = wx.Button(panel, label="Move Down") + self._move_down_button.SetName("Move selected saved location down") + self._move_down_button.Bind(wx.EVT_BUTTON, lambda event: self._move_selection(1)) + button_column.Add(self._move_down_button, 0, wx.EXPAND) + content.Add(button_column, 0, wx.TOP | wx.RIGHT | wx.BOTTOM, 10) + root.Add(content, 1, wx.EXPAND) + + self._status_text = wx.StaticText( + panel, + label="Use Move Up and Move Down to arrange your saved locations.", + ) + root.Add(self._status_text, 0, wx.LEFT | wx.RIGHT | wx.BOTTOM | wx.EXPAND, 10) + + root.Add(self.CreateSeparatedButtonSizer(wx.OK | wx.CANCEL), 0, wx.ALL | wx.EXPAND, 10) + panel.SetSizer(root) + + if self._ordered_names: + self._locations_list.SetSelection(0) + self._locations_list.Bind(wx.EVT_LISTBOX, self._on_selection_changed) + self._refresh_move_buttons() + self.SetMinSize(wx.Size(420, 320)) + self.SetSize(wx.Size(520, 360)) + self.CentreOnParent() + + def _on_selection_changed(self, event: wx.CommandEvent) -> None: + self._refresh_move_buttons() + event.Skip() + + def _refresh_move_buttons(self) -> None: + selection = self._locations_list.GetSelection() + has_selection = selection != wx.NOT_FOUND + self._move_up_button.Enable(has_selection and selection > 0) + self._move_down_button.Enable( + has_selection and 0 <= selection < len(self._ordered_names) - 1 + ) + + def _move_selection(self, direction: int) -> None: + selection = self._locations_list.GetSelection() + if selection == wx.NOT_FOUND: + return + + new_index = selection + direction + if new_index < 0 or new_index >= len(self._ordered_names): + return + + self._ordered_names[selection], self._ordered_names[new_index] = ( + self._ordered_names[new_index], + self._ordered_names[selection], + ) + self._locations_list.Set(self._ordered_names) + self._locations_list.SetSelection(new_index) + moved_name = self._ordered_names[new_index] + self._status_text.SetLabel(f"{moved_name} moved to position {new_index + 1}.") + self._refresh_move_buttons() + + def get_result(self) -> list[str]: + """Return the saved-location order chosen by the user.""" + return list(self._ordered_names) + + class AddLocationDialog(wx.Dialog): """Dialog for adding a new location with search functionality.""" diff --git a/src/accessiweather/ui/dialogs/settings_tabs/display.py b/src/accessiweather/ui/dialogs/settings_tabs/display.py index 1dc611ee..d88b5e95 100644 --- a/src/accessiweather/ui/dialogs/settings_tabs/display.py +++ b/src/accessiweather/ui/dialogs/settings_tabs/display.py @@ -21,10 +21,11 @@ _ALERT_DISPLAY_MAP = {"separate": 0, "combined": 1} _ALERT_DISPLAY_VALUES = ["separate", "combined"] _ALERT_DISPLAY_CHOICES = ["Separate fields (default)", "Single combined view"] -_LOCATION_SORT_MAP = {"alphabetical": 0, "nearest_current": 1} -_LOCATION_SORT_VALUES = ["alphabetical", "nearest_current"] +_LOCATION_SORT_MAP = {"alphabetical": 0, "manual": 1, "nearest_current": 2} +_LOCATION_SORT_VALUES = ["alphabetical", "manual", "nearest_current"] _LOCATION_SORT_CHOICES = [ "Alphabetical (default)", + "Manual (custom)", "Nearest to current location", ] diff --git a/src/accessiweather/ui/main_window.py b/src/accessiweather/ui/main_window.py index f2632ebc..a05f2b2a 100644 --- a/src/accessiweather/ui/main_window.py +++ b/src/accessiweather/ui/main_window.py @@ -16,7 +16,10 @@ from ..runtime_env import is_compiled_runtime # noqa: F401 - public monkeypatch surface from ..screen_reader import ScreenReaderAnnouncer from ..user_manual import open_user_manual # noqa: F401 - public monkeypatch surface -from .dialogs.location_dialog import show_edit_location_dialog # noqa: F401 +from .dialogs.location_dialog import ( # noqa: F401 + show_edit_location_dialog, + show_reorder_locations_dialog, +) from .main_window_commands import MainWindowCommandMixin from .main_window_display import MainWindowDisplayMixin from .main_window_locations import MainWindowLocationMixin diff --git a/src/accessiweather/ui/main_window_locations.py b/src/accessiweather/ui/main_window_locations.py index 1d5e558d..4486f5c5 100644 --- a/src/accessiweather/ui/main_window_locations.py +++ b/src/accessiweather/ui/main_window_locations.py @@ -180,6 +180,38 @@ def on_remove_location(self) -> None: self._populate_locations() self.refresh_weather_async() + def on_reorder_locations(self) -> None: + """Handle manual saved-location reordering.""" + locations = self.app.config_manager.get_all_locations() + if len(locations) < 2: + from . import main_window as base_module + + base_module.wx.MessageBox( + "Add at least two saved locations before reordering them.", + "Not Enough Locations", + base_module.wx.OK | base_module.wx.ICON_INFORMATION, + ) + return + + from . import main_window as base_module + + ordered_names = base_module.show_reorder_locations_dialog(self, self.app) + if ordered_names is None: + return + + if not self.app.config_manager.reorder_locations(ordered_names): + base_module.wx.MessageBox( + "Could not save the new saved-location order.", + "Reorder Failed", + base_module.wx.OK | base_module.wx.ICON_WARNING, + ) + return + + self.app.config_manager.update_settings(location_sort_order="manual") + self._populate_locations() + if getattr(self, "_all_locations_active", False): + self._show_all_locations_summary() + def on_refresh(self) -> None: """Handle refresh button click.""" self.refresh_weather_async(force_refresh=True) diff --git a/src/accessiweather/ui/main_window_shared.py b/src/accessiweather/ui/main_window_shared.py index 78915edf..0d3680b0 100644 --- a/src/accessiweather/ui/main_window_shared.py +++ b/src/accessiweather/ui/main_window_shared.py @@ -17,7 +17,7 @@ from ..user_manual import open_user_manual from ..utils.temperature_utils import format_temperature from . import main_window_notification_events -from .dialogs.location_dialog import show_edit_location_dialog +from .dialogs.location_dialog import show_edit_location_dialog, show_reorder_locations_dialog if TYPE_CHECKING: from ..app import AccessiWeatherApp @@ -62,6 +62,7 @@ def SetLabel(self, text: str) -> None: "open_user_manual", "resolve_temperature_unit_preference", "show_edit_location_dialog", + "show_reorder_locations_dialog", "sort_locations_for_display", "wx", ] diff --git a/src/accessiweather/ui/main_window_ui.py b/src/accessiweather/ui/main_window_ui.py index 295c9cb7..18c24b78 100644 --- a/src/accessiweather/ui/main_window_ui.py +++ b/src/accessiweather/ui/main_window_ui.py @@ -255,6 +255,12 @@ def _create_menu_bar(self) -> None: remove_item = location_menu.Append( self._remove_location_id, "&Remove Location\tCtrl+D", "Remove selected location" ) + self._reorder_locations_id = wx.NewIdRef() + reorder_item = location_menu.Append( + self._reorder_locations_id, + "Re&order Locations...", + "Choose a custom order for saved locations", + ) menu_bar.Append(location_menu, "&Location") # View menu @@ -368,6 +374,7 @@ def _create_menu_bar(self) -> None: self.Bind(wx.EVT_MENU, lambda e: self.on_add_location(), add_item) self.Bind(wx.EVT_MENU, lambda e: self.on_edit_location(), edit_item) self.Bind(wx.EVT_MENU, lambda e: self.on_remove_location(), remove_item) + self.Bind(wx.EVT_MENU, lambda e: self.on_reorder_locations(), reorder_item) self.Bind(wx.EVT_MENU, lambda e: self.on_refresh(), refresh_item) self.Bind(wx.EVT_MENU, lambda e: self._on_explain_weather(), explain_item) self.Bind(wx.EVT_MENU, lambda e: self.on_view_history(), history_item) diff --git a/tests/test_all_locations_view.py b/tests/test_all_locations_view.py index 283e192c..40e2a9dc 100644 --- a/tests/test_all_locations_view.py +++ b/tests/test_all_locations_view.py @@ -178,6 +178,20 @@ def test_real_locations_can_sort_nearest_to_current_location(self): call_args = win.location_dropdown.Append.call_args[0][0] assert call_args == ["All Locations", "Denver", "Boulder", "Miami", "Anchorage"] + def test_real_locations_can_follow_manual_saved_order(self): + from accessiweather.ui.main_window import MainWindow + + win = _make_window() + locs = [_make_location("Boston"), _make_location("Austin"), _make_location("Chicago")] + win.app.config_manager.get_all_locations.return_value = locs + win.app.config_manager.get_current_location.return_value = locs[0] + win.app.config_manager.get_settings.return_value.location_sort_order = "manual" + + MainWindow._populate_locations(win) + + call_args = win.location_dropdown.Append.call_args[0][0] + assert call_args == ["All Locations", "Boston", "Austin", "Chicago"] + def test_selects_current_location_when_set(self): from accessiweather.ui.main_window import MainWindow @@ -534,6 +548,48 @@ def test_edit_location_does_nothing_on_cancel(self): win.app.config_manager.update_location_marine_mode.assert_not_called() +class TestReorderLocations: + def test_reorder_locations_requires_two_saved_locations(self): + import accessiweather.ui.main_window as mw_module + from accessiweather.ui.main_window import MainWindow + + win = _make_window() + win.app.config_manager.get_all_locations.return_value = [_make_location("Chicago")] + + mock_wx = MagicMock() + with patch.object(mw_module, "wx", mock_wx): + MainWindow.on_reorder_locations(win) + + mock_wx.MessageBox.assert_called_once() + win.app.config_manager.reorder_locations.assert_not_called() + + def test_reorder_locations_persists_manual_mode_and_refreshes_ui(self): + import accessiweather.ui.main_window as mw_module + from accessiweather.ui.main_window import MainWindow + + win = _make_window() + locations = [ + _make_location("Chicago"), + _make_location("London"), + _make_location("Philadelphia"), + ] + win.app.config_manager.get_all_locations.return_value = locations + win._show_all_locations_summary = MagicMock() + + with patch.object( + mw_module, + "show_reorder_locations_dialog", + return_value=["Philadelphia", "Chicago", "London"], + ): + MainWindow.on_reorder_locations(win) + + win.app.config_manager.reorder_locations.assert_called_once_with( + ["Philadelphia", "Chicago", "London"] + ) + win.app.config_manager.update_settings.assert_called_once_with(location_sort_order="manual") + win._populate_locations.assert_called_once() + + # --------------------------------------------------------------------------- # refresh_weather_async — All Locations mode # --------------------------------------------------------------------------- diff --git a/tests/test_config_alert_display.py b/tests/test_config_alert_display.py index c9712464..b2a042a4 100644 --- a/tests/test_config_alert_display.py +++ b/tests/test_config_alert_display.py @@ -28,12 +28,12 @@ def test_from_dict_preserves_valid_values(self) -> None: { "alert_display_style": "combined", "date_format": "us_long", - "location_sort_order": "nearest_current", + "location_sort_order": "manual", } ) assert settings.alert_display_style == "combined" assert settings.date_format == "us_long" - assert settings.location_sort_order == "nearest_current" + assert settings.location_sort_order == "manual" def test_from_dict_falls_back_on_bogus_alert_display_style(self) -> None: settings = AppSettings.from_dict({"alert_display_style": "bogus"}) @@ -65,9 +65,9 @@ def test_full_round_trip_preserves_combined_and_us_long(self) -> None: original = AppSettings( alert_display_style="combined", date_format="us_long", - location_sort_order="nearest_current", + location_sort_order="manual", ) restored = AppSettings.from_dict(original.to_dict()) assert restored.alert_display_style == "combined" assert restored.date_format == "us_long" - assert restored.location_sort_order == "nearest_current" + assert restored.location_sort_order == "manual" diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 361ddddd..bacfe94b 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -157,30 +157,50 @@ def test_add_duplicate_location_fails(self, manager): result = manager.add_location("Test", 40.0, -74.0) assert result is False - def test_locations_are_returned_alphabetically(self, manager): - """Saved locations are listed alphabetically regardless of add order.""" + def test_locations_are_returned_in_saved_order(self, manager): + """Saved locations keep their stored order.""" manager.add_location("zurich", 47.3769, 8.5417) manager.add_location("Austin", 30.2672, -97.7431) manager.add_location("boston", 42.3601, -71.0589) assert [location.name for location in manager.get_all_locations()] == [ + "zurich", "Austin", "boston", - "zurich", ] - assert manager.get_location_names() == ["Austin", "boston", "zurich"] + assert manager.get_location_names() == ["zurich", "Austin", "boston"] - def test_locations_are_saved_alphabetically(self, manager): - """Location order is persisted alphabetically for future app launches.""" + def test_locations_are_saved_in_stored_order(self, manager): + """Location order is persisted exactly as saved for future app launches.""" manager.add_location("Zurich", 47.3769, 8.5417) manager.add_location("Austin", 30.2672, -97.7431) data = json.loads(manager.config_file.read_text(encoding="utf-8")) - assert [location["name"] for location in data["locations"]] == ["Austin", "Zurich"] + assert [location["name"] for location in data["locations"]] == ["Zurich", "Austin"] + + manager2 = ConfigManager(manager.app, config_dir=manager.config_dir) + manager2.load_config() + assert manager2.get_location_names() == ["Zurich", "Austin"] + + def test_reorder_locations_persists_manual_order(self, manager): + """Saved locations can be reordered manually and stay that way after reload.""" + manager.add_location("Chicago", 41.8781, -87.6298) + manager.add_location("London", 51.5072, -0.1276) + manager.add_location("Philadelphia", 39.9526, -75.1652) + + assert manager.reorder_locations(["Philadelphia", "Chicago", "London"]) is True + assert manager.get_location_names() == ["Philadelphia", "Chicago", "London"] + + data = json.loads(manager.config_file.read_text(encoding="utf-8")) + assert [location["name"] for location in data["locations"]] == [ + "Philadelphia", + "Chicago", + "London", + ] manager2 = ConfigManager(manager.app, config_dir=manager.config_dir) manager2.load_config() - assert manager2.get_location_names() == ["Austin", "Zurich"] + assert manager2.get_location_names() == ["Philadelphia", "Chicago", "London"] def test_remove_location(self, manager): """Test removing a location.""" diff --git a/tests/test_settings_dialog_tray_text.py b/tests/test_settings_dialog_tray_text.py index ab1e01fd..bf0af813 100644 --- a/tests/test_settings_dialog_tray_text.py +++ b/tests/test_settings_dialog_tray_text.py @@ -175,6 +175,15 @@ def test_load_settings_populates_saved_location_sort_order(): dialog._load_settings() + assert dialog._controls["location_sort_order"].GetSelection() == 2 + + +def test_load_settings_populates_manual_saved_location_sort_order(): + settings = SimpleNamespace(location_sort_order="manual") + dialog = _make_dialog_for_settings(settings) + + dialog._load_settings() + assert dialog._controls["location_sort_order"].GetSelection() == 1 @@ -182,10 +191,23 @@ def test_save_settings_persists_saved_location_sort_order(): dialog = _make_dialog_for_settings(SimpleNamespace()) dialog._get_ai_model_preference = lambda: "openrouter/free" dialog.config_manager.update_settings.return_value = True - dialog._controls["location_sort_order"].SetSelection(1) + dialog._controls["location_sort_order"].SetSelection(2) success = dialog._save_settings() assert success is True kwargs = dialog.config_manager.update_settings.call_args.kwargs assert kwargs["location_sort_order"] == "nearest_current" + + +def test_save_settings_persists_manual_saved_location_sort_order(): + dialog = _make_dialog_for_settings(SimpleNamespace()) + dialog._get_ai_model_preference = lambda: "openrouter/free" + dialog.config_manager.update_settings.return_value = True + dialog._controls["location_sort_order"].SetSelection(1) + + success = dialog._save_settings() + + assert success is True + kwargs = dialog.config_manager.update_settings.call_args.kwargs + assert kwargs["location_sort_order"] == "manual"