|
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 Cu = Components.utils; |
|
8 const Cc = Components.classes; |
|
9 const Ci = Components.interfaces; |
|
10 |
|
11 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
12 XPCOMUtils.defineLazyModuleGetter(this, 'Services', |
|
13 'resource://gre/modules/Services.jsm'); |
|
14 XPCOMUtils.defineLazyModuleGetter(this, 'Rect', |
|
15 'resource://gre/modules/Geometry.jsm'); |
|
16 XPCOMUtils.defineLazyModuleGetter(this, 'Roles', |
|
17 'resource://gre/modules/accessibility/Constants.jsm'); |
|
18 XPCOMUtils.defineLazyModuleGetter(this, 'Events', |
|
19 'resource://gre/modules/accessibility/Constants.jsm'); |
|
20 XPCOMUtils.defineLazyModuleGetter(this, 'Relations', |
|
21 'resource://gre/modules/accessibility/Constants.jsm'); |
|
22 XPCOMUtils.defineLazyModuleGetter(this, 'States', |
|
23 'resource://gre/modules/accessibility/Constants.jsm'); |
|
24 |
|
25 this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache', 'SettingCache']; |
|
26 |
|
27 this.Utils = { |
|
28 _buildAppMap: { |
|
29 '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g', |
|
30 '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser', |
|
31 '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'mobile/android', |
|
32 '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'mobile/xul' |
|
33 }, |
|
34 |
|
35 init: function Utils_init(aWindow) { |
|
36 if (this._win) |
|
37 // XXX: only supports attaching to one window now. |
|
38 throw new Error('Only one top-level window could used with AccessFu'); |
|
39 |
|
40 this._win = Cu.getWeakReference(aWindow); |
|
41 }, |
|
42 |
|
43 uninit: function Utils_uninit() { |
|
44 if (!this._win) { |
|
45 return; |
|
46 } |
|
47 delete this._win; |
|
48 }, |
|
49 |
|
50 get win() { |
|
51 if (!this._win) { |
|
52 return null; |
|
53 } |
|
54 return this._win.get(); |
|
55 }, |
|
56 |
|
57 get AccRetrieval() { |
|
58 if (!this._AccRetrieval) { |
|
59 this._AccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1']. |
|
60 getService(Ci.nsIAccessibleRetrieval); |
|
61 } |
|
62 |
|
63 return this._AccRetrieval; |
|
64 }, |
|
65 |
|
66 set MozBuildApp(value) { |
|
67 this._buildApp = value; |
|
68 }, |
|
69 |
|
70 get MozBuildApp() { |
|
71 if (!this._buildApp) |
|
72 this._buildApp = this._buildAppMap[Services.appinfo.ID]; |
|
73 return this._buildApp; |
|
74 }, |
|
75 |
|
76 get OS() { |
|
77 if (!this._OS) |
|
78 this._OS = Services.appinfo.OS; |
|
79 return this._OS; |
|
80 }, |
|
81 |
|
82 get widgetToolkit() { |
|
83 if (!this._widgetToolkit) |
|
84 this._widgetToolkit = Services.appinfo.widgetToolkit; |
|
85 return this._widgetToolkit; |
|
86 }, |
|
87 |
|
88 get ScriptName() { |
|
89 if (!this._ScriptName) |
|
90 this._ScriptName = |
|
91 (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu'; |
|
92 return this._ScriptName; |
|
93 }, |
|
94 |
|
95 get AndroidSdkVersion() { |
|
96 if (!this._AndroidSdkVersion) { |
|
97 if (Services.appinfo.OS == 'Android') { |
|
98 this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32('version'); |
|
99 } else { |
|
100 // Most useful in desktop debugging. |
|
101 this._AndroidSdkVersion = 16; |
|
102 } |
|
103 } |
|
104 return this._AndroidSdkVersion; |
|
105 }, |
|
106 |
|
107 set AndroidSdkVersion(value) { |
|
108 // When we want to mimic another version. |
|
109 this._AndroidSdkVersion = value; |
|
110 }, |
|
111 |
|
112 get BrowserApp() { |
|
113 if (!this.win) { |
|
114 return null; |
|
115 } |
|
116 switch (this.MozBuildApp) { |
|
117 case 'mobile/android': |
|
118 return this.win.BrowserApp; |
|
119 case 'browser': |
|
120 return this.win.gBrowser; |
|
121 case 'b2g': |
|
122 return this.win.shell; |
|
123 default: |
|
124 return null; |
|
125 } |
|
126 }, |
|
127 |
|
128 get CurrentBrowser() { |
|
129 if (!this.BrowserApp) { |
|
130 return null; |
|
131 } |
|
132 if (this.MozBuildApp == 'b2g') |
|
133 return this.BrowserApp.contentBrowser; |
|
134 return this.BrowserApp.selectedBrowser; |
|
135 }, |
|
136 |
|
137 get CurrentContentDoc() { |
|
138 let browser = this.CurrentBrowser; |
|
139 return browser ? browser.contentDocument : null; |
|
140 }, |
|
141 |
|
142 get AllMessageManagers() { |
|
143 let messageManagers = []; |
|
144 |
|
145 for (let i = 0; i < this.win.messageManager.childCount; i++) |
|
146 messageManagers.push(this.win.messageManager.getChildAt(i)); |
|
147 |
|
148 let document = this.CurrentContentDoc; |
|
149 |
|
150 if (document) { |
|
151 let remoteframes = document.querySelectorAll('iframe'); |
|
152 |
|
153 for (let i = 0; i < remoteframes.length; ++i) { |
|
154 let mm = this.getMessageManager(remoteframes[i]); |
|
155 if (mm) { |
|
156 messageManagers.push(mm); |
|
157 } |
|
158 } |
|
159 |
|
160 } |
|
161 |
|
162 return messageManagers; |
|
163 }, |
|
164 |
|
165 get isContentProcess() { |
|
166 delete this.isContentProcess; |
|
167 this.isContentProcess = |
|
168 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; |
|
169 return this.isContentProcess; |
|
170 }, |
|
171 |
|
172 get stringBundle() { |
|
173 delete this.stringBundle; |
|
174 this.stringBundle = Services.strings.createBundle( |
|
175 'chrome://global/locale/AccessFu.properties'); |
|
176 return this.stringBundle; |
|
177 }, |
|
178 |
|
179 getMessageManager: function getMessageManager(aBrowser) { |
|
180 try { |
|
181 return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner). |
|
182 frameLoader.messageManager; |
|
183 } catch (x) { |
|
184 Logger.logException(x); |
|
185 return null; |
|
186 } |
|
187 }, |
|
188 |
|
189 getViewport: function getViewport(aWindow) { |
|
190 switch (this.MozBuildApp) { |
|
191 case 'mobile/android': |
|
192 return aWindow.BrowserApp.selectedTab.getViewport(); |
|
193 default: |
|
194 return null; |
|
195 } |
|
196 }, |
|
197 |
|
198 getState: function getState(aAccessibleOrEvent) { |
|
199 if (aAccessibleOrEvent instanceof Ci.nsIAccessibleStateChangeEvent) { |
|
200 return new State( |
|
201 aAccessibleOrEvent.isExtraState ? 0 : aAccessibleOrEvent.state, |
|
202 aAccessibleOrEvent.isExtraState ? aAccessibleOrEvent.state : 0); |
|
203 } else { |
|
204 let state = {}; |
|
205 let extState = {}; |
|
206 aAccessibleOrEvent.getState(state, extState); |
|
207 return new State(state.value, extState.value); |
|
208 } |
|
209 }, |
|
210 |
|
211 getAttributes: function getAttributes(aAccessible) { |
|
212 let attributes = {}; |
|
213 |
|
214 if (aAccessible && aAccessible.attributes) { |
|
215 let attributesEnum = aAccessible.attributes.enumerate(); |
|
216 |
|
217 // Populate |attributes| object with |aAccessible|'s attribute key-value |
|
218 // pairs. |
|
219 while (attributesEnum.hasMoreElements()) { |
|
220 let attribute = attributesEnum.getNext().QueryInterface( |
|
221 Ci.nsIPropertyElement); |
|
222 attributes[attribute.key] = attribute.value; |
|
223 } |
|
224 } |
|
225 |
|
226 return attributes; |
|
227 }, |
|
228 |
|
229 getVirtualCursor: function getVirtualCursor(aDocument) { |
|
230 let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument : |
|
231 this.AccRetrieval.getAccessibleFor(aDocument); |
|
232 |
|
233 return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor; |
|
234 }, |
|
235 |
|
236 getBounds: function getBounds(aAccessible) { |
|
237 let objX = {}, objY = {}, objW = {}, objH = {}; |
|
238 aAccessible.getBounds(objX, objY, objW, objH); |
|
239 return new Rect(objX.value, objY.value, objW.value, objH.value); |
|
240 }, |
|
241 |
|
242 getTextBounds: function getTextBounds(aAccessible, aStart, aEnd) { |
|
243 let accText = aAccessible.QueryInterface(Ci.nsIAccessibleText); |
|
244 let objX = {}, objY = {}, objW = {}, objH = {}; |
|
245 accText.getRangeExtents(aStart, aEnd, objX, objY, objW, objH, |
|
246 Ci.nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE); |
|
247 return new Rect(objX.value, objY.value, objW.value, objH.value); |
|
248 }, |
|
249 |
|
250 /** |
|
251 * Get current display DPI. |
|
252 */ |
|
253 get dpi() { |
|
254 delete this.dpi; |
|
255 this.dpi = this.win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface( |
|
256 Ci.nsIDOMWindowUtils).displayDPI; |
|
257 return this.dpi; |
|
258 }, |
|
259 |
|
260 isInSubtree: function isInSubtree(aAccessible, aSubTreeRoot) { |
|
261 let acc = aAccessible; |
|
262 while (acc) { |
|
263 if (acc == aSubTreeRoot) { |
|
264 return true; |
|
265 } |
|
266 |
|
267 try { |
|
268 acc = acc.parent; |
|
269 } catch (x) { |
|
270 Logger.debug('Failed to get parent:', x); |
|
271 acc = null; |
|
272 } |
|
273 } |
|
274 |
|
275 return false; |
|
276 }, |
|
277 |
|
278 inHiddenSubtree: function inHiddenSubtree(aAccessible) { |
|
279 for (let acc=aAccessible; acc; acc=acc.parent) { |
|
280 let hidden = Utils.getAttributes(acc).hidden; |
|
281 if (hidden && JSON.parse(hidden)) { |
|
282 return true; |
|
283 } |
|
284 } |
|
285 return false; |
|
286 }, |
|
287 |
|
288 isAliveAndVisible: function isAliveAndVisible(aAccessible, aIsOnScreen) { |
|
289 if (!aAccessible) { |
|
290 return false; |
|
291 } |
|
292 |
|
293 try { |
|
294 let state = this.getState(aAccessible); |
|
295 if (state.contains(States.DEFUNCT) || state.contains(States.INVISIBLE) || |
|
296 (aIsOnScreen && state.contains(States.OFFSCREEN)) || |
|
297 Utils.inHiddenSubtree(aAccessible)) { |
|
298 return false; |
|
299 } |
|
300 } catch (x) { |
|
301 return false; |
|
302 } |
|
303 |
|
304 return true; |
|
305 }, |
|
306 |
|
307 matchAttributeValue: function matchAttributeValue(aAttributeValue, values) { |
|
308 let attrSet = new Set(aAttributeValue.split(' ')); |
|
309 for (let value of values) { |
|
310 if (attrSet.has(value)) { |
|
311 return value; |
|
312 } |
|
313 } |
|
314 }, |
|
315 |
|
316 getLandmarkName: function getLandmarkName(aAccessible) { |
|
317 const landmarks = [ |
|
318 'banner', |
|
319 'complementary', |
|
320 'contentinfo', |
|
321 'main', |
|
322 'navigation', |
|
323 'search' |
|
324 ]; |
|
325 let roles = this.getAttributes(aAccessible)['xml-roles']; |
|
326 if (!roles) { |
|
327 return; |
|
328 } |
|
329 |
|
330 // Looking up a role that would match a landmark. |
|
331 return this.matchAttributeValue(roles, landmarks); |
|
332 }, |
|
333 |
|
334 getEmbeddedControl: function getEmbeddedControl(aLabel) { |
|
335 if (aLabel) { |
|
336 let relation = aLabel.getRelationByType(Relations.LABEL_FOR); |
|
337 for (let i = 0; i < relation.targetsCount; i++) { |
|
338 let target = relation.getTarget(i); |
|
339 if (target.parent === aLabel) { |
|
340 return target; |
|
341 } |
|
342 } |
|
343 } |
|
344 |
|
345 return null; |
|
346 } |
|
347 }; |
|
348 |
|
349 /** |
|
350 * State object used internally to process accessible's states. |
|
351 * @param {Number} aBase Base state. |
|
352 * @param {Number} aExtended Extended state. |
|
353 */ |
|
354 function State(aBase, aExtended) { |
|
355 this.base = aBase; |
|
356 this.extended = aExtended; |
|
357 } |
|
358 |
|
359 State.prototype = { |
|
360 contains: function State_contains(other) { |
|
361 return !!(this.base & other.base || this.extended & other.extended); |
|
362 }, |
|
363 toString: function State_toString() { |
|
364 let stateStrings = Utils.AccRetrieval. |
|
365 getStringStates(this.base, this.extended); |
|
366 let statesArray = new Array(stateStrings.length); |
|
367 for (let i = 0; i < statesArray.length; i++) { |
|
368 statesArray[i] = stateStrings.item(i); |
|
369 } |
|
370 return '[' + statesArray.join(', ') + ']'; |
|
371 } |
|
372 }; |
|
373 |
|
374 this.Logger = { |
|
375 DEBUG: 0, |
|
376 INFO: 1, |
|
377 WARNING: 2, |
|
378 ERROR: 3, |
|
379 _LEVEL_NAMES: ['DEBUG', 'INFO', 'WARNING', 'ERROR'], |
|
380 |
|
381 logLevel: 1, // INFO; |
|
382 |
|
383 test: false, |
|
384 |
|
385 log: function log(aLogLevel) { |
|
386 if (aLogLevel < this.logLevel) |
|
387 return; |
|
388 |
|
389 let args = Array.prototype.slice.call(arguments, 1); |
|
390 let message = (typeof(args[0]) === 'function' ? args[0]() : args).join(' '); |
|
391 message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel] + |
|
392 ' ' + message + '\n'; |
|
393 dump(message); |
|
394 // Note: used for testing purposes. If |this.test| is true, also log to |
|
395 // the console service. |
|
396 if (this.test) { |
|
397 try { |
|
398 Services.console.logStringMessage(message); |
|
399 } catch (ex) { |
|
400 // There was an exception logging to the console service. |
|
401 } |
|
402 } |
|
403 }, |
|
404 |
|
405 info: function info() { |
|
406 this.log.apply( |
|
407 this, [this.INFO].concat(Array.prototype.slice.call(arguments))); |
|
408 }, |
|
409 |
|
410 debug: function debug() { |
|
411 this.log.apply( |
|
412 this, [this.DEBUG].concat(Array.prototype.slice.call(arguments))); |
|
413 }, |
|
414 |
|
415 warning: function warning() { |
|
416 this.log.apply( |
|
417 this, [this.WARNING].concat(Array.prototype.slice.call(arguments))); |
|
418 }, |
|
419 |
|
420 error: function error() { |
|
421 this.log.apply( |
|
422 this, [this.ERROR].concat(Array.prototype.slice.call(arguments))); |
|
423 }, |
|
424 |
|
425 logException: function logException( |
|
426 aException, aErrorMessage = 'An exception has occured') { |
|
427 try { |
|
428 let stackMessage = ''; |
|
429 if (aException.stack) { |
|
430 stackMessage = ' ' + aException.stack.replace(/\n/g, '\n '); |
|
431 } else if (aException.location) { |
|
432 let frame = aException.location; |
|
433 let stackLines = []; |
|
434 while (frame && frame.lineNumber) { |
|
435 stackLines.push( |
|
436 ' ' + frame.name + '@' + frame.filename + ':' + frame.lineNumber); |
|
437 frame = frame.caller; |
|
438 } |
|
439 stackMessage = stackLines.join('\n'); |
|
440 } else { |
|
441 stackMessage = '(' + aException.fileName + ':' + aException.lineNumber + ')'; |
|
442 } |
|
443 this.error(aErrorMessage + ':\n ' + |
|
444 aException.message + '\n' + |
|
445 stackMessage); |
|
446 } catch (x) { |
|
447 this.error(x); |
|
448 } |
|
449 }, |
|
450 |
|
451 accessibleToString: function accessibleToString(aAccessible) { |
|
452 let str = '[ defunct ]'; |
|
453 try { |
|
454 str = '[ ' + Utils.AccRetrieval.getStringRole(aAccessible.role) + |
|
455 ' | ' + aAccessible.name + ' ]'; |
|
456 } catch (x) { |
|
457 } |
|
458 |
|
459 return str; |
|
460 }, |
|
461 |
|
462 eventToString: function eventToString(aEvent) { |
|
463 let str = Utils.AccRetrieval.getStringEventType(aEvent.eventType); |
|
464 if (aEvent.eventType == Events.STATE_CHANGE) { |
|
465 let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent); |
|
466 let stateStrings = event.isExtraState ? |
|
467 Utils.AccRetrieval.getStringStates(0, event.state) : |
|
468 Utils.AccRetrieval.getStringStates(event.state, 0); |
|
469 str += ' (' + stateStrings.item(0) + ')'; |
|
470 } |
|
471 |
|
472 if (aEvent.eventType == Events.VIRTUALCURSOR_CHANGED) { |
|
473 let event = aEvent.QueryInterface( |
|
474 Ci.nsIAccessibleVirtualCursorChangeEvent); |
|
475 let pivot = aEvent.accessible.QueryInterface( |
|
476 Ci.nsIAccessibleDocument).virtualCursor; |
|
477 str += ' (' + this.accessibleToString(event.oldAccessible) + ' -> ' + |
|
478 this.accessibleToString(pivot.position) + ')'; |
|
479 } |
|
480 |
|
481 return str; |
|
482 }, |
|
483 |
|
484 statesToString: function statesToString(aAccessible) { |
|
485 return Utils.getState(aAccessible).toString(); |
|
486 }, |
|
487 |
|
488 dumpTree: function dumpTree(aLogLevel, aRootAccessible) { |
|
489 if (aLogLevel < this.logLevel) |
|
490 return; |
|
491 |
|
492 this._dumpTreeInternal(aLogLevel, aRootAccessible, 0); |
|
493 }, |
|
494 |
|
495 _dumpTreeInternal: function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) { |
|
496 let indentStr = ''; |
|
497 for (var i=0; i < aIndent; i++) |
|
498 indentStr += ' '; |
|
499 this.log(aLogLevel, indentStr, |
|
500 this.accessibleToString(aAccessible), |
|
501 '(' + this.statesToString(aAccessible) + ')'); |
|
502 for (var i=0; i < aAccessible.childCount; i++) |
|
503 this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i), aIndent + 1); |
|
504 } |
|
505 }; |
|
506 |
|
507 /** |
|
508 * PivotContext: An object that generates and caches context information |
|
509 * for a given accessible and its relationship with another accessible. |
|
510 * |
|
511 * If the given accessible is a label for a nested control, then this |
|
512 * context will represent the nested control instead of the label. |
|
513 * With the exception of bounds calculation, which will use the containing |
|
514 * label. In this case the |accessible| field would be the embedded control, |
|
515 * and the |accessibleForBounds| field would be the label. |
|
516 */ |
|
517 this.PivotContext = function PivotContext(aAccessible, aOldAccessible, |
|
518 aStartOffset, aEndOffset, aIgnoreAncestry = false, |
|
519 aIncludeInvisible = false) { |
|
520 this._accessible = aAccessible; |
|
521 this._nestedControl = Utils.getEmbeddedControl(aAccessible); |
|
522 this._oldAccessible = |
|
523 this._isDefunct(aOldAccessible) ? null : aOldAccessible; |
|
524 this.startOffset = aStartOffset; |
|
525 this.endOffset = aEndOffset; |
|
526 this._ignoreAncestry = aIgnoreAncestry; |
|
527 this._includeInvisible = aIncludeInvisible; |
|
528 } |
|
529 |
|
530 PivotContext.prototype = { |
|
531 get accessible() { |
|
532 // If the current pivot accessible has a nested control, |
|
533 // make this context use it publicly. |
|
534 return this._nestedControl || this._accessible; |
|
535 }, |
|
536 |
|
537 get oldAccessible() { |
|
538 return this._oldAccessible; |
|
539 }, |
|
540 |
|
541 get isNestedControl() { |
|
542 return !!this._nestedControl; |
|
543 }, |
|
544 |
|
545 get accessibleForBounds() { |
|
546 return this._accessible; |
|
547 }, |
|
548 |
|
549 get textAndAdjustedOffsets() { |
|
550 if (this.startOffset === -1 && this.endOffset === -1) { |
|
551 return null; |
|
552 } |
|
553 |
|
554 if (!this._textAndAdjustedOffsets) { |
|
555 let result = {startOffset: this.startOffset, |
|
556 endOffset: this.endOffset, |
|
557 text: this._accessible.QueryInterface(Ci.nsIAccessibleText). |
|
558 getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT)}; |
|
559 let hypertextAcc = this._accessible.QueryInterface(Ci.nsIAccessibleHyperText); |
|
560 |
|
561 // Iterate through the links in backwards order so text replacements don't |
|
562 // affect the offsets of links yet to be processed. |
|
563 for (let i = hypertextAcc.linkCount - 1; i >= 0; i--) { |
|
564 let link = hypertextAcc.getLinkAt(i); |
|
565 let linkText = ''; |
|
566 if (link instanceof Ci.nsIAccessibleText) { |
|
567 linkText = link.QueryInterface(Ci.nsIAccessibleText). |
|
568 getText(0, Ci.nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT); |
|
569 } |
|
570 |
|
571 let start = link.startIndex; |
|
572 let end = link.endIndex; |
|
573 for (let offset of ['startOffset', 'endOffset']) { |
|
574 if (this[offset] >= end) { |
|
575 result[offset] += linkText.length - (end - start); |
|
576 } |
|
577 } |
|
578 result.text = result.text.substring(0, start) + linkText + |
|
579 result.text.substring(end); |
|
580 } |
|
581 |
|
582 this._textAndAdjustedOffsets = result; |
|
583 } |
|
584 |
|
585 return this._textAndAdjustedOffsets; |
|
586 }, |
|
587 |
|
588 /** |
|
589 * Get a list of |aAccessible|'s ancestry up to the root. |
|
590 * @param {nsIAccessible} aAccessible. |
|
591 * @return {Array} Ancestry list. |
|
592 */ |
|
593 _getAncestry: function _getAncestry(aAccessible) { |
|
594 let ancestry = []; |
|
595 let parent = aAccessible; |
|
596 try { |
|
597 while (parent && (parent = parent.parent)) { |
|
598 ancestry.push(parent); |
|
599 } |
|
600 } catch (x) { |
|
601 // A defunct accessible will raise an exception geting parent. |
|
602 Logger.debug('Failed to get parent:', x); |
|
603 } |
|
604 return ancestry.reverse(); |
|
605 }, |
|
606 |
|
607 /** |
|
608 * A list of the old accessible's ancestry. |
|
609 */ |
|
610 get oldAncestry() { |
|
611 if (!this._oldAncestry) { |
|
612 if (!this._oldAccessible || this._ignoreAncestry) { |
|
613 this._oldAncestry = []; |
|
614 } else { |
|
615 this._oldAncestry = this._getAncestry(this._oldAccessible); |
|
616 this._oldAncestry.push(this._oldAccessible); |
|
617 } |
|
618 } |
|
619 return this._oldAncestry; |
|
620 }, |
|
621 |
|
622 /** |
|
623 * A list of the current accessible's ancestry. |
|
624 */ |
|
625 get currentAncestry() { |
|
626 if (!this._currentAncestry) { |
|
627 this._currentAncestry = this._ignoreAncestry ? [] : |
|
628 this._getAncestry(this.accessible); |
|
629 } |
|
630 return this._currentAncestry; |
|
631 }, |
|
632 |
|
633 /* |
|
634 * This is a list of the accessible's ancestry up to the common ancestor |
|
635 * of the accessible and the old accessible. It is useful for giving the |
|
636 * user context as to where they are in the heirarchy. |
|
637 */ |
|
638 get newAncestry() { |
|
639 if (!this._newAncestry) { |
|
640 this._newAncestry = this._ignoreAncestry ? [] : [currentAncestor for ( |
|
641 [index, currentAncestor] of Iterator(this.currentAncestry)) if ( |
|
642 currentAncestor !== this.oldAncestry[index])]; |
|
643 } |
|
644 return this._newAncestry; |
|
645 }, |
|
646 |
|
647 /* |
|
648 * Traverse the accessible's subtree in pre or post order. |
|
649 * It only includes the accessible's visible chidren. |
|
650 * Note: needSubtree is a function argument that can be used to determine |
|
651 * whether aAccessible's subtree is required. |
|
652 */ |
|
653 _traverse: function _traverse(aAccessible, aPreorder, aStop) { |
|
654 if (aStop && aStop(aAccessible)) { |
|
655 return; |
|
656 } |
|
657 let child = aAccessible.firstChild; |
|
658 while (child) { |
|
659 let include; |
|
660 if (this._includeInvisible) { |
|
661 include = true; |
|
662 } else { |
|
663 include = !(Utils.getState(child).contains(States.INVISIBLE)); |
|
664 } |
|
665 if (include) { |
|
666 if (aPreorder) { |
|
667 yield child; |
|
668 [yield node for (node of this._traverse(child, aPreorder, aStop))]; |
|
669 } else { |
|
670 [yield node for (node of this._traverse(child, aPreorder, aStop))]; |
|
671 yield child; |
|
672 } |
|
673 } |
|
674 child = child.nextSibling; |
|
675 } |
|
676 }, |
|
677 |
|
678 /* |
|
679 * A subtree generator function, used to generate a flattened |
|
680 * list of the accessible's subtree in pre or post order. |
|
681 * It only includes the accessible's visible chidren. |
|
682 * @param {boolean} aPreorder A flag for traversal order. If true, traverse |
|
683 * in preorder; if false, traverse in postorder. |
|
684 * @param {function} aStop An optional function, indicating whether subtree |
|
685 * traversal should stop. |
|
686 */ |
|
687 subtreeGenerator: function subtreeGenerator(aPreorder, aStop) { |
|
688 return this._traverse(this.accessible, aPreorder, aStop); |
|
689 }, |
|
690 |
|
691 getCellInfo: function getCellInfo(aAccessible) { |
|
692 if (!this._cells) { |
|
693 this._cells = new WeakMap(); |
|
694 } |
|
695 |
|
696 let domNode = aAccessible.DOMNode; |
|
697 if (this._cells.has(domNode)) { |
|
698 return this._cells.get(domNode); |
|
699 } |
|
700 |
|
701 let cellInfo = {}; |
|
702 let getAccessibleCell = function getAccessibleCell(aAccessible) { |
|
703 if (!aAccessible) { |
|
704 return null; |
|
705 } |
|
706 if ([Roles.CELL, Roles.COLUMNHEADER, Roles.ROWHEADER].indexOf( |
|
707 aAccessible.role) < 0) { |
|
708 return null; |
|
709 } |
|
710 try { |
|
711 return aAccessible.QueryInterface(Ci.nsIAccessibleTableCell); |
|
712 } catch (x) { |
|
713 Logger.logException(x); |
|
714 return null; |
|
715 } |
|
716 }; |
|
717 let getHeaders = function getHeaders(aHeaderCells) { |
|
718 let enumerator = aHeaderCells.enumerate(); |
|
719 while (enumerator.hasMoreElements()) { |
|
720 yield enumerator.getNext().QueryInterface(Ci.nsIAccessible).name; |
|
721 } |
|
722 }; |
|
723 |
|
724 cellInfo.current = getAccessibleCell(aAccessible); |
|
725 |
|
726 if (!cellInfo.current) { |
|
727 Logger.warning(aAccessible, |
|
728 'does not support nsIAccessibleTableCell interface.'); |
|
729 this._cells.set(domNode, null); |
|
730 return null; |
|
731 } |
|
732 |
|
733 let table = cellInfo.current.table; |
|
734 if (table.isProbablyForLayout()) { |
|
735 this._cells.set(domNode, null); |
|
736 return null; |
|
737 } |
|
738 |
|
739 cellInfo.previous = null; |
|
740 let oldAncestry = this.oldAncestry.reverse(); |
|
741 let ancestor = oldAncestry.shift(); |
|
742 while (!cellInfo.previous && ancestor) { |
|
743 let cell = getAccessibleCell(ancestor); |
|
744 if (cell && cell.table === table) { |
|
745 cellInfo.previous = cell; |
|
746 } |
|
747 ancestor = oldAncestry.shift(); |
|
748 } |
|
749 |
|
750 if (cellInfo.previous) { |
|
751 cellInfo.rowChanged = cellInfo.current.rowIndex !== |
|
752 cellInfo.previous.rowIndex; |
|
753 cellInfo.columnChanged = cellInfo.current.columnIndex !== |
|
754 cellInfo.previous.columnIndex; |
|
755 } else { |
|
756 cellInfo.rowChanged = true; |
|
757 cellInfo.columnChanged = true; |
|
758 } |
|
759 |
|
760 cellInfo.rowExtent = cellInfo.current.rowExtent; |
|
761 cellInfo.columnExtent = cellInfo.current.columnExtent; |
|
762 cellInfo.columnIndex = cellInfo.current.columnIndex; |
|
763 cellInfo.rowIndex = cellInfo.current.rowIndex; |
|
764 |
|
765 cellInfo.columnHeaders = []; |
|
766 if (cellInfo.columnChanged && cellInfo.current.role !== |
|
767 Roles.COLUMNHEADER) { |
|
768 cellInfo.columnHeaders = [headers for (headers of getHeaders( |
|
769 cellInfo.current.columnHeaderCells))]; |
|
770 } |
|
771 cellInfo.rowHeaders = []; |
|
772 if (cellInfo.rowChanged && cellInfo.current.role === Roles.CELL) { |
|
773 cellInfo.rowHeaders = [headers for (headers of getHeaders( |
|
774 cellInfo.current.rowHeaderCells))]; |
|
775 } |
|
776 |
|
777 this._cells.set(domNode, cellInfo); |
|
778 return cellInfo; |
|
779 }, |
|
780 |
|
781 get bounds() { |
|
782 if (!this._bounds) { |
|
783 this._bounds = Utils.getBounds(this.accessibleForBounds); |
|
784 } |
|
785 |
|
786 return this._bounds.clone(); |
|
787 }, |
|
788 |
|
789 _isDefunct: function _isDefunct(aAccessible) { |
|
790 try { |
|
791 return Utils.getState(aAccessible).contains(States.DEFUNCT); |
|
792 } catch (x) { |
|
793 return true; |
|
794 } |
|
795 } |
|
796 }; |
|
797 |
|
798 this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) { |
|
799 this.name = aName; |
|
800 this.callback = aCallback; |
|
801 |
|
802 let branch = Services.prefs; |
|
803 this.value = this._getValue(branch); |
|
804 |
|
805 if (this.callback && aRunCallbackNow) { |
|
806 try { |
|
807 this.callback(this.name, this.value); |
|
808 } catch (x) { |
|
809 Logger.logException(x); |
|
810 } |
|
811 } |
|
812 |
|
813 branch.addObserver(aName, this, true); |
|
814 }; |
|
815 |
|
816 PrefCache.prototype = { |
|
817 _getValue: function _getValue(aBranch) { |
|
818 try { |
|
819 if (!this.type) { |
|
820 this.type = aBranch.getPrefType(this.name); |
|
821 } |
|
822 switch (this.type) { |
|
823 case Ci.nsIPrefBranch.PREF_STRING: |
|
824 return aBranch.getCharPref(this.name); |
|
825 case Ci.nsIPrefBranch.PREF_INT: |
|
826 return aBranch.getIntPref(this.name); |
|
827 case Ci.nsIPrefBranch.PREF_BOOL: |
|
828 return aBranch.getBoolPref(this.name); |
|
829 default: |
|
830 return null; |
|
831 } |
|
832 } catch (x) { |
|
833 // Pref does not exist. |
|
834 return null; |
|
835 } |
|
836 }, |
|
837 |
|
838 observe: function observe(aSubject, aTopic, aData) { |
|
839 this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch)); |
|
840 if (this.callback) { |
|
841 try { |
|
842 this.callback(this.name, this.value); |
|
843 } catch (x) { |
|
844 Logger.logException(x); |
|
845 } |
|
846 } |
|
847 }, |
|
848 |
|
849 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, |
|
850 Ci.nsISupportsWeakReference]) |
|
851 }; |
|
852 |
|
853 this.SettingCache = function SettingCache(aName, aCallback, aOptions = {}) { |
|
854 this.value = aOptions.defaultValue; |
|
855 let runCallback = () => { |
|
856 if (aCallback) { |
|
857 aCallback(aName, this.value); |
|
858 if (aOptions.callbackOnce) { |
|
859 runCallback = () => {}; |
|
860 } |
|
861 } |
|
862 }; |
|
863 |
|
864 let settings = Utils.win.navigator.mozSettings; |
|
865 if (!settings) { |
|
866 if (aOptions.callbackNow) { |
|
867 runCallback(); |
|
868 } |
|
869 return; |
|
870 } |
|
871 |
|
872 |
|
873 let lock = settings.createLock(); |
|
874 let req = lock.get(aName); |
|
875 |
|
876 req.addEventListener('success', () => { |
|
877 this.value = req.result[aName] == undefined ? aOptions.defaultValue : req.result[aName]; |
|
878 if (aOptions.callbackNow) { |
|
879 runCallback(); |
|
880 } |
|
881 }); |
|
882 |
|
883 settings.addObserver(aName, |
|
884 (evt) => { |
|
885 this.value = evt.settingValue; |
|
886 runCallback(); |
|
887 }); |
|
888 }; |