Skip to content
Open
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Added
- New keyboard shortcuts now let you edit the selected location, show or hide the main window, and read the current notification-area text without using the tray menu.

### Changed
- Forecaster Notes can now show official NWS Surf Zone Forecasts for regional beaches where available, with surf/beach condition alternatives from supported global sources when an official SRF product is not available.
- AccessiWeather can now automatically tune NOAA Weather Radio for qualifying alerts that use Specific Area Message Encoding (SAME) when you turn on the opt-in setting on the Alerts tab, then stop after the duration you choose when a reliable station match is available.
Expand Down
4 changes: 4 additions & 0 deletions docs/user_manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -1058,12 +1058,16 @@ These shortcuts are available in the current app:

- F5 or Ctrl+R: Refresh weather
- Ctrl+L: Add location
- Ctrl+Shift+L: Edit the selected location
- Ctrl+D: Remove location
- Ctrl+S: Open Settings
- Ctrl+H: Open Weather History
- Ctrl+E: Explain Weather
- Ctrl+T: Open Weather Assistant
- Ctrl+Shift+R: Open NOAA Weather Radio
- Ctrl+Shift+W: Show and focus the main window
- Ctrl+Shift+H: Hide the main window to the notification area
- Ctrl+Shift+I: Read the current notification-area text
- Ctrl+Q: Quit

## 10. Troubleshooting
Expand Down
42 changes: 42 additions & 0 deletions src/accessiweather/app_shortcuts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def _setup_accelerators(self) -> None:
accelerators = [
(wx.ACCEL_CTRL, ord("R"), self._on_refresh_shortcut),
(wx.ACCEL_CTRL, ord("L"), self._on_add_location_shortcut),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord("L"), self._on_edit_location_shortcut),
(wx.ACCEL_CTRL, ord("D"), self._on_remove_location_shortcut),
(wx.ACCEL_CTRL, ord("H"), self._on_history_shortcut),
(wx.ACCEL_CTRL, ord("1"), self._on_focus_current_conditions_shortcut),
Expand All @@ -29,6 +30,9 @@ def _setup_accelerators(self) -> None:
(wx.ACCEL_CTRL, ord("4"), self._on_focus_alerts_shortcut),
(wx.ACCEL_CTRL, ord("5"), self._on_focus_event_center_shortcut),
(wx.ACCEL_CTRL, ord("S"), self._on_settings_shortcut),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord("W"), self._on_show_window_shortcut),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord("H"), self._on_hide_window_shortcut),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, ord("I"), self._on_read_tray_info_shortcut),
(wx.ACCEL_CTRL, ord("Q"), self._on_exit_shortcut),
(wx.ACCEL_NORMAL, wx.WXK_F5, self._on_refresh_shortcut),
(wx.ACCEL_NORMAL, getattr(wx, "WXK_F6", wx.WXK_F5), self._on_cycle_sections_shortcut),
Expand Down Expand Up @@ -56,6 +60,11 @@ def _on_add_location_shortcut(self, event) -> None:
if self.main_window:
self.main_window.on_add_location()

def _on_edit_location_shortcut(self, event) -> None:
"""Handle Ctrl+Shift+L shortcut."""
if self.main_window:
self.main_window.on_edit_location()

def _on_remove_location_shortcut(self, event) -> None:
"""Handle Ctrl+D shortcut."""
if self.main_window:
Expand Down Expand Up @@ -101,6 +110,39 @@ def _on_settings_shortcut(self, event) -> None:
if self.main_window:
self.main_window.on_settings()

def _on_show_window_shortcut(self, event) -> None:
"""Handle Ctrl+Shift+W shortcut."""
tray_icon = getattr(self, "tray_icon", None)
if tray_icon and hasattr(tray_icon, "show_main_window"):
tray_icon.show_main_window()
return
if self.main_window:
self.main_window.Show(True)
self.main_window.Iconize(False)
self.main_window.Raise()
self.main_window.SetFocus()

def _on_hide_window_shortcut(self, event) -> None:
"""Handle Ctrl+Shift+H shortcut."""
if self.main_window and getattr(self, "tray_icon", None):
self.main_window._minimize_to_tray()
return
self._announce_shortcut_status("System tray is unavailable.")

def _on_read_tray_info_shortcut(self, event) -> None:
"""Handle Ctrl+Shift+I shortcut."""
tray_icon = getattr(self, "tray_icon", None)
if tray_icon and hasattr(tray_icon, "get_tooltip_text"):
tooltip = tray_icon.get_tooltip_text()
self._announce_shortcut_status(f"Tray information: {tooltip}")
return
self._announce_shortcut_status("System tray information is unavailable.")

def _announce_shortcut_status(self, message: str) -> None:
"""Announce shortcut feedback via the main window status channel."""
if self.main_window and hasattr(self.main_window, "set_status"):
self.main_window.set_status(message)

def _on_exit_shortcut(self, event) -> None:
"""Handle Ctrl+Q shortcut."""
self.request_exit()
32 changes: 31 additions & 1 deletion src/accessiweather/ui/main_window_ui.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ def _create_menu_bar(self) -> None:
self._edit_location_id = wx.NewIdRef()
edit_item = location_menu.Append(
self._edit_location_id,
"&Edit Location...",
"&Edit Location...\tCtrl+Shift+L",
"Edit the selected location (e.g. enable Marine Mode)",
)
self._remove_location_id = wx.NewIdRef()
Expand All @@ -270,6 +270,25 @@ def _create_menu_bar(self) -> None:
history_item = view_menu.Append(
self._history_id, "Weather &History\tCtrl+H", "View weather history"
)
self._show_window_id = wx.NewIdRef()
show_window_item = view_menu.Append(
self._show_window_id,
"Sho&w Window\tCtrl+Shift+W",
"Restore and focus the main window",
)
self._hide_window_id = wx.NewIdRef()
hide_window_item = view_menu.Append(
self._hide_window_id,
"&Hide to Notification Area\tCtrl+Shift+H",
"Hide the main window to the system tray",
)
self._read_tray_info_id = wx.NewIdRef()
read_tray_info_item = view_menu.Append(
self._read_tray_info_id,
"Read Notification Area &Text\tCtrl+Shift+I",
"Announce the current system tray information",
)
view_menu.AppendSeparator()
self._precipitation_timeline_id = wx.NewIdRef()
self._precipitation_timeline_item = view_menu.Append(
self._precipitation_timeline_id,
Expand Down Expand Up @@ -371,6 +390,13 @@ def _create_menu_bar(self) -> None:
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)
self.Bind(wx.EVT_MENU, lambda e: self.app._on_show_window_shortcut(e), show_window_item)
self.Bind(wx.EVT_MENU, lambda e: self.app._on_hide_window_shortcut(e), hide_window_item)
self.Bind(
wx.EVT_MENU,
lambda e: self.app._on_read_tray_info_shortcut(e),
read_tray_info_item,
)
self.Bind(
wx.EVT_MENU,
lambda e: self._on_precipitation_timeline(),
Expand Down Expand Up @@ -430,9 +456,13 @@ def _setup_escape_accelerator(self) -> None:
(wx.ACCEL_CTRL, "S", "_settings_id"),
(wx.ACCEL_CTRL, "Q", "_exit_id"),
(wx.ACCEL_CTRL, "L", "_add_location_id"),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, "L", "_edit_location_id"),
(wx.ACCEL_CTRL, "D", "_remove_location_id"),
(wx.ACCEL_CTRL, "E", "_explain_id"),
(wx.ACCEL_CTRL, "H", "_history_id"),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, "W", "_show_window_id"),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, "H", "_hide_window_id"),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, "I", "_read_tray_info_id"),
(wx.ACCEL_CTRL | wx.ACCEL_SHIFT, "R", "_noaa_radio_id"),
(wx.ACCEL_CTRL, "T", "_weather_chat_id"),
]
Expand Down
13 changes: 10 additions & 3 deletions src/accessiweather/ui/system_tray.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..app import AccessiWeatherApp

logger = logging.getLogger(__name__)
DEFAULT_TRAY_TOOLTIP_TEXT = "AccessiWeather"


class SystemTrayIcon(wx.adv.TaskBarIcon):
Expand All @@ -46,6 +47,7 @@ def __init__(self, app: AccessiWeatherApp):
self.app = app
self._icon_set = False
self._cached_icon = None # Cache the icon to avoid reloading
self._tooltip_text = DEFAULT_TRAY_TOOLTIP_TEXT

# Set up the tray icon
self._setup_icon()
Expand All @@ -60,7 +62,7 @@ def _setup_icon(self) -> None:
icon = self._load_icon()
if icon and icon.IsOk():
self._cached_icon = icon # Cache the icon
self.SetIcon(icon, "AccessiWeather")
self.SetIcon(icon, DEFAULT_TRAY_TOOLTIP_TEXT)
self._icon_set = True
logger.debug("System tray icon set successfully")
else:
Expand Down Expand Up @@ -295,6 +297,11 @@ def update_tooltip(self, text: str) -> None:
text: The new tooltip text

"""
self._tooltip_text = text or DEFAULT_TRAY_TOOLTIP_TEXT
if self._icon_set and self._cached_icon and self._cached_icon.IsOk():
self.SetIcon(self._cached_icon, text)
logger.debug(f"Tray tooltip updated: {text}")
self.SetIcon(self._cached_icon, self._tooltip_text)
logger.debug(f"Tray tooltip updated: {self._tooltip_text}")

def get_tooltip_text(self) -> str:
"""Return the most recent tray tooltip text for announcements and tests."""
return self._tooltip_text
82 changes: 77 additions & 5 deletions tests/gui/test_main_window_minimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,17 +385,40 @@ def test_app_accelerator_table_preserves_alt_f4_as_native_close(self):
frame.Close.assert_called_once_with()
app.request_exit.assert_not_called()

def test_app_accelerator_table_registers_issue_683_shortcuts(self):
fake_wx = _FakeWxForAccelerators()
frame = _AcceleratorFrame()
app = AppShortcutsMixin()
app.main_window = frame
app.request_exit = MagicMock()

with (
patch("accessiweather.app_shortcuts.wx", fake_wx),
patch("accessiweather.native_shortcuts.wx", fake_wx),
):
app._setup_accelerators()

combos = {(entry.flags, entry.key) for entry in frame.accelerator_table}
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("L")) in combos
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("W")) in combos
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("H")) in combos
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("I")) in combos

def test_main_window_accelerator_table_keeps_alt_f4_after_title_change(self):
fake_wx = _FakeWxForAccelerators()
window = _AcceleratorFrame()
window._settings_id = 1
window._exit_id = 2
window._add_location_id = 3
window._remove_location_id = 4
window._explain_id = 5
window._history_id = 6
window._noaa_radio_id = 7
window._weather_chat_id = 8
window._edit_location_id = 4
window._remove_location_id = 5
window._explain_id = 6
window._history_id = 7
window._show_window_id = 8
window._hide_window_id = 9
window._read_tray_info_id = 10
window._noaa_radio_id = 11
window._weather_chat_id = 12
window._on_escape_pressed = MagicMock()

with (
Expand All @@ -414,3 +437,52 @@ def test_main_window_accelerator_table_keeps_alt_f4_after_title_change(self):

window.SetTitle.assert_called_once_with("AccessiWeather \u2014 Boston")
window.Close.assert_called_once_with()

def test_main_window_accelerator_table_re_registers_issue_683_shortcuts(self):
fake_wx = _FakeWxForAccelerators()
window = _AcceleratorFrame()
window._settings_id = 1
window._exit_id = 2
window._add_location_id = 3
window._edit_location_id = 4
window._remove_location_id = 5
window._explain_id = 6
window._history_id = 7
window._show_window_id = 8
window._hide_window_id = 9
window._read_tray_info_id = 10
window._noaa_radio_id = 11
window._weather_chat_id = 12
window._on_escape_pressed = MagicMock()

with (
patch("accessiweather.ui.main_window_ui.wx", fake_wx),
patch("accessiweather.native_shortcuts.wx", fake_wx),
):
MainWindowUIMixin._setup_escape_accelerator(window)

combos = {(entry.flags, entry.key) for entry in window.accelerator_table}
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("L")) in combos
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("W")) in combos
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("H")) in combos
assert (fake_wx.ACCEL_CTRL | fake_wx.ACCEL_SHIFT, ord("I")) in combos


class TestIssue683ShortcutHandlers:
def test_read_tray_info_shortcut_announces_cached_tray_text(self):
app = AppShortcutsMixin()
app.main_window = MagicMock()
app.tray_icon = SimpleNamespace(get_tooltip_text=MagicMock(return_value="72°F Sunny"))

app._on_read_tray_info_shortcut(MagicMock())

app.main_window.set_status.assert_called_once_with("Tray information: 72°F Sunny")

def test_hide_window_shortcut_uses_existing_minimize_to_tray_path(self):
app = AppShortcutsMixin()
app.main_window = MagicMock()
app.tray_icon = object()

app._on_hide_window_shortcut(MagicMock())

app.main_window._minimize_to_tray.assert_called_once_with()
22 changes: 22 additions & 0 deletions tests/test_system_tray.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,28 @@ def update_tooltip(icon_set, cached_icon, text):
assert result is False
assert set_icon_called == []

def test_get_tooltip_text_defaults_to_app_name(self):
"""The tray should keep a readable default tooltip before weather updates arrive."""
from accessiweather.ui.system_tray import DEFAULT_TRAY_TOOLTIP_TEXT, SystemTrayIcon

tray = SystemTrayIcon.__new__(SystemTrayIcon)
tray._tooltip_text = DEFAULT_TRAY_TOOLTIP_TEXT

assert tray.get_tooltip_text() == DEFAULT_TRAY_TOOLTIP_TEXT

def test_update_tooltip_caches_text_even_without_icon(self):
"""Reading tray info should still return the newest text when icon updates are skipped."""
from accessiweather.ui.system_tray import SystemTrayIcon

tray = SystemTrayIcon.__new__(SystemTrayIcon)
tray._icon_set = False
tray._cached_icon = None
tray._tooltip_text = "AccessiWeather"

tray.update_tooltip("72°F Sunny")

assert tray.get_tooltip_text() == "72°F Sunny"


class TestMinimizeOnStartup:
"""Tests for minimize on startup functionality."""
Expand Down
Loading