1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/dom/inputmethod/MozKeyboard.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,608 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +"use strict"; 1.9 + 1.10 +const Cc = Components.classes; 1.11 +const Ci = Components.interfaces; 1.12 +const Cu = Components.utils; 1.13 + 1.14 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.15 +Cu.import("resource://gre/modules/Services.jsm"); 1.16 +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); 1.17 + 1.18 +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", 1.19 + "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"); 1.20 + 1.21 +XPCOMUtils.defineLazyServiceGetter(this, "tm", 1.22 + "@mozilla.org/thread-manager;1", "nsIThreadManager"); 1.23 + 1.24 +/* 1.25 + * A WeakMap to map input method iframe window to its active status. 1.26 + */ 1.27 +let WindowMap = { 1.28 + // WeakMap of <window, boolean> pairs. 1.29 + _map: null, 1.30 + 1.31 + /* 1.32 + * Check if the given window is active. 1.33 + */ 1.34 + isActive: function(win) { 1.35 + if (!this._map || !win) { 1.36 + return false; 1.37 + } 1.38 + return this._map.get(win, false); 1.39 + }, 1.40 + 1.41 + /* 1.42 + * Set the active status of the given window. 1.43 + */ 1.44 + setActive: function(win, isActive) { 1.45 + if (!win) { 1.46 + return; 1.47 + } 1.48 + if (!this._map) { 1.49 + this._map = new WeakMap(); 1.50 + } 1.51 + this._map.set(win, isActive); 1.52 + } 1.53 +}; 1.54 + 1.55 +/** 1.56 + * ============================================== 1.57 + * InputMethodManager 1.58 + * ============================================== 1.59 + */ 1.60 +function MozInputMethodManager(win) { 1.61 + this._window = win; 1.62 +} 1.63 + 1.64 +MozInputMethodManager.prototype = { 1.65 + _supportsSwitching: false, 1.66 + _window: null, 1.67 + 1.68 + classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"), 1.69 + 1.70 + QueryInterface: XPCOMUtils.generateQI([]), 1.71 + 1.72 + showAll: function() { 1.73 + if (!WindowMap.isActive(this._window)) { 1.74 + return; 1.75 + } 1.76 + cpmm.sendAsyncMessage('Keyboard:ShowInputMethodPicker', {}); 1.77 + }, 1.78 + 1.79 + next: function() { 1.80 + if (!WindowMap.isActive(this._window)) { 1.81 + return; 1.82 + } 1.83 + cpmm.sendAsyncMessage('Keyboard:SwitchToNextInputMethod', {}); 1.84 + }, 1.85 + 1.86 + supportsSwitching: function() { 1.87 + if (!WindowMap.isActive(this._window)) { 1.88 + return false; 1.89 + } 1.90 + return this._supportsSwitching; 1.91 + }, 1.92 + 1.93 + hide: function() { 1.94 + if (!WindowMap.isActive(this._window)) { 1.95 + return; 1.96 + } 1.97 + cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {}); 1.98 + } 1.99 +}; 1.100 + 1.101 +/** 1.102 + * ============================================== 1.103 + * InputMethod 1.104 + * ============================================== 1.105 + */ 1.106 +function MozInputMethod() { } 1.107 + 1.108 +MozInputMethod.prototype = { 1.109 + _inputcontext: null, 1.110 + _layouts: {}, 1.111 + _window: null, 1.112 + _isSystem: false, 1.113 + _isKeyboard: true, 1.114 + 1.115 + classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"), 1.116 + 1.117 + QueryInterface: XPCOMUtils.generateQI([ 1.118 + Ci.nsIDOMGlobalPropertyInitializer, 1.119 + Ci.nsIObserver, 1.120 + Ci.nsISupportsWeakReference 1.121 + ]), 1.122 + 1.123 + init: function mozInputMethodInit(win) { 1.124 + this._window = win; 1.125 + this._mgmt = new MozInputMethodManager(win); 1.126 + this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.127 + .getInterface(Ci.nsIDOMWindowUtils) 1.128 + .currentInnerWindowID; 1.129 + 1.130 + Services.obs.addObserver(this, "inner-window-destroyed", false); 1.131 + 1.132 + let principal = win.document.nodePrincipal; 1.133 + let perm = Services.perms.testExactPermissionFromPrincipal(principal, 1.134 + "input-manage"); 1.135 + if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) { 1.136 + this._isSystem = true; 1.137 + } 1.138 + 1.139 + // Check if we can use keyboard related APIs. 1.140 + let testing = false; 1.141 + try { 1.142 + testing = Services.prefs.getBoolPref("dom.mozInputMethod.testing"); 1.143 + } catch (e) { 1.144 + } 1.145 + perm = Services.perms.testExactPermissionFromPrincipal(principal, "input"); 1.146 + if (!testing && perm !== Ci.nsIPermissionManager.ALLOW_ACTION) { 1.147 + this._isKeyboard = false; 1.148 + return; 1.149 + } 1.150 + 1.151 + cpmm.addWeakMessageListener('Keyboard:FocusChange', this); 1.152 + cpmm.addWeakMessageListener('Keyboard:SelectionChange', this); 1.153 + cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this); 1.154 + cpmm.addWeakMessageListener('Keyboard:LayoutsChange', this); 1.155 + }, 1.156 + 1.157 + uninit: function mozInputMethodUninit() { 1.158 + this._window = null; 1.159 + this._mgmt = null; 1.160 + Services.obs.removeObserver(this, "inner-window-destroyed"); 1.161 + if (!this._isKeyboard) { 1.162 + return; 1.163 + } 1.164 + 1.165 + cpmm.removeWeakMessageListener('Keyboard:FocusChange', this); 1.166 + cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this); 1.167 + cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this); 1.168 + cpmm.removeWeakMessageListener('Keyboard:LayoutsChange', this); 1.169 + this.setActive(false); 1.170 + }, 1.171 + 1.172 + receiveMessage: function mozInputMethodReceiveMsg(msg) { 1.173 + if (!WindowMap.isActive(this._window)) { 1.174 + return; 1.175 + } 1.176 + 1.177 + let json = msg.json; 1.178 + 1.179 + switch(msg.name) { 1.180 + case 'Keyboard:FocusChange': 1.181 + if (json.type !== 'blur') { 1.182 + // XXX Bug 904339 could receive 'text' event twice 1.183 + this.setInputContext(json); 1.184 + } 1.185 + else { 1.186 + this.setInputContext(null); 1.187 + } 1.188 + break; 1.189 + case 'Keyboard:SelectionChange': 1.190 + if (this.inputcontext) { 1.191 + this._inputcontext.updateSelectionContext(json); 1.192 + } 1.193 + break; 1.194 + case 'Keyboard:GetContext:Result:OK': 1.195 + this.setInputContext(json); 1.196 + break; 1.197 + case 'Keyboard:LayoutsChange': 1.198 + this._layouts = json; 1.199 + break; 1.200 + } 1.201 + }, 1.202 + 1.203 + observe: function mozInputMethodObserve(subject, topic, data) { 1.204 + let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data; 1.205 + if (wId == this.innerWindowID) 1.206 + this.uninit(); 1.207 + }, 1.208 + 1.209 + get mgmt() { 1.210 + return this._mgmt; 1.211 + }, 1.212 + 1.213 + get inputcontext() { 1.214 + if (!WindowMap.isActive(this._window)) { 1.215 + return null; 1.216 + } 1.217 + return this._inputcontext; 1.218 + }, 1.219 + 1.220 + set oninputcontextchange(handler) { 1.221 + this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler); 1.222 + }, 1.223 + 1.224 + get oninputcontextchange() { 1.225 + return this.__DOM_IMPL__.getEventHandler("oninputcontextchange"); 1.226 + }, 1.227 + 1.228 + setInputContext: function mozKeyboardContextChange(data) { 1.229 + if (this._inputcontext) { 1.230 + this._inputcontext.destroy(); 1.231 + this._inputcontext = null; 1.232 + this._mgmt._supportsSwitching = false; 1.233 + } 1.234 + 1.235 + if (data) { 1.236 + this._mgmt._supportsSwitching = this._layouts[data.type] ? 1.237 + this._layouts[data.type] > 1 : 1.238 + false; 1.239 + 1.240 + this._inputcontext = new MozInputContext(data); 1.241 + this._inputcontext.init(this._window); 1.242 + } 1.243 + 1.244 + let event = new this._window.Event("inputcontextchange", 1.245 + Cu.cloneInto({}, this._window)); 1.246 + this.__DOM_IMPL__.dispatchEvent(event); 1.247 + }, 1.248 + 1.249 + setActive: function mozInputMethodSetActive(isActive) { 1.250 + if (WindowMap.isActive(this._window) === isActive) { 1.251 + return; 1.252 + } 1.253 + 1.254 + WindowMap.setActive(this._window, isActive); 1.255 + 1.256 + if (isActive) { 1.257 + // Activate current input method. 1.258 + // If there is already an active context, then this will trigger 1.259 + // a GetContext:Result:OK event, and we can initialize ourselves. 1.260 + // Otherwise silently ignored. 1.261 + cpmm.sendAsyncMessage('Keyboard:Register', {}); 1.262 + cpmm.sendAsyncMessage("Keyboard:GetContext", {}); 1.263 + } else { 1.264 + // Deactive current input method. 1.265 + cpmm.sendAsyncMessage('Keyboard:Unregister', {}); 1.266 + if (this._inputcontext) { 1.267 + this.setInputContext(null); 1.268 + } 1.269 + } 1.270 + }, 1.271 + 1.272 + setValue: function(value) { 1.273 + this._ensureIsSystem(); 1.274 + cpmm.sendAsyncMessage('System:SetValue', { 1.275 + 'value': value 1.276 + }); 1.277 + }, 1.278 + 1.279 + setSelectedOption: function(index) { 1.280 + this._ensureIsSystem(); 1.281 + cpmm.sendAsyncMessage('System:SetSelectedOption', { 1.282 + 'index': index 1.283 + }); 1.284 + }, 1.285 + 1.286 + setSelectedOptions: function(indexes) { 1.287 + this._ensureIsSystem(); 1.288 + cpmm.sendAsyncMessage('System:SetSelectedOptions', { 1.289 + 'indexes': indexes 1.290 + }); 1.291 + }, 1.292 + 1.293 + removeFocus: function() { 1.294 + this._ensureIsSystem(); 1.295 + cpmm.sendAsyncMessage('System:RemoveFocus', {}); 1.296 + }, 1.297 + 1.298 + _ensureIsSystem: function() { 1.299 + if (!this._isSystem) { 1.300 + throw new this._window.DOMError("Security", 1.301 + "Should have 'input-manage' permssion."); 1.302 + } 1.303 + } 1.304 +}; 1.305 + 1.306 + /** 1.307 + * ============================================== 1.308 + * InputContext 1.309 + * ============================================== 1.310 + */ 1.311 +function MozInputContext(ctx) { 1.312 + this._context = { 1.313 + inputtype: ctx.type, 1.314 + inputmode: ctx.inputmode, 1.315 + lang: ctx.lang, 1.316 + type: ["textarea", "contenteditable"].indexOf(ctx.type) > -1 ? 1.317 + ctx.type : 1.318 + "text", 1.319 + selectionStart: ctx.selectionStart, 1.320 + selectionEnd: ctx.selectionEnd, 1.321 + textBeforeCursor: ctx.textBeforeCursor, 1.322 + textAfterCursor: ctx.textAfterCursor 1.323 + }; 1.324 + 1.325 + this._contextId = ctx.contextId; 1.326 +} 1.327 + 1.328 +MozInputContext.prototype = { 1.329 + __proto__: DOMRequestIpcHelper.prototype, 1.330 + 1.331 + _window: null, 1.332 + _context: null, 1.333 + _contextId: -1, 1.334 + 1.335 + classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"), 1.336 + 1.337 + QueryInterface: XPCOMUtils.generateQI([ 1.338 + Ci.nsIObserver, 1.339 + Ci.nsISupportsWeakReference 1.340 + ]), 1.341 + 1.342 + init: function ic_init(win) { 1.343 + this._window = win; 1.344 + this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor) 1.345 + .getInterface(Ci.nsIDOMWindowUtils); 1.346 + this.initDOMRequestHelper(win, 1.347 + ["Keyboard:GetText:Result:OK", 1.348 + "Keyboard:GetText:Result:Error", 1.349 + "Keyboard:SetSelectionRange:Result:OK", 1.350 + "Keyboard:ReplaceSurroundingText:Result:OK", 1.351 + "Keyboard:SendKey:Result:OK", 1.352 + "Keyboard:SendKey:Result:Error", 1.353 + "Keyboard:SetComposition:Result:OK", 1.354 + "Keyboard:EndComposition:Result:OK", 1.355 + "Keyboard:SequenceError"]); 1.356 + }, 1.357 + 1.358 + destroy: function ic_destroy() { 1.359 + let self = this; 1.360 + 1.361 + // All requests that are still pending need to be invalidated 1.362 + // because the context is no longer valid. 1.363 + this.forEachPromiseResolver(function(k) { 1.364 + self.takePromiseResolver(k).reject("InputContext got destroyed"); 1.365 + }); 1.366 + this.destroyDOMRequestHelper(); 1.367 + 1.368 + // A consuming application might still hold a cached version of 1.369 + // this object. After destroying all methods will throw because we 1.370 + // cannot create new promises anymore, but we still hold 1.371 + // (outdated) information in the context. So let's clear that out. 1.372 + for (var k in this._context) { 1.373 + if (this._context.hasOwnProperty(k)) { 1.374 + this._context[k] = null; 1.375 + } 1.376 + } 1.377 + 1.378 + this._window = null; 1.379 + }, 1.380 + 1.381 + receiveMessage: function ic_receiveMessage(msg) { 1.382 + if (!msg || !msg.json) { 1.383 + dump('InputContext received message without data\n'); 1.384 + return; 1.385 + } 1.386 + 1.387 + let json = msg.json; 1.388 + let resolver = this.takePromiseResolver(json.requestId); 1.389 + 1.390 + if (!resolver) { 1.391 + return; 1.392 + } 1.393 + 1.394 + switch (msg.name) { 1.395 + case "Keyboard:SendKey:Result:OK": 1.396 + resolver.resolve(); 1.397 + break; 1.398 + case "Keyboard:SendKey:Result:Error": 1.399 + resolver.reject(json.error); 1.400 + break; 1.401 + case "Keyboard:GetText:Result:OK": 1.402 + resolver.resolve(json.text); 1.403 + break; 1.404 + case "Keyboard:GetText:Result:Error": 1.405 + resolver.reject(json.error); 1.406 + break; 1.407 + case "Keyboard:SetSelectionRange:Result:OK": 1.408 + case "Keyboard:ReplaceSurroundingText:Result:OK": 1.409 + resolver.resolve( 1.410 + Cu.cloneInto(json.selectioninfo, this._window)); 1.411 + break; 1.412 + case "Keyboard:SequenceError": 1.413 + // Occurs when a new element got focus, but the inputContext was 1.414 + // not invalidated yet... 1.415 + resolver.reject("InputContext has expired"); 1.416 + break; 1.417 + case "Keyboard:SetComposition:Result:OK": // Fall through. 1.418 + case "Keyboard:EndComposition:Result:OK": 1.419 + resolver.resolve(); 1.420 + break; 1.421 + default: 1.422 + dump("Could not find a handler for " + msg.name); 1.423 + resolver.reject(); 1.424 + break; 1.425 + } 1.426 + }, 1.427 + 1.428 + updateSelectionContext: function ic_updateSelectionContext(ctx) { 1.429 + if (!this._context) { 1.430 + return; 1.431 + } 1.432 + 1.433 + let selectionDirty = this._context.selectionStart !== ctx.selectionStart || 1.434 + this._context.selectionEnd !== ctx.selectionEnd; 1.435 + let surroundDirty = this._context.textBeforeCursor !== ctx.textBeforeCursor || 1.436 + this._context.textAfterCursor !== ctx.textAfterCursor; 1.437 + 1.438 + this._context.selectionStart = ctx.selectionStart; 1.439 + this._context.selectionEnd = ctx.selectionEnd; 1.440 + this._context.textBeforeCursor = ctx.textBeforeCursor; 1.441 + this._context.textAfterCursor = ctx.textAfterCursor; 1.442 + 1.443 + if (selectionDirty) { 1.444 + this._fireEvent("selectionchange", { 1.445 + selectionStart: ctx.selectionStart, 1.446 + selectionEnd: ctx.selectionEnd 1.447 + }); 1.448 + } 1.449 + 1.450 + if (surroundDirty) { 1.451 + this._fireEvent("surroundingtextchange", { 1.452 + beforeString: ctx.textBeforeCursor, 1.453 + afterString: ctx.textAfterCursor 1.454 + }); 1.455 + } 1.456 + }, 1.457 + 1.458 + _fireEvent: function ic_fireEvent(eventName, aDetail) { 1.459 + let detail = { 1.460 + detail: aDetail 1.461 + }; 1.462 + 1.463 + let event = new this._window.Event(eventName, 1.464 + Cu.cloneInto(aDetail, this._window)); 1.465 + this.__DOM_IMPL__.dispatchEvent(event); 1.466 + }, 1.467 + 1.468 + // tag name of the input field 1.469 + get type() { 1.470 + return this._context.type; 1.471 + }, 1.472 + 1.473 + // type of the input field 1.474 + get inputType() { 1.475 + return this._context.inputtype; 1.476 + }, 1.477 + 1.478 + get inputMode() { 1.479 + return this._context.inputmode; 1.480 + }, 1.481 + 1.482 + get lang() { 1.483 + return this._context.lang; 1.484 + }, 1.485 + 1.486 + getText: function ic_getText(offset, length) { 1.487 + let self = this; 1.488 + return this._sendPromise(function(resolverId) { 1.489 + cpmm.sendAsyncMessage('Keyboard:GetText', { 1.490 + contextId: self._contextId, 1.491 + requestId: resolverId, 1.492 + offset: offset, 1.493 + length: length 1.494 + }); 1.495 + }); 1.496 + }, 1.497 + 1.498 + get selectionStart() { 1.499 + return this._context.selectionStart; 1.500 + }, 1.501 + 1.502 + get selectionEnd() { 1.503 + return this._context.selectionEnd; 1.504 + }, 1.505 + 1.506 + get textBeforeCursor() { 1.507 + return this._context.textBeforeCursor; 1.508 + }, 1.509 + 1.510 + get textAfterCursor() { 1.511 + return this._context.textAfterCursor; 1.512 + }, 1.513 + 1.514 + setSelectionRange: function ic_setSelectionRange(start, length) { 1.515 + let self = this; 1.516 + return this._sendPromise(function(resolverId) { 1.517 + cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", { 1.518 + contextId: self._contextId, 1.519 + requestId: resolverId, 1.520 + selectionStart: start, 1.521 + selectionEnd: start + length 1.522 + }); 1.523 + }); 1.524 + }, 1.525 + 1.526 + get onsurroundingtextchange() { 1.527 + return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange"); 1.528 + }, 1.529 + 1.530 + set onsurroundingtextchange(handler) { 1.531 + this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler); 1.532 + }, 1.533 + 1.534 + get onselectionchange() { 1.535 + return this.__DOM_IMPL__.getEventHandler("onselectionchange"); 1.536 + }, 1.537 + 1.538 + set onselectionchange(handler) { 1.539 + this.__DOM_IMPL__.setEventHandler("onselectionchange", handler); 1.540 + }, 1.541 + 1.542 + replaceSurroundingText: function ic_replaceSurrText(text, offset, length) { 1.543 + let self = this; 1.544 + return this._sendPromise(function(resolverId) { 1.545 + cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', { 1.546 + contextId: self._contextId, 1.547 + requestId: resolverId, 1.548 + text: text, 1.549 + offset: offset || 0, 1.550 + length: length || 0 1.551 + }); 1.552 + }); 1.553 + }, 1.554 + 1.555 + deleteSurroundingText: function ic_deleteSurrText(offset, length) { 1.556 + return this.replaceSurroundingText(null, offset, length); 1.557 + }, 1.558 + 1.559 + sendKey: function ic_sendKey(keyCode, charCode, modifiers, repeat) { 1.560 + let self = this; 1.561 + return this._sendPromise(function(resolverId) { 1.562 + cpmm.sendAsyncMessage('Keyboard:SendKey', { 1.563 + contextId: self._contextId, 1.564 + requestId: resolverId, 1.565 + keyCode: keyCode, 1.566 + charCode: charCode, 1.567 + modifiers: modifiers, 1.568 + repeat: repeat 1.569 + }); 1.570 + }); 1.571 + }, 1.572 + 1.573 + setComposition: function ic_setComposition(text, cursor, clauses) { 1.574 + let self = this; 1.575 + return this._sendPromise(function(resolverId) { 1.576 + cpmm.sendAsyncMessage('Keyboard:SetComposition', { 1.577 + contextId: self._contextId, 1.578 + requestId: resolverId, 1.579 + text: text, 1.580 + cursor: cursor || text.length, 1.581 + clauses: clauses || null 1.582 + }); 1.583 + }); 1.584 + }, 1.585 + 1.586 + endComposition: function ic_endComposition(text) { 1.587 + let self = this; 1.588 + return this._sendPromise(function(resolverId) { 1.589 + cpmm.sendAsyncMessage('Keyboard:EndComposition', { 1.590 + contextId: self._contextId, 1.591 + requestId: resolverId, 1.592 + text: text || '' 1.593 + }); 1.594 + }); 1.595 + }, 1.596 + 1.597 + _sendPromise: function(callback) { 1.598 + let self = this; 1.599 + return this.createPromise(function(resolve, reject) { 1.600 + let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject }); 1.601 + if (!WindowMap.isActive(self._window)) { 1.602 + self.removePromiseResolver(resolverId); 1.603 + reject('Input method is not active.'); 1.604 + return; 1.605 + } 1.606 + callback(resolverId); 1.607 + }); 1.608 + } 1.609 +}; 1.610 + 1.611 +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]);