Javascript  |  637行  |  20.75 KB

// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
(function(exports) {
  /**
   * Alignment options for a keyset.
   * @param {Object=} opt_keyset The keyset to calculate the dimensions for.
   *    Defaults to the current active keyset.
   */
  var AlignmentOptions = function(opt_keyset) {
    var keyboard = document.getElementById('keyboard');
    var keyset = opt_keyset || keyboard.activeKeyset;
    this.calculate(keyset);
  }

  AlignmentOptions.prototype = {
    /**
     * The width of a regular key in logical pixels.
     * @type {number}
     */
    keyWidth: 0,

    /**
     * The horizontal space between two keys in logical pixels.
     * @type {number}
     */
    pitchX: 0,

    /**
     * The vertical space between two keys in logical pixels.
     * @type {number}
     */
    pitchY: 0,

    /**
     * The width in logical pixels the row should expand within.
     * @type {number}
     */
    availableWidth: 0,

    /**
     * The x-coordinate in logical pixels of the left most edge of the keyset.
     * @type {number}
     */
    offsetLeft: 0,

    /**
     * The x-coordinate of the right most edge in logical pixels of the keyset.
     * @type {number}
     */
    offsetRight: 0,

    /**
     * The height in logical pixels of all keys.
     * @type {number}
     */
    keyHeight: 0,

    /**
     * The height in logical pixels the keyset should stretch to fit.
     * @type {number}
     */
    availableHeight: 0,

    /**
     * The y-coordinate in logical pixels of the top most edge of the keyset.
     * @type {number}
     */
    offsetTop: 0,

    /**
     * The y-coordinate in logical pixels of the bottom most edge of the keyset.
     * @type {number}
     */
    offsetBottom: 0,

    /**
     * The ideal width of the keyboard container.
     * @type {number}
     */
    width: 0,

    /**
     * The ideal height of the keyboard container.
     * @type {number}
     */
    height: 0,

    /**
     * Recalculates the alignment options for a specific keyset.
     * @param {Object} keyset The keyset to align.
     */
    calculate: function (keyset) {
      var rows = keyset.querySelectorAll('kb-row').array();
      // Pick candidate row. This is the row with the most keys.
      var row = rows[0];
      var candidateLength = rows[0].childElementCount;
      for (var i = 1; i < rows.length; i++) {
        if (rows[i].childElementCount > candidateLength &&
            rows[i].align == RowAlignment.STRETCH) {
          row = rows[i];
          candidateLength = rows[i].childElementCount;
        }
      }
      var allKeys = row.children;

      // Calculates widths first.
      // Weight of a single interspace.
      var pitches = keyset.pitch.split();
      var pitchWeightX;
      var pitchWeightY;
      pitchWeightX = parseFloat(pitches[0]);
      pitchWeightY = pitches.length < 2 ? pitchWeightX : parseFloat(pitch[1]);

      // Sum of all keys in the current row.
      var keyWeightSumX = 0;
      for (var i = 0; i < allKeys.length; i++) {
        keyWeightSumX += allKeys[i].weight;
      }

      var interspaceWeightSumX = (allKeys.length -1) * pitchWeightX;
      // Total weight of the row in X.
      var totalWeightX = keyWeightSumX + interspaceWeightSumX +
          keyset.weightLeft + keyset.weightRight;
      var keyAspectRatio = getKeyAspectRatio();
      var totalWeightY = (pitchWeightY * (rows.length - 1)) +
                         keyset.weightTop +
                         keyset.weightBottom;
      for (var i = 0; i < rows.length; i++) {
        totalWeightY += rows[i].weight / keyAspectRatio;
      }
      // Calculate width and height of the window.
      var bounds = exports.getKeyboardBounds();

      this.width = bounds.width;
      this.height = bounds.height;
      var pixelPerWeightX = bounds.width/totalWeightX;
      var pixelPerWeightY = bounds.height/totalWeightY;

      if (keyset.align == LayoutAlignment.CENTER) {
        if (totalWeightX/bounds.width < totalWeightY/bounds.height) {
          pixelPerWeightY = bounds.height/totalWeightY;
          pixelPerWeightX = pixelPerWeightY;
          this.width = Math.floor(pixelPerWeightX * totalWeightX)
        } else {
          pixelPerWeightX = bounds.width/totalWeightX;
          pixelPerWeightY = pixelPerWeightX;
          this.height = Math.floor(pixelPerWeightY * totalWeightY);
        }
      }
      // Calculate pitch.
      this.pitchX = Math.floor(pitchWeightX * pixelPerWeightX);
      this.pitchY = Math.floor(pitchWeightY * pixelPerWeightY);

      // Convert weight to pixels on x axis.
      this.keyWidth = Math.floor(DEFAULT_KEY_WEIGHT * pixelPerWeightX);
      var offsetLeft = Math.floor(keyset.weightLeft * pixelPerWeightX);
      var offsetRight = Math.floor(keyset.weightRight * pixelPerWeightX);
      this.availableWidth = this.width - offsetLeft - offsetRight;

      // Calculates weight to pixels on the y axis.
      var weightY = Math.floor(DEFAULT_KEY_WEIGHT / keyAspectRatio);
      this.keyHeight = Math.floor(weightY * pixelPerWeightY);
      var offsetTop = Math.floor(keyset.weightTop * pixelPerWeightY);
      var offsetBottom = Math.floor(keyset.weightBottom * pixelPerWeightY);
      this.availableHeight = this.height - offsetTop - offsetBottom;

      var dX = bounds.width - this.width;
      this.offsetLeft = offsetLeft + Math.floor(dX/2);
      this.offsetRight = offsetRight + Math.ceil(dX/2)

      var dY = bounds.height - this.height;
      this.offsetBottom = offsetBottom + dY;
      this.offsetTop = offsetTop;
    },
  };

  /**
   * A simple binary search.
   * @param {Array} array The array to search.
   * @param {number} start The start index.
   * @param {number} end The end index.
   * @param {Function<Object>:number} The test function used for searching.
   * @private
   * @return {number} The index of the search, or -1 if it was not found.
   */
  function binarySearch_(array, start, end, testFn) {
      if (start > end) {
        // No match found.
        return -1;
      }
      var mid = Math.floor((start+end)/2);
      var result = testFn(mid);
      if (result == 0)
        return mid;
      if (result < 0)
        return binarySearch_(array, start, mid - 1, testFn);
      else
        return binarySearch_(array, mid + 1, end, testFn);
  }

  /**
   * Calculate width and height of the window.
   * @private
   * @return {Array.<String, number>} The bounds of the keyboard container.
   */
  function getKeyboardBounds_() {
    return {
      "width": screen.width,
      "height": screen.height * DEFAULT_KEYBOARD_ASPECT_RATIO
    };
  }

  /**
   * Calculates the desired key aspect ratio based on screen size.
   * @return {number} The aspect ratio to use.
   */
  function getKeyAspectRatio() {
    return (screen.width > screen.height) ?
        KEY_ASPECT_RATIO_LANDSCAPE : KEY_ASPECT_RATIO_PORTRAIT;
  }

  /**
   * Callback function for when the window is resized.
   */
  var onResize = function() {
    var keyboard = $('keyboard');
    keyboard.stale = true;
    var keyset = keyboard.activeKeyset;
    if (keyset)
      realignAll();
  };

  /**
   * Updates a specific key to the position specified.
   * @param {Object} key The key to update.
   * @param {number} width The new width of the key.
   * @param {number} height The new height of the key.
   * @param {number} left The left corner of the key.
   * @param {number} top The top corner of the key.
   */
  function updateKey(key, width, height, left, top) {
    key.style.position = 'absolute';
    key.style.width = width + 'px';
    key.style.height = (height - KEY_PADDING_TOP - KEY_PADDING_BOTTOM) + 'px';
    key.style.left = left + 'px';
    key.style.top = (top + KEY_PADDING_TOP) + 'px';
  }

  /**
   * Returns the key closest to given x-coordinate
   * @param {Array.<kb-key>} allKeys Sorted array of all possible key
   *     candidates.
   * @param {number} x The x-coordinate.
   * @param {number} pitch The pitch of the row.
   * @param {boolean} alignLeft whether to search with respect to the left or
   *   or right edge.
   * @return {?kb-key}
   */
  function findClosestKey(allKeys, x, pitch, alignLeft) {
    // Test function.
    var testFn = function(i) {
      var ERROR_THRESH = 1;
      var key = allKeys[i];
      var left = parseFloat(key.style.left);
      if (!alignLeft)
        left += parseFloat(key.style.width);
      var deltaRight = 0.5*(parseFloat(key.style.width) + pitch)
      deltaLeft = 0.5 * pitch;
      if (i > 0)
        deltaLeft += 0.5*parseFloat(allKeys[i-1].style.width);
      var high = Math.ceil(left + deltaRight) + ERROR_THRESH;
      var low = Math.floor(left - deltaLeft) - ERROR_THRESH;
      if (x <= high && x >= low)
        return 0;
      return x >= high? 1 : -1;
    }
    var index = exports.binarySearch(allKeys, 0, allKeys.length -1, testFn);
    return index > 0 ? allKeys[index] : null;
  }

  /**
   * Redistributes the total width amongst the keys in the range provided.
   * @param {Array.<kb-key>} allKeys Ordered list of keys to stretch.
   * @param {AlignmentOptions} params Options for aligning the keyset.
   * @param {number} xOffset The x-coordinate of the key who's index is start.
   * @param {number} width The total extraneous width to distribute.
   * @param {number} keyHeight The height of each key.
   * @param {number} yOffset The y-coordinate of the top edge of the row.
   */
  function redistribute(allKeys, params, xOffset, width, keyHeight, yOffset) {
    var availableWidth = width - (allKeys.length - 1) * params.pitchX;
    var stretchWeight = 0;
    var nStretch = 0;
    for (var i = 0; i < allKeys.length; i++) {
      var key = allKeys[i];
      if (key.stretch) {
        stretchWeight += key.weight;
        nStretch++;
      } else if (key.weight == DEFAULT_KEY_WEIGHT) {
        availableWidth -= params.keyWidth;
      } else {
        availableWidth -=
            Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth);
      }
    }
    if (stretchWeight <= 0)
      console.error("Cannot stretch row without a stretchable key");
    // Rounding error to distribute.
    var pixelsPerWeight = availableWidth / stretchWeight;
    for (var i = 0; i < allKeys.length; i++) {
      var key = allKeys[i];
      var keyWidth = params.keyWidth;
      if (key.weight != DEFAULT_KEY_WEIGHT) {
        keyWidth =
            Math.floor(key.weight/DEFAULT_KEY_WEIGHT * params.keyWidth);
      }
      if (key.stretch) {
        nStretch--;
        if (nStretch > 0) {
          keyWidth = Math.floor(key.weight * pixelsPerWeight);
          availableWidth -= keyWidth;
        } else {
          keyWidth = availableWidth;
        }
      }
      updateKey(key, keyWidth, keyHeight, xOffset, yOffset)
      xOffset += keyWidth + params.pitchX;
    }
  }

  /**
   * Aligns a row such that the spacebar is perfectly aligned with the row above
   * it. A precondition is that all keys in this row can be stretched as needed.
   * @param {!kb-row} row The current row to be aligned.
   * @param {!kb-row} prevRow The row above the current row.
   * @param {!AlignmentOptions} params Options for aligning the keyset.
   * @param {number} keyHeight The height of the keys in this row.
   * @param {number} heightOffset The height offset caused by the rows above.
   */
  function realignSpacebarRow(row, prevRow, params, keyHeight, heightOffset) {
    var allKeys = row.children;
    var stretchWeightBeforeSpace = 0;
    var stretchBefore = 0;
    var stretchWeightAfterSpace = 0;
    var stretchAfter = 0;
    var spaceIndex = -1;

    for (var i=0; i< allKeys.length; i++) {
      if (spaceIndex == -1) {
        if (allKeys[i].classList.contains('space')) {
          spaceIndex = i;
          continue;
        } else {
          stretchWeightBeforeSpace += allKeys[i].weight;
          stretchBefore++;
        }
      } else {
        stretchWeightAfterSpace += allKeys[i].weight;
        stretchAfter++;
      }
    }
    if (spaceIndex == -1) {
      console.error("No spacebar found in this row.");
      return;
    }
    var totalWeight = stretchWeightBeforeSpace +
                      stretchWeightAfterSpace +
                      allKeys[spaceIndex].weight;
    var widthForKeys = params.availableWidth -
                       (params.pitchX * (allKeys.length - 1 ))
    // Number of pixels to assign per unit weight.
    var pixelsPerWeight = widthForKeys/totalWeight;
    // Predicted left edge of the space bar.
    var spacePredictedLeft = params.offsetLeft +
                          (spaceIndex * params.pitchX) +
                          (stretchWeightBeforeSpace * pixelsPerWeight);
    var prevRowKeys = prevRow.children;
    // Find closest keys to the spacebar in order to align it to them.
    var leftKey =
        findClosestKey(prevRowKeys, spacePredictedLeft, params.pitchX, true);

    var spacePredictedRight = spacePredictedLeft +
        allKeys[spaceIndex].weight * (params.keyWidth/100);

    var rightKey =
        findClosestKey(prevRowKeys, spacePredictedRight, params.pitchX, false);

    var yOffset = params.offsetTop + heightOffset;
    // Fix left side.
    var leftEdge = parseFloat(leftKey.style.left);
    var leftWidth = leftEdge - params.offsetLeft - params.pitchX;
    var leftKeys = allKeys.array().slice(0, spaceIndex);
    redistribute(leftKeys,
                 params,
                 params.offsetLeft,
                 leftWidth,
                 keyHeight,
                 yOffset);
    // Fix right side.
    var rightEdge = parseFloat(rightKey.style.left) +
        parseFloat(rightKey.style.width);
    var spacebarWidth = rightEdge - leftEdge;
    updateKey(allKeys[spaceIndex],
              spacebarWidth,
              keyHeight,
              leftEdge,
              yOffset);
    var rightWidth =
        params.availableWidth - (rightEdge - params.offsetLeft + params.pitchX);
    var rightKeys = allKeys.array().slice(spaceIndex + 1);
    redistribute(rightKeys,
                 params,
                 rightEdge + params.pitchX,//xOffset.
                 rightWidth,
                 keyHeight,
                 yOffset);
  }

  /**
   * Realigns a given row based on the parameters provided.
   * @param {!kb-row} row The row to realign.
   * @param {!AlignmentOptions} params The parameters used to align the keyset.
   * @param {number} keyHeight The height of the keys.
   * @param {number} heightOffset The offset caused by rows above it.
   */
  function realignRow(row, params, keyHeight, heightOffset) {
    var all = row.children;
    var nStretch = 0;
    var stretchWeightSum = 0;
    var allSum = 0;
    // Keeps track of where to distribute pixels caused by round off errors.
    var deltaWidth = [];
    for (var i = 0; i < all.length; i++) {
      deltaWidth.push(0)
      var key = all[i];
      if (key.weight == DEFAULT_KEY_WEIGHT){
        allSum += params.keyWidth;
      } else {
        var width =
          Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight);
        allSum += width;
      }
      if (!key.stretch)
        continue;
      nStretch++;
      stretchWeightSum += key.weight;
    }
    var nRegular = all.length - nStretch;
    // Extra space.
    var extra = params.availableWidth -
                allSum -
                (params.pitchX * (all.length -1));
    var xOffset = params.offsetLeft;

    var alignment = row.align;
    switch (alignment) {
      case RowAlignment.STRETCH:
        var extraPerWeight = extra/stretchWeightSum;
        for (var i = 0; i < all.length; i++) {
          if (!all[i].stretch)
            continue;
          var delta = Math.floor(all[i].weight * extraPerWeight);
          extra -= delta;
          deltaWidth[i] = delta;
          // All left-over pixels assigned to right most stretchable key.
          nStretch--;
          if (nStretch == 0)
            deltaWidth[i] += extra;
        }
        break;
      case RowAlignment.CENTER:
        xOffset += Math.floor(extra/2)
        break;
      case RowAlignment.RIGHT:
        xOffset += extra;
        break;
      default:
        break;
    };

    var yOffset = params.offsetTop + heightOffset;
    var left = xOffset;
    for (var i = 0; i < all.length; i++) {
      var key = all[i];
      var width = params.keyWidth;
      if (key.weight != DEFAULT_KEY_WEIGHT)
        width = Math.floor((params.keyWidth/DEFAULT_KEY_WEIGHT) * key.weight)
      width += deltaWidth[i];
      updateKey(key, width, keyHeight, left, yOffset)
      left += (width + params.pitchX);
    }
  }

  /**
   * Realigns the keysets in all layouts of the keyboard.
   */
  function realignAll() {
    resizeKeyboardContainer()
    var keyboard = $('keyboard');
    var layoutParams = {};
    var idToLayout = function(id) {
      var parts = id.split('-');
      parts.pop();
      return parts.join('-');
    }

    var keysets = keyboard.querySelectorAll('kb-keyset').array();
    for (var i=0; i< keysets.length; i++) {
      var keyset = keysets[i];
      var layout = idToLayout(keyset.id);
      // Caches the layouts size parameters since all keysets in the same layout
      // will have the same specs.
      if (!(layout in layoutParams))
        layoutParams[layout] = new AlignmentOptions(keyset);
      realignKeyset(keyset, layoutParams[layout]);
    }
    exports.recordKeysets();
  }

  /**
   * Realigns the keysets in the current layout of the keyboard.
   */
  function realign() {
    var keyboard = $('keyboard');
    var params = new AlignmentOptions();
    // Check if current window bounds are accurate.
    resizeKeyboardContainer(params)
    var layout = keyboard.layout;
    var keysets =
        keyboard.querySelectorAll('kb-keyset[id^=' + layout + ']').array();
    for (var i = 0; i<keysets.length ; i++) {
      realignKeyset(keysets[i], params);
    }
    keyboard.stale = false;
    exports.recordKeysets();
  }

  /**
   * Realigns a given keyset.
   * @param {Object} keyset The keyset to realign.
   * @param {!AlignmentOptions} params The parameters used to align the keyset.
   */
  function realignKeyset(keyset, params) {
    var rows = keyset.querySelectorAll('kb-row').array();
    keyset.style.fontSize = (params.availableHeight /
      FONT_SIZE_RATIO / rows.length) + 'px';
    var heightOffset = 0;
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var rowHeight =
          Math.floor(params.keyHeight * (row.weight / DEFAULT_KEY_WEIGHT));
      if (row.querySelector('.space') && (i > 1)) {
        realignSpacebarRow(row, rows[i-1], params, rowHeight, heightOffset)
      } else {
        realignRow(row, params, rowHeight, heightOffset);
      }
      heightOffset += (rowHeight + params.pitchY);
    }
  }

  /**
   * Resizes the keyboard container if needed.
   * @params {AlignmentOptions=} opt_params Optional parameters to use. Defaults
   *   to the parameters of the current active keyset.
   */
  function resizeKeyboardContainer(opt_params) {
    var params = opt_params ? opt_params : new AlignmentOptions();
    if (Math.abs(window.innerHeight - params.height) > RESIZE_THRESHOLD) {
      // Cannot resize more than 50% of screen height due to crbug.com/338829.
      window.resizeTo(params.width, params.height);
    }
  }

  addEventListener('resize', onResize);
  addEventListener('load', onResize);

  exports.getKeyboardBounds = getKeyboardBounds_;
  exports.binarySearch = binarySearch_;
  exports.realignAll = realignAll;
})(this);

/**
 * Recursively replace all kb-key-import elements with imported documents.
 * @param {!Document} content Document to process.
 */
function importHTML(content) {
  var dom = content.querySelector('template').createInstance();
  var keyImports = dom.querySelectorAll('kb-key-import');
  if (keyImports.length != 0) {
    keyImports.array().forEach(function(element) {
      if (element.importDoc(content)) {
        var generatedDom = importHTML(element.importDoc(content));
        element.parentNode.replaceChild(generatedDom, element);
      }
    });
  }
  return dom;
}

/**
  * Flatten the keysets which represents a keyboard layout.
  */
function flattenKeysets() {
  var keysets = $('keyboard').querySelectorAll('kb-keyset');
  if (keysets.length > 0) {
    keysets.array().forEach(function(element) {
      element.flattenKeyset();
    });
  }
}

function resolveAudio() {
  var keyboard = $('keyboard');
  keyboard.addSound(Sound.DEFAULT);
  var nodes = keyboard.querySelectorAll('[sound]').array();
  // Get id's of all unique sounds.
  for (var i = 0; i < nodes.length; i++) {
    var id = nodes[i].getAttribute('sound');
    keyboard.addSound(id);
  }
}

// Prevents all default actions of touch. Keyboard should use its own gesture
// recognizer.
addEventListener('touchstart', function(e) { e.preventDefault() });
addEventListener('touchend', function(e) { e.preventDefault() });
addEventListener('touchmove', function(e) { e.preventDefault() });
addEventListener('polymer-ready', function(e) {
  flattenKeysets();
  resolveAudio();
});
addEventListener('stateChange', function(e) {
  if (e.detail.value == $('keyboard').activeKeysetId)
    realignAll();
})