toolkit/modules/ShortcutUtils.jsm
author Edwin Gao <egao@mozilla.com>
Tue, 19 Mar 2019 01:12:28 +0000
changeset 465083 4753b348fce8eb228389cc97994af1c5293e8a74
parent 463528 4f506f3861997bfcea12588a0a724c54b5807ec6
child 481426 e5be4c59b7f15f98fabb32a68fc64050ddb62bcb
permissions -rw-r--r--
Bug 1536211 - disable tests in netwerk/test/unit/ that requires http/2 server r=jmaher Disable the following tests, with 1536211 serving as the tracking bug: - test_header_Server_Timing.js (1530871) - test_trr.js (1532395) - test_esni_dns_fetch.js (1530752) Differential Revision: https://phabricator.services.mozilla.com/D23922

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["ShortcutUtils"];

const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyModuleGetters(this, {
  AppConstants: "resource://gre/modules/AppConstants.jsm",
});

XPCOMUtils.defineLazyGetter(this, "PlatformKeys", function() {
  return Services.strings.createBundle(
    "chrome://global-platform/locale/platformKeys.properties");
});

XPCOMUtils.defineLazyGetter(this, "Keys", function() {
  return Services.strings.createBundle(
    "chrome://global/locale/keys.properties");
});

var ShortcutUtils = {
  IS_VALID: "valid",
  INVALID_KEY: "invalid_key",
  INVALID_MODIFIER: "invalid_modifier",
  INVALID_COMBINATION: "invalid_combination",
  DUPLICATE_MODIFIER: "duplicate_modifier",
  MODIFIER_REQUIRED: "modifier_required",

  MOVE_TAB_FORWARD: "MOVE_TAB_FORWARD",
  MOVE_TAB_BACKWARD: "MOVE_TAB_BACKWARD",
  CLOSE_TAB: "CLOSE_TAB",
  CYCLE_TABS: "CYCLE_TABS",
  PREVIOUS_TAB: "PREVIOUS_TAB",
  NEXT_TAB: "NEXT_TAB",

  /**
    * Prettifies the modifier keys for an element.
    *
    * @param Node aElemKey
    *        The key element to get the modifiers from.
    * @param boolean aNoCloverLeaf
    *        Pass true to use a descriptive string instead of the cloverleaf symbol. (OS X only)
    * @return string
    *         A prettified and properly separated modifier keys string.
    */
  prettifyShortcut(aElemKey, aNoCloverLeaf) {
    let elemString = this.getModifierString(
      aElemKey.getAttribute("modifiers"),
      aNoCloverLeaf);
    let key = this.getKeyString(
      aElemKey.getAttribute("keycode"),
      aElemKey.getAttribute("key"));
    return elemString + key;
  },

  getModifierString(elemMod, aNoCloverLeaf) {
    let elemString = "";
    let haveCloverLeaf = false;

    if (elemMod.match("accel")) {
      if (Services.appinfo.OS == "Darwin") {
        // XXX bug 779642 Use "Cmd-" literal vs. cloverleaf meta-key until
        // Orion adds variable height lines.
        if (aNoCloverLeaf) {
          elemString += "Cmd-";
        } else {
          haveCloverLeaf = true;
        }
      } else {
        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
          PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
      }
    }
    if (elemMod.match("access")) {
      if (Services.appinfo.OS == "Darwin") {
        elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
          PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
      } else {
        elemString += PlatformKeys.GetStringFromName("VK_ALT") +
          PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
      }
    }
    if (elemMod.match("os")) {
      elemString += PlatformKeys.GetStringFromName("VK_WIN") +
        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
    }
    if (elemMod.match("shift")) {
      elemString += PlatformKeys.GetStringFromName("VK_SHIFT") +
        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
    }
    if (elemMod.match("alt")) {
      elemString += PlatformKeys.GetStringFromName("VK_ALT") +
        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
    }
    if (elemMod.match("ctrl") || elemMod.match("control")) {
      elemString += PlatformKeys.GetStringFromName("VK_CONTROL") +
        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
    }
    if (elemMod.match("meta")) {
      elemString += PlatformKeys.GetStringFromName("VK_META") +
        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
    }

    if (haveCloverLeaf) {
      elemString += PlatformKeys.GetStringFromName("VK_META") +
        PlatformKeys.GetStringFromName("MODIFIER_SEPARATOR");
    }

    return elemString;
  },

  getKeyString(keyCode, keyAttribute) {
    let key;
    if (keyCode) {
      keyCode = keyCode.toUpperCase();
      try {
        let bundle = keyCode == "VK_RETURN" ? PlatformKeys : Keys;
        // Some keys might not exist in the locale file, which will throw.
        key = bundle.GetStringFromName(keyCode);
      } catch (ex) {
        Cu.reportError("Error finding " + keyCode + ": " + ex);
        key = keyCode.replace(/^VK_/, "");
      }
    } else {
      key = keyAttribute.toUpperCase();
    }

    return key;
  },

  getKeyAttribute(chromeKey) {
    if (/^[A-Z]$/.test(chromeKey)) {
      // We use the key attribute for single characters.
      return ["key", chromeKey];
    }
    return ["keycode", this.getKeycodeAttribute(chromeKey)];
  },

  /**
   * Determines the corresponding XUL keycode from the given chrome key.
   *
   * For example:
   *
   *    input     |  output
   *    ---------------------------------------
   *    "PageUp"  |  "VK_PAGE_UP"
   *    "Delete"  |  "VK_DELETE"
   *
   * @param {string} chromeKey The chrome key (e.g. "PageUp", "Space", ...)
   * @returns {string} The constructed value for the Key's 'keycode' attribute.
   */
  getKeycodeAttribute(chromeKey) {
    if (/^[0-9]/.test(chromeKey)) {
      return `VK_${chromeKey}`;
    }
    return `VK${chromeKey.replace(/([A-Z])/g, "_$&").toUpperCase()}`;
  },

  findShortcut(aElemCommand) {
    let document = aElemCommand.ownerDocument;
    return document.querySelector("key[command=\"" + aElemCommand.getAttribute("id") + "\"]");
  },

  chromeModifierKeyMap: {
    "Alt": "alt",
    "Command": "accel",
    "Ctrl": "accel",
    "MacCtrl": "control",
    "Shift": "shift",
  },

  /**
   * Determines the corresponding XUL modifiers from the chrome modifiers.
   *
   * For example:
   *
   *    input             |   output
   *    ---------------------------------------
   *    ["Ctrl", "Shift"] |   "accel,shift"
   *    ["MacCtrl"]       |   "control"
   *
   * @param {Array} chromeModifiers The array of chrome modifiers.
   * @returns {string} The constructed value for the Key's 'modifiers' attribute.
   */
  getModifiersAttribute(chromeModifiers) {
    return Array.from(chromeModifiers, modifier => {
      return ShortcutUtils.chromeModifierKeyMap[modifier];
    }).sort().join(",");
  },

  /**
   * Validate if a shortcut string is valid and return an error code if it
   * isn't valid.
   *
   * For example:
   *
   *    input            |   output
   *    ---------------------------------------
   *    "Ctrl+Shift+A"   |   IS_VALID
   *    "Shift+F"        |   MODIFIER_REQUIRED
   *    "Command+>"      |   INVALID_KEY
   *
   * @param {string} string The shortcut string.
   * @returns {string} The code for the validation result.
   */
  validate(string) {
    // A valid shortcut key for a webextension manifest
    const MEDIA_KEYS = /^(MediaNextTrack|MediaPlayPause|MediaPrevTrack|MediaStop)$/;
    const BASIC_KEYS = /^([A-Z0-9]|Comma|Period|Home|End|PageUp|PageDown|Space|Insert|Delete|Up|Down|Left|Right)$/;
    const FUNCTION_KEYS = /^(F[1-9]|F1[0-2])$/;

    if (MEDIA_KEYS.test(string.trim())) {
      return this.IS_VALID;
    }

    let modifiers = string.split("+").map(s => s.trim());
    let key = modifiers.pop();

    let chromeModifiers = modifiers.map(m => ShortcutUtils.chromeModifierKeyMap[m]);
    // If the modifier wasn't found it will be undefined.
    if (chromeModifiers.some(modifier => !modifier)) {
      return this.INVALID_MODIFIER;
    }

    switch (modifiers.length) {
      case 0:
        // A lack of modifiers is only allowed with function keys.
        if (!FUNCTION_KEYS.test(key)) {
          return this.MODIFIER_REQUIRED;
        }
        break;
      case 1:
        // Shift is only allowed on its own with function keys.
        if (chromeModifiers[0] == "shift" && !FUNCTION_KEYS.test(key)) {
          return this.MODIFIER_REQUIRED;
        }
        break;
      case 2:
        if (chromeModifiers[0] == chromeModifiers[1]) {
          return this.DUPLICATE_MODIFIER;
        }
        break;
      default:
        return this.INVALID_COMBINATION;
    }

    if (!BASIC_KEYS.test(key) && !FUNCTION_KEYS.test(key)) {
      return this.INVALID_KEY;
    }

    return this.IS_VALID;
  },

  /**
   * Attempt to find a key for a given shortcut string, such as
   * "Ctrl+Shift+A" and determine if it is a system shortcut.
   *
   * @param {Object} win The window to look for key elements in.
   * @param {string} value The shortcut string.
   * @returns {boolean} Whether a system shortcut was found or not.
   */
  isSystem(win, value) {
    let modifiers = value.split("+");
    let chromeKey = modifiers.pop();
    let modifiersString = this.getModifiersAttribute(modifiers);
    let keycode = this.getKeycodeAttribute(chromeKey);

    let baseSelector = "key";
    if (modifiers.length > 0) {
      baseSelector += `[modifiers="${modifiersString}"]`;
    }

    let keyEl = win.document.querySelector([
      `${baseSelector}[key="${chromeKey}"]`,
      `${baseSelector}[key="${chromeKey.toLowerCase()}"]`,
      `${baseSelector}[keycode="${keycode}"]`,
    ].join(","));
    return keyEl && !keyEl.closest("keyset").id.startsWith("ext-keyset-id");
  },

  /**
   * Determine what action a KeyboardEvent should perform, if any.
   *
   * @param {KeyboardEvent} event The event to check for a related system action.
   * @returns {string} A string identifying the action, or null if no action is found.
   */
  getSystemActionForEvent(event, {rtl} = {}) {
    switch (event.keyCode) {
      case event.DOM_VK_TAB:
        if (event.ctrlKey && !event.altKey && !event.metaKey)
          return ShortcutUtils.CYCLE_TABS;
        break;
      case event.DOM_VK_PAGE_UP:
        if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey)
          return ShortcutUtils.PREVIOUS_TAB;
        if (event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey)
          return ShortcutUtils.MOVE_TAB_BACKWARD;
        break;
      case event.DOM_VK_PAGE_DOWN:
        if (event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey)
          return ShortcutUtils.NEXT_TAB;
        if (event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey)
          return ShortcutUtils.MOVE_TAB_FORWARD;
        break;
      case event.DOM_VK_LEFT:
        if (event.metaKey && event.altKey && !event.shiftKey && !event.ctrlKey)
          return ShortcutUtils.PREVIOUS_TAB;
        break;
      case event.DOM_VK_RIGHT:
        if (event.metaKey && event.altKey && !event.shiftKey && !event.ctrlKey)
          return ShortcutUtils.NEXT_TAB;
        break;
    }

    if (AppConstants.platform == "macosx") {
      if (!event.altKey && event.metaKey) {
        switch (event.charCode) {
          case "}".charCodeAt(0):
            if (rtl)
              return ShortcutUtils.PREVIOUS_TAB;
            return ShortcutUtils.NEXT_TAB;
          case "{".charCodeAt(0):
            if (rtl)
              return ShortcutUtils.NEXT_TAB;
            return ShortcutUtils.PREVIOUS_TAB;
        }
      }
    }
    // Not on Mac from now on.
    if (AppConstants.platform != "macosx") {
      if (event.ctrlKey && !event.shiftKey && !event.metaKey &&
          event.keyCode == KeyEvent.DOM_VK_F4) {
        return ShortcutUtils.CLOSE_TAB;
      }
    }

    return null;
  },
};

Object.freeze(ShortcutUtils);