accessible/src/jsat/ContentControl.jsm

changeset 0
6474c204b198
     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 +};

mercurial