tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

CustomKeys.sys.mjs (7806B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { JSONFile } from "resource://gre/modules/JSONFile.sys.mjs";
      6 
      7 // All the attributes for a <key> element that might specify the key to press.
      8 // We include data-l10n-id because keys are often specified using Fluent. Note
      9 // that we deliberately remove data-l10n-id when a key is customized because
     10 // otherwise, Fluent would overwrite the customization.
     11 const ATTRS = ["data-l10n-id", "key", "keycode", "modifiers"];
     12 
     13 // The original keys for any shortcuts that have been customized. This is used
     14 // to identify whether a shortcut has been customized, as well as to reset a
     15 // shortcut.
     16 const original = {};
     17 // All the browser windows we are handling. Maps from a window to our
     18 // MutationObserver for that window.
     19 const windows = new Map();
     20 
     21 // The configuration is an Object with the form:
     22 // { "someKeyId": {
     23 //   modifiers: "mod1,mod2",
     24 //   key: "someKey",
     25 //   keycode: "someKeycode"
     26 // } }
     27 // Only one of `key` or `keycode` should be specified for each entry. An entry
     28 // may be empty to indicate that the default key has been cleared; i.e.
     29 // { "someKeyId": {} }
     30 const config = new JSONFile({
     31  path: PathUtils.join(PathUtils.profileDir, "customKeys.json"),
     32 });
     33 
     34 function applyToKeyEl(window, keyId, keysets) {
     35  const keyEl = window.document.getElementById(keyId);
     36  if (!keyEl || keyEl.tagName != "key") {
     37    return;
     38  }
     39  if (!original[keyId]) {
     40    // Save the original key in case the user wants to reset.
     41    const orig = (original[keyId] = {});
     42    for (const attr of ATTRS) {
     43      const val = keyEl.getAttribute(attr);
     44      if (val) {
     45        orig[attr] = val;
     46      }
     47    }
     48  }
     49  for (const attr of ATTRS) {
     50    keyEl.removeAttribute(attr);
     51  }
     52  const data = config.data[keyId];
     53  for (const attr of ["modifiers", "key", "keycode"]) {
     54    const val = data[attr];
     55    if (val) {
     56      keyEl.setAttribute(attr, val);
     57    }
     58  }
     59  keysets.add(keyEl.parentElement);
     60 }
     61 
     62 function resetKeyEl(window, keyId, keysets) {
     63  const keyEl = window.document.getElementById(keyId);
     64  if (!keyEl) {
     65    return;
     66  }
     67  const orig = original[keyId];
     68  for (const attr of ATTRS) {
     69    keyEl.removeAttribute(attr);
     70    const val = orig[attr];
     71    if (val !== undefined) {
     72      keyEl.setAttribute(attr, val);
     73    }
     74  }
     75  keysets.add(keyEl.parentElement);
     76 }
     77 
     78 async function applyToNewWindow(window) {
     79  await config.load();
     80  const keysets = new Set();
     81  for (const keyId in config.data) {
     82    applyToKeyEl(window, keyId, keysets);
     83  }
     84  refreshKeysets(window, keysets);
     85  observe(window);
     86 }
     87 
     88 function refreshKeysets(window, keysets) {
     89  if (keysets.size == 0) {
     90    return;
     91  }
     92  const observer = windows.get(window);
     93  if (observer) {
     94    // We don't want our MutationObserver to process this.
     95    observer.disconnect();
     96  }
     97  // Gecko doesn't watch for changes to key elements. It only sets up a key
     98  // element when its keyset is bound to the tree. See:
     99  // https://searchfox.org/mozilla-central/rev/7857ea04d142f2abc0d777085f9e54526c7cedf9/dom/xul/nsXULElement.cpp#587
    100  // Therefore, remove and re-add any modified keysets to apply the changes.
    101  for (const keyset of keysets) {
    102    const parent = keyset.parentElement;
    103    keyset.remove();
    104    parent.append(keyset);
    105  }
    106  if (observer) {
    107    observe(window);
    108  }
    109 }
    110 
    111 function observe(window) {
    112  let observer = windows.get(window);
    113  if (!observer) {
    114    // A keyset can be added dynamically. For example, DevTools does this during
    115    // delayed startup. Note that key elements cannot be added dynamically. We
    116    // know this because Gecko doesn't watch for changes to key elements. It
    117    // only sets up a key element when its keyset is bound to the tree. See:
    118    // https://searchfox.org/mozilla-central/rev/7857ea04d142f2abc0d777085f9e54526c7cedf9/dom/xul/nsXULElement.cpp#587
    119    observer = new window.MutationObserver(mutations => {
    120      const keysets = new Set();
    121      for (const mutation of mutations) {
    122        for (const node of mutation.addedNodes) {
    123          if (node.tagName != "keyset") {
    124            continue;
    125          }
    126          for (const key of node.children) {
    127            if (key.tagName != "key" || !config.data[key.id]) {
    128              continue;
    129            }
    130            applyToKeyEl(window, key.id, keysets);
    131          }
    132        }
    133      }
    134      refreshKeysets(window, keysets);
    135    });
    136    windows.set(window, observer);
    137  }
    138  for (const node of [window.document, window.document.body]) {
    139    observer.observe(node, { childList: true });
    140  }
    141  return observer;
    142 }
    143 
    144 export const CustomKeys = {
    145  /**
    146   * Customize a keyboard shortcut.
    147   *
    148   * @param {string} id The id of the key element.
    149   * @param {object} data
    150   * @param {string} data.modifiers The modifiers for the key; e.g. "accel,shift".
    151   * @param {string} data.key The key itself; e.g. "Y". Mutually exclusive with data.keycode.
    152   * @param {string} data.keycode The key code; e.g. VK_F1. Mutually exclusive with data.key.
    153   */
    154  changeKey(id, { modifiers, key, keycode }) {
    155    const existing = config.data[id];
    156    if (
    157      existing &&
    158      modifiers == existing.modifiers &&
    159      key == existing.key &&
    160      keycode == existing.keycode
    161    ) {
    162      return; // No change.
    163    }
    164    const defaultKey = this.getDefaultKey(id);
    165    if (
    166      defaultKey &&
    167      modifiers == defaultKey.modifiers &&
    168      key == defaultKey.key &&
    169      keycode == defaultKey.keycode
    170    ) {
    171      this.resetKey(id);
    172      return;
    173    }
    174    const data = (config.data[id] = {});
    175    if (modifiers) {
    176      data.modifiers = modifiers;
    177    }
    178    if (key) {
    179      data.key = key;
    180    }
    181    if (keycode) {
    182      data.keycode = keycode;
    183    }
    184    for (const window of windows.keys()) {
    185      const keysets = new Set();
    186      applyToKeyEl(window, id, keysets);
    187      refreshKeysets(window, keysets);
    188    }
    189    config.saveSoon();
    190  },
    191 
    192  /**
    193   * Reset a keyboard shortcut to its defalt.
    194   *
    195   * @param {string} id The id of the key element.
    196   */
    197  resetKey(id) {
    198    if (!config.data[id]) {
    199      return; // No change.
    200    }
    201    delete config.data[id];
    202    for (const window of windows.keys()) {
    203      const keysets = new Set();
    204      resetKeyEl(window, id, keysets);
    205      refreshKeysets(window, keysets);
    206    }
    207    delete original[id];
    208    config.saveSoon();
    209  },
    210 
    211  /**
    212   * Clear a keyboard shortcut; i.e. so it does nothing.
    213   *
    214   * @param {string} id The id of the key element.
    215   */
    216  clearKey(id) {
    217    this.changeKey(id, {});
    218  },
    219 
    220  /**
    221   * Reset all keyboard shortcuts to their defaults.
    222   */
    223  resetAll() {
    224    config.data = {};
    225    for (const window of windows.keys()) {
    226      const keysets = new Set();
    227      for (const id in original) {
    228        resetKeyEl(window, id, keysets);
    229      }
    230      refreshKeysets(window, keysets);
    231    }
    232    for (const id of Object.keys(original)) {
    233      delete original[id];
    234    }
    235    config.saveSoon();
    236  },
    237 
    238  initWindow(window) {
    239    applyToNewWindow(window);
    240  },
    241 
    242  uninitWindow(window) {
    243    windows.get(window).disconnect();
    244    windows.delete(window);
    245  },
    246 
    247  /**
    248   * Return the default key for this shortcut in the form:
    249   * { modifiers: "mod1,mod2", key: "someKey", keycode: "someKeycode" }
    250   * If this shortcut isn't customized, return null.
    251   *
    252   * @param {string} keyId The id of the key element.
    253   */
    254  getDefaultKey(keyId) {
    255    const origKey = original[keyId];
    256    if (!origKey) {
    257      return null;
    258    }
    259    const data = {};
    260    if (origKey.modifiers) {
    261      data.modifiers = origKey.modifiers;
    262    }
    263    if (origKey.key) {
    264      data.key = origKey.key;
    265    }
    266    if (origKey.keycode) {
    267      data.keycode = origKey.keycode;
    268    }
    269    return data;
    270  },
    271 };
    272 
    273 Object.freeze(CustomKeys);