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);