Skip to content

Commit

Permalink
Merge pull request #2768 from freakboy3742/status_icons
Browse files Browse the repository at this point in the history
Add status icons
  • Loading branch information
mhsmith authored Aug 27, 2024
2 parents 37525ec + dc009bb commit e757387
Show file tree
Hide file tree
Showing 55 changed files with 1,961 additions and 130 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,8 @@ jobs:
pre-command: |
sudo apt update -y
sudo apt install -y --no-install-recommends \
blackbox pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.1
blackbox pkg-config python3-dev libgirepository1.0-dev libcairo2-dev \
gir1.2-webkit2-4.1 gir1.2-xapp-1.0
# Start Virtual X Server
echo "Start X server..."
Expand All @@ -244,7 +245,8 @@ jobs:
pre-command: |
sudo apt update -y
sudo apt install -y --no-install-recommends \
mutter pkg-config python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-webkit2-4.1
mutter pkg-config python3-dev libgirepository1.0-dev libcairo2-dev \
gir1.2-webkit2-4.1 gir1.2-xapp-1.0
# Start Virtual X Server
echo "Start X server..."
Expand Down
5 changes: 5 additions & 0 deletions android/src/toga_android/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .icons import Icon
from .images import Image
from .paths import Paths
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 @@ -51,6 +52,10 @@ def not_implemented(feature):
# Hardware
"Camera",
"Location",
# Status icons
"MenuStatusIcon",
"SimpleStatusIcon",
"StatusIconSet",
# Widgets
# ActivityIndicator
"Box",
Expand Down
32 changes: 32 additions & 0 deletions android/src/toga_android/statusicons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import toga


class StatusIcon:
def __init__(self, interface):
self.interface = interface
self.native = None

def set_icon(self, icon):
pass

def create(self):
toga.NotImplementedWarning.warn("Android", "Status Icons")

def remove(self):
pass # pragma: no cover


class SimpleStatusIcon(StatusIcon):
pass


class MenuStatusIcon(StatusIcon):
pass


class StatusIconSet:
def __init__(self, interface):
self.interface = interface

def create(self):
pass
32 changes: 22 additions & 10 deletions android/tests_backend/app.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from pathlib import Path

import pytest
from android import R
from org.beeware.android import MainActivity
from pytest import xfail

from toga import Group

Expand Down Expand Up @@ -40,7 +40,7 @@ def logs_path(self):
return Path(self.get_app_context().getFilesDir().getPath()) / "log"

def assert_app_icon(self, icon):
xfail("Android apps don't have app icons at runtime")
pytest.xfail("Android apps don't have app icons at runtime")

def _menu_item(self, path):
menu = self.main_window_probe._native_menu()
Expand Down Expand Up @@ -68,7 +68,7 @@ def _activate_menu_item(self, path):
assert self.native.onOptionsItemSelected(self._menu_item(path))

def activate_menu_exit(self):
xfail("This backend doesn't have an exit command")
pytest.xfail("This backend doesn't have an exit command")

def activate_menu_about(self):
self._activate_menu_item(["About Toga Testbed"])
Expand All @@ -79,7 +79,7 @@ async def close_about_dialog(self):
await self.press_dialog_button(about_dialog, "OK")

def activate_menu_visit_homepage(self):
xfail("This backend doesn't have a visit homepage command")
pytest.xfail("This backend doesn't have a visit homepage command")

def assert_menu_item(self, path, *, enabled=True):
assert self._menu_item(path).isEnabled() == enabled
Expand All @@ -102,27 +102,39 @@ def assert_system_menus(self):
self.assert_menu_item(["About Toga Testbed"])

def activate_menu_close_window(self):
xfail("This backend doesn't have a window management menu")
pytest.xfail("This backend doesn't have a window management menu")

def activate_menu_close_all_windows(self):
xfail("This backend doesn't have a window management menu")
pytest.xfail("This backend doesn't have a window management menu")

def activate_menu_minimize(self):
xfail("This backend doesn't have a window management menu")
pytest.xfail("This backend doesn't have a window management menu")

def enter_background(self):
xfail(
pytest.xfail(
"This is possible (https://stackoverflow.com/a/7071289), but there's no "
"easy way to bring it to the foreground again"
)

def enter_foreground(self):
xfail("See enter_background")
pytest.xfail("See enter_background")

def terminate(self):
xfail("Can't simulate this action without killing the app")
pytest.xfail("Can't simulate this action without killing the app")

def rotate(self):
self.native.findViewById(
R.id.content
).getViewTreeObserver().dispatchOnGlobalLayout()

def has_status_icon(self, status_icon):
pytest.xfail("Status icons not implemented on Android")

def status_menu_items(self, status_icon):
pytest.xfail("Status icons not implemented on Android")

def activate_status_icon_button(self, item_id):
pytest.xfail("Status icons not implemented on Android")

def activate_status_menu_item(self, item_id, title):
pytest.xfail("Status icons not implemented on Android")
File renamed without changes.
1 change: 1 addition & 0 deletions changes/97.feature.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apps can now add items to the system tray.
82 changes: 15 additions & 67 deletions cocoa/src/toga_cocoa/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from toga.command import Command, Group, Separator
from toga.handlers import NativeHandler

from .keys import cocoa_key
from .command import Command as CommandImpl, submenu_for_group
from .libs import (
NSAboutPanelOptionApplicationIcon,
NSAboutPanelOptionApplicationName,
Expand Down Expand Up @@ -63,12 +63,12 @@ def application_openFiles_(self, app, filenames) -> None:

@objc_method
def selectMenuItem_(self, sender) -> None:
cmd = self.impl._menu_items[sender]
cmd = CommandImpl.for_menu_item(sender)
cmd.action()

@objc_method
def validateMenuItem_(self, sender) -> bool:
cmd = self.impl._menu_items[sender]
cmd = CommandImpl.for_menu_item(sender)
return cmd.enabled


Expand Down Expand Up @@ -104,8 +104,7 @@ def __init__(self, interface):
self.appDelegate.native = self.native
self.native.setDelegate(self.appDelegate)

# Create the lookup table for menu items
self._menu_groups = {}
# Create the lookup table for commands and menu items
self._menu_items = {}

# Call user code to populate the main window
Expand Down Expand Up @@ -250,81 +249,30 @@ def create_standard_commands(self):
),
)

def _submenu(self, group, menubar):
"""Obtain the submenu representing the command group.
This will create the submenu if it doesn't exist. It will call itself
recursively to build the full path to menus inside submenus, returning the
"leaf" node in the submenu path. Once created, it caches the menu that has been
created for future lookup.
"""
try:
return self._menu_groups[group]
except KeyError:
if group is None:
submenu = menubar
else:
parent_menu = self._submenu(group.parent, menubar)

menu_item = parent_menu.addItemWithTitle(
group.text, action=None, keyEquivalent=""
)
submenu = NSMenu.alloc().initWithTitle(group.text)
parent_menu.setSubmenu(submenu, forItem=menu_item)

# Install the item in the group cache.
self._menu_groups[group] = submenu
return submenu

def create_menus(self):
# Recreate the menu.
# Remove any native references to the existing menu
for menu_item, cmd in self._menu_items.items():
cmd._impl.native.remove(menu_item)
cmd._impl.remove_menu_item(menu_item)

# Create a clean menubar instance.
menubar = NSMenu.alloc().initWithTitle("MainMenu")
submenu = None
self._menu_groups = {}

# Warm the menu group cache with the root menubar
group_cache = {None: menubar}
self._menu_items = {}

for cmd in self.interface.commands:
submenu = self._submenu(cmd.group, menubar)
submenu = submenu_for_group(cmd.group, group_cache)

if isinstance(cmd, Separator):
submenu.addItem(NSMenuItem.separatorItem())
menu_item = NSMenuItem.separatorItem()
else:
if cmd.shortcut:
key, modifier = cocoa_key(cmd.shortcut)
else:
key = ""
modifier = None

# Native handlers can be invoked directly as menu actions.
# Standard wrapped menu items have a `_raw` attribute,
# and are invoked using the selectMenuItem:
if hasattr(cmd.action, "_raw"):
action = SEL("selectMenuItem:")
else:
action = cmd.action

item = NSMenuItem.alloc().initWithTitle(
cmd.text,
action=action,
keyEquivalent=key,
)

if modifier is not None:
item.keyEquivalentModifierMask = modifier

# Explicit set the initial enabled/disabled state on the menu item
item.setEnabled(cmd.enabled)

# Associated the MenuItem with the command, so that future
# changes to enabled etc are reflected.
cmd._impl.native.add(item)

self._menu_items[item] = cmd
submenu.addItem(item)
menu_item = cmd._impl.create_menu_item()
self._menu_items[menu_item] = cmd

submenu.addItem(menu_item)

# Set the menu for the app.
self.native.mainMenu = menubar
Expand Down
86 changes: 85 additions & 1 deletion cocoa/src/toga_cocoa/command.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,58 @@
import sys

from rubicon.objc import SEL

from toga import Command as StandardCommand, Group, Key
from toga_cocoa.libs import NSMenuItem
from toga_cocoa.keys import cocoa_key
from toga_cocoa.libs import NSMenu, NSMenuItem


def submenu_for_group(group, group_cache):
"""Obtain the submenu representing the command group.
This will create the submenu if it doesn't exist. It will call itself recursively to
build the full path to menus inside submenus, returning the "leaf" node in the
submenu path. Once created, it caches the menu that has been created for future
lookup.
This method assumes that the top-level item (for group=None) exists in the
group_cache. If it doesn't, a ValueError is raised when the top level group is
requested.
:param group: The group to turn into a submenu.
:param group_cache: The cache of existing groups.
:raises ValueError: If the top level group cannot be found in the group cache.
"""
try:
return group_cache[group]
except KeyError:
if group is None:
raise ValueError("Cannot find top level group")
else:
parent_menu = submenu_for_group(group.parent, group_cache)

menu_item = parent_menu.addItemWithTitle(
group.text, action=None, keyEquivalent=""
)
submenu = NSMenu.alloc().initWithTitle(group.text)
parent_menu.setSubmenu(submenu, forItem=menu_item)

# Install the item in the group cache.
group_cache[group] = submenu
return submenu


class Command:
menu_items = {}

def __init__(self, interface):
self.interface = interface
self.native = set()

@classmethod
def for_menu_item(cls, item):
return cls.menu_items[item]

@classmethod
def standard(cls, app, id):
# ---- App menu -----------------------------------
Expand Down Expand Up @@ -94,3 +138,43 @@ def set_enabled(self, value):
# Otherwise, assume the native object has
# and explicit enabled property
item.setEnabled(value)

def create_menu_item(self):
if self.interface.shortcut:
key, modifier = cocoa_key(self.interface.shortcut)
else:
key = ""
modifier = None

# Native handlers can be invoked directly as menu actions.
# Standard wrapped menu items have a `_raw` attribute,
# and are invoked using the selectMenuItem:
if hasattr(self.interface.action, "_raw"):
action = SEL("selectMenuItem:")
else:
action = self.interface.action

item = NSMenuItem.alloc().initWithTitle(
self.interface.text,
action=action,
keyEquivalent=key,
)

if modifier is not None:
item.keyEquivalentModifierMask = modifier

# Explicit set the initial enabled/disabled state on the menu item
item.setEnabled(self.interface.enabled)

# Add the NSMenuItem instance as a native representation of this command.
self.native.add(item)

# Add the menu item instance to the instance map.
self.menu_items[item] = self.interface

return item

def remove_menu_item(self, menu_item):
menu_item.menu.removeItem(menu_item)
self.native.remove(menu_item)
self.menu_items.pop(menu_item)
Loading

0 comments on commit e757387

Please sign in to comment.