Skip to content

Commit

Permalink
Wayland support via wlr-layer-shell
Browse files Browse the repository at this point in the history
This adds Wayland support on compositors that support the
wlr-layer-shell protocol, which includes KWin, Sway, COSMIC, niri, Mir,
GameScope, and Jay.  The only major compositors without support for
wlr-layer-shell are Mutter, which is generally only used by GNOME, and
Weston, which is not a general-purpose desktop compositor.
  • Loading branch information
DemiMarie committed Dec 7, 2024
1 parent 8c75256 commit 8d2ebd7
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 28 deletions.
4 changes: 2 additions & 2 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ checks:pylint:
stage: checks
before_script:
- sudo dnf install -y python3-gobject gtk3 xorg-x11-server-Xvfb
python3-pip python3-mypy
python3-pip python3-mypy gtk-layer-shell
- pip3 install --quiet -r ci/requirements.txt
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
script:
Expand All @@ -25,7 +25,7 @@ checks:tests:
- "PATH=$PATH:$HOME/.local/bin"
- sudo dnf install -y python3-gobject gtk3 python3-pytest python3-pytest-asyncio
python3-coverage xorg-x11-server-Xvfb python3-inotify sequoia-sqv
python3-pip
python3-pip gtk-layer-shell
- pip3 install --quiet -r ci/requirements.txt
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
- git clone https://github.com/QubesOS/qubes-desktop-linux-manager ~/desktop-linux-manager
Expand Down
3 changes: 2 additions & 1 deletion debian/control
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Build-Depends:
qubes-desktop-linux-manager,
python3-gi,
gobject-introspection,
gir1.2-gtk-3.0
gir1.2-gtk-3.0,
gir1.2-gtklayershell-0.1,
Standards-Version: 3.9.5
Homepage: https://www.qubes-os.org/
X-Python3-Version: >= 3.5
Expand Down
115 changes: 90 additions & 25 deletions qubes_menu/appmenu.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GLib, Gio
gi.require_version('GtkLayerShell', '0.1')
from gi.repository import Gtk, Gdk, GLib, Gio, GtkLayerShell

import gbulb
gbulb.install()
Expand Down Expand Up @@ -93,10 +94,12 @@ def __init__(self, qapp, dispatcher):
self.initial_page = "app_page"
self.sort_running = False
self.start_in_background = False
self.kde = "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":")

self._add_cli_options()

self.builder: Optional[Gtk.Builder] = None
self.layer_shell: bool = False
self.main_window: Optional[Gtk.Window] = None
self.main_notebook: Optional[Gtk.Notebook] = None

Expand Down Expand Up @@ -168,16 +171,13 @@ def parse_options(self, options: Dict[str, Any]):
if "background" in options:
self.start_in_background = True

@staticmethod
def _do_power_button(_widget):
def _do_power_button(self, _widget):
"""
Run xfce4's default logout button. Possible enhancement would be
providing our own tiny program.
"""
# pylint: disable=consider-using-with
current_environs = os.environ.get('XDG_CURRENT_DESKTOP', '').split(':')

if 'KDE' in current_environs:
if self.kde:

Check warning on line 180 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L180

Added line #L180 was not covered by tests
dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
proxy = Gio.DBusProxy.new_sync(
dbus, # dbus
Expand All @@ -203,21 +203,67 @@ def reposition(self):
assert self.main_window
match self.appmenu_position:
case 'top-left':
self.main_window.move(0, 0)
if self.layer_shell:
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 207 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L207

Added line #L207 was not covered by tests
GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 209 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L209

Added line #L209 was not covered by tests
GtkLayerShell.Edge.TOP, True)
else:
self.main_window.move(0, 0)
case 'top-right':
self.main_window.move(
self.main_window.get_screen().get_width() - \
self.main_window.get_size().width, 0)
if self.layer_shell:
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 215 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L215

Added line #L215 was not covered by tests
GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 217 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L217

Added line #L217 was not covered by tests
GtkLayerShell.Edge.TOP, True)
else:
self.main_window.move(
self.main_window.get_screen().get_width() -
self.main_window.get_size().width, 0)
case 'bottom-left':
self.main_window.move(0,
self.main_window.get_screen().get_height() - \
self.main_window.get_size().height)
if self.layer_shell:
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 225 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L225

Added line #L225 was not covered by tests
GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 227 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L227

Added line #L227 was not covered by tests
GtkLayerShell.Edge.BOTTOM, True)
else:
self.main_window.move(0,
self.main_window.get_screen().get_height() -
self.main_window.get_size().height)
case 'bottom-right':
self.main_window.move(
self.main_window.get_screen().get_width() - \
self.main_window.get_size().width,
self.main_window.get_screen().get_height() - \
self.main_window.get_size().height)
if self.layer_shell:
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 235 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L235

Added line #L235 was not covered by tests
GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_anchor(self.main_window,

Check warning on line 237 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L237

Added line #L237 was not covered by tests
GtkLayerShell.Edge.BOTTOM, True)
else:
self.main_window.move(
self.main_window.get_screen().get_width() -
self.main_window.get_size().width,
self.main_window.get_screen().get_height() -
self.main_window.get_size().height)

def __present(self) -> None:
assert self.main_window is not None
self.reposition()
self.main_window.present()
if not self.layer_shell:
return

Check warning on line 251 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L247-L251

Added lines #L247 - L251 were not covered by tests
# Under Wayland, the window size must be re-requested
# every time the window is shown.
current_width = self.main_window.get_allocated_width()
current_height = self.main_window.get_allocated_height()

Check warning on line 255 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L254-L255

Added lines #L254 - L255 were not covered by tests
# set size if too big
max_height = int(self.main_window.get_screen().get_height() * 0.9)
assert max_height > 0

Check warning on line 258 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L257-L258

Added lines #L257 - L258 were not covered by tests
# The default for layer shell is no keyboard input.
# Explicitly request exclusive access to the keyboard.
GtkLayerShell.set_keyboard_mode(self.main_window,

Check warning on line 261 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L261

Added line #L261 was not covered by tests
GtkLayerShell.KeyboardMode.EXCLUSIVE)
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
# by explicitly setting the window size.
self.main_window.set_size_request(current_width,

Check warning on line 265 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L265

Added line #L265 was not covered by tests
min(current_height, max_height))

def do_activate(self, *args, **kwargs):
"""
Expand All @@ -234,12 +280,24 @@ def do_activate(self, *args, **kwargs):
self.reposition()
self.main_window.show_all()
self.initialize_state()
# set size if too big
current_width = self.main_window.get_allocated_width()

Check warning on line 283 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L283

Added line #L283 was not covered by tests
current_height = self.main_window.get_allocated_height()
max_height = self.main_window.get_screen().get_height() * 0.9
if current_height > max_height:
self.main_window.resize(self.main_window.get_allocated_width(),
int(max_height))
# set size if too big
max_height = int(self.main_window.get_screen().get_height() * 0.9)
assert max_height > 0
if self.layer_shell:
if not self.start_in_background:

Check warning on line 289 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L286-L289

Added lines #L286 - L289 were not covered by tests
# The default for layer shell is no keyboard input.
# Explicitly request exclusive access to the keyboard.
GtkLayerShell.set_keyboard_mode(self.main_window,

Check warning on line 292 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L292

Added line #L292 was not covered by tests
GtkLayerShell.KeyboardMode.EXCLUSIVE)
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
# by explicitly setting the window size.
self.main_window.set_size_request(

Check warning on line 296 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L296

Added line #L296 was not covered by tests
current_width,
min(current_height, max_height))
elif current_height > max_height:
self.main_window.resize(current_height, max_height)

Check warning on line 300 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L299-L300

Added lines #L299 - L300 were not covered by tests

# grab a focus on the initially selected page so that keyboard
# navigation works
Expand All @@ -261,8 +319,7 @@ def do_activate(self, *args, **kwargs):
if self.main_window.is_visible() and not self.keep_visible:
self.main_window.hide()
else:
self.reposition()
self.main_window.present()
self.__present()

Check warning on line 322 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L322

Added line #L322 was not covered by tests

def hide_menu(self):
"""
Expand Down Expand Up @@ -331,6 +388,7 @@ def perform_setup(self):
self.builder.add_from_file(str(path))

self.main_window = self.builder.get_object('main_window')
self.layer_shell = GtkLayerShell.is_supported()
self.main_notebook = self.builder.get_object('main_notebook')

self.main_window.set_events(Gdk.EventMask.FOCUS_CHANGE_MASK)
Expand Down Expand Up @@ -375,6 +433,10 @@ def perform_setup(self):
'domain-feature-delete:' + feature,
self._update_settings)

if self.layer_shell:
GtkLayerShell.init_for_window(self.main_window)
GtkLayerShell.set_exclusive_zone(self.main_window, 0)

Check warning on line 438 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L437-L438

Added lines #L437 - L438 were not covered by tests

def load_style(self, *_args):
"""Load appropriate CSS stylesheet and associated properties."""
light_ref = (importlib.resources.files('qubes_menu') /
Expand Down Expand Up @@ -415,6 +477,9 @@ def load_settings(self):
position = local_vm.features.get(POSITION_FEATURE, "mouse")
if position not in POSITION_LIST:
position = "mouse"
if position == "mouse" and self.layer_shell:
# "mouse" unsupported under Wayland
position = "bottom-left" if self.kde else "top-left"

Check warning on line 482 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L482

Added line #L482 was not covered by tests
self.appmenu_position = position

for handler in self.handlers.values():
Expand Down
1 change: 1 addition & 0 deletions rpm_spec/qubes-desktop-linux-menu.spec.in
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ BuildRequires: gettext
Requires: python%{python3_pkgversion}-setuptools
Requires: python%{python3_pkgversion}-gbulb
Requires: gtk3
Requires: gtk-layer-shell
Requires: python%{python3_pkgversion}-qubesadmin >= 4.1.8
Requires: qubes-artwork >= 4.1.5
Requires: qubes-desktop-linux-manager
Expand Down

0 comments on commit 8d2ebd7

Please sign in to comment.