Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/pr/53'
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
marmarek committed Dec 17, 2024
2 parents 0d50db0 + 8d2ebd7 commit e5b0adc
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-pyxdg
python3-pip python3-mypy python3-pyxdg 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-pyxdg
python3-pip python3-pyxdg 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:
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,
GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_anchor(self.main_window,
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,
GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_anchor(self.main_window,
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,
GtkLayerShell.Edge.LEFT, True)
GtkLayerShell.set_anchor(self.main_window,
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,
GtkLayerShell.Edge.RIGHT, True)
GtkLayerShell.set_anchor(self.main_window,
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
# 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()
# set size if too big
max_height = int(self.main_window.get_screen().get_height() * 0.9)
assert max_height > 0
# The default for layer shell is no keyboard input.
# Explicitly request exclusive access to the keyboard.
GtkLayerShell.set_keyboard_mode(self.main_window,
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,
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()
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:
# The default for layer shell is no keyboard input.
# Explicitly request exclusive access to the keyboard.
GtkLayerShell.set_keyboard_mode(self.main_window,
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,
min(current_height, max_height))
elif current_height > max_height:
self.main_window.resize(current_height, max_height)

# 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()

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)

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

0 comments on commit e5b0adc

Please sign in to comment.