Skip to content

Commit e5b0adc

Browse files
committed
Merge remote-tracking branch 'origin/pr/53'
* origin/pr/53: Wayland support via wlr-layer-shell Pull request description: This is almost completely working under Wayland. To test, use a compositor that supports Layer Shell, such as KWin. KWin can be spawned nested inside an X11 window, such as what Qubes OS provides. Issues: 1. [x] The window is too short: fixed by explicitly setting the menu size every time the window is opened. 2. [x] The configuration options that control where the app menu appears did not work: fixed by explicitly checking for them and anchoring the window to the correct corner 3. [x] Tests code was included in the production script: I moved the test code to my own local branch. 4. [ ] `mouse` mode is interpreted as `bottom-left` (KDE) or `top-left` (otherwise): will be fixed by providing mouse information via IPC. Fixes QubesOS/qubes-issues#9600
2 parents 0d50db0 + 8d2ebd7 commit e5b0adc

File tree

4 files changed

+95
-28
lines changed

4 files changed

+95
-28
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 python3-pyxdg
13+
python3-pip python3-mypy python3-pyxdg 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 python3-pyxdg
28+
python3-pip python3-pyxdg 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: 90 additions & 25 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

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

171-
@staticmethod
172-
def _do_power_button(_widget):
174+
def _do_power_button(self, _widget):
173175
"""
174176
Run xfce4's default logout button. Possible enhancement would be
175177
providing our own tiny program.
176178
"""
177179
# pylint: disable=consider-using-with
178-
current_environs = os.environ.get('XDG_CURRENT_DESKTOP', '').split(':')
179-
180-
if 'KDE' in current_environs:
180+
if self.kde:
181181
dbus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
182182
proxy = Gio.DBusProxy.new_sync(
183183
dbus, # dbus
@@ -203,21 +203,67 @@ def reposition(self):
203203
assert self.main_window
204204
match self.appmenu_position:
205205
case 'top-left':
206-
self.main_window.move(0, 0)
206+
if self.layer_shell:
207+
GtkLayerShell.set_anchor(self.main_window,
208+
GtkLayerShell.Edge.LEFT, True)
209+
GtkLayerShell.set_anchor(self.main_window,
210+
GtkLayerShell.Edge.TOP, True)
211+
else:
212+
self.main_window.move(0, 0)
207213
case 'top-right':
208-
self.main_window.move(
209-
self.main_window.get_screen().get_width() - \
210-
self.main_window.get_size().width, 0)
214+
if self.layer_shell:
215+
GtkLayerShell.set_anchor(self.main_window,
216+
GtkLayerShell.Edge.RIGHT, True)
217+
GtkLayerShell.set_anchor(self.main_window,
218+
GtkLayerShell.Edge.TOP, True)
219+
else:
220+
self.main_window.move(
221+
self.main_window.get_screen().get_width() -
222+
self.main_window.get_size().width, 0)
211223
case 'bottom-left':
212-
self.main_window.move(0,
213-
self.main_window.get_screen().get_height() - \
214-
self.main_window.get_size().height)
224+
if self.layer_shell:
225+
GtkLayerShell.set_anchor(self.main_window,
226+
GtkLayerShell.Edge.LEFT, True)
227+
GtkLayerShell.set_anchor(self.main_window,
228+
GtkLayerShell.Edge.BOTTOM, True)
229+
else:
230+
self.main_window.move(0,
231+
self.main_window.get_screen().get_height() -
232+
self.main_window.get_size().height)
215233
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)
234+
if self.layer_shell:
235+
GtkLayerShell.set_anchor(self.main_window,
236+
GtkLayerShell.Edge.RIGHT, True)
237+
GtkLayerShell.set_anchor(self.main_window,
238+
GtkLayerShell.Edge.BOTTOM, True)
239+
else:
240+
self.main_window.move(
241+
self.main_window.get_screen().get_width() -
242+
self.main_window.get_size().width,
243+
self.main_window.get_screen().get_height() -
244+
self.main_window.get_size().height)
245+
246+
def __present(self) -> None:
247+
assert self.main_window is not None
248+
self.reposition()
249+
self.main_window.present()
250+
if not self.layer_shell:
251+
return
252+
# Under Wayland, the window size must be re-requested
253+
# every time the window is shown.
254+
current_width = self.main_window.get_allocated_width()
255+
current_height = self.main_window.get_allocated_height()
256+
# set size if too big
257+
max_height = int(self.main_window.get_screen().get_height() * 0.9)
258+
assert max_height > 0
259+
# The default for layer shell is no keyboard input.
260+
# Explicitly request exclusive access to the keyboard.
261+
GtkLayerShell.set_keyboard_mode(self.main_window,
262+
GtkLayerShell.KeyboardMode.EXCLUSIVE)
263+
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
264+
# by explicitly setting the window size.
265+
self.main_window.set_size_request(current_width,
266+
min(current_height, max_height))
221267

222268
def do_activate(self, *args, **kwargs):
223269
"""
@@ -234,12 +280,24 @@ def do_activate(self, *args, **kwargs):
234280
self.reposition()
235281
self.main_window.show_all()
236282
self.initialize_state()
237-
# set size if too big
283+
current_width = self.main_window.get_allocated_width()
238284
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))
285+
# set size if too big
286+
max_height = int(self.main_window.get_screen().get_height() * 0.9)
287+
assert max_height > 0
288+
if self.layer_shell:
289+
if not self.start_in_background:
290+
# The default for layer shell is no keyboard input.
291+
# Explicitly request exclusive access to the keyboard.
292+
GtkLayerShell.set_keyboard_mode(self.main_window,
293+
GtkLayerShell.KeyboardMode.EXCLUSIVE)
294+
# Work around https://github.com/wmww/gtk-layer-shell/issues/167
295+
# by explicitly setting the window size.
296+
self.main_window.set_size_request(
297+
current_width,
298+
min(current_height, max_height))
299+
elif current_height > max_height:
300+
self.main_window.resize(current_height, max_height)
243301

244302
# grab a focus on the initially selected page so that keyboard
245303
# navigation works
@@ -261,8 +319,7 @@ def do_activate(self, *args, **kwargs):
261319
if self.main_window.is_visible() and not self.keep_visible:
262320
self.main_window.hide()
263321
else:
264-
self.reposition()
265-
self.main_window.present()
322+
self.__present()
266323

267324
def hide_menu(self):
268325
"""
@@ -331,6 +388,7 @@ def perform_setup(self):
331388
self.builder.add_from_file(str(path))
332389

333390
self.main_window = self.builder.get_object('main_window')
391+
self.layer_shell = GtkLayerShell.is_supported()
334392
self.main_notebook = self.builder.get_object('main_notebook')
335393

336394
self.main_window.set_events(Gdk.EventMask.FOCUS_CHANGE_MASK)
@@ -375,6 +433,10 @@ def perform_setup(self):
375433
'domain-feature-delete:' + feature,
376434
self._update_settings)
377435

436+
if self.layer_shell:
437+
GtkLayerShell.init_for_window(self.main_window)
438+
GtkLayerShell.set_exclusive_zone(self.main_window, 0)
439+
378440
def load_style(self, *_args):
379441
"""Load appropriate CSS stylesheet and associated properties."""
380442
light_ref = (importlib.resources.files('qubes_menu') /
@@ -415,6 +477,9 @@ def load_settings(self):
415477
position = local_vm.features.get(POSITION_FEATURE, "mouse")
416478
if position not in POSITION_LIST:
417479
position = "mouse"
480+
if position == "mouse" and self.layer_shell:
481+
# "mouse" unsupported under Wayland
482+
position = "bottom-left" if self.kde else "top-left"
418483
self.appmenu_position = position
419484

420485
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: python%{python3_pkgversion}-pyxdg
5455
Requires: qubes-artwork >= 4.1.5

0 commit comments

Comments
 (0)