Skip to content

Commit

Permalink
Create a fullscreen invisible window for mouse input
Browse files Browse the repository at this point in the history
This will hopefully allow menus to be dismissed by clicking elsewhere.
  • Loading branch information
DemiMarie committed Dec 17, 2024
1 parent d075ac5 commit b8abcd6
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 0 deletions.
3 changes: 3 additions & 0 deletions qui/tray/disk_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
WARN_LEVEL = 0.9
URGENT_WARN_LEVEL = 0.95

from .gross_gtk3_bug_workaround import DisgustingX11FullscreenWindowHack

class VMUsage:
def __init__(self, vm):
Expand Down Expand Up @@ -338,6 +339,7 @@ class DiskSpace(Gtk.Application):
def __init__(self, **properties):
super().__init__(**properties)

self.disgusting_hack = DisgustingX11FullscreenWindowHack()
self.pool_warned = False
self.vms_warned = set()

Expand Down Expand Up @@ -423,6 +425,7 @@ def make_menu(self, _unused, _event):
vm_data = VMUsageData(self.qubes_app)

menu = Gtk.Menu()
self.disgusting_hack.show_for_widget(self.menu)

menu.append(self.make_top_box(pool_data))

Expand Down
4 changes: 4 additions & 0 deletions qui/tray/domains.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
t = gettext.translation("desktop-linux-manager", fallback=True)
_ = t.gettext

from .gross_gtk3_bug_workaround import DisgustingX11FullscreenWindowHack

STATE_DICTIONARY = {
'domain-pre-start': 'Transient',
'domain-start': 'Running',
Expand Down Expand Up @@ -537,6 +539,8 @@ def __init__(self, app_name, qapp, dispatcher, stats_dispatcher):
_('<b>Qubes Domains</b>\nView and manage running domains.'))

self.tray_menu = Gtk.Menu()
self.disgusting_hack = DisgustingX11FullscreenWindowHack()
self.disgusting_hack.show_for_widget(self.tray_menu)

self.icon_cache = IconCache()

Expand Down
120 changes: 120 additions & 0 deletions qui/tray/gross_gtk3_bug_workaround.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
#!/usr/bin/python3 --
from collections import deque
import gi
from os import abort
import select
import sys
import time
import traceback
from typing import List
import xcffib
import xcffib.xproto
gi.require_version("GLib", "2.0")
from gi.repository import GLib, GObject

class DisgustingX11FullscreenWindowHack(object):
"""
GTK3 menus have a horrible bug under Xwayland: if the user clicks on a native
Wayland surface, the menu is not dismissed. This class works around the problem
by using evil X11 hacks, such as a fullscreen input-only window.
"""
def __init__(self) -> None:
self.keep_going = False
self.runtime_cb = None
conn = self.conn = xcffib.connect()

setup = conn.get_setup()
if setup.roots_len != 1:
raise RuntimeError(f"X server has {setup.roots_len} screens, this is not supported")
screen, = setup.roots
# This is not guaranteed to work, but assume it will.
depth_32, = (depth for depth in screen.allowed_depths if depth.depth == 32)
self.window_id = p = conn.generate_id()
proto = self.proto = xcffib.xproto.xprotoExtension(conn)
assert screen.width_in_pixels > 0
assert screen.height_in_pixels > 0
fullscreen = "_NET_WM_STATE_FULLSCREEN"
wm_state_fullscreen_cookie = proto.InternAtom(only_if_exists=False,
name_len=len(fullscreen),
name=fullscreen,
is_checked=True)
cookie1 = proto.CreateWindow(depth=0,
parent=screen.root,
x=0,
y=0,
wid=p,
width=screen.width_in_pixels,
height=screen.height_in_pixels,
border_width=0,
_class=xcffib.xproto.WindowClass.InputOnly,
visual=depth_32.visuals[0].visual_id,
value_mask=xcffib.xproto.CW.OverrideRedirect|xcffib.xproto.CW.EventMask,
value_list=[0,xcffib.xproto.EventMask.ButtonPress],
is_checked=True)
wm_state_fullscreen = wm_state_fullscreen_cookie.reply().atom
cookie2 = proto.ChangeProperty(mode=xcffib.xproto.PropMode.Replace,
window=p,
property=wm_state_fullscreen,
type=xcffib.xproto.Atom.ATOM,
format=32,
data_len=1,
data=[wm_state_fullscreen],
is_checked=True)
self.cookies = deque([cookie1, cookie2])
source = GLib.unix_fd_source_new(conn.get_file_descriptor(),
GLib.IOCondition(GLib.IO_IN|GLib.IO_HUP|GLib.IO_PRI))
GObject.source_set_closure(source, self.source_callback)
main_loop = GLib.main_context_ref_thread_default()
assert main_loop is not None
source_id = source.attach(main_loop)
self.cb()
def show_for_widget(self, widget) -> None:
widget.connect("unmap-event", lambda _unused: self.hide())
widget.connect("map", lambda _unused: self.show(widget.hide))
def show(self, cb) -> None:
if self.keep_going:
return
self.keep_going = True
self.cookies.appendleft(self.proto.MapWindow(self.window_id, is_checked=True))
self.runtime_cb = cb
self.cb()
def unmap(self) -> None:
if self.keep_going:
self.cookies.appendleft(self.proto.UnmapWindow(self.window_id, is_checked=True))
self.keep_going = False
def cb(self) -> None:
self.conn.flush()
while True:
ev = self.conn.poll_for_event()
if ev is None:
return
if self.cookies and self.cookies[-1].sequence == ev.sequence:
self.cookies.pop().check()
if isinstance(ev, xcffib.xproto.ButtonPressEvent):
if ev.event == self.window_id:
self.runtime_cb()
self.keep_going = False
def source_callback(self, fd, flags) -> int:
try:
assert fd == self.conn.get_file_descriptor()
try:
self.cb()
except xcffib.ConnectionException:
self.keep_going = False
return GLib.SOURCE_REMOVE
if flags & GLib.IO_HUP:
self.keep_going = False
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE
except BaseException:
try:
traceback.print_exc()
finally:
abort()

if __name__ == '__main__':
a = DisgustingX11FullscreenWindowHack()
main_loop = GLib.main_context_ref_thread_default()
a.show(lambda *args, **kwargs: None)
while a.keep_going:
main_loop.iteration()
3 changes: 3 additions & 0 deletions qui/tray/updates.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
t = gettext.translation("desktop-linux-manager", fallback=True)
_ = t.gettext

from .gross_gtk3_bug_workaround import DisgustingX11FullscreenWindowHack

class TextItem(Gtk.MenuItem):
def __init__(self, text):
Expand Down Expand Up @@ -59,6 +60,7 @@ def __init__(self, app_name, qapp, dispatcher):
super().__init__()
self.name = app_name

self.disgusting_hack = DisgustingX11FullscreenWindowHack()
self.dispatcher = dispatcher
self.qapp = qapp

Expand All @@ -76,6 +78,7 @@ def __init__(self, app_name, qapp, dispatcher):
self.obsolete_vms = set()

self.tray_menu = Gtk.Menu()
self.disgusting_hack.show_for_widget(self.tray_menu)

def run(self): # pylint: disable=arguments-differ
self.check_vms_needing_update()
Expand Down

0 comments on commit b8abcd6

Please sign in to comment.