dom/inputmethod/MozKeyboard.js

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:18ce3faab8ee
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 file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 "use strict";
6
7 const Cc = Components.classes;
8 const Ci = Components.interfaces;
9 const Cu = Components.utils;
10
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
12 Cu.import("resource://gre/modules/Services.jsm");
13 Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
14
15 XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
16 "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender");
17
18 XPCOMUtils.defineLazyServiceGetter(this, "tm",
19 "@mozilla.org/thread-manager;1", "nsIThreadManager");
20
21 /*
22 * A WeakMap to map input method iframe window to its active status.
23 */
24 let WindowMap = {
25 // WeakMap of <window, boolean> pairs.
26 _map: null,
27
28 /*
29 * Check if the given window is active.
30 */
31 isActive: function(win) {
32 if (!this._map || !win) {
33 return false;
34 }
35 return this._map.get(win, false);
36 },
37
38 /*
39 * Set the active status of the given window.
40 */
41 setActive: function(win, isActive) {
42 if (!win) {
43 return;
44 }
45 if (!this._map) {
46 this._map = new WeakMap();
47 }
48 this._map.set(win, isActive);
49 }
50 };
51
52 /**
53 * ==============================================
54 * InputMethodManager
55 * ==============================================
56 */
57 function MozInputMethodManager(win) {
58 this._window = win;
59 }
60
61 MozInputMethodManager.prototype = {
62 _supportsSwitching: false,
63 _window: null,
64
65 classID: Components.ID("{7e9d7280-ef86-11e2-b778-0800200c9a66}"),
66
67 QueryInterface: XPCOMUtils.generateQI([]),
68
69 showAll: function() {
70 if (!WindowMap.isActive(this._window)) {
71 return;
72 }
73 cpmm.sendAsyncMessage('Keyboard:ShowInputMethodPicker', {});
74 },
75
76 next: function() {
77 if (!WindowMap.isActive(this._window)) {
78 return;
79 }
80 cpmm.sendAsyncMessage('Keyboard:SwitchToNextInputMethod', {});
81 },
82
83 supportsSwitching: function() {
84 if (!WindowMap.isActive(this._window)) {
85 return false;
86 }
87 return this._supportsSwitching;
88 },
89
90 hide: function() {
91 if (!WindowMap.isActive(this._window)) {
92 return;
93 }
94 cpmm.sendAsyncMessage('Keyboard:RemoveFocus', {});
95 }
96 };
97
98 /**
99 * ==============================================
100 * InputMethod
101 * ==============================================
102 */
103 function MozInputMethod() { }
104
105 MozInputMethod.prototype = {
106 _inputcontext: null,
107 _layouts: {},
108 _window: null,
109 _isSystem: false,
110 _isKeyboard: true,
111
112 classID: Components.ID("{4607330d-e7d2-40a4-9eb8-43967eae0142}"),
113
114 QueryInterface: XPCOMUtils.generateQI([
115 Ci.nsIDOMGlobalPropertyInitializer,
116 Ci.nsIObserver,
117 Ci.nsISupportsWeakReference
118 ]),
119
120 init: function mozInputMethodInit(win) {
121 this._window = win;
122 this._mgmt = new MozInputMethodManager(win);
123 this.innerWindowID = win.QueryInterface(Ci.nsIInterfaceRequestor)
124 .getInterface(Ci.nsIDOMWindowUtils)
125 .currentInnerWindowID;
126
127 Services.obs.addObserver(this, "inner-window-destroyed", false);
128
129 let principal = win.document.nodePrincipal;
130 let perm = Services.perms.testExactPermissionFromPrincipal(principal,
131 "input-manage");
132 if (perm === Ci.nsIPermissionManager.ALLOW_ACTION) {
133 this._isSystem = true;
134 }
135
136 // Check if we can use keyboard related APIs.
137 let testing = false;
138 try {
139 testing = Services.prefs.getBoolPref("dom.mozInputMethod.testing");
140 } catch (e) {
141 }
142 perm = Services.perms.testExactPermissionFromPrincipal(principal, "input");
143 if (!testing && perm !== Ci.nsIPermissionManager.ALLOW_ACTION) {
144 this._isKeyboard = false;
145 return;
146 }
147
148 cpmm.addWeakMessageListener('Keyboard:FocusChange', this);
149 cpmm.addWeakMessageListener('Keyboard:SelectionChange', this);
150 cpmm.addWeakMessageListener('Keyboard:GetContext:Result:OK', this);
151 cpmm.addWeakMessageListener('Keyboard:LayoutsChange', this);
152 },
153
154 uninit: function mozInputMethodUninit() {
155 this._window = null;
156 this._mgmt = null;
157 Services.obs.removeObserver(this, "inner-window-destroyed");
158 if (!this._isKeyboard) {
159 return;
160 }
161
162 cpmm.removeWeakMessageListener('Keyboard:FocusChange', this);
163 cpmm.removeWeakMessageListener('Keyboard:SelectionChange', this);
164 cpmm.removeWeakMessageListener('Keyboard:GetContext:Result:OK', this);
165 cpmm.removeWeakMessageListener('Keyboard:LayoutsChange', this);
166 this.setActive(false);
167 },
168
169 receiveMessage: function mozInputMethodReceiveMsg(msg) {
170 if (!WindowMap.isActive(this._window)) {
171 return;
172 }
173
174 let json = msg.json;
175
176 switch(msg.name) {
177 case 'Keyboard:FocusChange':
178 if (json.type !== 'blur') {
179 // XXX Bug 904339 could receive 'text' event twice
180 this.setInputContext(json);
181 }
182 else {
183 this.setInputContext(null);
184 }
185 break;
186 case 'Keyboard:SelectionChange':
187 if (this.inputcontext) {
188 this._inputcontext.updateSelectionContext(json);
189 }
190 break;
191 case 'Keyboard:GetContext:Result:OK':
192 this.setInputContext(json);
193 break;
194 case 'Keyboard:LayoutsChange':
195 this._layouts = json;
196 break;
197 }
198 },
199
200 observe: function mozInputMethodObserve(subject, topic, data) {
201 let wId = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
202 if (wId == this.innerWindowID)
203 this.uninit();
204 },
205
206 get mgmt() {
207 return this._mgmt;
208 },
209
210 get inputcontext() {
211 if (!WindowMap.isActive(this._window)) {
212 return null;
213 }
214 return this._inputcontext;
215 },
216
217 set oninputcontextchange(handler) {
218 this.__DOM_IMPL__.setEventHandler("oninputcontextchange", handler);
219 },
220
221 get oninputcontextchange() {
222 return this.__DOM_IMPL__.getEventHandler("oninputcontextchange");
223 },
224
225 setInputContext: function mozKeyboardContextChange(data) {
226 if (this._inputcontext) {
227 this._inputcontext.destroy();
228 this._inputcontext = null;
229 this._mgmt._supportsSwitching = false;
230 }
231
232 if (data) {
233 this._mgmt._supportsSwitching = this._layouts[data.type] ?
234 this._layouts[data.type] > 1 :
235 false;
236
237 this._inputcontext = new MozInputContext(data);
238 this._inputcontext.init(this._window);
239 }
240
241 let event = new this._window.Event("inputcontextchange",
242 Cu.cloneInto({}, this._window));
243 this.__DOM_IMPL__.dispatchEvent(event);
244 },
245
246 setActive: function mozInputMethodSetActive(isActive) {
247 if (WindowMap.isActive(this._window) === isActive) {
248 return;
249 }
250
251 WindowMap.setActive(this._window, isActive);
252
253 if (isActive) {
254 // Activate current input method.
255 // If there is already an active context, then this will trigger
256 // a GetContext:Result:OK event, and we can initialize ourselves.
257 // Otherwise silently ignored.
258 cpmm.sendAsyncMessage('Keyboard:Register', {});
259 cpmm.sendAsyncMessage("Keyboard:GetContext", {});
260 } else {
261 // Deactive current input method.
262 cpmm.sendAsyncMessage('Keyboard:Unregister', {});
263 if (this._inputcontext) {
264 this.setInputContext(null);
265 }
266 }
267 },
268
269 setValue: function(value) {
270 this._ensureIsSystem();
271 cpmm.sendAsyncMessage('System:SetValue', {
272 'value': value
273 });
274 },
275
276 setSelectedOption: function(index) {
277 this._ensureIsSystem();
278 cpmm.sendAsyncMessage('System:SetSelectedOption', {
279 'index': index
280 });
281 },
282
283 setSelectedOptions: function(indexes) {
284 this._ensureIsSystem();
285 cpmm.sendAsyncMessage('System:SetSelectedOptions', {
286 'indexes': indexes
287 });
288 },
289
290 removeFocus: function() {
291 this._ensureIsSystem();
292 cpmm.sendAsyncMessage('System:RemoveFocus', {});
293 },
294
295 _ensureIsSystem: function() {
296 if (!this._isSystem) {
297 throw new this._window.DOMError("Security",
298 "Should have 'input-manage' permssion.");
299 }
300 }
301 };
302
303 /**
304 * ==============================================
305 * InputContext
306 * ==============================================
307 */
308 function MozInputContext(ctx) {
309 this._context = {
310 inputtype: ctx.type,
311 inputmode: ctx.inputmode,
312 lang: ctx.lang,
313 type: ["textarea", "contenteditable"].indexOf(ctx.type) > -1 ?
314 ctx.type :
315 "text",
316 selectionStart: ctx.selectionStart,
317 selectionEnd: ctx.selectionEnd,
318 textBeforeCursor: ctx.textBeforeCursor,
319 textAfterCursor: ctx.textAfterCursor
320 };
321
322 this._contextId = ctx.contextId;
323 }
324
325 MozInputContext.prototype = {
326 __proto__: DOMRequestIpcHelper.prototype,
327
328 _window: null,
329 _context: null,
330 _contextId: -1,
331
332 classID: Components.ID("{1e38633d-d08b-4867-9944-afa5c648adb6}"),
333
334 QueryInterface: XPCOMUtils.generateQI([
335 Ci.nsIObserver,
336 Ci.nsISupportsWeakReference
337 ]),
338
339 init: function ic_init(win) {
340 this._window = win;
341 this._utils = win.QueryInterface(Ci.nsIInterfaceRequestor)
342 .getInterface(Ci.nsIDOMWindowUtils);
343 this.initDOMRequestHelper(win,
344 ["Keyboard:GetText:Result:OK",
345 "Keyboard:GetText:Result:Error",
346 "Keyboard:SetSelectionRange:Result:OK",
347 "Keyboard:ReplaceSurroundingText:Result:OK",
348 "Keyboard:SendKey:Result:OK",
349 "Keyboard:SendKey:Result:Error",
350 "Keyboard:SetComposition:Result:OK",
351 "Keyboard:EndComposition:Result:OK",
352 "Keyboard:SequenceError"]);
353 },
354
355 destroy: function ic_destroy() {
356 let self = this;
357
358 // All requests that are still pending need to be invalidated
359 // because the context is no longer valid.
360 this.forEachPromiseResolver(function(k) {
361 self.takePromiseResolver(k).reject("InputContext got destroyed");
362 });
363 this.destroyDOMRequestHelper();
364
365 // A consuming application might still hold a cached version of
366 // this object. After destroying all methods will throw because we
367 // cannot create new promises anymore, but we still hold
368 // (outdated) information in the context. So let's clear that out.
369 for (var k in this._context) {
370 if (this._context.hasOwnProperty(k)) {
371 this._context[k] = null;
372 }
373 }
374
375 this._window = null;
376 },
377
378 receiveMessage: function ic_receiveMessage(msg) {
379 if (!msg || !msg.json) {
380 dump('InputContext received message without data\n');
381 return;
382 }
383
384 let json = msg.json;
385 let resolver = this.takePromiseResolver(json.requestId);
386
387 if (!resolver) {
388 return;
389 }
390
391 switch (msg.name) {
392 case "Keyboard:SendKey:Result:OK":
393 resolver.resolve();
394 break;
395 case "Keyboard:SendKey:Result:Error":
396 resolver.reject(json.error);
397 break;
398 case "Keyboard:GetText:Result:OK":
399 resolver.resolve(json.text);
400 break;
401 case "Keyboard:GetText:Result:Error":
402 resolver.reject(json.error);
403 break;
404 case "Keyboard:SetSelectionRange:Result:OK":
405 case "Keyboard:ReplaceSurroundingText:Result:OK":
406 resolver.resolve(
407 Cu.cloneInto(json.selectioninfo, this._window));
408 break;
409 case "Keyboard:SequenceError":
410 // Occurs when a new element got focus, but the inputContext was
411 // not invalidated yet...
412 resolver.reject("InputContext has expired");
413 break;
414 case "Keyboard:SetComposition:Result:OK": // Fall through.
415 case "Keyboard:EndComposition:Result:OK":
416 resolver.resolve();
417 break;
418 default:
419 dump("Could not find a handler for " + msg.name);
420 resolver.reject();
421 break;
422 }
423 },
424
425 updateSelectionContext: function ic_updateSelectionContext(ctx) {
426 if (!this._context) {
427 return;
428 }
429
430 let selectionDirty = this._context.selectionStart !== ctx.selectionStart ||
431 this._context.selectionEnd !== ctx.selectionEnd;
432 let surroundDirty = this._context.textBeforeCursor !== ctx.textBeforeCursor ||
433 this._context.textAfterCursor !== ctx.textAfterCursor;
434
435 this._context.selectionStart = ctx.selectionStart;
436 this._context.selectionEnd = ctx.selectionEnd;
437 this._context.textBeforeCursor = ctx.textBeforeCursor;
438 this._context.textAfterCursor = ctx.textAfterCursor;
439
440 if (selectionDirty) {
441 this._fireEvent("selectionchange", {
442 selectionStart: ctx.selectionStart,
443 selectionEnd: ctx.selectionEnd
444 });
445 }
446
447 if (surroundDirty) {
448 this._fireEvent("surroundingtextchange", {
449 beforeString: ctx.textBeforeCursor,
450 afterString: ctx.textAfterCursor
451 });
452 }
453 },
454
455 _fireEvent: function ic_fireEvent(eventName, aDetail) {
456 let detail = {
457 detail: aDetail
458 };
459
460 let event = new this._window.Event(eventName,
461 Cu.cloneInto(aDetail, this._window));
462 this.__DOM_IMPL__.dispatchEvent(event);
463 },
464
465 // tag name of the input field
466 get type() {
467 return this._context.type;
468 },
469
470 // type of the input field
471 get inputType() {
472 return this._context.inputtype;
473 },
474
475 get inputMode() {
476 return this._context.inputmode;
477 },
478
479 get lang() {
480 return this._context.lang;
481 },
482
483 getText: function ic_getText(offset, length) {
484 let self = this;
485 return this._sendPromise(function(resolverId) {
486 cpmm.sendAsyncMessage('Keyboard:GetText', {
487 contextId: self._contextId,
488 requestId: resolverId,
489 offset: offset,
490 length: length
491 });
492 });
493 },
494
495 get selectionStart() {
496 return this._context.selectionStart;
497 },
498
499 get selectionEnd() {
500 return this._context.selectionEnd;
501 },
502
503 get textBeforeCursor() {
504 return this._context.textBeforeCursor;
505 },
506
507 get textAfterCursor() {
508 return this._context.textAfterCursor;
509 },
510
511 setSelectionRange: function ic_setSelectionRange(start, length) {
512 let self = this;
513 return this._sendPromise(function(resolverId) {
514 cpmm.sendAsyncMessage("Keyboard:SetSelectionRange", {
515 contextId: self._contextId,
516 requestId: resolverId,
517 selectionStart: start,
518 selectionEnd: start + length
519 });
520 });
521 },
522
523 get onsurroundingtextchange() {
524 return this.__DOM_IMPL__.getEventHandler("onsurroundingtextchange");
525 },
526
527 set onsurroundingtextchange(handler) {
528 this.__DOM_IMPL__.setEventHandler("onsurroundingtextchange", handler);
529 },
530
531 get onselectionchange() {
532 return this.__DOM_IMPL__.getEventHandler("onselectionchange");
533 },
534
535 set onselectionchange(handler) {
536 this.__DOM_IMPL__.setEventHandler("onselectionchange", handler);
537 },
538
539 replaceSurroundingText: function ic_replaceSurrText(text, offset, length) {
540 let self = this;
541 return this._sendPromise(function(resolverId) {
542 cpmm.sendAsyncMessage('Keyboard:ReplaceSurroundingText', {
543 contextId: self._contextId,
544 requestId: resolverId,
545 text: text,
546 offset: offset || 0,
547 length: length || 0
548 });
549 });
550 },
551
552 deleteSurroundingText: function ic_deleteSurrText(offset, length) {
553 return this.replaceSurroundingText(null, offset, length);
554 },
555
556 sendKey: function ic_sendKey(keyCode, charCode, modifiers, repeat) {
557 let self = this;
558 return this._sendPromise(function(resolverId) {
559 cpmm.sendAsyncMessage('Keyboard:SendKey', {
560 contextId: self._contextId,
561 requestId: resolverId,
562 keyCode: keyCode,
563 charCode: charCode,
564 modifiers: modifiers,
565 repeat: repeat
566 });
567 });
568 },
569
570 setComposition: function ic_setComposition(text, cursor, clauses) {
571 let self = this;
572 return this._sendPromise(function(resolverId) {
573 cpmm.sendAsyncMessage('Keyboard:SetComposition', {
574 contextId: self._contextId,
575 requestId: resolverId,
576 text: text,
577 cursor: cursor || text.length,
578 clauses: clauses || null
579 });
580 });
581 },
582
583 endComposition: function ic_endComposition(text) {
584 let self = this;
585 return this._sendPromise(function(resolverId) {
586 cpmm.sendAsyncMessage('Keyboard:EndComposition', {
587 contextId: self._contextId,
588 requestId: resolverId,
589 text: text || ''
590 });
591 });
592 },
593
594 _sendPromise: function(callback) {
595 let self = this;
596 return this.createPromise(function(resolve, reject) {
597 let resolverId = self.getPromiseResolverId({ resolve: resolve, reject: reject });
598 if (!WindowMap.isActive(self._window)) {
599 self.removePromiseResolver(resolverId);
600 reject('Input method is not active.');
601 return;
602 }
603 callback(resolverId);
604 });
605 }
606 };
607
608 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MozInputMethod]);

mercurial