mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-06 02:22:00 +00:00
keymap: clipboard chords with more modifiers run their bindings with clipboard events off
This commit is contained in:
1 parent
5160611bad
commit
423524c1bc
3 files changed
+106
-44
No files matched your search
+13
-9
@@ -220,16 +220,20 @@ assert_eq!((report.unmatched[0].item.as_str(), report.unmatched[0].line), ("File
|
||||
when the press matched through a stand-in (`:` for `Shift+;`, `+` for `=`).
|
||||
`dispatch_filtered` takes a predicate that can decline a hit, for example a chord the native
|
||||
menu has already handled: the press and its text then stay in egui's events, in their place.
|
||||
While a text field has the keyboard, clipboard key presses and presses that typed text in the
|
||||
While a text field has the keyboard, plain clipboard key presses and presses that typed text in the
|
||||
same frame stay with it. That covers AltGr, which a browser on Windows reports as Ctrl+Alt
|
||||
followed by the text; a Ctrl+Alt press that types nothing is a command chord.
|
||||
`Dispatcher::new().with_repeat(true)` also runs commands on key repeats (held arrows).
|
||||
|
||||
egui's Copy, Cut and Paste events stay with the app by default. On the desktop, egui-winit
|
||||
sends them in place of the Cmd+C, Cmd+X and Cmd+V presses (Ctrl off macOS), so a binding on
|
||||
those chords runs only with `with_clipboard_events(true)`. Then the events count as their
|
||||
chords outside text fields, and a Paste event's text moves to `Hit::pasted`. The web build
|
||||
sends the key press and then the event; the press runs the command and takes the event, with
|
||||
its text, along. An app whose
|
||||
paste command asks egui for a paste gets a Paste event back, which runs that command again;
|
||||
such an app keeps the default.
|
||||
On the desktop, egui-winit sends egui's Copy, Cut and Paste events in place of the key press
|
||||
for any chord of Cmd with C, X or V (Ctrl off macOS); the web build sends the press and then
|
||||
the event. For the plain chords Cmd+C, Cmd+X and Cmd+V (and Cmd+Insert, Shift+Delete,
|
||||
Shift+Insert), the events stay with the app by default, so a binding on those chords runs only
|
||||
with `with_clipboard_events(true)`. Then the events count as their chords outside text fields,
|
||||
and a Paste event's text moves to `Hit::pasted`. On the web, the press runs the command and
|
||||
takes the event, with its text, along. An app whose paste command asks egui for a paste gets a
|
||||
Paste event back, which runs that command again; such an app keeps the default. A clipboard
|
||||
event with more modifiers held (Cmd+Shift+V, Cmd+Alt+C) always counts as its chord, also while
|
||||
typing, and stays when that chord has no binding. egui-winit sends no Paste event when the
|
||||
clipboard is empty or holds an image, so a binding on a Cmd+V chord cannot run then on the
|
||||
desktop.
|
||||
+58
-35
@@ -173,8 +173,8 @@ pub struct Hit {
|
||||
/// the shifted key for a Shift binding (`:` for `Shift+;`), Shift held to type a symbol, or
|
||||
/// `+` and `=` standing for each other.
|
||||
pub exact: bool,
|
||||
/// The text of the Paste event that ran the command, or on the web the Paste event that came
|
||||
/// with the press that ran it ([`Dispatcher::with_clipboard_events`]).
|
||||
/// The text of the Paste event that ran the command, or on the web of the Paste event that
|
||||
/// came with the press that ran it.
|
||||
pub pasted: Option<String>,
|
||||
}
|
||||
|
||||
@@ -204,16 +204,21 @@ struct LastPress {
|
||||
/// that runs a command is removed from the events, together with the text it typed when it has
|
||||
/// no command modifier, so no widget sees it too. Other events stay.
|
||||
///
|
||||
/// egui's Copy, Cut and Paste events stay with the app unless
|
||||
/// [`with_clipboard_events`](Self::with_clipboard_events) is on. On the desktop, egui-winit sends
|
||||
/// these events in place of the key presses for Cmd+C, Cmd+X and Cmd+V (Ctrl off macOS, plus
|
||||
/// Shift+Delete, Ctrl+Insert and Shift+Insert on Windows), so bindings on those chords run only
|
||||
/// with clipboard events on. The web build sends the key press and then the clipboard event; with
|
||||
/// clipboard events off, such a press stays too.
|
||||
/// On the desktop, egui-winit sends egui's Copy, Cut and Paste events in place of the key press
|
||||
/// for any chord of Cmd with C, X or V (Ctrl off macOS; on Windows also Shift+Delete, Ctrl+Insert
|
||||
/// and Shift+Insert). The web build sends the key press and then the event. For the plain
|
||||
/// clipboard chords (Cmd+C, Cmd+X, Cmd+V, Cmd+Insert, Shift+Delete, Shift+Insert), the events and
|
||||
/// their presses stay with the app unless [`with_clipboard_events`](Self::with_clipboard_events)
|
||||
/// is on, so bindings on those chords run only with it on. An event with more modifiers held
|
||||
/// counts as its chord, read from the frame's modifiers (Cmd+Shift+V), and runs that chord's
|
||||
/// binding; without one, it stays. On Windows, egui-winit also sends Cut for Ctrl+Shift+Delete
|
||||
/// and Copy for Ctrl+Shift+Insert, which read as Cmd+Shift+X and Cmd+Shift+C.
|
||||
///
|
||||
/// egui-winit sends no Paste event when the clipboard is empty or holds no text (an image), so a
|
||||
/// binding on Cmd+V, or on Cmd and V with more modifiers, cannot run then on the desktop.
|
||||
///
|
||||
/// While [`Active::typing`], the text field keeps, without lookup:
|
||||
/// - clipboard events, and the clipboard key presses the web build sends with them (Cmd+C, Cmd+X,
|
||||
/// Cmd+V, Shift+Delete, Shift+Insert, Ctrl+Insert);
|
||||
/// - the plain clipboard chords' events and presses (other clipboard chords are looked up);
|
||||
/// - a press with a Text event of its own in the frame, since it typed that text. This covers
|
||||
/// AltGr: the browser on Windows reports AltGr+Q (`@` on a German layout) as Ctrl+Alt+Q, and
|
||||
/// the web build's text agent sends the `@` after it. egui-winit reports AltGr with neither Ctrl
|
||||
@@ -246,13 +251,14 @@ impl Dispatcher {
|
||||
self
|
||||
}
|
||||
|
||||
/// Whether egui's Copy, Cut and Paste events count as the chords that cause them (Cmd+C,
|
||||
/// Cmd+X, Cmd+V; Shift+Delete and Shift+Insert for Cut and Paste with Shift alone) outside text
|
||||
/// fields. Off by default: the events stay in egui's input for the app's own handlers. With it
|
||||
/// on, an event that runs a command is removed, and a Paste event's text moves to
|
||||
/// [`Hit::pasted`]. On the web, the key press runs the command and its clipboard event goes
|
||||
/// with it the same way; when the press runs nothing, or the hit is declined, both stay. An app that asks egui for a paste (`ViewportCommand::RequestPaste`) to run
|
||||
/// its paste command gets a Paste event back, which with this on runs that command again.
|
||||
/// Whether egui's Copy, Cut and Paste events for the plain clipboard chords count as those
|
||||
/// chords (Cmd+C, Cmd+X, Cmd+V; Shift+Delete and Shift+Insert for Cut and Paste with Shift
|
||||
/// alone) outside text fields. Off by default: the events stay in egui's input for the app's
|
||||
/// own handlers. With it on, an event that runs a command is removed, and a Paste event's text
|
||||
/// moves to [`Hit::pasted`]. On the web, the key press runs the command and its clipboard
|
||||
/// event goes with it the same way; when the press runs nothing, or the hit is declined, both
|
||||
/// stay. An app that asks egui for a paste (`ViewportCommand::RequestPaste`) to run its paste
|
||||
/// command gets a Paste event back, which with this on runs that command again.
|
||||
pub fn with_clipboard_events(mut self, on: bool) -> Dispatcher {
|
||||
self.clipboard = on;
|
||||
self
|
||||
@@ -288,13 +294,16 @@ impl Dispatcher {
|
||||
Event::Key { key, pressed: true, repeat, modifiers, .. } if !*repeat || self.repeat => {
|
||||
let held = Mods::from_egui(modifiers, platform);
|
||||
if let Some(key) = Key::from_egui(*key) {
|
||||
let clip = clipboard_kind(key, held);
|
||||
let plain_clip = is_plain_clipboard(key, held);
|
||||
let keep = if active.typing {
|
||||
text_of[i].is_some() || clip.is_some()
|
||||
text_of[i].is_some() || plain_clip
|
||||
} else {
|
||||
!self.clipboard && causes[i].is_some()
|
||||
!self.clipboard && plain_clip && causes[i].is_some()
|
||||
};
|
||||
if keep {
|
||||
if let Some((j, _)) = causes[i] {
|
||||
caused_taken[j] = Some(false);
|
||||
}
|
||||
kept.push((i, event));
|
||||
continue;
|
||||
}
|
||||
@@ -322,23 +331,26 @@ impl Dispatcher {
|
||||
caused_taken[j] = Some(false);
|
||||
}
|
||||
}
|
||||
// A focused text field copies and pastes on these events itself.
|
||||
Event::Copy | Event::Cut | Event::Paste(_) if self.clipboard && !active.typing => {
|
||||
let (key, held) = clipboard_chord(&event, frame_mods);
|
||||
if let Some(taken) = caused_taken[i] {
|
||||
if taken {
|
||||
continue;
|
||||
}
|
||||
} else if let Some(mut hit) = self.press(key, held, false, now, keymap, active) {
|
||||
if let Event::Paste(text) = &event {
|
||||
hit.pasted = Some(text.clone());
|
||||
}
|
||||
if accept(&hit) {
|
||||
hits.push(hit);
|
||||
continue;
|
||||
// Desktop clipboard events, and web ones whose press stayed or ran nothing.
|
||||
Event::Copy | Event::Cut | Event::Paste(_) => match caused_taken[i] {
|
||||
Some(true) => continue,
|
||||
Some(false) => {}
|
||||
None => {
|
||||
let (key, held) = clipboard_chord(&event, frame_mods);
|
||||
// A focused text field copies and pastes on the plain chords itself.
|
||||
let look = !is_plain_clipboard(key, held) || (self.clipboard && !active.typing);
|
||||
let hit = if look { self.press(key, held, false, now, keymap, active) } else { None };
|
||||
if let Some(mut hit) = hit {
|
||||
if let Event::Paste(text) = &event {
|
||||
hit.pasted = Some(text.clone());
|
||||
}
|
||||
if accept(&hit) {
|
||||
hits.push(hit);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
kept.push((i, event));
|
||||
@@ -444,6 +456,17 @@ fn clipboard_kind(key: Key, held: Mods) -> Option<Clip> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a chord is one of the plain clipboard chords, which egui's text fields act on: Cmd+C,
|
||||
/// Cmd+X, Cmd+V, Cmd+Insert, Shift+Delete and Shift+Insert.
|
||||
fn is_plain_clipboard(key: Key, held: Mods) -> bool {
|
||||
match key {
|
||||
Key::C | Key::X | Key::V => held == Mods::CMD,
|
||||
Key::Insert => held == Mods::CMD || held == Mods::SHIFT,
|
||||
Key::Delete => held == Mods::SHIFT,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The press a clipboard event stands for, with the modifiers held. Off macOS egui-winit also
|
||||
/// sends Cut for Shift+Delete and Paste for Shift+Insert.
|
||||
fn clipboard_chord(event: &Event, held: Mods) -> (Key, Mods) {
|
||||
|
||||
@@ -81,6 +81,8 @@ fn dispatch_keymap() -> Keymap {
|
||||
let catalog = vec![
|
||||
app("edit.copy", &["Cmd+C", "Cmd+Insert"]),
|
||||
app("edit.paste", &["Cmd+V", "Shift+Insert"]),
|
||||
app("edit.pasteInPlace", &["Cmd+Shift+V"]),
|
||||
app("edit.copyMerged", &["Cmd+Shift+C"]),
|
||||
app("app.quit", &["Cmd+Alt+Q"]),
|
||||
app("type.pageNumber", &["Cmd+Alt+Shift+N"]),
|
||||
app("view.refresh", &["Cmd+Alt+F5"]),
|
||||
@@ -168,6 +170,39 @@ fn clipboard_events_stay_with_the_app_by_default() {
|
||||
assert_eq!(ran, ["edit.copy"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_chords_with_more_modifiers_run_their_bindings() {
|
||||
let km = dispatch_keymap();
|
||||
let mut d = Dispatcher::new();
|
||||
let typing = Active { typing: true, ..Active::NONE };
|
||||
let ctrl_shift = Modifiers { shift: true, ..CTRL };
|
||||
// The desktop: egui-winit sends the Paste event in place of Ctrl+Shift+V.
|
||||
let mut input = frame(1.0, ctrl_shift, vec![Event::Paste("text".into())]);
|
||||
let hits = d.dispatch(&mut input, &km, &Active::NONE);
|
||||
let ran: Vec<_> = hits.iter().map(|h| (h.command.id.as_str(), h.pasted.as_deref())).collect();
|
||||
assert_eq!(ran, [("edit.pasteInPlace", Some("text"))]);
|
||||
assert!(input.events.is_empty());
|
||||
let (ran, left) = run(&mut d, &km, &typing, frame(2.0, ctrl_shift, vec![Event::Copy]));
|
||||
assert_eq!(ran, ["edit.copyMerged"]);
|
||||
assert!(left.is_empty());
|
||||
// Ctrl+Shift+X has no binding: the event stays.
|
||||
let (ran, left) = run(&mut d, &km, &Active::NONE, frame(3.0, ctrl_shift, vec![Event::Cut]));
|
||||
assert!(ran.is_empty());
|
||||
assert_eq!(left, [Event::Cut]);
|
||||
|
||||
// The web: the press decides for itself and its event, also while typing.
|
||||
for (time, active) in [(4.0, &Active::NONE), (5.0, &typing)] {
|
||||
let events = vec![press(EguiKey::C, ctrl_shift), Event::Copy];
|
||||
let (ran, left) = run(&mut d, &km, active, frame(time, ctrl_shift, events));
|
||||
assert_eq!(ran, ["edit.copyMerged"]);
|
||||
assert!(left.is_empty(), "{left:?}");
|
||||
}
|
||||
let events = vec![press(EguiKey::X, ctrl_shift), Event::Cut];
|
||||
let (ran, left) = run(&mut d, &km, &Active::NONE, frame(6.0, ctrl_shift, events.clone()));
|
||||
assert!(ran.is_empty());
|
||||
assert_eq!(left, events);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_events_run_their_chords_outside_text_fields_when_turned_on() {
|
||||
let km = dispatch_keymap();
|
||||
|
||||
Reference in new issue
Block a user