1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/accessible/src/jsat/ContentControl.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,420 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +let Ci = Components.interfaces; 1.9 +let Cu = Components.utils; 1.10 + 1.11 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.12 +XPCOMUtils.defineLazyModuleGetter(this, 'Services', 1.13 + 'resource://gre/modules/Services.jsm'); 1.14 +XPCOMUtils.defineLazyModuleGetter(this, 'Utils', 1.15 + 'resource://gre/modules/accessibility/Utils.jsm'); 1.16 +XPCOMUtils.defineLazyModuleGetter(this, 'Logger', 1.17 + 'resource://gre/modules/accessibility/Utils.jsm'); 1.18 +XPCOMUtils.defineLazyModuleGetter(this, 'Roles', 1.19 + 'resource://gre/modules/accessibility/Constants.jsm'); 1.20 +XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules', 1.21 + 'resource://gre/modules/accessibility/TraversalRules.jsm'); 1.22 +XPCOMUtils.defineLazyModuleGetter(this, 'Presentation', 1.23 + 'resource://gre/modules/accessibility/Presentation.jsm'); 1.24 + 1.25 +this.EXPORTED_SYMBOLS = ['ContentControl']; 1.26 + 1.27 +const MOVEMENT_GRANULARITY_CHARACTER = 1; 1.28 +const MOVEMENT_GRANULARITY_WORD = 2; 1.29 +const MOVEMENT_GRANULARITY_PARAGRAPH = 8; 1.30 + 1.31 +this.ContentControl = function ContentControl(aContentScope) { 1.32 + this._contentScope = Cu.getWeakReference(aContentScope); 1.33 + this._vcCache = new WeakMap(); 1.34 + this._childMessageSenders = new WeakMap(); 1.35 +}; 1.36 + 1.37 +this.ContentControl.prototype = { 1.38 + messagesOfInterest: ['AccessFu:MoveCursor', 1.39 + 'AccessFu:ClearCursor', 1.40 + 'AccessFu:MoveToPoint', 1.41 + 'AccessFu:AutoMove', 1.42 + 'AccessFu:Activate', 1.43 + 'AccessFu:MoveCaret', 1.44 + 'AccessFu:MoveByGranularity'], 1.45 + 1.46 + start: function cc_start() { 1.47 + let cs = this._contentScope.get(); 1.48 + for (let message of this.messagesOfInterest) { 1.49 + cs.addMessageListener(message, this); 1.50 + } 1.51 + }, 1.52 + 1.53 + stop: function cc_stop() { 1.54 + let cs = this._contentScope.get(); 1.55 + for (let message of this.messagesOfInterest) { 1.56 + cs.removeMessageListener(message, this); 1.57 + } 1.58 + }, 1.59 + 1.60 + get document() { 1.61 + return this._contentScope.get().content.document; 1.62 + }, 1.63 + 1.64 + get window() { 1.65 + return this._contentScope.get().content; 1.66 + }, 1.67 + 1.68 + get vc() { 1.69 + return Utils.getVirtualCursor(this.document); 1.70 + }, 1.71 + 1.72 + receiveMessage: function cc_receiveMessage(aMessage) { 1.73 + Logger.debug(() => { 1.74 + return ['ContentControl.receiveMessage', 1.75 + aMessage.name, 1.76 + JSON.stringify(aMessage.json)]; 1.77 + }); 1.78 + 1.79 + try { 1.80 + let func = this['handle' + aMessage.name.slice(9)]; // 'AccessFu:'.length 1.81 + if (func) { 1.82 + func.bind(this)(aMessage); 1.83 + } else { 1.84 + Logger.warning('ContentControl: Unhandled message:', aMessage.name); 1.85 + } 1.86 + } catch (x) { 1.87 + Logger.logException( 1.88 + x, 'Error handling message: ' + JSON.stringify(aMessage.json)); 1.89 + } 1.90 + }, 1.91 + 1.92 + handleMoveCursor: function cc_handleMoveCursor(aMessage) { 1.93 + let origin = aMessage.json.origin; 1.94 + let action = aMessage.json.action; 1.95 + let vc = this.vc; 1.96 + 1.97 + if (origin != 'child' && this.sendToChild(vc, aMessage)) { 1.98 + // Forwarded succesfully to child cursor. 1.99 + return; 1.100 + } 1.101 + 1.102 + let moved = vc[action](TraversalRules[aMessage.json.rule]); 1.103 + 1.104 + if (moved) { 1.105 + if (origin === 'child') { 1.106 + // We just stepped out of a child, clear child cursor. 1.107 + Utils.getMessageManager(aMessage.target).sendAsyncMessage( 1.108 + 'AccessFu:ClearCursor', {}); 1.109 + } else { 1.110 + // We potentially landed on a new child cursor. If so, we want to 1.111 + // either be on the first or last item in the child doc. 1.112 + let childAction = action; 1.113 + if (action === 'moveNext') { 1.114 + childAction = 'moveFirst'; 1.115 + } else if (action === 'movePrevious') { 1.116 + childAction = 'moveLast'; 1.117 + } 1.118 + 1.119 + // Attempt to forward move to a potential child cursor in our 1.120 + // new position. 1.121 + this.sendToChild(vc, aMessage, { action: childAction}); 1.122 + } 1.123 + } else if (!this._childMessageSenders.has(aMessage.target)) { 1.124 + // We failed to move, and the message is not from a child, so forward 1.125 + // to parent. 1.126 + this.sendToParent(aMessage); 1.127 + } 1.128 + }, 1.129 + 1.130 + handleMoveToPoint: function cc_handleMoveToPoint(aMessage) { 1.131 + let [x, y] = [aMessage.json.x, aMessage.json.y]; 1.132 + let rule = TraversalRules[aMessage.json.rule]; 1.133 + let vc = this.vc; 1.134 + let win = this.window; 1.135 + 1.136 + let dpr = win.devicePixelRatio; 1.137 + this.vc.moveToPoint(rule, x * dpr, y * dpr, true); 1.138 + 1.139 + let delta = Utils.isContentProcess ? 1.140 + { x: x - win.mozInnerScreenX, y: y - win.mozInnerScreenY } : {}; 1.141 + this.sendToChild(vc, aMessage, delta); 1.142 + }, 1.143 + 1.144 + handleClearCursor: function cc_handleClearCursor(aMessage) { 1.145 + let forwarded = this.sendToChild(this.vc, aMessage); 1.146 + this.vc.position = null; 1.147 + if (!forwarded) { 1.148 + this._contentScope.get().sendAsyncMessage('AccessFu:CursorCleared'); 1.149 + } 1.150 + }, 1.151 + 1.152 + handleAutoMove: function cc_handleAutoMove(aMessage) { 1.153 + this.autoMove(null, aMessage.json); 1.154 + }, 1.155 + 1.156 + handleActivate: function cc_handleActivate(aMessage) { 1.157 + let activateAccessible = (aAccessible) => { 1.158 + Logger.debug(() => { 1.159 + return ['activateAccessible', Logger.accessibleToString(aAccessible)]; 1.160 + }); 1.161 + try { 1.162 + if (aMessage.json.activateIfKey && 1.163 + aAccessible.role != Roles.KEY) { 1.164 + // Only activate keys, don't do anything on other objects. 1.165 + return; 1.166 + } 1.167 + } catch (e) { 1.168 + // accessible is invalid. Silently fail. 1.169 + return; 1.170 + } 1.171 + 1.172 + if (aAccessible.actionCount > 0) { 1.173 + aAccessible.doAction(0); 1.174 + } else { 1.175 + let control = Utils.getEmbeddedControl(aAccessible); 1.176 + if (control && control.actionCount > 0) { 1.177 + control.doAction(0); 1.178 + } 1.179 + 1.180 + // XXX Some mobile widget sets do not expose actions properly 1.181 + // (via ARIA roles, etc.), so we need to generate a click. 1.182 + // Could possibly be made simpler in the future. Maybe core 1.183 + // engine could expose nsCoreUtiles::DispatchMouseEvent()? 1.184 + let docAcc = Utils.AccRetrieval.getAccessibleFor(this.document); 1.185 + let docX = {}, docY = {}, docW = {}, docH = {}; 1.186 + docAcc.getBounds(docX, docY, docW, docH); 1.187 + 1.188 + let objX = {}, objY = {}, objW = {}, objH = {}; 1.189 + aAccessible.getBounds(objX, objY, objW, objH); 1.190 + 1.191 + let x = Math.round((objX.value - docX.value) + objW.value / 2); 1.192 + let y = Math.round((objY.value - docY.value) + objH.value / 2); 1.193 + 1.194 + let node = aAccessible.DOMNode || aAccessible.parent.DOMNode; 1.195 + 1.196 + for (let eventType of ['mousedown', 'mouseup']) { 1.197 + let evt = this.document.createEvent('MouseEvents'); 1.198 + evt.initMouseEvent(eventType, true, true, this.window, 1.199 + x, y, 0, 0, 0, false, false, false, false, 0, null); 1.200 + node.dispatchEvent(evt); 1.201 + } 1.202 + } 1.203 + 1.204 + if (aAccessible.role !== Roles.KEY) { 1.205 + // Keys will typically have a sound of their own. 1.206 + this._contentScope.get().sendAsyncMessage('AccessFu:Present', 1.207 + Presentation.actionInvoked(aAccessible, 'click')); 1.208 + } 1.209 + }; 1.210 + 1.211 + let focusedAcc = Utils.AccRetrieval.getAccessibleFor( 1.212 + this.document.activeElement); 1.213 + if (focusedAcc && focusedAcc.role === Roles.ENTRY) { 1.214 + let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText); 1.215 + let oldOffset = accText.caretOffset; 1.216 + let newOffset = aMessage.json.offset; 1.217 + let text = accText.getText(0, accText.characterCount); 1.218 + 1.219 + if (newOffset >= 0 && newOffset <= accText.characterCount) { 1.220 + accText.caretOffset = newOffset; 1.221 + } 1.222 + 1.223 + this.presentCaretChange(text, oldOffset, accText.caretOffset); 1.224 + return; 1.225 + } 1.226 + 1.227 + let vc = this.vc; 1.228 + if (!this.sendToChild(vc, aMessage)) { 1.229 + activateAccessible(vc.position); 1.230 + } 1.231 + }, 1.232 + 1.233 + handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) { 1.234 + // XXX: Add sendToChild. Right now this is only used in Android, so no need. 1.235 + let direction = aMessage.json.direction; 1.236 + let granularity; 1.237 + 1.238 + switch(aMessage.json.granularity) { 1.239 + case MOVEMENT_GRANULARITY_CHARACTER: 1.240 + granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY; 1.241 + break; 1.242 + case MOVEMENT_GRANULARITY_WORD: 1.243 + granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY; 1.244 + break; 1.245 + default: 1.246 + return; 1.247 + } 1.248 + 1.249 + if (direction === 'Previous') { 1.250 + this.vc.movePreviousByText(granularity); 1.251 + } else if (direction === 'Next') { 1.252 + this.vc.moveNextByText(granularity); 1.253 + } 1.254 + }, 1.255 + 1.256 + presentCaretChange: function cc_presentCaretChange( 1.257 + aText, aOldOffset, aNewOffset) { 1.258 + if (aOldOffset !== aNewOffset) { 1.259 + let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset, 1.260 + aOldOffset, aOldOffset, true); 1.261 + this._contentScope.get().sendAsyncMessage('AccessFu:Present', msg); 1.262 + } 1.263 + }, 1.264 + 1.265 + handleMoveCaret: function cc_handleMoveCaret(aMessage) { 1.266 + let direction = aMessage.json.direction; 1.267 + let granularity = aMessage.json.granularity; 1.268 + let accessible = this.vc.position; 1.269 + let accText = accessible.QueryInterface(Ci.nsIAccessibleText); 1.270 + let oldOffset = accText.caretOffset; 1.271 + let text = accText.getText(0, accText.characterCount); 1.272 + 1.273 + let start = {}, end = {}; 1.274 + if (direction === 'Previous' && !aMessage.json.atStart) { 1.275 + switch (granularity) { 1.276 + case MOVEMENT_GRANULARITY_CHARACTER: 1.277 + accText.caretOffset--; 1.278 + break; 1.279 + case MOVEMENT_GRANULARITY_WORD: 1.280 + accText.getTextBeforeOffset(accText.caretOffset, 1.281 + Ci.nsIAccessibleText.BOUNDARY_WORD_START, start, end); 1.282 + accText.caretOffset = end.value === accText.caretOffset ? 1.283 + start.value : end.value; 1.284 + break; 1.285 + case MOVEMENT_GRANULARITY_PARAGRAPH: 1.286 + let startOfParagraph = text.lastIndexOf('\n', accText.caretOffset - 1); 1.287 + accText.caretOffset = startOfParagraph !== -1 ? startOfParagraph : 0; 1.288 + break; 1.289 + } 1.290 + } else if (direction === 'Next' && !aMessage.json.atEnd) { 1.291 + switch (granularity) { 1.292 + case MOVEMENT_GRANULARITY_CHARACTER: 1.293 + accText.caretOffset++; 1.294 + break; 1.295 + case MOVEMENT_GRANULARITY_WORD: 1.296 + accText.getTextAtOffset(accText.caretOffset, 1.297 + Ci.nsIAccessibleText.BOUNDARY_WORD_END, start, end); 1.298 + accText.caretOffset = end.value; 1.299 + break; 1.300 + case MOVEMENT_GRANULARITY_PARAGRAPH: 1.301 + accText.caretOffset = text.indexOf('\n', accText.caretOffset + 1); 1.302 + break; 1.303 + } 1.304 + } 1.305 + 1.306 + this.presentCaretChange(text, oldOffset, accText.caretOffset); 1.307 + }, 1.308 + 1.309 + getChildCursor: function cc_getChildCursor(aAccessible) { 1.310 + let acc = aAccessible || this.vc.position; 1.311 + if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) { 1.312 + let domNode = acc.DOMNode; 1.313 + let mm = this._childMessageSenders.get(domNode, null); 1.314 + if (!mm) { 1.315 + mm = Utils.getMessageManager(domNode); 1.316 + mm.addWeakMessageListener('AccessFu:MoveCursor', this); 1.317 + this._childMessageSenders.set(domNode, mm); 1.318 + } 1.319 + 1.320 + return mm; 1.321 + } 1.322 + 1.323 + return null; 1.324 + }, 1.325 + 1.326 + sendToChild: function cc_sendToChild(aVirtualCursor, aMessage, aReplacer) { 1.327 + let mm = this.getChildCursor(aVirtualCursor.position); 1.328 + if (!mm) { 1.329 + return false; 1.330 + } 1.331 + 1.332 + // XXX: This is a silly way to make a deep copy 1.333 + let newJSON = JSON.parse(JSON.stringify(aMessage.json)); 1.334 + newJSON.origin = 'parent'; 1.335 + for (let attr in aReplacer) { 1.336 + newJSON[attr] = aReplacer[attr]; 1.337 + } 1.338 + 1.339 + mm.sendAsyncMessage(aMessage.name, newJSON); 1.340 + return true; 1.341 + }, 1.342 + 1.343 + sendToParent: function cc_sendToParent(aMessage) { 1.344 + // XXX: This is a silly way to make a deep copy 1.345 + let newJSON = JSON.parse(JSON.stringify(aMessage.json)); 1.346 + newJSON.origin = 'child'; 1.347 + aMessage.target.sendAsyncMessage(aMessage.name, newJSON); 1.348 + }, 1.349 + 1.350 + /** 1.351 + * Move cursor and/or present its location. 1.352 + * aOptions could have any of these fields: 1.353 + * - delay: in ms, before actual move is performed. Another autoMove call 1.354 + * would cancel it. Useful if we want to wait for a possible trailing 1.355 + * focus move. Default 0. 1.356 + * - noOpIfOnScreen: if accessible is alive and visible, don't do anything. 1.357 + * - forcePresent: present cursor location, whether we move or don't. 1.358 + * - moveToFocused: if there is a focused accessible move to that. This takes 1.359 + * precedence over given anchor. 1.360 + * - moveMethod: pivot move method to use, default is 'moveNext', 1.361 + */ 1.362 + autoMove: function cc_autoMove(aAnchor, aOptions = {}) { 1.363 + let win = this.window; 1.364 + win.clearTimeout(this._autoMove); 1.365 + 1.366 + let moveFunc = () => { 1.367 + let vc = this.vc; 1.368 + let acc = aAnchor; 1.369 + let rule = aOptions.onScreenOnly ? 1.370 + TraversalRules.SimpleOnScreen : TraversalRules.Simple; 1.371 + let forcePresentFunc = () => { 1.372 + if (aOptions.forcePresent) { 1.373 + this._contentScope.get().sendAsyncMessage( 1.374 + 'AccessFu:Present', Presentation.pivotChanged( 1.375 + vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE, 1.376 + vc.startOffset, vc.endOffset)); 1.377 + } 1.378 + }; 1.379 + 1.380 + if (aOptions.noOpIfOnScreen && 1.381 + Utils.isAliveAndVisible(vc.position, true)) { 1.382 + forcePresentFunc(); 1.383 + return; 1.384 + } 1.385 + 1.386 + if (aOptions.moveToFocused) { 1.387 + acc = Utils.AccRetrieval.getAccessibleFor( 1.388 + this.document.activeElement) || acc; 1.389 + } 1.390 + 1.391 + let moved = false; 1.392 + let moveMethod = aOptions.moveMethod || 'moveNext'; // default is moveNext 1.393 + let moveFirstOrLast = moveMethod in ['moveFirst', 'moveLast']; 1.394 + if (!moveFirstOrLast || acc) { 1.395 + // We either need next/previous or there is an anchor we need to use. 1.396 + moved = vc[moveFirstOrLast ? 'moveNext' : moveMethod](rule, acc, true); 1.397 + } 1.398 + if (moveFirstOrLast && !moved) { 1.399 + // We move to first/last after no anchor move happened or succeeded. 1.400 + moved = vc[moveMethod](rule); 1.401 + } 1.402 + 1.403 + let sentToChild = this.sendToChild(vc, { 1.404 + name: 'AccessFu:AutoMove', 1.405 + json: aOptions 1.406 + }); 1.407 + 1.408 + if (!moved && !sentToChild) { 1.409 + forcePresentFunc(); 1.410 + } 1.411 + }; 1.412 + 1.413 + if (aOptions.delay) { 1.414 + this._autoMove = win.setTimeout(moveFunc, aOptions.delay); 1.415 + } else { 1.416 + moveFunc(); 1.417 + } 1.418 + }, 1.419 + 1.420 + QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, 1.421 + Ci.nsIMessageListener 1.422 + ]) 1.423 +};