accessible/src/jsat/ContentControl.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 let Ci = Components.interfaces;
michael@0 6 let Cu = Components.utils;
michael@0 7
michael@0 8 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 9 XPCOMUtils.defineLazyModuleGetter(this, 'Services',
michael@0 10 'resource://gre/modules/Services.jsm');
michael@0 11 XPCOMUtils.defineLazyModuleGetter(this, 'Utils',
michael@0 12 'resource://gre/modules/accessibility/Utils.jsm');
michael@0 13 XPCOMUtils.defineLazyModuleGetter(this, 'Logger',
michael@0 14 'resource://gre/modules/accessibility/Utils.jsm');
michael@0 15 XPCOMUtils.defineLazyModuleGetter(this, 'Roles',
michael@0 16 'resource://gre/modules/accessibility/Constants.jsm');
michael@0 17 XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules',
michael@0 18 'resource://gre/modules/accessibility/TraversalRules.jsm');
michael@0 19 XPCOMUtils.defineLazyModuleGetter(this, 'Presentation',
michael@0 20 'resource://gre/modules/accessibility/Presentation.jsm');
michael@0 21
michael@0 22 this.EXPORTED_SYMBOLS = ['ContentControl'];
michael@0 23
michael@0 24 const MOVEMENT_GRANULARITY_CHARACTER = 1;
michael@0 25 const MOVEMENT_GRANULARITY_WORD = 2;
michael@0 26 const MOVEMENT_GRANULARITY_PARAGRAPH = 8;
michael@0 27
michael@0 28 this.ContentControl = function ContentControl(aContentScope) {
michael@0 29 this._contentScope = Cu.getWeakReference(aContentScope);
michael@0 30 this._vcCache = new WeakMap();
michael@0 31 this._childMessageSenders = new WeakMap();
michael@0 32 };
michael@0 33
michael@0 34 this.ContentControl.prototype = {
michael@0 35 messagesOfInterest: ['AccessFu:MoveCursor',
michael@0 36 'AccessFu:ClearCursor',
michael@0 37 'AccessFu:MoveToPoint',
michael@0 38 'AccessFu:AutoMove',
michael@0 39 'AccessFu:Activate',
michael@0 40 'AccessFu:MoveCaret',
michael@0 41 'AccessFu:MoveByGranularity'],
michael@0 42
michael@0 43 start: function cc_start() {
michael@0 44 let cs = this._contentScope.get();
michael@0 45 for (let message of this.messagesOfInterest) {
michael@0 46 cs.addMessageListener(message, this);
michael@0 47 }
michael@0 48 },
michael@0 49
michael@0 50 stop: function cc_stop() {
michael@0 51 let cs = this._contentScope.get();
michael@0 52 for (let message of this.messagesOfInterest) {
michael@0 53 cs.removeMessageListener(message, this);
michael@0 54 }
michael@0 55 },
michael@0 56
michael@0 57 get document() {
michael@0 58 return this._contentScope.get().content.document;
michael@0 59 },
michael@0 60
michael@0 61 get window() {
michael@0 62 return this._contentScope.get().content;
michael@0 63 },
michael@0 64
michael@0 65 get vc() {
michael@0 66 return Utils.getVirtualCursor(this.document);
michael@0 67 },
michael@0 68
michael@0 69 receiveMessage: function cc_receiveMessage(aMessage) {
michael@0 70 Logger.debug(() => {
michael@0 71 return ['ContentControl.receiveMessage',
michael@0 72 aMessage.name,
michael@0 73 JSON.stringify(aMessage.json)];
michael@0 74 });
michael@0 75
michael@0 76 try {
michael@0 77 let func = this['handle' + aMessage.name.slice(9)]; // 'AccessFu:'.length
michael@0 78 if (func) {
michael@0 79 func.bind(this)(aMessage);
michael@0 80 } else {
michael@0 81 Logger.warning('ContentControl: Unhandled message:', aMessage.name);
michael@0 82 }
michael@0 83 } catch (x) {
michael@0 84 Logger.logException(
michael@0 85 x, 'Error handling message: ' + JSON.stringify(aMessage.json));
michael@0 86 }
michael@0 87 },
michael@0 88
michael@0 89 handleMoveCursor: function cc_handleMoveCursor(aMessage) {
michael@0 90 let origin = aMessage.json.origin;
michael@0 91 let action = aMessage.json.action;
michael@0 92 let vc = this.vc;
michael@0 93
michael@0 94 if (origin != 'child' && this.sendToChild(vc, aMessage)) {
michael@0 95 // Forwarded succesfully to child cursor.
michael@0 96 return;
michael@0 97 }
michael@0 98
michael@0 99 let moved = vc[action](TraversalRules[aMessage.json.rule]);
michael@0 100
michael@0 101 if (moved) {
michael@0 102 if (origin === 'child') {
michael@0 103 // We just stepped out of a child, clear child cursor.
michael@0 104 Utils.getMessageManager(aMessage.target).sendAsyncMessage(
michael@0 105 'AccessFu:ClearCursor', {});
michael@0 106 } else {
michael@0 107 // We potentially landed on a new child cursor. If so, we want to
michael@0 108 // either be on the first or last item in the child doc.
michael@0 109 let childAction = action;
michael@0 110 if (action === 'moveNext') {
michael@0 111 childAction = 'moveFirst';
michael@0 112 } else if (action === 'movePrevious') {
michael@0 113 childAction = 'moveLast';
michael@0 114 }
michael@0 115
michael@0 116 // Attempt to forward move to a potential child cursor in our
michael@0 117 // new position.
michael@0 118 this.sendToChild(vc, aMessage, { action: childAction});
michael@0 119 }
michael@0 120 } else if (!this._childMessageSenders.has(aMessage.target)) {
michael@0 121 // We failed to move, and the message is not from a child, so forward
michael@0 122 // to parent.
michael@0 123 this.sendToParent(aMessage);
michael@0 124 }
michael@0 125 },
michael@0 126
michael@0 127 handleMoveToPoint: function cc_handleMoveToPoint(aMessage) {
michael@0 128 let [x, y] = [aMessage.json.x, aMessage.json.y];
michael@0 129 let rule = TraversalRules[aMessage.json.rule];
michael@0 130 let vc = this.vc;
michael@0 131 let win = this.window;
michael@0 132
michael@0 133 let dpr = win.devicePixelRatio;
michael@0 134 this.vc.moveToPoint(rule, x * dpr, y * dpr, true);
michael@0 135
michael@0 136 let delta = Utils.isContentProcess ?
michael@0 137 { x: x - win.mozInnerScreenX, y: y - win.mozInnerScreenY } : {};
michael@0 138 this.sendToChild(vc, aMessage, delta);
michael@0 139 },
michael@0 140
michael@0 141 handleClearCursor: function cc_handleClearCursor(aMessage) {
michael@0 142 let forwarded = this.sendToChild(this.vc, aMessage);
michael@0 143 this.vc.position = null;
michael@0 144 if (!forwarded) {
michael@0 145 this._contentScope.get().sendAsyncMessage('AccessFu:CursorCleared');
michael@0 146 }
michael@0 147 },
michael@0 148
michael@0 149 handleAutoMove: function cc_handleAutoMove(aMessage) {
michael@0 150 this.autoMove(null, aMessage.json);
michael@0 151 },
michael@0 152
michael@0 153 handleActivate: function cc_handleActivate(aMessage) {
michael@0 154 let activateAccessible = (aAccessible) => {
michael@0 155 Logger.debug(() => {
michael@0 156 return ['activateAccessible', Logger.accessibleToString(aAccessible)];
michael@0 157 });
michael@0 158 try {
michael@0 159 if (aMessage.json.activateIfKey &&
michael@0 160 aAccessible.role != Roles.KEY) {
michael@0 161 // Only activate keys, don't do anything on other objects.
michael@0 162 return;
michael@0 163 }
michael@0 164 } catch (e) {
michael@0 165 // accessible is invalid. Silently fail.
michael@0 166 return;
michael@0 167 }
michael@0 168
michael@0 169 if (aAccessible.actionCount > 0) {
michael@0 170 aAccessible.doAction(0);
michael@0 171 } else {
michael@0 172 let control = Utils.getEmbeddedControl(aAccessible);
michael@0 173 if (control && control.actionCount > 0) {
michael@0 174 control.doAction(0);
michael@0 175 }
michael@0 176
michael@0 177 // XXX Some mobile widget sets do not expose actions properly
michael@0 178 // (via ARIA roles, etc.), so we need to generate a click.
michael@0 179 // Could possibly be made simpler in the future. Maybe core
michael@0 180 // engine could expose nsCoreUtiles::DispatchMouseEvent()?
michael@0 181 let docAcc = Utils.AccRetrieval.getAccessibleFor(this.document);
michael@0 182 let docX = {}, docY = {}, docW = {}, docH = {};
michael@0 183 docAcc.getBounds(docX, docY, docW, docH);
michael@0 184
michael@0 185 let objX = {}, objY = {}, objW = {}, objH = {};
michael@0 186 aAccessible.getBounds(objX, objY, objW, objH);
michael@0 187
michael@0 188 let x = Math.round((objX.value - docX.value) + objW.value / 2);
michael@0 189 let y = Math.round((objY.value - docY.value) + objH.value / 2);
michael@0 190
michael@0 191 let node = aAccessible.DOMNode || aAccessible.parent.DOMNode;
michael@0 192
michael@0 193 for (let eventType of ['mousedown', 'mouseup']) {
michael@0 194 let evt = this.document.createEvent('MouseEvents');
michael@0 195 evt.initMouseEvent(eventType, true, true, this.window,
michael@0 196 x, y, 0, 0, 0, false, false, false, false, 0, null);
michael@0 197 node.dispatchEvent(evt);
michael@0 198 }
michael@0 199 }
michael@0 200
michael@0 201 if (aAccessible.role !== Roles.KEY) {
michael@0 202 // Keys will typically have a sound of their own.
michael@0 203 this._contentScope.get().sendAsyncMessage('AccessFu:Present',
michael@0 204 Presentation.actionInvoked(aAccessible, 'click'));
michael@0 205 }
michael@0 206 };
michael@0 207
michael@0 208 let focusedAcc = Utils.AccRetrieval.getAccessibleFor(
michael@0 209 this.document.activeElement);
michael@0 210 if (focusedAcc && focusedAcc.role === Roles.ENTRY) {
michael@0 211 let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText);
michael@0 212 let oldOffset = accText.caretOffset;
michael@0 213 let newOffset = aMessage.json.offset;
michael@0 214 let text = accText.getText(0, accText.characterCount);
michael@0 215
michael@0 216 if (newOffset >= 0 && newOffset <= accText.characterCount) {
michael@0 217 accText.caretOffset = newOffset;
michael@0 218 }
michael@0 219
michael@0 220 this.presentCaretChange(text, oldOffset, accText.caretOffset);
michael@0 221 return;
michael@0 222 }
michael@0 223
michael@0 224 let vc = this.vc;
michael@0 225 if (!this.sendToChild(vc, aMessage)) {
michael@0 226 activateAccessible(vc.position);
michael@0 227 }
michael@0 228 },
michael@0 229
michael@0 230 handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) {
michael@0 231 // XXX: Add sendToChild. Right now this is only used in Android, so no need.
michael@0 232 let direction = aMessage.json.direction;
michael@0 233 let granularity;
michael@0 234
michael@0 235 switch(aMessage.json.granularity) {
michael@0 236 case MOVEMENT_GRANULARITY_CHARACTER:
michael@0 237 granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY;
michael@0 238 break;
michael@0 239 case MOVEMENT_GRANULARITY_WORD:
michael@0 240 granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY;
michael@0 241 break;
michael@0 242 default:
michael@0 243 return;
michael@0 244 }
michael@0 245
michael@0 246 if (direction === 'Previous') {
michael@0 247 this.vc.movePreviousByText(granularity);
michael@0 248 } else if (direction === 'Next') {
michael@0 249 this.vc.moveNextByText(granularity);
michael@0 250 }
michael@0 251 },
michael@0 252
michael@0 253 presentCaretChange: function cc_presentCaretChange(
michael@0 254 aText, aOldOffset, aNewOffset) {
michael@0 255 if (aOldOffset !== aNewOffset) {
michael@0 256 let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset,
michael@0 257 aOldOffset, aOldOffset, true);
michael@0 258 this._contentScope.get().sendAsyncMessage('AccessFu:Present', msg);
michael@0 259 }
michael@0 260 },
michael@0 261
michael@0 262 handleMoveCaret: function cc_handleMoveCaret(aMessage) {
michael@0 263 let direction = aMessage.json.direction;
michael@0 264 let granularity = aMessage.json.granularity;
michael@0 265 let accessible = this.vc.position;
michael@0 266 let accText = accessible.QueryInterface(Ci.nsIAccessibleText);
michael@0 267 let oldOffset = accText.caretOffset;
michael@0 268 let text = accText.getText(0, accText.characterCount);
michael@0 269
michael@0 270 let start = {}, end = {};
michael@0 271 if (direction === 'Previous' && !aMessage.json.atStart) {
michael@0 272 switch (granularity) {
michael@0 273 case MOVEMENT_GRANULARITY_CHARACTER:
michael@0 274 accText.caretOffset--;
michael@0 275 break;
michael@0 276 case MOVEMENT_GRANULARITY_WORD:
michael@0 277 accText.getTextBeforeOffset(accText.caretOffset,
michael@0 278 Ci.nsIAccessibleText.BOUNDARY_WORD_START, start, end);
michael@0 279 accText.caretOffset = end.value === accText.caretOffset ?
michael@0 280 start.value : end.value;
michael@0 281 break;
michael@0 282 case MOVEMENT_GRANULARITY_PARAGRAPH:
michael@0 283 let startOfParagraph = text.lastIndexOf('\n', accText.caretOffset - 1);
michael@0 284 accText.caretOffset = startOfParagraph !== -1 ? startOfParagraph : 0;
michael@0 285 break;
michael@0 286 }
michael@0 287 } else if (direction === 'Next' && !aMessage.json.atEnd) {
michael@0 288 switch (granularity) {
michael@0 289 case MOVEMENT_GRANULARITY_CHARACTER:
michael@0 290 accText.caretOffset++;
michael@0 291 break;
michael@0 292 case MOVEMENT_GRANULARITY_WORD:
michael@0 293 accText.getTextAtOffset(accText.caretOffset,
michael@0 294 Ci.nsIAccessibleText.BOUNDARY_WORD_END, start, end);
michael@0 295 accText.caretOffset = end.value;
michael@0 296 break;
michael@0 297 case MOVEMENT_GRANULARITY_PARAGRAPH:
michael@0 298 accText.caretOffset = text.indexOf('\n', accText.caretOffset + 1);
michael@0 299 break;
michael@0 300 }
michael@0 301 }
michael@0 302
michael@0 303 this.presentCaretChange(text, oldOffset, accText.caretOffset);
michael@0 304 },
michael@0 305
michael@0 306 getChildCursor: function cc_getChildCursor(aAccessible) {
michael@0 307 let acc = aAccessible || this.vc.position;
michael@0 308 if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) {
michael@0 309 let domNode = acc.DOMNode;
michael@0 310 let mm = this._childMessageSenders.get(domNode, null);
michael@0 311 if (!mm) {
michael@0 312 mm = Utils.getMessageManager(domNode);
michael@0 313 mm.addWeakMessageListener('AccessFu:MoveCursor', this);
michael@0 314 this._childMessageSenders.set(domNode, mm);
michael@0 315 }
michael@0 316
michael@0 317 return mm;
michael@0 318 }
michael@0 319
michael@0 320 return null;
michael@0 321 },
michael@0 322
michael@0 323 sendToChild: function cc_sendToChild(aVirtualCursor, aMessage, aReplacer) {
michael@0 324 let mm = this.getChildCursor(aVirtualCursor.position);
michael@0 325 if (!mm) {
michael@0 326 return false;
michael@0 327 }
michael@0 328
michael@0 329 // XXX: This is a silly way to make a deep copy
michael@0 330 let newJSON = JSON.parse(JSON.stringify(aMessage.json));
michael@0 331 newJSON.origin = 'parent';
michael@0 332 for (let attr in aReplacer) {
michael@0 333 newJSON[attr] = aReplacer[attr];
michael@0 334 }
michael@0 335
michael@0 336 mm.sendAsyncMessage(aMessage.name, newJSON);
michael@0 337 return true;
michael@0 338 },
michael@0 339
michael@0 340 sendToParent: function cc_sendToParent(aMessage) {
michael@0 341 // XXX: This is a silly way to make a deep copy
michael@0 342 let newJSON = JSON.parse(JSON.stringify(aMessage.json));
michael@0 343 newJSON.origin = 'child';
michael@0 344 aMessage.target.sendAsyncMessage(aMessage.name, newJSON);
michael@0 345 },
michael@0 346
michael@0 347 /**
michael@0 348 * Move cursor and/or present its location.
michael@0 349 * aOptions could have any of these fields:
michael@0 350 * - delay: in ms, before actual move is performed. Another autoMove call
michael@0 351 * would cancel it. Useful if we want to wait for a possible trailing
michael@0 352 * focus move. Default 0.
michael@0 353 * - noOpIfOnScreen: if accessible is alive and visible, don't do anything.
michael@0 354 * - forcePresent: present cursor location, whether we move or don't.
michael@0 355 * - moveToFocused: if there is a focused accessible move to that. This takes
michael@0 356 * precedence over given anchor.
michael@0 357 * - moveMethod: pivot move method to use, default is 'moveNext',
michael@0 358 */
michael@0 359 autoMove: function cc_autoMove(aAnchor, aOptions = {}) {
michael@0 360 let win = this.window;
michael@0 361 win.clearTimeout(this._autoMove);
michael@0 362
michael@0 363 let moveFunc = () => {
michael@0 364 let vc = this.vc;
michael@0 365 let acc = aAnchor;
michael@0 366 let rule = aOptions.onScreenOnly ?
michael@0 367 TraversalRules.SimpleOnScreen : TraversalRules.Simple;
michael@0 368 let forcePresentFunc = () => {
michael@0 369 if (aOptions.forcePresent) {
michael@0 370 this._contentScope.get().sendAsyncMessage(
michael@0 371 'AccessFu:Present', Presentation.pivotChanged(
michael@0 372 vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE,
michael@0 373 vc.startOffset, vc.endOffset));
michael@0 374 }
michael@0 375 };
michael@0 376
michael@0 377 if (aOptions.noOpIfOnScreen &&
michael@0 378 Utils.isAliveAndVisible(vc.position, true)) {
michael@0 379 forcePresentFunc();
michael@0 380 return;
michael@0 381 }
michael@0 382
michael@0 383 if (aOptions.moveToFocused) {
michael@0 384 acc = Utils.AccRetrieval.getAccessibleFor(
michael@0 385 this.document.activeElement) || acc;
michael@0 386 }
michael@0 387
michael@0 388 let moved = false;
michael@0 389 let moveMethod = aOptions.moveMethod || 'moveNext'; // default is moveNext
michael@0 390 let moveFirstOrLast = moveMethod in ['moveFirst', 'moveLast'];
michael@0 391 if (!moveFirstOrLast || acc) {
michael@0 392 // We either need next/previous or there is an anchor we need to use.
michael@0 393 moved = vc[moveFirstOrLast ? 'moveNext' : moveMethod](rule, acc, true);
michael@0 394 }
michael@0 395 if (moveFirstOrLast && !moved) {
michael@0 396 // We move to first/last after no anchor move happened or succeeded.
michael@0 397 moved = vc[moveMethod](rule);
michael@0 398 }
michael@0 399
michael@0 400 let sentToChild = this.sendToChild(vc, {
michael@0 401 name: 'AccessFu:AutoMove',
michael@0 402 json: aOptions
michael@0 403 });
michael@0 404
michael@0 405 if (!moved && !sentToChild) {
michael@0 406 forcePresentFunc();
michael@0 407 }
michael@0 408 };
michael@0 409
michael@0 410 if (aOptions.delay) {
michael@0 411 this._autoMove = win.setTimeout(moveFunc, aOptions.delay);
michael@0 412 } else {
michael@0 413 moveFunc();
michael@0 414 }
michael@0 415 },
michael@0 416
michael@0 417 QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference,
michael@0 418 Ci.nsIMessageListener
michael@0 419 ])
michael@0 420 };

mercurial