Fix Cmd+plus zoom handling on non-US layouts (#680)
This commit is contained in:
parent
f73887154d
commit
c3b55e2a9f
3 changed files with 124 additions and 14 deletions
|
|
@ -727,32 +727,55 @@ struct CommandPaletteDebugSnapshot {
|
|||
func browserZoomShortcutAction(
|
||||
flags: NSEvent.ModifierFlags,
|
||||
chars: String,
|
||||
keyCode: UInt16
|
||||
keyCode: UInt16,
|
||||
literalChars: String? = nil
|
||||
) -> BrowserZoomShortcutAction? {
|
||||
let normalizedFlags = flags
|
||||
.intersection(.deviceIndependentFlagsMask)
|
||||
.subtracting([.numericPad, .function])
|
||||
let key = chars.lowercased()
|
||||
let hasCommand = normalizedFlags.contains(.command)
|
||||
let hasOnlyCommandAndOptionalShift = hasCommand && normalizedFlags.isDisjoint(with: [.control, .option])
|
||||
|
||||
guard hasOnlyCommandAndOptionalShift else { return nil }
|
||||
let keys = browserZoomShortcutKeyCandidates(
|
||||
chars: chars,
|
||||
literalChars: literalChars,
|
||||
keyCode: keyCode
|
||||
)
|
||||
|
||||
if key == "=" || key == "+" || keyCode == 24 || keyCode == 69 { // kVK_ANSI_Equal / kVK_ANSI_KeypadPlus
|
||||
if keys.contains("=") || keys.contains("+") || keyCode == 24 || keyCode == 69 { // kVK_ANSI_Equal / kVK_ANSI_KeypadPlus
|
||||
return .zoomIn
|
||||
}
|
||||
|
||||
if key == "-" || key == "_" || keyCode == 27 || keyCode == 78 { // kVK_ANSI_Minus / kVK_ANSI_KeypadMinus
|
||||
if keys.contains("-") || keys.contains("_") || keyCode == 27 || keyCode == 78 { // kVK_ANSI_Minus / kVK_ANSI_KeypadMinus
|
||||
return .zoomOut
|
||||
}
|
||||
|
||||
if key == "0" || keyCode == 29 || keyCode == 82 { // kVK_ANSI_0 / kVK_ANSI_Keypad0
|
||||
if keys.contains("0") || keyCode == 29 || keyCode == 82 { // kVK_ANSI_0 / kVK_ANSI_Keypad0
|
||||
return .reset
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func browserZoomShortcutKeyCandidates(
|
||||
chars: String,
|
||||
literalChars: String?,
|
||||
keyCode: UInt16
|
||||
) -> Set<String> {
|
||||
var keys: Set<String> = [chars.lowercased()]
|
||||
|
||||
if let literalChars, !literalChars.isEmpty {
|
||||
keys.insert(literalChars.lowercased())
|
||||
}
|
||||
|
||||
if let layoutChar = KeyboardLayout.character(forKeyCode: keyCode), !layoutChar.isEmpty {
|
||||
keys.insert(layoutChar)
|
||||
}
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
func shouldSuppressSplitShortcutForTransientTerminalFocusInputs(
|
||||
firstResponderIsWindow: Bool,
|
||||
hostedSize: CGSize,
|
||||
|
|
@ -768,10 +791,16 @@ func shouldRouteTerminalFontZoomShortcutToGhostty(
|
|||
firstResponderIsGhostty: Bool,
|
||||
flags: NSEvent.ModifierFlags,
|
||||
chars: String,
|
||||
keyCode: UInt16
|
||||
keyCode: UInt16,
|
||||
literalChars: String? = nil
|
||||
) -> Bool {
|
||||
guard firstResponderIsGhostty else { return false }
|
||||
return browserZoomShortcutAction(flags: flags, chars: chars, keyCode: keyCode) != nil
|
||||
return browserZoomShortcutAction(
|
||||
flags: flags,
|
||||
chars: chars,
|
||||
keyCode: keyCode,
|
||||
literalChars: literalChars
|
||||
) != nil
|
||||
}
|
||||
|
||||
func cmuxOwningGhosttyView(for responder: NSResponder?) -> GhosttyNSView? {
|
||||
|
|
@ -826,15 +855,20 @@ private func cmuxOwningGhosttyView(for view: NSView) -> GhosttyNSView? {
|
|||
func browserZoomShortcutTraceCandidate(
|
||||
flags: NSEvent.ModifierFlags,
|
||||
chars: String,
|
||||
keyCode: UInt16
|
||||
keyCode: UInt16,
|
||||
literalChars: String? = nil
|
||||
) -> Bool {
|
||||
let normalizedFlags = flags
|
||||
.intersection(.deviceIndependentFlagsMask)
|
||||
.subtracting([.numericPad, .function])
|
||||
guard normalizedFlags.contains(.command) else { return false }
|
||||
|
||||
let key = chars.lowercased()
|
||||
if key == "=" || key == "+" || key == "-" || key == "_" || key == "0" {
|
||||
let keys = browserZoomShortcutKeyCandidates(
|
||||
chars: chars,
|
||||
literalChars: literalChars,
|
||||
keyCode: keyCode
|
||||
)
|
||||
if keys.contains("=") || keys.contains("+") || keys.contains("-") || keys.contains("_") || keys.contains("0") {
|
||||
return true
|
||||
}
|
||||
switch keyCode {
|
||||
|
|
@ -5458,7 +5492,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
#if DEBUG
|
||||
logBrowserZoomShortcutTrace(stage: "probe", event: event, flags: flags, chars: chars)
|
||||
#endif
|
||||
let zoomAction = browserZoomShortcutAction(flags: flags, chars: chars, keyCode: event.keyCode)
|
||||
let zoomAction = browserZoomShortcutAction(
|
||||
flags: flags,
|
||||
chars: chars,
|
||||
keyCode: event.keyCode,
|
||||
literalChars: event.characters
|
||||
)
|
||||
#if DEBUG
|
||||
logBrowserZoomShortcutTrace(stage: "match", event: event, flags: flags, chars: chars, action: zoomAction)
|
||||
#endif
|
||||
|
|
@ -5552,7 +5591,12 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent
|
|||
action: BrowserZoomShortcutAction? = nil,
|
||||
handled: Bool? = nil
|
||||
) {
|
||||
guard browserZoomShortcutTraceCandidate(flags: flags, chars: chars, keyCode: event.keyCode) else {
|
||||
guard browserZoomShortcutTraceCandidate(
|
||||
flags: flags,
|
||||
chars: chars,
|
||||
keyCode: event.keyCode,
|
||||
literalChars: event.characters
|
||||
) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -7485,7 +7529,8 @@ private extension NSWindow {
|
|||
firstResponderIsGhostty: true,
|
||||
flags: event.modifierFlags,
|
||||
chars: event.charactersIgnoringModifiers ?? "",
|
||||
keyCode: event.keyCode
|
||||
keyCode: event.keyCode,
|
||||
literalChars: event.characters
|
||||
) {
|
||||
ghosttyView.keyDown(with: event)
|
||||
#if DEBUG
|
||||
|
|
@ -7538,7 +7583,8 @@ private extension NSWindow {
|
|||
if browserZoomShortcutTraceCandidate(
|
||||
flags: event.modifierFlags,
|
||||
chars: event.charactersIgnoringModifiers ?? "",
|
||||
keyCode: event.keyCode
|
||||
keyCode: event.keyCode,
|
||||
literalChars: event.characters
|
||||
) {
|
||||
dlog(
|
||||
"zoom.shortcut stage=window.mainMenuBypass event=\(Self.keyDescription(event)) " +
|
||||
|
|
|
|||
|
|
@ -11,4 +11,36 @@ class KeyboardLayout {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Translate a physical keyCode to the unmodified character under the current keyboard layout.
|
||||
static func character(forKeyCode keyCode: UInt16) -> String? {
|
||||
guard let source = TISCopyCurrentKeyboardInputSource()?.takeRetainedValue(),
|
||||
let layoutDataPointer = TISGetInputSourceProperty(source, kTISPropertyUnicodeKeyLayoutData) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let layoutData = unsafeBitCast(layoutDataPointer, to: CFData.self)
|
||||
guard let bytes = CFDataGetBytePtr(layoutData) else { return nil }
|
||||
let keyboardLayout = UnsafeRawPointer(bytes).assumingMemoryBound(to: UCKeyboardLayout.self)
|
||||
|
||||
var deadKeyState: UInt32 = 0
|
||||
var chars = [UniChar](repeating: 0, count: 4)
|
||||
var length = 0
|
||||
|
||||
let status = UCKeyTranslate(
|
||||
keyboardLayout,
|
||||
keyCode,
|
||||
UInt16(kUCKeyActionDisplay),
|
||||
0,
|
||||
UInt32(LMGetKbdType()),
|
||||
UInt32(kUCKeyTranslateNoDeadKeysBit),
|
||||
&deadKeyState,
|
||||
chars.count,
|
||||
&length,
|
||||
&chars
|
||||
)
|
||||
|
||||
guard status == noErr, length > 0 else { return nil }
|
||||
return String(utf16CodeUnits: chars, count: length).lowercased()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2045,6 +2045,26 @@ final class BrowserZoomShortcutActionTests: XCTestCase {
|
|||
)
|
||||
}
|
||||
|
||||
func testZoomInSupportsShiftedLiteralFromDifferentPhysicalKey() {
|
||||
XCTAssertEqual(
|
||||
browserZoomShortcutAction(
|
||||
flags: [.command, .shift],
|
||||
chars: ";",
|
||||
keyCode: 41,
|
||||
literalChars: "+"
|
||||
),
|
||||
.zoomIn
|
||||
)
|
||||
|
||||
XCTAssertNil(
|
||||
browserZoomShortcutAction(
|
||||
flags: [.command, .shift],
|
||||
chars: ";",
|
||||
keyCode: 41
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testZoomRequiresCommandWithoutOptionOrControl() {
|
||||
XCTAssertNil(browserZoomShortcutAction(flags: [], chars: "=", keyCode: 24))
|
||||
XCTAssertNil(browserZoomShortcutAction(flags: [.command, .option], chars: "=", keyCode: 24))
|
||||
|
|
@ -2108,6 +2128,18 @@ final class BrowserZoomShortcutRoutingPolicyTests: XCTestCase {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testRoutesForShiftedLiteralZoomShortcut() {
|
||||
XCTAssertTrue(
|
||||
shouldRouteTerminalFontZoomShortcutToGhostty(
|
||||
firstResponderIsGhostty: true,
|
||||
flags: [.command, .shift],
|
||||
chars: ";",
|
||||
keyCode: 41,
|
||||
literalChars: "+"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
final class GhosttyResponderResolutionTests: XCTestCase {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue