mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 00:04:44 +08:00
Keep SwiftUI's named keys out of the ASCII recovery (#1106)
* Keep SwiftUI's named keys out of the ASCII recovery * Cover the shortcut caller path, not just the recovery * Skip the caller-path cases when the layout has no trap to spring * Keep the recovery's contract in Platform, without the test seam
This commit is contained in:
@@ -251,6 +251,7 @@ run palette-filter-test Tinycast/Palette/PaletteMode.swift \
|
||||
run action-menu-search-test Tinycast/Palette/ActionMenuSearchQuery.swift \
|
||||
Tinycast/Features/Launcher/Model/SearchRelevance.swift
|
||||
run palette-shortcut-test Tinycast/Palette/PaletteShortcut.swift
|
||||
run ascii-layout-test Tinycast/Platform/ASCIIKeyboardLayout.swift
|
||||
run palette-tab-test Tinycast/Palette/PaletteMode.swift \
|
||||
Tinycast/Palette/PaletteTabAction.swift \
|
||||
Tinycast/Features/Quicklinks/Model/Quicklink.swift \
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
/// `UCKeyTranslate` answers a named key's keycode with a control character the ASCII test admits.
|
||||
@main
|
||||
@MainActor
|
||||
struct ASCIILayoutTests {
|
||||
static var failures = 0
|
||||
static var passes = 0
|
||||
|
||||
static func check(_ label: String, _ ok: Bool) {
|
||||
if ok {
|
||||
passes += 1
|
||||
} else {
|
||||
failures += 1
|
||||
print("FAIL \(label)")
|
||||
}
|
||||
}
|
||||
|
||||
/// What `UCKeyTranslate` hands back for each keycode under ⌘, against SwiftUI's own spelling.
|
||||
static let namedKeys: [(name: String, key: KeyEquivalent, translated: Character)] = [
|
||||
("↑", .upArrow, "\u{1e}"),
|
||||
("↓", .downArrow, "\u{1f}"),
|
||||
("←", .leftArrow, "\u{1c}"),
|
||||
("→", .rightArrow, "\u{1d}"),
|
||||
("⌦", .deleteForward, "\u{7f}"),
|
||||
("⇞", .pageUp, "\u{b}"),
|
||||
("⇟", .pageDown, "\u{c}"),
|
||||
("↖", .home, "\u{1}"),
|
||||
("↘", .end, "\u{4}")
|
||||
]
|
||||
|
||||
/// These four agree with SwiftUI already, and must keep resolving once the arrows are excluded.
|
||||
static let controlKeys: [(name: String, key: KeyEquivalent, translated: Character)] = [
|
||||
("↩", .return, "\u{d}"),
|
||||
("⌫", .delete, "\u{8}"),
|
||||
("⇥", .tab, "\u{9}"),
|
||||
("⎋", .escape, "\u{1b}"),
|
||||
("space", .space, " ")
|
||||
]
|
||||
|
||||
static func main() {
|
||||
print("# a named key is never recovered from the layout")
|
||||
for entry in namedKeys {
|
||||
check(
|
||||
"\(entry.name) keeps SwiftUI's key",
|
||||
ASCIIKeyboardLayout.recovered(entry.key, layoutCharacter: entry.translated)
|
||||
== entry.key)
|
||||
}
|
||||
|
||||
print("\n# a key whose control character is already SwiftUI's still resolves")
|
||||
for entry in controlKeys {
|
||||
check(
|
||||
"\(entry.name) resolves to itself",
|
||||
ASCIIKeyboardLayout.recovered(entry.key, layoutCharacter: entry.translated)
|
||||
== entry.key)
|
||||
}
|
||||
|
||||
print("\n# the recovery still does its job")
|
||||
check(
|
||||
"a non-QWERTY layout recovers the logical key",
|
||||
ASCIIKeyboardLayout.recovered(KeyEquivalent("t"), layoutCharacter: "k")
|
||||
== KeyEquivalent("k"))
|
||||
check(
|
||||
"a non-ASCII layout character falls back to SwiftUI's key",
|
||||
ASCIIKeyboardLayout.recovered(KeyEquivalent("k"), layoutCharacter: "ц")
|
||||
== KeyEquivalent("k"))
|
||||
check(
|
||||
"no layout character falls back to SwiftUI's key",
|
||||
ASCIIKeyboardLayout.recovered(KeyEquivalent("k"), layoutCharacter: nil)
|
||||
== KeyEquivalent("k"))
|
||||
check(
|
||||
"a shifted letter is still spelled lower case",
|
||||
ASCIIKeyboardLayout.recovered(KeyEquivalent("C"), layoutCharacter: nil)
|
||||
== KeyEquivalent("c"))
|
||||
|
||||
print("\n\(passes) passed, \(failures) failed")
|
||||
exit(failures == 0 ? 0 : 1)
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,22 @@ enum ASCIIKeyboardLayout {
|
||||
/// SwiftUI exposes the input-source character; recover the logical ASCII key from AppKit.
|
||||
@MainActor static func keyEquivalent(fallingBackTo key: KeyEquivalent) -> KeyEquivalent {
|
||||
guard let event = NSApp.currentEvent,
|
||||
!event.modifierFlags.isDisjoint(with: [.command, .control]),
|
||||
let character = character(for: event)?.lowercased().first,
|
||||
character.unicodeScalars.allSatisfy(\.isASCII)
|
||||
!event.modifierFlags.isDisjoint(with: [.command, .control])
|
||||
else { return lowercased(key) }
|
||||
return KeyEquivalent(character)
|
||||
return recovered(key, layoutCharacter: character(for: event)?.lowercased().first)
|
||||
}
|
||||
|
||||
/// The recovery itself, over a layout character the caller already translated.
|
||||
static func recovered(_ key: KeyEquivalent, layoutCharacter: Character?) -> KeyEquivalent {
|
||||
guard !isNamedKey(key), let layoutCharacter,
|
||||
layoutCharacter.unicodeScalars.allSatisfy(\.isASCII)
|
||||
else { return lowercased(key) }
|
||||
return KeyEquivalent(layoutCharacter)
|
||||
}
|
||||
|
||||
/// Arrows and page keys: `UCKeyTranslate` answers these with ASCII controls the test admits.
|
||||
private static func isNamedKey(_ key: KeyEquivalent) -> Bool {
|
||||
key.character.unicodeScalars.contains { (0xF700...0xF8FF).contains($0.value) }
|
||||
}
|
||||
|
||||
/// Shift uppercases SwiftUI's key, but every chord spells its letter in lower case.
|
||||
|
||||
@@ -520,8 +520,11 @@ Panel-owned chords use the same translation directly. A ⌘ chord translates thr
|
||||
Command table, so "Dvorak – QWERTY ⌘" keeps giving QWERTY positions while Command is held; a ⌃ chord
|
||||
translates without it, since only Command is remapped. A non-ASCII input source or IME therefore
|
||||
cannot turn ⌘K into a different logical key, while Dvorak and other ASCII layouts keep their own
|
||||
letter positions. No replacement event is synthesized, and unmodified typing stays on the active
|
||||
input source and follows the normal composition path.
|
||||
letter positions. A key SwiftUI spells in the private-use area — the arrows, and the page, home and
|
||||
forward-delete keys — skips the recovery outright: `UCKeyTranslate` answers those keycodes with ASCII
|
||||
control characters, which the ASCII test would otherwise accept in place of the key itself, and a
|
||||
layout has no letter position to recover for them anyway. No replacement event is synthesized, and
|
||||
unmodified typing stays on the active input source and follows the normal composition path.
|
||||
|
||||
## The keyboard belongs to the search field
|
||||
|
||||
|
||||
Reference in New Issue
Block a user