Reapply support for pasting images on x11 (#32121)
This brings back [linux(x11): Add support for pasting images from clipboard · Pull Request #29387](https://github.com/zed-industries/zed/pull/29387) while fixing #30523 (which caused it to be reverted). Commit message from that PR: > Closes: https://github.com/zed-industries/zed/pull/29177#issuecomment-2823359242 > > Removes dependency on [quininer/x11-clipboard](https://github.com/quininer/x11-clipboard) as it is in [maintenance mode](https://github.com/quininer/x11-clipboard/issues/19). > > X11 clipboard functionality is now built-in to GPUI which was accomplished by stripping the non-x11-related code/abstractions from [1Password/arboard](https://github.com/1Password/arboard) and extending it to support all image formats already supported by GPUI on wayland and macos. > > A benefit of switching over to the `arboard` implementation, is that we now make an attempt to have an X11 "clipboard manager" (if available - something the user has to setup themselves) save the contents of clipboard (if the last copy operation was within Zed) so that the copied contents can still be pasted once Zed has completely stopped. Before the fix for reapply, it was iterating through the formats and requesting conversion to each. Some clipboard providers just respond with a different format rather than saying the format is unsupported. The fix is to use this response if it matches a supported format. It also now typically avoids this iteration by requesting the `TARGETS` and taking the highest precedence supported target. Closes #30523 Release Notes: - Linux (X11): Restored the ability to paste images. --------- Co-authored-by: Ben <ben@zed.dev>
This commit is contained in:
parent
a2e98e9f0e
commit
3d9881121f
3 changed files with 126 additions and 66 deletions
|
@ -1,4 +1,5 @@
|
|||
mod client;
|
||||
mod clipboard;
|
||||
mod display;
|
||||
mod event;
|
||||
mod window;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use crate::platform::scap_screen_capture::scap_screen_sources;
|
||||
use core::str;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
|
@ -41,8 +40,9 @@ use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSIO
|
|||
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE};
|
||||
|
||||
use super::{
|
||||
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail, get_valuator_axis_index,
|
||||
modifiers_from_state, pressed_button_from_mask,
|
||||
ButtonOrScroll, ScrollDirection, button_or_scroll_from_event_detail,
|
||||
clipboard::{self, Clipboard},
|
||||
get_valuator_axis_index, modifiers_from_state, pressed_button_from_mask,
|
||||
};
|
||||
use super::{X11Display, X11WindowStatePtr, XcbAtoms};
|
||||
use super::{XimCallbackEvent, XimHandler};
|
||||
|
@ -56,6 +56,7 @@ use crate::platform::{
|
|||
reveal_path_internal,
|
||||
xdg_desktop_portal::{Event as XDPEvent, XDPEventSource},
|
||||
},
|
||||
scap_screen_capture::scap_screen_sources,
|
||||
};
|
||||
use crate::{
|
||||
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
|
||||
|
@ -201,7 +202,7 @@ pub struct X11ClientState {
|
|||
pointer_device_states: BTreeMap<xinput::DeviceId, PointerDeviceState>,
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
pub(crate) clipboard: Clipboard,
|
||||
pub(crate) clipboard_item: Option<ClipboardItem>,
|
||||
pub(crate) xdnd_state: Xdnd,
|
||||
}
|
||||
|
@ -388,7 +389,7 @@ impl X11Client {
|
|||
.reply()
|
||||
.unwrap();
|
||||
|
||||
let clipboard = x11_clipboard::Clipboard::new().unwrap();
|
||||
let clipboard = Clipboard::new().unwrap();
|
||||
|
||||
let xcb_connection = Rc::new(xcb_connection);
|
||||
|
||||
|
@ -1504,39 +1505,36 @@ impl LinuxClient for X11Client {
|
|||
let state = self.0.borrow_mut();
|
||||
state
|
||||
.clipboard
|
||||
.store(
|
||||
state.clipboard.setter.atoms.primary,
|
||||
state.clipboard.setter.atoms.utf8_string,
|
||||
item.text().unwrap_or_default().as_bytes(),
|
||||
.set_text(
|
||||
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
|
||||
clipboard::ClipboardKind::Primary,
|
||||
clipboard::WaitConfig::None,
|
||||
)
|
||||
.ok();
|
||||
.context("Failed to write to clipboard (primary)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state
|
||||
.clipboard
|
||||
.store(
|
||||
state.clipboard.setter.atoms.clipboard,
|
||||
state.clipboard.setter.atoms.utf8_string,
|
||||
item.text().unwrap_or_default().as_bytes(),
|
||||
.set_text(
|
||||
std::borrow::Cow::Owned(item.text().unwrap_or_default()),
|
||||
clipboard::ClipboardKind::Clipboard,
|
||||
clipboard::WaitConfig::None,
|
||||
)
|
||||
.ok();
|
||||
.context("Failed to write to clipboard (clipboard)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
state.clipboard_item.replace(item);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
let state = self.0.borrow_mut();
|
||||
state
|
||||
return state
|
||||
.clipboard
|
||||
.load(
|
||||
state.clipboard.getter.atoms.primary,
|
||||
state.clipboard.getter.atoms.utf8_string,
|
||||
state.clipboard.getter.atoms.property,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
|
||||
.ok()
|
||||
.get_any(clipboard::ClipboardKind::Primary)
|
||||
.context("Failed to read from clipboard (primary)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
|
@ -1545,26 +1543,15 @@ impl LinuxClient for X11Client {
|
|||
// which has metadata attached.
|
||||
if state
|
||||
.clipboard
|
||||
.setter
|
||||
.connection
|
||||
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
|
||||
.ok()
|
||||
.and_then(|r| r.reply().ok())
|
||||
.map(|reply| reply.owner == state.clipboard.setter.window)
|
||||
.unwrap_or(false)
|
||||
.is_owner(clipboard::ClipboardKind::Clipboard)
|
||||
{
|
||||
return state.clipboard_item.clone();
|
||||
}
|
||||
state
|
||||
return state
|
||||
.clipboard
|
||||
.load(
|
||||
state.clipboard.getter.atoms.clipboard,
|
||||
state.clipboard.getter.atoms.utf8_string,
|
||||
state.clipboard.getter.atoms.property,
|
||||
Duration::from_secs(3),
|
||||
)
|
||||
.map(|text| crate::ClipboardItem::new_string(String::from_utf8(text).unwrap()))
|
||||
.ok()
|
||||
.get_any(clipboard::ClipboardKind::Clipboard)
|
||||
.context("Failed to read from clipboard (clipboard)")
|
||||
.log_with_level(log::Level::Debug);
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
|
|
|
@ -200,7 +200,7 @@ struct ClipboardData {
|
|||
}
|
||||
|
||||
enum ReadSelNotifyResult {
|
||||
GotData(Vec<u8>),
|
||||
GotData(ClipboardData),
|
||||
IncrStarted,
|
||||
EventNotRecognized,
|
||||
}
|
||||
|
@ -297,30 +297,83 @@ impl Inner {
|
|||
}
|
||||
let reader = XContext::new()?;
|
||||
|
||||
log::trace!("Trying to get the clipboard data.");
|
||||
let highest_precedence_format =
|
||||
match self.read_single(&reader, selection, self.atoms.TARGETS) {
|
||||
Err(err) => {
|
||||
log::trace!("Clipboard TARGETS query failed with {err:?}");
|
||||
None
|
||||
}
|
||||
Ok(ClipboardData { bytes, format }) => {
|
||||
if format == self.atoms.ATOM {
|
||||
let available_formats = Self::parse_formats(&bytes);
|
||||
formats
|
||||
.iter()
|
||||
.find(|format| available_formats.contains(format))
|
||||
} else {
|
||||
log::trace!(
|
||||
"Unexpected clipboard TARGETS format {}",
|
||||
self.atom_name(format)
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(&format) = highest_precedence_format {
|
||||
let data = self.read_single(&reader, selection, format)?;
|
||||
if !formats.contains(&data.format) {
|
||||
// This shouldn't happen since the format is from the TARGETS list.
|
||||
log::trace!(
|
||||
"Conversion to {} responded with {} which is not supported",
|
||||
self.atom_name(format),
|
||||
self.atom_name(data.format),
|
||||
);
|
||||
return Err(Error::ConversionFailure);
|
||||
}
|
||||
return Ok(data);
|
||||
}
|
||||
|
||||
log::trace!("Falling back on attempting to convert clipboard to each format.");
|
||||
for format in formats {
|
||||
match self.read_single(&reader, selection, *format) {
|
||||
Ok(bytes) => {
|
||||
return Ok(ClipboardData {
|
||||
bytes,
|
||||
format: *format,
|
||||
});
|
||||
Ok(data) => {
|
||||
if formats.contains(&data.format) {
|
||||
return Ok(data);
|
||||
} else {
|
||||
log::trace!(
|
||||
"Conversion to {} responded with {} which is not supported",
|
||||
self.atom_name(*format),
|
||||
self.atom_name(data.format),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Err(Error::ContentNotAvailable) => {
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
Err(e) => {
|
||||
log::trace!("Conversion to {} failed: {}", self.atom_name(*format), e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
log::trace!("All conversions to supported formats failed.");
|
||||
Err(Error::ContentNotAvailable)
|
||||
}
|
||||
|
||||
fn parse_formats(bytes: &[u8]) -> Vec<Atom> {
|
||||
bytes
|
||||
.chunks_exact(4)
|
||||
.map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn read_single(
|
||||
&self,
|
||||
reader: &XContext,
|
||||
selection: ClipboardKind,
|
||||
target_format: Atom,
|
||||
) -> Result<Vec<u8>> {
|
||||
) -> Result<ClipboardData> {
|
||||
// Delete the property so that we can detect (using property notify)
|
||||
// when the selection owner receives our request.
|
||||
reader
|
||||
|
@ -392,10 +445,16 @@ impl Inner {
|
|||
event,
|
||||
)?;
|
||||
if result {
|
||||
return Ok(incr_data);
|
||||
return Ok(ClipboardData {
|
||||
bytes: incr_data,
|
||||
format: target_format,
|
||||
});
|
||||
}
|
||||
}
|
||||
_ => log::trace!("An unexpected event arrived while reading the clipboard."),
|
||||
_ => log::trace!(
|
||||
"An unexpected event arrived while reading the clipboard: {:?}",
|
||||
event
|
||||
),
|
||||
}
|
||||
}
|
||||
log::info!("Time-out hit while reading the clipboard.");
|
||||
|
@ -440,7 +499,7 @@ impl Inner {
|
|||
Ok(current == self.server.win_id)
|
||||
}
|
||||
|
||||
fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
|
||||
fn query_atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> Result<String> {
|
||||
String::from_utf8(
|
||||
self.server
|
||||
.conn
|
||||
|
@ -453,14 +512,14 @@ impl Inner {
|
|||
.map_err(into_unknown)
|
||||
}
|
||||
|
||||
fn atom_name_dbg(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
|
||||
fn atom_name(&self, atom: x11rb::protocol::xproto::Atom) -> &'static str {
|
||||
ATOM_NAME_CACHE.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
match cache.entry(atom) {
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => {
|
||||
let s = self
|
||||
.atom_name(atom)
|
||||
.query_atom_name(atom)
|
||||
.map(|s| Box::leak(s.into_boxed_str()) as &str)
|
||||
.unwrap_or("FAILED-TO-GET-THE-ATOM-NAME");
|
||||
entry.insert(s);
|
||||
|
@ -496,6 +555,12 @@ impl Inner {
|
|||
log::warn!("Received a SelectionNotify while already expecting INCR segments.");
|
||||
return Ok(ReadSelNotifyResult::EventNotRecognized);
|
||||
}
|
||||
// Accept any property type. The property type will typically match the format type except
|
||||
// when it is `TARGETS` in which case it is `ATOM`. `ANY` is provided to handle the case
|
||||
// where the clipboard is not convertible to the requested format. In this case
|
||||
// `reply.type_` will have format information, but `bytes` will only be non-empty if `ANY`
|
||||
// is provided.
|
||||
let property_type = AtomEnum::ANY;
|
||||
// request the selection
|
||||
let mut reply = reader
|
||||
.conn
|
||||
|
@ -503,7 +568,7 @@ impl Inner {
|
|||
true,
|
||||
event.requestor,
|
||||
event.property,
|
||||
event.target,
|
||||
property_type,
|
||||
0,
|
||||
u32::MAX / 4,
|
||||
)
|
||||
|
@ -511,12 +576,8 @@ impl Inner {
|
|||
.reply()
|
||||
.map_err(into_unknown)?;
|
||||
|
||||
//log::trace!("Property.type: {:?}", self.atom_name(reply.type_));
|
||||
|
||||
// we found something
|
||||
if reply.type_ == target_format {
|
||||
Ok(ReadSelNotifyResult::GotData(reply.value))
|
||||
} else if reply.type_ == self.atoms.INCR {
|
||||
if reply.type_ == self.atoms.INCR {
|
||||
// Note that we call the get_property again because we are
|
||||
// indicating that we are ready to receive the data by deleting the
|
||||
// property, however deleting only works if the type matches the
|
||||
|
@ -545,8 +606,10 @@ impl Inner {
|
|||
}
|
||||
Ok(ReadSelNotifyResult::IncrStarted)
|
||||
} else {
|
||||
// this should never happen, we have sent a request only for supported types
|
||||
Err(Error::unknown("incorrect type received from clipboard"))
|
||||
Ok(ReadSelNotifyResult::GotData(ClipboardData {
|
||||
bytes: reply.value,
|
||||
format: reply.type_,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -574,7 +637,11 @@ impl Inner {
|
|||
true,
|
||||
event.window,
|
||||
event.atom,
|
||||
target_format,
|
||||
if target_format == self.atoms.TARGETS {
|
||||
self.atoms.ATOM
|
||||
} else {
|
||||
target_format
|
||||
},
|
||||
0,
|
||||
u32::MAX / 4,
|
||||
)
|
||||
|
@ -612,7 +679,7 @@ impl Inner {
|
|||
if event.target == self.atoms.TARGETS {
|
||||
log::trace!(
|
||||
"Handling TARGETS, dst property is {}",
|
||||
self.atom_name_dbg(event.property)
|
||||
self.atom_name(event.property)
|
||||
);
|
||||
let mut targets = Vec::with_capacity(10);
|
||||
targets.push(self.atoms.TARGETS);
|
||||
|
@ -812,8 +879,8 @@ fn serve_requests(context: Arc<Inner>) -> Result<(), Box<dyn std::error::Error>>
|
|||
Event::SelectionRequest(event) => {
|
||||
log::trace!(
|
||||
"SelectionRequest - selection is: {}, target is {}",
|
||||
context.atom_name_dbg(event.selection),
|
||||
context.atom_name_dbg(event.target),
|
||||
context.atom_name(event.selection),
|
||||
context.atom_name(event.target),
|
||||
);
|
||||
// Someone is requesting the clipboard content from us.
|
||||
context
|
||||
|
@ -987,6 +1054,11 @@ impl Clipboard {
|
|||
|
||||
let result = self.inner.read(&format_atoms, selection)?;
|
||||
|
||||
log::trace!(
|
||||
"read clipboard as format {:?}",
|
||||
self.inner.atom_name(result.format)
|
||||
);
|
||||
|
||||
for (format_atom, image_format) in image_format_atoms.into_iter().zip(image_formats) {
|
||||
if result.format == format_atom {
|
||||
let bytes = result.bytes;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue