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 status icons #2768

Merged
merged 29 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f59387b
Update changenotes for status icons.
freakboy3742 Aug 15, 2024
fb00c0d
Add GTK dependency required for status icons.
freakboy3742 Aug 15, 2024
94b4f1b
Add public API for status icons, plus example app using that API.
freakboy3742 Aug 15, 2024
3c0cde4
Factor out some cocoa menu item creation tools.
freakboy3742 Aug 15, 2024
6bf6400
Add Cocoa implementation of status icons.
freakboy3742 Aug 15, 2024
e7122c7
Add Winforms implementation of status icons.
freakboy3742 Aug 15, 2024
0f008ec
Add GTK implementation of status icons.
freakboy3742 Aug 15, 2024
962749b
Modify API to use a single commandset for all status icon commands.
freakboy3742 Aug 16, 2024
eaf8286
Modify GTK and Winforms status icons to use single commandset.
freakboy3742 Aug 16, 2024
3f2492e
Add core API tests for status icons.
freakboy3742 Aug 16, 2024
611a7b9
Add testbed tests, plus cocoa probe handling.
freakboy3742 Aug 17, 2024
73d6f0b
Add stub implementations for iOS and Android.
freakboy3742 Aug 17, 2024
c245730
Add stub implementations for Textual and Web.
freakboy3742 Aug 17, 2024
9d19375
Add status icon testbed probe for GTK, Winforms, Android and iOS.
freakboy3742 Aug 17, 2024
011a8b8
Temporarily use the stable version of Briefcase.
freakboy3742 Aug 17, 2024
7d97ecc
Correct coverage markers for GTK library imports.
freakboy3742 Aug 17, 2024
c0e4a71
Extra coverage ignores for iOS and Android.
freakboy3742 Aug 17, 2024
2952da4
Add documentation for status icons.
freakboy3742 Aug 17, 2024
98f5197
Correct label of warning
freakboy3742 Aug 17, 2024
ff36364
Raise an error instead of just a warning when a bad command exists in…
freakboy3742 Aug 20, 2024
4c2768e
Ensure test will be skipped if on mobile.
freakboy3742 Aug 20, 2024
26e411a
Apply suggestions from code review
freakboy3742 Aug 21, 2024
3bc9eb4
Rename StatusIcon -> SimpleStatusIcon, and clean up constructor usage.
freakboy3742 Aug 21, 2024
c82cc76
Demote menu status icon properties to be internal only.
freakboy3742 Aug 22, 2024
2dd8255
Removed a stray debug statement.
freakboy3742 Aug 22, 2024
78f19d3
Merge branch 'main' into status_icons
freakboy3742 Aug 26, 2024
285484f
Use the standard commands for status icon menus.
freakboy3742 Aug 26, 2024
c13fa70
Tweaks to documentation from code review
freakboy3742 Aug 26, 2024
dc009bb
Update screenshots to reflect standard menu item text.
freakboy3742 Aug 26, 2024
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
4 changes: 2 additions & 2 deletions android/src/toga_android/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .icons import Icon
from .images import Image
from .paths import Paths
from .statusicons import MenuStatusIcon, StatusIcon, StatusIconSet
from .statusicons import MenuStatusIcon, SimpleStatusIcon, StatusIconSet
from .widgets.box import Box
from .widgets.button import Button
from .widgets.canvas import Canvas
Expand Down Expand Up @@ -54,7 +54,7 @@ def not_implemented(feature):
"Location",
# Status icons
"MenuStatusIcon",
"StatusIcon",
"SimpleStatusIcon",
"StatusIconSet",
# Widgets
# ActivityIndicator
Expand Down
6 changes: 3 additions & 3 deletions android/src/toga_android/statusicons.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import toga


class BaseStatusIcon:
class StatusIcon:
def __init__(self, interface):
self.interface = interface
self.native = None
Expand All @@ -16,11 +16,11 @@ def remove(self):
pass # pragma: no cover


class StatusIcon(BaseStatusIcon):
class SimpleStatusIcon(StatusIcon):
pass


class MenuStatusIcon(BaseStatusIcon):
class MenuStatusIcon(StatusIcon):
pass


Expand Down
4 changes: 2 additions & 2 deletions cocoa/src/toga_cocoa/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .icons import Icon
from .images import Image
from .paths import Paths
from .statusicons import MenuStatusIcon, StatusIcon, StatusIconSet
from .statusicons import MenuStatusIcon, SimpleStatusIcon, StatusIconSet

# Widgets
from .widgets.activityindicator import ActivityIndicator
Expand Down Expand Up @@ -60,7 +60,7 @@ def not_implemented(feature):
"Location",
# Status Icons
"MenuStatusIcon",
"StatusIcon",
"SimpleStatusIcon",
"StatusIconSet",
# Widgets
"ActivityIndicator",
Expand Down
10 changes: 5 additions & 5 deletions cocoa/src/toga_cocoa/statusicons.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from .libs import NSMenu, NSMenuItem, NSSquareStatusItemLength, NSStatusBar


class BaseStatusIcon:
class StatusIcon:
def __init__(self, interface):
self.interface = interface
self.native = None
Expand Down Expand Up @@ -48,7 +48,7 @@ def onPress_(self, sender) -> None:
self.interface.on_press()


class StatusIcon(BaseStatusIcon):
class SimpleStatusIcon(StatusIcon):
def __init__(self, interface):
super().__init__(interface)
self.delegate = StatusItemButtonDelegate.alloc().init()
Expand All @@ -60,7 +60,7 @@ def create(self):
self.native.button.target = self.delegate


class MenuStatusIcon(BaseStatusIcon):
class MenuStatusIcon(StatusIcon):
def create(self):
super().create()
self.create_menu()
Expand All @@ -83,15 +83,15 @@ def create(self):
cmd._impl.remove_menu_item(menu_item)

# Determine the primary status icon.
primary_group = self.interface.primary_menu_status_icon
primary_group = self.interface._primary_menu_status_icon
if primary_group is None: # pragma: no cover
# If there isn't at least one menu status icon, then there aren't any menus
# to populate. This can't happen in the testbed, so it's marked nocover.
return

# Recreate the menus for the menu status icons
group_cache = {
item: item._impl.create_menu() for item in self.interface.menu_status_icons
item: item._impl.create_menu() for item in self.interface._menu_status_icons
}
# Map the COMMANDS group to the primary status icon's menu.
group_cache[Group.COMMANDS] = primary_group._impl.native.menu
Expand Down
4 changes: 2 additions & 2 deletions core/src/toga/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .icons import Icon
from .images import Image
from .keys import Key
from .statusicons import MenuStatusIcon, StatusIcon
from .statusicons import MenuStatusIcon, SimpleStatusIcon
from .types import LatLng, Position, Size
from .widgets.activityindicator import ActivityIndicator
from .widgets.base import Widget
Expand Down Expand Up @@ -93,7 +93,7 @@ def warn(cls, platform: str, feature: str) -> None:
"Image",
# Status icons
"MenuStatusIcon",
"StatusIcon",
"SimpleStatusIcon",
# Types
"LatLng",
"Position",
Expand Down
7 changes: 6 additions & 1 deletion core/src/toga/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(
:param id: A unique identifier for the group.
"""
self._id = f"group-{_py_id(self)}" if id is None else id
self.text = text
self._text = text
self.order = order
if parent is None and section != 0:
raise ValueError("Section cannot be set without parent group")
Expand All @@ -54,6 +54,11 @@ def id(self) -> str:
"""A unique identifier for the group."""
return self._id

@property
def text(self) -> str:
"""A text label for the group."""
return self._text

@property
def parent(self) -> Group | None:
"""The parent of this group; returns ``None`` if the group is a root group."""
Expand Down
45 changes: 27 additions & 18 deletions core/src/toga/statusicons.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import sys
from abc import abstractmethod
from collections.abc import Iterator
from typing import TYPE_CHECKING, Mapping, Sequence

Expand All @@ -16,15 +17,24 @@
_py_id = id


class BaseStatusIcon:
def __init__(self, icon: IconContentT | None = None, **kwargs):
class StatusIcon:
def __init__(self, icon: IconContentT | None = None):
"""An abstract base class for all status icons."""
super().__init__(**kwargs)
self.factory = get_platform_factory()
self._impl = getattr(self.factory, self.__class__.__name__)(interface=self)

self.icon = icon

@property
@abstractmethod
def id(self) -> str:
"""A unique identifier for the status icon."""

@property
@abstractmethod
def text(self) -> str:
"""A text label for the status icon."""

@property
def icon(self) -> Icon | None:
"""The Icon to display in the status bar.
Expand All @@ -44,7 +54,7 @@ def icon(self, icon_or_name: IconContentT | None):
self._impl.set_icon(self._icon)


class StatusIcon(BaseStatusIcon):
class SimpleStatusIcon(StatusIcon):
def __init__(
self,
id: str | None = None,
Expand All @@ -71,16 +81,14 @@ def __init__(
self._text = text if text is not None else toga.App.app.formal_name

def __repr__(self):
return f"<StatusIcon {self.text!r}: {self.id}>"
return f"<SimpleStatusIcon {self.text!r}: {self.id}>"

@property
def id(self) -> str:
"""A unique identifier for the icon."""
return self._id

@property
def text(self) -> str:
"""A text label for the icon."""
return self._text

@property
Expand All @@ -93,7 +101,7 @@ def on_press(self, handler: toga.widgets.button.OnPressHandler) -> None:
self._on_press = wrapped_handler(self, handler)


class MenuStatusIcon(BaseStatusIcon, Group):
class MenuStatusIcon(Group, StatusIcon):
def __init__(
self,
id: str | None = None,
Expand All @@ -112,17 +120,18 @@ def __init__(
:param text: A text label for the status icon. Defaults to the formal name of
the app.
"""
super().__init__(
Group.__init__(
self,
id=f"menustatusitem-{_py_id(self)}" if id is None else id,
icon=icon,
text=(text if text is not None else toga.App.app.formal_name),
)
StatusIcon.__init__(self, icon=icon)

def __repr__(self):
return f"<MenuStatusIcon {self.text!r}: {self.id}>"


class StatusIconSet(Sequence[BaseStatusIcon], Mapping[str, BaseStatusIcon]):
class StatusIconSet(Sequence[StatusIcon], Mapping[str, StatusIcon]):
def __init__(self):
"""An ordered collection of status icons.

Expand All @@ -132,22 +141,22 @@ def __init__(self):
self.factory = get_platform_factory()
self._impl = self.factory.StatusIconSet(interface=self)

self.elements: dict[str, BaseStatusIcon] = {}
self.elements: dict[str, StatusIcon] = {}
self.commands = CommandSet()

@property
def menu_status_icons(self):
def _menu_status_icons(self):
"""An iterator over the menu status icons that have been registered."""
return (icon for icon in self if isinstance(icon, MenuStatusIcon))

@property
def primary_menu_status_icon(self):
def _primary_menu_status_icon(self):
"""The first menu status icon that has been registered.

Returns ``None`` if no menu status icons have been registered.
"""
try:
return next(self.menu_status_icons)
return next(self._menu_status_icons)
except StopIteration:
# No menu status icons registered.
return None
Expand Down Expand Up @@ -183,7 +192,7 @@ def _create_standard_commands(self):
)
)

def __iter__(self) -> Iterator[BaseStatusIcon]:
def __iter__(self) -> Iterator[StatusIcon]:
return iter(self.elements.values())

def __contains__(self, value: object) -> bool:
Expand All @@ -201,7 +210,7 @@ def __getitem__(self, index_or_id):
else:
return self.elements[index_or_id]

def add(self, *status_icons: BaseStatusIcon):
def add(self, *status_icons: StatusIcon):
"""Add one or more icons to the set.

:param status_icons: The icon (or icons) to add to the set.
Expand All @@ -216,7 +225,7 @@ def add(self, *status_icons: BaseStatusIcon):
if added and self.commands.on_change:
self.commands.on_change()

def remove(self, status_icon: BaseStatusIcon):
def remove(self, status_icon: StatusIcon):
"""Remove a single icon from the set.

:param status_icon: The status icon instance to remove.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
import pytest

import toga
from toga import StatusIcon
from toga import SimpleStatusIcon
from toga_dummy.utils import assert_action_not_performed, assert_action_performed


def test_create(app):
"""A group can be created with defaults."""
status_icon = StatusIcon()
status_icon = SimpleStatusIcon()

assert status_icon.id.startswith("statusicon-")
assert status_icon.icon is None
assert status_icon.text == "Test App"
assert status_icon.on_press._raw is None

assert repr(status_icon).startswith("<StatusIcon 'Test App': statusicon-")
assert repr(status_icon).startswith("<SimpleStatusIcon 'Test App': statusicon-")

# Status icon wasn't created as a result of being instantiated
assert_action_not_performed(status_icon, "create")
Expand All @@ -27,15 +27,15 @@ def test_create(app):

@pytest.mark.parametrize("construct_icon", [True, False])
def test_create_with_params(app, construct_icon):
"""A fully specified StatusIcon can be created."""
"""A fully specified SimpleStatusIcon can be created."""
if construct_icon:
icon = toga.Icon("path/to/icon")
else:
icon = "path/to/icon"

press_handler = Mock()

status_icon = StatusIcon(
status_icon = SimpleStatusIcon(
id="my-statusicon",
icon=icon,
text="My StatusIcon",
Expand All @@ -49,7 +49,7 @@ def test_create_with_params(app, construct_icon):
assert isinstance(status_icon.icon, toga.Icon)
assert status_icon.icon.path == Path("path/to/icon")

assert repr(status_icon) == "<StatusIcon 'My StatusIcon': my-statusicon>"
assert repr(status_icon) == "<SimpleStatusIcon 'My StatusIcon': my-statusicon>"

# Status icon wasn't created as a result of being instantiated
assert_action_not_performed(status_icon, "create")
Expand Down
Loading