accessible/src/jsat/EventManager.jsm

branch
TOR_BUG_9701
changeset 3
141e0f1194b1
equal deleted inserted replaced
-1:000000000000 0:9949cbdca690
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 Ci = Components.interfaces;
8 const Cu = Components.utils;
9
10 const TEXT_NODE = 3;
11
12 Cu.import('resource://gre/modules/XPCOMUtils.jsm');
13 XPCOMUtils.defineLazyModuleGetter(this, 'Services',
14 'resource://gre/modules/Services.jsm');
15 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
16 'resource://gre/modules/accessibility/Utils.jsm');
17 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
18 'resource://gre/modules/accessibility/Utils.jsm');
19 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
20 'resource://gre/modules/accessibility/Presentation.jsm');
21 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
22 'resource://gre/modules/accessibility/TraversalRules.jsm');
23 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
24 'resource://gre/modules/accessibility/Constants.jsm');
25 XPCOMUtils.defineLazyModuleGetter(this, 'Events',
26 'resource://gre/modules/accessibility/Constants.jsm');
27 XPCOMUtils.defineLazyModuleGetter(this, 'States',
28 'resource://gre/modules/accessibility/Constants.jsm');
29
30 this.EXPORTED_SYMBOLS = ['EventManager'];
31
32 this.EventManager = function EventManager(aContentScope) {
33 this.contentScope = aContentScope;
34 this.addEventListener = this.contentScope.addEventListener.bind(
35 this.contentScope);
36 this.removeEventListener = this.contentScope.removeEventListener.bind(
37 this.contentScope);
38 this.sendMsgFunc = this.contentScope.sendAsyncMessage.bind(
39 this.contentScope);
40 this.webProgress = this.contentScope.docShell.
41 QueryInterface(Ci.nsIInterfaceRequestor).
42 getInterface(Ci.nsIWebProgress);
43 };
44
45 this.EventManager.prototype = {
46 editState: {},
47
48 start: function start() {
49 try {
50 if (!this._started) {
51 Logger.debug('EventManager.start');
52
53 this._started = true;
54
55 AccessibilityEventObserver.addListener(this);
56
57 this.webProgress.addProgressListener(this,
58 (Ci.nsIWebProgress.NOTIFY_STATE_ALL |
59 Ci.nsIWebProgress.NOTIFY_LOCATION));
60 this.addEventListener('wheel', this, true);
61 this.addEventListener('scroll', this, true);
62 this.addEventListener('resize', this, true);
63 }
64 this.present(Presentation.tabStateChanged(null, 'newtab'));
65
66 } catch (x) {
67 Logger.logException(x, 'Failed to start EventManager');
68 }
69 },
70
71 // XXX: Stop is not called when the tab is closed (|TabClose| event is too
72 // late). It is only called when the AccessFu is disabled explicitly.
73 stop: function stop() {
74 if (!this._started) {
75 return;
76 }
77 Logger.debug('EventManager.stop');
78 AccessibilityEventObserver.removeListener(this);
79 try {
80 this.webProgress.removeProgressListener(this);
81 this.removeEventListener('wheel', this, true);
82 this.removeEventListener('scroll', this, true);
83 this.removeEventListener('resize', this, true);
84 } catch (x) {
85 // contentScope is dead.
86 } finally {
87 this._started = false;
88 }
89 },
90
91 handleEvent: function handleEvent(aEvent) {
92 Logger.debug(() => {
93 return ['DOMEvent', aEvent.type];
94 });
95
96 try {
97 switch (aEvent.type) {
98 case 'wheel':
99 {
100 let attempts = 0;
101 let delta = aEvent.deltaX || aEvent.deltaY;
102 this.contentScope.contentControl.autoMove(
103 null,
104 { moveMethod: delta > 0 ? 'moveNext' : 'movePrevious',
105 onScreenOnly: true, noOpIfOnScreen: true, delay: 500 });
106 break;
107 }
108 case 'scroll':
109 case 'resize':
110 {
111 // the target could be an element, document or window
112 let window = null;
113 if (aEvent.target instanceof Ci.nsIDOMWindow)
114 window = aEvent.target;
115 else if (aEvent.target instanceof Ci.nsIDOMDocument)
116 window = aEvent.target.defaultView;
117 else if (aEvent.target instanceof Ci.nsIDOMElement)
118 window = aEvent.target.ownerDocument.defaultView;
119 this.present(Presentation.viewportChanged(window));
120 break;
121 }
122 }
123 } catch (x) {
124 Logger.logException(x, 'Error handling DOM event');
125 }
126 },
127
128 handleAccEvent: function handleAccEvent(aEvent) {
129 Logger.debug(() => {
130 return ['A11yEvent', Logger.eventToString(aEvent),
131 Logger.accessibleToString(aEvent.accessible)];
132 });
133
134 // Don't bother with non-content events in firefox.
135 if (Utils.MozBuildApp == 'browser' &&
136 aEvent.eventType != Events.VIRTUALCURSOR_CHANGED &&
137 // XXX Bug 442005 results in DocAccessible::getDocType returning
138 // NS_ERROR_FAILURE. Checking for aEvent.accessibleDocument.docType ==
139 // 'window' does not currently work.
140 (aEvent.accessibleDocument.DOMDocument.doctype &&
141 aEvent.accessibleDocument.DOMDocument.doctype.name === 'window')) {
142 return;
143 }
144
145 switch (aEvent.eventType) {
146 case Events.VIRTUALCURSOR_CHANGED:
147 {
148 let pivot = aEvent.accessible.
149 QueryInterface(Ci.nsIAccessibleDocument).virtualCursor;
150 let position = pivot.position;
151 if (position && position.role == Roles.INTERNAL_FRAME)
152 break;
153 let event = aEvent.
154 QueryInterface(Ci.nsIAccessibleVirtualCursorChangeEvent);
155 let reason = event.reason;
156 let oldAccessible = event.oldAccessible;
157
158 if (this.editState.editing) {
159 aEvent.accessibleDocument.takeFocus();
160 }
161 this.present(
162 Presentation.pivotChanged(position, oldAccessible, reason,
163 pivot.startOffset, pivot.endOffset));
164
165 break;
166 }
167 case Events.STATE_CHANGE:
168 {
169 let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent);
170 let state = Utils.getState(event);
171 if (state.contains(States.CHECKED)) {
172 this.present(
173 Presentation.
174 actionInvoked(aEvent.accessible,
175 event.isEnabled ? 'check' : 'uncheck'));
176 } else if (state.contains(States.SELECTED)) {
177 this.present(
178 Presentation.
179 actionInvoked(aEvent.accessible,
180 event.isEnabled ? 'select' : 'unselect'));
181 }
182 break;
183 }
184 case Events.SCROLLING_START:
185 {
186 this.contentScope.contentControl.autoMove(aEvent.accessible);
187 break;
188 }
189 case Events.TEXT_CARET_MOVED:
190 {
191 let acc = aEvent.accessible;
192 let characterCount = acc.
193 QueryInterface(Ci.nsIAccessibleText).characterCount;
194 let caretOffset = aEvent.
195 QueryInterface(Ci.nsIAccessibleCaretMoveEvent).caretOffset;
196
197 // Update editing state, both for presenter and other things
198 let state = Utils.getState(acc);
199 let editState = {
200 editing: state.contains(States.EDITABLE),
201 multiline: state.contains(States.MULTI_LINE),
202 atStart: caretOffset == 0,
203 atEnd: caretOffset == characterCount
204 };
205
206 // Not interesting
207 if (!editState.editing && editState.editing == this.editState.editing)
208 break;
209
210 if (editState.editing != this.editState.editing)
211 this.present(Presentation.editingModeChanged(editState.editing));
212
213 if (editState.editing != this.editState.editing ||
214 editState.multiline != this.editState.multiline ||
215 editState.atEnd != this.editState.atEnd ||
216 editState.atStart != this.editState.atStart)
217 this.sendMsgFunc("AccessFu:Input", editState);
218
219 this.present(Presentation.textSelectionChanged(acc.getText(0,-1),
220 caretOffset, caretOffset, 0, 0, aEvent.isFromUserInput));
221
222 this.editState = editState;
223 break;
224 }
225 case Events.SHOW:
226 {
227 let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
228 ['additions', 'all']);
229 // Only handle show if it is a relevant live region.
230 if (!liveRegion) {
231 break;
232 }
233 // Show for text is handled by the EVENT_TEXT_INSERTED handler.
234 if (aEvent.accessible.role === Roles.TEXT_LEAF) {
235 break;
236 }
237 this._dequeueLiveEvent(Events.HIDE, liveRegion);
238 this.present(Presentation.liveRegion(liveRegion, isPolite, false));
239 break;
240 }
241 case Events.HIDE:
242 {
243 let evt = aEvent.QueryInterface(Ci.nsIAccessibleHideEvent);
244 let {liveRegion, isPolite} = this._handleLiveRegion(
245 evt, ['removals', 'all']);
246 if (liveRegion) {
247 // Hide for text is handled by the EVENT_TEXT_REMOVED handler.
248 if (aEvent.accessible.role === Roles.TEXT_LEAF) {
249 break;
250 }
251 this._queueLiveEvent(Events.HIDE, liveRegion, isPolite);
252 } else {
253 let vc = Utils.getVirtualCursor(this.contentScope.content.document);
254 if (vc.position &&
255 (Utils.getState(vc.position).contains(States.DEFUNCT) ||
256 Utils.isInSubtree(vc.position, aEvent.accessible))) {
257 this.contentScope.contentControl.autoMove(
258 evt.targetPrevSibling || evt.targetParent,
259 { moveToFocused: true, delay: 500 });
260 }
261 }
262 break;
263 }
264 case Events.TEXT_INSERTED:
265 case Events.TEXT_REMOVED:
266 {
267 let {liveRegion, isPolite} = this._handleLiveRegion(aEvent,
268 ['text', 'all']);
269 if (aEvent.isFromUserInput || liveRegion) {
270 // Handle all text mutations coming from the user or if they happen
271 // on a live region.
272 this._handleText(aEvent, liveRegion, isPolite);
273 }
274 break;
275 }
276 case Events.FOCUS:
277 {
278 // Put vc where the focus is at
279 let acc = aEvent.accessible;
280 let doc = aEvent.accessibleDocument;
281 if (acc.role != Roles.DOCUMENT && doc.role != Roles.CHROME_WINDOW) {
282 this.contentScope.contentControl.autoMove(acc);
283 }
284 break;
285 }
286 case Events.DOCUMENT_LOAD_COMPLETE:
287 {
288 if (aEvent.accessible === aEvent.accessibleDocument) {
289 break;
290 }
291 this.contentScope.contentControl.autoMove(
292 aEvent.accessible, { delay: 500 });
293 break;
294 }
295 case Events.VALUE_CHANGE:
296 {
297 let position = this.contentScope.contentControl.vc.position;
298 let target = aEvent.accessible;
299 if (position === target ||
300 Utils.getEmbeddedControl(position) === target) {
301 this.present(Presentation.valueChanged(target));
302 }
303 }
304 }
305 },
306
307 _handleText: function _handleText(aEvent, aLiveRegion, aIsPolite) {
308 let event = aEvent.QueryInterface(Ci.nsIAccessibleTextChangeEvent);
309 let isInserted = event.isInserted;
310 let txtIface = aEvent.accessible.QueryInterface(Ci.nsIAccessibleText);
311
312 let text = '';
313 try {
314 text = txtIface.getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT);
315 } catch (x) {
316 // XXX we might have gotten an exception with of a
317 // zero-length text. If we did, ignore it (bug #749810).
318 if (txtIface.characterCount) {
319 throw x;
320 }
321 }
322 // If there are embedded objects in the text, ignore them.
323 // Assuming changes to the descendants would already be handled by the
324 // show/hide event.
325 let modifiedText = event.modifiedText.replace(/\uFFFC/g, '').trim();
326 if (!modifiedText) {
327 return;
328 }
329 if (aLiveRegion) {
330 if (aEvent.eventType === Events.TEXT_REMOVED) {
331 this._queueLiveEvent(Events.TEXT_REMOVED, aLiveRegion, aIsPolite,
332 modifiedText);
333 } else {
334 this._dequeueLiveEvent(Events.TEXT_REMOVED, aLiveRegion);
335 this.present(Presentation.liveRegion(aLiveRegion, aIsPolite, false,
336 modifiedText));
337 }
338 } else {
339 this.present(Presentation.textChanged(isInserted, event.start,
340 event.length, text, modifiedText));
341 }
342 },
343
344 _handleLiveRegion: function _handleLiveRegion(aEvent, aRelevant) {
345 if (aEvent.isFromUserInput) {
346 return {};
347 }
348 let parseLiveAttrs = function parseLiveAttrs(aAccessible) {
349 let attrs = Utils.getAttributes(aAccessible);
350 if (attrs['container-live']) {
351 return {
352 live: attrs['container-live'],
353 relevant: attrs['container-relevant'] || 'additions text',
354 busy: attrs['container-busy'],
355 atomic: attrs['container-atomic'],
356 memberOf: attrs['member-of']
357 };
358 }
359 return null;
360 };
361 // XXX live attributes are not set for hidden accessibles yet. Need to
362 // climb up the tree to check for them.
363 let getLiveAttributes = function getLiveAttributes(aEvent) {
364 let liveAttrs = parseLiveAttrs(aEvent.accessible);
365 if (liveAttrs) {
366 return liveAttrs;
367 }
368 let parent = aEvent.targetParent;
369 while (parent) {
370 liveAttrs = parseLiveAttrs(parent);
371 if (liveAttrs) {
372 return liveAttrs;
373 }
374 parent = parent.parent
375 }
376 return {};
377 };
378 let {live, relevant, busy, atomic, memberOf} = getLiveAttributes(aEvent);
379 // If container-live is not present or is set to |off| ignore the event.
380 if (!live || live === 'off') {
381 return {};
382 }
383 // XXX: support busy and atomic.
384
385 // Determine if the type of the mutation is relevant. Default is additions
386 // and text.
387 let isRelevant = Utils.matchAttributeValue(relevant, aRelevant);
388 if (!isRelevant) {
389 return {};
390 }
391 return {
392 liveRegion: aEvent.accessible,
393 isPolite: live === 'polite'
394 };
395 },
396
397 _dequeueLiveEvent: function _dequeueLiveEvent(aEventType, aLiveRegion) {
398 let domNode = aLiveRegion.DOMNode;
399 if (this._liveEventQueue && this._liveEventQueue.has(domNode)) {
400 let queue = this._liveEventQueue.get(domNode);
401 let nextEvent = queue[0];
402 if (nextEvent.eventType === aEventType) {
403 Utils.win.clearTimeout(nextEvent.timeoutID);
404 queue.shift();
405 if (queue.length === 0) {
406 this._liveEventQueue.delete(domNode)
407 }
408 }
409 }
410 },
411
412 _queueLiveEvent: function _queueLiveEvent(aEventType, aLiveRegion, aIsPolite, aModifiedText) {
413 if (!this._liveEventQueue) {
414 this._liveEventQueue = new WeakMap();
415 }
416 let eventHandler = {
417 eventType: aEventType,
418 timeoutID: Utils.win.setTimeout(this.present.bind(this),
419 20, // Wait for a possible EVENT_SHOW or EVENT_TEXT_INSERTED event.
420 Presentation.liveRegion(aLiveRegion, aIsPolite, true, aModifiedText))
421 };
422
423 let domNode = aLiveRegion.DOMNode;
424 if (this._liveEventQueue.has(domNode)) {
425 this._liveEventQueue.get(domNode).push(eventHandler);
426 } else {
427 this._liveEventQueue.set(domNode, [eventHandler]);
428 }
429 },
430
431 present: function present(aPresentationData) {
432 this.sendMsgFunc("AccessFu:Present", aPresentationData);
433 },
434
435 onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
436 let tabstate = '';
437
438 let loadingState = Ci.nsIWebProgressListener.STATE_TRANSFERRING |
439 Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
440 let loadedState = Ci.nsIWebProgressListener.STATE_STOP |
441 Ci.nsIWebProgressListener.STATE_IS_NETWORK;
442
443 if ((aStateFlags & loadingState) == loadingState) {
444 tabstate = 'loading';
445 } else if ((aStateFlags & loadedState) == loadedState &&
446 !aWebProgress.isLoadingDocument) {
447 tabstate = 'loaded';
448 }
449
450 if (tabstate) {
451 let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
452 this.present(Presentation.tabStateChanged(docAcc, tabstate));
453 }
454 },
455
456 onProgressChange: function onProgressChange() {},
457
458 onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocation, aFlags) {
459 let docAcc = Utils.AccRetrieval.getAccessibleFor(aWebProgress.DOMWindow.document);
460 this.present(Presentation.tabStateChanged(docAcc, 'newdoc'));
461 },
462
463 onStatusChange: function onStatusChange() {},
464
465 onSecurityChange: function onSecurityChange() {},
466
467 QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
468 Ci.nsISupportsWeakReference,
469 Ci.nsISupports,
470 Ci.nsIObserver])
471 };
472
473 const AccessibilityEventObserver = {
474
475 /**
476 * A WeakMap containing [content, EventManager] pairs.
477 */
478 eventManagers: new WeakMap(),
479
480 /**
481 * A total number of registered eventManagers.
482 */
483 listenerCount: 0,
484
485 /**
486 * An indicator of an active 'accessible-event' observer.
487 */
488 started: false,
489
490 /**
491 * Start an AccessibilityEventObserver.
492 */
493 start: function start() {
494 if (this.started || this.listenerCount === 0) {
495 return;
496 }
497 Services.obs.addObserver(this, 'accessible-event', false);
498 this.started = true;
499 },
500
501 /**
502 * Stop an AccessibilityEventObserver.
503 */
504 stop: function stop() {
505 if (!this.started) {
506 return;
507 }
508 Services.obs.removeObserver(this, 'accessible-event');
509 // Clean up all registered event managers.
510 this.eventManagers.clear();
511 this.listenerCount = 0;
512 this.started = false;
513 },
514
515 /**
516 * Register an EventManager and start listening to the
517 * 'accessible-event' messages.
518 *
519 * @param aEventManager EventManager
520 * An EventManager object that was loaded into the specific content.
521 */
522 addListener: function addListener(aEventManager) {
523 let content = aEventManager.contentScope.content;
524 if (!this.eventManagers.has(content)) {
525 this.listenerCount++;
526 }
527 this.eventManagers.set(content, aEventManager);
528 // Since at least one EventManager was registered, start listening.
529 Logger.debug('AccessibilityEventObserver.addListener. Total:',
530 this.listenerCount);
531 this.start();
532 },
533
534 /**
535 * Unregister an EventManager and, optionally, stop listening to the
536 * 'accessible-event' messages.
537 *
538 * @param aEventManager EventManager
539 * An EventManager object that was stopped in the specific content.
540 */
541 removeListener: function removeListener(aEventManager) {
542 let content = aEventManager.contentScope.content;
543 if (!this.eventManagers.delete(content)) {
544 return;
545 }
546 this.listenerCount--;
547 Logger.debug('AccessibilityEventObserver.removeListener. Total:',
548 this.listenerCount);
549 if (this.listenerCount === 0) {
550 // If there are no EventManagers registered at the moment, stop listening
551 // to the 'accessible-event' messages.
552 this.stop();
553 }
554 },
555
556 /**
557 * Lookup an EventManager for a specific content. If the EventManager is not
558 * found, walk up the hierarchy of parent windows.
559 * @param content Window
560 * A content Window used to lookup the corresponding EventManager.
561 */
562 getListener: function getListener(content) {
563 let eventManager = this.eventManagers.get(content);
564 if (eventManager) {
565 return eventManager;
566 }
567 let parent = content.parent;
568 if (parent === content) {
569 // There is no parent or the parent is of a different type.
570 return null;
571 }
572 return this.getListener(parent);
573 },
574
575 /**
576 * Handle the 'accessible-event' message.
577 */
578 observe: function observe(aSubject, aTopic, aData) {
579 if (aTopic !== 'accessible-event') {
580 return;
581 }
582 let event = aSubject.QueryInterface(Ci.nsIAccessibleEvent);
583 if (!event.accessibleDocument) {
584 Logger.warning(
585 'AccessibilityEventObserver.observe: no accessible document:',
586 Logger.eventToString(event), "accessible:",
587 Logger.accessibleToString(event.accessible));
588 return;
589 }
590 let content = event.accessibleDocument.window;
591 // Match the content window to its EventManager.
592 let eventManager = this.getListener(content);
593 if (!eventManager || !eventManager._started) {
594 if (Utils.MozBuildApp === 'browser' &&
595 !(content instanceof Ci.nsIDOMChromeWindow)) {
596 Logger.warning(
597 'AccessibilityEventObserver.observe: ignored event:',
598 Logger.eventToString(event), "accessible:",
599 Logger.accessibleToString(event.accessible), "document:",
600 Logger.accessibleToString(event.accessibleDocument));
601 }
602 return;
603 }
604 try {
605 eventManager.handleAccEvent(event);
606 } catch (x) {
607 Logger.logException(x, 'Error handing accessible event');
608 } finally {
609 return;
610 }
611 }
612 };

mercurial