accessible/src/jsat/AccessFu.jsm

branch
TOR_BUG_9701
changeset 3
141e0f1194b1
equal deleted inserted replaced
-1:000000000000 0:e8834ba60e81
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 const Cr = Components.results;
11
12 this.EXPORTED_SYMBOLS = ['AccessFu'];
13
14 Cu.import('resource://gre/modules/Services.jsm');
15
16 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
17
18 const ACCESSFU_DISABLE = 0;
19 const ACCESSFU_ENABLE = 1;
20 const ACCESSFU_AUTO = 2;
21
22 const SCREENREADER_SETTING = 'accessibility.screenreader';
23
24 this.AccessFu = {
25 /**
26 * Initialize chrome-layer accessibility functionality.
27 * If accessibility is enabled on the platform, then a special accessibility
28 * mode is started.
29 */
30 attach: function attach(aWindow) {
31 Utils.init(aWindow);
32
33 try {
34 Services.androidBridge.handleGeckoMessage(
35 { type: 'Accessibility:Ready' });
36 Services.obs.addObserver(this, 'Accessibility:Settings', false);
37 } catch (x) {
38 // Not on Android
39 if (aWindow.navigator.mozSettings) {
40 let lock = aWindow.navigator.mozSettings.createLock();
41 let req = lock.get(SCREENREADER_SETTING);
42 req.addEventListener('success', () => {
43 this._systemPref = req.result[SCREENREADER_SETTING];
44 this._enableOrDisable();
45 });
46 aWindow.navigator.mozSettings.addObserver(
47 SCREENREADER_SETTING, this.handleEvent.bind(this));
48 }
49 }
50
51 this._activatePref = new PrefCache(
52 'accessibility.accessfu.activate', this._enableOrDisable.bind(this));
53
54 this._enableOrDisable();
55 },
56
57 /**
58 * Shut down chrome-layer accessibility functionality from the outside.
59 */
60 detach: function detach() {
61 // Avoid disabling twice.
62 if (this._enabled) {
63 this._disable();
64 }
65 if (Utils.MozBuildApp === 'mobile/android') {
66 Services.obs.removeObserver(this, 'Accessibility:Settings');
67 } else if (Utils.win.navigator.mozSettings) {
68 Utils.win.navigator.mozSettings.removeObserver(
69 SCREENREADER_SETTING, this.handleEvent.bind(this));
70 }
71 delete this._activatePref;
72 Utils.uninit();
73 },
74
75 /**
76 * Start AccessFu mode, this primarily means controlling the virtual cursor
77 * with arrow keys.
78 */
79 _enable: function _enable() {
80 if (this._enabled)
81 return;
82 this._enabled = true;
83
84 Cu.import('resource://gre/modules/accessibility/Utils.jsm');
85 Cu.import('resource://gre/modules/accessibility/PointerAdapter.jsm');
86 Cu.import('resource://gre/modules/accessibility/Presentation.jsm');
87
88 Logger.info('Enabled');
89
90 for each (let mm in Utils.AllMessageManagers) {
91 this._addMessageListeners(mm);
92 this._loadFrameScript(mm);
93 }
94
95 // Add stylesheet
96 let stylesheetURL = 'chrome://global/content/accessibility/AccessFu.css';
97 let stylesheet = Utils.win.document.createProcessingInstruction(
98 'xml-stylesheet', 'href="' + stylesheetURL + '" type="text/css"');
99 Utils.win.document.insertBefore(stylesheet, Utils.win.document.firstChild);
100 this.stylesheet = Cu.getWeakReference(stylesheet);
101
102
103 // Populate quicknav modes
104 this._quicknavModesPref =
105 new PrefCache(
106 'accessibility.accessfu.quicknav_modes',
107 (aName, aValue) => {
108 this.Input.quickNavMode.updateModes(aValue);
109 }, true);
110
111 // Check for output notification
112 this._notifyOutputPref =
113 new PrefCache('accessibility.accessfu.notify_output');
114
115
116 this.Input.start();
117 Output.start();
118 PointerAdapter.start();
119
120 Services.obs.addObserver(this, 'remote-browser-shown', false);
121 Services.obs.addObserver(this, 'inprocess-browser-shown', false);
122 Services.obs.addObserver(this, 'Accessibility:NextObject', false);
123 Services.obs.addObserver(this, 'Accessibility:PreviousObject', false);
124 Services.obs.addObserver(this, 'Accessibility:Focus', false);
125 Services.obs.addObserver(this, 'Accessibility:ActivateObject', false);
126 Services.obs.addObserver(this, 'Accessibility:LongPress', false);
127 Services.obs.addObserver(this, 'Accessibility:MoveByGranularity', false);
128 Utils.win.addEventListener('TabOpen', this);
129 Utils.win.addEventListener('TabClose', this);
130 Utils.win.addEventListener('TabSelect', this);
131
132 if (this.readyCallback) {
133 this.readyCallback();
134 delete this.readyCallback;
135 }
136
137 if (Utils.MozBuildApp !== 'mobile/android') {
138 this.announce(
139 Utils.stringBundle.GetStringFromName('screenReaderStarted'));
140 }
141 },
142
143 /**
144 * Disable AccessFu and return to default interaction mode.
145 */
146 _disable: function _disable() {
147 if (!this._enabled)
148 return;
149
150 this._enabled = false;
151
152 Logger.info('Disabled');
153
154 Utils.win.document.removeChild(this.stylesheet.get());
155
156 if (Utils.MozBuildApp !== 'mobile/android') {
157 this.announce(
158 Utils.stringBundle.GetStringFromName('screenReaderStopped'));
159 }
160
161 for each (let mm in Utils.AllMessageManagers) {
162 mm.sendAsyncMessage('AccessFu:Stop');
163 this._removeMessageListeners(mm);
164 }
165
166 this.Input.stop();
167 Output.stop();
168 PointerAdapter.stop();
169
170 Utils.win.removeEventListener('TabOpen', this);
171 Utils.win.removeEventListener('TabClose', this);
172 Utils.win.removeEventListener('TabSelect', this);
173
174 Services.obs.removeObserver(this, 'remote-browser-shown');
175 Services.obs.removeObserver(this, 'inprocess-browser-shown');
176 Services.obs.removeObserver(this, 'Accessibility:NextObject');
177 Services.obs.removeObserver(this, 'Accessibility:PreviousObject');
178 Services.obs.removeObserver(this, 'Accessibility:Focus');
179 Services.obs.removeObserver(this, 'Accessibility:ActivateObject');
180 Services.obs.removeObserver(this, 'Accessibility:LongPress');
181 Services.obs.removeObserver(this, 'Accessibility:MoveByGranularity');
182
183 delete this._quicknavModesPref;
184 delete this._notifyOutputPref;
185
186 if (this.doneCallback) {
187 this.doneCallback();
188 delete this.doneCallback;
189 }
190 },
191
192 _enableOrDisable: function _enableOrDisable() {
193 try {
194 if (!this._activatePref) {
195 return;
196 }
197 let activatePref = this._activatePref.value;
198 if (activatePref == ACCESSFU_ENABLE ||
199 this._systemPref && activatePref == ACCESSFU_AUTO)
200 this._enable();
201 else
202 this._disable();
203 } catch (x) {
204 dump('Error ' + x.message + ' ' + x.fileName + ':' + x.lineNumber);
205 }
206 },
207
208 receiveMessage: function receiveMessage(aMessage) {
209 Logger.debug(() => {
210 return ['Recieved', aMessage.name, JSON.stringify(aMessage.json)];
211 });
212
213 switch (aMessage.name) {
214 case 'AccessFu:Ready':
215 let mm = Utils.getMessageManager(aMessage.target);
216 if (this._enabled) {
217 mm.sendAsyncMessage('AccessFu:Start',
218 {method: 'start', buildApp: Utils.MozBuildApp});
219 }
220 break;
221 case 'AccessFu:Present':
222 this._output(aMessage.json, aMessage.target);
223 break;
224 case 'AccessFu:Input':
225 this.Input.setEditState(aMessage.json);
226 break;
227 case 'AccessFu:ActivateContextMenu':
228 this.Input.activateContextMenu(aMessage.json);
229 break;
230 case 'AccessFu:DoScroll':
231 this.Input.doScroll(aMessage.json);
232 break;
233 }
234 },
235
236 _output: function _output(aPresentationData, aBrowser) {
237 for each (let presenter in aPresentationData) {
238 if (!presenter)
239 continue;
240
241 try {
242 Output[presenter.type](presenter.details, aBrowser);
243 } catch (x) {
244 Logger.logException(x);
245 }
246 }
247
248 if (this._notifyOutputPref.value) {
249 Services.obs.notifyObservers(null, 'accessfu-output',
250 JSON.stringify(aPresentationData));
251 }
252 },
253
254 _loadFrameScript: function _loadFrameScript(aMessageManager) {
255 if (this._processedMessageManagers.indexOf(aMessageManager) < 0) {
256 aMessageManager.loadFrameScript(
257 'chrome://global/content/accessibility/content-script.js', true);
258 this._processedMessageManagers.push(aMessageManager);
259 } else if (this._enabled) {
260 // If the content-script is already loaded and AccessFu is enabled,
261 // send an AccessFu:Start message.
262 aMessageManager.sendAsyncMessage('AccessFu:Start',
263 {method: 'start', buildApp: Utils.MozBuildApp});
264 }
265 },
266
267 _addMessageListeners: function _addMessageListeners(aMessageManager) {
268 aMessageManager.addMessageListener('AccessFu:Present', this);
269 aMessageManager.addMessageListener('AccessFu:Input', this);
270 aMessageManager.addMessageListener('AccessFu:Ready', this);
271 aMessageManager.addMessageListener('AccessFu:ActivateContextMenu', this);
272 aMessageManager.addMessageListener('AccessFu:DoScroll', this);
273 },
274
275 _removeMessageListeners: function _removeMessageListeners(aMessageManager) {
276 aMessageManager.removeMessageListener('AccessFu:Present', this);
277 aMessageManager.removeMessageListener('AccessFu:Input', this);
278 aMessageManager.removeMessageListener('AccessFu:Ready', this);
279 aMessageManager.removeMessageListener('AccessFu:ActivateContextMenu', this);
280 aMessageManager.removeMessageListener('AccessFu:DoScroll', this);
281 },
282
283 _handleMessageManager: function _handleMessageManager(aMessageManager) {
284 if (this._enabled) {
285 this._addMessageListeners(aMessageManager);
286 }
287 this._loadFrameScript(aMessageManager);
288 },
289
290 observe: function observe(aSubject, aTopic, aData) {
291 switch (aTopic) {
292 case 'Accessibility:Settings':
293 this._systemPref = JSON.parse(aData).enabled;
294 this._enableOrDisable();
295 break;
296 case 'Accessibility:NextObject':
297 this.Input.moveCursor('moveNext', 'Simple', 'gesture');
298 break;
299 case 'Accessibility:PreviousObject':
300 this.Input.moveCursor('movePrevious', 'Simple', 'gesture');
301 break;
302 case 'Accessibility:ActivateObject':
303 this.Input.activateCurrent(JSON.parse(aData));
304 break;
305 case 'Accessibility:LongPress':
306 this.Input.sendContextMenuMessage();
307 break;
308 case 'Accessibility:Focus':
309 this._focused = JSON.parse(aData);
310 if (this._focused) {
311 this.autoMove({ forcePresent: true, noOpIfOnScreen: true });
312 }
313 break;
314 case 'Accessibility:MoveByGranularity':
315 this.Input.moveByGranularity(JSON.parse(aData));
316 break;
317 case 'remote-browser-shown':
318 case 'inprocess-browser-shown':
319 {
320 // Ignore notifications that aren't from a BrowserOrApp
321 let frameLoader = aSubject.QueryInterface(Ci.nsIFrameLoader);
322 if (!frameLoader.ownerIsBrowserOrAppFrame) {
323 return;
324 }
325 this._handleMessageManager(frameLoader.messageManager);
326 break;
327 }
328 }
329 },
330
331 handleEvent: function handleEvent(aEvent) {
332 switch (aEvent.type) {
333 case 'TabOpen':
334 {
335 let mm = Utils.getMessageManager(aEvent.target);
336 this._handleMessageManager(mm);
337 break;
338 }
339 case 'TabClose':
340 {
341 let mm = Utils.getMessageManager(aEvent.target);
342 let mmIndex = this._processedMessageManagers.indexOf(mm);
343 if (mmIndex > -1) {
344 this._removeMessageListeners(mm);
345 this._processedMessageManagers.splice(mmIndex, 1);
346 }
347 break;
348 }
349 case 'TabSelect':
350 {
351 if (this._focused) {
352 // We delay this for half a second so the awesomebar could close,
353 // and we could use the current coordinates for the content item.
354 // XXX TODO figure out how to avoid magic wait here.
355 this.autoMove({
356 delay: 500,
357 forcePresent: true,
358 noOpIfOnScreen: true,
359 moveMethod: 'moveFirst' });
360 }
361 break;
362 }
363 default:
364 {
365 // A settings change, it does not have an event type
366 if (aEvent.settingName == SCREENREADER_SETTING) {
367 this._systemPref = aEvent.settingValue;
368 this._enableOrDisable();
369 }
370 break;
371 }
372 }
373 },
374
375 autoMove: function autoMove(aOptions) {
376 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
377 mm.sendAsyncMessage('AccessFu:AutoMove', aOptions);
378 },
379
380 announce: function announce(aAnnouncement) {
381 this._output(Presentation.announce(aAnnouncement), Utils.CurrentBrowser);
382 },
383
384 // So we don't enable/disable twice
385 _enabled: false,
386
387 // Layerview is focused
388 _focused: false,
389
390 // Keep track of message managers tha already have a 'content-script.js'
391 // injected.
392 _processedMessageManagers: [],
393
394 /**
395 * Adjusts the given bounds relative to the given browser. Converts from screen
396 * or device pixels to either device or CSS pixels.
397 * @param {Rect} aJsonBounds the bounds to adjust
398 * @param {browser} aBrowser the browser we want the bounds relative to
399 * @param {bool} aToCSSPixels whether to convert to CSS pixels (as opposed to
400 * device pixels)
401 * @param {bool} aFromDevicePixels whether to convert from device pixels (as
402 * opposed to screen pixels)
403 */
404 adjustContentBounds: function(aJsonBounds, aBrowser, aToCSSPixels, aFromDevicePixels) {
405 let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
406 aJsonBounds.right - aJsonBounds.left,
407 aJsonBounds.bottom - aJsonBounds.top);
408 let win = Utils.win;
409 let dpr = win.devicePixelRatio;
410 let vp = Utils.getViewport(win);
411 let offset = { left: -win.mozInnerScreenX, top: -win.mozInnerScreenY };
412
413 if (!aBrowser.contentWindow) {
414 // OOP browser, add offset of browser.
415 // The offset of the browser element in relation to its parent window.
416 let clientRect = aBrowser.getBoundingClientRect();
417 let win = aBrowser.ownerDocument.defaultView;
418 offset.left += clientRect.left + win.mozInnerScreenX;
419 offset.top += clientRect.top + win.mozInnerScreenY;
420 }
421
422 // Here we scale from screen pixels to layout device pixels by dividing by
423 // the resolution (caused by pinch-zooming). The resolution is the viewport
424 // zoom divided by the devicePixelRatio. If there's no viewport, then we're
425 // on a platform without pinch-zooming and we can just ignore this.
426 if (!aFromDevicePixels && vp) {
427 bounds = bounds.scale(vp.zoom / dpr, vp.zoom / dpr);
428 }
429
430 // Add the offset; the offset is in CSS pixels, so multiply the
431 // devicePixelRatio back in before adding to preserve unit consistency.
432 bounds = bounds.translate(offset.left * dpr, offset.top * dpr);
433
434 // If we want to get to CSS pixels from device pixels, this needs to be
435 // further divided by the devicePixelRatio due to widget scaling.
436 if (aToCSSPixels) {
437 bounds = bounds.scale(1 / dpr, 1 / dpr);
438 }
439
440 return bounds.expandToIntegers();
441 }
442 };
443
444 var Output = {
445 brailleState: {
446 startOffset: 0,
447 endOffset: 0,
448 text: '',
449 selectionStart: 0,
450 selectionEnd: 0,
451
452 init: function init(aOutput) {
453 if (aOutput && 'output' in aOutput) {
454 this.startOffset = aOutput.startOffset;
455 this.endOffset = aOutput.endOffset;
456 // We need to append a space at the end so that the routing key corresponding
457 // to the end of the output (i.e. the space) can be hit to move the caret there.
458 this.text = aOutput.output + ' ';
459 this.selectionStart = typeof aOutput.selectionStart === 'number' ?
460 aOutput.selectionStart : this.selectionStart;
461 this.selectionEnd = typeof aOutput.selectionEnd === 'number' ?
462 aOutput.selectionEnd : this.selectionEnd;
463
464 return { text: this.text,
465 selectionStart: this.selectionStart,
466 selectionEnd: this.selectionEnd };
467 }
468
469 return null;
470 },
471
472 adjustText: function adjustText(aText) {
473 let newBraille = [];
474 let braille = {};
475
476 let prefix = this.text.substring(0, this.startOffset).trim();
477 if (prefix) {
478 prefix += ' ';
479 newBraille.push(prefix);
480 }
481
482 newBraille.push(aText);
483
484 let suffix = this.text.substring(this.endOffset).trim();
485 if (suffix) {
486 suffix = ' ' + suffix;
487 newBraille.push(suffix);
488 }
489
490 this.startOffset = braille.startOffset = prefix.length;
491 this.text = braille.text = newBraille.join('') + ' ';
492 this.endOffset = braille.endOffset = braille.text.length - suffix.length;
493 braille.selectionStart = this.selectionStart;
494 braille.selectionEnd = this.selectionEnd;
495
496 return braille;
497 },
498
499 adjustSelection: function adjustSelection(aSelection) {
500 let braille = {};
501
502 braille.startOffset = this.startOffset;
503 braille.endOffset = this.endOffset;
504 braille.text = this.text;
505 this.selectionStart = braille.selectionStart = aSelection.selectionStart + this.startOffset;
506 this.selectionEnd = braille.selectionEnd = aSelection.selectionEnd + this.startOffset;
507
508 return braille;
509 }
510 },
511
512 speechHelper: {
513 EARCONS: ['virtual_cursor_move.ogg',
514 'virtual_cursor_key.ogg',
515 'clicked.ogg'],
516
517 earconBuffers: {},
518
519 inited: false,
520
521 webspeechEnabled: false,
522
523 deferredOutputs: [],
524
525 init: function init() {
526 let window = Utils.win;
527 this.webspeechEnabled = !!window.speechSynthesis &&
528 !!window.SpeechSynthesisUtterance;
529
530 let settingsToGet = 2;
531 let settingsCallback = (aName, aSetting) => {
532 if (--settingsToGet > 0) {
533 return;
534 }
535
536 this.inited = true;
537
538 for (let actions of this.deferredOutputs) {
539 this.output(actions);
540 }
541 };
542
543 this._volumeSetting = new SettingCache(
544 'accessibility.screenreader-volume', settingsCallback,
545 { defaultValue: 1, callbackNow: true, callbackOnce: true });
546 this._rateSetting = new SettingCache(
547 'accessibility.screenreader-rate', settingsCallback,
548 { defaultValue: 0, callbackNow: true, callbackOnce: true });
549
550 for (let earcon of this.EARCONS) {
551 let earconName = /(^.*)\..*$/.exec(earcon)[1];
552 this.earconBuffers[earconName] = new WeakMap();
553 this.earconBuffers[earconName].set(
554 window, new window.Audio('chrome://global/content/accessibility/' + earcon));
555 }
556 },
557
558 uninit: function uninit() {
559 if (this.inited) {
560 delete this._volumeSetting;
561 delete this._rateSetting;
562 }
563 this.inited = false;
564 },
565
566 output: function output(aActions) {
567 if (!this.inited) {
568 this.deferredOutputs.push(aActions);
569 return;
570 }
571
572 for (let action of aActions) {
573 let window = Utils.win;
574 Logger.debug('tts.' + action.method, '"' + action.data + '"',
575 JSON.stringify(action.options));
576
577 if (!action.options.enqueue && this.webspeechEnabled) {
578 window.speechSynthesis.cancel();
579 }
580
581 if (action.method === 'speak' && this.webspeechEnabled) {
582 let utterance = new window.SpeechSynthesisUtterance(action.data);
583 let requestedRate = this._rateSetting.value;
584 utterance.volume = this._volumeSetting.value;
585 utterance.rate = requestedRate >= 0 ?
586 requestedRate + 1 : 1 / (Math.abs(requestedRate) + 1);
587 window.speechSynthesis.speak(utterance);
588 } else if (action.method === 'playEarcon') {
589 let audioBufferWeakMap = this.earconBuffers[action.data];
590 if (audioBufferWeakMap) {
591 let node = audioBufferWeakMap.get(window).cloneNode(false);
592 node.volume = this._volumeSetting.value;
593 node.play();
594 }
595 }
596 }
597 }
598 },
599
600 start: function start() {
601 Cu.import('resource://gre/modules/Geometry.jsm');
602 this.speechHelper.init();
603 },
604
605 stop: function stop() {
606 if (this.highlightBox) {
607 Utils.win.document.documentElement.removeChild(this.highlightBox.get());
608 delete this.highlightBox;
609 }
610
611 if (this.announceBox) {
612 Utils.win.document.documentElement.removeChild(this.announceBox.get());
613 delete this.announceBox;
614 }
615
616 this.speechHelper.uninit();
617 },
618
619 Speech: function Speech(aDetails, aBrowser) {
620 this.speechHelper.output(aDetails.actions);
621 },
622
623 Visual: function Visual(aDetails, aBrowser) {
624 switch (aDetails.method) {
625 case 'showBounds':
626 {
627 let highlightBox = null;
628 if (!this.highlightBox) {
629 // Add highlight box
630 highlightBox = Utils.win.document.
631 createElementNS('http://www.w3.org/1999/xhtml', 'div');
632 Utils.win.document.documentElement.appendChild(highlightBox);
633 highlightBox.id = 'virtual-cursor-box';
634
635 // Add highlight inset for inner shadow
636 let inset = Utils.win.document.
637 createElementNS('http://www.w3.org/1999/xhtml', 'div');
638 inset.id = 'virtual-cursor-inset';
639
640 highlightBox.appendChild(inset);
641 this.highlightBox = Cu.getWeakReference(highlightBox);
642 } else {
643 highlightBox = this.highlightBox.get();
644 }
645
646 let padding = aDetails.padding;
647 let r = AccessFu.adjustContentBounds(aDetails.bounds, aBrowser, true);
648
649 // First hide it to avoid flickering when changing the style.
650 highlightBox.style.display = 'none';
651 highlightBox.style.top = (r.top - padding) + 'px';
652 highlightBox.style.left = (r.left - padding) + 'px';
653 highlightBox.style.width = (r.width + padding*2) + 'px';
654 highlightBox.style.height = (r.height + padding*2) + 'px';
655 highlightBox.style.display = 'block';
656
657 break;
658 }
659 case 'hideBounds':
660 {
661 let highlightBox = this.highlightBox ? this.highlightBox.get() : null;
662 if (highlightBox)
663 highlightBox.style.display = 'none';
664 break;
665 }
666 case 'showAnnouncement':
667 {
668 let announceBox = this.announceBox ? this.announceBox.get() : null;
669 if (!announceBox) {
670 announceBox = Utils.win.document.
671 createElementNS('http://www.w3.org/1999/xhtml', 'div');
672 announceBox.id = 'announce-box';
673 Utils.win.document.documentElement.appendChild(announceBox);
674 this.announceBox = Cu.getWeakReference(announceBox);
675 }
676
677 announceBox.innerHTML = '<div>' + aDetails.text + '</div>';
678 announceBox.classList.add('showing');
679
680 if (this._announceHideTimeout)
681 Utils.win.clearTimeout(this._announceHideTimeout);
682
683 if (aDetails.duration > 0)
684 this._announceHideTimeout = Utils.win.setTimeout(
685 function () {
686 announceBox.classList.remove('showing');
687 this._announceHideTimeout = 0;
688 }.bind(this), aDetails.duration);
689 break;
690 }
691 case 'hideAnnouncement':
692 {
693 let announceBox = this.announceBox ? this.announceBox.get() : null;
694 if (announceBox)
695 announceBox.classList.remove('showing');
696 break;
697 }
698 }
699 },
700
701 get androidBridge() {
702 delete this.androidBridge;
703 if (Utils.MozBuildApp === 'mobile/android') {
704 this.androidBridge = Services.androidBridge;
705 } else {
706 this.androidBridge = null;
707 }
708 return this.androidBridge;
709 },
710
711 Android: function Android(aDetails, aBrowser) {
712 const ANDROID_VIEW_TEXT_CHANGED = 0x10;
713 const ANDROID_VIEW_TEXT_SELECTION_CHANGED = 0x2000;
714
715 if (!this.androidBridge) {
716 return;
717 }
718
719 for each (let androidEvent in aDetails) {
720 androidEvent.type = 'Accessibility:Event';
721 if (androidEvent.bounds)
722 androidEvent.bounds = AccessFu.adjustContentBounds(androidEvent.bounds, aBrowser);
723
724 switch(androidEvent.eventType) {
725 case ANDROID_VIEW_TEXT_CHANGED:
726 androidEvent.brailleOutput = this.brailleState.adjustText(androidEvent.text);
727 break;
728 case ANDROID_VIEW_TEXT_SELECTION_CHANGED:
729 androidEvent.brailleOutput = this.brailleState.adjustSelection(androidEvent.brailleOutput);
730 break;
731 default:
732 androidEvent.brailleOutput = this.brailleState.init(androidEvent.brailleOutput);
733 break;
734 }
735 this.androidBridge.handleGeckoMessage(androidEvent);
736 }
737 },
738
739 Haptic: function Haptic(aDetails, aBrowser) {
740 Utils.win.navigator.vibrate(aDetails.pattern);
741 },
742
743 Braille: function Braille(aDetails, aBrowser) {
744 Logger.debug('Braille output: ' + aDetails.text);
745 }
746 };
747
748 var Input = {
749 editState: {},
750
751 start: function start() {
752 // XXX: This is too disruptive on desktop for now.
753 // Might need to add special modifiers.
754 if (Utils.MozBuildApp != 'browser') {
755 Utils.win.document.addEventListener('keypress', this, true);
756 }
757 Utils.win.addEventListener('mozAccessFuGesture', this, true);
758 },
759
760 stop: function stop() {
761 if (Utils.MozBuildApp != 'browser') {
762 Utils.win.document.removeEventListener('keypress', this, true);
763 }
764 Utils.win.removeEventListener('mozAccessFuGesture', this, true);
765 },
766
767 handleEvent: function Input_handleEvent(aEvent) {
768 try {
769 switch (aEvent.type) {
770 case 'keypress':
771 this._handleKeypress(aEvent);
772 break;
773 case 'mozAccessFuGesture':
774 this._handleGesture(aEvent.detail);
775 break;
776 }
777 } catch (x) {
778 Logger.logException(x);
779 }
780 },
781
782 _handleGesture: function _handleGesture(aGesture) {
783 let gestureName = aGesture.type + aGesture.touches.length;
784 Logger.debug('Gesture', aGesture.type,
785 '(fingers: ' + aGesture.touches.length + ')');
786
787 switch (gestureName) {
788 case 'dwell1':
789 case 'explore1':
790 this.moveToPoint('Simple', aGesture.touches[0].x,
791 aGesture.touches[0].y);
792 break;
793 case 'doubletap1':
794 this.activateCurrent();
795 break;
796 case 'doubletaphold1':
797 this.sendContextMenuMessage();
798 break;
799 case 'swiperight1':
800 this.moveCursor('moveNext', 'Simple', 'gestures');
801 break;
802 case 'swipeleft1':
803 this.moveCursor('movePrevious', 'Simple', 'gesture');
804 break;
805 case 'swipeup1':
806 this.contextAction('backward');
807 break;
808 case 'swipedown1':
809 this.contextAction('forward');
810 break;
811 case 'exploreend1':
812 case 'dwellend1':
813 this.activateCurrent(null, true);
814 break;
815 case 'swiperight2':
816 this.sendScrollMessage(-1, true);
817 break;
818 case 'swipedown2':
819 this.sendScrollMessage(-1);
820 break;
821 case 'swipeleft2':
822 this.sendScrollMessage(1, true);
823 break;
824 case 'swipeup2':
825 this.sendScrollMessage(1);
826 break;
827 case 'explore2':
828 Utils.CurrentBrowser.contentWindow.scrollBy(
829 -aGesture.deltaX, -aGesture.deltaY);
830 break;
831 case 'swiperight3':
832 this.moveCursor('moveNext', this.quickNavMode.current, 'gesture');
833 break;
834 case 'swipeleft3':
835 this.moveCursor('movePrevious', this.quickNavMode.current, 'gesture');
836 break;
837 case 'swipedown3':
838 this.quickNavMode.next();
839 AccessFu.announce('quicknav_' + this.quickNavMode.current);
840 break;
841 case 'swipeup3':
842 this.quickNavMode.previous();
843 AccessFu.announce('quicknav_' + this.quickNavMode.current);
844 break;
845 }
846 },
847
848 _handleKeypress: function _handleKeypress(aEvent) {
849 let target = aEvent.target;
850
851 // Ignore keys with modifiers so the content could take advantage of them.
852 if (aEvent.ctrlKey || aEvent.altKey || aEvent.metaKey)
853 return;
854
855 switch (aEvent.keyCode) {
856 case 0:
857 // an alphanumeric key was pressed, handle it separately.
858 // If it was pressed with either alt or ctrl, just pass through.
859 // If it was pressed with meta, pass the key on without the meta.
860 if (this.editState.editing)
861 return;
862
863 let key = String.fromCharCode(aEvent.charCode);
864 try {
865 let [methodName, rule] = this.keyMap[key];
866 this.moveCursor(methodName, rule, 'keyboard');
867 } catch (x) {
868 return;
869 }
870 break;
871 case aEvent.DOM_VK_RIGHT:
872 if (this.editState.editing) {
873 if (!this.editState.atEnd)
874 // Don't move forward if caret is not at end of entry.
875 // XXX: Fix for rtl
876 return;
877 else
878 target.blur();
879 }
880 this.moveCursor(aEvent.shiftKey ? 'moveLast' : 'moveNext', 'Simple', 'keyboard');
881 break;
882 case aEvent.DOM_VK_LEFT:
883 if (this.editState.editing) {
884 if (!this.editState.atStart)
885 // Don't move backward if caret is not at start of entry.
886 // XXX: Fix for rtl
887 return;
888 else
889 target.blur();
890 }
891 this.moveCursor(aEvent.shiftKey ? 'moveFirst' : 'movePrevious', 'Simple', 'keyboard');
892 break;
893 case aEvent.DOM_VK_UP:
894 if (this.editState.multiline) {
895 if (!this.editState.atStart)
896 // Don't blur content if caret is not at start of text area.
897 return;
898 else
899 target.blur();
900 }
901
902 if (Utils.MozBuildApp == 'mobile/android')
903 // Return focus to native Android browser chrome.
904 Services.androidBridge.handleGeckoMessage(
905 { type: 'ToggleChrome:Focus' });
906 break;
907 case aEvent.DOM_VK_RETURN:
908 if (this.editState.editing)
909 return;
910 this.activateCurrent();
911 break;
912 default:
913 return;
914 }
915
916 aEvent.preventDefault();
917 aEvent.stopPropagation();
918 },
919
920 moveToPoint: function moveToPoint(aRule, aX, aY) {
921 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
922 mm.sendAsyncMessage('AccessFu:MoveToPoint', {rule: aRule,
923 x: aX, y: aY,
924 origin: 'top'});
925 },
926
927 moveCursor: function moveCursor(aAction, aRule, aInputType) {
928 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
929 mm.sendAsyncMessage('AccessFu:MoveCursor',
930 {action: aAction, rule: aRule,
931 origin: 'top', inputType: aInputType});
932 },
933
934 contextAction: function contextAction(aDirection) {
935 // XXX: For now, the only supported context action is adjusting a range.
936 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
937 mm.sendAsyncMessage('AccessFu:AdjustRange', {direction: aDirection});
938 },
939
940 moveByGranularity: function moveByGranularity(aDetails) {
941 const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
942
943 if (!this.editState.editing) {
944 if (aDetails.granularity === MOVEMENT_GRANULARITY_PARAGRAPH) {
945 this.moveCursor('move' + aDetails.direction, 'Paragraph', 'gesture');
946 return;
947 }
948 } else {
949 aDetails.atStart = this.editState.atStart;
950 aDetails.atEnd = this.editState.atEnd;
951 }
952
953 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
954 let type = this.editState.editing ? 'AccessFu:MoveCaret' :
955 'AccessFu:MoveByGranularity';
956 mm.sendAsyncMessage(type, aDetails);
957 },
958
959 activateCurrent: function activateCurrent(aData, aActivateIfKey = false) {
960 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
961 let offset = aData && typeof aData.keyIndex === 'number' ?
962 aData.keyIndex - Output.brailleState.startOffset : -1;
963
964 mm.sendAsyncMessage('AccessFu:Activate',
965 {offset: offset, activateIfKey: aActivateIfKey});
966 },
967
968 sendContextMenuMessage: function sendContextMenuMessage() {
969 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
970 mm.sendAsyncMessage('AccessFu:ContextMenu', {});
971 },
972
973 activateContextMenu: function activateContextMenu(aDetails) {
974 if (Utils.MozBuildApp === 'mobile/android') {
975 let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
976 true, true).center();
977 Services.obs.notifyObservers(null, 'Gesture:LongPress',
978 JSON.stringify({x: p.x, y: p.y}));
979 }
980 },
981
982 setEditState: function setEditState(aEditState) {
983 this.editState = aEditState;
984 },
985
986 // XXX: This is here for backwards compatability with screen reader simulator
987 // it should be removed when the extension is updated on amo.
988 scroll: function scroll(aPage, aHorizontal) {
989 this.sendScrollMessage(aPage, aHorizontal);
990 },
991
992 sendScrollMessage: function sendScrollMessage(aPage, aHorizontal) {
993 let mm = Utils.getMessageManager(Utils.CurrentBrowser);
994 mm.sendAsyncMessage('AccessFu:Scroll', {page: aPage, horizontal: aHorizontal, origin: 'top'});
995 },
996
997 doScroll: function doScroll(aDetails) {
998 let horizontal = aDetails.horizontal;
999 let page = aDetails.page;
1000 let p = AccessFu.adjustContentBounds(aDetails.bounds, Utils.CurrentBrowser,
1001 true, true).center();
1002 let wu = Utils.win.QueryInterface(Ci.nsIInterfaceRequestor).
1003 getInterface(Ci.nsIDOMWindowUtils);
1004 wu.sendWheelEvent(p.x, p.y,
1005 horizontal ? page : 0, horizontal ? 0 : page, 0,
1006 Utils.win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0);
1007 },
1008
1009 get keyMap() {
1010 delete this.keyMap;
1011 this.keyMap = {
1012 a: ['moveNext', 'Anchor'],
1013 A: ['movePrevious', 'Anchor'],
1014 b: ['moveNext', 'Button'],
1015 B: ['movePrevious', 'Button'],
1016 c: ['moveNext', 'Combobox'],
1017 C: ['movePrevious', 'Combobox'],
1018 d: ['moveNext', 'Landmark'],
1019 D: ['movePrevious', 'Landmark'],
1020 e: ['moveNext', 'Entry'],
1021 E: ['movePrevious', 'Entry'],
1022 f: ['moveNext', 'FormElement'],
1023 F: ['movePrevious', 'FormElement'],
1024 g: ['moveNext', 'Graphic'],
1025 G: ['movePrevious', 'Graphic'],
1026 h: ['moveNext', 'Heading'],
1027 H: ['movePrevious', 'Heading'],
1028 i: ['moveNext', 'ListItem'],
1029 I: ['movePrevious', 'ListItem'],
1030 k: ['moveNext', 'Link'],
1031 K: ['movePrevious', 'Link'],
1032 l: ['moveNext', 'List'],
1033 L: ['movePrevious', 'List'],
1034 p: ['moveNext', 'PageTab'],
1035 P: ['movePrevious', 'PageTab'],
1036 r: ['moveNext', 'RadioButton'],
1037 R: ['movePrevious', 'RadioButton'],
1038 s: ['moveNext', 'Separator'],
1039 S: ['movePrevious', 'Separator'],
1040 t: ['moveNext', 'Table'],
1041 T: ['movePrevious', 'Table'],
1042 x: ['moveNext', 'Checkbox'],
1043 X: ['movePrevious', 'Checkbox']
1044 };
1045
1046 return this.keyMap;
1047 },
1048
1049 quickNavMode: {
1050 get current() {
1051 return this.modes[this._currentIndex];
1052 },
1053
1054 previous: function quickNavMode_previous() {
1055 if (--this._currentIndex < 0)
1056 this._currentIndex = this.modes.length - 1;
1057 },
1058
1059 next: function quickNavMode_next() {
1060 if (++this._currentIndex >= this.modes.length)
1061 this._currentIndex = 0;
1062 },
1063
1064 updateModes: function updateModes(aModes) {
1065 if (aModes) {
1066 this.modes = aModes.split(',');
1067 } else {
1068 this.modes = [];
1069 }
1070 },
1071
1072 _currentIndex: -1
1073 }
1074 };
1075 AccessFu.Input = Input;

mercurial