From d0c8e1bd319fd0c447eb5d7e6dac16610cf87085 Mon Sep 17 00:00:00 2001 From: Uli Schlachter Date: Sun, 23 May 2021 15:08:12 +0200 Subject: [PATCH] x11: Add support for getting clipboard contents This commit implements X11 clipboard transfers as specified in ICCCM. This allows to get the contents of the clipboard. X11/ICCM call the underlying mechanism "selections". This works with ConvertSelection requests. The X11 server forwards these requests to the selection owner which then uses SendEvent requests to answer. Thus, this requires some way to blockingly wait for events. For this purpose, a FIFO queue (VecDeque) of pending events is introduced. When waiting for the "right" event, "wrong" events are pushed to this queue for later processing. Doing selection transfers requires an up-to-date-ish X11 timestamp. Thus, the Application now tracks a timestamp and updates it whenever it gets a newer timestamp. As an unrelated refactor, this changes the X11 screen number to be saved as usize instead of i32. This saves a couple of unnecessary casts. I didn't want to do this too much in this commit, so screen_num() still returns i32 instead of usize, even though I think it should be an usize. Besides the above, the actual selection transfer is fully contained in clipboard.rs. The basic steps for getting the selection contents are: - create an invisible window (used for the reply) - send a ConvertSelection request with this window - wait for a SelectionNotify event (at this point, the selection owner set a property on the window) - get the contents of the property from the window - in case the selection contents are larger than allowed for window properties, the property as type INCR. This indicates an "incremental transfer" which works as follows: - every time the property is deleted, the selection owner creates a new property with the next chunk of the property - thus, we have to wait for PropertyNotify events for our special window and react to them by getting the next piece of data Signed-off-by: Uli Schlachter --- CHANGELOG.md | 2 + druid-shell/src/platform/x11/application.rs | 55 +++-- druid-shell/src/platform/x11/clipboard.rs | 242 ++++++++++++++++++-- 3 files changed, 270 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a607a41df5..ebc0d29e6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ You can find its changes [documented below](#070---2021-01-01). - X11 backend now supports custom cursors ([#1801] by [@psychon]) - X11: Add support for transparent windows ([#1803] by [@psychon]) - `has_focus` method on `WidgetPod` ([#1825] by [@ForLoveOfCats]) +- x11: Add support for getting clipboard contents ([#1805] by [@psychon]) ### Changed @@ -732,6 +733,7 @@ Last release without a changelog :( [#1801]: https://github.com/linebender/druid/pull/1800 [#1802]: https://github.com/linebender/druid/pull/1802 [#1803]: https://github.com/linebender/druid/pull/1803 +[#1805]: https://github.com/linebender/druid/pull/1805 [#1820]: https://github.com/linebender/druid/pull/1820 [#1825]: https://github.com/linebender/druid/pull/1825 diff --git a/druid-shell/src/platform/x11/application.rs b/druid-shell/src/platform/x11/application.rs index aa380aaa00..d14152c5ca 100644 --- a/druid-shell/src/platform/x11/application.rs +++ b/druid-shell/src/platform/x11/application.rs @@ -14,8 +14,8 @@ //! X11 implementation of features at the application scope. -use std::cell::RefCell; -use std::collections::HashMap; +use std::cell::{Cell, RefCell}; +use std::collections::{HashMap, VecDeque}; use std::convert::{TryFrom, TryInto}; use std::os::unix::io::RawFd; use std::rc::Rc; @@ -27,7 +27,7 @@ use x11rb::protocol::present::ConnectionExt as _; use x11rb::protocol::render::{self, ConnectionExt as _, Pictformat}; use x11rb::protocol::xfixes::ConnectionExt as _; use x11rb::protocol::xproto::{ - self, ConnectionExt, CreateWindowAux, EventMask, Visualtype, WindowClass, + self, ConnectionExt, CreateWindowAux, EventMask, Timestamp, Visualtype, WindowClass, }; use x11rb::protocol::Event; use x11rb::resource_manager::Database as ResourceDb; @@ -58,6 +58,8 @@ pub(crate) struct Application { root_visual_type: Visualtype, /// The visual for windows with transparent backgrounds, if supported argb_visual_type: Option, + /// Pending events that need to be handled later + pending_events: Rc>>, /// The X11 resource database used to query dpi. pub(crate) rdb: Rc, @@ -72,7 +74,7 @@ pub(crate) struct Application { /// In practice multiple physical monitor drawing areas are present on a single screen. /// This is achieved via various X server extensions (XRandR/Xinerama/TwinView), /// with XRandR seeming like the best choice. - screen_num: i32, // Needs a container when no longer const + screen_num: usize, // Needs a container when no longer const /// The X11 window id of this `Application`. /// /// This is an input-only non-visual X11 window that is created first during initialization, @@ -93,6 +95,8 @@ pub(crate) struct Application { present_opcode: Option, /// Support for the render extension in at least version 0.5? render_argb32_pictformat_cursor: Option, + /// Newest timestamp that we received + timestamp: Rc>, } /// The mutable `Application` state. @@ -127,7 +131,7 @@ impl Application { let (conn, screen_num) = XCBConnection::connect(None)?; let rdb = Rc::new(ResourceDb::new_from_default(&conn)?); let connection = Rc::new(conn); - let window_id = Application::create_event_window(&connection, screen_num as i32)?; + let window_id = Application::create_event_window(&connection, screen_num)?; let state = Rc::new(RefCell::new(State { quitting: false, windows: HashMap::new(), @@ -208,7 +212,7 @@ impl Application { Ok(Application { connection, rdb, - screen_num: screen_num as i32, + screen_num, window_id, state, idle_read, @@ -217,8 +221,10 @@ impl Application { present_opcode, root_visual_type, argb_visual_type, + pending_events: Default::default(), marker: std::marker::PhantomData, render_argb32_pictformat_cursor, + timestamp: Rc::new(Cell::new(x11rb::CURRENT_TIME)), }) } @@ -275,12 +281,12 @@ impl Application { self.render_argb32_pictformat_cursor } - fn create_event_window(conn: &Rc, screen_num: i32) -> Result { + fn create_event_window(conn: &Rc, screen_num: usize) -> Result { let id = conn.generate_id()?; let setup = conn.setup(); let screen = setup .roots - .get(screen_num as usize) + .get(screen_num) .ok_or_else(|| anyhow!("invalid screen num: {}", screen_num))?; // Create the actual window @@ -341,7 +347,7 @@ impl Application { #[inline] pub(crate) fn screen_num(&self) -> i32 { - self.screen_num + self.screen_num as _ } #[inline] @@ -373,6 +379,21 @@ impl Application { /// Returns `Ok(true)` if we want to exit the main loop. fn handle_event(&self, ev: &Event) -> Result { + if ev.server_generated() { + // Update our latest timestamp + let timestamp = match ev { + Event::KeyPress(ev) => ev.time, + Event::KeyRelease(ev) => ev.time, + Event::ButtonPress(ev) => ev.time, + Event::ButtonRelease(ev) => ev.time, + Event::MotionNotify(ev) => ev.time, + Event::EnterNotify(ev) => ev.time, + Event::LeaveNotify(ev) => ev.time, + Event::PropertyNotify(ev) => ev.time, + _ => self.timestamp.get(), + }; + self.timestamp.set(timestamp); + } match ev { // NOTE: When adding handling for any of the following events, // there must be a check against self.window_id @@ -512,10 +533,15 @@ impl Application { self.connection.flush()?; + // Deal with pending events + let mut event = self.pending_events.borrow_mut().pop_front(); + // Before we poll on the connection's file descriptor, check whether there are any // events ready. It could be that XCB has some events in its internal buffers because // of something that happened during the idle loop. - let mut event = self.connection.poll_for_event()?; + if event.is_none() { + event = self.connection.poll_for_event()?; + } if event.is_none() { poll_with_timeout( @@ -605,9 +631,12 @@ impl Application { } pub fn clipboard(&self) -> Clipboard { - // TODO(x11/clipboard): implement Application::clipboard - tracing::warn!("Application::clipboard is currently unimplemented for X11 platforms."); - Clipboard {} + Clipboard::new( + Rc::clone(&self.connection), + self.screen_num, + Rc::clone(&self.pending_events), + Rc::clone(&self.timestamp), + ) } pub fn get_locale() -> String { diff --git a/druid-shell/src/platform/x11/clipboard.rs b/druid-shell/src/platform/x11/clipboard.rs index 88fcc4e9bf..536f32c03a 100644 --- a/druid-shell/src/platform/x11/clipboard.rs +++ b/druid-shell/src/platform/x11/clipboard.rs @@ -14,13 +14,56 @@ //! Interactions with the system pasteboard on X11. +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; +use std::rc::Rc; + +use x11rb::connection::Connection; +use x11rb::errors::ReplyOrIdError; +use x11rb::protocol::xproto::{ + AtomEnum, ChangeWindowAttributesAux, ConnectionExt, EventMask, GetPropertyReply, + GetPropertyType, Property, Timestamp, WindowClass, +}; +use x11rb::protocol::Event; +use x11rb::xcb_ffi::XCBConnection; + use crate::clipboard::{ClipboardFormat, FormatId}; -use tracing::warn; +use tracing::{debug, warn}; + +// We can pick an arbitrary atom that is used for the transfer. This is our pick. +const TRANSFER_ATOM: AtomEnum = AtomEnum::CUT_BUFFE_R4; + +const STRING_TARGETS: [&str; 5] = [ + "UTF8_STRING", + "TEXT", + "STRING", + "text/plain;charset=utf-8", + "text/plain", +]; -#[derive(Debug, Clone, Default)] -pub struct Clipboard; +#[derive(Debug, Clone)] +pub struct Clipboard { + connection: Rc, + screen_num: usize, + event_queue: Rc>>, + timestamp: Rc>, +} impl Clipboard { + pub(crate) fn new( + connection: Rc, + screen_num: usize, + event_queue: Rc>>, + timestamp: Rc>, + ) -> Self { + Clipboard { + connection, + screen_num, + event_queue, + timestamp, + } + } + pub fn put_string(&mut self, _s: impl AsRef) { // TODO(x11/clipboard): implement Clipboard::put_string warn!("Clipboard::put_string is currently unimplemented for X11 platforms."); @@ -32,26 +75,193 @@ impl Clipboard { } pub fn get_string(&self) -> Option { - // TODO(x11/clipboard): implement Clipboard::get_string - warn!("Clipboard::set_string is currently unimplemented for X11 platforms."); + for target in STRING_TARGETS.iter() { + if let Some(data) = self.get_format(target) { + return String::from_utf8(data).ok(); + } + } None } - pub fn preferred_format(&self, _formats: &[FormatId]) -> Option { - // TODO(x11/clipboard): implement Clipboard::preferred_format - warn!("Clipboard::preferred_format is currently unimplemented for X11 platforms."); - None + pub fn preferred_format(&self, formats: &[FormatId]) -> Option { + let available = self.available_type_names(); + formats + .iter() + .find(|f1| available.iter().any(|f2| *f1 == f2)) + .copied() } - pub fn get_format(&self, _format: FormatId) -> Option> { - // TODO(x11/clipboard): implement Clipboard::get_format - warn!("Clipboard::get_format is currently unimplemented for X11 platforms."); - None + pub fn get_format(&self, format: FormatId) -> Option> { + self.do_transfer(format, |prop| prop.value) } + #[allow(clippy::needless_collect)] pub fn available_type_names(&self) -> Vec { - // TODO(x11/clipboard): implement Clipboard::available_type_names - warn!("Clipboard::available_type_names is currently unimplemented for X11 platforms."); - vec![] + let requests = self + .do_transfer("TARGETS", |prop| { + prop.value32() + .map(|iter| iter.collect()) + .unwrap_or_default() + }) + .unwrap_or_default() + .into_iter() + .filter_map(|atom| self.connection.get_atom_name(atom).ok()) + .collect::>(); + // We first send all requests above and then fetch the replies with only one round-trip to + // the X11 server. Hence, the collect() above is not unnecessary! + requests + .into_iter() + .filter_map(|req| req.reply().ok()) + .filter_map(|reply| String::from_utf8(reply.name).ok()) + .collect() + } + + fn do_transfer(&self, format: FormatId, converter: F) -> Option> + where + R: Clone, + F: FnMut(GetPropertyReply) -> Vec, + { + match self.do_transfer_impl(format, converter) { + Ok(result) => result, + Err(error) => { + warn!("Error in Clipboard::do_transfer: {:?}", error); + None + } + } + } + + fn do_transfer_impl( + &self, + format: FormatId, + mut converter: F, + ) -> Result>, ReplyOrIdError> + where + R: Clone, + F: FnMut(GetPropertyReply) -> Vec, + { + debug!("Getting clipboard contents in format {}", format); + + let conn = &*self.connection; + let (format_atom, clipboard_atom, incr_atom) = { + let format = conn.intern_atom(false, format.as_bytes())?; + let clipboard = conn.intern_atom(false, b"CLIPBOARD")?; + let incr = conn.intern_atom(false, b"INCR")?; + ( + format.reply()?.atom, + clipboard.reply()?.atom, + incr.reply()?.atom, + ) + }; + + // Create a window for the transfer + let window = WindowContainer::new(conn, self.screen_num)?; + + conn.convert_selection( + window.window, + clipboard_atom, + format_atom, + TRANSFER_ATOM, + self.timestamp.get(), + )?; + + // Now wait for the selection notify event + conn.flush()?; + let notify = loop { + match conn.wait_for_event()? { + Event::SelectionNotify(notify) if notify.requestor == window.window => { + break notify + } + event => self.event_queue.borrow_mut().push_back(event), + } + }; + + if notify.property == x11rb::NONE { + // Selection is empty + debug!("Selection transfer was rejected"); + return Ok(None); + } + + conn.change_window_attributes( + window.window, + &ChangeWindowAttributesAux::default().event_mask(EventMask::PROPERTY_CHANGE), + )?; + + let property = conn + .get_property( + true, + window.window, + TRANSFER_ATOM, + GetPropertyType::ANY, + 0, + u32::MAX, + )? + .reply()?; + + if property.type_ != incr_atom { + debug!("Got selection contents directly"); + return Ok(Some(converter(property))); + } + + // The above GetProperty with delete=true indicated that the INCR transfer starts + // now, wait for the property notifies + debug!("Doing an INCR transfer for the selection"); + conn.flush()?; + let mut value = Vec::new(); + loop { + match conn.wait_for_event()? { + Event::PropertyNotify(notify) + if (notify.window, notify.state) == (window.window, Property::NEW_VALUE) => + { + let property = conn + .get_property( + true, + window.window, + TRANSFER_ATOM, + GetPropertyType::ANY, + 0, + u32::MAX, + )? + .reply()?; + if property.value.is_empty() { + debug!("INCR transfer finished"); + return Ok(Some(value)); + } else { + value.extend_from_slice(&converter(property)); + } + } + event => self.event_queue.borrow_mut().push_back(event), + } + } + } +} + +struct WindowContainer<'a> { + window: u32, + conn: &'a XCBConnection, +} + +impl<'a> WindowContainer<'a> { + fn new(conn: &'a XCBConnection, screen_num: usize) -> Result { + let window = conn.generate_id()?; + conn.create_window( + x11rb::COPY_DEPTH_FROM_PARENT, + window, + conn.setup().roots[screen_num].root, + 0, + 0, + 1, + 1, + 0, + WindowClass::INPUT_OUTPUT, + x11rb::COPY_FROM_PARENT, + &Default::default(), + )?; + Ok(WindowContainer { window, conn }) + } +} + +impl Drop for WindowContainer<'_> { + fn drop(&mut self) { + let _ = self.conn.destroy_window(self.window); } }