accessible/jsat/AccessFu.jsm
author Mark Banner <standard8@mozilla.com>
Fri, 19 Oct 2018 12:55:39 +0000
changeset 490493 edc4ae8f78e2fb02647d7cd85cb19830a17dcd1d
parent 489092 d41905041ca7f615f90fce70e1de3c325e490c44
permissions -rw-r--r--
Bug 1486741 - Enable ESLint rule comma-dangle for all of mozilla-central (automatic fixes). r=mossop Differential Revision: https://phabricator.services.mozilla.com/D8389

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["AccessFu"];

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
ChromeUtils.defineModuleGetter(this, "Rect",
                               "resource://gre/modules/Geometry.jsm");

if (Utils.MozBuildApp === "mobile/android") {
  ChromeUtils.import("resource://gre/modules/Messaging.jsm");
}

const GECKOVIEW_MESSAGE = {
  ACTIVATE: "GeckoView:AccessibilityActivate",
  BY_GRANULARITY: "GeckoView:AccessibilityByGranularity",
  CLIPBOARD: "GeckoView:AccessibilityClipboard",
  CURSOR_TO_FOCUSED: "GeckoView:AccessibilityCursorToFocused",
  EXPLORE_BY_TOUCH: "GeckoView:AccessibilityExploreByTouch",
  LONG_PRESS: "GeckoView:AccessibilityLongPress",
  NEXT: "GeckoView:AccessibilityNext",
  PREVIOUS: "GeckoView:AccessibilityPrevious",
  SCROLL_BACKWARD: "GeckoView:AccessibilityScrollBackward",
  SCROLL_FORWARD: "GeckoView:AccessibilityScrollForward",
  SELECT: "GeckoView:AccessibilitySelect",
  SET_SELECTION: "GeckoView:AccessibilitySetSelection",
  VIEW_FOCUSED: "GeckoView:AccessibilityViewFocused",
};

const ACCESSFU_MESSAGE = {
  DOSCROLL: "AccessFu:DoScroll",
};

const FRAME_SCRIPT = "chrome://global/content/accessibility/content-script.js";

var AccessFu = {
  /**
   * A lazy getter for event handler that binds the scope to AccessFu object.
   */
  get handleEvent() {
    delete this.handleEvent;
    this.handleEvent = this._handleEvent.bind(this);
    return this.handleEvent;
  },

  /**
   * Start AccessFu mode.
   */
  enable: function enable() {
    if (this._enabled) {
      return;
    }
    this._enabled = true;

    ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");

    Services.obs.addObserver(this, "remote-browser-shown");
    Services.obs.addObserver(this, "inprocess-browser-shown");
    Services.ww.registerNotification(this);

    for (let win of Services.wm.getEnumerator(null)) {
      this._attachWindow(win);
    }

    Logger.info("AccessFu:Enabled");
  },

  /**
   * Disable AccessFu and return to default interaction mode.
   */
  disable: function disable() {
    if (!this._enabled) {
      return;
    }

    this._enabled = false;

    Services.obs.removeObserver(this, "remote-browser-shown");
    Services.obs.removeObserver(this, "inprocess-browser-shown");
    Services.ww.unregisterNotification(this);

    for (let win of Services.wm.getEnumerator(null)) {
      this._detachWindow(win);
    }

    if (this.doneCallback) {
      this.doneCallback();
      delete this.doneCallback;
    }

    Logger.info("AccessFu:Disabled");
  },

  receiveMessage: function receiveMessage(aMessage) {
    Logger.debug(() => {
      return ["Recieved", aMessage.name, JSON.stringify(aMessage.json)];
    });

    switch (aMessage.name) {
      case ACCESSFU_MESSAGE.DOSCROLL:
        this.Input.doScroll(aMessage.json, aMessage.target);
        break;
    }
  },

  _attachWindow: function _attachWindow(win) {
    let wtype = win.document.documentElement.getAttribute("windowtype");
    if (wtype != "navigator:browser" && wtype != "navigator:geckoview") {
      // Don't attach to non-browser or geckoview windows.
      return;
    }

    // Set up frame script
    let mm = win.messageManager;
    for (let messageName of Object.values(ACCESSFU_MESSAGE)) {
      mm.addMessageListener(messageName, this);
    }
    mm.loadFrameScript(FRAME_SCRIPT, true);

    win.addEventListener("TabSelect", this);
    if (win.WindowEventDispatcher) {
      // desktop mochitests don't have this.
      win.WindowEventDispatcher.registerListener(this,
        Object.values(GECKOVIEW_MESSAGE));
    }
  },

  _detachWindow: function _detachWindow(win) {
    let mm = win.messageManager;
    mm.broadcastAsyncMessage("AccessFu:Stop");
    mm.removeDelayedFrameScript(FRAME_SCRIPT);
    for (let messageName of Object.values(ACCESSFU_MESSAGE)) {
      mm.removeMessageListener(messageName, this);
    }

    win.removeEventListener("TabSelect", this);
    if (win.WindowEventDispatcher) {
      // desktop mochitests don't have this.
      win.WindowEventDispatcher.unregisterListener(this,
        Object.values(GECKOVIEW_MESSAGE));
    }
  },

  onEvent(event, data, callback) {
    switch (event) {
      case GECKOVIEW_MESSAGE.SETTINGS:
        if (data.enabled) {
          this._enable();
        } else {
          this._disable();
        }
        break;
      case GECKOVIEW_MESSAGE.NEXT:
      case GECKOVIEW_MESSAGE.PREVIOUS: {
        let rule = "Simple";
        if (data && data.rule && data.rule.length) {
          rule = data.rule.substr(0, 1).toUpperCase() +
            data.rule.substr(1).toLowerCase();
        }
        let method = event.replace(/GeckoView:Accessibility(\w+)/, "move$1");
        this.Input.moveCursor(method, rule, "gesture");
        break;
      }
      case GECKOVIEW_MESSAGE.ACTIVATE:
        this.Input.activateCurrent(data);
        break;
      case GECKOVIEW_MESSAGE.LONG_PRESS:
        // XXX: Advertize long press on supported objects and implement action
        break;
      case GECKOVIEW_MESSAGE.SCROLL_FORWARD:
        this.Input.androidScroll("forward");
        break;
      case GECKOVIEW_MESSAGE.SCROLL_BACKWARD:
        this.Input.androidScroll("backward");
        break;
      case GECKOVIEW_MESSAGE.CURSOR_TO_FOCUSED:
        this.autoMove({ moveToFocused: true });
        break;
      case GECKOVIEW_MESSAGE.BY_GRANULARITY:
        this.Input.moveByGranularity(data);
        break;
      case GECKOVIEW_MESSAGE.EXPLORE_BY_TOUCH:
        this.Input.moveToPoint("Simple", ...data.coordinates);
        break;
      case GECKOVIEW_MESSAGE.SET_SELECTION:
        this.Input.setSelection(data);
        break;
      case GECKOVIEW_MESSAGE.CLIPBOARD:
        this.Input.clipboard(data);
        break;
      case GECKOVIEW_MESSAGE.SELECT:
        this.Input.selectCurrent(data);
        break;
    }
  },

  observe: function observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "domwindowopened": {
        let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
        win.addEventListener("load", () => {
          this._attachWindow(win);
        }, { once: true });
        break;
      }
    }
  },

  _handleEvent: function _handleEvent(aEvent) {
    switch (aEvent.type) {
      case "TabSelect":
      {
        if (this._focused) {
          // We delay this for half a second so the awesomebar could close,
          // and we could use the current coordinates for the content item.
          // XXX TODO figure out how to avoid magic wait here.
          this.autoMove({
            delay: 500,
            forcePresent: true,
            noOpIfOnScreen: true,
            moveMethod: "moveFirst" });
        }
        break;
      }
      default:
        break;
    }
  },

  autoMove: function autoMove(aOptions) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:AutoMove", aOptions);
  },

  // So we don't enable/disable twice
  _enabled: false,

  // Layerview is focused
  _focused: false,

  /**
   * Adjusts the given bounds that are defined in device display pixels
   * to client-relative CSS pixels of the chrome window.
   * @param {Rect} aJsonBounds the bounds to adjust
   * @param {Window} aWindow the window containing the item
   */
  screenToClientBounds(aJsonBounds, aWindow) {
      let bounds = new Rect(aJsonBounds.left, aJsonBounds.top,
                            aJsonBounds.right - aJsonBounds.left,
                            aJsonBounds.bottom - aJsonBounds.top);
      let { devicePixelRatio, mozInnerScreenX, mozInnerScreenY } = aWindow;

      bounds = bounds.scale(1 / devicePixelRatio, 1 / devicePixelRatio);
      bounds = bounds.translate(-mozInnerScreenX, -mozInnerScreenY);
      return bounds.expandToIntegers();
    },
};

var Input = {
  moveToPoint: function moveToPoint(aRule, aX, aY) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:MoveToPoint",
      {rule: aRule, x: aX, y: aY, origin: "top"});
  },

  moveCursor: function moveCursor(aAction, aRule, aInputType, aAdjustRange) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:MoveCursor",
                        { action: aAction, rule: aRule,
                          origin: "top", inputType: aInputType,
                          adjustRange: aAdjustRange });
  },

  androidScroll: function androidScroll(aDirection) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:AndroidScroll",
                        { direction: aDirection, origin: "top" });
  },

  moveByGranularity: function moveByGranularity(aDetails) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:MoveByGranularity", aDetails);
  },

  setSelection: function setSelection(aDetails) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:SetSelection", aDetails);
  },

  clipboard: function clipboard(aDetails) {
    const mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:Clipboard", aDetails);
  },

  activateCurrent: function activateCurrent(aData) {
    let mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:Activate", { offset: 0 });
  },

  selectCurrent: function selectCurrent(aData) {
    let mm = Utils.getMessageManager();
    mm.sendAsyncMessage("AccessFu:Select", aData);
  },

  doScroll: function doScroll(aDetails, aBrowser) {
    let horizontal = aDetails.horizontal;
    let page = aDetails.page;
    let win = aBrowser.ownerGlobal;
    let winUtils = win.windowUtils;
    let p = AccessFu.screenToClientBounds(aDetails.bounds, win).center();
    winUtils.sendWheelEvent(p.x, p.y,
      horizontal ? page : 0, horizontal ? 0 : page, 0,
      win.WheelEvent.DOM_DELTA_PAGE, 0, 0, 0, 0);
  },
};
AccessFu.Input = Input;