Skip to content

Commit 18051fc

Browse files
authored
Merge 3583808 into 2758453
2 parents 2758453 + 3583808 commit 18051fc

4 files changed

Lines changed: 74 additions & 32 deletions

File tree

source/globalCommands.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
import characterProcessing
4747
from baseObject import ScriptableObject
4848
import core
49-
from winAPI._powerTracking import reportCurrentBatteryStatus
49+
from winAPI._powerTracking import reportCurrentBatteryStatus, ReportContext
5050
import winVersion
5151
from base64 import b16encode
5252
import vision
@@ -2515,7 +2515,7 @@ def script_toggleAutoFocusFocusableElements(self,gesture):
25152515
gesture="kb:NVDA+shift+b"
25162516
)
25172517
def script_say_battery_status(self, gesture: inputCore.InputGesture) -> None:
2518-
reportCurrentBatteryStatus()
2518+
reportCurrentBatteryStatus(ReportContext.FETCH_STATUS)
25192519

25202520
@script(
25212521
description=_(

source/winAPI/_powerTracking.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313

1414
import ctypes
1515
from enum import (
16+
Enum,
1617
IntEnum,
1718
IntFlag,
19+
auto,
20+
unique,
1821
)
1922
from typing import (
2023
List,
@@ -124,14 +127,28 @@ def initialize():
124127
return
125128

126129

127-
def reportCurrentBatteryStatus(onlyReportIfStatusChanged: bool = False) -> None:
130+
@unique
131+
class ReportContext(Enum):
128132
"""
129-
@param onlyReportIfStatusChanged: sometimes multiple events may fire for a power status change.
130-
Set this to True to only report if the power status changes.
133+
Used to determine the order of information, based on relevance to the user,
134+
when announcing power status information
135+
"""
136+
137+
AC_STATUS_CHANGE = auto()
138+
"""e.g. a charger is connected/disconnected"""
139+
FETCH_STATUS = auto()
140+
"""e.g. when a user presses nvda+shift+b to fetch the current battery status"""
141+
142+
143+
def reportCurrentBatteryStatus(context: ReportContext) -> None:
144+
"""
145+
@param context: the context is used to order the announcement.
146+
When the context is AC_STATUS_CHANGE, this reports the current AC status first.
147+
When the context is FETCH_STATUS, this reports the remaining battery life first.
131148
"""
132149
global _powerState
133150
systemPowerStatus = _getPowerStatus()
134-
speechSequence = _getSpeechForBatteryStatus(systemPowerStatus, onlyReportIfStatusChanged, _powerState)
151+
speechSequence = _getSpeechForBatteryStatus(systemPowerStatus, context, _powerState)
135152
if speechSequence:
136153
ui.message(" ".join(speechSequence))
137154
if systemPowerStatus is not None:
@@ -149,7 +166,7 @@ def _getPowerStatus() -> Optional[SystemPowerStatus]:
149166

150167
def _getSpeechForBatteryStatus(
151168
systemPowerStatus: Optional[SystemPowerStatus],
152-
onlyReportIfStatusChanged: bool,
169+
context: ReportContext,
153170
oldPowerState: PowerState,
154171
) -> List[str]:
155172
if not systemPowerStatus or systemPowerStatus.BatteryFlag == BatteryFlag.UNKNOWN:
@@ -161,20 +178,44 @@ def _getSpeechForBatteryStatus(
161178
# and laptops with battery pack removed.
162179
return [_("No system battery")]
163180

164-
if onlyReportIfStatusChanged and systemPowerStatus.ACLineStatus == oldPowerState:
181+
if (
182+
context == ReportContext.AC_STATUS_CHANGE
183+
and systemPowerStatus.ACLineStatus == oldPowerState
184+
):
165185
# Sometimes, the power change event double fires.
166186
# The power change event also fires when the battery level decreases by 3%.
167187
return []
168188

169189
text: List[str] = []
190+
191+
if context == ReportContext.AC_STATUS_CHANGE:
192+
# When the AC status changes, users want to be alerted to the new AC status first.
193+
text.append(_getACStatusText(systemPowerStatus))
194+
text.extend(_getBatteryInformation(systemPowerStatus))
195+
elif context == ReportContext.FETCH_STATUS:
196+
# When fetching the current battery status,
197+
# users want to know the current battery status first,
198+
# rather than the AC status which should be unchanged.
199+
text.extend(_getBatteryInformation(systemPowerStatus))
200+
text.append(_getACStatusText(systemPowerStatus))
201+
else:
202+
raise NotImplementedError(f"Unexpected ReportContext: {context}")
203+
204+
return text
205+
206+
207+
def _getACStatusText(systemPowerStatus: SystemPowerStatus) -> str:
170208
# Translators: This is presented to inform the user of the current battery percentage.
171209
if systemPowerStatus.ACLineStatus & PowerState.AC_ONLINE:
172210
# Translators: Reported when the battery is plugged in, and now is charging.
173-
text.append(_("Charging battery"))
211+
return _("Charging battery")
174212
else:
175213
# Translators: Reported when the battery is no longer plugged in, and now is not charging.
176-
text.append(_("AC disconnected"))
214+
return _("AC disconnected")
215+
177216

217+
def _getBatteryInformation(systemPowerStatus: SystemPowerStatus) -> List[str]:
218+
text: List[str] = []
178219
# Translators: This is presented to inform the user of the current battery percentage.
179220
text.append(_("%d percent") % systemPowerStatus.BatteryLifePercent)
180221
SECONDS_PER_HOUR = 3600

source/winAPI/messageWindow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def handleWindowMessage(self, msg: int, wParam: int, lParam: int) -> None:
148148
"""
149149
if msg == WindowMessage.POWER_BROADCAST:
150150
if wParam == _powerTracking.PowerBroadcast.APM_POWER_STATUS_CHANGE:
151-
_powerTracking.reportCurrentBatteryStatus(onlyReportIfStatusChanged=True)
151+
_powerTracking.reportCurrentBatteryStatus(_powerTracking.ReportContext.AC_STATUS_CHANGE)
152152
elif msg == WindowMessage.DISPLAY_CHANGE:
153153
_displayTracking.reportScreenOrientationChange(lParam)
154154
elif msg == WindowMessage.WTS_SESSION_CHANGE:

tests/unit/test_winAPI/test_powerTracking.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
BATTERY_LIFE_TIME_UNKNOWN,
1212
BatteryFlag,
1313
PowerState,
14+
ReportContext,
1415
SystemPowerStatus,
1516
_getSpeechForBatteryStatus,
1617
)
@@ -23,118 +24,118 @@ def setUp(self) -> None:
2324
MagicMock(SystemPowerStatus())
2425
)
2526

26-
def test_unknownPowerStatus_failedFetch(self):
27+
def test_fetch_status_fetchFailed(self):
2728
actualSpeech = _getSpeechForBatteryStatus(
2829
systemPowerStatus=None,
29-
onlyReportIfStatusChanged=False,
30+
context=ReportContext.FETCH_STATUS,
3031
oldPowerState=PowerState.UNKNOWN,
3132
)
3233
self.assertEqual(
3334
["Unknown power status"],
3435
actualSpeech,
3536
)
3637

37-
def test_unknownPowerStatus_fetchSuccessful(self):
38+
def test_fetch_status_fetchSuccessful_unknownPowerStatus(self):
3839
self.testPowerStatus.BatteryFlag = BatteryFlag.UNKNOWN
3940
actualSpeech = _getSpeechForBatteryStatus(
4041
systemPowerStatus=self.testPowerStatus,
41-
onlyReportIfStatusChanged=False,
42+
context=ReportContext.FETCH_STATUS,
4243
oldPowerState=PowerState.UNKNOWN,
4344
)
4445
self.assertEqual(
4546
["Unknown power status"],
4647
actualSpeech,
4748
)
4849

49-
def test_noSystemBattery(self):
50+
def test_fetch_status_fetchSuccessful_noSystemBattery(self):
5051
self.testPowerStatus.BatteryFlag = 0 ^ BatteryFlag.NO_SYSTEM_BATTERY
5152
actualSpeech = _getSpeechForBatteryStatus(
5253
systemPowerStatus=self.testPowerStatus,
53-
onlyReportIfStatusChanged=False,
54+
context=ReportContext.FETCH_STATUS,
5455
oldPowerState=PowerState.UNKNOWN,
5556
)
5657
self.assertEqual(
5758
["No system battery"],
5859
actualSpeech,
5960
)
6061

61-
def test_statusUnchanged_ignore(self):
62+
def test_fetch_status_full_report(self):
6263
self.testPowerStatus.ACLineStatus = PowerState.AC_OFFLINE
6364
self.testPowerStatus.BatteryFlag = BatteryFlag.HIGH
65+
self.testPowerStatus.BatteryLifeTime = 3660
6466
actualSpeech = _getSpeechForBatteryStatus(
6567
systemPowerStatus=self.testPowerStatus,
66-
onlyReportIfStatusChanged=True,
68+
context=ReportContext.FETCH_STATUS,
6769
oldPowerState=PowerState.AC_OFFLINE,
6870
)
6971
self.assertEqual(
70-
[],
72+
['1 percent', '1 hours and 1 minutes remaining', "AC disconnected"],
7173
actualSpeech,
7274
)
7375

74-
def test_statusUnchanged_report(self):
76+
def test_ac_status_change_statusUnchanged_ignore(self):
7577
self.testPowerStatus.ACLineStatus = PowerState.AC_OFFLINE
7678
self.testPowerStatus.BatteryFlag = BatteryFlag.HIGH
77-
self.testPowerStatus.BatteryLifeTime = 3660
7879
actualSpeech = _getSpeechForBatteryStatus(
7980
systemPowerStatus=self.testPowerStatus,
80-
onlyReportIfStatusChanged=False,
81+
context=ReportContext.AC_STATUS_CHANGE,
8182
oldPowerState=PowerState.AC_OFFLINE,
8283
)
8384
self.assertEqual(
84-
["AC disconnected", '1 percent', '1 hours and 1 minutes remaining'],
85+
[],
8586
actualSpeech,
8687
)
8788

88-
def test_statusChanged_connected(self):
89+
def test_ac_status_change_statusChanged_connected(self):
8990
self.testPowerStatus.ACLineStatus = PowerState.AC_ONLINE
9091
self.testPowerStatus.BatteryFlag = BatteryFlag.HIGH
9192
self.testPowerStatus.BatteryLifeTime = 3660
9293
actualSpeech = _getSpeechForBatteryStatus(
9394
systemPowerStatus=self.testPowerStatus,
94-
onlyReportIfStatusChanged=True,
95+
context=ReportContext.AC_STATUS_CHANGE,
9596
oldPowerState=PowerState.AC_OFFLINE,
9697
)
9798
self.assertEqual(
9899
["Charging battery", '1 percent', '1 hours and 1 minutes remaining'],
99100
actualSpeech,
100101
)
101102

102-
def test_statusChanged_disconnected(self):
103+
def test_ac_status_change_statusChanged_disconnected(self):
103104
self.testPowerStatus.ACLineStatus = PowerState.AC_OFFLINE
104105
self.testPowerStatus.BatteryFlag = BatteryFlag.HIGH
105106
self.testPowerStatus.BatteryLifeTime = 3660
106107
actualSpeech = _getSpeechForBatteryStatus(
107108
systemPowerStatus=self.testPowerStatus,
108-
onlyReportIfStatusChanged=True,
109+
context=ReportContext.AC_STATUS_CHANGE,
109110
oldPowerState=PowerState.AC_ONLINE,
110111
)
111112
self.assertEqual(
112113
["AC disconnected", '1 percent', '1 hours and 1 minutes remaining'],
113114
actualSpeech,
114115
)
115116

116-
def test_batteryLifetimeUnknown(self):
117+
def test_ac_status_change_batteryLifetimeUnknown(self):
117118
self.testPowerStatus.ACLineStatus = PowerState.AC_OFFLINE
118119
self.testPowerStatus.BatteryFlag = BatteryFlag.HIGH
119120
self.testPowerStatus.BatteryLifeTime = BATTERY_LIFE_TIME_UNKNOWN
120121
actualSpeech = _getSpeechForBatteryStatus(
121122
systemPowerStatus=self.testPowerStatus,
122-
onlyReportIfStatusChanged=True,
123+
context=ReportContext.AC_STATUS_CHANGE,
123124
oldPowerState=PowerState.AC_ONLINE,
124125
)
125126
self.assertEqual(
126127
["AC disconnected", '1 percent'],
127128
actualSpeech,
128129
)
129130

130-
def test_batteryLifePercent(self):
131+
def test_ac_status_change_batteryLifePercent(self):
131132
self.testPowerStatus.ACLineStatus = PowerState.AC_OFFLINE
132133
self.testPowerStatus.BatteryFlag = BatteryFlag.HIGH
133134
self.testPowerStatus.BatteryLifePercent = 7
134135
self.testPowerStatus.BatteryLifeTime = BATTERY_LIFE_TIME_UNKNOWN
135136
actualSpeech = _getSpeechForBatteryStatus(
136137
systemPowerStatus=self.testPowerStatus,
137-
onlyReportIfStatusChanged=True,
138+
context=ReportContext.AC_STATUS_CHANGE,
138139
oldPowerState=PowerState.AC_ONLINE,
139140
)
140141
self.assertEqual(

0 commit comments

Comments
 (0)