namespace com.keyman.osk { type KeyDistribution = text.KeyDistribution; export class ActiveKey implements LayoutKey { 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: ActiveKey.DEFAULT_KEY_WIDTH.toString(), sp: '0', pad: ActiveKey.DEFAULT_PAD.toString() }; id?: string; width?: string; pad?: string; layer: string; displayLayer: string; nextlayer: "string"; proportionalX: number; proportionalWidth: number; static polyfill(key: LayoutKey, displayLayer: string) { // Add class functions to the existing layout object, allowing it to act as an ActiveLayout. let dummy = new ActiveKey(); for(let prop in dummy) { if(!key.hasOwnProperty(prop)) { key[prop] = dummy[prop]; } } let aKey = key as ActiveKey; aKey.displayLayer = displayLayer; aKey.layer = aKey.layer || displayLayer; } } class ActiveRow implements LayoutRow { // Identify key labels (e.g. *Shift*) that require the special OSK font static readonly SPECIAL_LABEL=/\*\w+\*/; id: string; key: ActiveKey[]; /** * Used for calculating fat-fingering offsets. */ proportionalY: number; private constructor() { } static polyfill(row: LayoutRow, displayLayer: string, totalWidth: number, proportionalY: number) { // Apply defaults, setting the width and other undefined properties for each key let keys=row['key']; for(let j=0; j keys[j]).proportionalX = (totalPercent + padPercent + (keyPercent/2)); ( keys[j]).proportionalWidth = keyPercent; totalPercent += padPercent+keyPercent; } // Allow for right OSK margin (15 layout units) let rightMargin = ActiveKey.DEFAULT_RIGHT_MARGIN/totalWidth; totalPercent += rightMargin; // If a single key, and padding is negative, add padding to right align the key if(keys.length == 1 && parseInt(keys[0]['pad'],10) < 0) { keyPercent=parseInt(keys[0]['width'],10)/totalWidth; keys[0]['widthpc']=keyPercent; totalPercent += keyPercent; keys[0]['padpc']=1-totalPercent; // compute center's default x-coord (used in headless modes) ( keys[0]).proportionalX = ((totalPercent - rightMargin) - keyPercent/2); ( keys[0]).proportionalWidth = keyPercent; } else if(keys.length > 0) { let j=keys.length-1; padPercent=parseInt(keys[j]['pad'],10)/totalWidth; keys[j]['padpc']=padPercent; totalPercent += padPercent; keys[j]['widthpc'] = keyPercent = 1-totalPercent; // compute center's default x-coord (used in headless modes) ( keys[j]).proportionalX = (1 - rightMargin) - keyPercent/2; ( keys[j]).proportionalWidth = keyPercent; } // 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; } populateKeyMap(map: {[keyId: string]: ActiveKey}) { this.key.forEach(function(key: ActiveKey) { if(key.id) { map[key.id] = key; } }); } } export class ActiveLayer implements LayoutLayer { row: ActiveRow[]; id: string; totalWidth: number; defaultKeyProportionalWidth: number; rowProportionalHeight: number; /** * Facilitates mapping key id strings to their specification objects. */ keyMap: {[keyId: string]: ActiveKey}; constructor() { } static polyfill(layer: LayoutLayer, formFactor: string) { layer.aligned=false; // Create a DIV for each row of the group let rows=layer['row']; // Calculate the maximum row width (in layout units) var totalWidth=0; for(let i=0; i totalWidth) { totalWidth = width; } } // Add default right margin if(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 probability, use a function parameter in place // of the formula in the loop below. for(let key in keyDists) { totalMass += keyProbs[key] = 1 / (keyDists[key] + 1e-6); // Prevent div-by-0 errors. } for(let key in keyProbs) { keyProbs[key] /= totalMass; } return keyProbs; } /** * Computes a squared 'pseudo-distance' for the touch from each key. (Not a proper metric.) * Intended for use in generating a probability distribution over the keys based on the touch input. * @param touchCoords A proportional (x, y) coordinate of the touch within the keyboard's geometry. * Should be within [0, 0] to [1, 1]. * @param kbdScaleRatio The ratio of the keyboard's horizontal scale to its vertical scale. * For a 400 x 200 keyboard, should be 2. */ private keyTouchDistances(touchCoords: {x: number, y: number}, kbdScaleRatio: number): {[keyId: string]: number} { let layer = this; let keyDists: {[keyId: string]: number} = {}; // This double-nested loop computes a pseudo-distance for the touch from each key. Quite useful for // generating a probability distribution. this.row.forEach(function(row: ActiveRow): void { row.key.forEach(function(key: ActiveKey): void { // If the key lacks an ID, just skip it. Sometimes used for padding. if(!key.id) { return; } // These represent the within-key distance of the touch from the key's center. // Both should be on the interval [0, 0.5]. let dx = Math.abs(touchCoords.x - key.proportionalX); let dy = Math.abs(touchCoords.y - row.proportionalY); // If the touch isn't within the key, these store the out-of-key distance // from the closest point on the key being checked. let distX: number, distY: number; if(dx > 0.5 * key.proportionalWidth) { distX = (dx - 0.5 * key.proportionalWidth); dx = 0.5; } else { distX = 0; dx /= key.proportionalWidth; } if(dy > 0.5 * layer.rowProportionalHeight) { distY = (dy - 0.5 * layer.rowProportionalHeight); dy = 0.5; } else { distY = 0; dy /= layer.rowProportionalHeight; } // Now that the differentials are computed, it's time to do distance scaling. // // For out-of-key distance, we scale the X component by the keyboard's aspect ratio // to get the actual out-of-key distance rather than proportional. distX *= kbdScaleRatio; // While the keys are rarely perfect squares, we map all within-key distance // to a square shape. (ALT/CMD should seem as close to SPACE as a 'B'.) // // For that square, we take the rowHeight as its edge lengths. distX += dx * layer.rowProportionalHeight; distY += dy * layer.rowProportionalHeight; let distance = distX * distX + distY * distY; keyDists[key.id] = distance; }); }); return keyDists; } 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 + '-', ''); } return this.keyMap[keyId]; } } export class ActiveLayout implements LayoutFormFactor{ layer: ActiveLayer[]; font: string; /** * Facilitates mapping layer id strings to their specification objects. */ layerMap: {[layerId: string]: ActiveLayer}; private constructor() { } getLayer(layerId: string): ActiveLayer { return this.layerMap[layerId]; } /** * * @param layout * @param formFactor */ static polyfill(layout: LayoutFormFactor, formFactor: string): ActiveLayout { if(layout == null) { throw new Error("Cannot build an ActiveLayout for a null specification."); } // Create a separate OSK div for each OSK layer, only one of which will ever be visible var n: number, i: number; var layers: LayoutLayer[], layer: LayoutLayer; let layerMap: {[layerId: string]: ActiveLayer} = {}; var rows: LayoutRow[]; layers=layout['layer']; // ***Delete any empty rows at the end added by compiler bug... for(n=0; n0; i--) { if(rows[i-1]['key'].length > 0) { break; } } if(i < rows.length) { rows.splice(i-rows.length,rows.length-i); } } // ...remove to here when compiler bug fixed *** for(n=0; n