From 2f3109b068a1aab46350f9e68032165b3b10b3e7 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Tue, 7 May 2024 00:14:39 +0200 Subject: [PATCH] Switch manual virtual keyboard control to bindings A previous attempt was made to allow manually controlling the virtual keyboard's state by changing the text-input enabled state from the compositor. While this can work well for disabling a virtual keyboard, it's not a great solution to force-enable it. This patch adds a new method of controlling the virtual keyboard visibility by introducing the Enable/Disable/AutoVirtualKeyboard key bindings. While these aren't real keys, binding an action to them allows manual control of the virtual keyboard through a side-channel the user is free to pick themselves. Only modes that have bindings defined can be toggled to, avoiding the necessity to toggle through a state that isn't available. --- catacomb_ipc/src/lib.rs | 26 +++++++++++----- src/catacomb.rs | 67 ++++++++++++++++++++++++++++++++++------- src/config.rs | 4 +-- src/input.rs | 4 +-- src/ipc_server.rs | 16 +++++++--- src/orientation.rs | 24 +++++---------- src/windows/layout.rs | 8 ++--- src/windows/window.rs | 2 +- 8 files changed, 104 insertions(+), 47 deletions(-) diff --git a/catacomb_ipc/src/lib.rs b/catacomb_ipc/src/lib.rs index 4f9243f..8529650 100644 --- a/catacomb_ipc/src/lib.rs +++ b/catacomb_ipc/src/lib.rs @@ -94,7 +94,7 @@ pub enum IpcMessage { #[cfg_attr(feature = "clap", clap(long, short))] mods: Option, /// X11 keysym for this binding. - key: ClapKeysym, + key: Keysym, }, /// Remove a gesture. UnbindGesture { @@ -121,7 +121,7 @@ pub enum IpcMessage { #[cfg_attr(feature = "clap", clap(long))] on_press: bool, /// Base key for this binding. - key: ClapKeysym, + key: Keysym, /// Program this gesture should spawn. program: String, /// Arguments for this gesture's program. @@ -136,7 +136,7 @@ pub enum IpcMessage { #[cfg_attr(feature = "clap", clap(long, short))] mods: Option, /// Base key for this binding. - key: ClapKeysym, + key: Keysym, }, /// Output power management. Dpms { @@ -406,19 +406,31 @@ impl FromStr for Modifiers { } /// Clap wrapper for XKB keysym. -#[derive(Deserialize, Serialize, Copy, Clone, Debug)] -pub struct ClapKeysym(pub u32); +#[derive(Deserialize, Serialize, PartialEq, Eq, Copy, Clone, Debug)] +pub enum Keysym { + EnableVirtualKeyboard, + DisableVirtualKeyboard, + AutoVirtualKeyboard, + Xkb(u32), +} #[cfg(feature = "clap")] -impl FromStr for ClapKeysym { +impl FromStr for Keysym { type Err = ClapError; fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "enablevirtualkeyboard" => return Ok(Self::EnableVirtualKeyboard), + "disablevirtualkeyboard" => return Ok(Self::DisableVirtualKeyboard), + "autovirtualkeyboard" => return Ok(Self::AutoVirtualKeyboard), + _ => (), + } + match xkb::keysym_from_name(s, xkb::KEYSYM_NO_FLAGS).raw() { keysyms::KEY_NoSymbol => { Err(ClapError::raw(ClapErrorKind::InvalidValue, format!("invalid keysym {s:?}"))) }, - keysym => Ok(Self(keysym)), + keysym => Ok(Self::Xkb(keysym)), } } } diff --git a/src/catacomb.rs b/src/catacomb.rs index abe6d4e..10a5493 100644 --- a/src/catacomb.rs +++ b/src/catacomb.rs @@ -7,7 +7,7 @@ use std::{cmp, env}; use _decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; use _server_decoration::server::org_kde_kwin_server_decoration_manager::Mode as ManagerMode; -use catacomb_ipc::Orientation; +use catacomb_ipc::{Keysym, Orientation}; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::renderer::ImportDma; use smithay::input::keyboard::XkbConfig; @@ -37,7 +37,7 @@ use smithay::wayland::fractional_scale::{ use smithay::wayland::idle_inhibit::{IdleInhibitHandler, IdleInhibitManagerState}; use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState}; use smithay::wayland::input_method::{ - InputMethodHandler, InputMethodManagerState, InputMethodSeat, PopupSurface as ImeSurface, + InputMethodHandler, InputMethodManagerState, PopupSurface as ImeSurface, }; use smithay::wayland::keyboard_shortcuts_inhibit::{ KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, @@ -93,7 +93,7 @@ use crate::protocols::single_pixel_buffer::SinglePixelBufferState; use crate::udev::Udev; use crate::windows::surface::Surface; use crate::windows::Windows; -use crate::{delegate_screencopy, delegate_single_pixel_buffer, ipc_server, trace_error}; +use crate::{daemon, delegate_screencopy, delegate_single_pixel_buffer, ipc_server, trace_error}; /// Time before xdg_activation tokens are invalidated. const ACTIVATION_TIMEOUT: Duration = Duration::from_secs(10); @@ -361,7 +361,7 @@ impl Catacomb { // Update surface focus. let focus = self.windows.focus().map(|(surface, _)| surface); if focus != self.last_focus { - self.last_focus = focus.clone(); + self.last_focus.clone_from(&focus); self.focus(focus); } @@ -472,18 +472,55 @@ impl Catacomb { /// Toggle IME force enable/disable. pub fn toggle_ime_override(&mut self) { - self.ime_override = match self.ime_override { - None => Some(false), - Some(false) => Some(true), - Some(true) => None, + // Find next state and corresponding action. + // + // If one state doesn't have an action, it is skipped. If two states don't have + // an action, we don't do anything. + let VkActions { enable, disable, auto } = self.vk_actions(); + let (new_override, (program, args)) = match (self.ime_override, enable, disable, auto) { + (None, _, Some(disable), _) | (Some(true), _, Some(disable), None) => { + (Some(false), disable) + }, + (None, Some(enable), ..) | (Some(false), Some(enable), ..) => (Some(true), enable), + (Some(false), _, _, Some(auto)) | (Some(true), _, _, Some(auto)) => (None, auto), + // Ignore toggle if there are less than two states to toggle between. + _ => { + println!("NOTHING TO TOGGLE TO"); + return; + }, }; + // Execute binding to toggle virtual keyboard override. + if let Err(err) = daemon::spawn(program, args) { + error!("Failed to update virtual keyboard state ({program} {args:?}): {err}"); + } + + self.ime_override = new_override; + // Ensure gesture handle is updated. self.windows.set_ime_override(self.ime_override); + } - // Update IME state. - let cseat = self.seat.clone(); - cseat.input_method().set_active(self, None, self.ime_override); + /// Get actions for setting virtual keyboard state to enabled/disabled/auto. + fn vk_actions(&self) -> VkActions<'_> { + let mut actions = VkActions::default(); + + for binding in &self.key_bindings { + let action = (&binding.program, &binding.arguments); + match binding.key { + Keysym::EnableVirtualKeyboard => actions.enable = Some(action), + Keysym::DisableVirtualKeyboard => actions.disable = Some(action), + Keysym::AutoVirtualKeyboard => actions.auto = Some(action), + _ => (), + } + + // Stop once all actions were found. + if actions.enable.is_some() && actions.disable.is_some() && actions.auto.is_some() { + break; + } + } + + actions } } @@ -913,3 +950,11 @@ impl FramePacer { Some(prediction + PREDICTION_PADDING) } } + +/// Programs for controlling virtual keyboard state. +#[derive(Default)] +struct VkActions<'a> { + enable: Option<(&'a String, &'a Vec)>, + disable: Option<(&'a String, &'a Vec)>, + auto: Option<(&'a String, &'a Vec)>, +} diff --git a/src/config.rs b/src/config.rs index b0a7f5a..e0a1a63 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ //! Compositor configuration. -use catacomb_ipc::{AppIdMatcher, GestureSector, Modifiers}; +use catacomb_ipc::{AppIdMatcher, GestureSector, Keysym, Modifiers}; /// User-defined gesture action. #[derive(Debug)] @@ -23,7 +23,7 @@ pub enum GestureBindingAction { pub struct KeyBinding { pub app_id: AppIdMatcher, pub mods: Modifiers, - pub key: u32, + pub key: Keysym, pub program: String, pub arguments: Vec, pub on_press: bool, diff --git a/src/input.rs b/src/input.rs index 2075966..e0d6229 100644 --- a/src/input.rs +++ b/src/input.rs @@ -2,7 +2,7 @@ use std::time::{Duration, Instant}; -use catacomb_ipc::{GestureSector, Modifiers}; +use catacomb_ipc::{GestureSector, Keysym, Modifiers}; use smithay::backend::input::{ AbsolutePositionEvent, ButtonState, Event, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, MouseButton, PointerButtonEvent, TouchEvent as _, TouchSlot, @@ -841,7 +841,7 @@ impl Catacomb { let mut filter_result = FilterResult::Forward; let pressed = state == KeyState::Pressed; for key_binding in &catacomb.key_bindings { - if key_binding.key == raw_keysym + if key_binding.key == Keysym::Xkb(raw_keysym) && key_binding.mods == mods && key_binding.app_id.matches(active_app.as_ref()) && key_binding.on_press == pressed diff --git a/src/ipc_server.rs b/src/ipc_server.rs index b17d858..cf6e7db 100644 --- a/src/ipc_server.rs +++ b/src/ipc_server.rs @@ -6,7 +6,7 @@ use std::io::{BufRead, BufReader, Write}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; -use catacomb_ipc::{AppIdMatcher, DpmsState, IpcMessage, WindowScale}; +use catacomb_ipc::{AppIdMatcher, DpmsState, IpcMessage, Keysym, WindowScale}; use smithay::reexports::calloop::LoopHandle; use tracing::{error, warn}; @@ -104,7 +104,16 @@ fn handle_message(buffer: &mut String, mut stream: UnixStream, catacomb: &mut Ca }, }; - let action = GestureBindingAction::Key((key.0, mods.unwrap_or_default())); + // Ignore custom keysyms like virtual keyboard enable/disable. + let key = match key { + Keysym::Xkb(key) => key, + _ => { + warn!("ignoring invalid ipc message: gestures must be bound to real keysyms"); + return; + }, + }; + + let action = GestureBindingAction::Key((key, mods.unwrap_or_default())); let gesture = GestureBinding { app_id, start, end, action }; catacomb.touch_state.user_gestures.push(gesture); }, @@ -127,14 +136,13 @@ fn handle_message(buffer: &mut String, mut stream: UnixStream, catacomb: &mut Ca app_id, program, arguments, + key, mods: mods.unwrap_or_default(), - key: key.0, }; catacomb.key_bindings.push(binding); }, IpcMessage::UnbindKey { app_id, mods, key } => { let mods = mods.unwrap_or_default(); - let key = key.0; catacomb.key_bindings.retain(|binding| { binding.app_id.base() != app_id || binding.key != key || binding.mods != mods diff --git a/src/orientation.rs b/src/orientation.rs index f5fd577..38dec8b 100644 --- a/src/orientation.rs +++ b/src/orientation.rs @@ -25,13 +25,9 @@ const DEADZONE: f32 = 5.; pub trait AccelerometerSource { /// Subscribe to orientation change events. - fn subscribe<'a, F: 'a>( - self, - loop_handle: &LoopHandle<'a, Catacomb>, - fun: F, - ) -> RegistrationToken + fn subscribe<'a, F>(self, loop_handle: &LoopHandle<'a, Catacomb>, fun: F) -> RegistrationToken where - F: FnMut(Orientation, &mut Catacomb); + F: FnMut(Orientation, &mut Catacomb) + 'a; } /// Platform-independent accelerometer implementation. @@ -49,13 +45,9 @@ impl Accelerometer { } impl AccelerometerSource for Accelerometer { - fn subscribe<'a, F: 'a>( - self, - loop_handle: &LoopHandle<'a, Catacomb>, - fun: F, - ) -> RegistrationToken + fn subscribe<'a, F>(self, loop_handle: &LoopHandle<'a, Catacomb>, fun: F) -> RegistrationToken where - F: FnMut(Orientation, &mut Catacomb), + F: FnMut(Orientation, &mut Catacomb) + 'a, { match self { Accelerometer::Sensor(accelerometer) => accelerometer.subscribe(loop_handle, fun), @@ -152,13 +144,13 @@ impl SensorAccelerometer { } impl AccelerometerSource for SensorAccelerometer { - fn subscribe<'a, F: 'a>( + fn subscribe<'a, F>( mut self, loop_handle: &LoopHandle<'a, Catacomb>, mut fun: F, ) -> RegistrationToken where - F: FnMut(Orientation, &mut Catacomb), + F: FnMut(Orientation, &mut Catacomb) + 'a, { loop_handle .insert_source(Timer::immediate(), move |_, _, catacomb| { @@ -182,13 +174,13 @@ impl AccelerometerSource for SensorAccelerometer { pub struct DummyAccelerometer; impl AccelerometerSource for DummyAccelerometer { - fn subscribe<'a, F: 'a>( + fn subscribe<'a, F>( self, loop_handle: &LoopHandle<'a, Catacomb>, mut fun: F, ) -> RegistrationToken where - F: FnMut(Orientation, &mut Catacomb), + F: FnMut(Orientation, &mut Catacomb) + 'a, { loop_handle .insert_source(Timer::immediate(), move |_, _, catacomb| { diff --git a/src/windows/layout.rs b/src/windows/layout.rs index 13f4280..d213427 100644 --- a/src/windows/layout.rs +++ b/src/windows/layout.rs @@ -65,18 +65,18 @@ impl Layouts { Transaction::Primary(position) => { if let Some(new_layout) = self.layouts.get(position.index) { if position.secondary { - layout.primary = new_layout.secondary.clone(); + layout.primary.clone_from(&new_layout.secondary); } else { - layout.primary = new_layout.primary.clone(); + layout.primary.clone_from(&new_layout.primary); } } }, Transaction::Secondary(position) => { if let Some(new_layout) = self.layouts.get(position.index) { if position.secondary { - layout.secondary = new_layout.secondary.clone(); + layout.secondary.clone_from(&new_layout.secondary); } else { - layout.secondary = new_layout.primary.clone(); + layout.secondary.clone_from(&new_layout.primary); } } }, diff --git a/src/windows/window.rs b/src/windows/window.rs index a9937f3..7ab079c 100644 --- a/src/windows/window.rs +++ b/src/windows/window.rs @@ -704,7 +704,7 @@ impl Window { ) -> Option> { if self.surface.surface() == parent { // Inherit parent properties. - popup.app_id = self.app_id.clone(); + popup.app_id.clone_from(&self.app_id); popup.visible = self.visible; popup.scale = self.scale;