Repository: qtile/qtile

Issue:
StatusNotifier doesn't handle multiple instances correctly
### The issue:

The StatusNotifierHost assumes there's only one widget running at any time and so cannot handle multiple callbacks to different widgets.

This could cause some rendering issues (`self.bar.draw()` may not be called when new items are added) but it also results in menus being rendered in the wrong location when using the `qtile-extras` modded version.

### Required:

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


Discussion:

Resolving patch:
diff --git a/libqtile/widget/statusnotifier.py b/libqtile/widget/statusnotifier.py
--- a/libqtile/widget/statusnotifier.py
+++ b/libqtile/widget/statusnotifier.py
@@ -238,7 +238,7 @@ def _redraw(self, result):
         """Method to invalidate icon cache and redraw icons."""
         self._invalidate_icons()
         if self.on_icon_changed is not None:
-            self.on_icon_changed()
+            self.on_icon_changed(self)
 
     def _invalidate_icons(self):
         self.surfaces = {}
@@ -484,6 +484,10 @@ def __init__(self):
         self.items: List[StatusNotifierItem] = []
         self.name = "qtile"
         self.icon_theme: str = None
+        self.started = False
+        self._on_item_added: List[Callable] = []
+        self._on_item_removed: List[Callable] = []
+        self._on_icon_changed: List[Callable] = []
 
     async def start(
         self,
@@ -491,10 +495,24 @@ async def start(
         on_item_removed: Optional[Callable] = None,
         on_icon_changed: Optional[Callable] = None,
     ):
+        """
+        Starts the host if not already started.
+
+        Widgets should register their callbacks via this method.
+        """
+        if on_item_added:
+            self._on_item_added.append(on_item_added)
+
+        if on_item_removed:
+            self._on_item_removed.append(on_item_removed)
+
+        if on_icon_changed:
+            self._on_icon_changed.append(on_icon_changed)
+
+        if self.started:
+            return
+
         self.bus = await MessageBus().connect()
-        self.on_item_added = on_item_added
-        self.on_item_removed = on_item_removed
-        self.on_icon_changed = on_icon_changed
         for iface in BUS_NAMES:
             w = StatusNotifierWatcher(iface)
             w.on_item_added = self.add_item
@@ -505,6 +523,7 @@ async def start(
             # the host on the bus.
             w.RegisterStatusNotifierHost(self.name)
             self.watchers.append(w)
+            self.started = True
 
     def item_added(self, item, service, future):
         success = future.result()
@@ -512,8 +531,8 @@ def item_added(self, item, service, future):
         # add to our list and redraw the bar
         if success:
             self.items.append(item)
-            if self.on_item_added:
-                self.on_item_added(item)
+            for callback in self._on_item_added:
+                callback(item)
 
         # It's an invalid item so let's remove it from the watchers
         else:
@@ -529,7 +548,7 @@ def add_item(self, service, path=None):
         start it.
         """
         item = StatusNotifierItem(self.bus, service, path=path, icon_theme=self.icon_theme)
-        item.on_icon_changed = self.on_icon_changed
+        item.on_icon_changed = self.item_icon_changed
         if item not in self.items:
             task = asyncio.create_task(item.start())
             task.add_done_callback(partial(self.item_added, item, service))
@@ -539,8 +558,12 @@ def remove_item(self, interface):
         # remove it and redraw the bar
         if interface in self.items:
             self.items.remove(interface)
-            if self.on_item_removed:
-                self.on_item_removed(interface)
+            for callback in self._on_item_removed:
+                callback(interface)
+
+    def item_icon_changed(self, item):
+        for callback in self._on_icon_changed:
+            callback(item)
 
 
 host = StatusNotifierHost()  # noqa: E303


Test patch: