Skip to content

Commit c113534

Browse files
committed
Wayland support via wlr-layer-shell
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.
1 parent 8c75256 commit c113534

File tree

4 files changed

+111
-29
lines changed

4 files changed

+111
-29
lines changed

.gitlab-ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ checks:pylint:
1010
stage: checks
1111
before_script:
1212
- sudo dnf install -y python3-gobject gtk3 xorg-x11-server-Xvfb
13-
python3-pip python3-mypy
13+
python3-pip python3-mypy gtk-layer-shell
1414
- pip3 install --quiet -r ci/requirements.txt
1515
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
1616
script:
@@ -25,7 +25,7 @@ checks:tests:
2525
- "PATH=$PATH:$HOME/.local/bin"
2626
- sudo dnf install -y python3-gobject gtk3 python3-pytest python3-pytest-asyncio
2727
python3-coverage xorg-x11-server-Xvfb python3-inotify sequoia-sqv
28-
python3-pip
28+
python3-pip gtk-layer-shell
2929
- pip3 install --quiet -r ci/requirements.txt
3030
- git clone https://github.com/QubesOS/qubes-core-admin-client ~/core-admin-client
3131
- git clone https://github.com/QubesOS/qubes-desktop-linux-manager ~/desktop-linux-manager

debian/control

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ Build-Depends:
1313
qubes-desktop-linux-manager,
1414
python3-gi,
1515
gobject-introspection,
16-
gir1.2-gtk-3.0
16+
gir1.2-gtk-3.0,
17+
gir1.2-gtklayershell-0.1,
1718
Standards-Version: 3.9.5
1819
Homepage: https://www.qubes-os.org/
1920
X-Python3-Version: >= 3.5

qubes_menu/appmenu.py

Lines changed: 106 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828

2929
import gi
3030
gi.require_version('Gtk', '3.0')
31-
from gi.repository import Gtk, Gdk, GLib, Gio
31+
gi.require_version('GtkLayerShell', '0.1')
32+
from gi.repository import Gtk, Gdk, GLib, Gio, GtkLayerShell
3233

3334
import gbulb
3435
gbulb.install()
@@ -93,10 +94,12 @@ def __init__(self, qapp, dispatcher):
9394
self.initial_page = "app_page"
9495
self.sort_running = False
9596
self.start_in_background = False
97+
self.kde = "KDE" in os.getenv("XDG_CURRENT_DESKTOP", "").split(":")
9698

9799
self._add_cli_options()
98100

99101
self.builder: Optional[Gtk.Builder] = None
102+
self.layer_shell: bool = False
100103
self.main_window: Optional[Gtk.Window] = None
101104
self.main_notebook: Optional[Gtk.Notebook] = None
102105

@@ -144,6 +147,15 @@ def _add_cli_options(self):
144147
None,
145148
)
146149

150+
self.add_main_option(
151+
"position",
152+
0,
153+
GLib.OptionFlags.NONE,
154+
GLib.OptionArg.STRING,
155+
"Position the window in the selected corner of the screen",
156+
None,
157+
)
158+
147159
def do_command_line(self, command_line):
148160
"""
149161
Handle CLI arguments. This method overrides default do_command_line
@@ -167,17 +179,20 @@ def parse_options(self, options: Dict[str, Any]):
167179
self.initial_page = PAGE_LIST[int(options["page"])]
168180
if "background" in options:
169181
self.start_in_background = True
170-
171-
@staticmethod
172-
def _do_power_button(_widget):
182+
if "position" in options:
183+
position = options["position"]
184+
if position == "mouse" or position not in POSITION_LIST:
185+
print(f"Invalid value for \"position\": {position!r}",
186+
file=sys.stderr)
187+
sys.exit(1)
188+
189+
def _do_power_button(self, _widget):
173190
"""
174191
Run xfce4's default logout button. Possible enhancement would be
175192
providing our own tiny program.
176193
"""
177194
# pylint: disable=consider-using-with
178-
current_environs = os.environ.get('XDG_CURRENT_DESKTOP', '').split(':')
179-
180-
if 'KDE' in current_environs:
195+
if self.kde:
181196
dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
182197
proxy = Gio.DBusProxy.new_sync(
183198
dbus, # dbus
@@ -203,21 +218,67 @@ def reposition(self):
203218
assert self.main_window
204219
match self.appmenu_position:
205220
case 'top-left':
206-
self.main_window.move(0, 0)
221+
if self.layer_shell:
222+
GtkLayerShell.set_anchor(self.main_window,
223+
GtkLayerShell.Edge.LEFT, True)
224+
GtkLayerShell.set_anchor(self.main_window,
225+
GtkLayerShell.Edge.TOP, True)
226+
else:
227+
self.main_window.move(0, 0)
207228
case 'top-right':
208-
self.main_window.move(
209-
self.main_window.get_screen().get_width() - \
210-
self.main_window.get_size().width, 0)
229+
if self.layer_shell:
230+
GtkLayerShell.set_anchor(self.main_window,
231+
GtkLayerShell.Edge.RIGHT, True)
232+
GtkLayerShell.set_anchor(self.main_window,
233+
GtkLayerShell.Edge.TOP, True)
234+
else:
235+
self.main_window.move(
236+
self.main_window.get_screen().get_width() -
237+
self.main_window.get_size().width, 0)
211238
case 'bottom-left':
212-
self.main_window.move(0,
213-
self.main_window.get_screen().get_height() - \
214-
self.main_window.get_size().height)
239+
if self.layer_shell:
240+
GtkLayerShell.set_anchor(self.main_window,
241+
GtkLayerShell.Edge.LEFT, True)
242+
GtkLayerShell.set_anchor(self.main_window,
243+
GtkLayerShell.Edge.BOTTOM, True)
244+
else:
245+
self.main_window.move(0,
246+
self.main_window.get_screen().get_height() -
247+
self.main_window.get_size().height)
215248
case 'bottom-right':
216-
self.main_window.move(
217-
self.main_window.get_screen().get_width() - \
218-
self.main_window.get_size().width,
219-
self.main_window.get_screen().get_height() - \
220-
self.main_window.get_size().height)
249+
if self.layer_shell:
250+
GtkLayerShell.set_anchor(self.main_window,
251+
GtkLayerShell.Edge.RIGHT, True)
252+
GtkLayerShell.set_anchor(self.main_window,
253+
GtkLayerShell.Edge.BOTTOM, True)
254+
else:
255+
self.main_window.move(
256+
self.main_window.get_screen().get_width() -
257+
self.main_window.get_size().width,
258+
self.main_window.get_screen().get_height() -
259+
self.main_window.get_size().height)
260+
261+
def __present(self) -> None:
262+
assert self.main_window is not None
263+
self.reposition()
264+
self.main_window.present()
265+
if not self.layer_shell:
266+
return
267+
# Under Wayland, the window size must be re-requested
268+
# every time the window is shown.
269+
current_width = self.main_window.get_allocated_width()
270+
current_height = self.main_window.get_allocated_height()
271+
# set size if too big
272+
max_height = int(self.main_window.get_screen().get_height() * 0.9)
273+
assert max_height > 0
274+
# The default for layer shell is no keyboard input.
275+
# Explicitly request exclusive access to the keyboard.
276+
GtkLayerShell.set_keyboard_mode(self.main_window,
277+
GtkLayerShell.KeyboardMode.EXCLUSIVE)
278+
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
279+
# by explicitly setting the window size.
280+
self.main_window.set_size_request(current_width,
281+
min(current_height, max_height))
221282

222283
def do_activate(self, *args, **kwargs):
223284
"""
@@ -234,12 +295,24 @@ def do_activate(self, *args, **kwargs):
234295
self.reposition()
235296
self.main_window.show_all()
236297
self.initialize_state()
237-
# set size if too big
298+
current_width = self.main_window.get_allocated_width()
238299
current_height = self.main_window.get_allocated_height()
239-
max_height = self.main_window.get_screen().get_height() * 0.9
240-
if current_height > max_height:
241-
self.main_window.resize(self.main_window.get_allocated_width(),
242-
int(max_height))
300+
# set size if too big
301+
max_height = int(self.main_window.get_screen().get_height() * 0.9)
302+
assert max_height > 0
303+
if self.layer_shell:
304+
if not self.start_in_background:
305+
# The default for layer shell is no keyboard input.
306+
# Explicitly request exclusive access to the keyboard.
307+
GtkLayerShell.set_keyboard_mode(self.main_window,
308+
GtkLayerShell.KeyboardMode.EXCLUSIVE)
309+
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
310+
# by explicitly setting the window size.
311+
self.main_window.set_size_request(
312+
current_width,
313+
min(current_height, max_height))
314+
elif current_height > max_height:
315+
self.main_window.resize(current_height, max_height)
243316

244317
# grab a focus on the initially selected page so that keyboard
245318
# navigation works
@@ -261,8 +334,7 @@ def do_activate(self, *args, **kwargs):
261334
if self.main_window.is_visible() and not self.keep_visible:
262335
self.main_window.hide()
263336
else:
264-
self.reposition()
265-
self.main_window.present()
337+
self.__present()
266338

267339
def hide_menu(self):
268340
"""
@@ -331,6 +403,7 @@ def perform_setup(self):
331403
self.builder.add_from_file(str(path))
332404

333405
self.main_window = self.builder.get_object('main_window')
406+
self.layer_shell = GtkLayerShell.is_supported()
334407
self.main_notebook = self.builder.get_object('main_notebook')
335408

336409
self.main_window.set_events(Gdk.EventMask.FOCUS_CHANGE_MASK)
@@ -375,6 +448,10 @@ def perform_setup(self):
375448
'domain-feature-delete:' + feature,
376449
self._update_settings)
377450

451+
if self.layer_shell:
452+
GtkLayerShell.init_for_window(self.main_window)
453+
GtkLayerShell.set_exclusive_zone(self.main_window, 0)
454+
378455
def load_style(self, *_args):
379456
"""Load appropriate CSS stylesheet and associated properties."""
380457
light_ref = (importlib.resources.files('qubes_menu') /
@@ -415,6 +492,9 @@ def load_settings(self):
415492
position = local_vm.features.get(POSITION_FEATURE, "mouse")
416493
if position not in POSITION_LIST:
417494
position = "mouse"
495+
if position == "mouse" and self.layer_shell:
496+
# "mouse" unsupported under Wayland
497+
position = "bottom-left" if self.kde else "top-left"
418498
self.appmenu_position = position
419499

420500
for handler in self.handlers.values():

rpm_spec/qubes-desktop-linux-menu.spec.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ BuildRequires: gettext
4949
Requires: python%{python3_pkgversion}-setuptools
5050
Requires: python%{python3_pkgversion}-gbulb
5151
Requires: gtk3
52+
Requires: gtk-layer-shell
5253
Requires: python%{python3_pkgversion}-qubesadmin >= 4.1.8
5354
Requires: qubes-artwork >= 4.1.5
5455
Requires: qubes-desktop-linux-manager

0 commit comments

Comments
 (0)