diff options
| author | sodiboo <37938646+sodiboo@users.noreply.github.com> | 2025-01-18 15:26:42 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-01-18 17:26:42 +0300 |
| commit | 0584dd2f1e82417bdabcc0d8cb20fddc2e8cc5e7 (patch) | |
| tree | 503168062d4b15245bdb1fdc940a7b3d748afa07 | |
| parent | bd559a26602874f4104e342e2ce02317ae1ae605 (diff) | |
| download | niri-0584dd2f1e82417bdabcc0d8cb20fddc2e8cc5e7.tar.gz niri-0584dd2f1e82417bdabcc0d8cb20fddc2e8cc5e7.tar.bz2 niri-0584dd2f1e82417bdabcc0d8cb20fddc2e8cc5e7.zip | |
implement `keyboard-shortcuts-inhibit` and `wlr-virtual-pointer` (#630)
* stub keyboard-shortcuts-inhibit and virtual-pointer impls
* implement keyboard-shortcuts-inhibit
* implement virtual-pointer
* deal with supressed key release edge-case; add allow-inhibiting property
* add toggle-keyboard-shortcuts-inhibit bind
* add InputBackend extensions; use Device::output() for absolute pos events
* add a `State` parameter to the backend exts and better document future intent
* Add some tests for is_inhibiting_shortcuts
---------
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
| -rw-r--r-- | niri-config/src/lib.rs | 49 | ||||
| -rw-r--r-- | resources/default-config.kdl | 10 | ||||
| -rw-r--r-- | src/handlers/mod.rs | 69 | ||||
| -rw-r--r-- | src/input/backend_ext.rs | 51 | ||||
| -rw-r--r-- | src/input/mod.rs | 151 | ||||
| -rw-r--r-- | src/niri.rs | 17 | ||||
| -rw-r--r-- | src/protocols/mod.rs | 1 | ||||
| -rw-r--r-- | src/protocols/virtual_pointer.rs | 563 |
8 files changed, 889 insertions, 22 deletions
diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 167ef6ac..fae5129e 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -1191,6 +1191,7 @@ pub struct Bind { pub repeat: bool, pub cooldown: Option<Duration>, pub allow_when_locked: bool, + pub allow_inhibiting: bool, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] @@ -1278,6 +1279,7 @@ pub enum Action { id: u64, write_to_disk: bool, }, + ToggleKeyboardShortcutsInhibit, CloseWindow, #[knuffel(skip)] CloseWindowById(u64), @@ -3015,6 +3017,7 @@ where let mut cooldown = None; let mut allow_when_locked = false; let mut allow_when_locked_node = None; + let mut allow_inhibiting = true; for (name, val) in &node.properties { match &***name { "repeat" => { @@ -3029,6 +3032,9 @@ where allow_when_locked = knuffel::traits::DecodeScalar::decode(val, ctx)?; allow_when_locked_node = Some(name); } + "allow-inhibiting" => { + allow_inhibiting = knuffel::traits::DecodeScalar::decode(val, ctx)?; + } name_str => { ctx.emit_error(DecodeError::unexpected( name, @@ -3050,6 +3056,7 @@ where repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }; if let Some(child) = children.next() { @@ -3072,12 +3079,19 @@ where } } + // The toggle-inhibit action must always be uninhibitable. + // Otherwise, it would be impossible to trigger it. + if matches!(action, Action::ToggleKeyboardShortcutsInhibit) { + allow_inhibiting = false; + } + Ok(Self { key, action, repeat, cooldown, allow_when_locked, + allow_inhibiting, }) } Err(e) => { @@ -3463,6 +3477,8 @@ mod tests { } binds { + Mod+Escape { toggle-keyboard-shortcuts-inhibit; } + Mod+Shift+Escape allow-inhibiting=true { toggle-keyboard-shortcuts-inhibit; } Mod+T allow-when-locked=true { spawn "alacritty"; } Mod+Q { close-window; } Mod+Shift+H { focus-monitor-left; } @@ -3470,7 +3486,7 @@ mod tests { Mod+Comma { consume-window-into-column; } Mod+1 { focus-workspace 1; } Mod+Shift+1 { focus-workspace "workspace-1"; } - Mod+Shift+E { quit skip-confirmation=true; } + Mod+Shift+E allow-inhibiting=false { quit skip-confirmation=true; } Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; } } @@ -3781,6 +3797,28 @@ mod tests { binds: Binds(vec![ Bind { key: Key { + trigger: Trigger::Keysym(Keysym::Escape), + modifiers: Modifiers::COMPOSITOR, + }, + action: Action::ToggleKeyboardShortcutsInhibit, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: false, + }, + Bind { + key: Key { + trigger: Trigger::Keysym(Keysym::Escape), + modifiers: Modifiers::COMPOSITOR | Modifiers::SHIFT, + }, + action: Action::ToggleKeyboardShortcutsInhibit, + repeat: true, + cooldown: None, + allow_when_locked: false, + allow_inhibiting: false, + }, + Bind { + key: Key { trigger: Trigger::Keysym(Keysym::t), modifiers: Modifiers::COMPOSITOR, }, @@ -3788,6 +3826,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: true, + allow_inhibiting: true, }, Bind { key: Key { @@ -3798,6 +3837,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3808,6 +3848,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3818,6 +3859,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3828,6 +3870,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3838,6 +3881,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3850,6 +3894,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3860,6 +3905,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: false, }, Bind { key: Key { @@ -3870,6 +3916,7 @@ mod tests { repeat: true, cooldown: Some(Duration::from_millis(150)), allow_when_locked: false, + allow_inhibiting: true, }, ]), switch_events: SwitchBinds { diff --git a/resources/default-config.kdl b/resources/default-config.kdl index 5936a80e..a3560ab8 100644 --- a/resources/default-config.kdl +++ b/resources/default-config.kdl @@ -536,6 +536,16 @@ binds { Ctrl+Print { screenshot-screen; } Alt+Print { screenshot-window; } + // Applications such as remote-desktop clients and software KVM switches may + // request that niri stops processing the keyboard shortcuts defined here + // so they may, for example, forward the key presses as-is to a remote machine. + // It's a good idea to bind an escape hatch to toggle the inhibitor, + // so a buggy application can't hold your session hostage. + // + // The allow-inhibiting=false property can be applied to other binds as well, + // which ensures niri always processes them, even when an inhibitor is active. + Mod+Escape allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; } + // The quit action will show a confirmation dialog to avoid accidental exits. Mod+Shift+E { quit; } Ctrl+Alt+Delete { quit; } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 9d147df4..9ffe5bab 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -11,7 +11,7 @@ use std::time::Duration; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::drm::DrmNode; -use smithay::backend::input::TabletToolDescriptor; +use smithay::backend::input::{InputEvent, TabletToolDescriptor}; use smithay::desktop::{PopupKind, PopupManager}; use smithay::input::pointer::{ CursorIcon, CursorImageStatus, CursorImageSurfaceData, PointerHandle, @@ -35,6 +35,9 @@ use smithay::wayland::fractional_scale::FractionalScaleHandler; use smithay::wayland::idle_inhibit::IdleInhibitHandler; use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState}; use smithay::wayland::input_method::{InputMethodHandler, PopupSurface}; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, +}; use smithay::wayland::output::OutputHandler; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler}; use smithay::wayland::security_context::{ @@ -59,11 +62,12 @@ use smithay::wayland::xdg_activation::{ use smithay::{ delegate_cursor_shape, delegate_data_control, delegate_data_device, delegate_dmabuf, delegate_drm_lease, delegate_fractional_scale, delegate_idle_inhibit, delegate_idle_notify, - delegate_input_method_manager, delegate_output, delegate_pointer_constraints, - delegate_pointer_gestures, delegate_presentation, delegate_primary_selection, - delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_session_lock, - delegate_single_pixel_buffer, delegate_tablet_manager, delegate_text_input_manager, - delegate_viewporter, delegate_virtual_keyboard_manager, delegate_xdg_activation, + delegate_input_method_manager, delegate_keyboard_shortcuts_inhibit, delegate_output, + delegate_pointer_constraints, delegate_pointer_gestures, delegate_presentation, + delegate_primary_selection, delegate_relative_pointer, delegate_seat, + delegate_security_context, delegate_session_lock, delegate_single_pixel_buffer, + delegate_tablet_manager, delegate_text_input_manager, delegate_viewporter, + delegate_virtual_keyboard_manager, delegate_xdg_activation, }; pub use crate::handlers::xdg_shell::KdeDecorationsModeState; @@ -75,10 +79,15 @@ use crate::protocols::gamma_control::{GammaControlHandler, GammaControlManagerSt use crate::protocols::mutter_x11_interop::MutterX11InteropHandler; use crate::protocols::output_management::{OutputManagementHandler, OutputManagementManagerState}; use crate::protocols::screencopy::{Screencopy, ScreencopyHandler, ScreencopyManagerState}; +use crate::protocols::virtual_pointer::{ + VirtualPointerAxisEvent, VirtualPointerButtonEvent, VirtualPointerHandler, + VirtualPointerInputBackend, VirtualPointerManagerState, VirtualPointerMotionAbsoluteEvent, + VirtualPointerMotionEvent, +}; use crate::utils::{output_size, send_scale_transform, with_toplevel_role}; use crate::{ delegate_foreign_toplevel, delegate_gamma_control, delegate_mutter_x11_interop, - delegate_output_management, delegate_screencopy, + delegate_output_management, delegate_screencopy, delegate_virtual_pointer, }; pub const XDG_ACTIVATION_TOKEN_TIMEOUT: Duration = Duration::from_secs(10); @@ -243,7 +252,28 @@ impl InputMethodHandler for State { } } +impl KeyboardShortcutsInhibitHandler for State { + fn keyboard_shortcuts_inhibit_state(&mut self) -> &mut KeyboardShortcutsInhibitState { + &mut self.niri.keyboard_shortcuts_inhibit_state + } + + fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + // FIXME: show a confirmation dialog with a "remember for this application" kind of toggle. + inhibitor.activate(); + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .insert(inhibitor.wl_surface().clone(), inhibitor); + } + + fn inhibitor_destroyed(&mut self, inhibitor: KeyboardShortcutsInhibitor) { + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .remove(&inhibitor.wl_surface().clone()); + } +} + delegate_input_method_manager!(State); +delegate_keyboard_shortcuts_inhibit!(State); delegate_virtual_keyboard_manager!(State); impl SelectionHandler for State { @@ -562,6 +592,31 @@ impl ScreencopyHandler for State { } delegate_screencopy!(State); +impl VirtualPointerHandler for State { + fn virtual_pointer_manager_state(&mut self) -> &mut VirtualPointerManagerState { + &mut self.niri.virtual_pointer_state + } + + fn on_virtual_pointer_motion(&mut self, event: VirtualPointerMotionEvent) { + self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerMotion { event }); + } + + fn on_virtual_pointer_motion_absolute(&mut self, event: VirtualPointerMotionAbsoluteEvent) { + self.process_input_event( + InputEvent::<VirtualPointerInputBackend>::PointerMotionAbsolute { event }, + ); + } + + fn on_virtual_pointer_button(&mut self, event: VirtualPointerButtonEvent) { + self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerButton { event }); + } + + fn on_virtual_pointer_axis(&mut self, event: VirtualPointerAxisEvent) { + self.process_input_event(InputEvent::<VirtualPointerInputBackend>::PointerAxis { event }); + } +} +delegate_virtual_pointer!(State); + impl DrmLeaseHandler for State { fn drm_lease_state(&mut self, node: DrmNode) -> &mut DrmLeaseState { self.backend diff --git a/src/input/backend_ext.rs b/src/input/backend_ext.rs new file mode 100644 index 00000000..99f4a904 --- /dev/null +++ b/src/input/backend_ext.rs @@ -0,0 +1,51 @@ +use ::input as libinput; +use smithay::backend::input; +use smithay::backend::winit::WinitVirtualDevice; +use smithay::output::Output; + +use crate::niri::State; +use crate::protocols::virtual_pointer::VirtualPointer; + +pub trait NiriInputBackend: input::InputBackend<Device = Self::NiriDevice> { + type NiriDevice: NiriInputDevice; +} +impl<T: input::InputBackend> NiriInputBackend for T +where + Self::Device: NiriInputDevice, +{ + type NiriDevice = Self::Device; +} + +pub trait NiriInputDevice: input::Device { + // FIXME: this should maybe be per-event, not per-device, + // but it's not clear that this matters in practice? + // it might be more obvious once we implement it for libinput + fn output(&self, state: &State) -> Option<Output>; +} + +impl NiriInputDevice for libinput::Device { + fn output(&self, _state: &State) -> Option<Output> { + // FIXME: Allow specifying the output per-device? + None + } +} + +impl NiriInputDevice for WinitVirtualDevice { + fn output(&self, _state: &State) -> Option<Output> { + // FIXME: we should be returning the single output that the winit backend creates, + // but for now, that will cause issues because the output is normally upside down, + // so we apply Transform::Flipped180 to it and that would also cause + // the cursor position to be flipped, which is not what we want. + // + // instead, we just return None and rely on the fact that it has only one output. + // doing so causes the cursor to be placed in *global* output coordinates, + // which are not flipped, and happen to be what we want. + None + } +} + +impl NiriInputDevice for VirtualPointer { + fn output(&self, _: &State) -> Option<Output> { + self.output().cloned() + } +} diff --git a/src/input/mod.rs b/src/input/mod.rs index b08fe380..8872df78 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -11,7 +11,7 @@ use niri_ipc::LayoutSwitchTarget; use smithay::backend::input::{ AbsolutePositionEvent, Axis, AxisSource, ButtonState, Device, DeviceCapability, Event, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, GestureSwipeUpdateEvent as _, - InputBackend, InputEvent, KeyState, KeyboardKeyEvent, Keycode, MouseButton, PointerAxisEvent, + InputEvent, KeyState, KeyboardKeyEvent, Keycode, MouseButton, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, ProximityState, Switch, SwitchState, SwitchToggleEvent, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, @@ -28,7 +28,9 @@ use smithay::input::touch::{ DownEvent, GrabStartData as TouchGrabStartData, MotionEvent as TouchMotionEvent, UpEvent, }; use smithay::input::SeatHandler; +use smithay::output::Output; use smithay::utils::{Logical, Point, Rectangle, Transform, SERIAL_COUNTER}; +use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitor; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraint}; use smithay::wayland::tablet_manager::{TabletDescriptor, TabletSeatTrait}; use touch_move_grab::TouchMoveGrab; @@ -42,6 +44,7 @@ use crate::ui::screenshot_ui::ScreenshotUi; use crate::utils::spawning::spawn; use crate::utils::{center, get_monotonic_time, ResizeEdge}; +pub mod backend_ext; pub mod move_grab; pub mod resize_grab; pub mod scroll_tracker; @@ -50,6 +53,8 @@ pub mod swipe_tracker; pub mod touch_move_grab; pub mod touch_resize_grab; +use backend_ext::{NiriInputBackend as InputBackend, NiriInputDevice as _}; + pub const DOUBLE_CLICK_TIME: Duration = Duration::from_millis(400); #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -264,8 +269,10 @@ impl State { where I::Device: 'static, { + let device_output = event.device().output(self); + let device_output = device_output.as_ref(); let (target_geo, keep_ratio, px, transform) = - if let Some(output) = self.niri.output_for_tablet() { + if let Some(output) = device_output.or_else(|| self.niri.output_for_tablet()) { ( self.niri.global_space.output_geometry(output).unwrap(), true, @@ -318,6 +325,18 @@ impl State { Some(pos + target_geo.loc.to_f64()) } + fn is_inhibiting_shortcuts(&self) -> bool { + self.niri + .keyboard_focus + .surface() + .and_then(|surface| { + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .get(surface) + }) + .is_some_and(KeyboardShortcutsInhibitor::is_active) + } + fn on_keyboard<I: InputBackend>(&mut self, event: I::KeyboardKeyEvent) { let comp_mod = self.backend.mod_key(); @@ -342,6 +361,8 @@ impl State { self.hide_cursor_if_needed(); } + let is_inhibiting_shortcuts = self.is_inhibiting_shortcuts(); + let Some(Some(bind)) = self.niri.seat.get_keyboard().unwrap().input( self, event.key_code(), @@ -372,6 +393,7 @@ impl State { *mods, &this.niri.screenshot_ui, this.niri.config.borrow().input.disable_power_key_handling, + is_inhibiting_shortcuts, ) }, ) else { @@ -610,6 +632,19 @@ impl State { }); } } + Action::ToggleKeyboardShortcutsInhibit => { + if let Some(inhibitor) = self.niri.keyboard_focus.surface().and_then(|surface| { + self.niri + .keyboard_shortcuts_inhibiting_surfaces + .get(surface) + }) { + if inhibitor.is_active() { + inhibitor.inactivate(); + } else { + inhibitor.activate(); + } + } + } Action::CloseWindow => { if let Some(mapped) = self.niri.layout.focus() { mapped.toplevel().send_close(); @@ -1731,12 +1766,14 @@ impl State { &mut self, event: I::PointerMotionAbsoluteEvent, ) { - let Some(output_geo) = self.global_bounding_rectangle() else { + let Some(pos) = self.compute_absolute_location(&event, None).or_else(|| { + self.global_bounding_rectangle().map(|output_geo| { + event.position_transformed(output_geo.size) + output_geo.loc.to_f64() + }) + }) else { return; }; - let pos = event.position_transformed(output_geo.size) + output_geo.loc.to_f64(); - let serial = SERIAL_COUNTER.next_serial(); let pointer = self.niri.seat.get_pointer().unwrap(); @@ -2613,14 +2650,13 @@ impl State { ); } - /// Computes the cursor position for the touch event. - /// - /// This function handles the touch output mapping, as well as coordinate transform - fn compute_touch_location<I: InputBackend, E: AbsolutePositionEvent<I>>( + fn compute_absolute_location<I: InputBackend>( &self, - evt: &E, + evt: &impl AbsolutePositionEvent<I>, + fallback_output: Option<&Output>, ) -> Option<Point<f64, Logical>> { - let output = self.niri.output_for_touch()?; + let output = evt.device().output(self); + let output = output.as_ref().or(fallback_output)?; let output_geo = self.niri.global_space.output_geometry(output).unwrap(); let transform = output.current_transform(); let size = transform.invert().transform_size(output_geo.size); @@ -2630,6 +2666,16 @@ impl State { ) } + /// Computes the cursor position for the touch event. + /// + /// This function handles the touch output mapping, as well as coordinate transform + fn compute_touch_location<I: InputBackend>( + &self, + evt: &impl AbsolutePositionEvent<I>, + ) -> Option<Point<f64, Logical>> { + self.compute_absolute_location(evt, self.niri.output_for_touch()) + } + fn on_touch_down<I: InputBackend>(&mut self, evt: I::TouchDownEvent) { let Some(handle) = self.niri.seat.get_touch() else { return; @@ -2780,6 +2826,7 @@ fn should_intercept_key( mods: ModifiersState, screenshot_ui: &ScreenshotUi, disable_power_key_handling: bool, + is_inhibiting_shortcuts: bool, ) -> FilterResult<Option<Bind>> { // Actions are only triggered on presses, release of the key // shouldn't try to intercept anything unless we have marked @@ -2820,6 +2867,10 @@ fn should_intercept_key( repeat: true, cooldown: None, allow_when_locked: false, + // The screenshot UI owns the focus anyway, so this doesn't really matter. + // But logically, nothing can inhibit its actions. Only opening it can be + // inhibited. + allow_inhibiting: false, }); } } @@ -2827,10 +2878,19 @@ fn should_intercept_key( match (final_bind, pressed) { (Some(bind), true) => { - suppressed_keys.insert(key_code); - FilterResult::Intercept(Some(bind)) + if is_inhibiting_shortcuts && bind.allow_inhibiting { + FilterResult::Forward + } else { + suppressed_keys.insert(key_code); + FilterResult::Intercept(Some(bind)) + } } (_, false) => { + // By this point, we know that the key was supressed on press. Even if we're inhibiting + // shortcuts, we should still suppress the release. + // But we don't need to check for shortcuts inhibition here, because + // if it was inhibited on press (forwarded to the client), it wouldn't be suppressed, + // so the release would already have been forwarded at the start of this function. suppressed_keys.remove(&key_code); FilterResult::Intercept(None) } @@ -2870,6 +2930,12 @@ fn find_bind( repeat: true, cooldown: None, allow_when_locked: false, + // In a worst-case scenario, the user has no way to unlock the compositor and a + // misbehaving client has a keyboard shortcuts inhibitor, "jailing" the user. + // The user must always be able to change VTs to recover from such a situation. + // It also makes no sense to inhibit the default power key handling. + // Hardcoded binds must never be inhibited. + allow_inhibiting: false, }); } @@ -3035,6 +3101,7 @@ fn allowed_when_locked(action: &Action) -> bool { | Action::PowerOffMonitors | Action::PowerOnMonitors | Action::SwitchLayout(_) + | Action::ToggleKeyboardShortcutsInhibit ) } @@ -3317,6 +3384,8 @@ pub fn mods_with_finger_scroll_binds(comp_mod: CompositorMod, binds: &Binds) -> #[cfg(test)] mod tests { + use std::cell::Cell; + use super::*; use crate::animation::Clock; @@ -3332,6 +3401,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }]); let comp_mod = CompositorMod::Super; @@ -3339,6 +3409,7 @@ mod tests { let screenshot_ui = ScreenshotUi::new(Clock::default(), Default::default()); let disable_power_key_handling = false; + let is_inhibiting_shortcuts = Cell::new(false); // The key_code we pick is arbitrary, the only thing // that matters is that they are different between cases. @@ -3356,6 +3427,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + is_inhibiting_shortcuts.get(), ) }; @@ -3372,6 +3444,7 @@ mod tests { mods, &screenshot_ui, disable_power_key_handling, + is_inhibiting_shortcuts.get(), ) }; @@ -3452,6 +3525,53 @@ mod tests { // Ensure that no keys are being suppressed. assert!(suppressed_keys.is_empty()); + + // Now test shortcut inhibiting. + + // With inhibited shortcuts, we don't intercept our shortcut. + is_inhibiting_shortcuts.set(true); + + mods = ModifiersState { + logo: true, + ctrl: true, + ..Default::default() + }; + + let filter = close_key_event(&mut suppressed_keys, mods, true); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + let filter = close_key_event(&mut suppressed_keys, mods, false); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + // Toggle it off after pressing the shortcut. + let filter = close_key_event(&mut suppressed_keys, mods, true); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + is_inhibiting_shortcuts.set(false); + + let filter = close_key_event(&mut suppressed_keys, mods, false); + assert!(matches!(filter, FilterResult::Forward)); + assert!(suppressed_keys.is_empty()); + + // Toggle it on after pressing the shortcut. + let filter = close_key_event(&mut suppressed_keys, mods, true); + assert!(matches!( + filter, + FilterResult::Intercept(Some(Bind { + action: Action::CloseWindow, + .. + })) + )); + assert!(suppressed_keys.contains(&close_key_code)); + + is_inhibiting_shortcuts.set(true); + + let filter = close_key_event(&mut suppressed_keys, mods, false); + assert!(matches!(filter, FilterResult::Intercept(None))); + assert!(suppressed_keys.is_empty()); } #[test] @@ -3466,6 +3586,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3476,6 +3597,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3486,6 +3608,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3496,6 +3619,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, Bind { key: Key { @@ -3506,6 +3630,7 @@ mod tests { repeat: true, cooldown: None, allow_when_locked: false, + allow_inhibiting: true, }, ]); diff --git a/src/niri.rs b/src/niri.rs index 16758ab5..c232ab88 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -77,6 +77,9 @@ use smithay::wayland::fractional_scale::FractionalScaleManagerState; use smithay::wayland::idle_inhibit::IdleInhibitManagerState; use smithay::wayland::idle_notify::IdleNotifierState; use smithay::wayland::input_method::{InputMethodManagerState, InputMethodSeat}; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, +}; use smithay::wayland::output::OutputManagerState; use smithay::wayland::pointer_constraints::{with_pointer_constraint, PointerConstraintsState}; use smithay::wayland::pointer_gestures::PointerGesturesState; @@ -131,6 +134,7 @@ use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState; use crate::protocols::output_management::OutputManagementManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManagerState}; +use crate::protocols::virtual_pointer::VirtualPointerManagerState; use crate::pw_utils::{Cast, PipeWire}; #[cfg(feature = "xdp-gnome-screencast")] use crate::pw_utils::{CastSizeChange, CastTarget, PwToNiri}; @@ -252,7 +256,9 @@ pub struct Niri { pub tablet_state: TabletManagerState, pub text_input_state: TextInputManagerState, pub input_method_state: InputMethodManagerState, + pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, pub virtual_keyboard_state: VirtualKeyboardManagerState, + pub virtual_pointer_state: VirtualPointerManagerState, pub pointer_gestures_state: PointerGesturesState, pub relative_pointer_state: RelativePointerManagerState, pub pointer_constraints_state: PointerConstraintsState, @@ -290,6 +296,7 @@ pub struct Niri { pub previously_focused_window: Option<Window>, pub idle_inhibiting_surfaces: HashSet<WlSurface>, pub is_fdo_idle_inhibited: Arc<AtomicBool>, + pub keyboard_shortcuts_inhibiting_surfaces: HashMap<WlSurface, KeyboardShortcutsInhibitor>, pub cursor_manager: CursorManager, pub cursor_texture_cache: CursorTextureCache, @@ -1818,11 +1825,16 @@ impl Niri { InputMethodManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted }); + let keyboard_shortcuts_inhibit_state = + KeyboardShortcutsInhibitState::new::<State>(&display_handle); let virtual_keyboard_state = VirtualKeyboardManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted }); - + let virtual_pointer_state = + VirtualPointerManagerState::new::<State, _>(&display_handle, |client| { + !client.get_data::<ClientState>().unwrap().restricted + }); let foreign_toplevel_state = ForeignToplevelManagerState::new::<State, _>(&display_handle, |client| { !client.get_data::<ClientState>().unwrap().restricted @@ -2014,7 +2026,9 @@ impl Niri { xdg_foreign_state, text_input_state, input_method_state, + keyboard_shortcuts_inhibit_state, virtual_keyboard_state, + virtual_pointer_state, shm_state, output_manager_state, dmabuf_state, @@ -2049,6 +2063,7 @@ impl Niri { previously_focused_window: None, idle_inhibiting_surfaces: HashSet::new(), is_fdo_idle_inhibited: Arc::new(AtomicBool::new(false)), + keyboard_shortcuts_inhibiting_surfaces: HashMap::new(), cursor_manager, cursor_texture_cache: Default::default(), cursor_shape_manager_state, diff --git a/src/protocols/mod.rs b/src/protocols/mod.rs index 3328fb7c..476f24eb 100644 --- a/src/protocols/mod.rs +++ b/src/protocols/mod.rs @@ -3,5 +3,6 @@ pub mod gamma_control; pub mod mutter_x11_interop; pub mod output_management; pub mod screencopy; +pub mod virtual_pointer; pub mod raw; diff --git a/src/protocols/virtual_pointer.rs b/src/protocols/virtual_pointer.rs new file mode 100644 index 00000000..ff3cb3e9 --- /dev/null +++ b/src/protocols/virtual_pointer.rs @@ -0,0 +1,563 @@ +use std::collections::HashSet; +use std::sync::Mutex; + +use smithay::backend::input::{ + AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, ButtonState, Device, + DeviceCapability, Event, InputBackend, PointerAxisEvent, PointerButtonEvent, + PointerMotionAbsoluteEvent, PointerMotionEvent, UnusedEvent, +}; +use smithay::input::pointer::AxisFrame; +use smithay::output::Output; +use smithay::reexports::wayland_protocols_wlr; +use smithay::reexports::wayland_server::protocol::wl_pointer; +use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; +use wayland_backend::protocol::WEnum; +use wayland_protocols_wlr::virtual_pointer::v1::server::{ + zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1, +}; +use zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1; +use zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1; + +const VERSION: u32 = 2; + +pub struct VirtualPointerManagerState { + virtual_pointers: HashSet<ZwlrVirtualPointerV1>, +} + +pub struct VirtualPointerManagerGlobalData { + filter: Box<dyn for<'c> Fn(&'c Client) -> bool + Send + Sync>, |
