From 9782d4a324d24179f42f381080699a20eb164d06 Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Thu, 13 Apr 2017 14:50:55 +0200 Subject: [PATCH] Use KeyboardEvent.key too look up keysyms And emulate it on browsers where it is missing or incorrect. This makes the code more future oriented as it primarily uses the standardised fields. --- core/input/domkeytable.js | 310 ++++++++++++++++++++++++++++++++++++++ core/input/fixedkeys.js | 215 ++++++++++++++------------ core/input/keysym.js | 50 ++++++ core/input/util.js | 132 ++++++++-------- tests/test.helper.js | 113 ++++++++++---- tests/test.keyboard.js | 22 +-- 6 files changed, 644 insertions(+), 198 deletions(-) create mode 100644 core/input/domkeytable.js diff --git a/core/input/domkeytable.js b/core/input/domkeytable.js new file mode 100644 index 0000000..6758c08 --- /dev/null +++ b/core/input/domkeytable.js @@ -0,0 +1,310 @@ +/* + * noVNC: HTML5 VNC client + * Copyright (C) 2017 Pierre Ossman for Cendio AB + * Licensed under MPL 2.0 or any later version (see LICENSE.txt) + */ + +import KeyTable from "./keysym.js" + +/* + * Mapping between HTML key values and VNC/X11 keysyms for "special" + * keys that cannot be handled via their Unicode codepoint. + * + * See https://www.w3.org/TR/uievents-key/ for possible values. + */ + +var DOMKeyTable = {}; + +function addStandard(key, standard) +{ + if (standard === undefined) throw "Undefined keysym for key \"" + key + "\""; + if (key in DOMKeyTable) throw "Duplicate entry for key \"" + key + "\""; + DOMKeyTable[key] = [standard, standard, standard, standard]; +} + +function addLeftRight(key, left, right) +{ + if (left === undefined) throw "Undefined keysym for key \"" + key + "\""; + if (right === undefined) throw "Undefined keysym for key \"" + key + "\""; + if (key in DOMKeyTable) throw "Duplicate entry for key \"" + key + "\""; + DOMKeyTable[key] = [left, left, right, left]; +} + +function addNumpad(key, standard, numpad) +{ + if (standard === undefined) throw "Undefined keysym for key \"" + key + "\""; + if (numpad === undefined) throw "Undefined keysym for key \"" + key + "\""; + if (key in DOMKeyTable) throw "Duplicate entry for key \"" + key + "\""; + DOMKeyTable[key] = [standard, standard, standard, numpad]; +} + +// 2.2. Modifier Keys + +addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R); +addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift); +addStandard("CapsLock", KeyTable.XK_Caps_Lock); +addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R); +// - Fn +// - FnLock +addLeftRight("Hyper", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addStandard("NumLock", KeyTable.XK_Num_Lock); +addStandard("ScrollLock", KeyTable.XK_Scroll_Lock); +addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); +addLeftRight("Super", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +// - Symbol +// - SymbolLock + +// 2.3. Whitespace Keys + +addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter); +addStandard("Tab", KeyTable.XK_Tab); +addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space); + +// 2.4. Navigation Keys + +addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down); +addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up); +addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left); +addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right); +addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End); +addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home); +addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next); +addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior); + +// 2.5. Editing Keys + +addStandard("Backspace", KeyTable.XK_BackSpace); +addStandard("Clear", KeyTable.XK_Clear); +addStandard("Copy", KeyTable.XF86XK_Copy); +// - CrSel +addStandard("Cut", KeyTable.XF86XK_Cut); +addNumpad("Delete", KeyTable.XK_Delete, KeyTable.XK_KP_Delete); +// - EraseEof +// - ExSel +addNumpad("Insert", KeyTable.XK_Insert, KeyTable.XK_KP_Insert); +addStandard("Paste", KeyTable.XF86XK_Paste); +addStandard("Redo", KeyTable.XK_Redo); +addStandard("Undo", KeyTable.XK_Undo); + +// 2.6. UI Keys + +// - Accept +// - Again (could just be XK_Redo) +// - Attn +addStandard("Cancel", KeyTable.XK_Cancel); +addStandard("ContextMenu", KeyTable.XK_Menu); +addStandard("Escape", KeyTable.XK_Escape); +addStandard("Execute", KeyTable.XK_Execute); +addStandard("Find", KeyTable.XK_Find); +addStandard("Help", KeyTable.XK_Help); +addStandard("Pause", KeyTable.XK_Pause); +// - Play +// - Props +addStandard("Select", KeyTable.XK_Select); +addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn); +addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut); + +// 2.7. Device Keys + +addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown); +addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp); +addStandard("Eject", KeyTable.XF86XK_Eject); +addStandard("LogOff", KeyTable.XF86XK_LogOff); +addStandard("Power", KeyTable.XF86XK_PowerOff); +addStandard("PowerOff", KeyTable.XF86XK_PowerDown); +addStandard("PrintScreen", KeyTable.XK_Print); +addStandard("Hibernate", KeyTable.XF86XK_Hibernate); +addStandard("Standby", KeyTable.XF86XK_Standby); +addStandard("WakeUp", KeyTable.XF86XK_WakeUp); + +// 2.8. IME and Composition Keys + +addStandard("AllCandidates", KeyTable.XK_MultipleCandidate); +addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle +addStandard("CodeInput", KeyTable.XK_Codeinput); +addStandard("Compose", KeyTable.XK_Multi_key); +addStandard("Convert", KeyTable.XK_Henkan); +// - Dead +// - FinalMode +addStandard("GroupFirst", KeyTable.XK_ISO_First_Group); +addStandard("GroupLast", KeyTable.XK_ISO_Last_Group); +addStandard("GroupNext", KeyTable.XK_ISO_Next_Group); +addStandard("GroupPrevious", KeyTable.XK_ISO_Prev_Group); +// - ModeChange (XK_Mode_switch is often used for AltGr) +// - NextCandidate +addStandard("NonConvert", KeyTable.XK_Muhenkan); +addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate); +// - Process +addStandard("SingleCandidate", KeyTable.XK_SingleCandidate); +addStandard("HangulMode", KeyTable.XK_Hangul); +addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja); +addStandard("JunjuaMode", KeyTable.XK_Hangul_Jeonja); +addStandard("Eisu", KeyTable.XK_Eisu_toggle); +addStandard("Hankaku", KeyTable.XK_Hankaku); +addStandard("Hiragana", KeyTable.XK_Hiragana); +addStandard("HiraganaKatakana", KeyTable.XK_Hiragana_Katakana); +addStandard("KanaMode", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock +addStandard("KanjiMode", KeyTable.XK_Kanji); +addStandard("Katakana", KeyTable.XK_Katakana); +addStandard("Romaji", KeyTable.XK_Romaji); +addStandard("Zenkaku", KeyTable.XK_Zenkaku); +addStandard("ZenkakuHanaku", KeyTable.XK_Zenkaku_Hankaku); + +// 2.9. General-Purpose Function Keys + +addStandard("F1", KeyTable.XK_F1); +addStandard("F2", KeyTable.XK_F2); +addStandard("F3", KeyTable.XK_F3); +addStandard("F4", KeyTable.XK_F4); +addStandard("F5", KeyTable.XK_F5); +addStandard("F6", KeyTable.XK_F6); +addStandard("F7", KeyTable.XK_F7); +addStandard("F8", KeyTable.XK_F8); +addStandard("F9", KeyTable.XK_F9); +addStandard("F10", KeyTable.XK_F10); +addStandard("F11", KeyTable.XK_F11); +addStandard("F12", KeyTable.XK_F12); +addStandard("F13", KeyTable.XK_F13); +addStandard("F14", KeyTable.XK_F14); +addStandard("F15", KeyTable.XK_F15); +addStandard("F16", KeyTable.XK_F16); +addStandard("F17", KeyTable.XK_F17); +addStandard("F18", KeyTable.XK_F18); +addStandard("F19", KeyTable.XK_F19); +addStandard("F20", KeyTable.XK_F20); +addStandard("F21", KeyTable.XK_F21); +addStandard("F22", KeyTable.XK_F22); +addStandard("F23", KeyTable.XK_F23); +addStandard("F24", KeyTable.XK_F24); +addStandard("F25", KeyTable.XK_F25); +addStandard("F26", KeyTable.XK_F26); +addStandard("F27", KeyTable.XK_F27); +addStandard("F28", KeyTable.XK_F28); +addStandard("F29", KeyTable.XK_F29); +addStandard("F30", KeyTable.XK_F30); +addStandard("F31", KeyTable.XK_F31); +addStandard("F32", KeyTable.XK_F32); +addStandard("F33", KeyTable.XK_F33); +addStandard("F34", KeyTable.XK_F34); +addStandard("F35", KeyTable.XK_F35); +// - Soft1... + +// 2.10. Multimedia Keys + +// - ChannelDown +// - ChannelUp +addStandard("Close", KeyTable.XF86XK_Close); +addStandard("MailForward", KeyTable.XF86XK_MailForward); +addStandard("MailReply", KeyTable.XF86XK_Reply); +addStandard("MainSend", KeyTable.XF86XK_Send); +addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward); +addStandard("MediaPause", KeyTable.XF86XK_AudioPause); +addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay); +addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord); +addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind); +addStandard("MediaStop", KeyTable.XF86XK_AudioStop); +addStandard("MediaTrackNext", KeyTable.XF86XK_AudioNext); +addStandard("MediaTrackPrevious", KeyTable.XF86XK_AudioPrev); +addStandard("New", KeyTable.XF86XK_New); +addStandard("Open", KeyTable.XF86XK_Open); +addStandard("Print", KeyTable.XK_Print); +addStandard("Save", KeyTable.XF86XK_Save); +addStandard("SpellCheck", KeyTable.XF86XK_Spell); + +// 2.11. Multimedia Numpad Keys + +// - Key11 +// - Key12 + +// 2.12. Audio Keys + +// - AudioBalanceLeft +// - AudioBalanceRight +// - AudioBassDown +// - AudioBassBoostDown +// - AudioBassBoostToggle +// - AudioBassBoostUp +// - AudioBassUp +// - AudioFaderFront +// - AudioFaderRear +// - AudioSurroundModeNext +// - AudioTrebleDown +// - AudioTrebleUp +addStandard("AudioVolumeDown", KeyTable.XF86XK_AudioLowerVolume); +addStandard("AudioVolumeUp", KeyTable.XF86XK_AudioRaiseVolume); +addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute); +// - MicrophoneToggle +// - MicrophoneVolumeDown +// - MicrophoneVolumeUp +addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute); + +// 2.13. Speech Keys + +// - SpeechCorrectionList +// - SpeechInputToggle + +// 2.14. Application Keys + +addStandard("LaunchCalculator", KeyTable.XF86XK_Calculator); +addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar); +addStandard("LaunchMail", KeyTable.XF86XK_Mail); +addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia); +addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music); +addStandard("LaunchMyComputer", KeyTable.XF86XK_MyComputer); +addStandard("LaunchPhone", KeyTable.XF86XK_Phone); +addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver); +addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel); +addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW); +addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam); +addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word); + +// 2.15. Browser Keys + +addStandard("BrowserBack", KeyTable.XF86XK_Back); +addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites); +addStandard("BrowserForward", KeyTable.XF86XK_Forward); +addStandard("BrowserHome", KeyTable.XF86XK_HomePage); +addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh); +addStandard("BrowserSearch", KeyTable.XF86XK_Search); +addStandard("BrowserStop", KeyTable.XF86XK_Stop); + +// 2.16. Mobile Phone Keys + +// - A whole bunch... + +// 2.17. TV Keys + +// - A whole bunch... + +// 2.18. Media Controller Keys + +// - A whole bunch... +addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust); +addStandard("MediaAudioTrack", KeyTable.XF86XK_AudioCycleTrack); +addStandard("RandomToggle", KeyTable.XF86XK_AudioRandomPlay); +addStandard("SplitScreenToggle", KeyTable.XF86XK_SplitScreen); +addStandard("Subtitle", KeyTable.XF86XK_Subtitle); +addStandard("VideoModeNext", KeyTable.XF86XK_Next_VMode); + +// Extra: Numpad + +addNumpad("=", KeyTable.XK_equal, KeyTable.XK_KP_Equal); +addNumpad("+", KeyTable.XK_plus, KeyTable.XK_KP_Add); +addNumpad("-", KeyTable.XK_minus, KeyTable.XK_KP_Subtract); +addNumpad("*", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply); +addNumpad("/", KeyTable.XK_slash, KeyTable.XK_KP_Divide); +addNumpad(".", KeyTable.XK_period, KeyTable.XK_KP_Decimal); +addNumpad(",", KeyTable.XK_comma, KeyTable.XK_KP_Separator); +addNumpad("0", KeyTable.XK_0, KeyTable.XK_KP_0); +addNumpad("1", KeyTable.XK_1, KeyTable.XK_KP_1); +addNumpad("2", KeyTable.XK_2, KeyTable.XK_KP_2); +addNumpad("3", KeyTable.XK_3, KeyTable.XK_KP_3); +addNumpad("4", KeyTable.XK_4, KeyTable.XK_KP_4); +addNumpad("5", KeyTable.XK_5, KeyTable.XK_KP_5); +addNumpad("6", KeyTable.XK_6, KeyTable.XK_KP_6); +addNumpad("7", KeyTable.XK_7, KeyTable.XK_KP_7); +addNumpad("8", KeyTable.XK_8, KeyTable.XK_KP_8); +addNumpad("9", KeyTable.XK_9, KeyTable.XK_KP_9); + +export default DOMKeyTable; diff --git a/core/input/fixedkeys.js b/core/input/fixedkeys.js index 2a2594e..6dd4222 100644 --- a/core/input/fixedkeys.js +++ b/core/input/fixedkeys.js @@ -5,108 +5,123 @@ */ /* - * 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. + * Fallback mapping between HTML key codes (physical keys) and + * HTML key values. This only works for keys that don't vary + * between layouts. We also omit those who manage fine by mapping the + * Unicode representation. + * + * See https://www.w3.org/TR/uievents-code/ for possible codes. + * See https://www.w3.org/TR/uievents-key/ for possible values. */ -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, + +// 3.1.1.1. Writing System Keys + + 'Backspace': 'Backspace', + +// 3.1.1.2. Functional Keys + + 'AltLeft': 'Alt', + 'AltRight': 'Alt', // This could also be 'AltGraph' + 'CapsLock': 'CapsLock', + 'ContextMenu': 'ContextMenu', + 'ControlLeft': 'Control', + 'ControlRight': 'Control', + 'Enter': 'Enter', + 'MetaLeft': 'Meta', + 'MetaRight': 'Meta', + 'ShiftLeft': 'Shift', + 'ShiftRight': 'Shift', + 'Tab': '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, + +// 3.1.2. Control Pad Section + + 'Delete': 'Delete', + 'End': 'End', + 'Help': 'Help', + 'Home': 'Home', + 'Insert': 'Insert', + 'PageDown': 'PageDown', + 'PageUp': 'PageUp', + +// 3.1.3. Arrow Pad Section + + 'ArrowDown': 'ArrowDown', + 'ArrowLeft': 'ArrowLeft', + 'ArrowRight': 'ArrowRight', + 'ArrowUp': 'ArrowUp', + +// 3.1.4. Numpad Section + + 'NumLock': 'NumLock', + 'NumpadBackspace': 'Backspace', + 'NumpadClear': 'Clear', + +// 3.1.5. Function Section + + 'Escape': 'Escape', + 'F1': 'F1', + 'F2': 'F2', + 'F3': 'F3', + 'F4': 'F4', + 'F5': 'F5', + 'F6': 'F6', + 'F7': 'F7', + 'F8': 'F8', + 'F9': 'F9', + 'F10': 'F10', + 'F11': 'F11', + 'F12': 'F12', + 'F13': 'F13', + 'F14': 'F14', + 'F15': 'F15', + 'F16': 'F16', + 'F17': 'F17', + 'F18': 'F18', + 'F19': 'F19', + 'F20': 'F20', + 'F21': 'F21', + 'F22': 'F22', + 'F23': 'F23', + 'F24': 'F24', + 'F25': 'F25', + 'F26': 'F26', + 'F27': 'F27', + 'F28': 'F28', + 'F29': 'F29', + 'F30': 'F30', + 'F31': 'F31', + 'F32': 'F32', + 'F33': 'F33', + 'F34': 'F34', + 'F35': 'F35', + 'PrintScreen': 'PrintScreen', + 'ScrollLock': 'ScrollLock', + 'Pause': 'Pause', + +// 3.1.6. Media Keys + + 'BrowserBack': 'BrowserBack', + 'BrowserFavorites': 'BrowserFavorites', + 'BrowserForward': 'BrowserForward', + 'BrowserHome': 'BrowserHome', + 'BrowserRefresh': 'BrowserRefresh', + 'BrowserSearch': 'BrowserSearch', + 'BrowserStop': 'BrowserStop', + 'Eject': 'Eject', + 'LaunchApp1': 'LaunchMyComputer', + 'LaunchApp2': 'LaunchCalendar', + 'LaunchMail': 'LaunchMail', + 'MediaPlayPause': 'MediaPlay', + 'MediaStop': 'MediaStop', + 'MediaTrackNext': 'MediaTrackNext', + 'MediaTrackPrevious': 'MediaTrackPrevious', + 'Power': 'Power', + 'Sleep': 'Sleep', + 'AudioVolumeDown': 'AudioVolumeDown', + 'AudioVolumeMute': 'AudioVolumeMute', + 'AudioVolumeUp': 'AudioVolumeUp', + 'WakeUp': 'WakeUp', }; diff --git a/core/input/keysym.js b/core/input/keysym.js index 06ea70f..ba58be6 100644 --- a/core/input/keysym.js +++ b/core/input/keysym.js @@ -12,6 +12,37 @@ export default { XK_Escape: 0xff1b, XK_Delete: 0xffff, /* Delete, rubout */ + /* International & multi-key character composition */ + + XK_Multi_key: 0xff20, /* Multi-key character compose */ + XK_Codeinput: 0xff37, + XK_SingleCandidate: 0xff3c, + XK_MultipleCandidate: 0xff3d, + XK_PreviousCandidate: 0xff3e, + + /* Japanese keyboard support */ + + XK_Kanji: 0xff21, /* Kanji, Kanji convert */ + XK_Muhenkan: 0xff22, /* Cancel Conversion */ + XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */ + XK_Henkan: 0xff23, /* Alias for Henkan_Mode */ + XK_Romaji: 0xff24, /* to Romaji */ + XK_Hiragana: 0xff25, /* to Hiragana */ + XK_Katakana: 0xff26, /* to Katakana */ + XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */ + XK_Zenkaku: 0xff28, /* to Zenkaku */ + XK_Hankaku: 0xff29, /* to Hankaku */ + XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */ + XK_Touroku: 0xff2b, /* Add to Dictionary */ + XK_Massyo: 0xff2c, /* Delete from Dictionary */ + XK_Kana_Lock: 0xff2d, /* Kana Lock */ + XK_Kana_Shift: 0xff2e, /* Kana Shift */ + XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */ + XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */ + XK_Kanji_Bangou: 0xff37, /* Codeinput */ + XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */ + XK_Mae_Koho: 0xff3e, /* Previous Candidate */ + /* Cursor control & motion */ XK_Home: 0xff50, @@ -171,7 +202,17 @@ export default { XK_Hyper_L: 0xffed, /* Left hyper */ XK_Hyper_R: 0xffee, /* Right hyper */ + /* + * Keyboard (XKB) Extension function and modifier keys + * (from Appendix C of "The X Keyboard Extension: Protocol Specification") + * Byte 3 = 0xfe + */ + XK_ISO_Level3_Shift: 0xfe03, /* AltGr */ + XK_ISO_Next_Group: 0xfe08, + XK_ISO_Prev_Group: 0xfe0a, + XK_ISO_First_Group: 0xfe0c, + XK_ISO_Last_Group: 0xfe0e, /* * Latin 1 @@ -378,6 +419,15 @@ export default { XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ + /* + * Korean + * Byte 3 = 0x0e + */ + + XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */ + XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */ + XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */ + /* * XFree86 vendor specific keysyms. * diff --git a/core/input/util.js b/core/input/util.js index d755c20..4335b0b 100644 --- a/core/input/util.js +++ b/core/input/util.js @@ -2,10 +2,17 @@ import KeyTable from "./keysym.js"; import keysyms from "./keysymdef.js"; import vkeys from "./vkeys.js"; import fixedkeys from "./fixedkeys.js"; +import DOMKeyTable from "./domkeytable.js"; function isMac() { return navigator && !!(/mac/i).exec(navigator.platform); } +function isIE() { + return navigator && !!(/trident/i).exec(navigator.userAgent); +} +function isEdge() { + return navigator && !!(/edge/i).exec(navigator.userAgent); +} // Get 'KeyboardEvent.code', handling legacy browsers export function getKeycode(evt){ @@ -67,88 +74,91 @@ export function getKeycode(evt){ return 'Unidentified'; } -// Get the most reliable keysym value we can get from a key event -export function getKeysym(evt){ +// Get 'KeyboardEvent.key', handling legacy browsers +export function getKey(evt) { + // Are we getting a proper key value? + if (evt.key !== undefined) { + // IE and Edge use some ancient version of the spec + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/ + switch (evt.key) { + case 'Spacebar': return ' '; + case 'Esc': return 'Escape'; + case 'Scroll': return 'ScrollLock'; + case 'Win': return 'Meta'; + case 'Apps': return 'ContextMenu'; + case 'Up': return 'ArrowUp'; + case 'Left': return 'ArrowLeft'; + case 'Right': return 'ArrowRight'; + case 'Down': return 'ArrowDown'; + case 'Del': return 'Delete'; + case 'Divide': return '/'; + case 'Multiply': return '*'; + case 'Subtract': return '-'; + case 'Add': return '+'; + case 'Decimal': return evt.char; + } - // We start with layout independent keys + // Mozilla isn't fully in sync with the spec yet + switch (evt.key) { + case 'OS': return 'Meta'; + } + + // IE and Edge have broken handling of AltGraph so we cannot + // trust them for printable characters + if ((evt.key.length !== 1) || (!isIE() && !isEdge())) { + return evt.key; + } + } + + // Try to deduce it based on the physical key 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; - } + // If that failed, then see if we have a printable character + if (evt.charCode) { + return String.fromCharCode(evt.charCode); } - // Or the numpad - if (evt.location === 3) { - var key = evt.key; + // At this point we have nothing left to go on + return 'Unidentified'; +} - // 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; +// Get the most reliable keysym value we can get from a key event +export function getKeysym(evt){ + var key = getKey(evt); + + if (key === 'Unidentified') { + return null; + } + + // First look up special keys + if (key in DOMKeyTable) { + var location = evt.location; + + // Safari screws up location for the right cmd key + if ((key === 'Meta') && (location === 0)) { + location = 2; } - // Safari doesn't support KeyboardEvent.key yet - if ((key === undefined) && (evt.charCode)) { - key = String.fromCharCode(evt.charCode); + if ((location === undefined) || (location > 3)) { + location = 0; } - 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; - } + return DOMKeyTable[key][location]; } // Now we need to look at the Unicode symbol instead var codepoint; - if ('key' in evt) { - // 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; + // Special key? (FIXME: Should have been caught earlier) + if (key.length !== 1) { + return null; } + codepoint = key.charCodeAt(); if (codepoint) { return keysyms.lookup(codepoint); } diff --git a/tests/test.helper.js b/tests/test.helper.js index f705cff..e0ae80a 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -98,39 +98,104 @@ describe('Helpers', function() { }); }); - describe('getKeysym', function() { + describe('getKey', function() { it('should prefer key', function() { - expect(KeyboardUtil.getKeysym({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x61); + expect(KeyboardUtil.getKey({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('a'); + }); + it('should map legacy values', function() { + expect(KeyboardUtil.getKey({key: 'Spacebar'})).to.be.equal(' '); + expect(KeyboardUtil.getKey({key: 'Left'})).to.be.equal('ArrowLeft'); + expect(KeyboardUtil.getKey({key: 'OS'})).to.be.equal('Meta'); + expect(KeyboardUtil.getKey({key: 'Win'})).to.be.equal('Meta'); + }); + it('should use code if no key', function() { + expect(KeyboardUtil.getKey({code: 'NumpadBackspace'})).to.be.equal('Backspace'); + }); + it('should not use code fallback for character keys', function() { + expect(KeyboardUtil.getKey({code: 'KeyA'})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({code: 'Digit1'})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({code: 'Period'})).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({code: 'Numpad1'})).to.be.equal('Unidentified'); }); it('should use charCode if no key', function() { - expect(KeyboardUtil.getKeysym({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal(0x01a9); + expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('Š'); + }); + it('should return Unidentified when it cannot map the key', function() { + expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified'); }); + describe('Broken key AltGraph on IE/Edge', function() { + var origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, "navigator", {value: {}}); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + }); + afterEach(function () { + Object.defineProperty(window, "navigator", origNavigator); + }); + + it('should ignore printable character key on IE', function() { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"; + expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified'); + }); + it('should ignore printable character key on Edge', function() { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; + expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified'); + }); + it('should allow non-printable character key on IE', function() { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"; + expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift'); + }); + it('should allow non-printable character key on Edge', function() { + window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; + expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift'); + }); + }); + }); + + describe('getKeysym', function() { describe('Non-character keys', function() { it('should recognize the right keys', function() { - 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); + expect(KeyboardUtil.getKeysym({key: 'Enter'})).to.be.equal(0xFF0D); + expect(KeyboardUtil.getKeysym({key: 'Backspace'})).to.be.equal(0xFF08); + expect(KeyboardUtil.getKeysym({key: 'Tab'})).to.be.equal(0xFF09); + expect(KeyboardUtil.getKeysym({key: 'Shift'})).to.be.equal(0xFFE1); + expect(KeyboardUtil.getKeysym({key: 'Control'})).to.be.equal(0xFFE3); + expect(KeyboardUtil.getKeysym({key: 'Alt'})).to.be.equal(0xFFE9); + expect(KeyboardUtil.getKeysym({key: 'Meta'})).to.be.equal(0xFFEB); + expect(KeyboardUtil.getKeysym({key: 'Escape'})).to.be.equal(0xFF1B); + expect(KeyboardUtil.getKeysym({key: 'ArrowUp'})).to.be.equal(0xFF52); + }); + it('should map left/right side', function() { + expect(KeyboardUtil.getKeysym({key: 'Shift', location: 1})).to.be.equal(0xFFE1); + expect(KeyboardUtil.getKeysym({key: 'Shift', location: 2})).to.be.equal(0xFFE2); + expect(KeyboardUtil.getKeysym({key: 'Control', location: 1})).to.be.equal(0xFFE3); + expect(KeyboardUtil.getKeysym({key: 'Control', location: 2})).to.be.equal(0xFFE4); }); 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); + expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Alt', location: 2})).to.be.equal(0xFFEA); + expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph', location: 2})).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 return null for unknown keys', function() { + expect(KeyboardUtil.getKeysym({key: 'Semicolon'})).to.be.null; + expect(KeyboardUtil.getKeysym({key: 'BracketRight'})).to.be.null; }); - it('should not recognize character keys', function() { - 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; + it('should handle remappings', function() { + expect(KeyboardUtil.getKeysym({code: 'ControlLeft', key: 'Tab'})).to.be.equal(0xFF09); }); }); @@ -145,10 +210,6 @@ describe('Helpers', function() { 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); diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index 1ebbbd4..a4ac630 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -187,8 +187,8 @@ describe('Key Event Handling', function() { break; } }}); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); expect(count).to.be.equal(2); }); it('should change left Super to Alt', function(done) { @@ -198,7 +198,7 @@ describe('Key Event Handling', function() { expect(code).to.be.equal('MetaLeft'); done(); }}); - kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta', location: 1})); }); it('should change right Super to left Super', function(done) { var kbd = new Keyboard({ @@ -207,7 +207,7 @@ describe('Key Event Handling', function() { expect(code).to.be.equal('MetaRight'); done(); }}); - kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2})); }); }); @@ -280,8 +280,8 @@ describe('Key Event Handling', function() { } }}); // First the modifier combo - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); // Next a normal character kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); expect(times_called).to.be.equal(7); @@ -299,8 +299,8 @@ describe('Key Event Handling', function() { } }}); // First the modifier combo - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); // Next a normal character kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); @@ -329,10 +329,10 @@ describe('Key Event Handling', function() { } }}); // First the modifier combo - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); + kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1})); // Then one of the keys again - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control'})); + kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); expect(times_called).to.be.equal(3); }); });