-
-
Notifications
You must be signed in to change notification settings - Fork 687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(winforms): location support #3025
base: main
Are you sure you want to change the base?
Changes from 58 commits
d2e1e19
edc1e02
8a1fcb1
6b22256
80fd37f
2f5c2e8
2b6d427
146e9c4
2823a85
3c6a121
7de79b5
8aab6b4
4dbd6c3
1266849
9087b15
8bce630
63e1000
75c4376
a719a58
35217f0
714e7f3
7728c86
704059c
3a6b1e9
7d35190
67ad190
312e51c
c078b7e
76eb2b0
0faf901
12294c5
ace1c5f
daa168b
d43b4ad
909775d
cd0f234
9567bb5
d16f780
b6360ec
01dcc10
da71e61
bd3717a
5fadf74
4c4ca08
8a29fc7
27beaa8
dceba83
b69ac70
07e46fc
8bb86b0
0cdd98c
4885cc8
e5332b3
8fb7b8d
2f6f1da
5deb049
9ca2091
fe39dfd
66a8081
c4b1fc5
8b28b68
5bad2e2
6fcac27
cefe8eb
ad2380f
6394e7b
8bb9752
de30f0b
0714dcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Location support is now available on Windows |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import clr | ||
|
||
clr.AddReference("System.Device") |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
from __future__ import annotations | ||
|
||
from System import EventHandler | ||
from System.Device.Location import ( | ||
GeoCoordinate, | ||
GeoCoordinateWatcher, | ||
GeoPositionAccuracy, | ||
GeoPositionChangedEventArgs, | ||
GeoPositionPermission, | ||
) | ||
|
||
from toga import LatLng | ||
from toga.handlers import AsyncResult | ||
|
||
|
||
def toga_location(location: GeoCoordinate): | ||
"""Convert a GeoCoordinate into a Toga LatLng and altitude.""" | ||
|
||
if location.IsUnknown: | ||
return None | ||
|
||
return { | ||
"location": LatLng(location.Latitude, location.Longitude), | ||
"altitude": location.Altitude, | ||
} | ||
|
||
|
||
class Location: | ||
def __init__(self, interface): | ||
self.interface = interface | ||
self.watcher = GeoCoordinateWatcher(GeoPositionAccuracy.Default) | ||
self._handler = EventHandler[GeoPositionChangedEventArgs[GeoCoordinate]]( | ||
self._position_changed | ||
) | ||
self._has_background_permission = False | ||
|
||
def _position_changed( | ||
self, sender, event: GeoPositionChangedEventArgs[GeoCoordinate] | ||
): | ||
location = toga_location(event.Position.Location) | ||
if location: | ||
self.interface.on_change(**location) | ||
|
||
def has_permission(self): | ||
return self.watcher.Permission == GeoPositionPermission.Granted | ||
|
||
def has_background_permission(self): | ||
return self._has_background_permission | ||
|
||
def request_permission(self, future: AsyncResult[bool]) -> None: | ||
self.watcher.Start(False) # TODO: where can we call stop? | ||
future.set_result(self.has_permission()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this overload of Start is blocking yes, but the docs aren't super clear, and it doesn't seem possible to clear the flag on the python exe to test locally properly |
||
|
||
def request_background_permission(self, future: AsyncResult[bool]) -> None: | ||
if not self.has_permission(): | ||
raise PermissionError() | ||
future.set_result(True) | ||
self._has_background_permission = True | ||
|
||
def current_location(self, result: AsyncResult[dict]) -> None: | ||
self.watcher.Start() # ensure watcher has started | ||
loco = toga_location(self.watcher.Position.Location) | ||
result.set_result(loco["location"] if loco else None) | ||
|
||
def start_tracking(self) -> None: | ||
self.watcher.Start() # ensure watcher has started | ||
self.watcher.add_PositionChanged(self._handler) | ||
|
||
def stop_tracking(self) -> None: | ||
self.watcher.remove_PositionChanged(self._handler) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from ..app import AppProbe | ||
|
||
|
||
class HardwareProbe(AppProbe): | ||
|
||
def __init__(self, monkeypatch, app_probe): | ||
super().__init__(app_probe.app) | ||
|
||
self.monkeypatch = monkeypatch |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
from unittest.mock import Mock | ||
|
||
from pytest import xfail | ||
from System.Device.Location import ( | ||
GeoCoordinate, | ||
GeoCoordinateWatcher, | ||
GeoPositionPermission, | ||
) | ||
|
||
from toga.types import LatLng | ||
|
||
from .hardware import HardwareProbe | ||
|
||
|
||
class LocationProbe(HardwareProbe): | ||
def __init__(self, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.app.location._impl.watcher = Mock(spec=GeoCoordinateWatcher) | ||
self.reset_locations() | ||
|
||
def cleanup(self): | ||
# Delete the location service instance. This ensures that a freshly mocked | ||
# LocationManager is installed for each test. | ||
try: | ||
del self.app._location | ||
except AttributeError: | ||
pass | ||
|
||
def allow_permission(self): | ||
self.app.location._impl.watcher.Permission = GeoPositionPermission.Granted | ||
|
||
def grant_permission(self): | ||
self.app.location._impl.watcher.Permission = GeoPositionPermission.Granted | ||
|
||
def reject_permission(self): | ||
self.app.location._impl.watcher.Permission = GeoPositionPermission.Denied | ||
|
||
def add_location(self, location: LatLng, altitude, cached=False): | ||
m = Mock(spec=GeoCoordinate) | ||
m.Position = Mock() | ||
m.Position.Location = Mock() | ||
m.Position.Location.IsUnknown = False | ||
m.Position.Location.Latitude = location.lat | ||
m.Position.Location.Longitude = location.lng | ||
m.Position.Location.Altitude = altitude | ||
|
||
self._locations.append(m) | ||
self.app.location._impl.watcher.Position = m.Position | ||
|
||
def reset_locations(self): | ||
self._locations = [] | ||
|
||
def allow_background_permission(self): | ||
""" | ||
winforms doesn't distinguish between foreground and background access | ||
""" | ||
pass | ||
|
||
async def simulate_location_error(self, loco): | ||
await self.redraw("Wait for location error") | ||
|
||
xfail("Winforms's location service doesn't raise errors on failure") | ||
|
||
async def simulate_current_location(self, location): | ||
await self.redraw("Wait for current location") | ||
|
||
self.reset_locations() | ||
|
||
return await location | ||
|
||
async def simulate_location_update(self): | ||
self.app.location._impl._position_changed(None, self._locations[-1]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach we take for this kind of skip is to make the probe do the skip/xfail when an attempt is made to use the feature. In this case, we'd make
request_background_location()
raise an XFAIL - we use skip to indicate a feature hasn't been implemented yet, and XFAIL to indicate "this won't ever be implemented because the platform fundamentally doesn't support this idea".