1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 (function(exports) { 5 /** 6 * Alignment options for a keyset. 7 * @param {Object=} opt_keyset The keyset to calculate the dimensions for. 8 * Defaults to the current active keyset. 9 */ 10 var AlignmentOptions = function(opt_keyset) { 11 var keyboard = document.getElementById('keyboard'); 12 var keyset = opt_keyset || keyboard.activeKeyset; 13 this.calculate(keyset); 14 } 15 16 AlignmentOptions.prototype = { 17 /** 18 * The width of a regular key in logical pixels. 19 * @type {number} 20 */ 21 keyWidth: 0, 22 23 /** 24 * The horizontal space between two keys in logical pixels. 25 * @type {number} 26 */ 27 pitchX: 0, 28 29 /** 30 * The vertical space between two keys in logical pixels. 31 * @type {number} 32 */ 33 pitchY: 0, 34 35 /** 36 * The width in logical pixels the row should expand within. 37 * @type {number} 38 */ 39 availableWidth: 0, 40 41 /** 42 * The x-coordinate in logical pixels of the left most edge of the keyset. 43 * @type {number} 44 */ 45 offsetLeft: 0, 46 47 /** 48 * The x-coordinate of the right most edge in logical pixels of the keyset. 49 * @type {number} 50 */ 51 offsetRight: 0, 52 53 /** 54 * The height in logical pixels of all keys. 55 * @type {number} 56 */ 57 keyHeight: 0, 58 59 /** 60 * The height in logical pixels the keyset should stretch to fit. 61 * @type {number} 62 */ 63 availableHeight: 0, 64 65 /** 66 * The y-coordinate in logical pixels of the top most edge of the keyset. 67 * @type {number} 68 */ 69 offsetTop: 0, 70 71 /** 72 * The y-coordinate in logical pixels of the bottom most edge of the keyset. 73 * @type {number} 74 */ 75 offsetBottom: 0, 76 77 /** 78 * The ideal width of the keyboard container. 79 * @type {number} 80 */ 81 width: 0, 82 83 /** 84 * The ideal height of the keyboard container. 85 * @type {number} 86 */ 87 height: 0, 88 89 /** 90 * Recalculates the alignment options for a specific keyset. 91 * @param {Object} keyset The keyset to align. 92 */ 93 calculate: function (keyset) { 94 var rows = keyset.querySelectorAll('kb-row').array(); 95 // Pick candidate row. This is the row with the most keys. 96 var row = rows[0]; 97 var candidateLength = rows[0].childElementCount; 98 for (var i = 1; i < rows.length; i++) { 99 if (rows[i].childElementCount > candidateLength && 100 rows[i].align == RowAlignment.STRETCH) { 101 row = rows[i]; 102 candidateLength = rows[i].childElementCount; 103 } 104 } 105 var allKeys = row.children; 106 107 // Calculates widths first. 108 // Weight of a single interspace. 109 var pitches = keyset.pitch.split(); 110 var pitchWeightX; 111 var pitchWeightY; 112 pitchWeightX = parseFloat(pitches[0]); 113 pitchWeightY = pitches.length < 2 ? pitchWeightX : parseFloat(pitch[1]); 114 115 // Sum of all keys in the current row. 116 var keyWeightSumX = 0; 117 for (var i = 0; i < allKeys.length; i++) { 118 keyWeightSumX += allKeys[i].weight; 119 } 120 121 var interspaceWeightSumX = (allKeys.length -1) * pitchWeightX; 122 // Total weight of the row in X. 123 var totalWeightX = keyWeightSumX + interspaceWeightSumX + 124 keyset.weightLeft + keyset.weightRight; 125 var keyAspectRatio = getKeyAspectRatio(); 126 var totalWeightY = (pitchWeightY * (rows.length - 1)) + 127 keyset.weightTop + 128 keyset.weightBottom; 129 for (var i = 0; i < rows.length; i++) { 130 totalWeightY += rows[i].weight / keyAspectRatio; 131 } 132 // Calculate width and height of the window. 133 var bounds = exports.getKeyboardBounds(); 134 135 this.width = bounds.width; 136 this.height = bounds.height; 137 var pixelPerWeightX = bounds.width/totalWeightX; 138 var pixelPerWeightY = bounds.height/totalWeightY; 139 140 if (keyset.align == LayoutAlignment.CENTER) { 141 if (totalWeightX/bounds.width < totalWeightY/bounds.height) { 142 pixelPerWeightY = bounds.height/totalWeightY; 143 pixelPerWeightX = pixelPerWeightY; 144 this.width = Math.floor(pixelPerWeightX * totalWeightX) 145 } else { 146 pixelPerWeightX = bounds.width/totalWeightX; 147 pixelPerWeightY = pixelPerWeightX; 148 this.height = Math.floor(pixelPerWeightY * totalWeightY); 149 } 150 } 151 // Calculate pitch. 152 this.pitchX = Math.floor(pitchWeightX * pixelPerWeightX); 153 this.pitchY = Math.floor(pitchWeightY * pixelPerWeightY); 154 155 // Convert weight to pixels on x axis. 156 this.keyWidth = Math.floor(DEFAULT_KEY_WEIGHT * pixelPerWeightX); 157 var offsetLeft = Math.floor(keyset.weightLeft * pixelPerWeightX); 158 var offsetRight = Math.floor(keyset.weightRight * pixelPerWeightX); 159 this.availableWidth = this.width - offsetLeft - offsetRight; 160 161 // Calculates weight to pixels on the y axis. 162 var weightY = Math.floor(DEFAULT_KEY_WEIGHT / keyAspectRatio); 163 this.keyHeight = Math.floor(weightY * pixelPerWeightY); 164 var offsetTop = Math.floor(keyset.weightTop * pixelPerWeightY); 165 var offsetBottom = Math.floor(keyset.weightBottom * pixelPerWeightY); 166 this.availableHeight = this.height - offsetTop - offsetBottom; 167 168 var dX = bounds.width - this.width; 169 this.offsetLeft = offsetLeft + Math.floor(dX/2); 170 this.offsetRight = offsetRight + Math.ceil(dX/2) 171 172 var dY = bounds.height - this.height; 173 this.offsetBottom = offsetBottom + dY; 174 this.offsetTop = offsetTop; 175 }, 176 }; 177 178 /** 179 * A simple binary search. 180 * @param {Array} array The array to search. 181 * @param {number} start The start index. 182 * @param {number} end The end index. 183 * @param {Function<Object>:number} The test function used for searching. 184 * @private 185 * @return {number} The index of the search, or -1 if it was not found. 186 */ 187 function binarySearch_(array, start, end, testFn) { 188 if (start > end) { 189 // No match found. 190 return -1; 191 } 192 var mid = Math.floor((start+end)/2); 193 var result = testFn(mid); 194 if (result == 0) 195 return mid; 196 if (result < 0) 197 return binarySearch_(array, start, mid - 1, testFn); 198 else 199 return binarySearch_(array, mid + 1, end, testFn); 200 } 201 202 /** 203 * Calculate width and height of the window. 204 * @private 205 * @return {Array.<String, number>} The bounds of the keyboard container. 206 */ 207 function getKeyboardBounds_() { 208 return { 209 "width": screen.width, 210 "height": screen.height * DEFAULT_KEYBOARD_ASPECT_RATIO 211 }; 212 } 213 214 /** 215 * Calculates the desired key aspect ratio based on screen size. 216 * @return {number} The aspect ratio to use. 217 */ 218 function getKeyAspectRatio() { 219 return (screen.width > screen.height) ? 220 KEY_ASPECT_RATIO_LANDSCAPE : KEY_ASPECT_RATIO_PORTRAIT; 221 } 222 223 /** 224 * Callback function for when the window is resized. 225 */ 226 var onResize = function() { 227 var keyboard = $('keyboard'); 228 keyboard.stale = true; 229 var keyset = keyboard.activeKeyset; 230 if (keyset) 231 realignAll(); 232 }; 233 234 /** 235 * Updates a specific key to the position specified. 236 * @param {Object} key The key to update. 237 * @param {number} width The new width of the key. 238 * @param {number} height The new height of the key. 239 * @param {number} left The left corner of the key. 240 * @param {number} top The top corner of the key. 241 */ 242 function updateKey(key, width, height, left, top) { 243 key.style.position = 'absolute'; 244 key.style.width = width + 'px'; 245 key.style.height = (height - KEY_PADDING_TOP - KEY_PADDING_BOTTOM) + 'px'; 246 key.style.left = left + 'px'; 247 key.style.top = (top + KEY_PADDING_TOP) + 'px'; 248 } 249 250 /** 251 * Returns the key closest to given x-coordinate 252 * @param {Array.<kb-key>} allKeys Sorted array of all possible key 253 * candidates. 254 * @param {number} x The x-coordinate. 255 * @param {number} pitch The pitch of the row. 256 * @param {boolean} alignLeft whether to search with respect to the left or 257 * or right edge. 258 * @return {?kb-key} 259 */ 260 function findClosestKey(allKeys, x, pitch, alignLeft) { 261 // Test function. 262 var testFn = function(i) { 263 var ERROR_THRESH = 1; 264 var key = allKeys[i]; 265 var left = parseFloat(key.style.left); 266 if (!alignLeft) 267 left += parseFloat(key.style.width); 268 var deltaRight = 0.5*(parseFloat(key.style.width) + pitch) 269 deltaLeft = 0.5 * pitch; 270 if (i > 0) 271 deltaLeft += 0.5*parseFloat(allKeys[i-1].style.width); 272 var high = Math.ceil(left + deltaRight) + ERROR_THRESH; 273 var low = Math.floor(left - deltaLeft) - ERROR_THRESH; 274 if (x <= high && x >= low) 275 return 0; 276 return x >= high? 1 : -1; 277 } 278 var index = exports.binarySearch(allKeys, 0, allKeys.length -1, testFn); 279 return index > 0 ? allKeys[index] : null; 280 } 281 282 /** 283 * Redistributes the total width amongst the keys in the range provided. 284 * @param {Array.<kb-key>} allKeys Ordered list of keys to stretch. 285 * @param {AlignmentOptions} params Options for aligning the keyset. 286 * @param {number} xOffset The x-coordinate of the key who's index is start. 287 * @param {number} width The total extraneous width to distribute. 288 * @param {number} keyHeight The height of each key. 289 * @param {number} yOffset The y-coordinate of the top edge of the row. 290 */ 291 function redistribute(allKeys, params, xOffset, width, keyHeight, yOffset) { 292 var availableWidth = width - (allKeys.length - 1) * params.pitchX; 293 var stretchWeight = 0; 294 var nStretch = 0; 295 for (var i = 0; i < allKeys.length; i++) { 296 var key = allKeys[i]; 297 if (key.stretch) { 298 stretchWeight += key.weight; 299 nStretch++; 300 } else if (key.weight == DEFAULT_KEY_WEIGHT) { 301 availableWidth -= params.keyWidth; 302 } else { 303 availableWidth -= 304 Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth); 305 } 306 } 307 if (stretchWeight <= 0) 308 console.error("Cannot stretch row without a stretchable key"); 309 // Rounding error to distribute. 310 var pixelsPerWeight = availableWidth / stretchWeight; 311 for (var i = 0; i < allKeys.length; i++) { 312 var key = allKeys[i]; 313 var keyWidth = params.keyWidth; 314 if (key.weight != DEFAULT_KEY_WEIGHT) { 315 keyWidth = 316 Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth); 317 } 318 if (key.stretch) { 319 nStretch--; 320 if (nStretch > 0) { 321 keyWidth = Math.floor(key.weight * pixelsPerWeight); 322 availableWidth -= keyWidth; 323 } else { 324 keyWidth = availableWidth; 325 } 326 } 327 updateKey(key, keyWidth, keyHeight, xOffset, yOffset) 328 xOffset += keyWidth + params.pitchX; 329 } 330 } 331 332 /** 333 * Aligns a row such that the spacebar is perfectly aligned with the row above 334 * it. A precondition is that all keys in this row can be stretched as needed. 335 * @param {!kb-row} row The current row to be aligned. 336 * @param {!kb-row} prevRow The row above the current row. 337 * @param {!AlignmentOptions} params Options for aligning the keyset. 338 * @param {number} keyHeight The height of the keys in this row. 339 * @param {number} heightOffset The height offset caused by the rows above. 340 */ 341 function realignSpacebarRow(row, prevRow, params, keyHeight, heightOffset) { 342 var allKeys = row.children; 343 var stretchWeightBeforeSpace = 0; 344 var stretchBefore = 0; 345 var stretchWeightAfterSpace = 0; 346 var stretchAfter = 0; 347 var spaceIndex = -1; 348 349 for (var i=0; i< allKeys.length; i++) { 350 if (spaceIndex == -1) { 351 if (allKeys[i].classList.contains('space')) { 352 spaceIndex = i; 353 continue; 354 } else { 355 stretchWeightBeforeSpace += allKeys[i].weight; 356 stretchBefore++; 357 } 358 } else { 359 stretchWeightAfterSpace += allKeys[i].weight; 360 stretchAfter++; 361 } 362 } 363 if (spaceIndex == -1) { 364 console.error("No spacebar found in this row."); 365 return; 366 } 367 var totalWeight = stretchWeightBeforeSpace + 368 stretchWeightAfterSpace + 369 allKeys[spaceIndex].weight; 370 var widthForKeys = params.availableWidth - 371 (params.pitchX * (allKeys.length - 1 )) 372 // Number of pixels to assign per unit weight. 373 var pixelsPerWeight = widthForKeys/totalWeight; 374 // Predicted left edge of the space bar. 375 var spacePredictedLeft = params.offsetLeft + 376 (spaceIndex * params.pitchX) + 377 (stretchWeightBeforeSpace * pixelsPerWeight); 378 var prevRowKeys = prevRow.children; 379 // Find closest keys to the spacebar in order to align it to them. 380 var leftKey = 381 findClosestKey(prevRowKeys, spacePredictedLeft, params.pitchX, true); 382 383 var spacePredictedRight = spacePredictedLeft + 384 allKeys[spaceIndex].weight * (params.keyWidth/100); 385 386 var rightKey = 387 findClosestKey(prevRowKeys, spacePredictedRight, params.pitchX, false); 388 389 var yOffset = params.offsetTop + heightOffset; 390 // Fix left side. 391 var leftEdge = parseFloat(leftKey.style.left); 392 var leftWidth = leftEdge - params.offsetLeft - params.pitchX; 393 var leftKeys = allKeys.array().slice(0, spaceIndex); 394 redistribute(leftKeys, 395 params, 396 params.offsetLeft, 397 leftWidth, 398 keyHeight, 399 yOffset); 400 // Fix right side. 401 var rightEdge = parseFloat(rightKey.style.left) + 402 parseFloat(rightKey.style.width); 403 var spacebarWidth = rightEdge - leftEdge; 404 updateKey(allKeys[spaceIndex], 405 spacebarWidth, 406 keyHeight, 407 leftEdge, 408 yOffset); 409 var rightWidth = 410 params.availableWidth - (rightEdge - params.offsetLeft + params.pitchX); 411 var rightKeys = allKeys.array().slice(spaceIndex + 1); 412 redistribute(rightKeys, 413 params, 414 rightEdge + params.pitchX,//xOffset. 415 rightWidth, 416 keyHeight, 417 yOffset); 418 } 419 420 /** 421 * Realigns a given row based on the parameters provided. 422 * @param {!kb-row} row The row to realign. 423 * @param {!AlignmentOptions} params The parameters used to align the keyset. 424 * @param {number} keyHeight The height of the keys. 425 * @param {number} heightOffset The offset caused by rows above it. 426 */ 427 function realignRow(row, params, keyHeight, heightOffset) { 428 var all = row.children; 429 var nStretch = 0; 430 var stretchWeightSum = 0; 431 var allSum = 0; 432 // Keeps track of where to distribute pixels caused by round off errors. 433 var deltaWidth = []; 434 for (var i = 0; i < all.length; i++) { 435 deltaWidth.push(0) 436 var key = all[i]; 437 if (key.weight == DEFAULT_KEY_WEIGHT){ 438 allSum += params.keyWidth; 439 } else { 440 var width = 441 Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight); 442 allSum += width; 443 } 444 if (!key.stretch) 445 continue; 446 nStretch++; 447 stretchWeightSum += key.weight; 448 } 449 var nRegular = all.length - nStretch; 450 // Extra space. 451 var extra = params.availableWidth - 452 allSum - 453 (params.pitchX * (all.length -1)); 454 var xOffset = params.offsetLeft; 455 456 var alignment = row.align; 457 switch (alignment) { 458 case RowAlignment.STRETCH: 459 var extraPerWeight = extra/stretchWeightSum; 460 for (var i = 0; i < all.length; i++) { 461 if (!all[i].stretch) 462 continue; 463 var delta = Math.floor(all[i].weight * extraPerWeight); 464 extra -= delta; 465 deltaWidth[i] = delta; 466 // All left-over pixels assigned to right most stretchable key. 467 nStretch--; 468 if (nStretch == 0) 469 deltaWidth[i] += extra; 470 } 471 break; 472 case RowAlignment.CENTER: 473 xOffset += Math.floor(extra/2) 474 break; 475 case RowAlignment.RIGHT: 476 xOffset += extra; 477 break; 478 default: 479 break; 480 }; 481 482 var yOffset = params.offsetTop + heightOffset; 483 var left = xOffset; 484 for (var i = 0; i < all.length; i++) { 485 var key = all[i]; 486 var width = params.keyWidth; 487 if (key.weight != DEFAULT_KEY_WEIGHT) 488 width = Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight) 489 width += deltaWidth[i]; 490 updateKey(key, width, keyHeight, left, yOffset) 491 left += (width + params.pitchX); 492 } 493 } 494 495 /** 496 * Realigns the keysets in all layouts of the keyboard. 497 */ 498 function realignAll() { 499 resizeKeyboardContainer() 500 var keyboard = $('keyboard'); 501 var layoutParams = {}; 502 var idToLayout = function(id) { 503 var parts = id.split('-'); 504 parts.pop(); 505 return parts.join('-'); 506 } 507 508 var keysets = keyboard.querySelectorAll('kb-keyset').array(); 509 for (var i=0; i< keysets.length; i++) { 510 var keyset = keysets[i]; 511 var layout = idToLayout(keyset.id); 512 // Caches the layouts size parameters since all keysets in the same layout 513 // will have the same specs. 514 if (!(layout in layoutParams)) 515 layoutParams[layout] = new AlignmentOptions(keyset); 516 realignKeyset(keyset, layoutParams[layout]); 517 } 518 exports.recordKeysets(); 519 } 520 521 /** 522 * Realigns the keysets in the current layout of the keyboard. 523 */ 524 function realign() { 525 var keyboard = $('keyboard'); 526 var params = new AlignmentOptions(); 527 // Check if current window bounds are accurate. 528 resizeKeyboardContainer(params) 529 var layout = keyboard.layout; 530 var keysets = 531 keyboard.querySelectorAll('kb-keyset[id^=' + layout + ']').array(); 532 for (var i = 0; i<keysets.length ; i++) { 533 realignKeyset(keysets[i], params); 534 } 535 keyboard.stale = false; 536 exports.recordKeysets(); 537 } 538 539 /** 540 * Realigns a given keyset. 541 * @param {Object} keyset The keyset to realign. 542 * @param {!AlignmentOptions} params The parameters used to align the keyset. 543 */ 544 function realignKeyset(keyset, params) { 545 var rows = keyset.querySelectorAll('kb-row').array(); 546 keyset.style.fontSize = (params.availableHeight / 547 FONT_SIZE_RATIO / rows.length) + 'px'; 548 var heightOffset = 0; 549 for (var i = 0; i < rows.length; i++) { 550 var row = rows[i]; 551 var rowHeight = 552 Math.floor(params.keyHeight * (row.weight / DEFAULT_KEY_WEIGHT)); 553 if (row.querySelector('.space') && (i > 1)) { 554 realignSpacebarRow(row, rows[i-1], params, rowHeight, heightOffset) 555 } else { 556 realignRow(row, params, rowHeight, heightOffset); 557 } 558 heightOffset += (rowHeight + params.pitchY); 559 } 560 } 561 562 /** 563 * Resizes the keyboard container if needed. 564 * @params {AlignmentOptions=} opt_params Optional parameters to use. Defaults 565 * to the parameters of the current active keyset. 566 */ 567 function resizeKeyboardContainer(opt_params) { 568 var params = opt_params ? opt_params : new AlignmentOptions(); 569 if (Math.abs(window.innerHeight - params.height) > RESIZE_THRESHOLD) { 570 // Cannot resize more than 50% of screen height due to crbug.com/338829. 571 window.resizeTo(params.width, params.height); 572 } 573 } 574 575 addEventListener('resize', onResize); 576 addEventListener('load', onResize); 577 578 exports.getKeyboardBounds = getKeyboardBounds_; 579 exports.binarySearch = binarySearch_; 580 exports.realignAll = realignAll; 581 })(this); 582 583 /** 584 * Recursively replace all kb-key-import elements with imported documents. 585 * @param {!Document} content Document to process. 586 */ 587 function importHTML(content) { 588 var dom = content.querySelector('template').createInstance(); 589 var keyImports = dom.querySelectorAll('kb-key-import'); 590 if (keyImports.length != 0) { 591 keyImports.array().forEach(function(element) { 592 if (element.importDoc(content)) { 593 var generatedDom = importHTML(element.importDoc(content)); 594 element.parentNode.replaceChild(generatedDom, element); 595 } 596 }); 597 } 598 return dom; 599 } 600 601 /** 602 * Flatten the keysets which represents a keyboard layout. 603 */ 604 function flattenKeysets() { 605 var keysets = $('keyboard').querySelectorAll('kb-keyset'); 606 if (keysets.length > 0) { 607 keysets.array().forEach(function(element) { 608 element.flattenKeyset(); 609 }); 610 } 611 } 612 613 function resolveAudio() { 614 var keyboard = $('keyboard'); 615 keyboard.addSound(Sound.DEFAULT); 616 var nodes = keyboard.querySelectorAll('[sound]').array(); 617 // Get id's of all unique sounds. 618 for (var i = 0; i < nodes.length; i++) { 619 var id = nodes[i].getAttribute('sound'); 620 keyboard.addSound(id); 621 } 622 } 623 624 // Prevents all default actions of touch. Keyboard should use its own gesture 625 // recognizer. 626 addEventListener('touchstart', function(e) { e.preventDefault() }); 627 addEventListener('touchend', function(e) { e.preventDefault() }); 628 addEventListener('touchmove', function(e) { e.preventDefault() }); 629 addEventListener('polymer-ready', function(e) { 630 flattenKeysets(); 631 resolveAudio(); 632 }); 633 addEventListener('stateChange', function(e) { 634 if (e.detail.value == $('keyboard').activeKeysetId) 635 realignAll(); 636 }) 637