From 40e78d361abbc3aac9456a26c1e12fca39ba1104 Mon Sep 17 00:00:00 2001 From: Mehdi Pourhadi Date: Tue, 22 Sep 2026 21:04:12 +0330 Subject: [PATCH] Fix indicator popovers failing to open from keybindings on Wayland Forward-port of ae9b6d6 (branch fix-indicator-popovers-wayland, based on r794) to the current PopoverManager rewrite: - The popover is created with autohide disabled: GTK's autohide requests an xdg_popup grab, and Wayland only grants that grab against a recent input event serial. A popover opened from a keybinding (org.gnome.Shell accelerator activation / D-Bus activation) has received no input, so the compositor refuses the popup and it is never mapped. Clicking an indicator supplies a serial, which is why exactly one keyboard-triggered open succeeded after every click. Dismissal now happens when the panel toplevel loses keyboard focus (PopoverManager.close ()). - The popover is only reparented when it actually moves to a different indicator. Unparent+set_parent on every cycle destroys and recreates the popover surface, eventually leaving it visible but unmapped and ending in a Wayland protocol error. - get_panel () is guarded against a second request, which the compositor answers by terminating the client, and realize handling runs only once. - Keyboard focus is requested when a popover opens (panels are never focused automatically), with a roundtrip so the popup is created after focus is actually granted; without this, typing in the Applications Menu search field does not work. --- src/PanelWindow.vala | 47 ++++++++++++++++++++++++++++++++ src/Services/PopoverManager.vala | 39 ++++++++++++++++++++++---- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/PanelWindow.vala b/src/PanelWindow.vala index 88c23192..5c691e28 100644 --- a/src/PanelWindow.vala +++ b/src/PanelWindow.vala @@ -56,19 +56,57 @@ public class Wingpanel.PanelWindow : Gtk.Window { return; } else { Services.BackgroundManager.get_default ().remember_window (); + + // Panels are never granted keyboard focus automatically by the + // compositor; it must be requested explicitly. Without it a + // popover opened from a keybinding has no keyboard focus and + // typing (e.g. in the Applications Menu search field) does not + // reach it. + if (desktop_panel != null) { + desktop_panel.focus (); + // focus () is an asynchronous request. The popover is + // popped up immediately after this returns, so without + // waiting for the compositor to actually grant focus the + // popover comes up unfocused and never gets mapped. + unowned var display = Gdk.Display.get_default (); + if (display is Gdk.Wayland.Display) { + ((Gdk.Wayland.Display) display).get_wl_display ().roundtrip (); + } + } } }); Services.BackgroundManager.get_default ().background_state_changed.connect (update_background); } + private bool realize_handled = false; private void on_realize () { + // A widget can be realized more than once. Connecting the surface + // signals again would run them multiple times per frame, and init_wl () + // would re-request our panel object and get us killed by the + // compositor, so only ever do this once. + if (realize_handled) { + return; + } + + realize_handled = true; + unowned var surface = (Gdk.Toplevel) get_surface (); surface.compute_size.connect (on_compute_size); surface.layout.connect (on_layout); surface.enter_monitor.connect (on_enter_monitor); + // The popover takes no Wayland grab (see PopoverManager) so it will not + // dismiss itself. Watch the toplevel's focus state instead and close it + // when the panel loses keyboard focus. + surface.notify["state"].connect (() => { + var focused = (surface.state & Gdk.ToplevelState.FOCUSED) != 0; + if (!focused && popover_manager.indicator_open) { + popover_manager.close (); + } + }); + init_wl (); } @@ -103,6 +141,15 @@ public class Wingpanel.PanelWindow : Gtk.Window { public void registry_handle_global (Wl.Registry wl_registry, uint32 name, string @interface, uint32 version) { if (@interface == "io_elementary_pantheon_shell_v1") { + // get_panel () may only be requested once per wl_surface: asking + // twice makes the compositor kill us with a protocol error + // ("get_panel already requested"), which takes the whole panel + // down. realize can fire more than once, so bail out if we already + // have our panel object. + if (desktop_panel != null) { + return; + } + desktop_shell = wl_registry.bind (name, ref Pantheon.Desktop.Shell.iface, uint32.min (version, 1)); unowned var window = get_surface (); if (window is Gdk.Wayland.Surface) { diff --git a/src/Services/PopoverManager.vala b/src/Services/PopoverManager.vala index ea74b347..859af810 100644 --- a/src/Services/PopoverManager.vala +++ b/src/Services/PopoverManager.vala @@ -48,12 +48,24 @@ public class Wingpanel.Services.PopoverManager : Object { _current_indicator.display_widget.has_tooltip = true; _current_indicator.base_indicator.closed (); _current_indicator = value; - popover.unparent (); } if (_current_indicator != null) { popover.child = _current_indicator.indicator_widget; - popover.set_parent (_current_indicator); + // Only reparent when the popover actually needs to move to a + // different indicator. Unparenting destroys the popover's + // surface and set_parent () builds a new one; doing that on + // every open/close cycle eventually leaves the popover + // unmapped (visible but never drawn) and ends in a Wayland + // protocol error. + if (popover.parent != _current_indicator) { + if (popover.parent != null) { + popover.unparent (); + } + + popover.set_parent (_current_indicator); + } + popover.popup (); _current_indicator.set_state_flags (CHECKED, true); @@ -63,16 +75,33 @@ public class Wingpanel.Services.PopoverManager : Object { } } + public void close () { + current_indicator = null; + } + construct { popover = new Gtk.Popover () { has_arrow = false, - position = BOTTOM + position = BOTTOM, + // autohide makes GTK request an xdg_popup grab. Wayland only grants + // that grab against a recent input event serial, and a popover + // opened from a keybinding has received no input at all, so the + // compositor refuses the popup and it is never mapped. Dismissal + // is handled by PanelWindow watching the toplevel focus state. + autohide = false }; popover.add_css_class ("indicator"); popover.closed.connect (() => { - current_indicator = null; - popover.unparent (); + // Fires for closes the setter already handled (it nulls + // _current_indicator before the asynchronous popdown), so guard + // against dereferencing null. Deliberately does NOT unparent: the + // popover stays parented to its indicator and is only moved when a + // different indicator is opened. + if (_current_indicator != null) { + _current_indicator.set_state_flags (NORMAL, true); + current_indicator = null; + } }); } }