diff --git a/crates/ui-egui/src/native_menu.rs b/crates/ui-egui/src/native_menu.rs index 09059c397..c8d046a68 100644 --- a/crates/ui-egui/src/native_menu.rs +++ b/crates/ui-egui/src/native_menu.rs @@ -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) -> Vec { 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(); + } _ => {} } } diff --git a/crates/ui-egui/src/tests/nativemenu.rs b/crates/ui-egui/src/tests/nativemenu.rs index 77a83291d..cc6e4b346 100644 --- a/crates/ui-egui/src/tests/nativemenu.rs +++ b/crates/ui-egui/src/tests/nativemenu.rs @@ -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"); + } +}