├── .gitignore ├── Makefile ├── README.md ├── chrome.manifest ├── components └── keyconfig-service.js ├── content ├── defaultPreferencesLoader.jsm ├── edit.xul ├── keyconfig.js ├── keyconfig.xul ├── menumanipulator.js └── overlay.xul ├── defaults └── preferences │ └── keyconfig.js ├── functions_for_keyconfig-1.3.7-fx.xpi ├── icon.png ├── icon64.png ├── install.rdf ├── locale ├── cs-CZ │ └── keyconfig.ent ├── da-DK │ └── keyconfig.ent ├── de-DE │ └── keyconfig.ent ├── en-US │ └── keyconfig.ent ├── es-AR │ └── keyconfig.ent ├── fr-FR │ └── keyconfig.ent ├── it-IT │ └── keyconfig.ent ├── ja-JP │ └── keyconfig.ent ├── ko-KR │ └── keyconfig.ent ├── ru-RU │ └── keyconfig.ent ├── sk-SK │ └── keyconfig.ent ├── zh-CN │ └── keyconfig.ent └── zh-TW │ └── keyconfig.ent └── skin └── keyconfig.css /.gitignore: -------------------------------------------------------------------------------- 1 | keyconfig.xpi 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ZIPFILES=install.rdf chrome.manifest README.md icon.png icon64.png defaults \ 2 | components content locale skin 3 | DEPFILES=$(shell find $(ZIPFILES) -type f -print) 4 | 5 | keyconfig.xpi: $(DEPFILES) 6 | rm -f $@.tmp 7 | zip -r $@.tmp $(ZIPFILES) 8 | mv -f $@.tmp $@ 9 | 10 | clean: ; -rm -rf keyconfig.xpi 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dorando-keyconfig 2 | 3 | A modified version of [Dorando](http://mozilla.dorando.at/readme.html)'s tool to rebind keys in Mozilla apps. It is designed to be a drop-in replacement, and will keep your old settings. (See official version webpage for upgrade instructions.) 4 | 5 | The only changes here are non-functional changes for compatibility with newer app versions, since Dorando is no longer maintaining the add-on. 6 | 7 | Note that this add-on is _not_ compatible with Firefox 57 and newer, since it uses old add-on APIs that are no longer supported as of Firefox 57. However, it remains compatible with current versions of Thunderbird and SeaMonkey. 8 | 9 | You can install this add-on from [addons.thunderbird.net](https://addons.thunderbird.net/thunderbird/addon/dorando-keyconfig) and [addons.palemoon.org](https://addons.palemoon.org/addon/keyconfig/). **If you are replacing Dorando's version of the add-on from 2011 or earlier with this version, make sure to uninstall Dorando's version before installing this one.** 10 | 11 | ## Working with the source code 12 | 13 | To deploy the source code directly into Firefox, Thunderbird, or Mozilla, so that you can make changes here and then test them in the application without having to create and install a new XPI file: 14 | 15 | 1. Install the add-on normally from addons.mozilla.org. 16 | 17 | 2. Shut down the app. 18 | 19 | 3. Locate the "extensions" subdirectory of your app profile directory. 20 | 21 | 4. Locate the file "keyconfig@mozilla.dorando.at.xpi" in that directory and delete it. 22 | 23 | 5. Create a new text file called "keyconfig@mozilla.dorando.at" (note: no ".xpi" extension). In that file, put the full path to the directory this source code is in. 24 | 25 | 6. Locate the "prefs.js" file in your app profile directory. 26 | 27 | 7. Put this in it: 28 | 29 | user_pref("extensions.startupScanScopes", 5); 30 | 31 | 8. Restart the app from the command line with the argument `-purgecaches`. 32 | 33 | 9. Confirm that the add-on shows up in the add-ons listing. 34 | 35 | Whenever you start the app with `-purgecaches` from this point forward, it will reload the current version of the add-on code from your source directory. It may also pick up your changes even when you don't specify `-purgecaches`, but the only way to be _certain_ that it will notice your changes is to specify that argument. 36 | 37 | ## Copyright 38 | 39 | Dorando's original copyright is as follows: 40 | 41 | >Copyright (c) 2004-2011 Dorando. 42 | >Permission is granted to copy, distribute, and/or modify any part of this package. 43 | 44 | The maintainers of this updated version of the add-on do not claim any additional copyright. In other words, the copyright above still applies. 45 | -------------------------------------------------------------------------------- /chrome.manifest: -------------------------------------------------------------------------------- 1 | content keyconfig content/ 2 | content keyconfig-defaults defaults/ 3 | locale keyconfig en-US locale/en-US/ 4 | locale keyconfig cs-CZ locale/cs-CZ/ 5 | locale keyconfig da-DK locale/da-DK/ 6 | locale keyconfig de-DE locale/de-DE/ 7 | locale keyconfig es-AR locale/es-AR/ 8 | locale keyconfig fr-FR locale/fr-FR/ 9 | locale keyconfig it-IT locale/it-IT/ 10 | locale keyconfig ja-JP locale/ja-JP/ 11 | locale keyconfig ko-KR locale/ko-KR/ 12 | locale keyconfig ru-RU locale/ru-RU/ 13 | locale keyconfig sk-SK locale/sk-SK/ 14 | locale keyconfig zh-CN locale/zh-CN/ 15 | locale keyconfig zh-TW locale/zh-TW/ 16 | skin keyconfig classic/1.0 skin/ 17 | 18 | overlay chrome://browser/content/browser.xul chrome://keyconfig/content/overlay.xul 19 | overlay chrome://messenger/content/mailWindowOverlay.xul chrome://keyconfig/content/overlay.xul 20 | overlay chrome://messenger/content/messengercompose/messengercompose.xul chrome://keyconfig/content/overlay.xul 21 | overlay chrome://communicator/content/tasksOverlay.xul chrome://keyconfig/content/overlay.xul 22 | style chrome://keyconfig/content/keyconfig.xul chrome://keyconfig/skin/keyconfig.css 23 | 24 | component {e9f7950e-d78d-4aaa-900a-c43588052eba} components/keyconfig-service.js 25 | contract @dorando.at/keyconfig;1 {e9f7950e-d78d-4aaa-900a-c43588052eba} 26 | category profile-after-change keyconfigService @dorando.at/keyconfig;1 27 | -------------------------------------------------------------------------------- /components/keyconfig-service.js: -------------------------------------------------------------------------------- 1 | Components.utils.import("chrome://keyconfig/content/defaultPreferencesLoader.jsm"); 2 | 3 | function NSGetModule(compMgr, fileSpec) { return Module; } 4 | function NSGetFactory() { return Factory; } 5 | 6 | var Module = { 7 | CID: Components.ID("{e9f7950e-d78d-4aaa-900a-c43588052eba}"), 8 | contractID: "@dorando.at/keyconfig;1", 9 | className: "keyconfigService", 10 | 11 | registerSelf: function (aComponentManager, aFileSpec, aLocation, aType) { 12 | aComponentManager = aComponentManager.QueryInterface(Components.interfaces.nsIComponentRegistrar); 13 | 14 | aComponentManager.registerFactoryLocation(this.CID, this.className, this.contractID, aFileSpec, aLocation, aType); 15 | 16 | var CategoryManager = Components.classes["@mozilla.org/categorymanager;1"] 17 | .getService(Components.interfaces.nsICategoryManager); 18 | CategoryManager.addCategoryEntry("app-startup", this.className, "service," + this.contractID, true, true, null); 19 | }, 20 | 21 | getClassObject: function (aComponentManager, aCID, aIID) { 22 | if (!aIID.equals(Components.interfaces.nsIFactory)) throw Components.results.NS_ERROR_NOT_IMPLEMENTED; 23 | 24 | if (aCID.equals(this.CID)) return Factory; 25 | 26 | throw Components.results.NS_ERROR_NO_INTERFACE; 27 | }, 28 | 29 | canUnload: function () { return true; } 30 | }; 31 | 32 | var Factory = { 33 | createInstance: function (aOuter, aIID) 34 | { 35 | if (aOuter != null) throw Components.results.NS_ERROR_NO_AGGREGATION; 36 | 37 | return new keyconfigService(); 38 | } 39 | }; 40 | 41 | function keyconfigService() { 42 | this.os.addObserver(this,"domwindowopened",false); 43 | } 44 | 45 | keyconfigService.prototype = { 46 | js: Components.classes["@mozilla.org/moz/jssubscript-loader;1"].getService(Components.interfaces.mozIJSSubScriptLoader), 47 | os: Components.classes["@mozilla.org/observer-service;1"].getService(Components.interfaces.nsIObserverService), 48 | ps: Components.classes['@mozilla.org/preferences-service;1'] 49 | .getService(Components.interfaces.nsIPrefService).getBranch(""), 50 | 51 | observe: function (aSubject, aTopic, aData) { 52 | if(aTopic == "domwindowopened") { 53 | aSubject.keyconfig = {service: this}; 54 | aSubject.addEventListener("load",this.load,true); 55 | } 56 | }, 57 | 58 | load: function(event) { 59 | this.removeEventListener(event.type,this.keyconfig.service.load,true); 60 | 61 | this.addEventListener(event.eventPhase == 2 ? "pageshow" : "load",this.keyconfig.service.init,false); 62 | }, 63 | 64 | init: function(event) { 65 | // Current Thunderbird nightly builds do not load default preferences 66 | // from overlay add-ons. They're probably going to fix this, but it may go 67 | // away again at some point in the future, and in any case we'll need to do 68 | // it ourselves when we convert from overlay to bootstrapped, and there 69 | // shouldn't be any harm in setting the default values of preferences twice 70 | // (i.e., both Thunderbird and our code doing it). 71 | // This is in a try/catch because if it fails it's probably because 72 | // setStringPref failed, in which case we're running inside an earlier 73 | // application version which has already loaded the default preferences 74 | // automatically. 75 | try { 76 | var loader = new DefaultPreferencesLoader(); 77 | loader.parseUri( 78 | "chrome://keyconfig-defaults/content/preferences/keyconfig.js"); 79 | } catch (ex) {} 80 | 81 | if(event && event.eventPhase != 2) return; 82 | 83 | this.removeEventListener("pageshow",this.keyconfig.service.init,false); 84 | 85 | this.keyconfig.removedKeys = this.document.documentElement.appendChild(this.document.createElement("keyconfig")); 86 | 87 | 88 | if (this.keyconfig.service.ps.prefHasUserValue("keyconfig.global.20110522")) { 89 | var oldBranch = "keyconfig."; var newBranch = "extensions.dorandoKeyConfig."; 90 | var oldKeys = this.keyconfig.service.ps.getChildList(oldBranch) 91 | for (var i = 0; i < oldKeys.length; i++) { 92 | var newKey = newBranch + oldKeys[i].split(oldBranch)[1]; 93 | switch (this.keyconfig.service.ps.getPrefType(oldKeys[i])) { 94 | case 32: //PREF_STRING 95 | var value = this.keyconfig.service.ps.getCharPref(oldKeys[i]); 96 | this.keyconfig.service.ps.setCharPref(newKey, value); 97 | break; 98 | 99 | case 64: //PREF_INT 100 | var value = this.keyconfig.service.ps.getIntPref(oldKeys[i]); 101 | this.keyconfig.service.ps.setIntPref(newKey, value); 102 | break; 103 | 104 | case 128: //PREF_BOOLEAN 105 | var value = this.keyconfig.service.ps.getBoolPref(oldKeys[i]); 106 | this.keyconfig.service.ps.setBoolPref(newKey, value); 107 | break; 108 | } 109 | } 110 | this.keyconfig.service.ps.deleteBranch(oldBranch); 111 | } 112 | 113 | this.keyconfig.service.ps.deleteBranch("extensions.dorandoKeyConfig.global") 114 | 115 | this.keyconfig.profile = "extensions.dorandoKeyConfig." + this.keyconfig.service.ps.getCharPref("extensions.dorandoKeyConfig.profile") + "."; 116 | 117 | var i, l; 118 | 119 | var keyset = this.document.getElementsByTagName("keyset")[0] || 120 | this.document.documentElement.appendChild(this.document.createElement("keyset")); 121 | 122 | var nodes = this.document.getElementsByTagName("key"); 123 | for(i = 0, l = nodes.length; i < l; i++) if(!nodes[i].id) 124 | nodes[i].id = "xxx_key"+ i +"_"+nodes[i].getAttribute("command")+nodes[i].getAttribute("oncommand"); 125 | 126 | var keys = this.keyconfig.service.ps.getChildList(this.keyconfig.profile, {}); 127 | 128 | for(i = 0, l = keys.length; i < l; i++) { 129 | var key, node; 130 | try { 131 | try { 132 | // Gecko 58+ 133 | key = this.keyconfig.service.ps.getStringPref(keys[i]).split("]["); 134 | } 135 | catch (e) { 136 | key = this.keyconfig.service.ps.getComplexValue(keys[i], Components.interfaces.nsISupportsString).data.split("]["); 137 | } 138 | } catch(e) { continue; } 139 | if(key[3] && (!key[4] || key[4] == this.document.location)) { 140 | node = keyset.appendChild(this.document.createElement("key")); 141 | node.id = keys[i].substr(this.keyconfig.profile.length); 142 | // node.addEventListener("command",key[3]); 143 | node.setAttribute("oncommand",key[3]) 144 | } else { 145 | node = this.document.getElementById(keys[i].substr(this.keyconfig.profile.length)); 146 | if(!node) continue; 147 | } 148 | 149 | node.removeAttribute("modifiers"); node.removeAttribute("key"); node.removeAttribute("keycode"); 150 | node.removeAttribute("charcode"); node.removeAttribute("keytext"); 151 | if(key[0] == "!") {this.keyconfig.removedKeys.appendChild(node); continue;} 152 | 153 | if(key[0]) node.setAttribute("modifiers",key[0]); 154 | if(key[1]) node.setAttribute("key",key[1]); 155 | if(key[2]) node.setAttribute("keycode",key[2]); 156 | } 157 | }, 158 | 159 | Module: function(module) { try { 160 | this.js.loadSubScript("chrome://keyconfig/content/" + module + ".js", this); 161 | } catch(err){} }, 162 | 163 | Load: function(path) { try { 164 | this.js.loadSubScript(path, this); 165 | } catch(err){} } 166 | 167 | } 168 | -------------------------------------------------------------------------------- /content/defaultPreferencesLoader.jsm: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/oshybystyi/8cf882bc8b0c9a95a116 2 | 3 | /** 4 | * Working example can be found here 5 | * https://github.com/oshybystyi/FireX-Pixel-Perfect/blob/issue-5-make-addon-restartless/content/lib/defaultPreferencesLoader.jsm 6 | * 7 | * Important this module was tested only with true, most 8 | * likely it won't work with false value 9 | * 10 | * A lot of stuff was borrowed from https://github.com/firebug/firebug/blob/master/extension/modules/prefLoader.js 11 | */ 12 | 13 | const { utils: Cu, classes: Cc, interfaces: Ci } = Components; 14 | 15 | Cu.import('resource://gre/modules/Services.jsm'); 16 | 17 | var EXPORTED_SYMBOLS = ['DefaultPreferencesLoader']; 18 | 19 | /** 20 | * Read defaults/preferences/* and set Services.pref default branch 21 | */ 22 | function DefaultPreferencesLoader(installPath, prefDir) { 23 | var readFrom = []; 24 | 25 | this.readFrom = readFrom; 26 | 27 | this.defaultBranch = Services.prefs.getDefaultBranch(""); 28 | 29 | if (! installPath) 30 | // We'll be using parseUri instead of parseDirectory 31 | return; 32 | 33 | if (! prefDir) 34 | prefDir = 'defaults/preferences'; 35 | 36 | // Maybe instead just test if it's a file rather than a directory? 37 | // Not sure. 38 | if (/\.xpi$/i.test(installPath.path)) { 39 | let baseURI = Services.io.newFileURI(installPath); 40 | // Packed extension, need to read ZIP to get list of preference files 41 | // and then use "jar:" URIs to access them. 42 | let zr = Cc['@mozilla.org/libjar/zip-reader;1'].createInstance( 43 | Ci.nsIZipReader); 44 | zr.open(installPath); 45 | let entries = zr.findEntries(prefDir + '/?*'); 46 | while (entries.hasMore()) { 47 | let entry = entries.getNext(); 48 | readFrom.push('jar:' + baseURI.spec + "!/" + entry); 49 | } 50 | } 51 | else { 52 | let dirPath = installPath.clone(); // don't modify the original object 53 | 54 | prefDir.split('/').forEach(function(dir) { 55 | dirPath.append(dir); 56 | }); 57 | 58 | if (dirPath.exists() !== true) { 59 | throw new DefaultsDirectoryMissingError(dirPath); 60 | } 61 | 62 | let entries = dirPath.directoryEntries; 63 | 64 | while (entries.hasMoreElements()) { 65 | let fileURI = Services.io.newFileURI(entries.getNext()); 66 | readFrom.push(fileURI.spec); 67 | } 68 | } 69 | } 70 | 71 | DefaultPreferencesLoader.prototype = { 72 | /** 73 | * Iterate over files in the default/preferences/* 74 | * 75 | * @param {function} prefFunc the function that should be used instead of 76 | * pref 77 | */ 78 | parseDirectory: function(prefFunc) { 79 | this.readFrom.forEach(function(uri) { 80 | this.parseUri(uri, prefFunc); 81 | }); 82 | }, 83 | 84 | parseUri: function(uri, prefFunc) { 85 | prefFunc = prefFunc || this.pref.bind(this); 86 | Services.scriptloader.loadSubScript(uri, { pref: prefFunc }); 87 | }, 88 | 89 | /** 90 | * Emulates firefox pref function to load default preferences 91 | */ 92 | pref: function(key, value) { 93 | switch (typeof value) { 94 | case 'boolean': 95 | this.defaultBranch.setBoolPref(key, value); 96 | break; 97 | 98 | case 'number': 99 | this.defaultBranch.setIntPref(key, value); 100 | break; 101 | 102 | case 'string': 103 | this.defaultBranch.setStringPref(key, value); 104 | break; 105 | 106 | default: 107 | throw new NotSupportedValueTypeError(key); 108 | break; 109 | } 110 | }, 111 | 112 | /** 113 | * Clears default preferences according to AMO reviewers reccommendation 114 | * This should be invoked on bootstrap::shutdown 115 | * @see https://github.com/firebug/firebug/blob/master/extension/modules/prefLoader.js 116 | */ 117 | clearDefaultPrefs: function() { 118 | this.parseDirectory(this.prefUnload.bind(this)); 119 | }, 120 | 121 | prefUnload: function(key) { 122 | let branch = this.defaultBranch; 123 | if (branch.prefHasUserValue(key) !== true) { 124 | branch.deleteBranch(key); 125 | } 126 | } 127 | 128 | }; 129 | 130 | /** 131 | * Exception type on missing defaults/preferences folder 132 | */ 133 | function DefaultsDirectoryMissingError(installPath) { 134 | this.name = 'DefaultsDirectoryMissingError'; 135 | this.message = '\'' + installPath.path + '\' does no exist'; 136 | } 137 | 138 | /** Inherit from Error for error stack and pretty output in terminal **/ 139 | DefaultsDirectoryMissingError.prototype = new Error(); 140 | 141 | /** 142 | * Not supported value type to store by pref 143 | */ 144 | function NotSupportedValueTypeError(key) { 145 | this.name = 'NotSupportedValueType'; 146 | this.message = 'Value type for key \'' + key + '\' is not supported'; 147 | } 148 | 149 | NotSupportedValueTypeError.prototype = new Error(); 150 | -------------------------------------------------------------------------------- /content/edit.xul: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 16 | 17 | 18 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /content/keyconfig.js: -------------------------------------------------------------------------------- 1 | var gPrefService = Components.classes['@mozilla.org/preferences-service;1'] 2 | .getService(Components.interfaces.nsIPrefService).getBranch(""); 3 | var gUnicodeConverter = Components.classes['@mozilla.org/intl/scriptableunicodeconverter'] 4 | .createInstance(Components.interfaces.nsIScriptableUnicodeConverter); 5 | var gClipboardHelper = Components.classes["@mozilla.org/widget/clipboardhelper;1"] 6 | .getService(Components.interfaces.nsIClipboardHelper); 7 | var WindowMediator = Components.classes['@mozilla.org/appshell/window-mediator;1'] 8 | .getService(Components.interfaces.nsIWindowMediator); 9 | 10 | var gDocument, gLocation, gProfile, gKeys, gUsedKeys, gRemovedKeys; 11 | 12 | var gExtra2, keyTree, gEditbox, gEdit; 13 | 14 | var gLocaleKeys; 15 | var gPlatformKeys = new Object(); 16 | var gVKNames = []; 17 | var gReverseNames; 18 | 19 | function onLoad() { 20 | gUnicodeConverter.charset = "UTF-8"; 21 | 22 | gExtra2 = document.documentElement.getButton("extra2"); 23 | keyTree = document.getElementById("key-tree"); 24 | gEditbox = document.getElementById("editbox"); 25 | gEdit = document.getElementById("edit"); 26 | gLocaleKeys = document.getElementById("localeKeys"); 27 | 28 | var platformKeys = document.getElementById("platformKeys"); 29 | gPlatformKeys.shift = platformKeys.getString("VK_SHIFT"); 30 | gPlatformKeys.meta = platformKeys.getString("VK_META"); 31 | gPlatformKeys.alt = platformKeys.getString("VK_ALT"); 32 | gPlatformKeys.ctrl = platformKeys.getString("VK_CONTROL"); 33 | gPlatformKeys.sep = platformKeys.getString("MODIFIER_SEPARATOR"); 34 | switch (gPrefService.getIntPref("ui.key.accelKey")){ 35 | case 17: gPlatformKeys.accel = gPlatformKeys.ctrl; break; 36 | case 18: gPlatformKeys.accel = gPlatformKeys.alt; break; 37 | case 224: gPlatformKeys.accel = gPlatformKeys.meta; break; 38 | default: gPlatformKeys.accel = (window.navigator.platform.search("Mac") == 0 ? gPlatformKeys.meta : gPlatformKeys.ctrl); 39 | } 40 | 41 | for (var property in KeyEvent) { 42 | gVKNames[KeyEvent[property]] = property.replace("DOM_",""); 43 | } 44 | gVKNames[8] = "VK_BACK"; 45 | 46 | var XULAppInfo = Components.classes["@mozilla.org/xre/app-info;1"].getService(Components.interfaces.nsIXULAppInfo); 47 | var isThunderbird = XULAppInfo.name == "Thunderbird"; 48 | 49 | gReverseNames = isThunderbird ^ gPrefService.getBoolPref("extensions.dorandoKeyConfig.nicenames.reverse_order"); 50 | 51 | if(gPrefService.getBoolPref("extensions.dorandoKeyConfig.devmode")){ this.getFormattedKey = function(a,b,c) {return (a+"+"+b+c).replace(/null/g,"");} } 52 | 53 | var target = window.arguments ? window.arguments[0] : WindowMediator.getEnumerator(null).getNext(); 54 | 55 | var windowList = document.getElementById("window-list"); 56 | var i, l; 57 | for(i = 0, l = windowList.firstChild.childNodes.length; i < l; i++) 58 | if(windowList.firstChild.childNodes[i].label == target.document.title) 59 | windowList.selectedIndex = i; 60 | 61 | init(target); 62 | 63 | windowList.focus(); 64 | } 65 | 66 | function init(target) { 67 | if(!target) return; 68 | 69 | gDocument = target.document; 70 | gLocation = gDocument.location.href; 71 | gProfile = target.keyconfig.profile; 72 | 73 | gKeys = []; 74 | gRemovedKeys = target.keyconfig.removedKeys; 75 | 76 | var hideDisabled = gPrefService.getBoolPref("extensions.dorandoKeyConfig.hideDisabled"); 77 | 78 | var keys = gDocument.getElementsByTagName("key"); 79 | for(var i = 0, l = keys.length; i < l; i++) { 80 | var key = new Key(keys[i]); 81 | if(!(hideDisabled && key.disabled)) 82 | gKeys.push(key); 83 | } 84 | 85 | detectUsedKeys(); 86 | 87 | var elem = keyTree.getElementsByAttribute("sortActive","true")[0] || document.getElementById("name"); 88 | 89 | gKeys.sort(sorter[elem.id]); 90 | if(elem.getAttribute("sortDirection") == "descending") gKeys.reverse(); 91 | 92 | keyTree.view = keyView; 93 | keyTree.view.selection.select(-1); 94 | 95 | gExtra2.label = gStrings.add; 96 | gEditbox.setAttribute("disabled","true"); 97 | gEdit.value = ""; 98 | gEdit.keys = ["!",null,null]; 99 | } 100 | 101 | function onOK() { } 102 | 103 | function getFormattedKey(modifiers,key,keycode) { 104 | if(modifiers == "shift,alt,control,accel" && keycode == "VK_SCROLL_LOCK") return gStrings.disabled; 105 | if(!key && !keycode) return gStrings.disabled; 106 | 107 | var val = ""; 108 | if(modifiers) val = modifiers 109 | .replace(/^[\s,]+|[\s,]+$/g,"").split(/[\s,]+/g).join(gPlatformKeys.sep) 110 | .replace("alt",gPlatformKeys.alt) 111 | .replace("shift",gPlatformKeys.shift) 112 | .replace("control",gPlatformKeys.ctrl) 113 | .replace("meta",gPlatformKeys.meta) 114 | .replace("accel",gPlatformKeys.accel) 115 | +gPlatformKeys.sep; 116 | if(key == " ") { 117 | key = ""; keycode = "VK_SPACE"; 118 | } 119 | if(key) 120 | val += key; 121 | if(keycode) try { 122 | val += gLocaleKeys.getString(keycode) 123 | } catch(e){val += gStrings.unrecognized.replace("$1",keycode);} 124 | 125 | return val; 126 | } 127 | 128 | function getNameForKey(aKey) { 129 | var val; 130 | 131 | if(aKey.hasAttribute("label")) return aKey.getAttribute("label"); 132 | 133 | if(aKey.hasAttribute("command") || aKey.hasAttribute("observes")) { 134 | var command = aKey.getAttribute("command") || aKey.getAttribute("observes"); 135 | var node = gDocument.getElementById(command); 136 | if(node && node.hasAttribute("label")) return node.getAttribute("label"); 137 | val = getLabel("command", command); 138 | if(!val) val = getLabel("observes", command); 139 | } 140 | 141 | if(!val) val = getLabel("key", aKey.id); 142 | 143 | if(val) return val; 144 | 145 | var id = aKey.id.replace(/xxx_key.+?_/,""); 146 | try {id = gUnicodeConverter.ConvertToUnicode(id);} catch(err) { gUnicodeConverter.charset = "UTF-8"; } 147 | 148 | if(keyname[id]) { 149 | var key = gDocument.getElementById(keyname[id]); 150 | if(key) return getNameForKey(key); 151 | return keyname[id]; 152 | } 153 | 154 | return id; 155 | } 156 | 157 | function getLabel(attr, value) { 158 | var Users = gDocument.getElementsByAttribute(attr,value); 159 | var User; 160 | 161 | for(var i = 0, l = Users.length; i < l; i++) 162 | if(Users[i].hasAttribute("label") && (!User || User.localName == "menuitem")) User = Users[i]; 163 | 164 | if(!User) return null; 165 | 166 | if(User.localName == "menuitem" && User.parentNode.parentNode.parentNode.localName == "menupopup") { 167 | if(gReverseNames) return User.parentNode.parentNode.getAttribute("label") + " > " + User.getAttribute("label"); 168 | else return User.getAttribute("label") + " [" + User.parentNode.parentNode.getAttribute("label") + "]"; 169 | } else return User.getAttribute("label"); 170 | } 171 | 172 | function Recognize(event) { 173 | event.preventDefault(); 174 | event.stopPropagation(); 175 | 176 | var modifiers = []; 177 | if(event.altKey) modifiers.push("alt"); 178 | if(event.ctrlKey) modifiers.push("control"); 179 | if(event.metaKey) modifiers.push("meta"); 180 | if(event.shiftKey) modifiers.push("shift"); 181 | 182 | modifiers = modifiers.join(" "); 183 | 184 | var key = null; var keycode = null; 185 | if(event.charCode) key = String.fromCharCode(event.charCode).toUpperCase(); 186 | else { keycode = gVKNames[event.keyCode]; if(!keycode) return;} 187 | 188 | gEdit.value = getFormattedKey(modifiers,key,keycode); 189 | gEdit.keys = [modifiers,key,keycode]; 190 | 191 | if(!(gPrefService.getBoolPref("extensions.dorandoKeyConfig.allowAltCodes") && modifiers == "alt" && key && !isNaN(key))) { 192 | if(gPrefService.getBoolPref("extensions.dorandoKeyConfig.warnOnDuplicate") && gEdit.value != gEdit.key.shortcut && gUsedKeys[gEdit.value]) 193 | window.setTimeout(function(){ window.alert(gStrings.used.replace("$1",gUsedKeys[gEdit.value].join("\n"))) },0); 194 | 195 | gEdit.nextSibling.focus(); 196 | } 197 | } 198 | 199 | function Apply() { 200 | var key = gKeys[keyTree.currentIndex]; 201 | 202 | keyTree.focus(); 203 | 204 | if(key.shortcut == gEdit.value) return; 205 | 206 | key.shortcut = gEdit.value; 207 | key.pref.splice(0,3,gEdit.keys[0],gEdit.keys[1],gEdit.keys[2]); 208 | 209 | detectUsedKeys(); 210 | 211 | var value = key.pref.join("]["); 212 | try { 213 | // Gecko 58+ 214 | gPrefService.setStringPref(gProfile+key.id, value); 215 | } 216 | catch (e) { 217 | var str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString); 218 | str.data = value; 219 | gPrefService.setComplexValue(gProfile+key.id, Components.interfaces.nsISupportsString, str); 220 | } 221 | 222 | keyTree.treeBoxObject.invalidate(); 223 | 224 | var targets = WindowMediator.getEnumerator(null); 225 | var target; 226 | while(target = targets.getNext()) { 227 | if(key.pref[4] && target.location != key.pref[4]) 228 | continue; 229 | 230 | var node = target.document.getElementById(key.id); 231 | if(node) { 232 | node.removeAttribute("modifiers"); node.removeAttribute("key"); node.removeAttribute("keycode"); 233 | node.removeAttribute("charcode"); node.removeAttribute("keytext"); 234 | node.removeAttribute("keyconfig"); 235 | 236 | var keyset = null; 237 | 238 | if(key.pref[0] == "!") { 239 | keyset = node.parentNode; 240 | target.keyconfig.removedKeys.appendChild(node); 241 | } else { 242 | if(key.pref[0]) node.setAttribute("modifiers",key.pref[0]); 243 | if(key.pref[1]) node.setAttribute("key",key.pref[1]); 244 | if(key.pref[2]) node.setAttribute("keycode",key.pref[2]); 245 | 246 | if(node.parentNode.localName != "keyset") 247 | target.document.getElementsByTagName("keyset")[0].appendChild(node); 248 | } 249 | 250 | keyset = keyset || node.parentNode; 251 | while(keyset.parentNode && keyset.parentNode.localName == "keyset") 252 | keyset = keyset.parentNode; 253 | keyset.parentNode.insertBefore(keyset, keyset.nextSibling); 254 | 255 | var menuitems = target.document.getElementsByAttribute("key",key.id); 256 | for(var i = 0, l = menuitems.length; i < l; i++) { 257 | menuitems[i].setAttribute("acceltext",""); 258 | menuitems[i].removeAttribute("acceltext"); 259 | } 260 | } 261 | } 262 | } 263 | 264 | function Disable() { 265 | gEdit.value = gStrings.disabled; 266 | gEdit.keys = ["!",null,null]; 267 | Apply(); 268 | } 269 | 270 | function Reset() { 271 | var key = gKeys[keyTree.currentIndex]; 272 | 273 | try{ gPrefService.clearUserPref(gProfile+key.id); } catch(err) {} 274 | 275 | key.pref = []; 276 | key.shortcut = gEdit.value = gStrings.uponreset; 277 | gEdit.keys = ["!",null,null]; 278 | 279 | gExtra2.label = gStrings.add; 280 | 281 | var targets = WindowMediator.getEnumerator(null); 282 | var target; 283 | while(target = targets.getNext()) { 284 | var node = target.document.getElementById(key.id); 285 | if(node) 286 | node.setAttribute("keyconfig","resetted"); 287 | } 288 | 289 | detectUsedKeys(); 290 | 291 | keyTree.treeBoxObject.invalidate(); 292 | keyTree.focus(); 293 | } 294 | 295 | function Key(aKey) { 296 | this.name = getNameForKey(aKey); 297 | this.shortcut = getFormattedKey( 298 | aKey.hasAttribute("modifiers") ? aKey.getAttribute("modifiers") : null, 299 | aKey.hasAttribute("keytext") ? aKey.getAttribute("keytext") : 300 | aKey.hasAttribute("key") ? aKey.getAttribute("key").toUpperCase() : aKey.hasAttribute("charcode") ? aKey.getAttribute("charcode").toUpperCase() : null, 301 | aKey.hasAttribute("keycode") ? aKey.getAttribute("keycode") : null 302 | ); 303 | this.id = aKey.id; 304 | if(aKey.getAttribute("keyconfig") == "resetted") this.shortcut = gStrings.uponreset; 305 | 306 | try { 307 | try { 308 | // Gecko 58+ 309 | this.pref = gPrefService.getStringPref(gProfile+aKey.id).split("]["); 310 | } 311 | catch (e) { 312 | this.pref = gPrefService.getComplexValue(gProfile+aKey.id, Components.interfaces.nsISupportsString).data.split("]["); 313 | } 314 | } catch(err) { this.pref = []; } 315 | 316 | this.code = getCodeFor(aKey); 317 | if(this.code == null) this.hardcoded = true; 318 | } 319 | Key.prototype = { 320 | get disabled() { return this.shortcut == gStrings.disabled; } 321 | } 322 | 323 | var sorter = { 324 | name: function(a,b) { return a.name.localeCompare(b.name); }, 325 | id: function(a,b) { return a.id.localeCompare(b.id); }, 326 | shortcut: function(a,b) { 327 | if(a.shortcut == b.shortcut) return 0; 328 | if(!a.shortcut) return 1; 329 | if(!b.shortcut) return -1; 330 | if(a.shortcut > b.shortcut) return 1; 331 | return -1; 332 | } 333 | } 334 | 335 | function detectUsedKeys() { 336 | gUsedKeys = []; 337 | 338 | for(var i = 0, l = gKeys.length; i < l; i++) { 339 | if(gUsedKeys[gKeys[i].shortcut]) 340 | gUsedKeys[gKeys[i].shortcut].push(gKeys[i].name); 341 | else 342 | gUsedKeys[gKeys[i].shortcut]=[gKeys[i].name]; 343 | } 344 | 345 | gUsedKeys[gStrings.disabled] = gUsedKeys[gStrings.uponreset] = {length: 0} 346 | } 347 | 348 | function openEditor(type) { 349 | var key, code; 350 | switch(type) { 351 | case 1: 352 | key = gKeys[keyTree.currentIndex]; 353 | break; 354 | case 2: 355 | key = gKeys[keyTree.currentIndex]; 356 | if(key && !key.pref[3]) code = key.code; 357 | break; 358 | } 359 | 360 | openDialog('chrome://keyconfig/content/edit.xul', '_blank', 'resizable,modal', key, code, this); 361 | } 362 | 363 | function closeEditor(fields) { 364 | var key; 365 | 366 | if(fields.key) { 367 | key = fields.key; 368 | gPrefService.clearUserPref(gProfile+key.id); 369 | } else { 370 | key = { shortcut: gStrings.disabled, pref: ["!",null,null," "] } 371 | gKeys.push(key); 372 | 373 | keyTree.treeBoxObject.rowCountChanged(keyTree.view.rowCount-1,1); 374 | keyTree.view.selection.select(keyTree.view.rowCount-1); 375 | keyTree.treeBoxObject.ensureRowIsVisible(keyTree.view.rowCount-1); 376 | gEdit.focus(); 377 | } 378 | 379 | var currentId = key.id; 380 | 381 | if(key.name != fields.name.value) try { 382 | key.name = fields.name.value || "key"+Date.now(); 383 | 384 | var i = 1; 385 | do { 386 | key.id = "xxx_key"+(i++)+"_" + gUnicodeConverter.ConvertFromUnicode(key.name); 387 | } while(gPrefService.prefHasUserValue(gProfile+key.id)); 388 | } catch(err){ gUnicodeConverter.charset = "UTF-8"; key.id = "key"+Date.now(); } 389 | 390 | key.code = key.pref[3] = fields.code.value.replace(/]\[/g,"] [") || " "; 391 | 392 | key.pref[4] = fields.global.checked ? "" : gLocation; 393 | 394 | var value = key.pref.join("]["); 395 | try { 396 | // Gecko 58+ 397 | gPrefService.setStringPref(gProfile+key.id, value); 398 | } 399 | catch (e) { 400 | var str = Components.classes["@mozilla.org/supports-string;1"].createInstance(Components.interfaces.nsISupportsString); 401 | str.data = value; 402 | gPrefService.setComplexValue(gProfile+key.id, Components.interfaces.nsISupportsString, str); 403 | } 404 | 405 | var targets = WindowMediator.getEnumerator(null); 406 | var target; 407 | while(target = targets.getNext()) { 408 | if(key.pref[4] && target.location != key.pref[4]) 409 | continue; 410 | 411 | var node = target.document.getElementById(currentId); 412 | if(!node) 413 | node = target.keyconfig.removedKeys.appendChild(target.document.createElement("key")); 414 | 415 | node.id = key.id; 416 | //node.addEventListener("command",key.code); 417 | node.setAttribute("oncommand",key.code); 418 | 419 | } 420 | 421 | keyTree.treeBoxObject.invalidateRow(keyTree.currentIndex); 422 | } 423 | 424 | var keyView = { 425 | get rowCount() { return gKeys.length; }, 426 | getCellText : function(row,col){ return gKeys[row][col.id || col];}, 427 | setTree: function(treebox) { this.treebox=treebox; }, 428 | isContainer: function() { return false; }, 429 | isSeparator: function() { return false; }, 430 | isSorted: function() { return false; }, 431 | getLevel: function() { return 0; }, 432 | getImageSrc: function() { return null; }, 433 | getRowProperties: function(row, prop) { this.getCellProperties(row, "", prop); }, 434 | canDropBeforeAfter: function() { return false; }, 435 | canDrop: function() { return false; }, 436 | getParentIndex: function() { return -1; }, 437 | 438 | getCellProperties: function(row,col) { 439 | var key = gKeys[row]; 440 | // try { console.log ( key + " " + key.shortcut ) } catch(e) {}; 441 | //try { if (key.shortcut ) return "reset"; } catch(e) {console.log(e)} 442 | if(key.hardcoded) return "hardcoded"; 443 | if(key.disabled) return "disabled"; 444 | if(key.pref[3]) return "custom"; 445 | if(key.pref.length) return "user"; 446 | if((col.id || col) == "shortcut" && gUsedKeys[key.shortcut].length > 1) 447 | return "duplicate"; 448 | return ""; 449 | }, 450 | getColumnProperties: function(){}, 451 | selectionChanged: function() { 452 | var key = gKeys[this.selection.currentIndex]; 453 | 454 | if(!key) return; 455 | 456 | gExtra2.label = key.pref[3] ? gStrings.edit : gStrings.add; 457 | if(gEditbox.hasAttribute("disabled")) gEditbox.removeAttribute("disabled"); 458 | gEdit.key = key; 459 | gEdit.value = key.shortcut; 460 | }, 461 | cycleHeader: function cycleHeader(col, elem) { 462 | if(col.id) elem = col.element; 463 | 464 | var direction = elem.getAttribute("sortDirection") == "ascending" ? "descending" : "ascending"; 465 | var columns = this.treebox.firstChild.childNodes; 466 | for(var i = 0, l = columns.length; i < l; i++) { 467 | columns[i].setAttribute("sortDirection","none"); 468 | columns[i].setAttribute("sortActive",false); 469 | } 470 | 471 | elem.setAttribute("sortDirection",direction); 472 | elem.setAttribute("sortActive",true); 473 | 474 | var currentRow = gKeys[this.selection.currentIndex]; 475 | 476 | gKeys.sort(sorter[col.id || col]); 477 | if(direction == "descending") gKeys.reverse(); 478 | 479 | this.treebox.invalidate(); 480 | if(currentRow) { 481 | i = -1; 482 | do { i++; } while(currentRow != gKeys[i]); 483 | this.selection.select(i); 484 | this.treebox.ensureRowIsVisible(i); 485 | } 486 | } 487 | } 488 | 489 | function switchWindow(event) { 490 | var mediator = Components.classes["@mozilla.org/rdf/datasource;1?name=window-mediator"].getService(); 491 | mediator.QueryInterface(Components.interfaces.nsIWindowDataSource); 492 | 493 | var target = mediator.getWindowForResource(event.target.getAttribute('id')); 494 | 495 | if (target) init(target); 496 | } 497 | 498 | function getCodeFor(node) { 499 | if(node.hasAttribute("oncommand")) { 500 | return node.getAttribute("oncommand"); 501 | } else if(node.hasAttribute("command")) { 502 | var command = gDocument.getElementById(node.getAttribute("command")); 503 | if(command) return command.getAttribute("oncommand"); 504 | } else if(node.hasAttribute("observes")) { 505 | var observer = gDocument.getElementById(node.getAttribute("observes")); 506 | if(observer) return command.getAttribute("oncommand"); 507 | } 508 | return null; 509 | } 510 | 511 | function copyKey() { 512 | var key = gKeys[keyTree.currentIndex]; 513 | if(!key) return; 514 | 515 | var data = 'name: ' + key.name + ', id: ' + key.id + ', shortcut: ' + key.shortcut + ', code:\n' + key.code; 516 | 517 | gClipboardHelper.copyString(data); 518 | } 519 | -------------------------------------------------------------------------------- /content/keyconfig.xul: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | %keyconfig; %textcontext; 9 | ]> 10 | 11 | 16 | 17 | 42 | 43 |