xstate: refactor clipboard selections
Before, every time the clipboard selection changed, satellite would copy everything on it and force itself to be the clipboard owner, regardless of X11 or Wayland. Now, satellite is only the owner when the clipboard owner comes from Wayland, and uses the XFixes extension to watch for changes in clipboard ownership X11 side. Satellite also now avoids copying all of the clipboard contents into memory every time, instead copying directly on request. This is a pretty big change, but should hopefully help make the clipboard more stable. Also added some misc test cleanup/using helper functions where possible. Using the XFixes extension may also end up being necessary for implementing drag and drop, so it's good the infrastructure is there now.
This commit is contained in:
parent
42ffd06d1e
commit
47e7357eab
7 changed files with 755 additions and 777 deletions
|
|
@ -7,6 +7,7 @@ use crate::server::{PendingSurfaceState, ServerState};
|
|||
use crate::xstate::{RealConnection, XState};
|
||||
use log::{error, info};
|
||||
use rustix::event::{poll, PollFd, PollFlags};
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
|
@ -16,7 +17,7 @@ use xcb::x;
|
|||
|
||||
pub trait XConnection: Sized + 'static {
|
||||
type ExtraData: FromServerState<Self>;
|
||||
type MimeTypeData: MimeTypeData;
|
||||
type X11Selection: X11Selection;
|
||||
|
||||
fn root_window(&self) -> x::Window;
|
||||
fn set_window_dims(&mut self, window: x::Window, dims: PendingSurfaceState);
|
||||
|
|
@ -35,9 +36,9 @@ pub trait FromServerState<C: XConnection> {
|
|||
fn create(state: &ServerState<C>) -> Self;
|
||||
}
|
||||
|
||||
pub trait MimeTypeData {
|
||||
fn name(&self) -> &str;
|
||||
fn data(&self) -> &[u8];
|
||||
pub trait X11Selection {
|
||||
fn mime_types(&self) -> Vec<&str>;
|
||||
fn write_to(&self, mime: &str, pipe: WritePipe);
|
||||
}
|
||||
|
||||
type RealServerState = ServerState<RealConnection>;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use self::event::*;
|
|||
use super::FromServerState;
|
||||
use crate::clientside::*;
|
||||
use crate::xstate::{Atoms, WindowDims, WmHints, WmName, WmNormalHints};
|
||||
use crate::{MimeTypeData, XConnection};
|
||||
use crate::{X11Selection, XConnection};
|
||||
use log::{debug, warn};
|
||||
use rustix::event::{poll, PollFd, PollFlags};
|
||||
use slotmap::{new_key_type, HopSlotMap, SparseSecondaryMap};
|
||||
|
|
@ -18,10 +18,9 @@ use smithay_client_toolkit::data_device_manager::{
|
|||
};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Read;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsFd, BorrowedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::rc::Rc;
|
||||
use std::rc::{Rc, Weak};
|
||||
use wayland_client::{globals::Global, protocol as client, Proxy};
|
||||
use wayland_protocols::{
|
||||
wp::{
|
||||
|
|
@ -492,7 +491,7 @@ pub struct ServerState<C: XConnection> {
|
|||
pub connection: Option<C>,
|
||||
|
||||
xdg_wm_base: XdgWmBase,
|
||||
clipboard_data: Option<ClipboardData<C::MimeTypeData>>,
|
||||
clipboard_data: Option<ClipboardData<C::X11Selection>>,
|
||||
last_kb_serial: Option<u32>,
|
||||
}
|
||||
|
||||
|
|
@ -518,7 +517,7 @@ impl<C: XConnection> ServerState<C> {
|
|||
let clipboard_data = manager.map(|manager| ClipboardData {
|
||||
manager,
|
||||
device: None,
|
||||
source: None::<CopyPasteData<C::MimeTypeData>>,
|
||||
source: None::<CopyPasteData<C::X11Selection>>,
|
||||
});
|
||||
|
||||
dh.create_global::<Self, XwaylandShellV1, _>(1, ());
|
||||
|
|
@ -680,11 +679,7 @@ impl<C: XConnection> ServerState<C> {
|
|||
return true;
|
||||
};
|
||||
|
||||
if win.mapped && !win.attrs.override_redirect {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
!(win.mapped && !win.attrs.override_redirect)
|
||||
}
|
||||
|
||||
pub fn reconfigure_window(&mut self, event: x::ConfigureNotifyEvent) {
|
||||
|
|
@ -805,14 +800,14 @@ impl<C: XConnection> ServerState<C> {
|
|||
let _ = self.windows.remove(&window);
|
||||
}
|
||||
|
||||
pub(crate) fn set_copy_paste_source(&mut self, mime_types: Rc<Vec<C::MimeTypeData>>) {
|
||||
pub(crate) fn set_copy_paste_source(&mut self, selection: &Rc<C::X11Selection>) {
|
||||
if let Some(d) = &mut self.clipboard_data {
|
||||
let src = d
|
||||
.manager
|
||||
.create_copy_paste_source(&self.qh, mime_types.iter().map(|m| m.name()));
|
||||
.create_copy_paste_source(&self.qh, selection.mime_types());
|
||||
let data = CopyPasteData::X11 {
|
||||
inner: src,
|
||||
data: mime_types,
|
||||
data: Rc::downgrade(selection),
|
||||
};
|
||||
let CopyPasteData::X11 { inner, .. } = d.source.insert(data) else {
|
||||
unreachable!();
|
||||
|
|
@ -891,13 +886,12 @@ impl<C: XConnection> ServerState<C> {
|
|||
let globals = &mut self.clientside.globals;
|
||||
|
||||
if let Some(clipboard) = self.clipboard_data.as_mut() {
|
||||
for (mime_type, mut fd) in std::mem::take(&mut globals.selection_requests) {
|
||||
for (mime_type, fd) in std::mem::take(&mut globals.selection_requests) {
|
||||
let CopyPasteData::X11 { data, .. } = clipboard.source.as_ref().unwrap() else {
|
||||
unreachable!()
|
||||
unreachable!("Got selection request without having set the selection?")
|
||||
};
|
||||
let pos = data.iter().position(|m| m.name() == mime_type).unwrap();
|
||||
if let Err(e) = fd.write_all(data[pos].data()) {
|
||||
warn!("Failed to write selection data: {e:?}");
|
||||
if let Some(data) = data.upgrade() {
|
||||
data.write_to(&mime_type, fd);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1090,10 +1084,10 @@ pub struct PendingSurfaceState {
|
|||
pub height: i32,
|
||||
}
|
||||
|
||||
struct ClipboardData<M: MimeTypeData> {
|
||||
struct ClipboardData<X: X11Selection> {
|
||||
manager: DataDeviceManagerState,
|
||||
device: Option<DataDevice>,
|
||||
source: Option<CopyPasteData<M>>,
|
||||
source: Option<CopyPasteData<X>>,
|
||||
}
|
||||
|
||||
pub struct ForeignSelection {
|
||||
|
|
@ -1121,10 +1115,10 @@ impl Drop for ForeignSelection {
|
|||
}
|
||||
}
|
||||
|
||||
enum CopyPasteData<M: MimeTypeData> {
|
||||
enum CopyPasteData<X: X11Selection> {
|
||||
X11 {
|
||||
inner: CopyPasteSource,
|
||||
data: Rc<Vec<M>>,
|
||||
data: Weak<X>,
|
||||
},
|
||||
Foreign(ForeignSelection),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use crate::xstate::{SetState, WmName};
|
|||
use paste::paste;
|
||||
use rustix::event::{poll, PollFd, PollFlags};
|
||||
use std::collections::HashMap;
|
||||
use std::io::Write;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
|
@ -182,19 +183,30 @@ impl super::FromServerState<FakeXConnection> for () {
|
|||
fn create(_: &FakeServerState) -> Self {}
|
||||
}
|
||||
|
||||
impl crate::MimeTypeData for testwl::PasteData {
|
||||
fn name(&self) -> &str {
|
||||
&self.mime_type
|
||||
impl crate::X11Selection for Vec<testwl::PasteData> {
|
||||
fn mime_types(&self) -> Vec<&str> {
|
||||
self.iter().map(|data| data.mime_type.as_str()).collect()
|
||||
}
|
||||
|
||||
fn data(&self) -> &[u8] {
|
||||
&self.data
|
||||
fn write_to(
|
||||
&self,
|
||||
mime: &str,
|
||||
mut pipe: smithay_client_toolkit::data_device_manager::WritePipe,
|
||||
) {
|
||||
println!("writing");
|
||||
let data = self
|
||||
.iter()
|
||||
.find(|data| data.mime_type == mime)
|
||||
.unwrap_or_else(|| panic!("Couldn't find mime type {mime}"));
|
||||
pipe.write_all(&data.data)
|
||||
.expect("Couldn't write paste data");
|
||||
println!("goodbye pipe {mime}");
|
||||
}
|
||||
}
|
||||
|
||||
impl super::XConnection for FakeXConnection {
|
||||
type ExtraData = ();
|
||||
type MimeTypeData = testwl::PasteData;
|
||||
type X11Selection = Vec<testwl::PasteData>;
|
||||
fn root_window(&self) -> Window {
|
||||
self.root
|
||||
}
|
||||
|
|
@ -1162,7 +1174,7 @@ fn copy_from_x11() {
|
|||
},
|
||||
]);
|
||||
|
||||
f.satellite.set_copy_paste_source(mimes.clone());
|
||||
f.satellite.set_copy_paste_source(&mimes);
|
||||
f.run();
|
||||
|
||||
let server_mimes = f.testwl.data_source_mimes();
|
||||
|
|
@ -1170,9 +1182,10 @@ fn copy_from_x11() {
|
|||
assert!(server_mimes.contains(&mime.mime_type));
|
||||
}
|
||||
|
||||
let data = f.testwl.paste_data();
|
||||
f.run();
|
||||
let data = data.resolve();
|
||||
let data = f.testwl.paste_data(|_, _| {
|
||||
f.satellite.run();
|
||||
true
|
||||
});
|
||||
assert_eq!(*mimes, data);
|
||||
}
|
||||
|
||||
|
|
@ -1236,7 +1249,7 @@ fn clipboard_x11_then_wayland() {
|
|||
},
|
||||
]);
|
||||
|
||||
f.satellite.set_copy_paste_source(x11data.clone());
|
||||
f.satellite.set_copy_paste_source(&x11data);
|
||||
f.run();
|
||||
|
||||
let waylanddata = vec![
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
mod selection;
|
||||
use selection::{SelectionData, SelectionTarget};
|
||||
use selection::{Selection, SelectionData};
|
||||
|
||||
use crate::{server::WindowAttributes, XConnection};
|
||||
use bitflags::bitflags;
|
||||
|
|
@ -117,7 +117,11 @@ impl XState {
|
|||
xcb::Connection::connect_to_fd_with_extensions(
|
||||
fd.as_raw_fd(),
|
||||
None,
|
||||
&[xcb::Extension::Composite, xcb::Extension::RandR],
|
||||
&[
|
||||
xcb::Extension::Composite,
|
||||
xcb::Extension::RandR,
|
||||
xcb::Extension::XFixes,
|
||||
],
|
||||
&[],
|
||||
)
|
||||
.unwrap(),
|
||||
|
|
@ -156,6 +160,28 @@ impl XState {
|
|||
})
|
||||
.unwrap();
|
||||
|
||||
// negotiate xfixes version
|
||||
let reply = connection
|
||||
.wait_for_reply(connection.send_request(&xcb::xfixes::QueryVersion {
|
||||
client_major_version: 1,
|
||||
client_minor_version: 0,
|
||||
}))
|
||||
.unwrap();
|
||||
log::info!(
|
||||
"xfixes version: {}.{}",
|
||||
reply.major_version(),
|
||||
reply.minor_version()
|
||||
);
|
||||
use xcb::xfixes::SelectionEventMask;
|
||||
connection
|
||||
.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
|
||||
window: root,
|
||||
selection: atoms.clipboard,
|
||||
event_mask: SelectionEventMask::SET_SELECTION_OWNER
|
||||
| SelectionEventMask::SELECTION_WINDOW_DESTROY
|
||||
| SelectionEventMask::SELECTION_CLIENT_CLOSE,
|
||||
})
|
||||
.unwrap();
|
||||
{
|
||||
// Setup default cursor theme
|
||||
let ctx = CursorContext::new(&connection, screen).unwrap();
|
||||
|
|
@ -169,13 +195,14 @@ impl XState {
|
|||
}
|
||||
|
||||
let wm_window = connection.generate_id();
|
||||
let selection_data = SelectionData::new(&connection, root);
|
||||
|
||||
let mut r = Self {
|
||||
connection,
|
||||
wm_window,
|
||||
root,
|
||||
atoms,
|
||||
selection_data: Default::default(),
|
||||
selection_data,
|
||||
};
|
||||
r.create_ewmh_window();
|
||||
r
|
||||
|
|
@ -240,8 +267,6 @@ impl XState {
|
|||
data: b"xwayland-satellite",
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.set_clipboard_owner(x::CURRENT_TIME);
|
||||
}
|
||||
|
||||
pub fn handle_events(&mut self, server_state: &mut super::RealServerState) {
|
||||
|
|
@ -666,7 +691,7 @@ impl XState {
|
|||
server_state.set_win_class(window, class);
|
||||
}
|
||||
_ => {
|
||||
if !self.handle_selection_property_change(&event, server_state)
|
||||
if !self.handle_selection_property_change(&event)
|
||||
&& log::log_enabled!(log::Level::Debug)
|
||||
{
|
||||
debug!(
|
||||
|
|
@ -872,7 +897,7 @@ impl RealConnection {
|
|||
|
||||
impl XConnection for RealConnection {
|
||||
type ExtraData = Atoms;
|
||||
type MimeTypeData = SelectionTarget;
|
||||
type X11Selection = Selection;
|
||||
|
||||
fn root_window(&self) -> x::Window {
|
||||
self.connection.get_setup().roots().next().unwrap().root()
|
||||
|
|
@ -898,14 +923,16 @@ impl XConnection for RealConnection {
|
|||
&[]
|
||||
};
|
||||
|
||||
if let Err(e) = self.connection
|
||||
if let Err(e) = self
|
||||
.connection
|
||||
.send_and_check_request(&x::ChangeProperty::<x::Atom> {
|
||||
mode: x::PropMode::Replace,
|
||||
window,
|
||||
property: atoms.net_wm_state,
|
||||
r#type: x::ATOM_ATOM,
|
||||
data,
|
||||
}) {
|
||||
})
|
||||
{
|
||||
warn!("Failed to set fullscreen state on {window:?} ({e})");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,91 +1,213 @@
|
|||
use super::{get_atom_name, XState};
|
||||
use crate::server::ForeignSelection;
|
||||
use crate::{MimeTypeData, RealServerState};
|
||||
use log::{debug, trace, warn};
|
||||
use crate::{RealServerState, X11Selection};
|
||||
use log::{debug, error, warn};
|
||||
use smithay_client_toolkit::data_device_manager::WritePipe;
|
||||
use std::cell::RefCell;
|
||||
use std::io::Write;
|
||||
use std::rc::Rc;
|
||||
use xcb::x;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TargetValue {
|
||||
U8(Vec<u8>),
|
||||
U16(Vec<u16>),
|
||||
U32(Vec<u32>),
|
||||
Foreign,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SelectionTargetId {
|
||||
name: String,
|
||||
atom: x::Atom,
|
||||
}
|
||||
|
||||
pub struct SelectionTarget {
|
||||
id: SelectionTargetId,
|
||||
value: TargetValue,
|
||||
struct PendingSelectionData {
|
||||
target: x::Atom,
|
||||
pipe: WritePipe,
|
||||
incr: bool,
|
||||
}
|
||||
|
||||
impl MimeTypeData for SelectionTarget {
|
||||
fn name(&self) -> &str {
|
||||
&self.id.name
|
||||
pub struct Selection {
|
||||
mimes: Vec<SelectionTargetId>,
|
||||
connection: Rc<xcb::Connection>,
|
||||
window: x::Window,
|
||||
pending: RefCell<Vec<PendingSelectionData>>,
|
||||
clipboard: x::Atom,
|
||||
selection_time: u32,
|
||||
incr: x::Atom,
|
||||
}
|
||||
|
||||
fn data(&self) -> &[u8] {
|
||||
match &self.value {
|
||||
TargetValue::U8(v) => v,
|
||||
TargetValue::U32(v) => unsafe { v.align_to().1 },
|
||||
other => {
|
||||
impl X11Selection for Selection {
|
||||
fn mime_types(&self) -> Vec<&str> {
|
||||
self.mimes
|
||||
.iter()
|
||||
.map(|target| target.name.as_str())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn write_to(&self, mime: &str, pipe: WritePipe) {
|
||||
if let Some(target) = self.mimes.iter().find(|target| target.name == mime) {
|
||||
// We use the target as the property to write to
|
||||
if let Err(e) = self
|
||||
.connection
|
||||
.send_and_check_request(&x::ConvertSelection {
|
||||
requestor: self.window,
|
||||
selection: self.clipboard,
|
||||
target: target.atom,
|
||||
property: target.atom,
|
||||
time: self.selection_time,
|
||||
})
|
||||
{
|
||||
error!("Failed to request clipboard data (mime type: {mime}, error: {e})");
|
||||
return;
|
||||
}
|
||||
|
||||
self.pending.borrow_mut().push(PendingSelectionData {
|
||||
target: target.atom,
|
||||
pipe,
|
||||
incr: false,
|
||||
})
|
||||
} else {
|
||||
warn!("Could not find mime type {mime}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Selection {
|
||||
fn handle_notify(&self, target: x::Atom) {
|
||||
let mut pending = self.pending.borrow_mut();
|
||||
let Some(idx) = pending.iter().position(|t| t.target == target) else {
|
||||
warn!(
|
||||
"Unexpectedly requesting data from mime type with data type {} - nothing will be copied",
|
||||
std::any::type_name_of_val(other)
|
||||
"Got selection notify for unknown target {}",
|
||||
get_atom_name(&self.connection, target),
|
||||
);
|
||||
&[]
|
||||
return;
|
||||
};
|
||||
|
||||
let PendingSelectionData {
|
||||
mut pipe,
|
||||
incr,
|
||||
target,
|
||||
} = pending.swap_remove(idx);
|
||||
let reply = match get_property_any(&self.connection, self.window, target) {
|
||||
Ok(reply) => reply,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Couldn't get mime type for {}: {e:?}",
|
||||
get_atom_name(&self.connection, target)
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"got type {} for mime type {}",
|
||||
get_atom_name(&self.connection, reply.r#type()),
|
||||
get_atom_name(&self.connection, target)
|
||||
);
|
||||
|
||||
if reply.r#type() == self.incr {
|
||||
debug!(
|
||||
"beginning incr for {}",
|
||||
get_atom_name(&self.connection, target)
|
||||
);
|
||||
pending.push(PendingSelectionData {
|
||||
target,
|
||||
pipe,
|
||||
incr: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let data = match reply.format() {
|
||||
8 => reply.value::<u8>(),
|
||||
32 => unsafe { reply.value::<u32>().align_to().1 },
|
||||
other => {
|
||||
warn!("Unexpected format {other} in selection reply");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !incr || !data.is_empty() {
|
||||
if let Err(e) = pipe.write_all(data) {
|
||||
warn!("Failed to write selection data: {e:?}");
|
||||
} else if incr {
|
||||
debug!(
|
||||
"recieved some incr data for {}",
|
||||
get_atom_name(&self.connection, target)
|
||||
);
|
||||
pending.push(PendingSelectionData {
|
||||
target,
|
||||
pipe,
|
||||
incr: true,
|
||||
})
|
||||
}
|
||||
} else if incr {
|
||||
// data is empty
|
||||
debug!(
|
||||
"completed incr for mime {}",
|
||||
get_atom_name(&self.connection, target)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn check_for_incr(&self, event: &x::PropertyNotifyEvent) -> bool {
|
||||
if event.window() != self.window || event.state() != x::Property::NewValue {
|
||||
return false;
|
||||
}
|
||||
|
||||
let target = self.pending.borrow().iter().find_map(|pending| {
|
||||
(pending.target == event.atom() && pending.incr).then_some(pending.target)
|
||||
});
|
||||
if let Some(target) = target {
|
||||
self.handle_notify(target);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PendingMimeDataType {
|
||||
Standard,
|
||||
Incremental(TargetValue),
|
||||
}
|
||||
|
||||
struct PendingMimeData {
|
||||
ty: PendingMimeDataType,
|
||||
id: SelectionTargetId,
|
||||
dest_property: x::Atom,
|
||||
}
|
||||
|
||||
enum MimeTypes {
|
||||
Temporary {
|
||||
/// Temporary mime data, being built
|
||||
data: Vec<SelectionTarget>,
|
||||
/// Mime types we still need to receive feedback on
|
||||
to_grab: Vec<PendingMimeData>,
|
||||
enum CurrentSelection {
|
||||
X11(Rc<Selection>),
|
||||
Wayland {
|
||||
mimes: Vec<SelectionTargetId>,
|
||||
inner: ForeignSelection,
|
||||
},
|
||||
/// Done grabbing mime data
|
||||
Complete(Rc<Vec<SelectionTarget>>),
|
||||
}
|
||||
|
||||
impl Default for MimeTypes {
|
||||
fn default() -> Self {
|
||||
Self::Complete(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct SelectionData {
|
||||
clear_time: Option<u32>,
|
||||
mime_types: MimeTypes,
|
||||
foreign_data: Option<ForeignSelection>,
|
||||
last_selection_timestamp: u32,
|
||||
target_window: x::Window,
|
||||
current_selection: Option<CurrentSelection>,
|
||||
}
|
||||
|
||||
impl SelectionData {
|
||||
pub fn new(connection: &xcb::Connection, root: x::Window) -> Self {
|
||||
let target_window = connection.generate_id();
|
||||
connection
|
||||
.send_and_check_request(&x::CreateWindow {
|
||||
wid: target_window,
|
||||
width: 1,
|
||||
height: 1,
|
||||
depth: 0,
|
||||
parent: root,
|
||||
x: 0,
|
||||
y: 0,
|
||||
border_width: 0,
|
||||
class: x::WindowClass::InputOnly,
|
||||
visual: x::COPY_FROM_PARENT,
|
||||
// Watch for INCR property changes.
|
||||
value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
|
||||
})
|
||||
.expect("Couldn't create window for selections");
|
||||
Self {
|
||||
last_selection_timestamp: x::CURRENT_TIME,
|
||||
target_window,
|
||||
current_selection: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl XState {
|
||||
pub(crate) fn set_clipboard_owner(&mut self, time: u32) {
|
||||
fn set_clipboard_owner(&mut self) {
|
||||
self.connection
|
||||
.send_and_check_request(&x::SetSelectionOwner {
|
||||
owner: self.wm_window,
|
||||
selection: self.atoms.clipboard,
|
||||
time,
|
||||
time: self.selection_data.last_selection_timestamp,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -105,7 +227,7 @@ impl XState {
|
|||
}
|
||||
|
||||
pub(crate) fn set_clipboard(&mut self, selection: ForeignSelection) {
|
||||
let types = selection
|
||||
let mimes = selection
|
||||
.mime_types
|
||||
.iter()
|
||||
.map(|mime| {
|
||||
|
|
@ -117,18 +239,18 @@ impl XState {
|
|||
}))
|
||||
.unwrap();
|
||||
|
||||
SelectionTarget {
|
||||
id: SelectionTargetId {
|
||||
SelectionTargetId {
|
||||
name: mime.clone(),
|
||||
atom: atom.atom(),
|
||||
},
|
||||
value: TargetValue::Foreign,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.selection_data.mime_types = MimeTypes::Complete(Rc::new(types));
|
||||
self.selection_data.foreign_data = Some(selection);
|
||||
self.selection_data.current_selection = Some(CurrentSelection::Wayland {
|
||||
mimes,
|
||||
inner: selection,
|
||||
});
|
||||
self.set_clipboard_owner();
|
||||
debug!("Clipboard set from Wayland");
|
||||
}
|
||||
|
||||
|
|
@ -138,29 +260,9 @@ impl XState {
|
|||
server_state: &mut RealServerState,
|
||||
) -> bool {
|
||||
match event {
|
||||
// Someone else is the clipboard owner - get the data from them and then reestablish
|
||||
// ourselves as the owner
|
||||
// Someone else took the clipboard owner
|
||||
xcb::Event::X(x::Event::SelectionClear(e)) => {
|
||||
if e.selection() != self.atoms.clipboard {
|
||||
warn!(
|
||||
"Got SelectionClear for unexpected atom {}, ignoring",
|
||||
get_atom_name(&self.connection, e.selection())
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// get the mime types
|
||||
self.connection
|
||||
.send_and_check_request(&x::ConvertSelection {
|
||||
requestor: self.wm_window,
|
||||
selection: self.atoms.clipboard,
|
||||
target: self.atoms.targets,
|
||||
property: self.atoms.selection_reply,
|
||||
time: e.time(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
self.selection_data.clear_time = Some(e.time());
|
||||
self.handle_new_selection_owner(e.owner(), e.time());
|
||||
}
|
||||
xcb::Event::X(x::Event::SelectionNotify(e)) => {
|
||||
if e.property() == x::ATOM_NONE {
|
||||
|
|
@ -168,24 +270,33 @@ impl XState {
|
|||
return true;
|
||||
}
|
||||
|
||||
trace!(
|
||||
"selection notify target: {}",
|
||||
debug!(
|
||||
"selection notify requestor: {:?} target: {}",
|
||||
e.requestor(),
|
||||
get_atom_name(&self.connection, e.target())
|
||||
);
|
||||
match e.target() {
|
||||
x if x == self.atoms.targets => self.handle_target_list(e.property()),
|
||||
atom => self.handle_clipboard_data(atom),
|
||||
}
|
||||
|
||||
if let MimeTypes::Temporary { to_grab, .. } = &self.selection_data.mime_types {
|
||||
if to_grab.is_empty() {
|
||||
let MimeTypes::Temporary { data, .. } =
|
||||
std::mem::take(&mut self.selection_data.mime_types)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
self.finish_mime_data(server_state, data);
|
||||
if e.requestor() == self.wm_window {
|
||||
match e.target() {
|
||||
x if x == self.atoms.targets => {
|
||||
self.handle_target_list(e.property(), server_state)
|
||||
}
|
||||
other => warn!(
|
||||
"got unexpected selection notify for target {}",
|
||||
get_atom_name(&self.connection, other)
|
||||
),
|
||||
}
|
||||
} else if e.requestor() == self.selection_data.target_window {
|
||||
if let Some(CurrentSelection::X11(selection)) =
|
||||
&self.selection_data.current_selection
|
||||
{
|
||||
selection.handle_notify(e.target());
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Got selection notify from unexpected requestor: {:?}",
|
||||
e.requestor()
|
||||
);
|
||||
}
|
||||
}
|
||||
xcb::Event::X(x::Event::SelectionRequest(e)) => {
|
||||
|
|
@ -219,15 +330,17 @@ impl XState {
|
|||
return true;
|
||||
}
|
||||
|
||||
let MimeTypes::Complete(mime_data) = &self.selection_data.mime_types else {
|
||||
warn!("Got selection request, but mime data is incomplete");
|
||||
let Some(CurrentSelection::Wayland { mimes, inner }) =
|
||||
&self.selection_data.current_selection
|
||||
else {
|
||||
warn!("Got selection request, but we don't seem to be the selection owner");
|
||||
refuse();
|
||||
return true;
|
||||
};
|
||||
|
||||
match e.target() {
|
||||
x if x == self.atoms.targets => {
|
||||
let atoms: Box<[x::Atom]> = mime_data.iter().map(|t| t.id.atom).collect();
|
||||
let atoms: Box<[x::Atom]> = mimes.iter().map(|t| t.atom).collect();
|
||||
|
||||
self.connection
|
||||
.send_and_check_request(&x::ChangeProperty {
|
||||
|
|
@ -242,7 +355,7 @@ impl XState {
|
|||
success();
|
||||
}
|
||||
other => {
|
||||
let Some(target) = mime_data.iter().find(|t| t.id.atom == other) else {
|
||||
let Some(target) = mimes.iter().find(|t| t.atom == other) else {
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
let name = get_atom_name(&self.connection, other);
|
||||
debug!("refusing selection request because given atom could not be found ({})", name);
|
||||
|
|
@ -251,14 +364,13 @@ impl XState {
|
|||
return true;
|
||||
};
|
||||
|
||||
macro_rules! set_property {
|
||||
($data:expr) => {
|
||||
let data = inner.receive(target.name.clone(), server_state);
|
||||
match self.connection.send_and_check_request(&x::ChangeProperty {
|
||||
mode: x::PropMode::Replace,
|
||||
window: e.requestor(),
|
||||
property: e.property(),
|
||||
r#type: target.id.atom,
|
||||
data: $data,
|
||||
r#type: target.atom,
|
||||
data: &data,
|
||||
}) {
|
||||
Ok(_) => success(),
|
||||
Err(e) => {
|
||||
|
|
@ -266,23 +378,24 @@ impl XState {
|
|||
refuse();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match &target.value {
|
||||
TargetValue::U8(v) => set_property!(v),
|
||||
TargetValue::U16(v) => set_property!(v),
|
||||
TargetValue::U32(v) => set_property!(v),
|
||||
TargetValue::Foreign => {
|
||||
let data = self
|
||||
.selection_data
|
||||
.foreign_data
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.receive(target.id.name.clone(), server_state);
|
||||
set_property!(&data);
|
||||
xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e)) => {
|
||||
assert_eq!(e.selection(), self.atoms.clipboard);
|
||||
match e.subtype() {
|
||||
xcb::xfixes::SelectionEvent::SetSelectionOwner => {
|
||||
if e.owner() == self.wm_window {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.handle_new_selection_owner(e.owner(), e.selection_timestamp());
|
||||
}
|
||||
xcb::xfixes::SelectionEvent::SelectionClientClose
|
||||
| xcb::xfixes::SelectionEvent::SelectionWindowDestroy => {
|
||||
debug!("Selection owner destroyed, selection will be unset");
|
||||
self.selection_data.current_selection = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -292,7 +405,22 @@ impl XState {
|
|||
true
|
||||
}
|
||||
|
||||
fn handle_target_list(&mut self, dest_property: x::Atom) {
|
||||
fn handle_new_selection_owner(&mut self, owner: x::Window, timestamp: u32) {
|
||||
debug!("new selection owner: {:?}", owner);
|
||||
self.selection_data.last_selection_timestamp = timestamp;
|
||||
// Grab targets
|
||||
self.connection
|
||||
.send_and_check_request(&x::ConvertSelection {
|
||||
requestor: self.wm_window,
|
||||
selection: self.atoms.clipboard,
|
||||
target: self.atoms.targets,
|
||||
property: self.atoms.selection_reply,
|
||||
time: timestamp,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn handle_target_list(&mut self, dest_property: x::Atom, server_state: &mut RealServerState) {
|
||||
let reply = self
|
||||
.connection
|
||||
.wait_for_reply(self.connection.send_request(&x::GetProperty {
|
||||
|
|
@ -306,6 +434,29 @@ impl XState {
|
|||
.unwrap();
|
||||
|
||||
let targets: &[x::Atom] = reply.value();
|
||||
if targets.is_empty() {
|
||||
warn!("Got empty selection target list, trying again...");
|
||||
match self.connection.wait_for_reply(self.connection.send_request(
|
||||
&x::GetSelectionOwner {
|
||||
selection: self.atoms.clipboard,
|
||||
},
|
||||
)) {
|
||||
Ok(reply) => {
|
||||
if reply.owner() == self.wm_window {
|
||||
warn!("We are unexpectedly the selection owner? Clipboard may be broken!");
|
||||
} else {
|
||||
self.handle_new_selection_owner(
|
||||
reply.owner(),
|
||||
self.selection_data.last_selection_timestamp,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Couldn't grab selection owner: {e:?}. Clipboard is stale!");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if log::log_enabled!(log::Level::Debug) {
|
||||
let targets_str: Vec<String> = targets
|
||||
.iter()
|
||||
|
|
@ -314,7 +465,7 @@ impl XState {
|
|||
debug!("got targets: {targets_str:?}");
|
||||
}
|
||||
|
||||
let to_grab = targets
|
||||
let mimes = targets
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|atom| {
|
||||
|
|
@ -325,218 +476,35 @@ impl XState {
|
|||
]
|
||||
.contains(atom)
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(idx, target_atom)| {
|
||||
let dest_name = [b"dest", idx.to_string().as_bytes()].concat();
|
||||
let reply = self
|
||||
.connection
|
||||
.wait_for_reply(self.connection.send_request(&x::InternAtom {
|
||||
name: &dest_name,
|
||||
only_if_exists: false,
|
||||
}))
|
||||
.unwrap();
|
||||
let dest_property = reply.atom();
|
||||
|
||||
self.connection
|
||||
.send_and_check_request(&x::ConvertSelection {
|
||||
requestor: self.wm_window,
|
||||
selection: self.atoms.clipboard,
|
||||
target: target_atom,
|
||||
property: dest_property,
|
||||
time: self.selection_data.clear_time.as_ref().copied().unwrap(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let target_name = get_atom_name(&self.connection, target_atom);
|
||||
PendingMimeData {
|
||||
ty: PendingMimeDataType::Standard,
|
||||
id: SelectionTargetId {
|
||||
name: target_name,
|
||||
.map(|target_atom| SelectionTargetId {
|
||||
name: get_atom_name(&self.connection, target_atom),
|
||||
atom: target_atom,
|
||||
},
|
||||
dest_property,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.selection_data.mime_types = MimeTypes::Temporary {
|
||||
to_grab,
|
||||
data: Vec::new(),
|
||||
};
|
||||
}
|
||||
let selection = Rc::new(Selection {
|
||||
mimes,
|
||||
connection: self.connection.clone(),
|
||||
window: self.selection_data.target_window,
|
||||
pending: RefCell::default(),
|
||||
clipboard: self.atoms.clipboard,
|
||||
selection_time: self.selection_data.last_selection_timestamp,
|
||||
incr: self.atoms.incr,
|
||||
});
|
||||
|
||||
fn handle_clipboard_data(&mut self, atom: x::Atom) {
|
||||
let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else {
|
||||
warn!("Got selection notify, but not awaiting selection data...");
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(idx) = to_grab
|
||||
.iter()
|
||||
.position(|PendingMimeData { id, .. }| id.atom == atom)
|
||||
else {
|
||||
warn!(
|
||||
"unexpected SelectionNotify type: {}",
|
||||
get_atom_name(&self.connection, atom)
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let PendingMimeData {
|
||||
ty,
|
||||
id,
|
||||
dest_property,
|
||||
} = to_grab.swap_remove(idx);
|
||||
|
||||
let value = match atom {
|
||||
x if x == self.atoms.timestamp => TargetValue::U32(vec![self
|
||||
.selection_data
|
||||
.clear_time
|
||||
.as_ref()
|
||||
.copied()
|
||||
.unwrap()]),
|
||||
_ => {
|
||||
let reply = get_property_any(&self.connection, self.wm_window, dest_property);
|
||||
|
||||
trace!(
|
||||
"got type {} for mime type {}",
|
||||
get_atom_name(&self.connection, reply.r#type()),
|
||||
get_atom_name(&self.connection, atom)
|
||||
);
|
||||
|
||||
match reply.r#type() {
|
||||
x if x == self.atoms.incr => {
|
||||
assert!(matches!(ty, PendingMimeDataType::Standard));
|
||||
debug!(
|
||||
"beginning incr process for {}",
|
||||
get_atom_name(&self.connection, atom)
|
||||
);
|
||||
if let Some(data) =
|
||||
begin_incr(&self.connection, self.wm_window, reply, id, dest_property)
|
||||
{
|
||||
to_grab.push(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ => 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_property);
|
||||
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:?}");
|
||||
data.push(SelectionTarget { id, value });
|
||||
server_state.set_copy_paste_source(&selection);
|
||||
self.selection_data.current_selection = Some(CurrentSelection::X11(selection));
|
||||
debug!("Clipboard set from X11");
|
||||
}
|
||||
|
||||
pub(super) fn handle_selection_property_change(
|
||||
&mut self,
|
||||
event: &x::PropertyNotifyEvent,
|
||||
server_state: &mut RealServerState,
|
||||
) -> bool {
|
||||
if event.window() != self.wm_window {
|
||||
return false;
|
||||
if let Some(CurrentSelection::X11(selection)) = &self.selection_data.current_selection {
|
||||
return selection.check_for_incr(event);
|
||||
}
|
||||
|
||||
let MimeTypes::Temporary { data, to_grab } = &mut self.selection_data.mime_types else {
|
||||
debug!("Got potential selection property change, but not awaiting mime data");
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(idx) = to_grab.iter().position(|p| {
|
||||
matches!(p.ty, PendingMimeDataType::Incremental(_)) && p.dest_property == event.atom()
|
||||
}) else {
|
||||
debug!(
|
||||
"Changed non selection property: {}",
|
||||
get_atom_name(&self.connection, event.atom())
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
let pending = &mut to_grab[idx];
|
||||
let reply = get_property_any(&self.connection, self.wm_window, pending.dest_property);
|
||||
|
||||
if reply.r#type() != pending.id.atom {
|
||||
warn!(
|
||||
"wrong getproperty type: {}",
|
||||
get_atom_name(&self.connection, reply.r#type())
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
match reply.format() {
|
||||
8 => {
|
||||
let value: &[u8] = reply.value();
|
||||
trace!("got incr data ({} bytes)", value.len());
|
||||
if value.is_empty() {
|
||||
let pending = to_grab.swap_remove(idx);
|
||||
let PendingMimeDataType::Incremental(value) = pending.ty else {
|
||||
unreachable!()
|
||||
};
|
||||
let atom = pending.id.atom;
|
||||
data.push(SelectionTarget {
|
||||
id: pending.id,
|
||||
value,
|
||||
});
|
||||
trace!(
|
||||
"finalized incr for {}",
|
||||
get_atom_name(&self.connection, atom)
|
||||
);
|
||||
} else {
|
||||
let PendingMimeDataType::Incremental(TargetValue::U8(data)) = &mut pending.ty
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
data.extend_from_slice(value);
|
||||
trace!("new incr len: {}", data.len());
|
||||
}
|
||||
}
|
||||
other => {
|
||||
warn!("Got unexpected format {other} for INCR data - copy/pasting with mime type {} will fail!", get_atom_name(&self.connection, reply.r#type()));
|
||||
to_grab.swap_remove(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if to_grab.is_empty() {
|
||||
let MimeTypes::Temporary { data, .. } =
|
||||
std::mem::take(&mut self.selection_data.mime_types)
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
self.finish_mime_data(server_state, data);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn finish_mime_data(&mut self, server_state: &mut RealServerState, data: Vec<SelectionTarget>) {
|
||||
self.connection
|
||||
.send_and_check_request(&x::ChangeWindowAttributes {
|
||||
window: self.wm_window,
|
||||
value_list: &[x::Cw::EventMask(x::EventMask::empty())],
|
||||
})
|
||||
.unwrap();
|
||||
let data = Rc::new(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);
|
||||
debug!("Clipboard set from X11");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -544,9 +512,8 @@ fn get_property_any(
|
|||
connection: &xcb::Connection,
|
||||
window: x::Window,
|
||||
property: x::Atom,
|
||||
) -> x::GetPropertyReply {
|
||||
connection
|
||||
.wait_for_reply(connection.send_request(&x::GetProperty {
|
||||
) -> xcb::Result<x::GetPropertyReply> {
|
||||
connection.wait_for_reply(connection.send_request(&x::GetProperty {
|
||||
delete: true,
|
||||
window,
|
||||
property,
|
||||
|
|
@ -554,38 +521,4 @@ fn get_property_any(
|
|||
long_offset: 0,
|
||||
long_length: u32::MAX,
|
||||
}))
|
||||
.unwrap()
|
||||
}
|
||||
fn begin_incr(
|
||||
connection: &xcb::Connection,
|
||||
window: x::Window,
|
||||
reply: x::GetPropertyReply,
|
||||
id: SelectionTargetId,
|
||||
dest_property: x::Atom,
|
||||
) -> Option<PendingMimeData> {
|
||||
let size = match reply.format() {
|
||||
8 => reply.value::<u8>()[0] as usize,
|
||||
16 => reply.value::<u16>()[0] as usize,
|
||||
32 => reply.value::<u32>()[0] as usize,
|
||||
other => {
|
||||
warn!("unexpected incr format: {other}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::ChangeWindowAttributes {
|
||||
window,
|
||||
value_list: &[x::Cw::EventMask(x::EventMask::PROPERTY_CHANGE)],
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// XXX: storing INCR property data in memory could significantly bloat memory depending on how
|
||||
// much data is going to be stuck into the clipboard, but we'll cross that bridge when we get
|
||||
// to it.
|
||||
Some(PendingMimeData {
|
||||
ty: PendingMimeDataType::Incremental(TargetValue::U8(Vec::with_capacity(size))),
|
||||
id,
|
||||
dest_property,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,11 +185,9 @@ impl Fixture {
|
|||
.configure_toplevel(surface, 100, 100, vec![xdg_toplevel::State::Activated]);
|
||||
self.testwl.focus_toplevel(surface);
|
||||
self.wait_and_dispatch();
|
||||
let geometry = connection
|
||||
.wait_for_reply(connection.send_request(&x::GetGeometry {
|
||||
let geometry = connection.get_reply(&x::GetGeometry {
|
||||
drawable: x::Drawable::Window(window),
|
||||
}))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(geometry.x(), 0);
|
||||
assert_eq!(geometry.y(), 0);
|
||||
|
|
@ -278,6 +276,7 @@ struct Connection {
|
|||
atoms: Atoms,
|
||||
root: x::Window,
|
||||
visual: u32,
|
||||
wm_window: x::Window,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for Connection {
|
||||
|
|
@ -289,7 +288,18 @@ impl std::ops::Deref for Connection {
|
|||
|
||||
impl Connection {
|
||||
fn new(display: &str) -> Self {
|
||||
let (inner, _) = xcb::Connection::connect(Some(display)).unwrap();
|
||||
let (inner, _) =
|
||||
xcb::Connection::connect_with_extensions(Some(display), &[xcb::Extension::XFixes], &[])
|
||||
.unwrap();
|
||||
// xfixes init
|
||||
let reply = inner
|
||||
.wait_for_reply(inner.send_request(&xcb::xfixes::QueryVersion {
|
||||
client_major_version: 1,
|
||||
client_minor_version: 0,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(reply.major_version(), 1);
|
||||
|
||||
let fd = unsafe { BorrowedFd::borrow_raw(inner.as_raw_fd()) };
|
||||
let pollfd = PollFd::from_borrowed_fd(fd, PollFlags::IN);
|
||||
let atoms = Atoms::intern_all(&inner).unwrap();
|
||||
|
|
@ -297,12 +307,25 @@ impl Connection {
|
|||
let root = screen.root();
|
||||
let visual = screen.root_visual();
|
||||
|
||||
let wm_window: x::Window = inner
|
||||
.wait_for_reply(inner.send_request(&x::GetProperty {
|
||||
delete: false,
|
||||
window: root,
|
||||
property: atoms.wm_check,
|
||||
r#type: x::ATOM_WINDOW,
|
||||
long_offset: 0,
|
||||
long_length: 1,
|
||||
}))
|
||||
.expect("Couldn't get WM window")
|
||||
.value()[0];
|
||||
|
||||
Self {
|
||||
inner,
|
||||
pollfd,
|
||||
atoms,
|
||||
root,
|
||||
visual,
|
||||
wm_window,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -368,6 +391,7 @@ impl Connection {
|
|||
|
||||
#[track_caller]
|
||||
fn await_event(&mut self) {
|
||||
self.pollfd.clear_revents();
|
||||
assert!(
|
||||
poll(&mut [self.pollfd.clone()], 100).expect("poll failed") > 0,
|
||||
"Did not get any X11 events"
|
||||
|
|
@ -393,12 +417,29 @@ impl Connection {
|
|||
time: x::CURRENT_TIME,
|
||||
})
|
||||
.unwrap();
|
||||
let owner = self
|
||||
.wait_for_reply(self.send_request(&x::GetSelectionOwner {
|
||||
let owner = self.get_reply(&x::GetSelectionOwner {
|
||||
selection: self.atoms.clipboard,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_eq!(window, owner.owner());
|
||||
});
|
||||
|
||||
assert_eq!(window, owner.owner(), "Unexpected selection owner");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn await_selection_request(&mut self) -> x::SelectionRequestEvent {
|
||||
self.await_event();
|
||||
match self.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
||||
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn await_selection_notify(&mut self) -> x::SelectionNotifyEvent {
|
||||
self.await_event();
|
||||
match self.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
|
||||
other => panic!("Didn't get selection notify event, instead got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
|
|
@ -419,11 +460,35 @@ impl Connection {
|
|||
}
|
||||
|
||||
#[track_caller]
|
||||
fn atom_name(&self, atom: x::Atom) -> String {
|
||||
self.get_reply(&x::GetAtomName { atom })
|
||||
.name()
|
||||
.as_ascii()
|
||||
.to_string()
|
||||
fn verify_clipboard_owner(&self, window: x::Window) {
|
||||
let owner = self.get_reply(&x::GetSelectionOwner {
|
||||
selection: self.atoms.clipboard,
|
||||
});
|
||||
assert_eq!(owner.owner(), window, "Clipboard owner does not match");
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn await_selection_owner_change(&mut self) -> xcb::xfixes::SelectionNotifyEvent {
|
||||
self.await_event();
|
||||
match self.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::XFixes(xcb::xfixes::Event::SelectionNotify(e))) => e,
|
||||
other => panic!("Expected XFixes SelectionNotify, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn get_selection_owner_change_events(&self, enable: bool, window: x::Window) {
|
||||
let event_mask = if enable {
|
||||
xcb::xfixes::SelectionEventMask::SET_SELECTION_OWNER
|
||||
} else {
|
||||
xcb::xfixes::SelectionEventMask::empty()
|
||||
};
|
||||
self.send_and_check_request(&xcb::xfixes::SelectSelectionInput {
|
||||
window,
|
||||
selection: self.atoms.clipboard,
|
||||
event_mask,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -607,22 +672,17 @@ fn input_focus() {
|
|||
let conn = std::cell::RefCell::new(&mut connection);
|
||||
let check_focus = |win: x::Window| {
|
||||
let connection = conn.borrow();
|
||||
let focus = connection
|
||||
.wait_for_reply(connection.send_request(&x::GetInputFocus {}))
|
||||
.unwrap()
|
||||
.focus();
|
||||
let focus = connection.get_reply(&x::GetInputFocus {}).focus();
|
||||
assert_eq!(win, focus);
|
||||
|
||||
let reply = connection
|
||||
.wait_for_reply(connection.send_request(&x::GetProperty {
|
||||
let reply = connection.get_reply(&x::GetProperty {
|
||||
delete: false,
|
||||
window: connection.root,
|
||||
property: connection.atoms.net_active_window,
|
||||
r#type: x::ATOM_WINDOW,
|
||||
long_offset: 0,
|
||||
long_length: 1,
|
||||
}))
|
||||
.unwrap();
|
||||
});
|
||||
|
||||
assert_eq!(&[win], reply.value::<x::Window>());
|
||||
};
|
||||
|
|
@ -717,23 +777,10 @@ fn copy_from_x11() {
|
|||
let mut connection = Connection::new(&f.display);
|
||||
|
||||
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||
connection.map_window(window);
|
||||
f.wait_and_dispatch();
|
||||
let surface = f
|
||||
.testwl
|
||||
.last_created_surface_id()
|
||||
.expect("No surface created");
|
||||
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
|
||||
|
||||
f.map_as_toplevel(&mut connection, window);
|
||||
connection.set_selection_owner(window);
|
||||
|
||||
// wait for requests to come through
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
||||
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
||||
};
|
||||
|
||||
let request = connection.await_selection_request();
|
||||
assert_eq!(request.target(), connection.atoms.targets);
|
||||
connection.set_property(
|
||||
request.requestor(),
|
||||
|
|
@ -742,75 +789,61 @@ fn copy_from_x11() {
|
|||
&[connection.atoms.mime1, connection.atoms.mime2],
|
||||
);
|
||||
connection.send_selection_notify(&request);
|
||||
|
||||
connection.await_event();
|
||||
let mut mime_data = vec![
|
||||
(
|
||||
connection.atoms.mime1,
|
||||
x::ATOM_STRING,
|
||||
b"hello world".as_slice(),
|
||||
),
|
||||
(connection.atoms.mime2, x::ATOM_INTEGER, &[1u8, 2, 3, 4]),
|
||||
];
|
||||
|
||||
while let Some(request) = connection.poll_for_event().unwrap() {
|
||||
let xcb::Event::X(x::Event::SelectionRequest(request)) = request else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let target = request.target();
|
||||
let Some(idx) = mime_data.iter().position(|(atom, _, _)| *atom == target) else {
|
||||
panic!("Expected atom in {mime_data:?}, got {target:?}");
|
||||
};
|
||||
|
||||
let (_, ty, data) = mime_data.swap_remove(idx);
|
||||
connection.set_property(request.requestor(), ty, request.property(), data);
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::SendEvent {
|
||||
propagate: false,
|
||||
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();
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
|
||||
assert!(
|
||||
mime_data.is_empty(),
|
||||
"Didn't get all mime types: {mime_data:?}"
|
||||
);
|
||||
f.wait_and_dispatch();
|
||||
|
||||
let owner = connection
|
||||
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
|
||||
selection: connection.atoms.clipboard,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_ne!(window, owner.owner());
|
||||
struct MimeData {
|
||||
mime: x::Atom,
|
||||
data: testwl::PasteData,
|
||||
}
|
||||
let mimes_truth = [
|
||||
MimeData {
|
||||
mime: connection.atoms.mime1,
|
||||
data: testwl::PasteData {
|
||||
mime_type: "text/plain".to_string(),
|
||||
data: b"hello world".to_vec(),
|
||||
},
|
||||
},
|
||||
MimeData {
|
||||
mime: connection.atoms.mime2,
|
||||
data: testwl::PasteData {
|
||||
mime_type: "blah/blah".to_string(),
|
||||
data: vec![1, 2, 3, 4],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let mimes = f.testwl.data_source_mimes();
|
||||
let advertised_mimes = f.testwl.data_source_mimes();
|
||||
assert_eq!(
|
||||
advertised_mimes.len(),
|
||||
mimes_truth.len(),
|
||||
"Wrong number of advertised mimes: {advertised_mimes:?}"
|
||||
);
|
||||
for MimeData { data, .. } in &mimes_truth {
|
||||
assert!(
|
||||
mimes.contains(&"text/plain".into()),
|
||||
"text/plain not in mimes: {mimes:?}"
|
||||
); // mime1
|
||||
assert!(
|
||||
mimes.contains(&"blah/blah".into()),
|
||||
"blah/blah not in mimes: {mimes:?}"
|
||||
); // mime2
|
||||
advertised_mimes.contains(&data.mime_type),
|
||||
"Missing mime type {}",
|
||||
data.mime_type
|
||||
);
|
||||
}
|
||||
|
||||
let data = f.testwl.paste_data();
|
||||
f.testwl.dispatch();
|
||||
let data = data.resolve();
|
||||
let data = f.testwl.paste_data(|mime, _| {
|
||||
let request = connection.await_selection_request();
|
||||
let data = mimes_truth
|
||||
.iter()
|
||||
.find(|data| data.data.mime_type == mime)
|
||||
.unwrap_or_else(|| panic!("Asked for unknown mime: {mime}"));
|
||||
connection.set_property(
|
||||
request.requestor(),
|
||||
data.mime,
|
||||
request.property(),
|
||||
&data.data.data,
|
||||
);
|
||||
connection.send_selection_notify(&request);
|
||||
true
|
||||
});
|
||||
let mut found_mimes = Vec::new();
|
||||
for testwl::PasteData { mime_type, data } in data {
|
||||
match mime_type {
|
||||
match &mime_type {
|
||||
x if x == "text/plain" => {
|
||||
assert_eq!(&data, b"hello world");
|
||||
}
|
||||
|
|
@ -819,7 +852,17 @@ fn copy_from_x11() {
|
|||
}
|
||||
other => panic!("unexpected mime type: {other} ({data:?})"),
|
||||
}
|
||||
found_mimes.push(mime_type);
|
||||
}
|
||||
|
||||
assert!(
|
||||
found_mimes.contains(&"text/plain".to_string()),
|
||||
"Didn't get mime data for text/plain"
|
||||
);
|
||||
assert!(
|
||||
found_mimes.contains(&"blah/blah".to_string()),
|
||||
"Didn't get mime data for blah/blah"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -828,13 +871,8 @@ fn copy_from_wayland() {
|
|||
let mut connection = Connection::new(&f.display);
|
||||
|
||||
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||
connection.map_window(window);
|
||||
f.wait_and_dispatch();
|
||||
let surface = f
|
||||
.testwl
|
||||
.last_created_surface_id()
|
||||
.expect("No surface created");
|
||||
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
|
||||
connection.get_selection_owner_change_events(true, window);
|
||||
f.map_as_toplevel(&mut connection, window);
|
||||
let offer = vec![
|
||||
testwl::PasteData {
|
||||
mime_type: "text/plain".into(),
|
||||
|
|
@ -847,22 +885,10 @@ fn copy_from_wayland() {
|
|||
];
|
||||
|
||||
f.testwl.create_data_offer(offer.clone());
|
||||
connection.await_selection_owner_change();
|
||||
connection.verify_clipboard_owner(connection.wm_window);
|
||||
connection.get_selection_owner_change_events(false, window);
|
||||
|
||||
let wm_window: x::Window = connection
|
||||
.get_reply(&x::GetProperty {
|
||||
delete: false,
|
||||
window: connection.root,
|
||||
property: connection.atoms.wm_check,
|
||||
r#type: x::ATOM_WINDOW,
|
||||
long_offset: 0,
|
||||
long_length: 1,
|
||||
})
|
||||
.value()[0];
|
||||
|
||||
let reply = connection.get_reply(&x::GetSelectionOwner {
|
||||
selection: connection.atoms.clipboard,
|
||||
});
|
||||
assert_eq!(reply.owner(), wm_window);
|
||||
let dest1_atom = connection
|
||||
.get_reply(&x::InternAtom {
|
||||
name: b"dest1",
|
||||
|
|
@ -870,9 +896,6 @@ fn copy_from_wayland() {
|
|||
})
|
||||
.atom();
|
||||
|
||||
// I don't know why, but omitting this little sleep prevents the SelectionRequest notification
|
||||
// from being sent, and I don't have the heart to determine why.
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
connection
|
||||
.send_and_check_request(&x::ConvertSelection {
|
||||
requestor: window,
|
||||
|
|
@ -883,12 +906,7 @@ fn copy_from_wayland() {
|
|||
})
|
||||
.unwrap();
|
||||
|
||||
connection.await_event();
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
|
||||
other => panic!("Didn't get selection notify event, instead got {other:?}"),
|
||||
};
|
||||
|
||||
let request = connection.await_selection_notify();
|
||||
assert_eq!(request.requestor(), window);
|
||||
assert_eq!(request.selection(), connection.atoms.clipboard);
|
||||
assert_eq!(request.target(), connection.atoms.targets);
|
||||
|
|
@ -927,11 +945,7 @@ fn copy_from_wayland() {
|
|||
.unwrap();
|
||||
|
||||
f.wait_and_dispatch();
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionNotify(r))) => r,
|
||||
other => panic!("Didn't get selection notify event, instead got {other:?}"),
|
||||
};
|
||||
|
||||
let request = connection.await_selection_notify();
|
||||
assert_eq!(request.requestor(), window);
|
||||
assert_eq!(request.selection(), connection.atoms.clipboard);
|
||||
assert_eq!(request.target(), atom);
|
||||
|
|
@ -974,7 +988,7 @@ fn different_output_position() {
|
|||
f.testwl.move_pointer_to(surface, 10.0, 10.0);
|
||||
f.wait_and_dispatch();
|
||||
let reply = connection.get_reply(&x::QueryPointer { window });
|
||||
assert_eq!(reply.same_screen(), true);
|
||||
assert!(reply.same_screen());
|
||||
assert_eq!(reply.win_x(), 10);
|
||||
assert_eq!(reply.win_y(), 10);
|
||||
|
||||
|
|
@ -985,7 +999,7 @@ fn different_output_position() {
|
|||
f.wait_and_dispatch();
|
||||
let reply = connection.get_reply(&x::QueryPointer { window });
|
||||
println!("reply: {reply:?}");
|
||||
assert_eq!(reply.same_screen(), true);
|
||||
assert!(reply.same_screen());
|
||||
assert_eq!(reply.win_x(), 150);
|
||||
assert_eq!(reply.win_y(), 12);
|
||||
}
|
||||
|
|
@ -1002,20 +1016,9 @@ fn bad_clipboard_data() {
|
|||
.last_created_surface_id()
|
||||
.expect("No surface created");
|
||||
f.configure_and_verify_new_toplevel(&mut connection, window, surface);
|
||||
connection.set_selection_owner(window);
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::SetSelectionOwner {
|
||||
owner: window,
|
||||
selection: connection.atoms.clipboard,
|
||||
time: x::CURRENT_TIME,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
connection.await_event();
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
||||
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
||||
};
|
||||
let request = connection.await_selection_request();
|
||||
assert_eq!(request.target(), connection.atoms.targets);
|
||||
connection.set_property(
|
||||
request.requestor(),
|
||||
|
|
@ -1023,73 +1026,22 @@ fn bad_clipboard_data() {
|
|||
request.property(),
|
||||
&[connection.atoms.mime2],
|
||||
);
|
||||
connection
|
||||
.send_and_check_request(&x::SendEvent {
|
||||
propagate: false,
|
||||
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();
|
||||
connection.send_selection_notify(&request);
|
||||
|
||||
connection.await_event();
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
||||
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
||||
};
|
||||
f.wait_and_dispatch();
|
||||
let mut data = f.testwl.paste_data(|_, _| {
|
||||
let request = connection.await_selection_request();
|
||||
assert_eq!(request.target(), connection.atoms.mime2);
|
||||
|
||||
// Don't actually set any data as requested - just report success
|
||||
connection.send_selection_notify(&request);
|
||||
true
|
||||
});
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::SendEvent {
|
||||
propagate: false,
|
||||
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();
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
let owner = connection
|
||||
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
|
||||
selection: connection.atoms.clipboard,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_ne!(window, owner.owner());
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::ConvertSelection {
|
||||
requestor: window,
|
||||
selection: connection.atoms.clipboard,
|
||||
target: connection.atoms.mime2,
|
||||
property: connection.atoms.mime1,
|
||||
time: x::CURRENT_TIME,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
connection.await_event();
|
||||
let mut e = None;
|
||||
while let Some(event) = connection.poll_for_event().unwrap() {
|
||||
if let xcb::Event::X(x::Event::SelectionNotify(event)) = event {
|
||||
e = Some(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let e = e.expect("No selection notify event");
|
||||
assert_eq!(e.property(), x::ATOM_NONE);
|
||||
connection.verify_clipboard_owner(window);
|
||||
assert_eq!(data.len(), 1, "Unexpected data: {data:?}");
|
||||
let data = data.pop().unwrap();
|
||||
assert_eq!(data.mime_type, "blah/blah");
|
||||
assert!(data.data.is_empty(), "Unexpected data: {:?}", data.data);
|
||||
}
|
||||
|
||||
// issue #42
|
||||
|
|
@ -1161,7 +1113,7 @@ fn primary_output() {
|
|||
|
||||
let reply = conn.get_reply(&xcb::randr::GetScreenResources { window: conn.root });
|
||||
let config_timestamp = reply.config_timestamp();
|
||||
let mut it = reply.outputs().into_iter().copied().map(|output| {
|
||||
let mut it = reply.outputs().iter().copied().map(|output| {
|
||||
let reply = conn.get_reply(&xcb::randr::GetOutputInfo {
|
||||
output,
|
||||
config_timestamp,
|
||||
|
|
@ -1214,21 +1166,15 @@ fn primary_output() {
|
|||
assert_eq!(reply.output(), output3);
|
||||
}
|
||||
|
||||
// TODO: these sleeps are horrible.
|
||||
#[test]
|
||||
fn incr_copy_from_x11() {
|
||||
let mut f = Fixture::new();
|
||||
let mut connection = Connection::new(&f.display);
|
||||
|
||||
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||
f.map_as_toplevel(&mut connection, window);
|
||||
|
||||
connection.set_selection_owner(window);
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
||||
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
||||
};
|
||||
let request = connection.await_selection_request();
|
||||
assert_eq!(request.target(), connection.atoms.targets);
|
||||
connection.set_property(
|
||||
request.requestor(),
|
||||
|
|
@ -1237,14 +1183,12 @@ fn incr_copy_from_x11() {
|
|||
&[connection.atoms.targets, connection.atoms.mime1],
|
||||
);
|
||||
connection.send_selection_notify(&request);
|
||||
connection.await_event();
|
||||
f.wait_and_dispatch();
|
||||
|
||||
let request = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::SelectionRequest(r))) => r,
|
||||
other => panic!("Didn't get selection request event, instead got {other:?}"),
|
||||
};
|
||||
let mut destination_property = x::Atom::none();
|
||||
let mut begin_incr = Some(|connection: &mut Connection| {
|
||||
let request = connection.await_selection_request();
|
||||
assert_eq!(request.target(), connection.atoms.mime1);
|
||||
let destination_property = request.property();
|
||||
|
||||
connection
|
||||
.send_and_check_request(&x::ChangeWindowAttributes {
|
||||
|
|
@ -1255,7 +1199,7 @@ fn incr_copy_from_x11() {
|
|||
connection.set_property(
|
||||
request.requestor(),
|
||||
connection.atoms.incr,
|
||||
destination_property,
|
||||
request.property(),
|
||||
&[3000u32],
|
||||
);
|
||||
connection.send_selection_notify(&request);
|
||||
|
|
@ -1266,25 +1210,38 @@ fn incr_copy_from_x11() {
|
|||
};
|
||||
assert_eq!(notify.atom(), request.property());
|
||||
assert_eq!(notify.state(), x::Property::NewValue);
|
||||
request.property()
|
||||
});
|
||||
|
||||
let data: Vec<u8> = std::iter::successors(Some(1u8), |n| Some(n.wrapping_add(1)))
|
||||
.take(3000)
|
||||
.collect();
|
||||
for (idx, chunk) in data.chunks(500).enumerate() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
let mut it = data.chunks(500).enumerate();
|
||||
let mut paste_data = f.testwl.paste_data(|_, testwl| {
|
||||
if let Some(begin) = begin_incr.take() {
|
||||
destination_property = begin(&mut connection);
|
||||
testwl.dispatch();
|
||||
return false;
|
||||
}
|
||||
assert_ne!(destination_property, x::Atom::none());
|
||||
|
||||
connection.await_event();
|
||||
let notify = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
|
||||
other => panic!("Didn't get property notify event, instead got {other:?}"),
|
||||
};
|
||||
|
||||
match it.next() {
|
||||
Some((idx, chunk)) => {
|
||||
assert_eq!(notify.atom(), destination_property, "chunk {idx}");
|
||||
assert_eq!(notify.state(), x::Property::Delete, "chunk {idx}");
|
||||
|
||||
connection.set_property(
|
||||
request.requestor(),
|
||||
notify.window(),
|
||||
connection.atoms.mime1,
|
||||
destination_property,
|
||||
chunk,
|
||||
);
|
||||
testwl.dispatch();
|
||||
// skip NewValue
|
||||
let notify = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
|
||||
|
|
@ -1292,37 +1249,60 @@ fn incr_copy_from_x11() {
|
|||
};
|
||||
assert_eq!(notify.atom(), destination_property, "chunk {idx}");
|
||||
assert_eq!(notify.state(), x::Property::NewValue, "chunk {idx}");
|
||||
false
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
let notify = match connection.poll_for_event().unwrap() {
|
||||
Some(xcb::Event::X(x::Event::PropertyNotify(p))) => p,
|
||||
other => panic!("Didn't get property notify event, instead got {other:?}"),
|
||||
};
|
||||
None => {
|
||||
// INCR completed!
|
||||
assert_eq!(notify.atom(), destination_property);
|
||||
assert_eq!(notify.state(), x::Property::Delete);
|
||||
connection.set_property::<u8>(
|
||||
request.requestor(),
|
||||
notify.window(),
|
||||
connection.atoms.mime1,
|
||||
destination_property,
|
||||
&[],
|
||||
);
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
let owner = connection
|
||||
.wait_for_reply(connection.send_request(&x::GetSelectionOwner {
|
||||
selection: connection.atoms.clipboard,
|
||||
}))
|
||||
.unwrap();
|
||||
assert_ne!(window, owner.owner());
|
||||
f.wait_and_dispatch();
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(f.testwl.data_source_mimes(), vec!["text/plain"]);
|
||||
let wl_data = f.testwl.paste_data();
|
||||
f.testwl.dispatch();
|
||||
let mut wl_data = wl_data.resolve();
|
||||
assert_eq!(wl_data.len(), 1);
|
||||
let wl_data = wl_data.swap_remove(0);
|
||||
assert_eq!(wl_data.mime_type, "text/plain");
|
||||
assert_eq!(&wl_data.data, &data);
|
||||
assert_eq!(paste_data.len(), 1);
|
||||
let paste_data = paste_data.swap_remove(0);
|
||||
assert_eq!(paste_data.mime_type, "text/plain");
|
||||
assert_eq!(&paste_data.data, &data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wayland_then_x11_clipboard_owner() {
|
||||
let mut f = Fixture::new();
|
||||
let mut connection = Connection::new(&f.display);
|
||||
let window = connection.new_window(connection.root, 0, 0, 20, 20, false);
|
||||
connection.get_selection_owner_change_events(true, window);
|
||||
|
||||
f.map_as_toplevel(&mut connection, window);
|
||||
let offer = vec![
|
||||
testwl::PasteData {
|
||||
mime_type: "text/plain".into(),
|
||||
data: b"boingloings".to_vec(),
|
||||
},
|
||||
testwl::PasteData {
|
||||
mime_type: "yah/hah".into(),
|
||||
data: vec![1, 2, 3, 2, 1],
|
||||
},
|
||||
];
|
||||
f.testwl.create_data_offer(offer.clone());
|
||||
|
||||
connection.await_selection_owner_change();
|
||||
connection.verify_clipboard_owner(connection.wm_window);
|
||||
connection.get_selection_owner_change_events(false, window);
|
||||
|
||||
connection.set_selection_owner(window);
|
||||
f.testwl.dispatch();
|
||||
connection.verify_clipboard_owner(window);
|
||||
|
||||
connection.await_event();
|
||||
let request = connection.await_selection_request();
|
||||
assert_eq!(request.selection(), connection.atoms.clipboard);
|
||||
assert_eq!(request.target(), connection.atoms.targets);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -503,6 +503,7 @@ impl Server {
|
|||
self.state.pointer.as_ref().unwrap()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn data_source_mimes(&self) -> Vec<String> {
|
||||
let Some(selection) = &self.state.selection else {
|
||||
panic!("No selection set on data device");
|
||||
|
|
@ -513,13 +514,79 @@ impl Server {
|
|||
data.mimes.to_vec()
|
||||
}
|
||||
|
||||
pub fn paste_data(&mut self) -> PasteDataResolver {
|
||||
let Some(selection) = &self.state.selection else {
|
||||
#[track_caller]
|
||||
pub fn paste_data(
|
||||
&mut self,
|
||||
mut send_data_for_mime: impl FnMut(&str, &mut Self) -> bool,
|
||||
) -> Vec<PasteData> {
|
||||
struct PendingData {
|
||||
rx: std::fs::File,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
let Some(selection) = self.state.selection.take() else {
|
||||
panic!("No selection set on data device");
|
||||
};
|
||||
type PendingRet = Vec<(String, Option<PendingData>)>;
|
||||
let mut pending_ret: PendingRet = {
|
||||
let data: &Mutex<DataSourceData> = selection.data().unwrap();
|
||||
data.lock()
|
||||
.unwrap()
|
||||
.mimes
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|mime| (mime.clone(), None))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let ret = PasteDataResolver::new(&selection);
|
||||
let mut ret = Vec::new();
|
||||
let mut try_transfer =
|
||||
|pending_ret: &mut PendingRet, mime: String, mut pending: PendingData| {
|
||||
self.display.flush_clients().unwrap();
|
||||
let transfer_complete = send_data_for_mime(&mime, self);
|
||||
if transfer_complete {
|
||||
pending.rx.read_to_end(&mut pending.data).unwrap();
|
||||
ret.push(PasteData {
|
||||
mime_type: mime,
|
||||
data: pending.data,
|
||||
});
|
||||
} else {
|
||||
loop {
|
||||
match pending.rx.read(&mut pending.data) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||
Err(e) => panic!("Failed reading data for mime {mime}: {e:?}"),
|
||||
}
|
||||
}
|
||||
pending_ret.push((mime, Some(pending)));
|
||||
self.dispatch();
|
||||
}
|
||||
};
|
||||
|
||||
while !pending_ret.is_empty() {
|
||||
let (mime, pending) = pending_ret.pop().unwrap();
|
||||
match pending {
|
||||
Some(pending) => try_transfer(&mut pending_ret, mime, pending),
|
||||
None => {
|
||||
let (rx, tx) = rustix::pipe::pipe().unwrap();
|
||||
selection.send(mime.clone(), tx.as_fd());
|
||||
drop(tx);
|
||||
|
||||
let rx = std::fs::File::from(rx);
|
||||
try_transfer(
|
||||
&mut pending_ret,
|
||||
mime,
|
||||
PendingData {
|
||||
rx,
|
||||
data: Vec::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state.selection = Some(selection);
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
|
|
@ -527,6 +594,7 @@ impl Server {
|
|||
self.state.selection.is_none()
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn create_data_offer(&mut self, data: Vec<PasteData>) {
|
||||
let Some(dev) = &self.state.data_device else {
|
||||
panic!("No data device created");
|
||||
|
|
@ -611,45 +679,6 @@ impl Server {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct PasteDataResolver {
|
||||
fds: Vec<(String, OwnedFd, OwnedFd)>,
|
||||
}
|
||||
|
||||
impl PasteDataResolver {
|
||||
fn new(source: &WlDataSource) -> Self {
|
||||
let data: &Mutex<DataSourceData> = source.data().unwrap();
|
||||
let data = data.lock().unwrap();
|
||||
let mimes = &data.mimes;
|
||||
|
||||
let fds = mimes
|
||||
.iter()
|
||||
.map(|mime| {
|
||||
let (rx, tx) = rustix::pipe::pipe().unwrap();
|
||||
source.send(mime.clone(), tx.as_fd());
|
||||
(mime.clone(), tx, rx)
|
||||
})
|
||||
.collect();
|
||||
|
||||
PasteDataResolver { fds }
|
||||
}
|
||||
|
||||
pub fn resolve(self) -> Vec<PasteData> {
|
||||
self.fds
|
||||
.into_iter()
|
||||
.map(|(mime, tx, rx)| {
|
||||
drop(tx);
|
||||
let mut data = Vec::new();
|
||||
let mut file = std::fs::File::from(rx);
|
||||
file.read_to_end(&mut data).unwrap();
|
||||
PasteData {
|
||||
mime_type: mime,
|
||||
data,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Eq, PartialEq, Debug)]
|
||||
pub struct PasteData {
|
||||
pub mime_type: String,
|
||||
|
|
@ -857,6 +886,7 @@ impl Dispatch<WlDataOffer, Vec<PasteData>> for State {
|
|||
let mut stream = UnixStream::from(fd);
|
||||
stream.write_all(&data[pos].data).unwrap();
|
||||
}
|
||||
wl_data_offer::Request::Destroy => {}
|
||||
other => todo!("unhandled request: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue