| 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 | Cu.import("resource://gre/modules/Services.jsm"); | 
|---|
| 6 |  | 
|---|
| 7 | let validModifiers = | 
|---|
| 8 | { | 
|---|
| 9 | ACCEL: null, | 
|---|
| 10 | CTRL: "control", | 
|---|
| 11 | CONTROL: "control", | 
|---|
| 12 | SHIFT: "shift", | 
|---|
| 13 | ALT: "alt", | 
|---|
| 14 | META: "meta", | 
|---|
| 15 | __proto__: null | 
|---|
| 16 | }; | 
|---|
| 17 |  | 
|---|
| 18 | /** | 
|---|
| 19 | * Sets the correct value of validModifiers.ACCEL. | 
|---|
| 20 | */ | 
|---|
| 21 | function initAccelKey() | 
|---|
| 22 | { | 
|---|
| 23 | validModifiers.ACCEL = "control"; | 
|---|
| 24 | try | 
|---|
| 25 | { | 
|---|
| 26 | let accelKey = Services.prefs.getIntPref("ui.key.accelKey"); | 
|---|
| 27 | if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_CONTROL) | 
|---|
| 28 | validModifiers.ACCEL = "control"; | 
|---|
| 29 | else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_ALT) | 
|---|
| 30 | validModifiers.ACCEL = "alt"; | 
|---|
| 31 | else if (accelKey == Ci.nsIDOMKeyEvent.DOM_VK_META) | 
|---|
| 32 | validModifiers.ACCEL = "meta"; | 
|---|
| 33 | } | 
|---|
| 34 | catch(e) | 
|---|
| 35 | { | 
|---|
| 36 | Cu.reportError(e); | 
|---|
| 37 | } | 
|---|
| 38 | } | 
|---|
| 39 |  | 
|---|
| 40 | exports.KeySelector = KeySelector; | 
|---|
| 41 |  | 
|---|
| 42 | /** | 
|---|
| 43 | * This class provides capabilities to find and use available keyboard shortcut | 
|---|
| 44 | * keys. | 
|---|
| 45 | * @param {ChromeWindow} window   the window where to look up existing shortcut | 
|---|
| 46 | *                                keys | 
|---|
| 47 | * @constructor | 
|---|
| 48 | */ | 
|---|
| 49 | function KeySelector(window) | 
|---|
| 50 | { | 
|---|
| 51 | this._initExistingShortcuts(window); | 
|---|
| 52 | } | 
|---|
| 53 | KeySelector.prototype = | 
|---|
| 54 | { | 
|---|
| 55 | /** | 
|---|
| 56 | * Map listing existing shortcut keys as its keys. | 
|---|
| 57 | * @type Object | 
|---|
| 58 | */ | 
|---|
| 59 | _existingShortcuts: null, | 
|---|
| 60 |  | 
|---|
| 61 | /** | 
|---|
| 62 | * Sets up _existingShortcuts property for a window. | 
|---|
| 63 | */ | 
|---|
| 64 | _initExistingShortcuts: function(/**ChromeWindow*/ window) | 
|---|
| 65 | { | 
|---|
| 66 | if (!validModifiers.ACCEL) | 
|---|
| 67 | initAccelKey(); | 
|---|
| 68 |  | 
|---|
| 69 | this._existingShortcuts = {__proto__: null}; | 
|---|
| 70 |  | 
|---|
| 71 | let keys = window.document.getElementsByTagName("key"); | 
|---|
| 72 | for (let i = 0; i < keys.length; i++) | 
|---|
| 73 | { | 
|---|
| 74 | let key = keys[i]; | 
|---|
| 75 | let keyData = | 
|---|
| 76 | { | 
|---|
| 77 | shift: false, | 
|---|
| 78 | meta: false, | 
|---|
| 79 | alt: false, | 
|---|
| 80 | control: false, | 
|---|
| 81 | char: null, | 
|---|
| 82 | code: null | 
|---|
| 83 | }; | 
|---|
| 84 |  | 
|---|
| 85 | let keyChar = key.getAttribute("key"); | 
|---|
| 86 | if (keyChar && keyChar.length == 1) | 
|---|
| 87 | keyData.char = keyChar.toUpperCase(); | 
|---|
| 88 |  | 
|---|
| 89 | let keyCode = key.getAttribute("keycode"); | 
|---|
| 90 | if (keyCode && "DOM_" + keyCode.toUpperCase() in Ci.nsIDOMKeyEvent) | 
|---|
| 91 | keyData.code = Ci.nsIDOMKeyEvent["DOM_" + keyCode.toUpperCase()]; | 
|---|
| 92 |  | 
|---|
| 93 | if (!keyData.char && !keyData.code) | 
|---|
| 94 | continue; | 
|---|
| 95 |  | 
|---|
| 96 | let keyModifiers = key.getAttribute("modifiers"); | 
|---|
| 97 | if (keyModifiers) | 
|---|
| 98 | for (let modifier of keyModifiers.toUpperCase().match(/\w+/g)) | 
|---|
| 99 | if (modifier in validModifiers) | 
|---|
| 100 | keyData[validModifiers[modifier]] = true; | 
|---|
| 101 |  | 
|---|
| 102 | let canonical = [keyData.shift, keyData.meta, keyData.alt, keyData.control, keyData.char || keyData.code].join(" "); | 
|---|
| 103 | this._existingShortcuts[canonical] = true; | 
|---|
| 104 | } | 
|---|
| 105 | }, | 
|---|
| 106 |  | 
|---|
| 107 | /** | 
|---|
| 108 | * Selects a keyboard shortcut variant that isn't already taken, | 
|---|
| 109 | * parses it into an object. | 
|---|
| 110 | */ | 
|---|
| 111 | selectKey: function(/**String*/ variants) /**Object*/ | 
|---|
| 112 | { | 
|---|
| 113 | for (let variant of variants.split(/\s*,\s*/)) | 
|---|
| 114 | { | 
|---|
| 115 | if (!variant) | 
|---|
| 116 | continue; | 
|---|
| 117 |  | 
|---|
| 118 | let keyData = | 
|---|
| 119 | { | 
|---|
| 120 | shift: false, | 
|---|
| 121 | meta: false, | 
|---|
| 122 | alt: false, | 
|---|
| 123 | control: false, | 
|---|
| 124 | char: null, | 
|---|
| 125 | code: null, | 
|---|
| 126 | codeName: null | 
|---|
| 127 | }; | 
|---|
| 128 | for (let part of variant.toUpperCase().split(/\s+/)) | 
|---|
| 129 | { | 
|---|
| 130 | if (part in validModifiers) | 
|---|
| 131 | keyData[validModifiers[part]] = true; | 
|---|
| 132 | else if (part.length == 1) | 
|---|
| 133 | keyData.char = part; | 
|---|
| 134 | else if ("DOM_VK_" + part in Ci.nsIDOMKeyEvent) | 
|---|
| 135 | { | 
|---|
| 136 | keyData.code = Ci.nsIDOMKeyEvent["DOM_VK_" + part]; | 
|---|
| 137 | keyData.codeName = "VK_" + part; | 
|---|
| 138 | } | 
|---|
| 139 | } | 
|---|
| 140 |  | 
|---|
| 141 | if (!keyData.char && !keyData.code) | 
|---|
| 142 | continue; | 
|---|
| 143 |  | 
|---|
| 144 | let canonical = [keyData.shift, keyData.meta, keyData.alt, keyData.control, keyData.char || keyData.code].join(" "); | 
|---|
| 145 | if (canonical in this._existingShortcuts) | 
|---|
| 146 | continue; | 
|---|
| 147 |  | 
|---|
| 148 | return keyData; | 
|---|
| 149 | } | 
|---|
| 150 |  | 
|---|
| 151 | return null; | 
|---|
| 152 | } | 
|---|
| 153 | }; | 
|---|
| 154 |  | 
|---|
| 155 | /** | 
|---|
| 156 | * Creates the text representation for a key. | 
|---|
| 157 | * @static | 
|---|
| 158 | */ | 
|---|
| 159 | KeySelector.getTextForKey = function (/**Object*/ key) /**String*/ | 
|---|
| 160 | { | 
|---|
| 161 | if (!key) | 
|---|
| 162 | return null; | 
|---|
| 163 |  | 
|---|
| 164 | if (!("text" in key)) | 
|---|
| 165 | { | 
|---|
| 166 | key.text = null; | 
|---|
| 167 | try | 
|---|
| 168 | { | 
|---|
| 169 | let stringBundle = Services.strings.createBundle("chrome://global-platform/locale/platformKeys.properties"); | 
|---|
| 170 | let parts = []; | 
|---|
| 171 | if (key.control) | 
|---|
| 172 | parts.push(stringBundle.GetStringFromName("VK_CONTROL")); | 
|---|
| 173 | if (key.alt) | 
|---|
| 174 | parts.push(stringBundle.GetStringFromName("VK_ALT")); | 
|---|
| 175 | if (key.meta) | 
|---|
| 176 | parts.push(stringBundle.GetStringFromName("VK_META")); | 
|---|
| 177 | if (key.shift) | 
|---|
| 178 | parts.push(stringBundle.GetStringFromName("VK_SHIFT")); | 
|---|
| 179 | if (key.char) | 
|---|
| 180 | parts.push(key.char.toUpperCase()); | 
|---|
| 181 | else | 
|---|
| 182 | { | 
|---|
| 183 | let stringBundle2 = Services.strings.createBundle("chrome://global/locale/keys.properties"); | 
|---|
| 184 | parts.push(stringBundle2.GetStringFromName(key.codeName)); | 
|---|
| 185 | } | 
|---|
| 186 | key.text = parts.join(stringBundle.GetStringFromName("MODIFIER_SEPARATOR")); | 
|---|
| 187 | } | 
|---|
| 188 | catch (e) | 
|---|
| 189 | { | 
|---|
| 190 | Cu.reportError(e); | 
|---|
| 191 | return null; | 
|---|
| 192 | } | 
|---|
| 193 | } | 
|---|
| 194 | return key.text; | 
|---|
| 195 | }; | 
|---|
| 196 |  | 
|---|
| 197 | /** | 
|---|
| 198 | * Tests whether a keypress event matches the given key. | 
|---|
| 199 | * @static | 
|---|
| 200 | */ | 
|---|
| 201 | KeySelector.matchesKey = function(/**Event*/ event, /**Object*/ key) /**Boolean*/ | 
|---|
| 202 | { | 
|---|
| 203 | if (event.defaultPrevented || !key) | 
|---|
| 204 | return false; | 
|---|
| 205 | if (key.shift != event.shiftKey || key.alt != event.altKey) | 
|---|
| 206 | return false; | 
|---|
| 207 | if (key.meta != event.metaKey || key.control != event.ctrlKey) | 
|---|
| 208 | return false; | 
|---|
| 209 |  | 
|---|
| 210 | if (key.char && event.charCode && String.fromCharCode(event.charCode).toUpperCase() == key.char) | 
|---|
| 211 | return true; | 
|---|
| 212 | if (key.code && event.keyCode && event.keyCode == key.code) | 
|---|
| 213 | return true; | 
|---|
| 214 | return false; | 
|---|
| 215 | }; | 
|---|