Improve lookup of special keys

Look up keys that are independent of layout and state first,
followed by keys that are only mild variations in layouts.
This is more robust as there might be multiple physical keys
generating the same symbols, and Keysyms don't map directly to
Unicode in all cases.

At the same time switch over to using the modern, standardised
'code' field for lookup.
This commit is contained in:
Pierre Ossman 2017-01-24 17:18:43 +01:00
parent bfa1b237b9
commit f714f7deae
6 changed files with 466 additions and 254 deletions

112
core/input/fixedkeys.js Normal file
View File

@ -0,0 +1,112 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2017 Pierre Ossman for Cendio AB
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
/*
* Mapping between HTML key codes and VNC/X11 keysyms for the
* subset of keys that have the same mapping on every keyboard
* layout. Keys that vary between layouts must never be included
* in this list.
*/
import KeyTable from "./keysym.js";
export default {
'Backspace': KeyTable.XK_BackSpace,
'AltLeft': KeyTable.XK_Alt_L,
// AltRight is special
'CapsLock': KeyTable.XK_Caps_Lock,
'ContextMenu': KeyTable.XK_Menu,
'ControlLeft': KeyTable.XK_Control_L,
'ControlRight': KeyTable.XK_Control_R,
'Enter': KeyTable.XK_Return,
'MetaLeft': KeyTable.XK_Super_L,
'MetaRight': KeyTable.XK_Super_R,
'ShiftLeft': KeyTable.XK_Shift_L,
'ShiftRight': KeyTable.XK_Shift_R,
'Space': KeyTable.XK_space,
'Tab': KeyTable.XK_Tab,
// FIXME: Japanese/Korean keys
'Delete': KeyTable.XK_Delete,
'End': KeyTable.XK_End,
'Help': KeyTable.XK_Help,
'Home': KeyTable.XK_Home,
'Insert': KeyTable.XK_Insert,
'PageDown': KeyTable.XK_Next,
'PageUp': KeyTable.XK_Prior,
'ArrowDown': KeyTable.XK_Down,
'ArrowLeft': KeyTable.XK_Left,
'ArrowRight': KeyTable.XK_Right,
'ArrowUp': KeyTable.XK_Up,
'NumLock': KeyTable.XK_Num_Lock,
'NumpadAdd': KeyTable.XK_KP_Add,
'NumpadBackspace': KeyTable.XK_KP_Delete,
'NumpadClear': KeyTable.XK_Clear,
// NumpadDecimal is special
'NumpadDivide': KeyTable.XK_KP_Divide,
'NumpadEnter': KeyTable.XK_KP_Enter,
'NumpadEqual': KeyTable.XK_KP_Equal,
'NumpadMultiply': KeyTable.XK_KP_Multiply,
'NumpadSubtract': KeyTable.XK_KP_Subtract,
'Escape': KeyTable.XK_Escape,
'F1': KeyTable.XK_F1,
'F2': KeyTable.XK_F2,
'F3': KeyTable.XK_F3,
'F4': KeyTable.XK_F4,
'F5': KeyTable.XK_F5,
'F6': KeyTable.XK_F6,
'F7': KeyTable.XK_F7,
'F8': KeyTable.XK_F8,
'F9': KeyTable.XK_F9,
'F10': KeyTable.XK_F10,
'F11': KeyTable.XK_F11,
'F12': KeyTable.XK_F12,
'F13': KeyTable.XK_F13,
'F14': KeyTable.XK_F14,
'F15': KeyTable.XK_F15,
'F16': KeyTable.XK_F16,
'F17': KeyTable.XK_F17,
'F18': KeyTable.XK_F18,
'F19': KeyTable.XK_F19,
'F20': KeyTable.XK_F20,
'F21': KeyTable.XK_F21,
'F22': KeyTable.XK_F22,
'F23': KeyTable.XK_F23,
'F24': KeyTable.XK_F24,
'F25': KeyTable.XK_F25,
'F26': KeyTable.XK_F26,
'F27': KeyTable.XK_F27,
'F28': KeyTable.XK_F28,
'F29': KeyTable.XK_F29,
'F30': KeyTable.XK_F30,
'F31': KeyTable.XK_F31,
'F32': KeyTable.XK_F32,
'F33': KeyTable.XK_F33,
'F34': KeyTable.XK_F34,
'F35': KeyTable.XK_F35,
'PrintScreen': KeyTable.XK_Print,
'ScrollLock': KeyTable.XK_Scroll_Lock,
'Pause': KeyTable.XK_Pause,
'BrowserBack': KeyTable.XF86XK_Back,
'BrowserFavorites': KeyTable.XF86XK_Favorites,
'BrowserForward': KeyTable.XF86XK_Forward,
'BrowserHome': KeyTable.XF86XK_HomePage,
'BrowserRefresh': KeyTable.XF86XK_Refresh,
'BrowserSearch': KeyTable.XF86XK_Search,
'BrowserStop': KeyTable.XF86XK_Stop,
'LaunchApp1': KeyTable.XF86XK_Explorer,
'LaunchApp2': KeyTable.XF86XK_Calculator,
'LaunchMail': KeyTable.XF86XK_Mail,
'MediaPlayPause': KeyTable.XF86XK_AudioPlay,
'MediaStop': KeyTable.XF86XK_AudioStop,
'MediaTrackNext': KeyTable.XF86XK_AudioNext,
'MediaTrackPrevious': KeyTable.XF86XK_AudioPrev,
'Power': KeyTable.XF86XK_PowerOff,
'Sleep': KeyTable.XF86XK_Sleep,
'AudioVolumeDown': KeyTable.XF86XK_AudioLowerVolume,
'AudioVolumeMute': KeyTable.XF86XK_AudioMute,
'AudioVolumeUp': KeyTable.XF86XK_AudioRaiseVolume,
'WakeUp': KeyTable.XF86XK_WakeUp,
};

View File

@ -377,4 +377,188 @@ export default {
XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */
XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */
XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */
/*
* XFree86 vendor specific keysyms.
*
* The XFree86 keysym range is 0x10080001 - 0x1008FFFF.
*/
XF86XK_ModeLock: 0x1008FF01,
XF86XK_MonBrightnessUp: 0x1008FF02,
XF86XK_MonBrightnessDown: 0x1008FF03,
XF86XK_KbdLightOnOff: 0x1008FF04,
XF86XK_KbdBrightnessUp: 0x1008FF05,
XF86XK_KbdBrightnessDown: 0x1008FF06,
XF86XK_Standby: 0x1008FF10,
XF86XK_AudioLowerVolume: 0x1008FF11,
XF86XK_AudioMute: 0x1008FF12,
XF86XK_AudioRaiseVolume: 0x1008FF13,
XF86XK_AudioPlay: 0x1008FF14,
XF86XK_AudioStop: 0x1008FF15,
XF86XK_AudioPrev: 0x1008FF16,
XF86XK_AudioNext: 0x1008FF17,
XF86XK_HomePage: 0x1008FF18,
XF86XK_Mail: 0x1008FF19,
XF86XK_Start: 0x1008FF1A,
XF86XK_Search: 0x1008FF1B,
XF86XK_AudioRecord: 0x1008FF1C,
XF86XK_Calculator: 0x1008FF1D,
XF86XK_Memo: 0x1008FF1E,
XF86XK_ToDoList: 0x1008FF1F,
XF86XK_Calendar: 0x1008FF20,
XF86XK_PowerDown: 0x1008FF21,
XF86XK_ContrastAdjust: 0x1008FF22,
XF86XK_RockerUp: 0x1008FF23,
XF86XK_RockerDown: 0x1008FF24,
XF86XK_RockerEnter: 0x1008FF25,
XF86XK_Back: 0x1008FF26,
XF86XK_Forward: 0x1008FF27,
XF86XK_Stop: 0x1008FF28,
XF86XK_Refresh: 0x1008FF29,
XF86XK_PowerOff: 0x1008FF2A,
XF86XK_WakeUp: 0x1008FF2B,
XF86XK_Eject: 0x1008FF2C,
XF86XK_ScreenSaver: 0x1008FF2D,
XF86XK_WWW: 0x1008FF2E,
XF86XK_Sleep: 0x1008FF2F,
XF86XK_Favorites: 0x1008FF30,
XF86XK_AudioPause: 0x1008FF31,
XF86XK_AudioMedia: 0x1008FF32,
XF86XK_MyComputer: 0x1008FF33,
XF86XK_VendorHome: 0x1008FF34,
XF86XK_LightBulb: 0x1008FF35,
XF86XK_Shop: 0x1008FF36,
XF86XK_History: 0x1008FF37,
XF86XK_OpenURL: 0x1008FF38,
XF86XK_AddFavorite: 0x1008FF39,
XF86XK_HotLinks: 0x1008FF3A,
XF86XK_BrightnessAdjust: 0x1008FF3B,
XF86XK_Finance: 0x1008FF3C,
XF86XK_Community: 0x1008FF3D,
XF86XK_AudioRewind: 0x1008FF3E,
XF86XK_BackForward: 0x1008FF3F,
XF86XK_Launch0: 0x1008FF40,
XF86XK_Launch1: 0x1008FF41,
XF86XK_Launch2: 0x1008FF42,
XF86XK_Launch3: 0x1008FF43,
XF86XK_Launch4: 0x1008FF44,
XF86XK_Launch5: 0x1008FF45,
XF86XK_Launch6: 0x1008FF46,
XF86XK_Launch7: 0x1008FF47,
XF86XK_Launch8: 0x1008FF48,
XF86XK_Launch9: 0x1008FF49,
XF86XK_LaunchA: 0x1008FF4A,
XF86XK_LaunchB: 0x1008FF4B,
XF86XK_LaunchC: 0x1008FF4C,
XF86XK_LaunchD: 0x1008FF4D,
XF86XK_LaunchE: 0x1008FF4E,
XF86XK_LaunchF: 0x1008FF4F,
XF86XK_ApplicationLeft: 0x1008FF50,
XF86XK_ApplicationRight: 0x1008FF51,
XF86XK_Book: 0x1008FF52,
XF86XK_CD: 0x1008FF53,
XF86XK_Calculater: 0x1008FF54,
XF86XK_Clear: 0x1008FF55,
XF86XK_Close: 0x1008FF56,
XF86XK_Copy: 0x1008FF57,
XF86XK_Cut: 0x1008FF58,
XF86XK_Display: 0x1008FF59,
XF86XK_DOS: 0x1008FF5A,
XF86XK_Documents: 0x1008FF5B,
XF86XK_Excel: 0x1008FF5C,
XF86XK_Explorer: 0x1008FF5D,
XF86XK_Game: 0x1008FF5E,
XF86XK_Go: 0x1008FF5F,
XF86XK_iTouch: 0x1008FF60,
XF86XK_LogOff: 0x1008FF61,
XF86XK_Market: 0x1008FF62,
XF86XK_Meeting: 0x1008FF63,
XF86XK_MenuKB: 0x1008FF65,
XF86XK_MenuPB: 0x1008FF66,
XF86XK_MySites: 0x1008FF67,
XF86XK_New: 0x1008FF68,
XF86XK_News: 0x1008FF69,
XF86XK_OfficeHome: 0x1008FF6A,
XF86XK_Open: 0x1008FF6B,
XF86XK_Option: 0x1008FF6C,
XF86XK_Paste: 0x1008FF6D,
XF86XK_Phone: 0x1008FF6E,
XF86XK_Q: 0x1008FF70,
XF86XK_Reply: 0x1008FF72,
XF86XK_Reload: 0x1008FF73,
XF86XK_RotateWindows: 0x1008FF74,
XF86XK_RotationPB: 0x1008FF75,
XF86XK_RotationKB: 0x1008FF76,
XF86XK_Save: 0x1008FF77,
XF86XK_ScrollUp: 0x1008FF78,
XF86XK_ScrollDown: 0x1008FF79,
XF86XK_ScrollClick: 0x1008FF7A,
XF86XK_Send: 0x1008FF7B,
XF86XK_Spell: 0x1008FF7C,
XF86XK_SplitScreen: 0x1008FF7D,
XF86XK_Support: 0x1008FF7E,
XF86XK_TaskPane: 0x1008FF7F,
XF86XK_Terminal: 0x1008FF80,
XF86XK_Tools: 0x1008FF81,
XF86XK_Travel: 0x1008FF82,
XF86XK_UserPB: 0x1008FF84,
XF86XK_User1KB: 0x1008FF85,
XF86XK_User2KB: 0x1008FF86,
XF86XK_Video: 0x1008FF87,
XF86XK_WheelButton: 0x1008FF88,
XF86XK_Word: 0x1008FF89,
XF86XK_Xfer: 0x1008FF8A,
XF86XK_ZoomIn: 0x1008FF8B,
XF86XK_ZoomOut: 0x1008FF8C,
XF86XK_Away: 0x1008FF8D,
XF86XK_Messenger: 0x1008FF8E,
XF86XK_WebCam: 0x1008FF8F,
XF86XK_MailForward: 0x1008FF90,
XF86XK_Pictures: 0x1008FF91,
XF86XK_Music: 0x1008FF92,
XF86XK_Battery: 0x1008FF93,
XF86XK_Bluetooth: 0x1008FF94,
XF86XK_WLAN: 0x1008FF95,
XF86XK_UWB: 0x1008FF96,
XF86XK_AudioForward: 0x1008FF97,
XF86XK_AudioRepeat: 0x1008FF98,
XF86XK_AudioRandomPlay: 0x1008FF99,
XF86XK_Subtitle: 0x1008FF9A,
XF86XK_AudioCycleTrack: 0x1008FF9B,
XF86XK_CycleAngle: 0x1008FF9C,
XF86XK_FrameBack: 0x1008FF9D,
XF86XK_FrameForward: 0x1008FF9E,
XF86XK_Time: 0x1008FF9F,
XF86XK_Select: 0x1008FFA0,
XF86XK_View: 0x1008FFA1,
XF86XK_TopMenu: 0x1008FFA2,
XF86XK_Red: 0x1008FFA3,
XF86XK_Green: 0x1008FFA4,
XF86XK_Yellow: 0x1008FFA5,
XF86XK_Blue: 0x1008FFA6,
XF86XK_Suspend: 0x1008FFA7,
XF86XK_Hibernate: 0x1008FFA8,
XF86XK_TouchpadToggle: 0x1008FFA9,
XF86XK_TouchpadOn: 0x1008FFB0,
XF86XK_TouchpadOff: 0x1008FFB1,
XF86XK_AudioMicMute: 0x1008FFB2,
XF86XK_Switch_VT_1: 0x1008FE01,
XF86XK_Switch_VT_2: 0x1008FE02,
XF86XK_Switch_VT_3: 0x1008FE03,
XF86XK_Switch_VT_4: 0x1008FE04,
XF86XK_Switch_VT_5: 0x1008FE05,
XF86XK_Switch_VT_6: 0x1008FE06,
XF86XK_Switch_VT_7: 0x1008FE07,
XF86XK_Switch_VT_8: 0x1008FE08,
XF86XK_Switch_VT_9: 0x1008FE09,
XF86XK_Switch_VT_10: 0x1008FE0A,
XF86XK_Switch_VT_11: 0x1008FE0B,
XF86XK_Switch_VT_12: 0x1008FE0C,
XF86XK_Ungrab: 0x1008FE20,
XF86XK_ClearGrab: 0x1008FE21,
XF86XK_Next_VMode: 0x1008FE22,
XF86XK_Prev_VMode: 0x1008FE23,
XF86XK_LogWindowTree: 0x1008FE24,
XF86XK_LogGrabInfo: 0x1008FE25,
};

View File

@ -1,6 +1,7 @@
import KeyTable from "./keysym.js";
import keysyms from "./keysymdef.js";
import vkeys from "./vkeys.js";
import fixedkeys from "./fixedkeys.js";
function isMac() {
return navigator && !!(/mac/i).exec(navigator.platform);
@ -12,29 +13,6 @@ function isLinux() {
return navigator && !!(/linux/i).exec(navigator.platform);
}
// Return true if a modifier which is not the specified char modifier (and is not shift) is down
export function hasShortcutModifier(charModifier, currentModifiers) {
var mods = {};
for (var key in currentModifiers) {
if (parseInt(key) !== KeyTable.XK_Shift_L) {
mods[key] = currentModifiers[key];
}
}
var sum = 0;
for (var k in currentModifiers) {
if (mods[k]) {
++sum;
}
}
if (hasCharModifier(charModifier, mods)) {
return sum > charModifier.length;
}
else {
return sum > 0;
}
}
// Return true if the specified char modifier is currently down
export function hasCharModifier(charModifier, currentModifiers) {
if (charModifier.length === 0) { return false; }
@ -125,8 +103,6 @@ export function ModifierSync(charModifier) {
// Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
syncAny: function(evt) { return sync(evt);},
// is a shortcut modifier down?
hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); },
// if a char modifier is down, return the keys it consists of, otherwise return null
activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
};
@ -193,15 +169,83 @@ export function getKeycode(evt){
}
// Get the most reliable keysym value we can get from a key event
// if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
export function getKeysym(evt){
// We start with layout independent keys
var code = getKeycode(evt);
if (code in fixedkeys) {
return fixedkeys[code];
}
// Next with mildly layout or state sensitive stuff
// Like AltGraph
if (code === 'AltRight') {
if (evt.key === 'AltGraph') {
return KeyTable.XK_ISO_Level3_Shift;
} else {
return KeyTable.XK_Alt_R;
}
}
// Or the numpad
if (evt.location === 3) {
var key = evt.key;
// IE and Edge use some ancient version of the spec
// https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/
switch (key) {
case 'Up': key = 'ArrowUp'; break;
case 'Left': key = 'ArrowLeft'; break;
case 'Right': key = 'ArrowRight'; break;
case 'Down': key = 'ArrowDown'; break;
case 'Del': key = 'Delete'; break;
}
// Safari doesn't support KeyboardEvent.key yet
if ((key === undefined) && (evt.charCode)) {
key = String.fromCharCode(evt.charCode);
}
switch (key) {
case '0': return KeyTable.XK_KP_0;
case '1': return KeyTable.XK_KP_1;
case '2': return KeyTable.XK_KP_2;
case '3': return KeyTable.XK_KP_3;
case '4': return KeyTable.XK_KP_4;
case '5': return KeyTable.XK_KP_5;
case '6': return KeyTable.XK_KP_6;
case '7': return KeyTable.XK_KP_7;
case '8': return KeyTable.XK_KP_8;
case '9': return KeyTable.XK_KP_9;
// There is utter mayhem in the world when it comes to which
// character to use as a decimal separator...
case '.': return KeyTable.XK_KP_Decimal;
case ',': return KeyTable.XK_KP_Separator;
case 'Home': return KeyTable.XK_KP_Home;
case 'End': return KeyTable.XK_KP_End;
case 'PageUp': return KeyTable.XK_KP_Prior;
case 'PageDown': return KeyTable.XK_KP_Next;
case 'Insert': return KeyTable.XK_KP_Insert;
case 'Delete': return KeyTable.XK_KP_Delete;
case 'ArrowUp': return KeyTable.XK_KP_Up;
case 'ArrowLeft': return KeyTable.XK_KP_Left;
case 'ArrowRight': return KeyTable.XK_KP_Right;
case 'ArrowDown': return KeyTable.XK_KP_Down;
}
}
// Now we need to look at the Unicode symbol instead
var codepoint;
if ('key' in evt) {
// Ignore special keys
if (evt.key.length === 1) {
codepoint = evt.key.charCodeAt();
// Special key? (FIXME: Should have been caught earlier)
if (evt.key.length !== 1) {
return null;
}
codepoint = evt.key.charCodeAt();
} else if ('charCode' in evt) {
codepoint = evt.charCode;
}
@ -210,94 +254,9 @@ export function getKeysym(evt){
return keysyms.lookup(codepoint);
}
if (evt.keyCode) {
return keysymFromKeyCode(evt.keyCode, evt.shiftKey);
}
if (evt.which) {
return keysymFromKeyCode(evt.which, evt.shiftKey);
}
return null;
}
// Given a keycode, try to predict which keysym it might be.
// If the keycode is unknown, null is returned.
function keysymFromKeyCode(keycode, shiftPressed) {
if (typeof(keycode) !== 'number') {
return null;
}
// won't be accurate for azerty
if (keycode >= 0x30 && keycode <= 0x39) {
return keycode; // digit
}
if (keycode >= 0x41 && keycode <= 0x5a) {
// remap to lowercase unless shift is down
return shiftPressed ? keycode : keycode + 32; // A-Z
}
if (keycode >= 0x60 && keycode <= 0x69) {
return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9
}
switch(keycode) {
case 0x20: return KeyTable.XK_space;
case 0x6a: return KeyTable.XK_KP_Multiply;
case 0x6b: return KeyTable.XK_KP_Add;
case 0x6c: return KeyTable.XK_KP_Separator;
case 0x6d: return KeyTable.XK_KP_Subtract;
case 0x6e: return KeyTable.XK_KP_Decimal;
case 0x6f: return KeyTable.XK_KP_Divide;
case 0xbb: return KeyTable.XK_plus;
case 0xbc: return KeyTable.XK_comma;
case 0xbd: return KeyTable.XK_minus;
case 0xbe: return KeyTable.XK_period;
}
return nonCharacterKey({keyCode: keycode});
}
// if the key is a known non-character key (any key which doesn't generate character data)
// return its keysym value. Otherwise return null
function nonCharacterKey(evt) {
// evt.key not implemented yet
if (!evt.keyCode) { return null; }
var keycode = evt.keyCode;
if (keycode >= 0x70 && keycode <= 0x87) {
return KeyTable.XK_F1 + keycode - 0x70; // F1-F24
}
switch (keycode) {
case 8 : return KeyTable.XK_BackSpace;
case 13 : return KeyTable.XK_Return;
case 9 : return KeyTable.XK_Tab;
case 27 : return KeyTable.XK_Escape;
case 46 : return KeyTable.XK_Delete;
case 36 : return KeyTable.XK_Home;
case 35 : return KeyTable.XK_End;
case 33 : return KeyTable.XK_Page_Up;
case 34 : return KeyTable.XK_Page_Down;
case 45 : return KeyTable.XK_Insert;
case 37 : return KeyTable.XK_Left;
case 38 : return KeyTable.XK_Up;
case 39 : return KeyTable.XK_Right;
case 40 : return KeyTable.XK_Down;
case 16 : return KeyTable.XK_Shift_L;
case 17 : return KeyTable.XK_Control_L;
case 18 : return KeyTable.XK_Alt_L; // also: Option-key on Mac
case 224 : return KeyTable.XK_Meta_L;
case 225 : return KeyTable.XK_ISO_Level3_Shift; // AltGr
case 91 : return KeyTable.XK_Super_L; // also: Windows-key
case 92 : return KeyTable.XK_Super_R; // also: Windows-key
case 93 : return KeyTable.XK_Menu; // also: Windows-Menu, Command on Mac
default: return null;
}
}
export function QEMUKeyEventDecoder (modifierState, next) {
"use strict";
@ -341,10 +300,7 @@ export function QEMUKeyEventDecoder (modifierState, next) {
result.keysym = getNumPadKeySym(evt);
}
var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
var isShift = result.code === 'ShiftLeft' || result.code === 'ShiftRight';
var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt));
var suppress = type !== 'keydown' || !!getKeysym(evt);
next(result);
return suppress;
@ -457,20 +413,17 @@ export function KeyEventDecoder (modifierState, next) {
var keysym = getKeysym(evt);
var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
// Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
// "special" keys like enter, tab or backspace don't send keypress events,
// and some browsers don't send keypresses at all if a modifier is down
if (keysym && (type !== 'keydown' || nonCharacterKey(evt) || hasModifier)) {
if (keysym) {
result.keysym = keysym;
}
var isShift = code === 'ShiftLeft' || code === 'ShiftRight';
// Should we prevent the browser from handling the event?
// Doing so on a keydown (in most browsers) prevents keypress from being generated
// so only do that if we have to.
var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt));
var suppress = type !== 'keydown' || !!keysym;
// if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
var active = modifierState.activeCharModifier();

View File

@ -108,26 +108,50 @@ describe('Helpers', function() {
describe('Non-character keys', function() {
it('should recognize the right keys', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0x0d})).to.be.equal(0xFF0D);
expect(KeyboardUtil.getKeysym({keyCode: 0x08})).to.be.equal(0xFF08);
expect(KeyboardUtil.getKeysym({keyCode: 0x09})).to.be.equal(0xFF09);
expect(KeyboardUtil.getKeysym({keyCode: 0x10})).to.be.equal(0xFFE1);
expect(KeyboardUtil.getKeysym({keyCode: 0x11})).to.be.equal(0xFFE3);
expect(KeyboardUtil.getKeysym({keyCode: 0x12})).to.be.equal(0xFFE9);
expect(KeyboardUtil.getKeysym({keyCode: 0xe0})).to.be.equal(0xFFE7);
expect(KeyboardUtil.getKeysym({keyCode: 0xe1})).to.be.equal(0xFE03);
expect(KeyboardUtil.getKeysym({keyCode: 0x1b})).to.be.equal(0xFF1B);
expect(KeyboardUtil.getKeysym({keyCode: 0x26})).to.be.equal(0xFF52);
expect(KeyboardUtil.getKeysym({code: 'Enter'})).to.be.equal(0xFF0D);
expect(KeyboardUtil.getKeysym({code: 'Backspace'})).to.be.equal(0xFF08);
expect(KeyboardUtil.getKeysym({code: 'Tab'})).to.be.equal(0xFF09);
expect(KeyboardUtil.getKeysym({code: 'ShiftLeft'})).to.be.equal(0xFFE1);
expect(KeyboardUtil.getKeysym({code: 'ControlLeft'})).to.be.equal(0xFFE3);
expect(KeyboardUtil.getKeysym({code: 'AltLeft'})).to.be.equal(0xFFE9);
expect(KeyboardUtil.getKeysym({code: 'MetaLeft'})).to.be.equal(0xFFEB);
expect(KeyboardUtil.getKeysym({code: 'Escape'})).to.be.equal(0xFF1B);
expect(KeyboardUtil.getKeysym({code: 'ArrowUp'})).to.be.equal(0xFF52);
});
it('should return null for unknown keycodes', function() {
expect(KeyboardUtil.getKeysym({keyCode: 0xc0})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: 0xde})).to.be.null;
it('should handle AltGraph', function() {
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltRight'})).to.be.equal(0xFFEA);
expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph'})).to.be.equal(0xFE03);
});
it('should return null for unknown codes', function() {
expect(KeyboardUtil.getKeysym({code: 'Semicolon'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'BracketRight'})).to.be.null;
});
it('should not recognize character keys', function() {
expect(KeyboardUtil.getKeysym({keyCode: 'A'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: '1'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: '.'})).to.be.null;
expect(KeyboardUtil.getKeysym({keyCode: ' '})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'KeyA'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'Digit1'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'Period'})).to.be.null;
expect(KeyboardUtil.getKeysym({code: 'Numpad1'})).to.be.null;
});
});
describe('Numpad', function() {
it('should handle Numpad numbers', function() {
expect(KeyboardUtil.getKeysym({code: 'Digit5', key: '5', location: 0})).to.be.equal(0x0035);
expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: '5', location: 3})).to.be.equal(0xFFB5);
});
it('should handle Numpad non-character keys', function() {
expect(KeyboardUtil.getKeysym({code: 'Home', key: 'Home', location: 0})).to.be.equal(0xFF50);
expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: 'Home', location: 3})).to.be.equal(0xFF95);
expect(KeyboardUtil.getKeysym({code: 'Delete', key: 'Delete', location: 0})).to.be.equal(0xFFFF);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Delete', location: 3})).to.be.equal(0xFF9F);
});
it('should handle IE/Edge key names', function() {
expect(KeyboardUtil.getKeysym({code: 'Numpad6', key: 'Right', location: 3})).to.be.equal(0xFF98);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Del', location: 3})).to.be.equal(0xFF9F);
});
it('should handle Numpad Decimal key', function() {
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: '.', location: 3})).to.be.equal(0xFFAE);
expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC);
});
});
});
@ -137,7 +161,7 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it ('should do nothing if all modifiers are up as expected', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: false,
altKey: false,
altGraphKey: false,
@ -147,7 +171,7 @@ describe('Helpers', function() {
});
it ('should synthesize events if all keys are unexpectedly down', function() {
var result = sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
altKey: true,
altGraphKey: true,
@ -167,7 +191,7 @@ describe('Helpers', function() {
});
it ('should do nothing if all modifiers are down as expected', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
altKey: true,
altGraphKey: true,
@ -180,13 +204,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
})).to.be.deep.equal([{keysym: 0xffe3, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: false
})).to.be.deep.equal([{keysym: 0xffe3, type: 'keyup'}]);
});
@ -195,13 +219,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altKey: true,
})).to.be.deep.equal([{keysym: 0xffe9, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altKey: false
})).to.be.deep.equal([{keysym: 0xffe9, type: 'keyup'}]);
});
@ -210,13 +234,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altGraphKey: true,
})).to.be.deep.equal([{keysym: 0xfe03, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
altGraphKey: false
})).to.be.deep.equal([{keysym: 0xfe03, type: 'keyup'}]);
});
@ -225,13 +249,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
shiftKey: true,
})).to.be.deep.equal([{keysym: 0xffe1, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
shiftKey: false
})).to.be.deep.equal([{keysym: 0xffe1, type: 'keyup'}]);
});
@ -240,13 +264,13 @@ describe('Helpers', function() {
var sync = KeyboardUtil.ModifierSync();
it('should sync if modifier is suddenly down', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
metaKey: true,
})).to.be.deep.equal([{keysym: 0xffe7, type: 'keydown'}]);
});
it('should sync if modifier is suddenly up', function() {
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
metaKey: false
})).to.be.deep.equal([{keysym: 0xffe7, type: 'keyup'}]);
});
@ -254,27 +278,27 @@ describe('Helpers', function() {
describe('Modifier keyevents', function() {
it('should not sync a modifier on its own events', function() {
expect(KeyboardUtil.ModifierSync().keydown({
keyCode: 0x11,
code: 'ControlLeft',
ctrlKey: false
})).to.be.deep.equal([]);
expect(KeyboardUtil.ModifierSync().keydown({
keyCode: 0x11,
code: 'ControlLeft',
ctrlKey: true
}), 'B').to.be.deep.equal([]);
})
it('should update state on modifier keyevents', function() {
var sync = KeyboardUtil.ModifierSync();
sync.keydown({
keyCode: 0x11,
code: 'ControlLeft',
});
expect(sync.keydown({
keyCode: 0x41,
code: 'KeyA',
ctrlKey: true,
})).to.be.deep.equal([]);
});
it('should sync other modifiers on ctrl events', function() {
expect(KeyboardUtil.ModifierSync().keydown({
keyCode: 0x11,
code: 'ControlLeft',
altKey: true
})).to.be.deep.equal([{keysym: 0xffe9, type: 'keydown'}]);
})
@ -287,9 +311,6 @@ describe('Helpers', function() {
});
});
describe('do not treat shift as a modifier key', function() {
it('should not treat shift as a shortcut modifier', function() {
expect(KeyboardUtil.hasShortcutModifier([], {0xffe1 : true})).to.be.false;
});
it('should not treat shift as a char modifier', function() {
expect(KeyboardUtil.hasCharModifier([], {0xffe1 : true})).to.be.false;
});

View File

@ -51,7 +51,7 @@ describe('Key Event Pipeline Stages', function() {
});
it('should forward keydown events with the right type', function(done) {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', type: 'keydown'});
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
done();
}).keydown({code: 'KeyA', key: 'a'});
});
@ -71,65 +71,57 @@ describe('Key Event Pipeline Stages', function() {
it('should suppress anything while a shortcut modifier is down', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
obj.keydown({keyCode: 0x11}); // press ctrl
expect(obj.keydown({key: 'A'})).to.be.true;
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.true;
expect(obj.keydown({key: '1'})).to.be.true;
expect(obj.keydown({key: '<'})).to.be.true;
expect(obj.keydown({key: 'ø'})).to.be.true;
obj.keydown({code: 'ControlLeft'});
expect(obj.keydown({code: 'KeyA', key: 'a'})).to.be.true;
expect(obj.keydown({code: 'Space', key: ' '})).to.be.true;
expect(obj.keydown({code: 'Digit1', key: '1'})).to.be.true;
expect(obj.keydown({code: 'IntlBackslash', key: '<'})).to.be.true;
expect(obj.keydown({code: 'Semicolon', key: 'ø'})).to.be.true;
});
it('should suppress non-character keys', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keydown({keyCode: 0x08}), 'a').to.be.true;
expect(obj.keydown({keyCode: 0x09}), 'b').to.be.true;
expect(obj.keydown({keyCode: 0x11}), 'd').to.be.true;
expect(obj.keydown({keyCode: 0x12}), 'e').to.be.true;
});
it('should not suppress shift', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keydown({keyCode: 0x10}), 'd').to.be.false;
expect(obj.keydown({code: 'Backspace'}), 'a').to.be.true;
expect(obj.keydown({code: 'Tab'}), 'b').to.be.true;
expect(obj.keydown({code: 'ControlLeft'}), 'd').to.be.true;
expect(obj.keydown({code: 'AltLeft'}), 'e').to.be.true;
});
it('should generate event for shift keydown', function() {
var called = false;
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.have.property('keysym');
called = true;
}).keydown({keyCode: 0x10});
}).keydown({code: 'ShiftLeft'});
expect(called).to.be.true;
});
it('should not suppress character keys', function() {
it('should suppress character keys with key', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keydown({key: 'A'})).to.be.false;
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
expect(obj.keydown({key: '1'})).to.be.false;
expect(obj.keydown({key: '<'})).to.be.false; // < key on DK Windows
expect(obj.keydown({key: 'ø'})).to.be.false; // Ø key on DK
expect(obj.keydown({code: 'KeyA', key: 'a'})).to.be.true;
expect(obj.keydown({code: 'Digit1', key: '1'})).to.be.true;
expect(obj.keydown({code: 'IntlBackslash', key: '<'})).to.be.true;
expect(obj.keydown({code: 'Semicolon', key: 'ø'})).to.be.true;
});
it('should not suppress if a char modifier is down', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {});
it('should not suppress character keys without key', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
obj.keydown({keyCode: 0xe1}); // press altgr
expect(obj.keydown({key: 'A'})).to.be.false;
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
expect(obj.keydown({key: '1'})).to.be.false;
expect(obj.keydown({key: '<'})).to.be.false;
expect(obj.keydown({key: 'ø'})).to.be.false;
expect(obj.keydown({code: 'KeyA'})).to.be.false;
expect(obj.keydown({code: 'Digit1'})).to.be.false;
expect(obj.keydown({code: 'IntlBackslash'})).to.be.false;
expect(obj.keydown({code: 'Semicolon'})).to.be.false;
});
});
describe('Keypress and keyup events', function() {
it('should always suppress event propagation', function() {
var obj = KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {});
expect(obj.keypress({key: 'A'})).to.be.true;
expect(obj.keypress({key: '<'})).to.be.true;
expect(obj.keypress({keyCode: 0x11})).to.be.true;
expect(obj.keypress({code: 'KeyA', key: 'a'})).to.be.true;
expect(obj.keypress({code: 'IntlBackslash', key: '<'})).to.be.true;
expect(obj.keypress({code: 'ControlLeft', key: 'Control'})).to.be.true;
expect(obj.keyup({key: 'A'})).to.be.true;
expect(obj.keyup({key: '<'})).to.be.true;
expect(obj.keyup({keyCode: 0x11})).to.be.true;
expect(obj.keyup({code: 'KeyA', key: 'a'})).to.be.true;
expect(obj.keyup({code: 'IntlBackslash', key: '<'})).to.be.true;
expect(obj.keyup({code: 'ControlLeft', key: 'Control'})).to.be.true;
});
});
describe('mark events if a char modifier is down', function() {
@ -145,7 +137,7 @@ describe('Key Event Pipeline Stages', function() {
}
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keydown({code: 'AltRight', key: 'AltGraph'})
obj.keydown({code: 'KeyA', key: 'a'});
});
@ -167,7 +159,7 @@ describe('Key Event Pipeline Stages', function() {
}
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keydown({code: 'AltRight', key: 'AltGraph'})
obj.keypress({code: 'KeyA', key: 'a'});
});
it('should indicate on events if a multi-key char modifier is down', function(done) {
@ -190,8 +182,8 @@ describe('Key Event Pipeline Stages', function() {
}
});
obj.keydown({keyCode: 0x11}); // press ctrl
obj.keydown({keyCode: 0x12}); // press alt
obj.keydown({code: 'ControlLeft'});
obj.keydown({code: 'AltLeft'});
obj.keypress({code: 'KeyA', key: 'a'});
});
it('should not consider a char modifier to be down on the modifier key itself', function() {
@ -199,60 +191,10 @@ describe('Key Event Pipeline Stages', function() {
expect(evt).to.not.have.property('escape');
});
obj.keydown({keyCode: 0xe1}); // press altgr
obj.keydown({code: 'AltRight', key: 'AltGraph'})
});
});
describe('add/remove keysym', function() {
it('should remove keysym from keydown if a char key and no modifier', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', type: 'keydown'});
}).keydown({code: 'KeyA', key: 'a'});
});
it('should not remove keysym from keydown if a shortcut modifier is down', function() {
var times_called = 0;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
switch (times_called++) {
case 1:
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
break;
}
}).keydown({code: 'KeyA', key: 'a', ctrlKey: true});
expect(times_called).to.be.equal(2);
});
it('should not remove keysym from keydown if a char modifier is down', function() {
var times_called = 0;
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync([0xfe03]), function(evt) {
switch (times_called++) {
case 1:
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keydown'});
break;
}
}).keydown({code: 'KeyA', key: 'a', altGraphKey: true});
expect(times_called).to.be.equal(2);
});
it('should not remove keysym from keydown if key is noncharacter', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt, 'tab').to.be.deep.equal({code: 'Tab', keysym: 0xff09, type: 'keydown'});
}).keydown({keyCode: 0x09});
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt, 'ctrl').to.be.deep.equal({code: 'ControlLeft', keysym: 0xffe3, type: 'keydown'});
}).keydown({keyCode: 0x11});
});
it('should never remove keysym from keypress', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keypress'});
}).keypress({code: 'KeyA', key: 'a'});
});
it('should never remove keysym from keyup', function() {
KeyboardUtil.KeyEventDecoder(KeyboardUtil.ModifierSync(), function(evt) {
expect(evt).to.be.deep.equal({code: 'KeyA', keysym: 0x61, type: 'keyup'});
}).keyup({code: 'KeyA', key: 'a'});
});
});
// on keypress, keyup(?), always set keysym
// on keydown, only do it if we don't expect a keypress: if noncharacter OR modifier is down
});
describe('Track Key State', function() {

View File

@ -51,7 +51,7 @@
'core': ["base64.js", "websock.js", "des.js", "input/keysym.js",
"input/keysymdef.js", "input/xtscancodes.js", "input/util.js",
"input/devices.js", "display.js", "rfb.js", "inflator.js",
"input/vkeys.js"],
"input/vkeys.js", "input/fixedkeys.js"],
'tests': ["playback.js"],
'recordings': [fname]});
} else {