Skip to content

Commit 283f43f

Browse files
committed
feat(web): implement doModifierPress
Fixes: #15287 Test-bot: skip
1 parent 5b3ea22 commit 283f43f

File tree

3 files changed

+211
-9
lines changed

3 files changed

+211
-9
lines changed

web/src/engine/src/core-processor/coreKeyboardProcessor.ts

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ import {
99
DeviceSpec, EventMap, Keyboard, KeyboardMinimalInterface, KeyboardProcessor,
1010
KeyEvent, KMXKeyboard, SyntheticTextStore, MutableSystemStore, TextStore, ProcessorAction,
1111
StateKeyMap,
12-
Deadkey
12+
Deadkey,
13+
Codes
1314
} from "keyman/engine/keyboard";
1415
import { KM_CORE_EVENT_FLAG } from '../core-adapter/KM_Core.js';
16+
import { ModifierKeyConstants } from '@keymanapp/common-types';
1517

1618
export class CoreKeyboardInterface implements KeyboardMinimalInterface {
1719
public activeKeyboard: Keyboard;
@@ -150,6 +152,12 @@ export class CoreKeyboardProcessor extends EventEmitter<EventMap> implements Key
150152
this._layerStore.set(value);
151153
}
152154

155+
private getLayerId(modifier: number): string {
156+
// TODO-web-core: implement
157+
// return Layouts.getLayerId(modifier);
158+
return 'default'; // TODO-web-core: put into LayerNames enum
159+
}
160+
153161
/**
154162
* Retrieve context including deadkeys from TextStore and apply to Core's context
155163
*
@@ -302,6 +310,7 @@ export class CoreKeyboardProcessor extends EventEmitter<EventMap> implements Key
302310
return null;
303311
}
304312

313+
// TODO-web-core: this could be shared with JsKeyboardProcessor
305314
/**
306315
* Determines if the given key event is a modifier key press.
307316
* Returns true if the event corresponds to a modifier key, otherwise false.
@@ -313,10 +322,113 @@ export class CoreKeyboardProcessor extends EventEmitter<EventMap> implements Key
313322
* @returns {boolean} True if the event is a modifier key press, false otherwise.
314323
*/
315324
public doModifierPress(keyEvent: KeyEvent, textStore: TextStore, isKeyDown: boolean): boolean {
316-
// TODO-web-core: Implement this method (#15287)
325+
if(!this.activeKeyboard) {
326+
return false;
327+
}
328+
329+
if(keyEvent.isModifier) {
330+
this.activeKeyboard.notify(keyEvent.Lcode, textStore, isKeyDown);
331+
// For eventual integration - we bypass an OSK update for physical keystrokes when in touch mode.
332+
if(!keyEvent.device.touchable) {
333+
return this._UpdateVKShift(keyEvent); // I2187
334+
} else {
335+
return true;
336+
}
337+
}
338+
339+
if(keyEvent.LmodifierChange) {
340+
this.activeKeyboard.notify(0, textStore, true);
341+
if(!keyEvent.device.touchable) {
342+
this._UpdateVKShift(keyEvent);
343+
}
344+
}
345+
346+
// No modifier keypresses detected.
317347
return false;
318348
}
319349

350+
351+
// TODO-web-core: this could be shared with JsKeyboardProcessor
352+
/**
353+
* Updates the virtual keyboard shift state based on the provided key event.
354+
* Handles modifier key simulation, state key updates, and layer selection for the OSK.
355+
*
356+
* @param {KeyEvent} e - The key event used to update the shift state.
357+
*
358+
* @returns {boolean} True if the update was processed, otherwise true if no active keyboard.
359+
*/
360+
private _UpdateVKShift(e: KeyEvent): boolean {
361+
let keyShiftState=0;
362+
363+
const lockNames = ['CAPS', 'NUM_LOCK', 'SCROLL_LOCK'] as const;
364+
const lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'] as const;
365+
const lockModifiers = [ModifierKeyConstants.CAPITALFLAG, ModifierKeyConstants.NUMLOCKFLAG, ModifierKeyConstants.SCROLLFLAG] as const;
366+
367+
if(!this.activeKeyboard) {
368+
return true;
369+
}
370+
371+
if(e) {
372+
// read shift states from event
373+
keyShiftState = e.Lmodifiers;
374+
375+
// Are we simulating AltGr? If it's a simulation and not real, time to un-simulate for the OSK.
376+
if(this.activeKeyboard.isChiral && this.activeKeyboard.emulatesAltGr &&
377+
(this.modStateFlags & Codes.modifierBitmasks['ALT_GR_SIM']) == Codes.modifierBitmasks['ALT_GR_SIM']) {
378+
keyShiftState |= Codes.modifierBitmasks['ALT_GR_SIM'];
379+
keyShiftState &= ~ModifierKeyConstants.RALTFLAG;
380+
}
381+
382+
// Set stateKeys where corresponding value is passed in e.Lstates
383+
let stateMutation = false;
384+
for(let i=0; i < lockNames.length; i++) {
385+
if((e.Lstates & Codes.stateBitmasks[lockNames[i]]) != 0) {
386+
this.stateKeys[lockKeys[i]] = ((e.Lstates & lockModifiers[i]) != 0);
387+
stateMutation = true;
388+
}
389+
}
390+
391+
if(stateMutation) {
392+
this.emit('statekeychange', this.stateKeys);
393+
}
394+
}
395+
396+
this.updateStates();
397+
398+
if (this.activeKeyboard.isMnemonic && this.stateKeys['K_CAPS'] && (!e || !e.isModifier)) {
399+
// Modifier keypresses don't trigger mnemonic manipulation of modifier state.
400+
// Only an output key does; active use of Caps will also flip the SHIFT flag.
401+
// Mnemonic keystrokes manipulate the SHIFT property based on CAPS state.
402+
// We need to unflip them when tracking the OSK layer.
403+
keyShiftState ^= ModifierKeyConstants.K_SHIFTFLAG;
404+
}
405+
406+
this.layerId = this.getLayerId(keyShiftState);
407+
return true;
408+
}
409+
410+
// TODO-web-core: this could be shared with JsKeyboardProcessor
411+
private updateStates(): void {
412+
const lockKeys = ['K_CAPS', 'K_NUMLOCK', 'K_SCROLL'] as const;
413+
const lockModifiers = [ModifierKeyConstants.CAPITALFLAG, ModifierKeyConstants.NUMLOCKFLAG, ModifierKeyConstants.SCROLLFLAG] as const;
414+
const noLockModifers = [ModifierKeyConstants.NOTCAPITALFLAG, ModifierKeyConstants.NOTNUMLOCKFLAG, ModifierKeyConstants.NOTSCROLLFLAG] as const;
415+
416+
for (let i = 0; i < lockKeys.length; i++) {
417+
const key = lockKeys[i];
418+
const flag = this.stateKeys[key];
419+
420+
// Ensures that the current mod-state info properly matches the currently-simulated
421+
// state key states.
422+
if (flag) {
423+
this.modStateFlags |= lockModifiers[i];
424+
this.modStateFlags &= ~noLockModifers[i];
425+
} else {
426+
this.modStateFlags &= ~lockModifiers[i];
427+
this.modStateFlags |= noLockModifers[i];
428+
}
429+
}
430+
}
431+
320432
/**
321433
* Resets the keyboard context, optionally using the provided text store.
322434
* Clears or reinitializes the context for subsequent keyboard processing.

web/src/engine/src/keyboard/keyEvent.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,19 @@ export class KeyEvent implements KeyEventSpec {
135135

136136
get isModifier(): boolean {
137137
switch(this.Lcode) {
138-
case 16: //"K_SHIFT":16,"K_CONTROL":17,"K_ALT":18
139-
case 17:
140-
case 18:
141-
case 20: //"K_CAPS":20, "K_NUMLOCK":144,"K_SCROLL":145
142-
case 144:
143-
case 145:
138+
case Codes.keyCodes.K_SHIFT:
139+
case Codes.keyCodes.K_CONTROL:
140+
case Codes.keyCodes.K_ALT:
141+
case Codes.keyCodes.K_CAPS:
142+
case Codes.keyCodes.K_NUMLOCK:
143+
case Codes.keyCodes.K_SCROLL:
144+
case Codes.keyCodes.K_LSHIFT:
145+
case Codes.keyCodes.K_RSHIFT:
146+
case Codes.keyCodes.K_LCTRL:
147+
case Codes.keyCodes.K_RCTRL:
148+
case Codes.keyCodes.K_LALT:
149+
case Codes.keyCodes.K_RALT:
150+
case Codes.keyCodes.K_ALTGR:
144151
return true;
145152
default:
146153
return false;

web/src/test/auto/headless/engine/core-processor/coreKeyboardProcessor.tests.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { assert } from 'chai';
66
import sinon from 'sinon';
77
import { KM_Core, km_core_context, km_core_keyboard, km_core_state, KM_CORE_CT, KM_CORE_STATUS, km_core_context_items } from 'keyman/engine/core-adapter';
88
import { coreurl, loadKeyboardBlob } from '../loadKeyboardHelper.js';
9-
import { Codes, Deadkey, KeyEvent, KMXKeyboard, SyntheticTextStore } from 'keyman/engine/keyboard';
9+
import { Codes, Deadkey, DeviceSpec, KeyEvent, KMXKeyboard, SyntheticTextStore } from 'keyman/engine/keyboard';
1010
import { CoreKeyboardProcessor } from 'keyman/engine/core-processor';
1111

1212
describe('CoreKeyboardProcessor', function () {
@@ -532,4 +532,87 @@ describe('CoreKeyboardProcessor', function () {
532532
});
533533
}
534534
});
535+
536+
describe('doModifierPress', function () {
537+
// const touchable = true;
538+
const nonTouchable = false;
539+
540+
beforeEach(async function () {
541+
coreProcessor = new CoreKeyboardProcessor();
542+
await coreProcessor.init(coreurl);
543+
state = createState('/common/test/resources/keyboards/test_8568_deadkeys.kmx');
544+
context = KM_Core.instance.state_context(state);
545+
sandbox = sinon.createSandbox();
546+
const coreKeyboard = loadKeyboard('/common/test/resources/keyboards/test_8568_deadkeys.kmx');
547+
coreProcessor.activeKeyboard = new KMXKeyboard(coreKeyboard);
548+
});
549+
550+
afterEach(() => {
551+
sandbox.restore();
552+
sandbox = null;
553+
})
554+
555+
for (const key of [
556+
{ code: Codes.keyCodes.K_SHIFT, name: 'Shift' },
557+
{ code: Codes.keyCodes.K_CONTROL, name: 'Control' },
558+
{ code: Codes.keyCodes.K_ALT, name: 'Alt' },
559+
{ code: Codes.keyCodes.K_CAPS, name: 'CapsLock' },
560+
{ code: Codes.keyCodes.K_NUMLOCK, name: 'NumLock' },
561+
{ code: Codes.keyCodes.K_SCROLL, name: 'ScrollLock' },
562+
// TODO-web-core: should LSHIFT/RSHIFT etc also be detected as modifier?
563+
// Currently .js keyboards don't don't support distinguishing
564+
// between left and right keys, but should KMX keyboards in Web?
565+
// { code: Codes.keyCodes.K_LSHIFT, name: 'LeftShift' },
566+
// { code: Codes.keyCodes.K_RSHIFT, name: 'RightShift' },
567+
// { code: Codes.keyCodes.K_LCTRL, name: 'LeftControl' },
568+
// { code: Codes.keyCodes.K_RCTRL, name: 'RightControl' },
569+
// { code: Codes.keyCodes.K_LALT, name: 'LeftAlt' },
570+
// { code: Codes.keyCodes.K_RALT, name: 'RightAlt' },
571+
// { code: Codes.keyCodes.K_ALTGR, name: 'AltGr'},
572+
]) {
573+
it(`recognizes ${key.name} as modifier`, function () {
574+
// Setup
575+
const keyEvent = new KeyEvent({
576+
Lcode: key.code,
577+
Lmodifiers: 0,
578+
Lstates: Codes.modifierCodes.NO_CAPS | Codes.modifierCodes.NO_NUM_LOCK | Codes.modifierCodes.NO_SCROLL_LOCK,
579+
LisVirtualKey: true,
580+
device: new DeviceSpec('chrome', 'desktop', 'windows', nonTouchable),
581+
kName: key.name
582+
});
583+
keyEvent.source = { type: 'keydown' };
584+
585+
// Execute
586+
const result = coreProcessor.doModifierPress(keyEvent, new SyntheticTextStore(), true);
587+
588+
// Verify
589+
assert.isTrue(result);
590+
});
591+
}
592+
593+
for (const key of [
594+
{ modifiers: 0, name: 'a' },
595+
{ modifiers: Codes.modifierCodes.SHIFT, name: 'A' }
596+
]) {
597+
it(`recognizes ${key.name} not as modifier`, function () {
598+
// Setup
599+
const keyEvent = new KeyEvent({
600+
Lcode: Codes.keyCodes.K_A,
601+
Lmodifiers: key.modifiers,
602+
Lstates: Codes.modifierCodes.NO_CAPS | Codes.modifierCodes.NO_NUM_LOCK | Codes.modifierCodes.NO_SCROLL_LOCK,
603+
LisVirtualKey: true,
604+
device: new DeviceSpec('chrome', 'desktop', 'windows', nonTouchable),
605+
kName: 'K_A'
606+
});
607+
keyEvent.source = { type: 'keydown' };
608+
609+
// Execute
610+
const result = coreProcessor.doModifierPress(keyEvent, new SyntheticTextStore(), true);
611+
612+
// Verify
613+
assert.isFalse(result);
614+
});
615+
}
616+
617+
});
535618
});

0 commit comments

Comments
 (0)