From 98a1b4421354c66499773bd2d2eec4d9fdef1ab3 Mon Sep 17 00:00:00 2001 From: Michael Adler Date: Mon, 10 Aug 2026 13:18:35 -0400 Subject: [PATCH] VNCConn: fix wrong keysym for Ctrl+Shift+symbol keys Ctrl-held keys collapse to ASCII C0 control codes (0..31); the revert added 96 unconditionally, correct only for Ctrl+'a'..'z'. Codes 0 and 27..31 are Ctrl+'@ [ \ ] ^ _' and need +64, so Ctrl+Shift+Minus (code 31) became DEL (127) instead of '_' (95). On Mac, Ctrl also routes non-letter keys through Cocoa's doCommandBySelector:, which drops Shift before wx ever sees the char, independent of the above. Reconstruct it via UCKeyTranslate against the live keyboard layout when Shift+Ctrl are both down. --- src/VNCConn.cpp | 61 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/src/VNCConn.cpp b/src/VNCConn.cpp index 61e3fa2d..feddcfbd 100644 --- a/src/VNCConn.cpp +++ b/src/VNCConn.cpp @@ -33,6 +33,9 @@ #else #include #endif +#if defined __APPLE__ +#include +#endif #include "VNCConn.h" bool VNCConn::libsshtunnel_initialized = false; @@ -1529,6 +1532,38 @@ void VNCConn::sendPointerEvent(wxMouseEvent &event) } +#if defined __APPLE__ +// translate a raw macOS hardware key code to the character it produces +// with Shift held, using the currently active keyboard layout, so this +// works for any input source (not just US). +static int MacShiftedCharForRawKeyCode(wxUint32 rawKeyCode) +{ + TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource(); + if(!currentKeyboard) + return 0; + CFDataRef layoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); + CFRelease(currentKeyboard); + if(!layoutData) + return 0; + const UCKeyboardLayout *layout = (const UCKeyboardLayout*)CFDataGetBytePtr(layoutData); + if(!layout) + return 0; + + UInt32 deadKeyState = 0; + UniCharCount actualLength = 0; + UniChar chars[4]; + OSStatus status = UCKeyTranslate(layout, (UInt16)rawKeyCode, kUCKeyActionDown, + shiftKey >> 8, LMGetKbdType(), + kUCKeyTranslateNoDeadKeysMask, + &deadKeyState, 4, &actualLength, chars); + if(status != noErr || actualLength != 1) + return 0; + + return chars[0]; +} +#endif + + // because of the possible wxKeyEvent.Skip(), this posts the found keysym + down bool VNCConn::sendKeyEvent(wxKeyEvent &event, bool down, bool isChar) { @@ -1551,15 +1586,33 @@ bool VNCConn::sendKeyEvent(wxKeyEvent &event, bool down, bool isChar) if(kev.keysym==0) kev.keysym = event.GetUnicodeKey(); - // if wxwidgets translates a key combination into a - // value below 32, revert this here. - // we dont't send ASCII 0x03, but ctrl and then a 'c'! + // wxwidgets (or the underlying toolkit) translates a Ctrl-held key + // combination into its ASCII C0 control code (0..31), revert this + // here: we dont't send ASCII 0x03, but ctrl and then a 'c'! + // codes 1..26 are Ctrl+'a'..'z', while 0 and 27..31 are + // Ctrl+one of '@ [ \ ] ^ _', so the two ranges revert differently. if(kev.keysym <= 32) { - kev.keysym += 96; + kev.keysym += (kev.keysym >= 1 && kev.keysym <= 26) ? 96 : 64; wxLogDebug(wxT("VNCConn %p: translating key to: %d"), this, kev.keysym); } +#if defined __APPLE__ + // on Mac, wx's char translation loses the Shift state when Ctrl is + // held at the same time (it then routes the key through Cocoa's + // doCommandBySelector: instead of insertText:, falling back to a + // wx-internal helper that queries the keyboard layout without the + // Shift modifier). Reconstruct the shifted character ourselves from + // the currently active keyboard layout so this also works correctly + // for non-US layouts. + if(event.ShiftDown() && (event.RawControlDown() || event.ControlDown())) + { + int shifted = MacShiftedCharForRawKeyCode(event.GetRawKeyCode()); + if(shifted) + kev.keysym = shifted; + } +#endif + wxLogDebug(wxT("VNCConn %p: sending rfbkeysym: 0x%.3x down"), this, kev.keysym); wxLogDebug(wxT("VNCConn %p: sending rfbkeysym: 0x%.3x up"), this, kev.keysym);