Repository: qtile/qtile

Issue:
Discord (and some other programs) doesn't show in StatusNotifier widget.
### The issue:

I'm using Discord and Ferdium from flatpak. After the update, I lost the ability to see icons in the tray provided by `StatusNotifier`. I've tried both master's and qtile-extras' version of the widget.

    ~ ❯ qtile --version
    0.22.2.dev174+ge88537fb

Messages in the logs:

    2023-04-05 11:51:05,117 WARNING libqtile statusnotifier.py:start():L137 Unable to find StatusNotifierItem interface on :1.270
    2023-04-05 11:51:05,117 WARNING libqtile statusnotifier.py:start():L137 Unable to find StatusNotifierItem interface on org.kde.StatusNotifierItem-6-1

It works with previous versions (I can provide flatpak's commits, if necessary). Everything works fine on X with `Systray()` widget.

I've tested it with Waybar and it all works, in both old and new version. Other apps (Slack, Nextcloud client - also from flatpak) are working fine.

### Required:

- [X] I have searched past issues to see if this bug has already been reported.


Discussion:
This is a very odd issue. For some reason, the Discord service on dbus is not allowing introspection so our code cannot see what methods are available.

Trying to access the discord service gives me this:
```
╰─❯ busctl --user introspect org.kde.StatusNotifierItem-1863-1  /com/canonical/dbusmenu com.canonical.dbusmenu
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
```
Compare that to the VLC service:
```
╰─❯ busctl --user introspect :1.469  /MenuBar com.canonical.dbusmenu
NAME                     TYPE      SIGNATURE       RESULT/VALUE FLAGS
.AboutToShow             method    i               b            -
.AboutToShowGroup        method    ai              aiai         -
.Event                   method    isvu            -            -
.EventGroup              method    a(isvu)         ai           -
.GetGroupProperties      method    aias            a(ia{sv})    -
.GetLayout               method    iias            u(ia{sv}av)  -
.GetProperty             method    is              v            -
.IconThemePath           property  as              -            emits-change
.Status                  property  s               "normal"     emits-change
.TextDirection           property  s               "ltr"        emits-change
.Version                 property  u               4            emits-change
.ItemActivationRequested signal    iu              -            -
.ItemsPropertiesUpdated  signal    a(ia{sv})a(ias) -            -
.LayoutUpdated           signal    ui              -            -
```

Waybar seems to take a slightly different approach and just requests all properties. That's not the approach our code takes and it wouldn't be a small change to adopt that route. 

This may take me a bit of time to solve...
Looking through the Waybar code, they don't try to introspect objects. They assume there's a consistent approach to the object name and the type of object exposed. They also assume the type of object is consistent and use a pre-defined introspection definition.

We/I didn't take that approach because there are differences between implementations. Waybar acknowledges the differences but decides they don't matter (which is probably reasonable) and just removes conflicting methods from their definition.

It should be easy enough to add this approach as a fallback to our current approach.

qtile-extras will also likely need to be updated to ensure a similar approach can be taken for the item menu.

Resolving patch:
diff --git a/libqtile/resources/status_notifier/__init__.py b/libqtile/resources/status_notifier/__init__.py
new file mode 100644
diff --git a/libqtile/resources/status_notifier/statusnotifieritem.py b/libqtile/resources/status_notifier/statusnotifieritem.py
new file mode 100644
--- /dev/null
+++ b/libqtile/resources/status_notifier/statusnotifieritem.py
@@ -0,0 +1,51 @@
+STATUS_NOTIFIER_ITEM_SPEC = """
+<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
+"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
+<node>
+  <interface name='org.kde.StatusNotifierItem'>
+    <annotation name="org.gtk.GDBus.C.Name" value="Item" />
+    <method name='ContextMenu'>
+      <arg type='i' direction='in' name='x'/>
+      <arg type='i' direction='in' name='y'/>
+    </method>
+    <method name='Activate'>
+      <arg type='i' direction='in' name='x'/>
+      <arg type='i' direction='in' name='y'/>
+    </method>
+    <method name='SecondaryActivate'>
+      <arg type='i' direction='in' name='x'/>
+      <arg type='i' direction='in' name='y'/>
+    </method>
+    <method name='Scroll'>
+      <arg type='i' direction='in' name='delta'/>
+      <arg type='s' direction='in' name='orientation'/>
+    </method>
+    <signal name='NewTitle'/>
+    <signal name='NewIcon'/>
+    <signal name='NewAttentionIcon'/>
+    <signal name='NewOverlayIcon'/>
+    <signal name='NewToolTip'/>
+    <signal name='NewStatus'>
+      <arg type='s' name='status'/>
+    </signal>
+    <property name='Category' type='s' access='read'/>
+    <property name='Id' type='s' access='read'/>
+    <property name='Title' type='s' access='read'/>
+    <property name='Status' type='s' access='read'/>
+    <!-- See discussion on pull #536
+    <property name='WindowId' type='u' access='read'/>
+    -->
+    <property name='IconThemePath' type='s' access='read'/>
+    <property name='IconName' type='s' access='read'/>
+    <property name='IconPixmap' type='a(iiay)' access='read'/>
+    <property name='OverlayIconName' type='s' access='read'/>
+    <property name='OverlayIconPixmap' type='a(iiay)' access='read'/>
+    <property name='AttentionIconName' type='s' access='read'/>
+    <property name='AttentionIconPixmap' type='a(iiay)' access='read'/>
+    <property name='AttentionMovieName' type='s' access='read'/>
+    <property name='ToolTip' type='(sa(iiay)ss)' access='read'/>
+    <property name='Menu' type='o' access='read'/>
+    <property name='ItemIsMenu' type='b' access='read'/>
+  </interface>
+</node>
+"""
diff --git a/libqtile/widget/statusnotifier.py b/libqtile/widget/statusnotifier.py
--- a/libqtile/widget/statusnotifier.py
+++ b/libqtile/widget/statusnotifier.py
@@ -40,6 +40,7 @@
 from libqtile import bar
 from libqtile.images import Img
 from libqtile.log_utils import logger
+from libqtile.resources.status_notifier.statusnotifieritem import STATUS_NOTIFIER_ITEM_SPEC
 from libqtile.utils import add_signal_receiver, create_task
 from libqtile.widget import base
 
@@ -134,8 +135,21 @@ async def start(self):
                 continue
 
         if not interface_found:
-            logger.warning("Unable to find StatusNotifierItem interface on %s", self.service)
-            return False
+            logger.info(
+                "Unable to find StatusNotifierItem interface on %s. Falling back to default spec.",
+                self.service,
+            )
+            try:
+                obj = self.bus.get_proxy_object(
+                    self.service, STATUSNOTIFIER_PATH, STATUS_NOTIFIER_ITEM_SPEC
+                )
+                self.item = obj.get_interface("org.kde.StatusNotifierItem")
+            except InterfaceNotFoundError:
+                logger.warning(
+                    "Failed to find StatusNotifierItem interface on %s and fallback to default spec also failed.",
+                    self.service,
+                )
+                return False
 
         await self._get_local_icon()
 
@@ -162,8 +176,15 @@ async def start(self):
         return True
 
     async def _get_local_icon(self):
-        # Default to XDG icon:
-        icon_name = await self.item.get_icon_name()
+        # Default to XDG icon
+        # Some implementations don't provide an IconName property so we
+        # need to catch an error if we can't read it.
+        # We can't use hasattr to check this as the method will be created
+        # where we've used the default XML spec to provide the object introspection
+        try:
+            icon_name = await self.item.get_icon_name()
+        except DBusError:
+            return
 
         try:
             icon_path = await self.item.get_icon_theme_path()
@@ -334,7 +355,7 @@ def get_icon(self, size):
         return icon
 
     def activate(self):
-        if hasattr(self, "call_activate"):
+        if hasattr(self.item, "call_activate"):
             create_task(self._activate())
 
     async def _activate(self):
@@ -389,9 +410,9 @@ def _message_handler(self, message):
         if message.member != "RegisterStatusNotifierItem":
             return False
 
-        # If the argument passed to the method is the service name then we
-        # don't need to do anything else.
-        if message.sender == message.body[0]:
+        # If the argument is not an object path (starting with "/") then we assume
+        # it is the bus name and we don't need to do anything else.
+        if not message.body[0].startswith("/"):
             return False
 
         if message.sender not in self._items:
@@ -525,6 +546,7 @@ async def start(
             return
 
         self.bus = await MessageBus().connect()
+        await self.bus.request_name("org.freedesktop.StatusNotifierHost-qtile")
         for iface in BUS_NAMES:
             w = StatusNotifierWatcher(iface)
             w.on_item_added = self.add_item


Test patch: