accessible/src/jsat/ContentControl.jsm

Wed, 31 Dec 2014 07:16:47 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:16:47 +0100
branch
TOR_BUG_9701
changeset 3
141e0f1194b1
permissions
-rw-r--r--

Revert simplistic fix pending revisit of Mozilla integration attempt.

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

mercurial