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); } }