diff --git a/qtile_extras/widget/systray.py b/qtile_extras/widget/systray.py index 1819cd4e..f7431a79 100644 --- a/qtile_extras/widget/systray.py +++ b/qtile_extras/widget/systray.py @@ -19,18 +19,77 @@ # SOFTWARE. import xcffib from libqtile import widget - -from qtile_extras.widget.decorations import RectDecoration +from libqtile.backend.x11 import window +from libqtile.widget.systray import Icon as QIcon +from xcffib.xproto import ClientMessageData, ClientMessageEvent, EventMask, ExposeEvent, SetMode XEMBED_PROTOCOL_VERSION = 0 +class Icon(QIcon): + def __init__(self, win, qtile, systray): + super().__init__(win, qtile, systray) + self._pixmap = None + + def handle_DestroyNotify(self, event): # noqa: N802 + r = super().handle_DestroyNotify(event) + self._free_pixmap() + return r + + def _free_pixmap(self): + if self._pixmap is not None: + self.qtile.core.conn.conn.core.FreePixmap(self._pixmap) + self._pixmap = None + + def set_pixmap(self, x, y, drawer): + """ + Sets the icon's backpixmap to be the widget's background. + When using pseudotransparency, this will mean the root wallpaper + (and any background rendered by the widget) will also be applied + to the icons. + """ + + # Create a new pixmap the size of the icon window + if self._pixmap is None: + self._pixmap = self.qtile.core.conn.conn.generate_id() + + self.qtile.core.conn.conn.core.CreatePixmap( + drawer._depth, + self._pixmap, + self.window.wid, + self.width, + self.height, + ) + + # Copy the widget's pixmap to the new pixmap + with self.qtile.core.masked(): + self.qtile.core.conn.conn.core.CopyArea( + drawer.pixmap, + self._pixmap, + drawer._gc, + x, + y, # Source x, y positions equal the icon's offset in the widget + 0, + 0, # Pixmap is placed at 0, 0 in new pixmap + self.width, + self.height, + ) + + # Apply the pixmap to the window + self.window.set_attribute(backpixmap=self._pixmap) + + # We need to send an Expose event to force the window to redraw + event = ExposeEvent.synthetic(self.window.wid, 0, 0, self.width, self.height, 0) + self.window.send_event(event, mask=EventMask.Exposure) + self.qtile.core.conn.flush() + + class Systray(widget.Systray): """ A modified version of Qtile's Systray widget. The only difference is to improve behaviour of the icon background when using - ``RectDecoration`` decorations. + decorations. This widget does not and will not fix the issue with icons having a transparent background when displaying on a (semi-)transparent bar. @@ -38,36 +97,68 @@ class Systray(widget.Systray): _qte_compatibility = True + def handle_ClientMessage(self, event): # noqa: N802 + atoms = self.conn.atoms + + opcode = event.type + data = event.data.data32 + message = data[1] + wid = data[2] + + parent = self.bar.window.window + + if opcode == atoms["_NET_SYSTEM_TRAY_OPCODE"] and message == 0: + w = window.XWindow(self.conn, wid) + icon = Icon(w, self.qtile, self) + if icon not in self.tray_icons: + self.tray_icons.append(icon) + self.tray_icons.sort(key=lambda icon: icon.name) + self.qtile.windows_map[wid] = icon + + self.conn.conn.core.ChangeSaveSet(SetMode.Insert, wid) + self.conn.conn.core.ReparentWindow(wid, parent.wid, 0, 0) + self.conn.conn.flush() + + info = icon.window.get_property("_XEMBED_INFO", unpack=int) + + if not info: + self.bar.draw() + return False + + if info[1]: + self.bar.draw() + + return False + def draw(self): offset = self.padding self.drawer.clear(self.background or self.bar.background) self.drawer.draw(offsetx=self.offset, offsety=self.offsety, width=self.length) for pos, icon in enumerate(self.tray_icons): - # Check if we're using a decoration and set backpixel colour accordingly - rect_decs = [d for d in self.decorations if isinstance(d, RectDecoration)] - if rect_decs: - top = rect_decs[-1] - if top.filled: - fill_colour = top.fill_colour - else: - fill_colour = self.background or self.bar.background - if fill_colour.startswith("#"): - fill_colour = fill_colour[1:] - icon.window.set_attribute(backpixel=int(fill_colour, 16)) - else: - # Back pixmap results in translation issues as it copies pixmap from 0, 0 - icon.window.set_attribute(backpixmap=self.drawer.pixmap) - if self.bar.horizontal: - xoffset = self.offsetx + offset - yoffset = self.bar.height // 2 - self.icon_size // 2 + self.offsety + xoffset = offset + yoffset = self.bar.height // 2 - self.icon_size // 2 step = icon.width else: - xoffset = self.bar.width // 2 - self.icon_size // 2 + self.offsetx - yoffset = self.offsety + offset + xoffset = self.bar.width // 2 - self.icon_size // 2 + yoffset = offset step = icon.height - icon.place(xoffset, yoffset, icon.width, self.icon_size, 0, None) + if self.decorations: + icon.set_pixmap(xoffset, yoffset, self.drawer) + else: + icon.window.set_attribute(backpixmap=self.drawer.pixmap) + + icon.place( + self.offsetx + xoffset, + self.offsety + yoffset, + icon.width, + self.icon_size, + 0, + None, + ) + + # icon.place(xoffset, yoffset, icon.width, self.icon_size, 0, None) if icon.hidden: icon.unhide() data = [ @@ -77,8 +168,8 @@ def draw(self): self.bar.window.wid, XEMBED_PROTOCOL_VERSION, ] - u = xcffib.xproto.ClientMessageData.synthetic(data, "I" * 5) - event = xcffib.xproto.ClientMessageEvent.synthetic( + u = ClientMessageData.synthetic(data, "I" * 5) + event = ClientMessageEvent.synthetic( format=32, window=icon.wid, type=self.conn.atoms["_XEMBED"], data=u ) self.window.send_event(event)