michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let Ci = Components.interfaces; michael@0: let Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Services', michael@0: 'resource://gre/modules/Services.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Utils', michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Logger', michael@0: 'resource://gre/modules/accessibility/Utils.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Roles', michael@0: 'resource://gre/modules/accessibility/Constants.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'TraversalRules', michael@0: 'resource://gre/modules/accessibility/TraversalRules.jsm'); michael@0: XPCOMUtils.defineLazyModuleGetter(this, 'Presentation', michael@0: 'resource://gre/modules/accessibility/Presentation.jsm'); michael@0: michael@0: this.EXPORTED_SYMBOLS = ['ContentControl']; michael@0: michael@0: const MOVEMENT_GRANULARITY_CHARACTER = 1; michael@0: const MOVEMENT_GRANULARITY_WORD = 2; michael@0: const MOVEMENT_GRANULARITY_PARAGRAPH = 8; michael@0: michael@0: this.ContentControl = function ContentControl(aContentScope) { michael@0: this._contentScope = Cu.getWeakReference(aContentScope); michael@0: this._vcCache = new WeakMap(); michael@0: this._childMessageSenders = new WeakMap(); michael@0: }; michael@0: michael@0: this.ContentControl.prototype = { michael@0: messagesOfInterest: ['AccessFu:MoveCursor', michael@0: 'AccessFu:ClearCursor', michael@0: 'AccessFu:MoveToPoint', michael@0: 'AccessFu:AutoMove', michael@0: 'AccessFu:Activate', michael@0: 'AccessFu:MoveCaret', michael@0: 'AccessFu:MoveByGranularity'], michael@0: michael@0: start: function cc_start() { michael@0: let cs = this._contentScope.get(); michael@0: for (let message of this.messagesOfInterest) { michael@0: cs.addMessageListener(message, this); michael@0: } michael@0: }, michael@0: michael@0: stop: function cc_stop() { michael@0: let cs = this._contentScope.get(); michael@0: for (let message of this.messagesOfInterest) { michael@0: cs.removeMessageListener(message, this); michael@0: } michael@0: }, michael@0: michael@0: get document() { michael@0: return this._contentScope.get().content.document; michael@0: }, michael@0: michael@0: get window() { michael@0: return this._contentScope.get().content; michael@0: }, michael@0: michael@0: get vc() { michael@0: return Utils.getVirtualCursor(this.document); michael@0: }, michael@0: michael@0: receiveMessage: function cc_receiveMessage(aMessage) { michael@0: Logger.debug(() => { michael@0: return ['ContentControl.receiveMessage', michael@0: aMessage.name, michael@0: JSON.stringify(aMessage.json)]; michael@0: }); michael@0: michael@0: try { michael@0: let func = this['handle' + aMessage.name.slice(9)]; // 'AccessFu:'.length michael@0: if (func) { michael@0: func.bind(this)(aMessage); michael@0: } else { michael@0: Logger.warning('ContentControl: Unhandled message:', aMessage.name); michael@0: } michael@0: } catch (x) { michael@0: Logger.logException( michael@0: x, 'Error handling message: ' + JSON.stringify(aMessage.json)); michael@0: } michael@0: }, michael@0: michael@0: handleMoveCursor: function cc_handleMoveCursor(aMessage) { michael@0: let origin = aMessage.json.origin; michael@0: let action = aMessage.json.action; michael@0: let vc = this.vc; michael@0: michael@0: if (origin != 'child' && this.sendToChild(vc, aMessage)) { michael@0: // Forwarded succesfully to child cursor. michael@0: return; michael@0: } michael@0: michael@0: let moved = vc[action](TraversalRules[aMessage.json.rule]); michael@0: michael@0: if (moved) { michael@0: if (origin === 'child') { michael@0: // We just stepped out of a child, clear child cursor. michael@0: Utils.getMessageManager(aMessage.target).sendAsyncMessage( michael@0: 'AccessFu:ClearCursor', {}); michael@0: } else { michael@0: // We potentially landed on a new child cursor. If so, we want to michael@0: // either be on the first or last item in the child doc. michael@0: let childAction = action; michael@0: if (action === 'moveNext') { michael@0: childAction = 'moveFirst'; michael@0: } else if (action === 'movePrevious') { michael@0: childAction = 'moveLast'; michael@0: } michael@0: michael@0: // Attempt to forward move to a potential child cursor in our michael@0: // new position. michael@0: this.sendToChild(vc, aMessage, { action: childAction}); michael@0: } michael@0: } else if (!this._childMessageSenders.has(aMessage.target)) { michael@0: // We failed to move, and the message is not from a child, so forward michael@0: // to parent. michael@0: this.sendToParent(aMessage); michael@0: } michael@0: }, michael@0: michael@0: handleMoveToPoint: function cc_handleMoveToPoint(aMessage) { michael@0: let [x, y] = [aMessage.json.x, aMessage.json.y]; michael@0: let rule = TraversalRules[aMessage.json.rule]; michael@0: let vc = this.vc; michael@0: let win = this.window; michael@0: michael@0: let dpr = win.devicePixelRatio; michael@0: this.vc.moveToPoint(rule, x * dpr, y * dpr, true); michael@0: michael@0: let delta = Utils.isContentProcess ? michael@0: { x: x - win.mozInnerScreenX, y: y - win.mozInnerScreenY } : {}; michael@0: this.sendToChild(vc, aMessage, delta); michael@0: }, michael@0: michael@0: handleClearCursor: function cc_handleClearCursor(aMessage) { michael@0: let forwarded = this.sendToChild(this.vc, aMessage); michael@0: this.vc.position = null; michael@0: if (!forwarded) { michael@0: this._contentScope.get().sendAsyncMessage('AccessFu:CursorCleared'); michael@0: } michael@0: }, michael@0: michael@0: handleAutoMove: function cc_handleAutoMove(aMessage) { michael@0: this.autoMove(null, aMessage.json); michael@0: }, michael@0: michael@0: handleActivate: function cc_handleActivate(aMessage) { michael@0: let activateAccessible = (aAccessible) => { michael@0: Logger.debug(() => { michael@0: return ['activateAccessible', Logger.accessibleToString(aAccessible)]; michael@0: }); michael@0: try { michael@0: if (aMessage.json.activateIfKey && michael@0: aAccessible.role != Roles.KEY) { michael@0: // Only activate keys, don't do anything on other objects. michael@0: return; michael@0: } michael@0: } catch (e) { michael@0: // accessible is invalid. Silently fail. michael@0: return; michael@0: } michael@0: michael@0: if (aAccessible.actionCount > 0) { michael@0: aAccessible.doAction(0); michael@0: } else { michael@0: let control = Utils.getEmbeddedControl(aAccessible); michael@0: if (control && control.actionCount > 0) { michael@0: control.doAction(0); michael@0: } michael@0: michael@0: // XXX Some mobile widget sets do not expose actions properly michael@0: // (via ARIA roles, etc.), so we need to generate a click. michael@0: // Could possibly be made simpler in the future. Maybe core michael@0: // engine could expose nsCoreUtiles::DispatchMouseEvent()? michael@0: let docAcc = Utils.AccRetrieval.getAccessibleFor(this.document); michael@0: let docX = {}, docY = {}, docW = {}, docH = {}; michael@0: docAcc.getBounds(docX, docY, docW, docH); michael@0: michael@0: let objX = {}, objY = {}, objW = {}, objH = {}; michael@0: aAccessible.getBounds(objX, objY, objW, objH); michael@0: michael@0: let x = Math.round((objX.value - docX.value) + objW.value / 2); michael@0: let y = Math.round((objY.value - docY.value) + objH.value / 2); michael@0: michael@0: let node = aAccessible.DOMNode || aAccessible.parent.DOMNode; michael@0: michael@0: for (let eventType of ['mousedown', 'mouseup']) { michael@0: let evt = this.document.createEvent('MouseEvents'); michael@0: evt.initMouseEvent(eventType, true, true, this.window, michael@0: x, y, 0, 0, 0, false, false, false, false, 0, null); michael@0: node.dispatchEvent(evt); michael@0: } michael@0: } michael@0: michael@0: if (aAccessible.role !== Roles.KEY) { michael@0: // Keys will typically have a sound of their own. michael@0: this._contentScope.get().sendAsyncMessage('AccessFu:Present', michael@0: Presentation.actionInvoked(aAccessible, 'click')); michael@0: } michael@0: }; michael@0: michael@0: let focusedAcc = Utils.AccRetrieval.getAccessibleFor( michael@0: this.document.activeElement); michael@0: if (focusedAcc && focusedAcc.role === Roles.ENTRY) { michael@0: let accText = focusedAcc.QueryInterface(Ci.nsIAccessibleText); michael@0: let oldOffset = accText.caretOffset; michael@0: let newOffset = aMessage.json.offset; michael@0: let text = accText.getText(0, accText.characterCount); michael@0: michael@0: if (newOffset >= 0 && newOffset <= accText.characterCount) { michael@0: accText.caretOffset = newOffset; michael@0: } michael@0: michael@0: this.presentCaretChange(text, oldOffset, accText.caretOffset); michael@0: return; michael@0: } michael@0: michael@0: let vc = this.vc; michael@0: if (!this.sendToChild(vc, aMessage)) { michael@0: activateAccessible(vc.position); michael@0: } michael@0: }, michael@0: michael@0: handleMoveByGranularity: function cc_handleMoveByGranularity(aMessage) { michael@0: // XXX: Add sendToChild. Right now this is only used in Android, so no need. michael@0: let direction = aMessage.json.direction; michael@0: let granularity; michael@0: michael@0: switch(aMessage.json.granularity) { michael@0: case MOVEMENT_GRANULARITY_CHARACTER: michael@0: granularity = Ci.nsIAccessiblePivot.CHAR_BOUNDARY; michael@0: break; michael@0: case MOVEMENT_GRANULARITY_WORD: michael@0: granularity = Ci.nsIAccessiblePivot.WORD_BOUNDARY; michael@0: break; michael@0: default: michael@0: return; michael@0: } michael@0: michael@0: if (direction === 'Previous') { michael@0: this.vc.movePreviousByText(granularity); michael@0: } else if (direction === 'Next') { michael@0: this.vc.moveNextByText(granularity); michael@0: } michael@0: }, michael@0: michael@0: presentCaretChange: function cc_presentCaretChange( michael@0: aText, aOldOffset, aNewOffset) { michael@0: if (aOldOffset !== aNewOffset) { michael@0: let msg = Presentation.textSelectionChanged(aText, aNewOffset, aNewOffset, michael@0: aOldOffset, aOldOffset, true); michael@0: this._contentScope.get().sendAsyncMessage('AccessFu:Present', msg); michael@0: } michael@0: }, michael@0: michael@0: handleMoveCaret: function cc_handleMoveCaret(aMessage) { michael@0: let direction = aMessage.json.direction; michael@0: let granularity = aMessage.json.granularity; michael@0: let accessible = this.vc.position; michael@0: let accText = accessible.QueryInterface(Ci.nsIAccessibleText); michael@0: let oldOffset = accText.caretOffset; michael@0: let text = accText.getText(0, accText.characterCount); michael@0: michael@0: let start = {}, end = {}; michael@0: if (direction === 'Previous' && !aMessage.json.atStart) { michael@0: switch (granularity) { michael@0: case MOVEMENT_GRANULARITY_CHARACTER: michael@0: accText.caretOffset--; michael@0: break; michael@0: case MOVEMENT_GRANULARITY_WORD: michael@0: accText.getTextBeforeOffset(accText.caretOffset, michael@0: Ci.nsIAccessibleText.BOUNDARY_WORD_START, start, end); michael@0: accText.caretOffset = end.value === accText.caretOffset ? michael@0: start.value : end.value; michael@0: break; michael@0: case MOVEMENT_GRANULARITY_PARAGRAPH: michael@0: let startOfParagraph = text.lastIndexOf('\n', accText.caretOffset - 1); michael@0: accText.caretOffset = startOfParagraph !== -1 ? startOfParagraph : 0; michael@0: break; michael@0: } michael@0: } else if (direction === 'Next' && !aMessage.json.atEnd) { michael@0: switch (granularity) { michael@0: case MOVEMENT_GRANULARITY_CHARACTER: michael@0: accText.caretOffset++; michael@0: break; michael@0: case MOVEMENT_GRANULARITY_WORD: michael@0: accText.getTextAtOffset(accText.caretOffset, michael@0: Ci.nsIAccessibleText.BOUNDARY_WORD_END, start, end); michael@0: accText.caretOffset = end.value; michael@0: break; michael@0: case MOVEMENT_GRANULARITY_PARAGRAPH: michael@0: accText.caretOffset = text.indexOf('\n', accText.caretOffset + 1); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: this.presentCaretChange(text, oldOffset, accText.caretOffset); michael@0: }, michael@0: michael@0: getChildCursor: function cc_getChildCursor(aAccessible) { michael@0: let acc = aAccessible || this.vc.position; michael@0: if (Utils.isAliveAndVisible(acc) && acc.role === Roles.INTERNAL_FRAME) { michael@0: let domNode = acc.DOMNode; michael@0: let mm = this._childMessageSenders.get(domNode, null); michael@0: if (!mm) { michael@0: mm = Utils.getMessageManager(domNode); michael@0: mm.addWeakMessageListener('AccessFu:MoveCursor', this); michael@0: this._childMessageSenders.set(domNode, mm); michael@0: } michael@0: michael@0: return mm; michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: sendToChild: function cc_sendToChild(aVirtualCursor, aMessage, aReplacer) { michael@0: let mm = this.getChildCursor(aVirtualCursor.position); michael@0: if (!mm) { michael@0: return false; michael@0: } michael@0: michael@0: // XXX: This is a silly way to make a deep copy michael@0: let newJSON = JSON.parse(JSON.stringify(aMessage.json)); michael@0: newJSON.origin = 'parent'; michael@0: for (let attr in aReplacer) { michael@0: newJSON[attr] = aReplacer[attr]; michael@0: } michael@0: michael@0: mm.sendAsyncMessage(aMessage.name, newJSON); michael@0: return true; michael@0: }, michael@0: michael@0: sendToParent: function cc_sendToParent(aMessage) { michael@0: // XXX: This is a silly way to make a deep copy michael@0: let newJSON = JSON.parse(JSON.stringify(aMessage.json)); michael@0: newJSON.origin = 'child'; michael@0: aMessage.target.sendAsyncMessage(aMessage.name, newJSON); michael@0: }, michael@0: michael@0: /** michael@0: * Move cursor and/or present its location. michael@0: * aOptions could have any of these fields: michael@0: * - delay: in ms, before actual move is performed. Another autoMove call michael@0: * would cancel it. Useful if we want to wait for a possible trailing michael@0: * focus move. Default 0. michael@0: * - noOpIfOnScreen: if accessible is alive and visible, don't do anything. michael@0: * - forcePresent: present cursor location, whether we move or don't. michael@0: * - moveToFocused: if there is a focused accessible move to that. This takes michael@0: * precedence over given anchor. michael@0: * - moveMethod: pivot move method to use, default is 'moveNext', michael@0: */ michael@0: autoMove: function cc_autoMove(aAnchor, aOptions = {}) { michael@0: let win = this.window; michael@0: win.clearTimeout(this._autoMove); michael@0: michael@0: let moveFunc = () => { michael@0: let vc = this.vc; michael@0: let acc = aAnchor; michael@0: let rule = aOptions.onScreenOnly ? michael@0: TraversalRules.SimpleOnScreen : TraversalRules.Simple; michael@0: let forcePresentFunc = () => { michael@0: if (aOptions.forcePresent) { michael@0: this._contentScope.get().sendAsyncMessage( michael@0: 'AccessFu:Present', Presentation.pivotChanged( michael@0: vc.position, null, Ci.nsIAccessiblePivot.REASON_NONE, michael@0: vc.startOffset, vc.endOffset)); michael@0: } michael@0: }; michael@0: michael@0: if (aOptions.noOpIfOnScreen && michael@0: Utils.isAliveAndVisible(vc.position, true)) { michael@0: forcePresentFunc(); michael@0: return; michael@0: } michael@0: michael@0: if (aOptions.moveToFocused) { michael@0: acc = Utils.AccRetrieval.getAccessibleFor( michael@0: this.document.activeElement) || acc; michael@0: } michael@0: michael@0: let moved = false; michael@0: let moveMethod = aOptions.moveMethod || 'moveNext'; // default is moveNext michael@0: let moveFirstOrLast = moveMethod in ['moveFirst', 'moveLast']; michael@0: if (!moveFirstOrLast || acc) { michael@0: // We either need next/previous or there is an anchor we need to use. michael@0: moved = vc[moveFirstOrLast ? 'moveNext' : moveMethod](rule, acc, true); michael@0: } michael@0: if (moveFirstOrLast && !moved) { michael@0: // We move to first/last after no anchor move happened or succeeded. michael@0: moved = vc[moveMethod](rule); michael@0: } michael@0: michael@0: let sentToChild = this.sendToChild(vc, { michael@0: name: 'AccessFu:AutoMove', michael@0: json: aOptions michael@0: }); michael@0: michael@0: if (!moved && !sentToChild) { michael@0: forcePresentFunc(); michael@0: } michael@0: }; michael@0: michael@0: if (aOptions.delay) { michael@0: this._autoMove = win.setTimeout(moveFunc, aOptions.delay); michael@0: } else { michael@0: moveFunc(); michael@0: } michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsISupportsWeakReference, michael@0: Ci.nsIMessageListener michael@0: ]) michael@0: };