mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-06 08:55:34 +00:00
533 lines
18 KiB
TypeScript
533 lines
18 KiB
TypeScript
namespace com.keyman.osk {
|
|
export class KeyData {
|
|
['key']: OSKKey;
|
|
['keyId']: string;
|
|
['subKeys']?: OSKKeySpec[];
|
|
|
|
constructor(keyData: OSKKey, keyId: string) {
|
|
this['key'] = keyData;
|
|
this['keyId'] = keyId;
|
|
}
|
|
}
|
|
|
|
export type KeyElement = HTMLDivElement & KeyData;
|
|
|
|
// Many thanks to https://www.typescriptlang.org/docs/handbook/advanced-types.html for this.
|
|
export function link(elem: HTMLDivElement, data: KeyData): KeyElement {
|
|
let e = <KeyElement> elem;
|
|
|
|
// Merges all properties and methods of KeyData onto the underlying HTMLDivElement, creating a merged class.
|
|
for(let id in data) {
|
|
if(!e.hasOwnProperty(id)) {
|
|
(<any>e)[id] = (<any>data)[id];
|
|
}
|
|
}
|
|
|
|
return e;
|
|
}
|
|
|
|
export function isKey(elem: Node): boolean {
|
|
return elem && ('key' in elem) && ((<any> elem['key']) instanceof OSKKey);
|
|
}
|
|
|
|
export function getKeyFrom(elem: Node): KeyElement {
|
|
if(isKey(elem)) {
|
|
return <KeyElement> elem;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export class OSKKeySpec implements keyboards.LayoutKey {
|
|
id: string;
|
|
|
|
// Only set (within @keymanapp/keyboard-processor) for keys actually specified in a loaded layout
|
|
baseKeyID?: string;
|
|
coreID?: string;
|
|
elementID?: string;
|
|
|
|
text?: string;
|
|
sp?: number | keyboards.ButtonClass;
|
|
width: string;
|
|
layer?: string; // The key will derive its base modifiers from this property - may not equal the layer on which it is displayed.
|
|
nextlayer?: string;
|
|
pad?: string;
|
|
sk?: OSKKeySpec[];
|
|
|
|
constructor(id: string, text?: string, width?: string, sp?: number | keyboards.ButtonClass, nextlayer?: string, pad?: string) {
|
|
this.id = id;
|
|
this.text = text;
|
|
this.width = width ? width : "50";
|
|
this.sp = sp;
|
|
this.nextlayer = nextlayer;
|
|
this.pad = pad;
|
|
}
|
|
}
|
|
|
|
export abstract class OSKKey {
|
|
// Defines the PUA code mapping for the various 'special' modifier/control keys on keyboards.
|
|
// `specialCharacters` must be kept in sync with the same variable in builder.js. See also CompileKeymanWeb.pas: CSpecialText10
|
|
static readonly specialCharacters = {
|
|
'*Shift*': 8,
|
|
'*Enter*': 5,
|
|
'*Tab*': 6,
|
|
'*BkSp*': 4,
|
|
'*Menu*': 11,
|
|
'*Hide*': 10,
|
|
'*Alt*': 25,
|
|
'*Ctrl*': 1,
|
|
'*Caps*': 3,
|
|
'*ABC*': 16,
|
|
'*abc*': 17,
|
|
'*123*': 19,
|
|
'*Symbol*': 21,
|
|
'*Currency*': 20,
|
|
'*Shifted*': 8, // set SHIFTED->9 for filled arrow icon
|
|
'*AltGr*': 2,
|
|
'*TabLeft*': 7,
|
|
'*LAlt*': 0x56,
|
|
'*RAlt*': 0x57,
|
|
'*LCtrl*': 0x58,
|
|
'*RCtrl*': 0x59,
|
|
'*LAltCtrl*': 0x60,
|
|
'*RAltCtrl*': 0x61,
|
|
'*LAltCtrlShift*': 0x62,
|
|
'*RAltCtrlShift*': 0x63,
|
|
'*AltShift*': 0x64,
|
|
'*CtrlShift*': 0x65,
|
|
'*AltCtrlShift*': 0x66,
|
|
'*LAltShift*': 0x67,
|
|
'*RAltShift*': 0x68,
|
|
'*LCtrlShift*': 0x69,
|
|
'*RCtrlShift*': 0x70,
|
|
// Added in Keyman 14.0.
|
|
'*LTREnter*': 0x05, // Default alias of '*Enter*'.
|
|
'*LTRBkSp*': 0x04, // Default alias of '*BkSp*'.
|
|
'*RTLEnter*': 0x71,
|
|
'*RTLBkSp*': 0x72,
|
|
'*ShiftLock*': 0x73,
|
|
'*ShiftedLock*': 0x74,
|
|
'*ZWNJ*': 0x75, // If this one is specified, auto-detection will kick in.
|
|
'*ZWNJiOS*': 0x75, // The iOS version will be used by default, but the
|
|
'*ZWNJAndroid*': 0x76, // Android platform has its own default glyph.
|
|
};
|
|
|
|
static readonly BUTTON_CLASSES = [
|
|
'default',
|
|
'shift',
|
|
'shift-on',
|
|
'special',
|
|
'special-on',
|
|
'', // Key classes 5 through 7 are reserved for future use.
|
|
'',
|
|
'',
|
|
'deadkey',
|
|
'blank',
|
|
'hidden'
|
|
];
|
|
|
|
static readonly HIGHLIGHT_CLASS = 'kmw-key-touched';
|
|
readonly spec: OSKKeySpec;
|
|
|
|
btn: KeyElement;
|
|
label: HTMLSpanElement;
|
|
square: HTMLDivElement;
|
|
|
|
/**
|
|
* The layer of the OSK on which the key is displayed.
|
|
*/
|
|
readonly layer: string;
|
|
|
|
constructor(spec: OSKKeySpec, layer: string) {
|
|
this.spec = spec;
|
|
this.layer = layer;
|
|
}
|
|
|
|
abstract getId(): string;
|
|
|
|
/**
|
|
* Attach appropriate class to each key button, according to the layout
|
|
*
|
|
* @param {Object=} layout source layout description (optional, sometimes)
|
|
*/
|
|
public setButtonClass() {
|
|
let key = this.spec;
|
|
let btn = this.btn;
|
|
|
|
var n=0;
|
|
if(typeof key['dk'] == 'string' && key['dk'] == '1') {
|
|
n=8;
|
|
}
|
|
|
|
if(typeof key['sp'] == 'string') {
|
|
n=parseInt(key['sp'],10);
|
|
}
|
|
|
|
if(n < 0 || n > 10) {
|
|
n=0;
|
|
}
|
|
|
|
btn.className='kmw-key kmw-key-'+OSKKey.BUTTON_CLASSES[n];
|
|
}
|
|
|
|
/**
|
|
* For keys with button classes that support toggle states, this method
|
|
* may be used to toggle which state the key's button class is in.
|
|
* - shift <=> shift-on
|
|
* - special <=> special-on
|
|
* @param {boolean=} flag The new toggle state
|
|
*/
|
|
public setToggleState(flag?: boolean) {
|
|
let btnClassId: number;
|
|
let classAsString: boolean;
|
|
|
|
if(classAsString = typeof this.spec['sp'] == 'string') {
|
|
btnClassId = parseInt(this.spec['sp'], 10);
|
|
} else {
|
|
btnClassId = this.spec['sp'];
|
|
}
|
|
|
|
// 1 + 2: shift + shift-on
|
|
// 3 + 4: special + special-on
|
|
switch(OSKKey.BUTTON_CLASSES[btnClassId]) {
|
|
case 'shift':
|
|
case 'shift-on':
|
|
if(flag === undefined) {
|
|
flag = OSKKey.BUTTON_CLASSES[btnClassId] == 'shift';
|
|
}
|
|
|
|
this.spec['sp'] = 1 + (flag ? 1 : 0);
|
|
break;
|
|
// Added in 15.0: special key highlight toggling.
|
|
// Was _intended_ in earlier versions, but not actually implemented.
|
|
case 'special':
|
|
case 'special-on':
|
|
if(flag === undefined) {
|
|
flag = OSKKey.BUTTON_CLASSES[btnClassId] == 'special';
|
|
}
|
|
|
|
this.spec['sp'] = 3 + (flag ? 1 : 0);
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
if(classAsString) {
|
|
// KMW currently doesn't handle raw numbers for 'sp' properly.
|
|
this.spec['sp'] = ('' + this.spec['sp']) as keyboards.ButtonClass;
|
|
}
|
|
|
|
this.setButtonClass();
|
|
}
|
|
|
|
// "Frame key" - generally refers to non-linguistic keys on the keyboard
|
|
public isFrameKey(): boolean {
|
|
let classIndex = this.spec['sp'] || 0;
|
|
switch(OSKKey.BUTTON_CLASSES[classIndex]) {
|
|
case 'default':
|
|
case 'deadkey':
|
|
// Note: will (generally) include the spacebar.
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public allowsKeyTip(): boolean {
|
|
if(this.isFrameKey()) {
|
|
return false;
|
|
} else {
|
|
return !this.btn.classList.contains('kmw-spacebar');
|
|
}
|
|
}
|
|
|
|
public highlight(on: boolean) {
|
|
var classes=this.btn.classList;
|
|
|
|
if(on) {
|
|
if(!classes.contains(OSKKey.HIGHLIGHT_CLASS)) {
|
|
classes.add(OSKKey.HIGHLIGHT_CLASS);
|
|
}
|
|
} else {
|
|
classes.remove(OSKKey.HIGHLIGHT_CLASS);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
|
*
|
|
* @param {String} text The text to be rendered.
|
|
* @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.
|
|
*
|
|
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
|
* This version has been substantially modified to work for this particular application.
|
|
*/
|
|
static getTextMetrics(text: string, emScale: number, style: {fontFamily?: string, fontSize: string}): TextMetrics {
|
|
// Since we may mutate the incoming style, let's make sure to copy it first.
|
|
// Only the relevant properties, though.
|
|
style = {
|
|
fontFamily: style.fontFamily,
|
|
fontSize: style.fontSize
|
|
};
|
|
|
|
// A final fallback - having the right font selected makes a world of difference.
|
|
if(!style.fontFamily) {
|
|
style.fontFamily = getComputedStyle(document.body).fontFamily;
|
|
}
|
|
|
|
if(!style.fontSize || style.fontSize == "") {
|
|
style.fontSize = '1em';
|
|
}
|
|
|
|
let fontFamily = style.fontFamily;
|
|
let fontSpec = getFontSizeStyle(style.fontSize);
|
|
|
|
var fontSize: string;
|
|
if(fontSpec.absolute) {
|
|
// We've already got an exact size - use it!
|
|
fontSize = fontSpec.val + 'px';
|
|
} else {
|
|
fontSize = fontSpec.val * emScale + 'px';
|
|
}
|
|
|
|
// re-use canvas object for better performance
|
|
var canvas: HTMLCanvasElement = OSKKey.getTextMetrics['canvas'] ||
|
|
(OSKKey.getTextMetrics['canvas'] = document.createElement("canvas"));
|
|
var context = canvas.getContext("2d");
|
|
context.font = fontSize + " " + fontFamily;
|
|
var metrics = context.measureText(text);
|
|
|
|
return metrics;
|
|
}
|
|
|
|
/**
|
|
* Calculate the font size required for a key cap, scaling to fit longer text
|
|
* @param vkbd
|
|
* @param style specification for the desired base font size
|
|
* @param override if true, don't use the font spec from the button, just use the passed in spec
|
|
* @returns font size as a style string
|
|
*/
|
|
getIdealFontSize(vkbd: VisualKeyboard, style: {height?: string, fontFamily?: string, fontSize: string}, override?: boolean): string {
|
|
let buttonStyle = getComputedStyle(this.btn);
|
|
let keyWidth = parseFloat(buttonStyle.width);
|
|
let emScale = 1;
|
|
|
|
const originalSize = getFontSizeStyle(style.fontSize || '1em');
|
|
|
|
// Not yet available; it'll be handled in a later layout pass.
|
|
if(!buttonStyle.fontSize) {
|
|
// NOTE: preserves old behavior for use in documentation keyboards, for now.
|
|
// Once we no longer need to maintain this code block, we can drop all current
|
|
// method parameters safely.
|
|
//
|
|
// Recompute the new width for use in autoscaling calculations below, just in case.
|
|
emScale = vkbd.getKeyEmFontSize();
|
|
keyWidth = this.getKeyWidth(vkbd);
|
|
} else if(!override) {
|
|
// When available, just use computedStyle instead.
|
|
style = buttonStyle;
|
|
}
|
|
|
|
let fontSpec = getFontSizeStyle(style.fontSize || '1em');
|
|
let metrics = OSKKey.getTextMetrics(this.spec.text, emScale, style);
|
|
|
|
const MAX_X_PROPORTION = 0.90;
|
|
const MAX_Y_PROPORTION = 0.90;
|
|
const X_PADDING = 2;
|
|
const Y_PADDING = 2;
|
|
|
|
var fontHeight: number, keyHeight: number;
|
|
if(metrics.fontBoundingBoxAscent) {
|
|
fontHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
|
|
}
|
|
|
|
let textHeight = fontHeight ? fontHeight + Y_PADDING : 0;
|
|
if(style.height && style.height.indexOf('px') != -1) {
|
|
keyHeight = Number.parseFloat(style.height.substring(0, style.height.indexOf('px')));
|
|
}
|
|
|
|
let xProportion = (keyWidth * MAX_X_PROPORTION) / (metrics.width + X_PADDING); // How much of the key does the text want to take?
|
|
let yProportion = textHeight && keyHeight ? (keyHeight * MAX_Y_PROPORTION) / textHeight : undefined;
|
|
|
|
var proportion: number = xProportion;
|
|
if(yProportion && yProportion < xProportion) {
|
|
proportion = yProportion;
|
|
}
|
|
|
|
// Never upscale keys past the default - only downscale them.
|
|
// Proportion < 1: ratio of key width to (padded [loosely speaking]) text width
|
|
// maxProportion determines the 'padding' involved.
|
|
//
|
|
if(proportion < 1) {
|
|
if(originalSize.absolute) {
|
|
return proportion * fontSpec.val + 'px';
|
|
} else {
|
|
return proportion * originalSize.val + 'em';
|
|
}
|
|
} else {
|
|
if(originalSize.absolute) {
|
|
return fontSpec.val + 'px';
|
|
} else {
|
|
return originalSize.val + 'em';
|
|
}
|
|
}
|
|
}
|
|
|
|
getKeyWidth(vkbd: VisualKeyboard): number {
|
|
let key = this.spec as keyboards.ActiveKey;
|
|
return key.proportionalWidth * vkbd.width;
|
|
}
|
|
|
|
/**
|
|
* Replace default key names by special font codes for modifier keys
|
|
*
|
|
* @param {string} oldText
|
|
* @return {string}
|
|
**/
|
|
protected renameSpecialKey(oldText: string, vkbd: VisualKeyboard): string {
|
|
// If a 'special key' mapping exists for the text, replace it with its corresponding special OSK character.
|
|
switch(oldText) {
|
|
case '*ZWNJ*':
|
|
// Default ZWNJ symbol comes from iOS. We'd rather match the system defaults where
|
|
// possible / available though, and there's a different standard symbol on Android.
|
|
oldText = vkbd.device.OS == com.keyman.utils.OperatingSystem.Android ?
|
|
'*ZWNJAndroid*' :
|
|
'*ZWNJiOS*';
|
|
break;
|
|
case '*Enter*':
|
|
oldText = vkbd.isRTL ? '*RTLEnter*' : '*LTREnter*';
|
|
break;
|
|
case '*BkSp*':
|
|
oldText = vkbd.isRTL ? '*RTLBkSp*' : '*LTRBkSp*';
|
|
break;
|
|
default:
|
|
// do nothing.
|
|
}
|
|
|
|
let specialCodePUA = 0XE000 + VisualKeyboard.specialCharacters[oldText];
|
|
|
|
return VisualKeyboard.specialCharacters[oldText] ?
|
|
String.fromCharCode(specialCodePUA) :
|
|
oldText;
|
|
}
|
|
|
|
|
|
private unicodeKeyIdToString(id: string): string {
|
|
// This is similar to defaultOutput.ts:forUnicodeKeynames and could potentially
|
|
// be refactored in the future.
|
|
if(!id || id.substr(0,2) != 'U_') {
|
|
return null;
|
|
}
|
|
|
|
let result = '';
|
|
const codePoints = id.substr(2).split('_');
|
|
for(let codePoint of codePoints) {
|
|
const codePointValue = parseInt(codePoint, 16);
|
|
if (((0x0 <= codePointValue) && (codePointValue <= 0x1F)) || ((0x80 <= codePointValue) && (codePointValue <= 0x9F))) {
|
|
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;
|
|
}
|
|
|
|
// Produces a HTMLSpanElement with the key's actual text.
|
|
protected generateKeyText(vkbd: VisualKeyboard): HTMLSpanElement {
|
|
let spec = this.spec;
|
|
|
|
// Add OSK key labels
|
|
var keyText;
|
|
var t = document.createElement('span'), ts=t.style;
|
|
if(spec['text'] == null || spec['text'] == '') {
|
|
keyText='\xa0'; // default: nbsp.
|
|
if(typeof spec['id'] == 'string') {
|
|
// If the ID's Unicode-based, just use that code.
|
|
keyText = this.unicodeKeyIdToString(spec['id']) || keyText;
|
|
}
|
|
} else {
|
|
keyText=spec['text'];
|
|
|
|
// Unique layer-based transformation: SHIFT-TAB uses a different glyph.
|
|
if(keyText == '*Tab*' && this.layer == 'shift') {
|
|
keyText = '*TabLeft*';
|
|
}
|
|
}
|
|
|
|
t.className='kmw-key-text';
|
|
|
|
let specialText = this.renameSpecialKey(keyText, vkbd);
|
|
if(specialText != keyText) {
|
|
// The keyboard wants to use the code for a special glyph defined by the SpecialOSK font.
|
|
keyText = specialText;
|
|
spec['font'] = "SpecialOSK";
|
|
}
|
|
|
|
//Override font spec if set for this key in the layout
|
|
if(typeof spec['font'] == 'string' && spec['font'] != '') {
|
|
ts.fontFamily=spec['font'];
|
|
}
|
|
|
|
if(typeof spec['fontsize'] == 'string' && spec['fontsize'] != '') {
|
|
ts.fontSize=spec['fontsize'];
|
|
}
|
|
|
|
// For some reason, fonts will sometimes 'bug out' for the embedded iOS page if we
|
|
// instead assign fontFamily to the existing style 'ts'. (Occurs in iOS 12.)
|
|
let styleSpec: {fontFamily?: string, fontSize: string} = {fontSize: ts.fontSize};
|
|
|
|
if(ts.fontFamily) {
|
|
styleSpec.fontFamily = ts.fontFamily;
|
|
} else {
|
|
styleSpec.fontFamily = vkbd.fontFamily; // Helps with style sheet calculations.
|
|
}
|
|
|
|
// Check the key's display width - does the key visualize well?
|
|
let emScale = vkbd.getKeyEmFontSize();
|
|
var width: number = OSKKey.getTextMetrics(keyText, emScale, styleSpec).width;
|
|
if(width == 0 && keyText != '' && keyText != '\xa0') {
|
|
// Add the Unicode 'empty circle' as a base support for needy diacritics.
|
|
|
|
// Disabled by mcdurdin 2020-10-19; dotted circle display is inconsistent on iOS/Safari
|
|
// at least and doesn't combine with diacritic marks. For consistent display, it may be
|
|
// necessary to build a custom font that does not depend on renderer choices for base
|
|
// mark display -- e.g. create marks with custom base included, potentially even on PUA
|
|
// code points and use those in rendering the OSK. See #3039 for more details.
|
|
// keyText = '\u25cc' + keyText;
|
|
|
|
if(vkbd.isRTL) {
|
|
// Add the RTL marker to ensure it displays properly.
|
|
keyText = '\u200f' + keyText;
|
|
}
|
|
}
|
|
|
|
ts.fontSize = this.getIdealFontSize(vkbd, styleSpec);
|
|
|
|
// Finalize the key's text.
|
|
t.innerText = keyText;
|
|
|
|
return t;
|
|
}
|
|
|
|
public isUnderTouch(input: InputEventCoordinate): boolean {
|
|
let x = input.x;
|
|
let y = input.y;
|
|
|
|
let btn = this.btn;
|
|
let x0 = dom.Utils.getAbsoluteX(btn);
|
|
let y0 = dom.Utils.getAbsoluteY(btn);
|
|
let x1 = x0 + btn.offsetWidth;
|
|
let y1 = y0 + btn.offsetHeight;
|
|
|
|
return (x > x0 && x < x1 && y > y0 && y < y1);
|
|
}
|
|
|
|
public refreshLayout(vkbd: VisualKeyboard) {
|
|
if(this.label) { // space bar may not define the text span!
|
|
this.label.style.fontSize = this.getIdealFontSize(vkbd, this.btn.style);
|
|
}
|
|
}
|
|
}
|
|
}
|