Merge pull request #5719 from dotCooCoo/fix/type-paste-copy-during-refused-write

Type: a copy elsewhere during a refused write wins the paste
This commit is contained in:
BFlat
2026-11-05 08:17:37 +07:00
committed by GitHub
3 changed files with 89 additions and 8 deletions
+12 -2
View File
@@ -208,15 +208,25 @@ impl VectorcraftApp {
let formats = r.and_then(|_| made.try_recv().map_err(|e| e.to_string()));
let written = formats.and_then(|(f, warnings)| {
let Some(cb) = app.services.system_clipboard.as_mut() else { return Ok(warnings) };
let before_write = Held::now(cb.as_mut());
// Nothing copied elsewhere since the Copy or Cut.
let current = before.as_ref().is_some_and(|b| *b == Held::now(cb.as_mut()));
let current = before.as_ref() == Some(&before_write);
if let Err(e) = cb.write(&f) {
// Another app may hold the clipboard open. It keeps what it held (or what the
// failed write left), and a paste into text takes this copy's text until
// something else is copied.
let held = Held::now(cb.as_mut());
let text = f.iter().find(|t| t.mime == TEXT).map(|t| String::from_utf8_lossy(&t.data).into_owned());
app.clipboard_publishing.unwritten = text.filter(|_| current).map(|text| (held, text));
// A change during the write is the failed write emptying the clipboard and
// writing some of the formats, or another app's copy: the clipboard then holds
// nothing to paste, this copy's text, or no text and one of this copy's
// formats only after the former.
let left_by_write = held == before_write
|| match cb.read(&[TEXT]) {
Some(now) => text.as_deref().is_some_and(|ours| now.data == ours.as_bytes()),
None => !cb.has(&PASTE_ORDER) || f.iter().any(|ours| cb.read(&[ours.mime]).is_some_and(|now| now.data == ours.data)),
};
app.clipboard_publishing.unwritten = text.filter(|_| current && left_by_write).map(|text| (held, text));
return Err(e);
}
Ok(warnings)
+45 -2
View File
@@ -411,9 +411,17 @@ fn a_copy_elsewhere_before_a_refused_write_wins_the_type_paste() {
#[test]
fn type_paste_keeps_a_copy_whose_write_failed_after_emptying_the_clipboard() {
let _guard = TEST_CLIP_LOCK.lock().unwrap();
assert_eq!(paste_after_a_write_that_failed_after_emptying(false), "Own: Hello");
// The formats other than text were written before the failure.
assert_eq!(paste_after_a_write_that_failed_after_emptying(true), "Own: Hello");
}
/// Copy "Hello" in the Type tool with a write that empties the clipboard and fails (`keep_others`:
/// after writing the formats other than text), then paste into "Own: ".
fn paste_after_a_write_that_failed_after_emptying(keep_others: bool) -> String {
let (mut app, board) = super::sysclip::app();
super::sysclip::copy_elsewhere(&board, vec![(vectorcraft_engine::cmd::clipboard::TEXT, b"Old".to_vec())]);
super::sysclip::fail_writes_after_emptying(&board);
super::sysclip::fail_writes_after_emptying(&board, keep_others);
let source = run(&mut app, "text.create", json!({"x": 20, "y": 40, "text": "Hello", "size": 36, "color": "#ff0000"}))["id"].as_u64().unwrap();
let destination =
run(&mut app, "text.create", json!({"x": 20, "y": 60, "text": "Own: ", "size": 12, "color": "#000000"}))["id"].as_u64().unwrap();
@@ -429,7 +437,42 @@ fn type_paste_keeps_a_copy_whose_write_failed_after_emptying_the_clipboard() {
crate::panels::character::intercept_text_command(&mut app, "edit.paste");
let node = app.session.doc().unwrap().doc.node(NodeId(destination)).unwrap();
let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { panic!() };
assert_eq!(t.plain_text(), "Own: Hello");
t.plain_text()
}
#[test]
fn a_copy_elsewhere_during_a_refused_write_wins_the_type_paste() {
let _guard = TEST_CLIP_LOCK.lock().unwrap();
use vectorcraft_engine::cmd::clipboard::{PNG, TEXT};
assert_eq!(paste_after_a_copy_during_a_refused_write(TEXT, b"Race".to_vec(), false), "Own: Race");
// Its other formats may match this copy's; its text still differs.
assert_eq!(paste_after_a_copy_during_a_refused_write(TEXT, b"Race".to_vec(), true), "Own: Race");
// A picture has no text to paste into type.
assert_eq!(paste_after_a_copy_during_a_refused_write(PNG, super::sysclip::blue_png(), false), "Own: ");
}
/// Copy in the Type tool while another app copies `data` of type `mime` (with `our_formats`, also
/// the copy's formats other than text) and the write fails, then paste into "Own: ".
fn paste_after_a_copy_during_a_refused_write(mime: &'static str, data: Vec<u8>, our_formats: bool) -> String {
let (mut app, board) = super::sysclip::app();
super::sysclip::copy_elsewhere(&board, vec![(vectorcraft_engine::cmd::clipboard::TEXT, b"Old".to_vec())]);
super::sysclip::copy_during_next_write(&board, mime, data, our_formats);
let source = run(&mut app, "text.create", json!({"x": 20, "y": 40, "text": "Hello", "size": 36, "color": "#ff0000"}))["id"].as_u64().unwrap();
let destination =
run(&mut app, "text.create", json!({"x": 20, "y": 60, "text": "Own: ", "size": 12, "color": "#000000"}))["id"].as_u64().unwrap();
let v = app.view_info();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": source, "start": 0, "end": 5}));
crate::panels::character::intercept_text_command(&mut app, "edit.copy");
crate::background::wait_all(&mut app);
app.session.select_tool("selection", v).unwrap();
app.session.select_tool("type", v).unwrap();
app.session.set_tool_option("textSelection", &json!({"id": destination, "start": 5, "end": 5}));
crate::panels::character::intercept_text_command(&mut app, "edit.paste");
let node = app.session.doc().unwrap().doc.node(NodeId(destination)).unwrap();
let vectorcraft_doc::NodeKind::Text(t) = &node.kind else { panic!() };
t.plain_text()
}
/// https://helpx.adobe.com/illustrator/using/format-text.html
+32 -4
View File
@@ -31,6 +31,13 @@ pub(super) struct Board {
uncounted: bool,
/// The app's writes empty the clipboard and then fail.
partial: bool,
/// With `partial`, the formats other than text are written before the failure.
partial_keeps_others: bool,
/// Another app copies this while the app's write tries to open the clipboard, and the write
/// fails.
race: Option<Flavour>,
/// With `race`, the other app's copy also carries the app's formats other than text.
race_with_our_formats: bool,
}
struct Fake(Rc<RefCell<Board>>);
@@ -52,8 +59,17 @@ impl SystemClipboard for Fake {
if b.locked {
return Err("another app has the clipboard open".into());
}
if let Some(copied) = b.race.take() {
b.flavours = vec![copied];
if b.race_with_our_formats {
b.flavours.extend(flavours.iter().filter(|f| f.mime != TEXT).cloned());
}
b.ours = false;
b.changes += 1;
return Err("another app has the clipboard open".into());
}
if b.partial {
b.flavours.clear();
b.flavours = if b.partial_keeps_others { flavours.iter().filter(|f| f.mime != TEXT).cloned().collect() } else { vec![] };
b.changes += 1;
return Err("a format could not be written".into());
}
@@ -103,9 +119,21 @@ pub(super) fn without_change_count(board: &Rc<RefCell<Board>>) {
board.borrow_mut().uncounted = true;
}
/// The app's writes empty the clipboard and then fail, as when one format can't be written.
pub(super) fn fail_writes_after_emptying(board: &Rc<RefCell<Board>>) {
board.borrow_mut().partial = true;
/// The app's writes empty the clipboard and then fail, as when one format can't be written; with
/// `keep_others`, the formats other than text are written.
pub(super) fn fail_writes_after_emptying(board: &Rc<RefCell<Board>>, keep_others: bool) {
let mut b = board.borrow_mut();
b.partial = true;
b.partial_keeps_others = keep_others;
}
/// Another app copies `data` of type `mime` while the app's next write tries to open the
/// clipboard, and that write fails. With `our_formats`, its copy also carries the app's formats
/// other than text.
pub(super) fn copy_during_next_write(board: &Rc<RefCell<Board>>, mime: &'static str, data: Vec<u8>, our_formats: bool) {
let mut b = board.borrow_mut();
b.race = Some(Flavour { mime, data });
b.race_with_our_formats = our_formats;
}
pub(super) fn run(app: &mut VectorcraftApp, id: &str, p: Value) -> Value {