spiegel-keyman/common/web/keyboard-processor/src/keyboards/activeLayout.ts

905 lines
28 KiB
TypeScript

import Codes from "../text/codes.js";
import KeyEvent, { KeyEventSpec } from "../text/keyEvent.js";
import KeyMapping from "../text/keyMapping.js";
import type { KeyDistribution } from "../text/keyEvent.js";
import { ButtonClasses, Layouts } from "./defaultLayouts.js";
import type { LayoutKey, LayoutSubKey, LayoutRow, LayoutLayer, LayoutFormFactor, ButtonClass } from "./defaultLayouts.js";
import type Keyboard from "./keyboard.js";
import { TouchLayout } from "@keymanapp/common-types";
import TouchLayoutDefaultHint = TouchLayout.TouchLayoutDefaultHint;
import TouchLayoutFlick = TouchLayout.TouchLayoutFlick;
import TouchLayoutSpec = TouchLayout.TouchLayoutPlatform;
import TouchLayerSpec = TouchLayout.TouchLayoutLayer;
import { type DeviceSpec } from "@keymanapp/web-utils";
// TS 3.9 changed behavior of getters to make them
// non-enumerable by default. This broke our 'polyfill'
// functions which depended on enumeration to copy the
// relevant props over.
// https://github.com/microsoft/TypeScript/pull/32264#issuecomment-677718191
function Enumerable(
target: unknown,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = true;
};
/**
* Designed for use by call-by-reference objects during keyboard-load preprocessing
* to note properties of a keyboard that are only specified at lower levels of the
* layout object.
*/
interface AnalysisMetadata {
hasFlicks: boolean;
hasMultitaps: boolean;
hasLongpresses: boolean;
}
/** A map of key field names with values matching the `typeof` the corresponding property
* seen in keyman-touch-layout-file.ts from common/web/types.
*
* Make sure that when one is updated, the other also is. TS types are compile-time only,
* so this run-time-accessible mapping cannot be auto-generated by TS. */
const KeyTypesOfKeyMap = {
id: 'string',
text: 'string',
layer: 'string',
nextlayer: 'string',
font: 'string',
fontsize: 'string',
sp: 'number',
pad: 'number',
width: 'number',
sk: 'subkeys',
flick: 'flicks',
multitap: 'subkeys',
hint: 'string',
default: 'boolean'
} as const;
// Keep in this specific order: it's the ordering of priority for default hint selection when
// based on available hints. (i.e., `layout.defaultHint == 'flick'`)
const KeyTypesOfFlickList = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw'] as const;
export class ActiveKeyBase {
static readonly DEFAULT_PAD=15; // Padding to left of key, in virtual units
static readonly DEFAULT_RIGHT_MARGIN=15; // Padding to right of right-most key, in virtual units
static readonly DEFAULT_KEY_WIDTH=100; // Width of a key, if not specified, in virtual units
// Defines key defaults
static readonly DEFAULT_KEY = {
text: '',
width: ActiveKeyBase.DEFAULT_KEY_WIDTH,
sp: ButtonClasses.normal,
pad: ActiveKeyBase.DEFAULT_PAD
};
/** WARNING - DO NOT USE DIRECTLY outside of @keymanapp/keyboard-processor! */
id: TouchLayout.TouchLayoutKeyId;
text: string;
hint?: string;
hintSrc?: TouchLayout.TouchLayoutSubKey | TouchLayout.TouchLayoutKey;
font?: string;
fontsize?: string;
// These are fine.
width?: number;
pad?: number;
layer: string;
displayLayer: string;
nextlayer: string;
sp?: ButtonClass;
private _baseKeyEvent: KeyEvent | (() => KeyEvent);
isMnemonic: boolean = false;
/**
* Only available on subkeys, but we don't distinguish between base keys and subkeys
* at this level yet in KMW.
*/
default?: boolean;
proportionalPad: number;
proportionalX: number;
proportionalWidth: number;
// While they're only valid on ActiveKey, spec'ing them here makes references more concise within the OSK.
sk?: ActiveSubKey[];
multitap?: ActiveSubKey[];
flick?: TouchLayout.TouchLayoutFlick;
// Keeping things simple here, as this was added LATE in 14.0 beta.
// Could definitely extend in the future to instead return an object
// that denotes the 'nature' of the key.
// - isUnicode
// - isHardwareKey
// - etc.
// Reference for the terminology in the comments below:
// https://help.keyman.com/developer/current-version/guides/develop/creating-a-touch-keyboard-layout-for-amharic-the-nitty-gritty
/**
* Matches the key code as set within Keyman Developer for the layout.
* For example, K_R or U_0020. Denotes either physical keys or virtual keys with custom output,
* with no additional metadata like layer or active modifiers.
*
* Is used to determine the keycode for input events, rule-matching, and keystroke processing.
*/
@Enumerable
public get baseKeyID(): string {
if(typeof this.id === 'undefined') {
return undefined;
}
return this.id;
}
@Enumerable
public get isPadding(): boolean {
// Does not include 9 (class: blank) as that may be an intentional 'catch' for misplaced
// keystrokes.
return this.sp == ButtonClasses.spacer; // Button class: hidden.
}
/**
* A unique identifier based on both the key ID & the 'desktop layer' to be used for the key.
*
* Allows diambiguation of scenarios where the same key ID is used twice within a layer, but
* with different innate modifiers. (Refer to https://github.com/keymanapp/keyman/issues/4617)
* The 'desktop layer' may be omitted if it matches the key's display layer.
*
* Examples, given a 'default' display layer, matching keys to Keyman keyboard language:
*
* ```
* "K_Q"
* + [K_Q]
* "K_Q+shift"
* + [K_Q SHIFT]
* ```
*
* Useful when the active layer of an input-event is already known.
*/
@Enumerable
public get coreID(): string {
if(typeof this.id === 'undefined') {
return undefined;
}
let baseID = this.id || '';
if(this.displayLayer != this.layer) {
baseID = baseID + '+' + this.layer;
}
return baseID;
}
/**
* A keyboard-unique identifier to be used for any display elements representing this key
* in user interfaces and/or on-screen keyboards.
*
* Distinguishes between otherwise-identical keys on different layers of an OSK.
* Includes identifying information about the key's display layer.
*
* Examples, given a 'default' display layer, matching keys to Keyman keyboard language:
*
* ```
* "default-K_Q"
* + [K_Q]
* "default-K_Q+shift"
* + [K_Q SHIFT]
* ```
*
* Useful when only the active keyboard is known about an input event.
*/
@Enumerable
public get elementID(): string {
if(typeof this.id === 'undefined') {
return undefined;
}
return this.displayLayer + '-' + this.coreID;
}
@Enumerable
public get baseKeyEvent(): KeyEvent {
let val = this._baseKeyEvent;
if(typeof val == 'function') {
val = val();
}
return new KeyEvent(val);
}
constructor();
constructor(spec: LayoutKey | LayoutSubKey, layout: ActiveLayout, displayLayer: string);
constructor(spec?: LayoutKey | LayoutSubKey, layout?: ActiveLayout, displayLayer?: string) {
// First things first: this class's fields are designed to match that of the spec.
Object.assign(this, spec);
if(!this.text && typeof this.id == 'string') {
this.text = ActiveKey.unicodeIDToText(this.id);
}
this.displayLayer = displayLayer;
this.layer = this.layer || displayLayer;
// Compute the key's base KeyEvent properties for use in future event generation
// It's actually somewhat expensive to do this at the start, so we do a lazy-init.
this._baseKeyEvent = () => this.constructBaseKeyEvent(layout, displayLayer);
}
/**
* Converts key IDs of the U_* form to their corresponding UTF-16 text.
* If an ID not matching the pattern is received, returns null.
* @param id
* @returns
*/
static unicodeIDToText(id: string, errorCallback?: (codeAsString: string) => void) {
if(!id || id.substring(0,2) != 'U_') {
return null;
}
let result = '';
const codePoints = id.substring(2).split('_');
for(let codePoint of codePoints) {
const codePointValue = parseInt(codePoint, 16);
if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) ||
((0x80 <= codePointValue) && (codePointValue <= 0x9F)) ||
isNaN(codePointValue)) {
if(errorCallback) {
errorCallback(codePoint);
}
continue;
} else {
// String.fromCharCode() is inadequate to handle the entire range of Unicode
// Someday after upgrading to ES2015, can use String.fromCodePoint()
result += String.kmwFromCharCode(codePointValue);
}
}
return result ? result : null;
}
static sanitize(rawKey: LayoutKey) {
// In older versions of KeymanWeb, we specified these three properties as strings...
// despite them holding a numerical value.
if(typeof rawKey.width == 'string') {
rawKey.width = parseInt(rawKey.width, 10);
}
// Handles NaN cases as well as 'set to 0' cases; both are intentional here.
rawKey.width ||= ActiveKey.DEFAULT_KEY_WIDTH;
if(typeof rawKey.pad == 'string') {
rawKey.pad = parseInt(rawKey.pad, 10);
}
rawKey.pad ||= ActiveKey.DEFAULT_PAD;
if(typeof rawKey.sp == 'string') {
rawKey.sp = Number.parseInt(rawKey.sp, 10) as ButtonClass;
}
rawKey.sp ||= ActiveKey.DEFAULT_KEY.sp; // The default button class.
// And now for generalized type validation. -----------------------------------------
// WARNING: Object.values and Object.entries is NOT polyfilled by es6-shim and thus
// is NOT available within the Android app in extremely early APIs.
// Object.entries requires Android 54.
for(const key of Object.keys(KeyTypesOfKeyMap)) {
const value = KeyTypesOfKeyMap[key as keyof typeof KeyTypesOfKeyMap];
switch(value) {
case 'subkeys':
const arr = rawKey[key] as LayoutSubKey[];
if(arr === undefined) {
break;
} else if(!Array.isArray(arr)) {
delete rawKey[key];
} else {
for(let i=0; i < arr.length; i++) {
const sk = arr[i];
if(typeof sk != 'object') {
arr.splice(i--, 1);
} else {
ActiveKey.sanitize(sk);
}
}
}
break;
case 'flicks':
const flickObj = rawKey[key];
if(flickObj === undefined) {
break;
} else if(typeof flickObj != 'object') {
delete rawKey[key];
} else {
for(const flickKey of KeyTypesOfFlickList) {
const sk = flickObj[flickKey];
if(sk === undefined) {
break;
} else if(typeof sk != 'object') {
delete flickObj[flickKey];
} else {
ActiveKey.sanitize(sk);
}
}
}
break;
default:
const prop = rawKey[key];
if(prop !== undefined && typeof prop != value) {
delete rawKey[key];
}
}
}
rawKey.text ||= ActiveKey.DEFAULT_KEY.text;
}
@Enumerable
private constructBaseKeyEvent(layout: ActiveLayout, displayLayer: string) {
// Get key name and keyboard shift state (needed only for default layouts and physical keyboard handling)
// Note - virtual keys should be treated case-insensitive, so we force uppercasing here.
let layer = this.layer || displayLayer || '';
let keyName= this.id ? this.id.toUpperCase() : null;
// Start: mirrors _GetKeyEventProperties
// First check the virtual key, and process shift, control, alt or function keys
let props: KeyEventSpec = {
// Override key shift state if specified for key in layout (corrected for popup keys KMEW-93)
Lmodifiers: Codes.getModifierState(layer),
Lstates: Codes.getStateFromLayer(layer),
Lcode: keyName ? Codes.keyCodes[keyName] : 0,
LisVirtualKey: true,
vkCode: 0,
kName: keyName,
kLayer: layer,
kbdLayer: displayLayer,
kNextLayer: this.nextlayer,
device: null,
isSynthetic: true
};
let Lkc: KeyEvent = new KeyEvent(props);
if(layout.keyboard) {
let keyboard = layout.keyboard;
// Include *limited* support for mnemonic keyboards (Sept 2012)
// If a touch layout has been defined for a mnemonic keyout, do not perform mnemonic mapping for rules on touch devices.
if(keyboard.isMnemonic && !(layout.isDefault && layout.formFactor != 'desktop')) {
if(Lkc.Lcode != Codes.keyCodes['K_SPACE']) { // exception required, March 2013
// Jan 2019 - interesting that 'K_SPACE' also affects the caps-state check...
Lkc.vkCode = Lkc.Lcode;
this.isMnemonic = true;
}
} else {
Lkc.vkCode=Lkc.Lcode;
}
// Support version 1.0 KeymanWeb keyboards that do not define positional vs mnemonic
if(!keyboard.definesPositionalOrMnemonic) {
Lkc.Lcode = KeyMapping._USKeyCodeToCharCode(Lkc);
Lkc.LisVirtualKey=false;
}
}
return Lkc;
}
}
export class ActiveKey extends ActiveKeyBase implements LayoutKey {
public getSubkey(coreID: string): ActiveSubKey {
if(this.sk) {
for(let key of this.sk) {
if(key.coreID == coreID) {
return key;
}
}
}
return null;
}
constructor();
constructor(spec: LayoutKey, layout: ActiveLayout, displayLayer: string);
constructor(spec?: LayoutKey, layout?: ActiveLayout, displayLayer?: string) {
super(spec, layout, displayLayer);
// Ensure subkeys are also properly extended.
const sk = this.sk;
if(sk) {
for(let i=0; i < sk.length; i++) {
sk[i] = new ActiveSubKey(sk[i], layout, displayLayer);
}
}
// Also multitap keys.
const multitap = this.multitap;
if(multitap) {
for(let i=0; i < multitap.length; i++) {
multitap[i] = new ActiveSubKey(multitap[i], layout, displayLayer);
}
}
const flick = this.flick;
if(flick) {
for(let flickKey in flick) {
flick[flickKey] = new ActiveSubKey(flick[flickKey], layout, displayLayer);
}
}
ActiveKey.determineHint(this, layout.defaultHint);
}
private static determineHint(spec: ActiveKey, defaultHint: TouchLayout.TouchLayoutDefaultHint): void {
// If a hint was directly specified, don't override it.
if(spec.hint) {
spec.hintSrc = spec;
return;
}
// Is more compact than writing 8 separate cases.
if(defaultHint?.includes('flick-')) {
if(spec.flick) {
// 6 = length of 'flick-'
const dir = defaultHint.substring(6);
if(spec.flick[dir]?.text) {
spec.hintSrc = spec.flick[dir];
}
}
return;
}
switch(defaultHint) {
case 'none':
return;
case 'multitap':
if(spec.multitap) {
spec.hintSrc = spec.multitap[0];
}
return;
case 'flick':
if(spec.flick) {
for(const key of KeyTypesOfFlickList) {
if(spec.flick[key]) {
spec.hintSrc = spec.flick[key];
return;
}
}
}
return;
case 'longpress':
if(spec.sk) {
spec.hintSrc = spec.sk[0];
}
return;
case 'dot':
default:
if(spec.sk) {
spec.hint = '\u2022';
spec.hintSrc = spec;
}
return;
}
}
}
export class ActiveSubKey extends ActiveKeyBase implements LayoutSubKey {
//
}
export class ActiveRow implements LayoutRow {
// Identify key labels (e.g. *Shift*) that require the special OSK font
static readonly SPECIAL_LABEL=/\*\w+\*/;
id: number;
key: ActiveKey[];
/**
* Used for calculating fat-fingering offsets.
*/
proportionalY: number;
private constructor() {
}
static sanitize(rawRow: LayoutRow) {
for(const key of rawRow.key) {
// Test for a trailing comma included in spec, added as null object by IE
// It has only ever appeared at the end of a row's spec.
if(key == null) {
rawRow.key.length = rawRow.key.length-1;
} else {
ActiveKey.sanitize(key);
}
}
if(typeof rawRow.id == 'string') {
rawRow.id = Number.parseInt(rawRow.id, 10);
}
}
static polyfill(
row: LayoutRow,
keyboard: Keyboard,
layout: ActiveLayout,
displayLayer: string,
totalWidth: number,
proportionalY: number,
analysisFlagObj: AnalysisMetadata
) {
// Apply defaults, setting the width and other undefined properties for each key
let keys=row['key'];
for(let j=0; j<keys.length; j++) {
let key=keys[j];
for(var tp in ActiveKey.DEFAULT_KEY) {
if(typeof key[tp] != 'string' && typeof key[tp] != 'number') {
key[tp]=ActiveKey.DEFAULT_KEY[tp];
}
}
// Modify the key type for special keys with non-standard labels
// to allow the keyboard font to overide the SpecialOSK font.
// Blank keys are no longer reclassed - can use before/after CSS to add text
switch(key['sp']) {
case ButtonClasses.special:
if(!ActiveRow.SPECIAL_LABEL.test(key['text']) && key['text'] != '') {
key.sp=ButtonClasses.customSpecial;
}
break;
case ButtonClasses.specialActive:
if(!ActiveRow.SPECIAL_LABEL.test(key['text']) && key['text'] != '') {
key.sp=ButtonClasses.customSpecialActive;
}
break;
}
const processedKey = new ActiveKey(key, layout, displayLayer);
keys[j] = processedKey;
if(processedKey.sk) {
analysisFlagObj.hasLongpresses = true;
}
if(processedKey.multitap) {
analysisFlagObj.hasMultitaps = true;
}
if(processedKey.flick) {
analysisFlagObj.hasFlicks = true;
}
}
/* The calculations here are effectively 'virtualized'. When used with the OSK, the VisualKeyboard
* will overwrite these values with their true runtime geometry.
*
* These calculations approximate those of the actual OSK (without fitting to a specific resolution)
* and are intended for use with layout testing (while headless) in the future.
*/
let setProportions = function(key: ActiveKey, padPc: number, keyPc: number, totalPc: number) {
key.proportionalPad = padPc;
key.proportionalWidth = keyPc;
key.proportionalX = (totalPc + padPc + (keyPc/2));
}
// Calculate percentage-based scalings by summing defined widths and scaling each key to %.
// Save each percentage key width as a separate member (do *not* overwrite layout specified width!)
let totalPercent=0;
for(let j=0; j<keys.length-1; j++) {
const key = keys[j] as ActiveKey; // already 'polyfilled' in prior loop
// compute center's default x-coord (used in headless modes), assign 'proportional' props
setProportions(key, key.pad/totalWidth, key.width/totalWidth, totalPercent);
// These values are set on the key as part of the prior call.
totalPercent += key.proportionalPad;
totalPercent += key.proportionalWidth;
}
// Allow for right OSK margin (15 layout units)
let rightMargin = ActiveKey.DEFAULT_RIGHT_MARGIN/totalWidth;
if(keys.length > 0) {
const finalKey = keys[keys.length-1] as ActiveKey;
// If a single key, and padding is negative, add padding to right align the key
if(keys.length == 1 && finalKey.pad < 0) {
const keyPercent = finalKey.width/totalWidth;
const padPercent = 1-(totalPercent + keyPercent + rightMargin);
// compute center's default x-coord (used in headless modes)
setProportions(finalKey, padPercent, keyPercent, totalPercent);
} else {
const padPercent = finalKey.pad/totalWidth;
const keyPercent = 1-(totalPercent + padPercent + rightMargin);
// compute center's default x-coord (used in headless modes)
setProportions(finalKey, padPercent, keyPercent, totalPercent);
}
}
// Add class functions to the existing layout object, allowing it to act as an ActiveLayout.
let dummy = new ActiveRow();
for(let key in dummy) {
if(!row.hasOwnProperty(key)) {
row[key] = dummy[key];
}
}
let aRow = row as ActiveRow;
aRow.proportionalY = proportionalY;
}
@Enumerable
populateKeyMap(map: {[keyId: string]: ActiveKey}) {
this.key.forEach(function(key: ActiveKey) {
if(key.coreID) {
map[key.coreID] = key;
}
});
}
}
export class ActiveLayer implements LayoutLayer {
row: ActiveRow[];
id: string;
// These already exist on the objects, pre-polyfill...
// but they still need to be proactively declared on this type.
capsKey?: ActiveKey;
numKey?: ActiveKey;
scrollKey?: ActiveKey;
totalWidth: number;
defaultKeyProportionalWidth: number;
rowProportionalHeight: number;
/**
* Facilitates mapping key id strings to their specification objects.
*/
keyMap: {[keyId: string]: ActiveKey};
constructor() {
}
static sanitize(rawLayer: LayoutLayer) {
for(const row of rawLayer.row) {
ActiveRow.sanitize(row);
}
}
static polyfill(layer: LayoutLayer, keyboard: Keyboard, layout: ActiveLayout, analysisFlagObj: AnalysisMetadata) {
layer.aligned=false;
// Create a DIV for each row of the group
let rows=layer['row'];
// Calculate the maximum row width (in layout units)
let totalWidth=0;
for(const row of rows) {
let width=0;
const keys=row['key'];
for(const key of keys) {
// So long as `sanitize` is called first, these coercions are safe.
width += (key.width as number) + (key.pad as number);
}
if(width > totalWidth) {
totalWidth = width;
}
}
// Add default right margin
if(layout.formFactor == 'desktop') {
totalWidth += 5; // TODO: resolve difference between touch and desktop; why don't we use ActiveKey.DEFAULT_RIGHT_MARGIN?
} else {
totalWidth += ActiveKey.DEFAULT_RIGHT_MARGIN;
}
let rowCount = layer.row.length;
for(let i=0; i<rowCount; i++) {
// Calculate proportional y-coord of row. 0 is at top with highest y-coord.
let rowProportionalY = (i + 0.5) / rowCount;
ActiveRow.polyfill(layer.row[i], keyboard, layout, layer.id, totalWidth, rowProportionalY, analysisFlagObj);
}
// Add class functions and properties to the existing layout object, allowing it to act as an ActiveLayout.
let dummy = new ActiveLayer();
for(let key in dummy) {
if(!layer.hasOwnProperty(key)) {
layer[key] = dummy[key];
}
}
let aLayer = layer as ActiveLayer;
aLayer.totalWidth = totalWidth;
aLayer.defaultKeyProportionalWidth = ActiveKey.DEFAULT_KEY.width / totalWidth;
aLayer.rowProportionalHeight = 1.0 / rowCount;
aLayer.keyMap = aLayer.constructKeyMap();
}
@Enumerable
private constructKeyMap(): {[keyId: string]: ActiveKey} {
let map: {[keyId: string]: ActiveKey} = {};
this.row.forEach(function(row: ActiveRow) {
row.populateKeyMap(map);
});
return map;
}
@Enumerable
getKey(keyId: string) {
// Keys usually are specified in a "long form" prefixed with their layer's ID.
if(keyId.indexOf(this.id + '-') == 0) {
keyId = keyId.replace(this.id + '-', '');
}
let idComponents = keyId.split('::');
if(idComponents.length > 1) {
let baseKey = this.keyMap[idComponents[0]];
return baseKey.getSubkey(idComponents[1]);
} else {
return this.keyMap[keyId];
}
}
}
export class ActiveLayout implements LayoutFormFactor{
layer: ActiveLayer[];
font: string;
keyLabels: boolean;
isDefault?: boolean;
keyboard: Keyboard;
formFactor: DeviceSpec.FormFactor;
defaultHint: TouchLayoutDefaultHint;
hasFlicks: boolean = false;
hasLongpresses: boolean = false;
hasMultitaps: boolean = false;
/**
* Facilitates mapping layer id strings to their specification objects.
*/
private layerMap: {[layerId: string]: ActiveLayer};
private constructor() {
}
@Enumerable
getLayer(layerId: string): ActiveLayer {
return this.layerMap[layerId];
}
/**
* Refer to https://github.com/keymanapp/keyman/issues/254, which mentions
* KD-11 from a prior issue-tracking system from the closed-source days that
* resulted in an unintended extra empty row.
*
* It'll be pretty rare to see a keyboard affected by the bug, but we don't
* 100% control all keyboards out there, so it's best we make sure the edge
* case is covered.
*
* @param layers The layer group to be loaded for the form factor. Will be
* mutated by this operation.
*/
static correctLayerEmptyRowBug(layers: LayoutLayer[]) {
for(let n=0; n<layers.length; n++) {
let layer=layers[n];
let rows=layer['row'];
let i: number;
for(i=rows.length-1; i>=0; i--) {
if(!Array.isArray(rows[i]['key']) || rows[i]['key'].length == 0) {
rows.splice(i, 1)
}
}
}
}
static sanitize(rawLayout: TouchLayoutSpec) {
ActiveLayout.correctLayerEmptyRowBug(rawLayout.layer);
for(const layer of rawLayout.layer) {
ActiveLayer.sanitize(layer);
}
}
/**
*
* @param layout
* @param formFactor
*/
static polyfill(layout: TouchLayoutSpec, keyboard: Keyboard, formFactor: DeviceSpec.FormFactor): ActiveLayout {
/* c8 ignore start */
if(layout == null) {
throw new Error("Cannot build an ActiveLayout for a null specification.");
}
/* c8 ignore end */
const analysisMetadata: AnalysisMetadata = {
hasFlicks: false,
hasLongpresses: false,
hasMultitaps: false
};
/* Standardize the layout object's data types.
*
* In older versions of KMW, some numeric properties were long represented as strings instead,
* and that lives on within a _lot_ of keyboards. The data should be sanitized before it
* is processed by this method.
*/
this.sanitize(layout);
// Create a separate OSK div for each OSK layer, only one of which will ever be visible
var n: number;
let layerMap: {[layerId: string]: ActiveLayer} = {};
let layers=layout.layer;
// Add class functions to the existing layout object, allowing it to act as an ActiveLayout.
let dummy = new ActiveLayout();
for(let key in dummy) {
if(!layout.hasOwnProperty(key)) {
layout[key] = dummy[key];
}
}
let aLayout = layout as ActiveLayout;
aLayout.keyboard = keyboard;
aLayout.formFactor = formFactor;
for(n=0; n<layers.length; n++) {
ActiveLayer.polyfill(layers[n], keyboard, aLayout, analysisMetadata);
layerMap[layers[n].id] = layers[n] as ActiveLayer;
}
// After all layers are preprocessed...
// The default-layer shift key & shift-layer shift key on mobile platforms should have a
// default multitap re: a 'caps' layer under select conditions.
//
// Note: whether or not any other keys have multitaps doesn't matter here. Just THESE.
if(formFactor != 'desktop' && !!layout.layer.find((entry) => entry.id == 'caps')) {
const defaultLayer = layout.layer.find((entry) => entry.id == 'default') as ActiveLayer;
const shiftLayer = layout.layer.find((entry) => entry.id == 'shift') as ActiveLayer;
const defaultShift = defaultLayer.getKey('K_SHIFT');
const shiftShift = shiftLayer ?.getKey('K_SHIFT');
// If BOTH default & shift layer SHIFT keys lack multitaps & longpresses, proceed.
if(defaultShift && shiftShift && // doesn't make much sense if there's no shift layer or SHIFT on either
!defaultShift.multitap && !shiftShift.multitap &&
!defaultShift.sk && !shiftShift.sk
) {
// May cause the layout to gain its first multitaps, which does matter for the next lines after the block.
analysisMetadata.hasMultitaps = true;
defaultShift.multitap = [{...Layouts.dfltShiftToCaps}, {...Layouts.dfltShiftToDefault}] as ActiveSubKey[];
shiftShift.multitap = [{...Layouts.dfltShiftToCaps}, {...Layouts.dfltShiftToShift}] as ActiveSubKey[];
defaultShift.multitap.forEach((sk, index) => defaultShift.multitap[index] = new ActiveSubKey(sk, aLayout, 'default'));
shiftShift .multitap.forEach((sk, index) => shiftShift.multitap[index] = new ActiveSubKey(sk, aLayout, 'shift'));
} // else no default shift -> caps multitaps.
}
aLayout.hasFlicks = analysisMetadata.hasFlicks;
aLayout.hasLongpresses = analysisMetadata.hasLongpresses;
aLayout.hasMultitaps = analysisMetadata.hasMultitaps;
aLayout.layerMap = layerMap;
return aLayout;
}
}