-
-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(web): prototype KMX+ TouchLayout generator (in TS) #12305
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
import { Layer, LayerList } from './layr.js'; | ||
import { TouchLayout } from '@keymanapp/common-types'; | ||
import TouchLayoutLayer = TouchLayout.TouchLayoutLayer; | ||
import TouchLayoutRow = TouchLayout.TouchLayoutRow; | ||
import TouchLayoutKey = TouchLayout.TouchLayoutKey; | ||
import TouchLayoutSubKey = TouchLayout.TouchLayoutSubKey; | ||
import TouchLayoutFlick = TouchLayout.TouchLayoutFlick; | ||
|
||
import Codes from '../codes.js'; | ||
import { KeySpec, Keys } from './keys.js'; | ||
import { ButtonClasses } from '../keyboards/defaultLayouts.js'; | ||
|
||
const keyNames = Object.keys(Codes.keyCodes); | ||
|
||
export function convertLayerList(layerList: LayerList, keys: Keys) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function converts one layer list (one pre-parsed entry of the |
||
return layerList.layers.map((layer) => convertLayer(layer, keys)); | ||
} | ||
|
||
function nameFromModifier(code: number) { | ||
const modNames = Object.keys(Codes.modifierCodes); | ||
|
||
for(let mod of modNames) { | ||
if(Codes.modifierCodes[mod] == code) { | ||
return mod.toLowerCase(); | ||
} | ||
} | ||
|
||
if(code == 0) { | ||
return 'default'; | ||
} | ||
|
||
return ''; | ||
} | ||
|
||
export function convertLayer(layer: Layer, keys: Keys): TouchLayoutLayer { | ||
const modName = nameFromModifier(layer.modifiers); | ||
const name = layer.id == '' ? modName : layer.id; | ||
|
||
const rows: TouchLayoutRow[] = layer.keys.map((row, index) => { | ||
return { | ||
id: index, | ||
key: row.map((keySpec) => convertKey(keySpec, keys, modName)) | ||
// | ||
} as TouchLayoutRow | ||
}); | ||
|
||
// TODO: add frame keys as needed | ||
// like shift, etc. | ||
|
||
return { | ||
id: name, | ||
row: rows | ||
}; | ||
} | ||
|
||
export function convertKey(keySpec: KeySpec, keys: Keys, defaultModifier: string): TouchLayoutKey | TouchLayoutSubKey { | ||
if(keySpec.isGap) { | ||
return { | ||
sp: ButtonClasses.spacer, | ||
width: keySpec.width | ||
} | ||
} | ||
|
||
let id = keySpec.id; | ||
for(let keyId of keyNames) { | ||
if(Codes.keyCodes[keyId] == keySpec.keyCode) { | ||
id = keyId; | ||
} | ||
} | ||
|
||
// TODO: if unmatched, we need a synthetic key ID (or similar) for | ||
// custom keys. | ||
// | ||
// MD recommendation: build a `T_` id for such keys, keeping them unique. | ||
// May need a table with matching codes (per JS-keyboard VKDictionary) to | ||
// use within Core as a numeric key ID for rules... or maybe the string itself | ||
// will work within Core? I leave that to y'all. | ||
|
||
let obj: TouchLayoutKey = { | ||
id: id as unknown as any, // may not actually match Keyman keyboard id restrictions! | ||
text: keySpec.to, // TODO: Look up correct value from `disp` table. | ||
width: keySpec.width | ||
// | ||
}; | ||
|
||
if(keySpec.switch) { | ||
obj.nextlayer = keySpec.switch; | ||
} | ||
|
||
if(keySpec.longpress && keySpec.longpress.length > 0) { | ||
obj.sk = keySpec.longpress.map((id) => convertKey(keys.keys.get(id), keys, defaultModifier) as TouchLayoutSubKey); | ||
} | ||
|
||
if(keySpec.multitap && keySpec.multitap.length > 0) { | ||
obj.multitap = keySpec.multitap.map((id) => convertKey(keys.keys.get(id), keys, defaultModifier) as TouchLayoutSubKey); | ||
} | ||
|
||
if(keySpec.flicks) { | ||
const flickObj: TouchLayoutFlick = {}; | ||
|
||
for(let flick of keySpec.flicks) { | ||
if(flick.dirSequence.length == 1) { | ||
const dir = flick.dirSequence[0] as keyof TouchLayoutFlick; | ||
const key = convertKey(keys.keys.get(flick.keyId), keys, defaultModifier) as TouchLayoutSubKey; | ||
|
||
flickObj[dir] = key; | ||
} | ||
// else skip - we don't support multisegment flicks. | ||
} | ||
|
||
obj.flick = flickObj; | ||
} | ||
|
||
// Should match the modifier | ||
if(keySpec.modifiers) { | ||
// [JH] Wait... but the key always has a modifier value on it... right? | ||
// I don't see where the KMX+ spec allows that to be undefined. | ||
obj.layer = keySpec.modifiers !== undefined ? nameFromModifier(keySpec.modifiers) : defaultModifier; | ||
} | ||
|
||
return obj; | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,185 @@ | ||||||
import { List } from './list.js'; | ||||||
import { Strs } from './strs.js'; | ||||||
import { decodeNumber } from './utils.js'; | ||||||
|
||||||
type Flick = { dirSequence: readonly string[], keyId: string }; | ||||||
type FlickSet = { flicks: Flick[], flickId: string }; | ||||||
type VkeyMapping = { code: number, modifiers: number, keyIndex: number }; | ||||||
export type KeySpec = { | ||||||
to: string; | ||||||
isGap: boolean; | ||||||
id: string; | ||||||
switch: string; | ||||||
width: number; | ||||||
longpress: readonly string[]; | ||||||
defaultLongpressId: string; | ||||||
multitap: readonly string[]; | ||||||
flicks: Flick[]; | ||||||
keyCode: number, | ||||||
modifiers: number | ||||||
}; | ||||||
|
||||||
export class Keys { | ||||||
readonly name = 'keys'; | ||||||
readonly data: Uint8Array; | ||||||
readonly keys: Readonly<Map<string, KeySpec>>; | ||||||
|
||||||
constructor(rawData: Uint8Array, strs: Strs, list: List) { | ||||||
this.data = rawData; | ||||||
|
||||||
const keyCount = decodeNumber(rawData, 8); | ||||||
const flicksCount = decodeNumber(rawData, 12); | ||||||
const flickCount = decodeNumber(rawData, 16); | ||||||
|
||||||
const FLICK_GROUP_LEN = 12; | ||||||
const FLICK_LEN = 8; | ||||||
|
||||||
const keyTableOffset = 24; | ||||||
const flicksTableOffset = keyTableOffset + keyCount * 36; // 9 entries * 4 bytes ea | ||||||
const flickTableOffset = flicksTableOffset + flicksCount * FLICK_GROUP_LEN; | ||||||
const kmapTableOffset = flickTableOffset + flickCount * FLICK_LEN; | ||||||
|
||||||
// keys.keys subtable wants to index within keys.flicks subtable. | ||||||
// So let's build the latter first. | ||||||
|
||||||
const flickSets = this.processFlickSubtables(strs, list, keyTableOffset); | ||||||
const vkeyMap = this.processKeymap(kmapTableOffset); | ||||||
const keybag = this.processKeybag(strs, list, flickSets, vkeyMap); | ||||||
|
||||||
const keyMap: Map<string, KeySpec> = new Map(); | ||||||
for(let key of keybag) { | ||||||
keyMap.set(key.id, key); | ||||||
} | ||||||
this.keys = keyMap; | ||||||
} | ||||||
|
||||||
private processFlickSubtables(strs: Strs, list: List, baseOffset: number) { | ||||||
const rawData = this.data; | ||||||
|
||||||
const keyCount = decodeNumber(rawData, 8); | ||||||
const flicksCount = decodeNumber(rawData, 12); | ||||||
|
||||||
const FLICK_GROUP_LEN = 12; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. now, shouldn't this file be able to use https://github.com/keymanapp/keyman/blob/master/core/include/ldml/keyman_core_ldml.ts ?
Suggested change
|
||||||
const FLICK_LEN = 8; | ||||||
|
||||||
const flicksTableOffset = baseOffset + keyCount * 36; // 9 entries * 4 bytes ea | ||||||
const flickTableOffset = flicksTableOffset + flicksCount * FLICK_GROUP_LEN; | ||||||
|
||||||
// keys.keys subtable wants to index within keys.flicks subtable. | ||||||
// So let's build the latter first. | ||||||
|
||||||
const flickSets: FlickSet[] = []; | ||||||
for(let i = 0; i < flicksCount; i++) { | ||||||
const rowOffset = flicksTableOffset + i * FLICK_GROUP_LEN; | ||||||
const elemCount = decodeNumber(rawData, rowOffset); | ||||||
const firstIndex = decodeNumber(rawData, rowOffset + 4); | ||||||
const stringIndex = decodeNumber(rawData, rowOffset + 8); | ||||||
|
||||||
if(i == 0) { | ||||||
if(elemCount == 0 && firstIndex == 0 && stringIndex == 0) { | ||||||
continue; | ||||||
} else { | ||||||
throw new Error("Empty flick set expected at index 0 in `keys.flicks` subtable"); | ||||||
} | ||||||
} | ||||||
|
||||||
// TODO: no good current fixture available with data for this. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. unit tests only |
||||||
const id = strs.entries[decodeNumber(rawData, rowOffset + 8)]; | ||||||
const baseFlickOffset = flickTableOffset + firstIndex * FLICK_LEN; | ||||||
const flicks: Flick[] = []; | ||||||
for(let j = 0; j < elemCount; j++) { | ||||||
const flickOffset = baseFlickOffset + j * FLICK_LEN; | ||||||
const directions = list.entries[decodeNumber(rawData, flickOffset)]; | ||||||
const keyId = strs.entries[decodeNumber(rawData, flickOffset + 4)]; | ||||||
flicks.push({ | ||||||
dirSequence: directions, | ||||||
keyId: keyId | ||||||
}); | ||||||
} | ||||||
|
||||||
flickSets.push({ | ||||||
flicks: flicks, | ||||||
flickId: id | ||||||
}); | ||||||
} | ||||||
|
||||||
return flickSets; | ||||||
} | ||||||
|
||||||
private processKeymap(offset: number) { | ||||||
const kmapCount = decodeNumber(this.data, 20); | ||||||
|
||||||
const mappings: VkeyMapping[] = []; | ||||||
for(let i=0; i < kmapCount; i++) { | ||||||
const rowOffset = offset + 12 * i; | ||||||
|
||||||
mappings.push({ | ||||||
code: decodeNumber(this.data, rowOffset), | ||||||
modifiers: decodeNumber(this.data, rowOffset + 4), | ||||||
keyIndex: decodeNumber(this.data, rowOffset + 8) | ||||||
}); | ||||||
} | ||||||
|
||||||
return mappings; | ||||||
} | ||||||
|
||||||
private processKeybag(strs: Strs, list: List, flickSets: FlickSet[], keyMapping: VkeyMapping[]) { | ||||||
// | ∆ | Bits | Name | Description | | ||||||
// |---|------|-------------|------------------------------------------| | ||||||
// | 0 | 32 | ident | `key2` | | ||||||
// | 4 | 32 | size | int: Length of section | | ||||||
// | 8 | 32 | keyCount | int: Number of keys | | ||||||
// |12 | 32 | flicksCount | int: Number of flick lists | | ||||||
// |16 | 32 | flickCount | int: Number of flick elements | | ||||||
// |20 | 32 | kmapCount | int: Number of kmap elements | | ||||||
// |24 | var | keys | keys sub-table | | ||||||
// | - | var | flicks | flick lists sub-table | | ||||||
// | - | var | flick | flick elements sub-table | | ||||||
// | - | var | kmap | key map sub-table | | ||||||
const rawData = this.data; | ||||||
|
||||||
const keyCount = decodeNumber(rawData, 8); | ||||||
const keyTableOffset = 24; | ||||||
|
||||||
const keys: KeySpec[] = []; | ||||||
for(let i=0; i < keyCount; i++) { | ||||||
const rowOffset = keyTableOffset + i * 36; // 9 cols per row in keys.keys subtable. | ||||||
|
||||||
// is either an index into `strs` OR a UTF-32LE codepoint. | ||||||
const toVal = decodeNumber(rawData, rowOffset); | ||||||
const flags = decodeNumber(rawData, rowOffset + 4); | ||||||
const to = (flags & 1) ? strs.entries[toVal] : String.fromCodePoint(toVal); | ||||||
|
||||||
const isGap = !!(flags & 2); | ||||||
const id = strs.entries[decodeNumber(rawData, rowOffset + 8)]; | ||||||
const switchLayer = strs.entries[decodeNumber(rawData, rowOffset + 12)]; | ||||||
|
||||||
const width = decodeNumber(rawData, rowOffset + 16); | ||||||
const longpressListIndex = decodeNumber(rawData, rowOffset + 20); | ||||||
const longpress = longpressListIndex == 0 ? [] : list.entries[longpressListIndex]; | ||||||
|
||||||
const defaultLongpressId = strs.entries[decodeNumber(rawData, rowOffset + 24)]; | ||||||
|
||||||
const multitapListIndex = decodeNumber(rawData, rowOffset + 28); | ||||||
const multitap = multitapListIndex == 0 ? [] : list.entries[multitapListIndex]; | ||||||
|
||||||
const flickSet = flickSets[decodeNumber(rawData, rowOffset + 32)]; | ||||||
|
||||||
const keySpec: KeySpec = { | ||||||
to, isGap, id, switch: switchLayer, width, longpress, defaultLongpressId, multitap, flicks: flickSet?.flicks, | ||||||
keyCode: 0, | ||||||
modifiers: 0 | ||||||
} | ||||||
|
||||||
keys.push(keySpec); | ||||||
} | ||||||
|
||||||
for(let entry of keyMapping) { | ||||||
const keyObj = keys[entry.keyIndex]; | ||||||
keyObj.keyCode = entry.code; | ||||||
keyObj.modifiers = entry.modifiers; | ||||||
} | ||||||
|
||||||
return keys; | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops, good catch!