The gesture book is generated from GESTURE tags, so it could not describe a gesture nobody tagged, but nothing made anyone tag one. The arrow keys, Enter, P, X, U, Delete, F1 and F2 all worked in the grid with no line in the help sheet, and a tag could name a key whose handler had gone. Key handlers now compare one canonical string, Keys.chord(event) == "Ctrl+Z", instead of reading event.text and the modifiers themselves. keys.slint folds the key and its modifiers into that spelling, so the literal in the handler is the whole binding and the checker reads exactly what the handler dispatches on. Each handler carries a KEYMAP comment naming the gesture-book section its keys belong to, and a tag's keys field names its keys between backticks. gestures-check now fails when a handler binds a key no tag in that section names, when a tag names a key no handler there binds, when any .slint file other than keys.slint reads event.text, when a compared literal is not canonical, and when keys.slint's named keys drift from the Rust list. Spellings are normalised in one place, chord.rs: Ctrl+z, Control+Z and LeftArrow all mean what the handler's "Ctrl+Z" and "Left" mean. Shift and Alt count only for letters and named keys, because on the French layout every digit needs shift and a 6 has to be a 6 however it was typed. A Rust keymap that both dispatched and was read by the generator was the alternative. It would have moved the handlers' decisions away from the Slint state they depend on, and a window that forgot to install it would have had no working keys at all. The keys that were already bound and undocumented are now tagged.
93 lines
4.3 KiB
Plaintext
93 lines
4.3 KiB
Plaintext
// TRACES: FR-UI-5 | FR-DEV-16
|
|
//
|
|
// One name for every key press, and the only code that reads the raw key.
|
|
//
|
|
// A handler compares `Keys.chord(event)` against a canonical spelling —
|
|
// "Ctrl+Shift+Z", "Shift+Left", "6", "Escape" — rather than reading
|
|
// `event.text` and `event.modifiers` itself. Two reasons, one for each reader:
|
|
//
|
|
// **For the handler**, the literal is the whole binding. Undo used to be
|
|
// `control && (text == "z" || text == "Z")` with a nested `if` on shift to
|
|
// split it from redo; now it is "Ctrl+Z", and redo is "Ctrl+Shift+Z", and
|
|
// neither can accidentally answer the other.
|
|
//
|
|
// **For the checker**, the literal is the whole binding too. `traces
|
|
// gestures-check` reads every `Keys.chord(event) == "…"` in a key handler and
|
|
// fails CI if the key is not named by a GESTURE tag for that place, or if a
|
|
// tag names a key no handler binds (tools/traceability/src/keymap.rs). It can
|
|
// only do that because nothing else reads `event.text` — which it also checks.
|
|
//
|
|
// The canonical form is defined in tools/traceability/src/chord.rs, and this
|
|
// file must agree with it; the checker compares the names `named` can return
|
|
// against the list there. The rules, briefly:
|
|
//
|
|
// - Modifiers in the order Ctrl, Alt, Shift, Meta, each only when held.
|
|
// - A letter is upper case whatever its case arrived in — caps lock must not
|
|
// stop `P` picking — and shift is read from the modifier.
|
|
// - **Shift and Alt count only for letters and named keys.** For any other
|
|
// character the layout has already spent them: on the French layout every
|
|
// digit needs shift, and a 6 is a 6 however it was made.
|
|
// - `+` is "Plus", because it is the separator.
|
|
|
|
export global Keys {
|
|
// The canonical name of a key press. What every key handler compares.
|
|
public pure function chord(event: KeyEvent) -> string {
|
|
return self.name(event.text, event.modifiers.control, event.modifiers.alt,
|
|
event.modifiers.shift, event.modifiers.meta);
|
|
}
|
|
|
|
// The same, from its parts.
|
|
public pure function name(text: string, control: bool, alt: bool, shift: bool,
|
|
meta: bool) -> string {
|
|
return (control ? "Ctrl+" : "")
|
|
+ (alt && self.modifiable(text) ? "Alt+" : "")
|
|
+ (shift && self.modifiable(text) ? "Shift+" : "")
|
|
+ (meta ? "Meta+" : "")
|
|
+ self.base(text);
|
|
}
|
|
|
|
// Whether shift and alt are part of the chord rather than of the character.
|
|
pure function modifiable(text: string) -> bool {
|
|
return self.named(text) != "" || text.to-uppercase() != text.to-lowercase();
|
|
}
|
|
|
|
pure function base(text: string) -> string {
|
|
if (self.named(text) != "") { return self.named(text); }
|
|
if (text == "+") { return "Plus"; }
|
|
return text.to-uppercase();
|
|
}
|
|
|
|
// Keys with a name rather than a character. Mirrors `NAMED` in chord.rs.
|
|
pure function named(text: string) -> string {
|
|
if (text == Key.LeftArrow) { return "Left"; }
|
|
if (text == Key.RightArrow) { return "Right"; }
|
|
if (text == Key.UpArrow) { return "Up"; }
|
|
if (text == Key.DownArrow) { return "Down"; }
|
|
if (text == Key.Home) { return "Home"; }
|
|
if (text == Key.End) { return "End"; }
|
|
if (text == Key.PageUp) { return "PageUp"; }
|
|
if (text == Key.PageDown) { return "PageDown"; }
|
|
if (text == Key.Return) { return "Enter"; }
|
|
if (text == Key.Escape) { return "Escape"; }
|
|
if (text == Key.Back) { return "Back"; }
|
|
if (text == Key.Tab || text == Key.Backtab) { return "Tab"; }
|
|
if (text == Key.Backspace) { return "Backspace"; }
|
|
if (text == Key.Delete) { return "Delete"; }
|
|
if (text == Key.Insert) { return "Insert"; }
|
|
if (text == Key.Space) { return "Space"; }
|
|
if (text == Key.F1) { return "F1"; }
|
|
if (text == Key.F2) { return "F2"; }
|
|
if (text == Key.F3) { return "F3"; }
|
|
if (text == Key.F4) { return "F4"; }
|
|
if (text == Key.F5) { return "F5"; }
|
|
if (text == Key.F6) { return "F6"; }
|
|
if (text == Key.F7) { return "F7"; }
|
|
if (text == Key.F8) { return "F8"; }
|
|
if (text == Key.F9) { return "F9"; }
|
|
if (text == Key.F10) { return "F10"; }
|
|
if (text == Key.F11) { return "F11"; }
|
|
if (text == Key.F12) { return "F12"; }
|
|
return "";
|
|
}
|
|
}
|