Skip to content

Commit

Permalink
Modify GTK and Winforms status icons to use single commandset.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed Aug 16, 2024
1 parent 962749b commit eaf8286
Show file tree
Hide file tree
Showing 8 changed files with 130 additions and 57 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ jobs:
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 gir1.2-ayatanaappindicator3-0.1
gir1.2-webkit2-4.1 gir1.2-xapp-1.0
# Start Virtual X Server
echo "Start X server..."
Expand All @@ -245,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
8 changes: 4 additions & 4 deletions cocoa/src/toga_cocoa/statusicons.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ def __init__(self, interface):
self._menu_items = {}

def create(self):
# Menu status icons are the only icons that have extra construction needs.
# Clear existing menu items
for menu_item, cmd in self._menu_items.items():
cmd._impl.remove_menu_item(menu_item)
Expand All @@ -84,9 +85,7 @@ def create(self):

# Add the menu status items to the cache
group_cache = {
item: item._impl.native.menu
for item in self.interface
if isinstance(item, Group)
item: item._impl.native.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 All @@ -97,7 +96,8 @@ def create(self):
submenu = submenu_for_group(cmd.group, group_cache)
except ValueError:
print(
f"Command {cmd.text!r} does not belong to a current status icon group; ignoring"
f"Command {cmd.text!r} does not belong to "
"a current status icon group; ignoring"
)
else:
if isinstance(cmd, Separator):
Expand Down
9 changes: 7 additions & 2 deletions core/src/toga/statusicons.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,12 +127,17 @@ def __init__(self):
self.elements: dict[str, StatusIcon] = {}
self.commands = CommandSet()

@property
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):
"""The first menu status icon that has been registered."""
try:
return [icon for icon in self if isinstance(icon, Group)][0]
except IndexError:
return next(self.menu_status_icons)
except StopIteration:
# No menu status icons registered.
return None

Expand Down
3 changes: 2 additions & 1 deletion gtk/src/toga_gtk/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from .icons import Icon
from .images import Image
from .paths import Paths
from .statusicons import MenuStatusIcon, StatusIcon
from .statusicons import MenuStatusIcon, StatusIcon, StatusIconSet
from .widgets.activityindicator import ActivityIndicator
from .widgets.box import Box
from .widgets.button import Button
Expand Down Expand Up @@ -54,6 +54,7 @@ def not_implemented(feature):
# Status icons
"MenuStatusIcon",
"StatusIcon",
"StatusIconSet",
# Widgets
"ActivityIndicator",
"Box",
Expand Down
6 changes: 6 additions & 0 deletions gtk/src/toga_gtk/libs/gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@
from gi.repository import AyatanaAppIndicator3 as AppIndicator
except (ImportError, ValueError):
AppIndicator = None

try:
gi.require_version("XApp", "1.0")
from gi.repository import XApp
except (ImportError, ValueError):
XApp = None
93 changes: 60 additions & 33 deletions gtk/src/toga_gtk/statusicons.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import toga
from toga.command import Separator
from toga.command import Group, Separator

from .libs import AppIndicator, Gtk
from .libs import Gtk, XApp


class BaseStatusIcon:
Expand All @@ -14,42 +14,47 @@ def set_icon(self, icon):
path = str(
icon._impl.paths[32] if icon else toga.App.app.icon._impl.paths[32]
)
self.native.set_icon_full(path, "")
self.native.set_icon_name(path)

def create(self):
if AppIndicator is None:
if XApp is None:
raise RuntimeError(
"Unable to import AyatanaAppIndicator3. Ensure that "
"the system package providing AyatanaAppIndicator3 "
"its GTK bindings have been installed."
"Unable to import XApp. Ensure that the system package "
"providing libxapp and its GTK bindings have been installed."
)

self.native = AppIndicator.Indicator.new(
f"indicator-{id(self)}",
"",
AppIndicator.IndicatorCategory.APPLICATION_STATUS,
)
self.native.set_status(AppIndicator.IndicatorStatus.ACTIVE)
self.native = XApp.StatusIcon.new()
self.native.set_tooltip_text(self.interface.text)
self.set_icon(self.interface.icon)

def remove(self):
self.native.set_status(AppIndicator.IndicatorStatus.PASSIVE)
del self.native


class StatusIcon(BaseStatusIcon):
def create(self):
super().create()
# FIXME: Need to work out how to display an icon-only status item,
# and connect the activate event to self.interface.on_press()
self.native.connect("activate", self.gtk_activate)

def gtk_activate(self, icon, button, time):
self.interface.on_press()


class MenuStatusIcon(BaseStatusIcon):
pass


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

def _submenu(self, group, group_cache):
try:
return group_cache[group]
except KeyError:
if group.parent is None:
submenu = self.native.get_menu()
if group is None:
raise ValueError("Unknown top level item")
else:
parent_menu = self._submenu(group.parent, group_cache)

Expand All @@ -63,21 +68,43 @@ def _submenu(self, group, group_cache):
group_cache[group] = submenu
return submenu

def create_menus(self):
# Clear existing menu
submenu = Gtk.Menu.new()
self.native.set_menu(submenu)
def create(self):
# Menu status icons are the only icons that have extra construction needs.
# Clear existing menus
for item in self.interface.menu_status_icons:
submenu = Gtk.Menu.new()
item._impl.native.set_primary_menu(submenu)

# Determine the primary status icon.
primary_group = self.interface.primary_menu_status_icon
if primary_group is None:
# If there isn't at least one menu status icon, then there aren't any menus
# to populate.
return

# Add the menu status items to the cache
group_cache = {
item: item._impl.native.get_primary_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.get_primary_menu()
self._menu_items = {}

# Create a clean menubar instance.
group_cache = {}
for cmd in self.interface.commands:
submenu = self._submenu(cmd.group, group_cache)

if isinstance(cmd, Separator):
menu_item = Gtk.SeparatorMenuItem.new()
try:
submenu = self._submenu(cmd.group, group_cache)
except ValueError:
print(
f"Command {cmd.text!r} does not belong to "
"a current status icon group; ignoring"
)
else:
menu_item = Gtk.MenuItem.new_with_label(cmd.text)
menu_item.connect("activate", cmd._impl.gtk_activate)

submenu.append(menu_item)
submenu.show_all()
if isinstance(cmd, Separator):
menu_item = Gtk.SeparatorMenuItem.new()
else:
menu_item = Gtk.MenuItem.new_with_label(cmd.text)
menu_item.connect("activate", cmd._impl.gtk_activate)

submenu.append(menu_item)
submenu.show_all()
3 changes: 2 additions & 1 deletion winforms/src/toga_winforms/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from .icons import Icon
from .images import Image
from .paths import Paths
from .statusicons import MenuStatusIcon, StatusIcon
from .statusicons import MenuStatusIcon, StatusIcon, StatusIconSet
from .widgets.box import Box
from .widgets.button import Button
from .widgets.canvas import Canvas
Expand Down Expand Up @@ -51,6 +51,7 @@ def not_implemented(feature):
# Status Icons
"MenuStatusIcon",
"StatusIcon",
"StatusIconSet",
# Widgets
"Box",
"Button",
Expand Down
60 changes: 46 additions & 14 deletions winforms/src/toga_winforms/statusicons.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import System.Windows.Forms as WinForms

import toga
from toga.command import Separator
from toga.command import Group, Separator

from .libs.wrapper import WeakrefCallable

Expand All @@ -20,6 +20,7 @@ def set_icon(self, icon):
def create(self):
self.native = WinForms.NotifyIcon()
self.native.Visible = True
self.native.Text = self.interface.text
self.set_icon(self.interface.icon)

def remove(self):
Expand All @@ -37,12 +38,20 @@ def winforms_click(self, sender, event):


class MenuStatusIcon(BaseStatusIcon):
pass


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

def _submenu(self, group, group_cache):
try:
return group_cache[group]
except KeyError:
if group.parent is None:
submenu = self.native.ContextMenu
if group is None:
raise ValueError("Unknown top level item")
else:
parent_menu = self._submenu(group.parent, group_cache)

Expand All @@ -53,18 +62,41 @@ def _submenu(self, group, group_cache):
group_cache[group] = submenu
return submenu

def create_menus(self):
# Clear existing items
submenu = WinForms.ContextMenu()
self.native.ContextMenu = submenu
def create(self):
# Menu status icons are the only icons that have extra construction needs.
# Clear existing menus
for item in self.interface.menu_status_icons:
submenu = WinForms.ContextMenu()
item._impl.native.ContextMenu = submenu

# Determine the primary status icon.
primary_group = self.interface.primary_menu_status_icon
if primary_group is None:
# If there isn't at least one menu status icon, then there aren't any menus
# to populate.
return

# Add the menu status items to the cache
group_cache = {
item: item._impl.native.ContextMenu
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.ContextMenu
self._menu_items = {}

# Create a clean menubar instance.
group_cache = {}
for cmd in self.interface.commands:
submenu = self._submenu(cmd.group, group_cache)
if isinstance(cmd, Separator):
menu_item = "-"
try:
submenu = self._submenu(cmd.group, group_cache)
except ValueError:
print(
f"Command {cmd.text!r} does not belong to "
"a current status icon group; ignoring"
)
else:
menu_item = cmd._impl.create_menu_item(WinForms.MenuItem)
if isinstance(cmd, Separator):
menu_item = "-"
else:
menu_item = cmd._impl.create_menu_item(WinForms.MenuItem)

submenu.MenuItems.Add(menu_item)
submenu.MenuItems.Add(menu_item)

0 comments on commit eaf8286

Please sign in to comment.