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 7const Cu = Components.utils; 8const Cc = Components.classes; 9const Ci = Components.interfaces; 10 11Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 12XPCOMUtils.defineLazyModuleGetter(this, 'Services', 13 'resource://gre/modules/Services.jsm'); 14XPCOMUtils.defineLazyModuleGetter(this, 'Rect', 15 'resource://gre/modules/Geometry.jsm'); 16 17this.EXPORTED_SYMBOLS = ['Utils', 'Logger', 'PivotContext', 'PrefCache']; 18 19this.Utils = { 20 _buildAppMap: { 21 '{3c2e2abc-06d4-11e1-ac3b-374f68613e61}': 'b2g', 22 '{ec8030f7-c20a-464f-9b0e-13a3a9e97384}': 'browser', 23 '{aa3c5121-dab2-40e2-81ca-7ea25febc110}': 'mobile/android', 24 '{a23983c0-fd0e-11dc-95ff-0800200c9a66}': 'mobile/xul' 25 }, 26 27 init: function Utils_init(aWindow) { 28 if (this._win) 29 // XXX: only supports attaching to one window now. 30 throw new Error('Only one top-level window could used with AccessFu'); 31 32 this._win = Cu.getWeakReference(aWindow); 33 }, 34 35 uninit: function Utils_uninit() { 36 if (!this._win) { 37 return; 38 } 39 delete this._win; 40 }, 41 42 get win() { 43 if (!this._win) { 44 return null; 45 } 46 return this._win.get(); 47 }, 48 49 get AccRetrieval() { 50 if (!this._AccRetrieval) { 51 this._AccRetrieval = Cc['@mozilla.org/accessibleRetrieval;1']. 52 getService(Ci.nsIAccessibleRetrieval); 53 } 54 55 return this._AccRetrieval; 56 }, 57 58 set MozBuildApp(value) { 59 this._buildApp = value; 60 }, 61 62 get MozBuildApp() { 63 if (!this._buildApp) 64 this._buildApp = this._buildAppMap[Services.appinfo.ID]; 65 return this._buildApp; 66 }, 67 68 get OS() { 69 if (!this._OS) 70 this._OS = Services.appinfo.OS; 71 return this._OS; 72 }, 73 74 get ScriptName() { 75 if (!this._ScriptName) 76 this._ScriptName = 77 (Services.appinfo.processType == 2) ? 'AccessFuContent' : 'AccessFu'; 78 return this._ScriptName; 79 }, 80 81 get AndroidSdkVersion() { 82 if (!this._AndroidSdkVersion) { 83 if (Services.appinfo.OS == 'Android') { 84 this._AndroidSdkVersion = Services.sysinfo.getPropertyAsInt32('version'); 85 } else { 86 // Most useful in desktop debugging. 87 this._AndroidSdkVersion = 15; 88 } 89 } 90 return this._AndroidSdkVersion; 91 }, 92 93 set AndroidSdkVersion(value) { 94 // When we want to mimic another version. 95 this._AndroidSdkVersion = value; 96 }, 97 98 get BrowserApp() { 99 if (!this.win) { 100 return null; 101 } 102 switch (this.MozBuildApp) { 103 case 'mobile/android': 104 return this.win.BrowserApp; 105 case 'browser': 106 return this.win.gBrowser; 107 case 'b2g': 108 return this.win.shell; 109 default: 110 return null; 111 } 112 }, 113 114 get CurrentBrowser() { 115 if (!this.BrowserApp) { 116 return null; 117 } 118 if (this.MozBuildApp == 'b2g') 119 return this.BrowserApp.contentBrowser; 120 return this.BrowserApp.selectedBrowser; 121 }, 122 123 get CurrentContentDoc() { 124 let browser = this.CurrentBrowser; 125 return browser ? browser.contentDocument : null; 126 }, 127 128 get AllMessageManagers() { 129 let messageManagers = []; 130 131 for (let i = 0; i < this.win.messageManager.childCount; i++) 132 messageManagers.push(this.win.messageManager.getChildAt(i)); 133 134 let document = this.CurrentContentDoc; 135 136 if (document) { 137 let remoteframes = document.querySelectorAll('iframe'); 138 139 for (let i = 0; i < remoteframes.length; ++i) { 140 let mm = this.getMessageManager(remoteframes[i]); 141 if (mm) { 142 messageManagers.push(mm); 143 } 144 } 145 146 } 147 148 return messageManagers; 149 }, 150 151 get isContentProcess() { 152 delete this.isContentProcess; 153 this.isContentProcess = 154 Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT; 155 return this.isContentProcess; 156 }, 157 158 getMessageManager: function getMessageManager(aBrowser) { 159 try { 160 return aBrowser.QueryInterface(Ci.nsIFrameLoaderOwner). 161 frameLoader.messageManager; 162 } catch (x) { 163 Logger.logException(x); 164 return null; 165 } 166 }, 167 168 getViewport: function getViewport(aWindow) { 169 switch (this.MozBuildApp) { 170 case 'mobile/android': 171 return aWindow.BrowserApp.selectedTab.getViewport(); 172 default: 173 return null; 174 } 175 }, 176 177 getStates: function getStates(aAccessible) { 178 if (!aAccessible) 179 return [0, 0]; 180 181 let state = {}; 182 let extState = {}; 183 aAccessible.getState(state, extState); 184 return [state.value, extState.value]; 185 }, 186 187 getAttributes: function getAttributes(aAccessible) { 188 let attributesEnum = aAccessible.attributes.enumerate(); 189 let attributes = {}; 190 191 // Populate |attributes| object with |aAccessible|'s attribute key-value 192 // pairs. 193 while (attributesEnum.hasMoreElements()) { 194 let attribute = attributesEnum.getNext().QueryInterface( 195 Ci.nsIPropertyElement); 196 attributes[attribute.key] = attribute.value; 197 } 198 199 return attributes; 200 }, 201 202 getVirtualCursor: function getVirtualCursor(aDocument) { 203 let doc = (aDocument instanceof Ci.nsIAccessible) ? aDocument : 204 this.AccRetrieval.getAccessibleFor(aDocument); 205 206 return doc.QueryInterface(Ci.nsIAccessibleDocument).virtualCursor; 207 }, 208 209 getPixelsPerCSSPixel: function getPixelsPerCSSPixel(aWindow) { 210 return aWindow.QueryInterface(Ci.nsIInterfaceRequestor) 211 .getInterface(Ci.nsIDOMWindowUtils).screenPixelsPerCSSPixel; 212 } 213}; 214 215this.Logger = { 216 DEBUG: 0, 217 INFO: 1, 218 WARNING: 2, 219 ERROR: 3, 220 _LEVEL_NAMES: ['DEBUG', 'INFO', 'WARNING', 'ERROR'], 221 222 logLevel: 1, // INFO; 223 224 test: false, 225 226 log: function log(aLogLevel) { 227 if (aLogLevel < this.logLevel) 228 return; 229 230 let message = Array.prototype.slice.call(arguments, 1).join(' '); 231 message = '[' + Utils.ScriptName + '] ' + this._LEVEL_NAMES[aLogLevel] + 232 ' ' + message + '\n'; 233 dump(message); 234 // Note: used for testing purposes. If |this.test| is true, also log to 235 // the console service. 236 if (this.test) { 237 try { 238 Services.console.logStringMessage(message); 239 } catch (ex) { 240 // There was an exception logging to the console service. 241 } 242 } 243 }, 244 245 info: function info() { 246 this.log.apply( 247 this, [this.INFO].concat(Array.prototype.slice.call(arguments))); 248 }, 249 250 debug: function debug() { 251 this.log.apply( 252 this, [this.DEBUG].concat(Array.prototype.slice.call(arguments))); 253 }, 254 255 warning: function warning() { 256 this.log.apply( 257 this, [this.WARNING].concat(Array.prototype.slice.call(arguments))); 258 }, 259 260 error: function error() { 261 this.log.apply( 262 this, [this.ERROR].concat(Array.prototype.slice.call(arguments))); 263 }, 264 265 logException: function logException(aException) { 266 try { 267 let args = [aException.message]; 268 args.push.apply(args, aException.stack ? ['\n', aException.stack] : 269 ['(' + aException.fileName + ':' + aException.lineNumber + ')']); 270 this.error.apply(this, args); 271 } catch (x) { 272 this.error(x); 273 } 274 }, 275 276 accessibleToString: function accessibleToString(aAccessible) { 277 let str = '[ defunct ]'; 278 try { 279 str = '[ ' + Utils.AccRetrieval.getStringRole(aAccessible.role) + 280 ' | ' + aAccessible.name + ' ]'; 281 } catch (x) { 282 } 283 284 return str; 285 }, 286 287 eventToString: function eventToString(aEvent) { 288 let str = Utils.AccRetrieval.getStringEventType(aEvent.eventType); 289 if (aEvent.eventType == Ci.nsIAccessibleEvent.EVENT_STATE_CHANGE) { 290 let event = aEvent.QueryInterface(Ci.nsIAccessibleStateChangeEvent); 291 let stateStrings = event.isExtraState ? 292 Utils.AccRetrieval.getStringStates(0, event.state) : 293 Utils.AccRetrieval.getStringStates(event.state, 0); 294 str += ' (' + stateStrings.item(0) + ')'; 295 } 296 297 return str; 298 }, 299 300 statesToString: function statesToString(aAccessible) { 301 let [state, extState] = Utils.getStates(aAccessible); 302 let stringArray = []; 303 let stateStrings = Utils.AccRetrieval.getStringStates(state, extState); 304 for (var i=0; i < stateStrings.length; i++) 305 stringArray.push(stateStrings.item(i)); 306 return stringArray.join(' '); 307 }, 308 309 dumpTree: function dumpTree(aLogLevel, aRootAccessible) { 310 if (aLogLevel < this.logLevel) 311 return; 312 313 this._dumpTreeInternal(aLogLevel, aRootAccessible, 0); 314 }, 315 316 _dumpTreeInternal: function _dumpTreeInternal(aLogLevel, aAccessible, aIndent) { 317 let indentStr = ''; 318 for (var i=0; i < aIndent; i++) 319 indentStr += ' '; 320 this.log(aLogLevel, indentStr, 321 this.accessibleToString(aAccessible), 322 '(' + this.statesToString(aAccessible) + ')'); 323 for (var i=0; i < aAccessible.childCount; i++) 324 this._dumpTreeInternal(aLogLevel, aAccessible.getChildAt(i), aIndent + 1); 325 } 326}; 327 328/** 329 * PivotContext: An object that generates and caches context information 330 * for a given accessible and its relationship with another accessible. 331 */ 332this.PivotContext = function PivotContext(aAccessible, aOldAccessible) { 333 this._accessible = aAccessible; 334 this._oldAccessible = 335 this._isDefunct(aOldAccessible) ? null : aOldAccessible; 336} 337 338PivotContext.prototype = { 339 get accessible() { 340 return this._accessible; 341 }, 342 343 get oldAccessible() { 344 return this._oldAccessible; 345 }, 346 347 /* 348 * This is a list of the accessible's ancestry up to the common ancestor 349 * of the accessible and the old accessible. It is useful for giving the 350 * user context as to where they are in the heirarchy. 351 */ 352 get newAncestry() { 353 if (!this._newAncestry) { 354 let newLineage = []; 355 let oldLineage = []; 356 357 let parent = this._accessible; 358 while (parent && (parent = parent.parent)) 359 newLineage.push(parent); 360 361 parent = this._oldAccessible; 362 while (parent && (parent = parent.parent)) 363 oldLineage.push(parent); 364 365 this._newAncestry = []; 366 367 while (true) { 368 let newAncestor = newLineage.pop(); 369 let oldAncestor = oldLineage.pop(); 370 371 if (newAncestor == undefined) 372 break; 373 374 if (newAncestor != oldAncestor) 375 this._newAncestry.push(newAncestor); 376 } 377 378 } 379 380 return this._newAncestry; 381 }, 382 383 /* 384 * Traverse the accessible's subtree in pre or post order. 385 * It only includes the accessible's visible chidren. 386 */ 387 _traverse: function _traverse(aAccessible, preorder) { 388 let list = []; 389 let child = aAccessible.firstChild; 390 while (child) { 391 let state = {}; 392 child.getState(state, {}); 393 if (!(state.value & Ci.nsIAccessibleStates.STATE_INVISIBLE)) { 394 let traversed = _traverse(child, preorder); 395 // Prepend or append a child, based on traverse order. 396 traversed[preorder ? "unshift" : "push"](child); 397 list.push.apply(list, traversed); 398 } 399 child = child.nextSibling; 400 } 401 return list; 402 }, 403 404 /* 405 * This is a flattened list of the accessible's subtree in preorder. 406 * It only includes the accessible's visible chidren. 407 */ 408 get subtreePreorder() { 409 if (!this._subtreePreOrder) 410 this._subtreePreOrder = this._traverse(this._accessible, true); 411 412 return this._subtreePreOrder; 413 }, 414 415 /* 416 * This is a flattened list of the accessible's subtree in postorder. 417 * It only includes the accessible's visible chidren. 418 */ 419 get subtreePostorder() { 420 if (!this._subtreePostOrder) 421 this._subtreePostOrder = this._traverse(this._accessible, false); 422 423 return this._subtreePostOrder; 424 }, 425 426 get bounds() { 427 if (!this._bounds) { 428 let objX = {}, objY = {}, objW = {}, objH = {}; 429 430 this._accessible.getBounds(objX, objY, objW, objH); 431 432 this._bounds = new Rect(objX.value, objY.value, objW.value, objH.value); 433 } 434 435 return this._bounds.clone(); 436 }, 437 438 _isDefunct: function _isDefunct(aAccessible) { 439 try { 440 let extstate = {}; 441 aAccessible.getState({}, extstate); 442 return !!(aAccessible.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT); 443 } catch (x) { 444 return true; 445 } 446 } 447}; 448 449this.PrefCache = function PrefCache(aName, aCallback, aRunCallbackNow) { 450 this.name = aName; 451 this.callback = aCallback; 452 453 let branch = Services.prefs; 454 this.value = this._getValue(branch); 455 456 if (this.callback && aRunCallbackNow) { 457 try { 458 this.callback(this.name, this.value); 459 } catch (x) { 460 Logger.logException(x); 461 } 462 } 463 464 branch.addObserver(aName, this, true); 465}; 466 467PrefCache.prototype = { 468 _getValue: function _getValue(aBranch) { 469 if (!this.type) { 470 this.type = aBranch.getPrefType(this.name); 471 } 472 473 switch (this.type) { 474 case Ci.nsIPrefBranch.PREF_STRING: 475 return aBranch.getCharPref(this.name); 476 case Ci.nsIPrefBranch.PREF_INT: 477 return aBranch.getIntPref(this.name); 478 case Ci.nsIPrefBranch.PREF_BOOL: 479 return aBranch.getBoolPref(this.name); 480 default: 481 return null; 482 } 483 }, 484 485 observe: function observe(aSubject, aTopic, aData) { 486 this.value = this._getValue(aSubject.QueryInterface(Ci.nsIPrefBranch)); 487 if (this.callback) { 488 try { 489 this.callback(this.name, this.value); 490 } catch (x) { 491 Logger.logException(x); 492 } 493 } 494 }, 495 496 QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, 497 Ci.nsISupportsWeakReference]) 498};