mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 10:40:45 +00:00
Fix #5159: ⌘V from the macOS menu bar pastes a picture another app copied
The menu bar takes ⌘V's key-down as Edit › Paste's key equivalent, and `native_menu::key_events` turns it back into egui input. With text on the clipboard that was a Paste event; with only a picture (a screenshot) it was nothing, leaving the paste to the window's own key-up. egui's window integration only releases a key whose press it saw, so that release never came and ⌘V pasted nothing, while Edit › Paste and ⌘V with the menu open worked. `key_events` now ends ⌘V with the key's release, as it already did for ⌘X and ⌘C: without a Paste event that release is the picture paste `PasteChord` looks for, and after one it ends that paste so the next picture paste still fires. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Claude Opus 5.5
parent
683abdb2d6
commit
b6f90d42fb
@@ -260,16 +260,20 @@ pub fn is_function_key(key: Key) -> bool {
|
||||
|
||||
/// The input egui's window integration would have made of a press of `sc` (key and modifiers
|
||||
/// held): the key press and its release, but for ⌘X, ⌘C and ⌘V the Cut, Copy and Paste events
|
||||
/// (Paste with the clipboard's text, from `paste_text`). A paste's release is left to the window's
|
||||
/// own key-up: one without a press or a Paste event is how a paste of a picture or a PDF is told
|
||||
/// apart ([`crate::shortcuts`]).
|
||||
/// (Paste with the clipboard's text, from `paste_text`), each followed by the key's release. The
|
||||
/// window never reports that release itself: AppKit gave the key-down to the menu, and egui's
|
||||
/// window integration only releases a key whose press it saw. A paste's release without a press or
|
||||
/// a Paste event is how a paste of a picture or a PDF is told apart ([`crate::shortcuts`], #5159).
|
||||
pub fn key_events(sc: KeyboardShortcut, paste_text: impl FnOnce() -> Option<String>) -> Vec<egui::Event> {
|
||||
let key = |pressed| egui::Event::Key { key: sc.logical_key, physical_key: None, pressed, repeat: false, modifiers: sc.modifiers };
|
||||
if sc.modifiers.command {
|
||||
match sc.logical_key {
|
||||
Key::X => return vec![egui::Event::Cut, key(false)],
|
||||
Key::C => return vec![egui::Event::Copy, key(false)],
|
||||
Key::V => return paste_text().map(|t| t.replace("\r\n", "\n")).filter(|t| !t.is_empty()).map(egui::Event::Paste).into_iter().collect(),
|
||||
Key::V => {
|
||||
let text = paste_text().map(|t| t.replace("\r\n", "\n")).filter(|t| !t.is_empty());
|
||||
return text.map(egui::Event::Paste).into_iter().chain([key(false)]).collect();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,7 @@ fn key_equivalents_are_command_and_function_keys_only() {
|
||||
}
|
||||
|
||||
/// What a key equivalent becomes: the press and release egui's window integration sends, or for
|
||||
/// ⌘X, ⌘C and ⌘V its clipboard events (a paste's release comes from the window).
|
||||
/// ⌘X, ⌘C and ⌘V its clipboard events and the key's release (the window never sends it, #5159).
|
||||
#[test]
|
||||
fn key_equivalents_become_the_input_their_keys_make() {
|
||||
let cmd = |key| KeyboardShortcut::new(Modifiers::COMMAND, key);
|
||||
@@ -252,8 +252,12 @@ fn key_equivalents_become_the_input_their_keys_make() {
|
||||
assert_eq!(native_menu::key_events(cmd(Key::A), || None), [key(Key::A, true), key(Key::A, false)]);
|
||||
assert_eq!(native_menu::key_events(cmd(Key::C), || None), [Event::Copy, key(Key::C, false)]);
|
||||
assert_eq!(native_menu::key_events(cmd(Key::X), || None), [Event::Cut, key(Key::X, false)]);
|
||||
assert_eq!(native_menu::key_events(cmd(Key::V), || Some("a\r\nb".into())), [Event::Paste("a\nb".into())]);
|
||||
assert_eq!(native_menu::key_events(cmd(Key::V), || None), [], "a picture: the release tells the paste");
|
||||
assert_eq!(native_menu::key_events(cmd(Key::V), || Some("a\r\nb".into())), [Event::Paste("a\nb".into()), key(Key::V, false)]);
|
||||
assert_eq!(native_menu::key_events(cmd(Key::V), || None), [key(Key::V, false)], "a picture: the release tells the paste");
|
||||
// A text paste's release is taken with its Paste event, so the next picture paste still fires.
|
||||
let mut chord = crate::shortcuts::PasteChord::default();
|
||||
assert_eq!(chord.textless_paste(&native_menu::key_events(cmd(Key::V), || Some("a".into()))), None);
|
||||
assert_eq!(chord.textless_paste(&native_menu::key_events(cmd(Key::V), || None)), Some(Modifiers::COMMAND));
|
||||
}
|
||||
|
||||
/// The fake backend: the menus the app synced, and events to report.
|
||||
@@ -682,3 +686,30 @@ fn the_menu_bar_is_inert_while_a_file_dialog_is_open() {
|
||||
m.key_equivalent("Cmd+A");
|
||||
assert_eq!(m.selected(), 2, "⌘A selected every object");
|
||||
}
|
||||
|
||||
/// #5159: ⌘V chosen by the menu bar's key equivalent pastes a picture another app copied (a
|
||||
/// screenshot): AppKit gives the key-down to the menu, so the window never reports the key, and
|
||||
/// the release the key equivalent makes is what tells the paste. Each press pastes once.
|
||||
#[test]
|
||||
fn a_key_equivalent_paste_pastes_a_picture_from_another_app() {
|
||||
use vectorcraft_doc::NodeKind;
|
||||
use vectorcraft_engine::cmd::clipboard::PNG;
|
||||
let (mut app, board) = super::sysclip::app();
|
||||
super::sysclip::copy_elsewhere(&board, vec![(PNG, super::sysclip::blue_png())]);
|
||||
let fake = Fake::default();
|
||||
app.services.native_menu = Some(NativeMenu::new(Box::new(fake.clone())));
|
||||
let mut m = Mac { app, fake, ctx: egui::Context::default(), time: 0.0 };
|
||||
for _ in 0..3 {
|
||||
m.frame(vec![]);
|
||||
}
|
||||
let images = |m: &Mac| {
|
||||
let d = &m.app.session.active().unwrap().doc;
|
||||
d.layers[0].children().unwrap().iter().filter(|n| matches!(n.kind, NodeKind::Image(_))).count()
|
||||
};
|
||||
for n in 1..=2 {
|
||||
m.key_equivalent("Cmd+V");
|
||||
m.frame(vec![]);
|
||||
m.frame(vec![]);
|
||||
assert_eq!(images(&m), n, "press {n} pastes one picture");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user