mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 01:20:59 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user