Don't use MULTIPLE target atom for getting selections

The ICCCM claims this is a "required" target for selection owners,
however several GTK clients (zenity, winecfg) don't seem to support it.
(So much for required.) Just manually grab all the supported targets
individually from the selection owner instead.
Fix for #50
This commit is contained in:
Shawn Wallace 2024-09-15 01:28:32 -04:00
parent 402a764a11
commit b962a0f33b
3 changed files with 200 additions and 212 deletions

View file

@ -4,7 +4,7 @@ use selection::{SelectionData, SelectionTarget};
use crate::server::WindowAttributes; use crate::server::WindowAttributes;
use bitflags::bitflags; use bitflags::bitflags;
use log::{debug, trace, warn}; use log::{debug, trace, warn};
use std::ffi::{CString, CStr}; use std::ffi::CString;
use std::os::fd::{AsRawFd, BorrowedFd}; use std::os::fd::{AsRawFd, BorrowedFd};
use std::sync::Arc; use std::sync::Arc;
use xcb::{x, Xid, XidNew}; use xcb::{x, Xid, XidNew};
@ -405,16 +405,6 @@ impl XState {
} }
} }
fn get_atom_name(&self, atom: x::Atom) -> String {
match self
.connection
.wait_for_reply(self.connection.send_request(&x::GetAtomName { atom }))
{
Ok(reply) => reply.name().to_string(),
Err(err) => format!("<error getting atom name: {err:?}> {atom:?}"),
}
}
fn get_window_attributes(&self, window: x::Window) -> XResult<WindowAttributes> { fn get_window_attributes(&self, window: x::Window) -> XResult<WindowAttributes> {
let geometry = self.connection.send_request(&x::GetGeometry { let geometry = self.connection.send_request(&x::GetGeometry {
drawable: x::Drawable::Window(window), drawable: x::Drawable::Window(window),
@ -637,7 +627,7 @@ impl XState {
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
debug!( debug!(
"changed property {:?} for {:?}", "changed property {:?} for {:?}",
self.get_atom_name(event.atom()), get_atom_name(&self.connection, event.atom()),
window window
); );
} }
@ -665,6 +655,7 @@ xcb::atoms_struct! {
pub utf8_string => b"UTF8_STRING" only_if_exists = false, pub utf8_string => b"UTF8_STRING" only_if_exists = false,
pub clipboard => b"CLIPBOARD" only_if_exists = false, pub clipboard => b"CLIPBOARD" only_if_exists = false,
pub targets => b"TARGETS" only_if_exists = false, pub targets => b"TARGETS" only_if_exists = false,
pub save_targets => b"SAVE_TARGETS" only_if_exists = false,
pub multiple => b"MULTIPLE" only_if_exists = false, pub multiple => b"MULTIPLE" only_if_exists = false,
pub timestamp => b"TIMESTAMP" only_if_exists = false, pub timestamp => b"TIMESTAMP" only_if_exists = false,
pub selection_reply => b"_selection_reply" only_if_exists = false, pub selection_reply => b"_selection_reply" only_if_exists = false,
@ -864,3 +855,10 @@ impl super::FromServerState<Arc<xcb::Connection>> for Atoms {
state.atoms.as_ref().unwrap().clone() state.atoms.as_ref().unwrap().clone()
} }
} }
fn get_atom_name(connection: &xcb::Connection, atom: x::Atom) -> String {
match connection.wait_for_reply(connection.send_request(&x::GetAtomName { atom })) {
Ok(reply) => reply.name().to_string(),
Err(err) => format!("<error getting atom name: {err:?}> {atom:?}"),
}
}

View file

@ -1,4 +1,4 @@
use super::XState; use super::{get_atom_name, XState};
use crate::server::ForeignSelection; use crate::server::ForeignSelection;
use crate::{MimeTypeData, RealServerState}; use crate::{MimeTypeData, RealServerState};
use log::{debug, trace, warn}; use log::{debug, trace, warn};
@ -43,12 +43,28 @@ impl MimeTypeData for SelectionTarget {
} }
} }
enum MimeTypes {
Temporary {
/// Temporary mime data, being built
data: Vec<SelectionTarget>,
/// Mime types we still need to receive feedback on
/// 2nd field is the destination property
to_grab: Vec<(SelectionTargetId, x::Atom)>,
},
/// Done grabbing mime data
Complete(Rc<Vec<SelectionTarget>>),
}
impl Default for MimeTypes {
fn default() -> Self {
Self::Complete(Default::default())
}
}
#[derive(Default)] #[derive(Default)]
pub(crate) struct SelectionData { pub(crate) struct SelectionData {
clear_time: Option<u32>, clear_time: Option<u32>,
// Selection ID and destination atom mime_types: MimeTypes,
tmp_mimes: Vec<(SelectionTargetId, x::Atom)>,
mime_types: Rc<Vec<SelectionTarget>>,
foreign_data: Option<ForeignSelection>, foreign_data: Option<ForeignSelection>,
} }
@ -100,7 +116,7 @@ impl XState {
}) })
.collect(); .collect();
self.selection_data.mime_types = Rc::new(types); self.selection_data.mime_types = MimeTypes::Complete(Rc::new(types));
self.selection_data.foreign_data = Some(selection); self.selection_data.foreign_data = Some(selection);
trace!("Clipboard set from Wayland"); trace!("Clipboard set from Wayland");
} }
@ -111,11 +127,13 @@ impl XState {
server_state: &mut RealServerState, server_state: &mut RealServerState,
) -> bool { ) -> bool {
match event { match event {
// Someone else is the clipboard owner - get the data from them and then reestablish
// ourselves as the owner
xcb::Event::X(x::Event::SelectionClear(e)) => { xcb::Event::X(x::Event::SelectionClear(e)) => {
if e.selection() != self.atoms.clipboard { if e.selection() != self.atoms.clipboard {
warn!( warn!(
"Got SelectionClear for unexpected atom {}, ignoring", "Got SelectionClear for unexpected atom {}, ignoring",
self.get_atom_name(e.selection()) get_atom_name(&self.connection, e.selection())
); );
return true; return true;
} }
@ -141,12 +159,17 @@ impl XState {
match e.target() { match e.target() {
x if x == self.atoms.targets => self.handle_target_list(e.property()), x if x == self.atoms.targets => self.handle_target_list(e.property()),
x if x == self.atoms.multiple => self.handle_new_clipboard_data(server_state), atom => self.handle_clipboard_data(atom),
atom => { }
warn!(
"unexpected SelectionNotify type: {}", if let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types
self.get_atom_name(atom) {
) if to_grab.is_empty() {
let data = Rc::new(std::mem::take(data));
self.selection_data.mime_types = MimeTypes::Complete(data.clone());
self.set_clipboard_owner(self.selection_data.clear_time.unwrap());
server_state.set_copy_paste_source(data);
trace!("Clipboard set from X11");
} }
} }
} }
@ -171,7 +194,7 @@ impl XState {
let success = || send_notify(e.property()); let success = || send_notify(e.property());
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
let target = self.get_atom_name(e.target()); let target = get_atom_name(&self.connection, e.target());
debug!("Got selection request for target {target}"); debug!("Got selection request for target {target}");
} }
@ -181,14 +204,15 @@ impl XState {
return true; return true;
} }
let MimeTypes::Complete(mime_data) = &self.selection_data.mime_types else {
warn!("Got selection request, but mime data is incomplete");
refuse();
return true;
};
match e.target() { match e.target() {
x if x == self.atoms.targets => { x if x == self.atoms.targets => {
let atoms: Box<[x::Atom]> = self let atoms: Box<[x::Atom]> = mime_data.iter().map(|t| t.id.atom).collect();
.selection_data
.mime_types
.iter()
.map(|t| t.id.atom)
.collect();
self.connection self.connection
.send_and_check_request(&x::ChangeProperty { .send_and_check_request(&x::ChangeProperty {
@ -203,14 +227,9 @@ impl XState {
success(); success();
} }
other => { other => {
let Some(target) = self let Some(target) = mime_data.iter().find(|t| t.id.atom == other) else {
.selection_data
.mime_types
.iter()
.find(|t| t.id.atom == other)
else {
if log::log_enabled!(log::Level::Debug) { if log::log_enabled!(log::Level::Debug) {
let name = self.get_atom_name(other); let name = get_atom_name(&self.connection, other);
debug!("refusing selection request because given atom could not be found ({})", name); debug!("refusing selection request because given atom could not be found ({})", name);
} }
refuse(); refuse();
@ -272,127 +291,123 @@ impl XState {
.unwrap(); .unwrap();
let targets: &[x::Atom] = reply.value(); let targets: &[x::Atom] = reply.value();
let target_props: Box<[x::Atom]> = targets if log::log_enabled!(log::Level::Debug) {
let targets_str: Vec<String> = targets
.iter()
.map(|t| get_atom_name(&self.connection, *t))
.collect();
debug!("got targets: {targets_str:?}");
}
let to_grab = targets
.iter() .iter()
.copied() .copied()
.filter(|atom| ![self.atoms.targets, self.atoms.multiple].contains(atom)) .filter(|atom| {
![
self.atoms.targets,
self.atoms.multiple,
self.atoms.save_targets,
]
.contains(atom)
})
.enumerate() .enumerate()
.flat_map(|(idx, target)| { .map(|(idx, target_atom)| {
let name = [b"dest", idx.to_string().as_bytes()].concat(); let dest_name = [b"dest", idx.to_string().as_bytes()].concat();
let reply = self let reply = self
.connection .connection
.wait_for_reply(self.connection.send_request(&x::InternAtom { .wait_for_reply(self.connection.send_request(&x::InternAtom {
name: &name, name: &dest_name,
only_if_exists: false, only_if_exists: false,
})) }))
.unwrap(); .unwrap();
let dest = reply.atom(); let dest = reply.atom();
[target, dest] self.connection
}) .send_and_check_request(&x::ConvertSelection {
.collect(); requestor: self.wm_window,
selection: self.atoms.clipboard,
// Setup target list target: target_atom,
self.connection property: dest,
.send_and_check_request(&x::ChangeProperty { time: self.selection_data.clear_time.as_ref().copied().unwrap(),
mode: x::PropMode::Replace, })
window: self.wm_window,
property: self.atoms.selection_reply,
r#type: x::ATOM_ATOM,
data: &target_props,
})
.unwrap();
// Request data for our targets
self.connection
.send_and_check_request(&x::ConvertSelection {
requestor: self.wm_window,
selection: self.atoms.clipboard,
target: self.atoms.multiple,
property: self.atoms.selection_reply,
time: self.selection_data.clear_time.as_ref().copied().unwrap(),
})
.unwrap();
let tmp = target_props
.chunks_exact(2)
.map(|atoms| {
let [target, property] = atoms.try_into().unwrap();
let name = self
.connection
.wait_for_reply(
self.connection
.send_request(&x::GetAtomName { atom: target }),
)
.unwrap(); .unwrap();
let name = name.name().to_string();
let target = SelectionTargetId { atom: target, name }; let target_name = get_atom_name(&self.connection, target_atom);
(target, property) (
SelectionTargetId {
name: target_name,
atom: target_atom,
},
dest,
)
}) })
.collect(); .collect();
self.selection_data.tmp_mimes = tmp; self.selection_data.mime_types = MimeTypes::Temporary {
to_grab,
data: Vec::new(),
};
} }
fn handle_new_clipboard_data(&mut self, server_state: &mut RealServerState) { fn handle_clipboard_data(&mut self, atom: x::Atom) {
let mut mime_types = Vec::new(); let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else {
for (id, dest) in std::mem::take(&mut self.selection_data.tmp_mimes) { warn!("Got selection notify, but not awaiting selection data...");
let value = { return;
if id.atom == self.atoms.timestamp { };
TargetValue::U32(vec![self
.selection_data
.clear_time
.as_ref()
.copied()
.unwrap()])
} else {
let reply = self
.connection
.wait_for_reply(self.connection.send_request(&x::GetProperty {
delete: true,
window: self.wm_window,
property: dest,
r#type: x::ATOM_ANY,
long_offset: 0,
long_length: u32::MAX,
}))
.unwrap();
match reply.format() { let Some(idx) = to_grab.iter().position(|(id, _)| id.atom == atom) else {
8 => TargetValue::U8(reply.value().to_vec()), warn!(
16 => TargetValue::U16(reply.value().to_vec()), "unexpected SelectionNotify type: {}",
32 => TargetValue::U32(reply.value().to_vec()), get_atom_name(&self.connection, atom)
other => { );
if log::log_enabled!(log::Level::Debug) { return;
let atom = id.atom; };
let target = self.get_atom_name(atom);
let ty = if reply.r#type() == x::ATOM_NONE { let (id, dest) = to_grab.swap_remove(idx);
"None".to_string()
} else { let value = match atom {
self.get_atom_name(reply.r#type()) x if x == self.atoms.timestamp => TargetValue::U32(vec![self
}; .selection_data
debug!("unexpected format: {other} (atom: {target}, type: {ty:?}, property: {dest:?})"); .clear_time
} .as_ref()
continue; .copied()
.unwrap()]),
_ => {
let reply = self
.connection
.wait_for_reply(self.connection.send_request(&x::GetProperty {
delete: true,
window: self.wm_window,
property: dest,
r#type: x::ATOM_ANY,
long_offset: 0,
long_length: u32::MAX,
}))
.unwrap();
match reply.format() {
8 => TargetValue::U8(reply.value().to_vec()),
16 => TargetValue::U16(reply.value().to_vec()),
32 => TargetValue::U32(reply.value().to_vec()),
other => {
if log::log_enabled!(log::Level::Debug) {
let target_name = &id.name;
let ty = if reply.r#type() == x::ATOM_NONE {
"None".to_string()
} else {
get_atom_name(&self.connection, reply.r#type())
};
let dest = get_atom_name(&self.connection, dest);
let value = reply.value::<u8>().to_vec();
debug!("unexpected format: {other} (atom: {target_name}, type: {ty:?}, property: {dest}, value: {value:?})");
} }
return;
} }
} }
}; }
};
trace!("Selection data: {id:?} {value:?}"); trace!("Selection data: {id:?} {value:?}");
mime_types.push(SelectionTarget { id, value }); data.push(SelectionTarget { id, value });
}
self.selection_data.mime_types = Rc::new(mime_types);
self.connection
.send_and_check_request(&x::DeleteProperty {
window: self.wm_window,
property: self.atoms.selection_reply,
})
.unwrap();
self.set_clipboard_owner(self.selection_data.clear_time.unwrap());
server_state.set_copy_paste_source(Rc::clone(&self.selection_data.mime_types));
trace!("Clipboard set from X11");
} }
} }

View file

@ -575,7 +575,7 @@ fn copy_from_x11() {
.unwrap(); .unwrap();
assert_eq!(window, owner.owner()); assert_eq!(window, owner.owner());
// wait for request to come through // wait for requests to come through
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(100));
let request = match connection.poll_for_event().unwrap() { let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
@ -604,61 +604,50 @@ fn copy_from_x11() {
}) })
.unwrap(); .unwrap();
std::thread::sleep(std::time::Duration::from_millis(100)); connection.await_event();
let request = match connection.poll_for_event().unwrap() { let mut mime_data = vec![
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, (
other => panic!("Didn't get selection request event, instead got {other:?}"), connection.atoms.mime1,
}; x::ATOM_STRING,
b"hello world".as_slice(),
),
(connection.atoms.mime2, x::ATOM_INTEGER, &[1u8, 2, 3, 4]),
];
assert_eq!(request.target(), connection.atoms.multiple); while let Some(request) = connection.poll_for_event().unwrap() {
let pairs = connection let xcb::Event::X(x::Event::SelectionRequest(request)) = request else {
.wait_for_reply(connection.send_request(&x::GetProperty { continue;
delete: true, };
window: request.requestor(),
property: request.property(),
r#type: x::ATOM_ATOM,
long_offset: 0,
long_length: 4,
}))
.unwrap();
let pairs: &[x::Atom] = pairs.value(); let target = request.target();
assert_eq!(pairs.len(), 4); let Some(idx) = mime_data.iter().position(|(atom, _, _)| *atom == target) else {
assert!(pairs.contains(&connection.atoms.mime1)); panic!("Expected atom in {mime_data:?}, got {target:?}");
assert!(pairs.contains(&connection.atoms.mime2)); };
let mime1data = b"hello world"; let (_, ty, data) = mime_data.swap_remove(idx);
let mime2data = &[1u8, 2, 3, 4]; connection.set_property(request.requestor(), ty, request.property(), data);
for [target, property] in pairs
.chunks_exact(2) connection
.map(|pair| <[x::Atom; 2]>::try_from(pair).unwrap()) .send_and_check_request(&x::SendEvent {
{ propagate: false,
match target { destination: x::SendEventDest::Window(request.requestor()),
x if x == connection.atoms.mime1 => { event_mask: x::EventMask::empty(),
connection.set_property(request.requestor(), x::ATOM_STRING, property, mime1data); event: &x::SelectionNotifyEvent::new(
} request.time(),
x if x == connection.atoms.mime2 => { request.requestor(),
connection.set_property(request.requestor(), x::ATOM_INTEGER, property, mime2data); request.selection(),
} request.target(),
_ => panic!("unexpected target: {target:?}"), request.property(),
} ),
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
} }
connection assert!(
.send_and_check_request(&x::SendEvent { mime_data.is_empty(),
propagate: false, "Didn't get all mime types: {mime_data:?}"
destination: x::SendEventDest::Window(request.requestor()), );
event_mask: x::EventMask::empty(),
event: &x::SelectionNotifyEvent::new(
request.time(),
request.requestor(),
request.selection(),
request.target(),
request.property(),
),
})
.unwrap();
f.wait_and_dispatch(); f.wait_and_dispatch();
let owner = connection let owner = connection
@ -684,10 +673,10 @@ fn copy_from_x11() {
for testwl::PasteData { mime_type, data } in data { for testwl::PasteData { mime_type, data } in data {
match mime_type { match mime_type {
x if x == "text/plain" => { x if x == "text/plain" => {
assert_eq!(&data, mime1data); assert_eq!(&data, b"hello world");
} }
x if x == "blah/blah" => { x if x == "blah/blah" => {
assert_eq!(&data, mime2data); assert_eq!(&data, &[1, 2, 3, 4]);
} }
other => panic!("unexpected mime type: {other} ({data:?})"), other => panic!("unexpected mime type: {other} ({data:?})"),
} }
@ -755,7 +744,7 @@ fn copy_from_wayland() {
}) })
.unwrap(); .unwrap();
std::thread::sleep(std::time::Duration::from_millis(50)); connection.await_event();
let request = match connection.poll_for_event().unwrap() { let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r, Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
other => panic!("Didn't get selection notify event, instead got {other:?}"), other => panic!("Didn't get selection notify event, instead got {other:?}"),
@ -887,8 +876,7 @@ fn bad_clipboard_data() {
}) })
.unwrap(); .unwrap();
// wait for request to come through connection.await_event();
std::thread::sleep(std::time::Duration::from_millis(100));
let request = match connection.poll_for_event().unwrap() { let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"), other => panic!("Didn't get selection request event, instead got {other:?}"),
@ -915,26 +903,14 @@ fn bad_clipboard_data() {
}) })
.unwrap(); .unwrap();
std::thread::sleep(std::time::Duration::from_millis(100)); connection.await_event();
let request = match connection.poll_for_event().unwrap() { let request = match connection.poll_for_event().unwrap() {
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r, Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
other => panic!("Didn't get selection request event, instead got {other:?}"), other => panic!("Didn't get selection request event, instead got {other:?}"),
}; };
assert_eq!(request.target(), connection.atoms.multiple); assert_eq!(request.target(), connection.atoms.mime2);
let pairs = connection
.wait_for_reply(connection.send_request(&x::GetProperty {
delete: true,
window: request.requestor(),
property: request.property(),
r#type: x::ATOM_ATOM,
long_offset: 0,
long_length: 4,
}))
.unwrap();
let pairs: &[x::Atom] = pairs.value(); // Don't actually set any data as requested - just report success
assert_eq!(pairs.len(), 2);
assert!(pairs.contains(&connection.atoms.mime2));
connection connection
.send_and_check_request(&x::SendEvent { .send_and_check_request(&x::SendEvent {
@ -951,7 +927,7 @@ fn bad_clipboard_data() {
}) })
.unwrap(); .unwrap();
f.wait_and_dispatch(); std::thread::sleep(std::time::Duration::from_millis(50));
let owner = connection let owner = connection
.wait_for_reply(connection.send_request(&x::GetSelectionOwner { .wait_for_reply(connection.send_request(&x::GetSelectionOwner {
selection: connection.atoms.clipboard, selection: connection.atoms.clipboard,
@ -969,8 +945,7 @@ fn bad_clipboard_data() {
}) })
.unwrap(); .unwrap();
std::thread::sleep(std::time::Duration::from_millis(100)); connection.await_event();
let mut e = None; let mut e = None;
while let Some(event) = connection.poll_for_event().unwrap() { while let Some(event) = connection.poll_for_event().unwrap() {
if let xcb::Event::X(x::Event::SelectionNotify(event)) = event { if let xcb::Event::X(x::Event::SelectionNotify(event)) = event {