Skip to content
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

Add hardware ABCs to encode platform implementation requrements #3023

Closed
wants to merge 2 commits into from
Closed
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
75 changes: 72 additions & 3 deletions core/src/toga/hardware/camera.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any
import abc
from collections.abc import Iterable
from typing import TYPE_CHECKING, Protocol

from toga.constants import FlashMode
from toga.handlers import AsyncResult, PermissionResult
Expand All @@ -15,8 +17,27 @@ class PhotoResult(AsyncResult):
RESULT_TYPE = "photo"


class PlatformCameraDevice(abc.ABC):
"""Interface required for a platform's camera devices."""

@abc.abstractmethod
def id(self) -> str:
"""Get the unique identifier for the device."""
...

@abc.abstractmethod
def name(self) -> str:
"""Get a human-readable name for the device."""
...

@abc.abstractmethod
def has_flash(self) -> bool:
"""Whether the device has flash."""
...


class CameraDevice:
def __init__(self, impl: Any):
def __init__(self, impl: PlatformCameraDevice):
self._impl = impl

@property
Expand Down Expand Up @@ -44,6 +65,54 @@ def __str__(self) -> str:
return self.name


class CameraInterface(Protocol):
app: App
"""The currently running application."""


class PlatformCamera(abc.ABC):
"""Interface required of each platform's ``Camera`` implementation."""

interface: CameraInterface
"""Application location interface, containing a change-handler reference."""

def __init__(self, interface: CameraInterface):
self.interface = interface

@abc.abstractmethod
def has_permission(self) -> bool:
"""Whether the application has permission to use the camera."""
...

@abc.abstractmethod
def request_permission(self, result: PermissionResult) -> None:
"""Asynchronously request permission to use the camera.

:param result: a future representing the result of the permission request."""
...

@abc.abstractmethod
def get_devices(self) -> Iterable[PlatformCameraDevice]:
"""List the available cameras."""
...

@abc.abstractmethod
def take_photo(
self, result, *, device: CameraDevice | None, flash: FlashMode
) -> None:
"""Asynchronously take a photo with the specified device and flash setting.

If permissions have not been requested, permission should be requested before
taking the photo.

:param result: A future representing the path to the file of the photograph.
:param device: A device with which to take the photo. If none is provided, the
most appropriate default should be used.
:param flash: The flash mode to use when taking the photograph.
"""
...


class Camera:
def __init__(self, app: App):
self.factory = get_platform_factory()
Expand All @@ -57,7 +126,7 @@ def app(self) -> App:

@property
def has_permission(self) -> bool:
"""Does the user have permission to use camera devices?
"""Does the app have permission to use camera devices?

If the platform requires the user to explicitly confirm permission, and
the user has not yet given permission, this will return ``False``.
Expand Down
77 changes: 76 additions & 1 deletion core/src/toga/hardware/location.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import abc
from typing import TYPE_CHECKING, Any, Protocol

import toga
Expand Down Expand Up @@ -33,11 +34,85 @@ def __call__(
"""


class LocationInterface(Protocol):
app: App
"""The running application."""

# Distinct from ``OnLocationChangeHandler`` as this matches the @property
# on ``Location``, which is the result of ``OnLocationChangeHandler`` passed
# through ``wrapped_handler``, hence missing the ``service`` argument
# In other words, ``OnLocationChangeHandler`` is what's used by applications,
# whereas ``LocationInterface.on_change`` is what the platform implementations
# interact with
def on_change(*, location: toga.LatLng, altitude: float | None, **kwargs: Any):
"""Application location change handler.

When tracking, this should be called when the location changes."""
...


class PlatformLocation(abc.ABC):
"""Interface required of each platform's ``Location`` implementation."""

native: Any
"""The platform's location provider."""

interface: LocationInterface
"""Application location interface, containing a change-handler reference."""

def __init__(self, interface: LocationInterface):
self.interface = interface

@abc.abstractmethod
def has_permission(self) -> bool:
"""Whether the application has permission to read the location."""
...

@abc.abstractmethod
def request_permission(self, result: PermissionResult) -> None:
"""Asynchronously request permission to retrieve the current location.

:param result: a future on which to set the result of the permission request."""
...

@abc.abstractmethod
def has_background_permission(self) -> bool:
"""Whether the application has permission to track the location."""
...

@abc.abstractmethod
def request_background_permission(self, result: PermissionResult) -> None:
"""Asynchronously request permission to track location changes.

:param result: a future on which to set the result of the permission request."""
...

@abc.abstractmethod
def start_tracking(self) -> None:
"""Start tracking the location.

The implementation should call :ref:`~.PlatformLocation.interface.on_change`
each time the location changes.
"""

@abc.abstractmethod
def stop_tracking(self) -> None:
"""Stop tracking the location."""
...

@abc.abstractmethod
def current_location(self, result: LocationResult) -> None:
"""Asynchronously retrieve the current location.

:param result: a future on which to set the result of the location request."""
...


class Location:
def __init__(self, app: App):
self.factory = get_platform_factory()
self._app = app
self._impl = self.factory.Location(self)
self._impl: PlatformLocation = self.factory.Location(self)

self.on_change = None

Expand Down
8 changes: 5 additions & 3 deletions dummy/src/toga_dummy/hardware/camera.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from toga.hardware.camera import PlatformCamera, PlatformCameraDevice

from ..utils import LoggedObject


class CameraDevice:
class CameraDevice(PlatformCameraDevice):
def __init__(self, id, name, has_flash):
self._id = id
self._name = name
Expand All @@ -17,12 +19,12 @@ def has_flash(self):
return self._has_flash


class Camera(LoggedObject):
class Camera(LoggedObject, PlatformCamera):
CAMERA_1 = CameraDevice(id="camera-1", name="Camera 1", has_flash=True)
CAMERA_2 = CameraDevice(id="camera-2", name="Camera 2", has_flash=False)

def __init__(self, interface):
self.interface = interface
super().__init__(interface)

# -1: permission *could* be granted, but hasn't been
# 1: permission has been granted
Expand Down
3 changes: 2 additions & 1 deletion dummy/src/toga_dummy/hardware/location.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from __future__ import annotations

from toga import LatLng
from toga.hardware.location import PlatformLocation

from ..utils import LoggedObject


class Location(LoggedObject):
class Location(LoggedObject, PlatformLocation):
def __init__(self, interface):
self.interface = interface

Expand Down
Loading