// Establishes key-code definitions. /// // Defines our generalized "KeyEvent" class. /// // Defines the RuleBehavior keyboard-processing return object. /// // Defines default key handling behaviors. /// // Defines the keyboard wrapper object. /// // Defines built-in keymapping. /// // Defines a core-compatible 'Device' analogue for use in keyEvent processing /// // Defines the getGlobalObject() utility method. /// namespace com.keyman.text { export type BeepHandler = (outputTarget: OutputTarget) => void; export type LogMessageHandler = (str: string) => void; export interface VariableStoreSerializer { loadStore(keyboardID: string, storeName: string): VariableStore; saveStore(keyboardID: string, storeName: string, storeMap: VariableStore); } export interface ProcessorInitOptions { baseLayout?: string; variableStoreSerializer?: VariableStoreSerializer; } export class KeyboardProcessor { public static readonly DEFAULT_OPTIONS: ProcessorInitOptions = { baseLayout: 'us' } // Tracks the simulated value for supported state keys, allowing the OSK to mirror a physical keyboard for them. // Using the exact keyCode name from the Codes definitions will allow for certain optimizations elsewhere in the code. stateKeys = { "K_CAPS":false, "K_NUMLOCK":false, "K_SCROLL":false }; // Tracks the most recent modifier state information in order to quickly detect changes // in keyboard state not otherwise captured by the hosting page in the browser. // Needed for AltGr simulation. modStateFlags: number = 0; keyboardInterface: KeyboardInterface; baseLayout: string; // Callbacks for various feedback types beepHandler?: BeepHandler; warningLogger?: LogMessageHandler; errorLogger?: LogMessageHandler; constructor(options?: ProcessorInitOptions) { if(!options) { options = KeyboardProcessor.DEFAULT_OPTIONS; } this.baseLayout = options.baseLayout || 'us'; // default BaseLayout this.keyboardInterface = new KeyboardInterface(options.variableStoreSerializer); this.installInterface(); } private installInterface() { // TODO: replace 'window' with a (currently-unwritten) utility call that retrieves // the global object (whether browser, Node, WebWorker). // // We must ensure that the keyboard can find the API functions at the expected place. let globalThis = utils.getGlobalObject(); globalThis[KeyboardInterface.GLOBAL_NAME] = this.keyboardInterface; // Ensure that the active keyboard is set on the keyboard interface object. if(this.activeKeyboard) { this.keyboardInterface.activeKeyboard = this.activeKeyboard; } } public get activeKeyboard(): keyboards.Keyboard { return this.keyboardInterface.activeKeyboard; } public set activeKeyboard(keyboard: keyboards.Keyboard) { this.keyboardInterface.activeKeyboard = keyboard; // All old deadkeys and keyboard-specific cache should immediately be invalidated // on a keyboard change. this.resetContext(); } get layerStore(): MutableSystemStore { return this.keyboardInterface.systemStores[KeyboardInterface.TSS_LAYER] as MutableSystemStore; } public get layerId(): string { return this.layerStore.value; } // Note: will trigger an 'event' callback designed to notify the OSK of layer changes. public set layerId(value: string) { this.layerStore.set(value); } /** * Get the default RuleBehavior for the specified key, attempting to mimic standard browser defaults * where and when appropriate. * * @param {object} Lkc The pre-analyzed key event object * @param {boolean} usingOSK * @return {string} */ defaultRuleBehavior(Lkc: KeyEvent): RuleBehavior { let outputTarget = Lkc.Ltarg; let preInput = Mock.from(outputTarget); let ruleBehavior = new RuleBehavior(); let matched = false; var char = ''; var special: EmulationKeystrokes; if(Lkc.isSynthetic || outputTarget.isSynthetic) { matched = true; // All the conditions below result in matches until the final else, which restores the expected default // if no match occurs. if(DefaultOutput.isCommand(Lkc)) { // Note this in the rule behavior, return successfully. We'll consider applying it later. ruleBehavior.triggersDefaultCommand = true; // We'd rather let the browser handle these keys, but we're using emulated keystrokes, forcing KMW // to emulate default behavior here. } else if((special = DefaultOutput.forSpecialEmulation(Lkc)) != null) { switch(special) { case EmulationKeystrokes.Backspace: this.keyboardInterface.defaultBackspace(outputTarget); break; case EmulationKeystrokes.Enter: outputTarget.handleNewlineAtCaret(); break; case EmulationKeystrokes.Space: this.keyboardInterface.output(0, outputTarget, ' '); break; // case '\u007f': // K_DEL // // For (possible) future implementation. // // Would recommend (conceptually) equaling K_RIGHT + K_BKSP, the former of which would technically be a 'command'. default: // In case we extend the allowed set, but forget to implement its handling case above. ruleBehavior.errorLog = "Unexpected 'special emulation' character (\\u" + (special as String).kmwCharCodeAt(0).toString(16) + ") went unhandled!"; } } else { // Back to the standard default, pending normal matching. matched = false; } } let isMnemonic = this.activeKeyboard && this.activeKeyboard.isMnemonic; if(!matched) { if((char = DefaultOutput.forAny(Lkc, isMnemonic)) != null) { special = DefaultOutput.forSpecialEmulation(Lkc) if(special == EmulationKeystrokes.Backspace) { // A browser's default backspace may fail to delete both parts of an SMP character. this.keyboardInterface.defaultBackspace(Lkc.Ltarg); } else if(special || DefaultOutput.isCommand(Lkc)) { // Filters out 'commands' like TAB. // We only do the "for special emulation" cases under the condition above... aside from backspace // Let the browser handle those. return null; } else { this.keyboardInterface.output(0, outputTarget, char); } } else { // No match, no default RuleBehavior. return null; } } // Shortcut things immediately if there were issues generating this rule behavior. if(ruleBehavior.errorLog) { return ruleBehavior; } let transcription = outputTarget.buildTranscriptionFrom(preInput, Lkc); ruleBehavior.transcription = transcription; return ruleBehavior; } setSyntheticEventDefaults(Lkc: text.KeyEvent) { // Set the flags for the state keys. Lkc.Lstates |= this.stateKeys['K_CAPS'] ? Codes.modifierCodes['CAPS'] : Codes.modifierCodes['NO_CAPS']; Lkc.Lstates |= this.stateKeys['K_NUMLOCK'] ? Codes.modifierCodes['NUM_LOCK'] : Codes.modifierCodes['NO_NUM_LOCK']; Lkc.Lstates |= this.stateKeys['K_SCROLL'] ? Codes.modifierCodes['SCROLL_LOCK'] : Codes.modifierCodes['NO_SCROLL_LOCK']; // Set LisVirtualKey to false to ensure that nomatch rule does fire for U_xxxx keys if(Lkc.kName.substr(0,2) == 'U_') { Lkc.LisVirtualKey=false; } // Get code for non-physical keys (T_KOKAI, U_05AB etc) if(typeof Lkc.Lcode == 'undefined') { Lkc.Lcode = this.getVKDictionaryCode(Lkc.kName);// Updated for Build 347 if(!Lkc.Lcode) { // Special case for U_xxxx keys. This vk code will never be used // in a keyboard, so we use this to ensure that keystroke processing // occurs for the key. Lkc.Lcode = 1; } } // Handles modifier states when the OSK is emulating rightalt through the leftctrl-leftalt layer. if((Lkc.Lmodifiers & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM'] && this.activeKeyboard.emulatesAltGr) { Lkc.Lmodifiers &= ~Codes.modifierBitmasks['ALT_GR_SIM']; Lkc.Lmodifiers |= Codes.modifierCodes['RALT']; } } processKeystroke(keyEvent: KeyEvent, outputTarget: OutputTarget): RuleBehavior { var matchBehavior: RuleBehavior; // Pass this key code and state to the keyboard program if(this.activeKeyboard && keyEvent.Lcode != 0) { /* * The `this.installInterface()` call is insurance against something I've seen in unit tests when things break a bit. * * Currently, when a KMW shutdown doesn't go through properly or completely, sometimes we end up with parallel * versions of KMW running, and an old, partially-shutdown one will "snipe" a command meant for the most-recent * one's test. So, installing here ensures that the active Processor has its matching KeyboardInterface ready, * even should that occur. */ this.installInterface(); matchBehavior = this.keyboardInterface.processKeystroke(outputTarget, keyEvent); } if(!matchBehavior) { // Restore the virtual key code if a mnemonic keyboard is being used // If no vkCode value was stored, maintain the original Lcode value. keyEvent.Lcode=keyEvent.vkCode || keyEvent.Lcode; // Handle unmapped keys, including special keys // The following is physical layout dependent, so should be avoided if possible. All keys should be mapped. this.keyboardInterface.activeTargetOutput = outputTarget; // Match against the 'default keyboard' - rules to mimic the default string output when typing in a browser. // Many keyboards rely upon these 'implied rules'. matchBehavior = this.defaultRuleBehavior(keyEvent); this.keyboardInterface.activeTargetOutput = null; } return matchBehavior; } // FIXME: makes some bad assumptions. static setMnemonicCode(Lkc: KeyEvent, shifted: boolean, capsActive: boolean) { // K_SPACE is not handled by defaultKeyOutput for physical keystrokes unless using touch-aliased elements. // It's also a "exception required, March 2013" for clickKey, so at least they both have this requirement. if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { // So long as the key name isn't prefixed with 'U_', we'll get a default mapping based on the Lcode value. // We need to determine the mnemonic base character - for example, SHIFT + K_PERIOD needs to map to '>'. let mappingEvent: KeyEvent = new KeyEvent(); for(var key in Lkc) { mappingEvent[key] = Lkc[key]; } // To facilitate storing relevant commands, we should probably reverse-lookup // the actual keyname instead. mappingEvent.kName = 'K_xxxx'; mappingEvent.Ltarg = new Mock(); // helps prevent breakage for mnemonics. mappingEvent.Lmodifiers = (shifted ? 0x10 : 0); // mnemonic lookups only exist for default & shift layers. var mappedChar: string = DefaultOutput.forAny(mappingEvent, true); /* First, save a backup of the original code. This one won't needlessly trigger keyboard * rules, but allows us to replicate/emulate commands after rule processing if needed. * (Like backspaces) */ Lkc.vkCode = Lkc.Lcode; if(mappedChar) { // Will return 96 for 'a', which is a keycode corresponding to Codes.keyCodes('K_NP1') - a numpad key. // That stated, we're in mnemonic mode - this keyboard's rules are based on the char codes. Lkc.Lcode = mappedChar.charCodeAt(0); } else { // Don't let command-type keys (like K_DEL, which will output '.' otherwise!) // trigger keyboard rules. delete Lkc.Lcode; } } if(capsActive) { // TODO: Needs fixing - does not properly mirror physical keystrokes, as Lcode range 96-111 corresponds // to numpad keys! (Physical keyboard section has its own issues here.) if((Lkc.Lcode >= 65 && Lkc.Lcode <= 90) /* 'A' - 'Z' */ || (Lkc.Lcode >= 97 && Lkc.Lcode <= 122) /* 'a' - 'z' */) { Lkc.Lmodifiers ^= 0x10; // Flip the 'shifted' bit, so it'll act as the opposite key. Lkc.Lcode ^= 0x20; // Flips the 'upper' vs 'lower' bit for the base 'a'-'z' ASCII alphabetics. } } } /** * Get modifier key state from layer id * * @param {string} layerId layer id (e.g. ctrlshift) * @return {number} modifier key state (desktop keyboards) */ static getModifierState(layerId: string): number { var modifier=0; if(layerId.indexOf('shift') >= 0) { modifier |= Codes.modifierCodes['SHIFT']; } // The chiral checks must not be directly exclusive due each other to visual OSK feedback. var ctrlMatched=false; if(layerId.indexOf('leftctrl') >= 0) { modifier |= Codes.modifierCodes['LCTRL']; ctrlMatched=true; } if(layerId.indexOf('rightctrl') >= 0) { modifier |= Codes.modifierCodes['RCTRL']; ctrlMatched=true; } if(layerId.indexOf('ctrl') >= 0 && !ctrlMatched) { modifier |= Codes.modifierCodes['CTRL']; } var altMatched=false; if(layerId.indexOf('leftalt') >= 0) { modifier |= Codes.modifierCodes['LALT']; altMatched=true; } if(layerId.indexOf('rightalt') >= 0) { modifier |= Codes.modifierCodes['RALT']; altMatched=true; } if(layerId.indexOf('alt') >= 0 && !altMatched) { modifier |= Codes.modifierCodes['ALT']; } return modifier; } /** * @summary Look up a custom virtual key code in the virtual key code dictionary KVKD. On first run, will build the dictionary. * * `VKDictionary` is constructed from the keyboard's `KVKD` member. This list is constructed * at compile-time and is a list of 'additional' virtual key codes, starting at 256 (i.e. * outside the range of standard virtual key codes). These additional codes are both * `[T_xxx]` and `[U_xxxx]` custom key codes from the Keyman keyboard language. However, * `[U_xxxx]` keys only generate an entry in `KVKD` if there is a corresponding rule that * is associated with them in the keyboard rules. If the `[U_xxxx]` key code is only * referenced as the id of a key in the touch layout, then it does not get an entry in * the `KVKD` property. * * @private * @param {string} keyName custom virtual key code to lookup in the dictionary * @return {number} key code > 255 on success, or 0 if not found */ getVKDictionaryCode(keyName: string) { var activeKeyboard = this.activeKeyboard; if(!activeKeyboard.scriptObject['VKDictionary']) { var a=[]; if(typeof activeKeyboard.scriptObject['KVKD'] == 'string') { // Build the VK dictionary // TODO: Move the dictionary build into the compiler -- so compiler generates code such as following. // Makes the VKDictionary member unnecessary. // this.KVKD={"K_ABC":256,"K_DEF":257,...}; var s=activeKeyboard.scriptObject['KVKD'].split(' '); for(var i=0; i