Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Systray to better handle decorations #389

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 116 additions & 25 deletions qtile_extras/widget/systray.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,55 +19,146 @@
# 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.
"""

_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 = [
Expand All @@ -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)
Expand Down
Loading