diff options
| author | srdusr <[email protected]> | 2024-10-31 00:18:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2024-10-31 00:18:00 +0200 |
| commit | 4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c (patch) | |
| tree | 30281a9b7f24e1c996450289738a2a71b2aefbec /crates/x11 | |
| parent | 9748bca006cd2498c4c0f2730d8789c667bda52b (diff) | |
| download | srdwm-4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c.tar.gz srdwm-4cfe2da2de5886f503ce8bf78bf15cb4f31cfb3c.zip | |
Add global-menu support (dbusmenu/appmenu) for Wayland and X11 clients
Exposes each window's application menu (Firefox/GTK's dbusmenu export,
X11's _GTK_APPLICATION_OBJECT_PATH-style menus via global_menu.rs) so
an external panel can render it as a system menu bar rather than each
window drawing its own, the same convention appmenu.rs/gtk_shell.rs
and appmenu_registrar.rs wire up across both backends.
Diffstat (limited to 'crates/x11')
| -rw-r--r-- | crates/x11/src/platform/connect.rs | 1 | ||||
| -rw-r--r-- | crates/x11/src/platform/global_menu.rs | 90 | ||||
| -rw-r--r-- | crates/x11/src/platform/mod.rs | 30 | ||||
| -rw-r--r-- | crates/x11/src/platform/trait_impl.rs | 15 |
4 files changed, 134 insertions, 2 deletions
diff --git a/crates/x11/src/platform/connect.rs b/crates/x11/src/platform/connect.rs index d5c3a59..2d2f726 100644 --- a/crates/x11/src/platform/connect.rs +++ b/crates/x11/src/platform/connect.rs @@ -94,6 +94,7 @@ impl X11Platform { keyboard_mapping, numlock_mask, ipc, + appmenu_registrar: Some(srdwm_platform::AppmenuRegistrarState::new()), }) } diff --git a/crates/x11/src/platform/global_menu.rs b/crates/x11/src/platform/global_menu.rs new file mode 100644 index 0000000..161fe80 --- /dev/null +++ b/crates/x11/src/platform/global_menu.rs @@ -0,0 +1,90 @@ +//! Global-menu support for the native X11 backend - parity with the +//! Wayland backend's XWayland integration (`crates/wayland/src/xwayland.rs` +//! and `srdwm_platform::appmenu_registrar`), reading the identical X11 +//! properties and running the identical `com.canonical.AppMenu.Registrar` +//! D-Bus service, since a client-side toolkit (GTK/Qt) exports its menu the +//! same way regardless of which X server it's actually talking to. + +use super::*; + +impl X11Platform { + /// Reads `xid`'s global-menu D-Bus address straight off its own X11 + /// properties - `_GTK_UNIQUE_BUS_NAME` plus whichever menu-path atom + /// the client actually set. Exact mirror of `crates/wayland/src/ + /// xwayland.rs::EwmhState::read_global_menu`; see that method's doc + /// comment for the full reasoning (menubar wins over app-menu, the + /// `appmenu-gtk-module` Unity-shim case `classify_menu_source` exists + /// for). No bus name means no menu at all, so this returns `None` + /// rather than a `GlobalMenu` with an empty `bus_name`. + pub(super) fn read_global_menu(&self, xid: XWindow) -> Option<srdwm_core::GlobalMenu> { + let read_string = |atom: u32| -> Option<String> { + let reply = self.conn.get_property(false, xid, atom, x11rb::protocol::xproto::AtomEnum::ANY, 0, u32::MAX).ok()?.reply().ok()?; + if reply.value.is_empty() { + return None; + } + String::from_utf8(reply.value).ok().filter(|s| !s.is_empty()) + }; + + // Checked before anything GTK-atom-related - see `xwayland.rs`'s + // identical check in its own `read_global_menu` for why: these two + // are already a complete address on their own, and a Qt app under + // a KDE Plasma session never sets `_GTK_UNIQUE_BUS_NAME` at all. + if let (Some(bus_name), Some(menu_path)) = (read_string(self.atoms._KDE_NET_WM_APPMENU_SERVICE_NAME), read_string(self.atoms._KDE_NET_WM_APPMENU_OBJECT_PATH)) { + return Some(srdwm_core::GlobalMenu { bus_name, menu_path: Some(menu_path), app_path: None, window_path: None, source: srdwm_core::MenuSource::DbusMenu }); + } + + let bus_name = read_string(self.atoms._GTK_UNIQUE_BUS_NAME)?; + let app_path = read_string(self.atoms._GTK_APPLICATION_OBJECT_PATH); + let window_path = read_string(self.atoms._GTK_WINDOW_OBJECT_PATH); + let is_real_gtk_application = app_path.is_some() || window_path.is_some(); + let gtk_menu_path = read_string(self.atoms._GTK_MENUBAR_OBJECT_PATH).or_else(|| read_string(self.atoms._GTK_APP_MENU_OBJECT_PATH)); + let unity_path = read_string(self.atoms._UNITY_OBJECT_PATH); + let (menu_path, source) = srdwm_core::classify_menu_source(gtk_menu_path, is_real_gtk_application, unity_path); + Some(srdwm_core::GlobalMenu { bus_name, menu_path, app_path, window_path, source }) + } + + /// Refreshes the focused window's `global_menu` from its own X11 + /// properties - call on every real focus change (`Platform::focus`, + /// the single chokepoint every focus path already goes through, same + /// role `update_net_active_window` plays for the Wayland backend). + /// Global-menu properties are usually set once, shortly after a client + /// registers on the session bus, which can race a window's own initial + /// map - reading only at map time would miss a client that finished + /// registering a moment later, so this re-reads on every focus instead. + /// `read_global_menu` returning `None` (the common case for anything + /// non-GTK, or a GTK app with no menu to export) correctly clears a + /// stale value left over from whichever window was focused before. + pub(super) fn refresh_focused_global_menu(&mut self, id: WindowId, client: XWindow) { + let menu = self.read_global_menu(client); + if let Some(w) = self.wm.borrow_mut().window_mut(id) { + w.global_menu = menu; + } + } + + /// Drains `AppmenuRegistrarState`'s channel and applies every event to + /// the matching `Window.global_menu` - call once per `poll_events` + /// tick, same as the Wayland backend's `xwayland.rs::apply_registrar_ + /// events`. `xid_to_core` already maps XID straight to `WindowId` here + /// (unlike the Wayland backend, which has to scan for the matching + /// `X11Surface`), so this needs no extra lookup structure of its own. + pub(super) fn apply_registrar_events(&mut self) { + let Some(registrar) = &self.appmenu_registrar else { return }; + let events = registrar.drain_events(); + if events.is_empty() { + return; + } + for event in events { + let (window_id, menu) = match event { + srdwm_platform::RegistrarEvent::Registered { window_id, bus_name, menu_path } => ( + window_id, + Some(srdwm_core::GlobalMenu { bus_name, menu_path: Some(menu_path), app_path: None, window_path: None, source: srdwm_core::MenuSource::DbusMenu }), + ), + srdwm_platform::RegistrarEvent::Unregistered { window_id } => (window_id, None), + }; + let Some(&id) = self.xid_to_core.get(&window_id) else { continue }; + if let Some(w) = self.wm.borrow_mut().window_mut(id) { + w.global_menu = menu; + } + } + } +} diff --git a/crates/x11/src/platform/mod.rs b/crates/x11/src/platform/mod.rs index eea936f..04c918c 100644 --- a/crates/x11/src/platform/mod.rs +++ b/crates/x11/src/platform/mod.rs @@ -55,6 +55,26 @@ x11rb::atom_manager! { _NET_CLIENT_LIST, _NET_ACTIVE_WINDOW, UTF8_STRING, + // Global-menu properties - see `read_global_menu`'s doc comment. + // A native X11 client is exactly the same GTK/Qt app the Wayland + // backend's `xwayland.rs::read_global_menu` already reads these + // from (XWayland is just another X server as far as a toolkit is + // concerned), so this is the identical atom set for the identical + // reason. + _GTK_UNIQUE_BUS_NAME, + _GTK_APPLICATION_OBJECT_PATH, + _GTK_WINDOW_OBJECT_PATH, + _GTK_MENUBAR_OBJECT_PATH, + _GTK_APP_MENU_OBJECT_PATH, + _UNITY_OBJECT_PATH, + // KWin's own global-menu property pair - what `libdbusmenu-qt`'s + // KDE integration sets. Already a complete, unambiguous + // `com.canonical.dbusmenu` address on its own (no classification + // needed the way the GTK/Unity atoms above need), and checked + // first in `read_global_menu` for exactly that reason - see that + // method's doc comment. + _KDE_NET_WM_APPMENU_SERVICE_NAME, + _KDE_NET_WM_APPMENU_OBJECT_PATH, } } @@ -121,6 +141,15 @@ pub struct X11Platform { /// itself still starts either way, matching how the Wayland backends /// already treat this as non-fatal. ipc: Option<srdwm_platform::IpcServer>, + /// `com.canonical.AppMenu.Registrar` - the classic Qt/`appmenu-qt5` + /// global-menu source, see `srdwm_platform::appmenu_registrar`'s module + /// doc comment. Unlike the Wayland backend (where this is `None` until + /// XWayland finishes starting up), a native X11 session always has a + /// real X server the moment this struct exists, so it's started + /// unconditionally in `connect` - still `Option` because starting the + /// D-Bus service itself can independently fail (see that module's own + /// `None` handling). + appmenu_registrar: Option<srdwm_platform::AppmenuRegistrarState>, } @@ -139,6 +168,7 @@ impl ClonedForRender for Option<&CoreWindow> { mod actions; mod connect; mod events; +mod global_menu; mod trait_impl; mod window; diff --git a/crates/x11/src/platform/trait_impl.rs b/crates/x11/src/platform/trait_impl.rs index 0a082be..1ff7845 100644 --- a/crates/x11/src/platform/trait_impl.rs +++ b/crates/x11/src/platform/trait_impl.rs @@ -34,6 +34,7 @@ impl Platform for X11Platform { out.push(e); } } + self.apply_registrar_events(); if let Some(ipc) = self.ipc.as_mut() { if ipc.poll(&self.wm) { out.push(Event::WorkspaceChanged); @@ -106,9 +107,11 @@ impl Platform for X11Platform { fn focus(&mut self, window: WindowId) -> PlatformResult<()> { if let Some(frame) = self.frames.get(&window) { - self.conn.set_input_focus(x11rb::protocol::xproto::InputFocus::POINTER_ROOT, frame.client, x11rb::CURRENT_TIME).map_err(err)?; - self.conn.change_property32(x11rb::protocol::xproto::PropMode::REPLACE, self.root, self.atoms._NET_ACTIVE_WINDOW, x11rb::protocol::xproto::AtomEnum::WINDOW, &[frame.client]).map_err(err)?; + let client = frame.client; + self.conn.set_input_focus(x11rb::protocol::xproto::InputFocus::POINTER_ROOT, client, x11rb::CURRENT_TIME).map_err(err)?; + self.conn.change_property32(x11rb::protocol::xproto::PropMode::REPLACE, self.root, self.atoms._NET_ACTIVE_WINDOW, x11rb::protocol::xproto::AtomEnum::WINDOW, &[client]).map_err(err)?; self.conn.flush().map_err(err)?; + self.refresh_focused_global_menu(window, client); } Ok(()) } @@ -217,4 +220,12 @@ impl Platform for X11Platform { self.conn.ungrab_key(0, self.root, ModMask::ANY).map_err(err)?; Ok(()) } + + fn keyboard_layout(&mut self) -> PlatformResult<String> { + Err(PlatformError::Unsupported("keyboard_layout")) + } + + fn cycle_keyboard_layout(&mut self) -> PlatformResult<String> { + Err(PlatformError::Unsupported("cycle_keyboard_layout")) + } } |