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 6, 2024
1 parent 8c75256 commit c113534
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 29 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
132 changes: 106 additions & 26 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 @@ -144,6 +147,15 @@ def _add_cli_options(self):
None,
)

self.add_main_option(
"position",
0,
GLib.OptionFlags.NONE,
GLib.OptionArg.STRING,
"Position the window in the selected corner of the screen",
None,
)

def do_command_line(self, command_line):
"""
Handle CLI arguments. This method overrides default do_command_line
Expand All @@ -167,17 +179,20 @@ def parse_options(self, options: Dict[str, Any]):
self.initial_page = PAGE_LIST[int(options["page"])]
if "background" in options:
self.start_in_background = True

@staticmethod
def _do_power_button(_widget):
if "position" in options:
position = options["position"]
if position == "mouse" or position not in POSITION_LIST:
print(f"Invalid value for \"position\": {position!r}",

Check warning on line 185 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L183-L185

Added lines #L183 - L185 were not covered by tests
file=sys.stderr)
sys.exit(1)

Check warning on line 187 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L187

Added line #L187 was not covered by tests

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 195 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L195

Added line #L195 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 +218,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 222 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L222

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

Check warning on line 224 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L224

Added line #L224 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 230 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L230

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

Check warning on line 232 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L232

Added line #L232 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 240 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L240

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

Check warning on line 242 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L242

Added line #L242 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 250 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L250

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

Check warning on line 252 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L252

Added line #L252 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 266 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L262-L266

Added lines #L262 - L266 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 270 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L269-L270

Added lines #L269 - L270 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 273 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L272-L273

Added lines #L272 - L273 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 276 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L276

Added line #L276 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 280 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L280

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

def do_activate(self, *args, **kwargs):
"""
Expand All @@ -234,12 +295,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 298 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L298

Added line #L298 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 304 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L301-L304

Added lines #L301 - L304 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 307 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L307

Added line #L307 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 311 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L311

Added line #L311 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 315 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L314-L315

Added lines #L314 - L315 were not covered by tests

# grab a focus on the initially selected page so that keyboard
# navigation works
Expand All @@ -261,8 +334,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 337 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L337

Added line #L337 was not covered by tests

def hide_menu(self):
"""
Expand Down Expand Up @@ -331,6 +403,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 +448,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 453 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L452-L453

Added lines #L452 - L453 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 +492,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 497 in qubes_menu/appmenu.py

View check run for this annotation

Codecov / codecov/patch

qubes_menu/appmenu.py#L497

Added line #L497 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 c113534

Please sign in to comment.