зеркало из https://github.com/mozilla/gecko-dev.git
314 строки
10 KiB
JavaScript
314 строки
10 KiB
JavaScript
/* 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"];
|
|
|
|
const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
|
const {Logger, Utils} = ChromeUtils.import("resource://gre/modules/accessibility/Utils.jsm");
|
|
ChromeUtils.defineModuleGetter(this, "Rect",
|
|
"resource://gre/modules/Geometry.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",
|
|
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;
|
|
|
|
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 && !this._eventDispatcherListeners.has(win)) {
|
|
const listener = (event, data, callback) => {
|
|
this.onEvent(event, data, callback, win);
|
|
};
|
|
this._eventDispatcherListeners.set(win, listener);
|
|
// desktop mochitests don't have this.
|
|
win.WindowEventDispatcher.registerListener(listener,
|
|
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 && this._eventDispatcherListeners.has(win)) {
|
|
// desktop mochitests don't have this.
|
|
win.WindowEventDispatcher.unregisterListener(
|
|
this._eventDispatcherListeners.get(win),
|
|
Object.values(GECKOVIEW_MESSAGE));
|
|
this._eventDispatcherListeners.delete(win);
|
|
}
|
|
},
|
|
|
|
onEvent(event, data, callback, win) {
|
|
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", win);
|
|
break;
|
|
}
|
|
case GECKOVIEW_MESSAGE.ACTIVATE:
|
|
this.Input.activateCurrent(data, win);
|
|
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", win);
|
|
break;
|
|
case GECKOVIEW_MESSAGE.SCROLL_BACKWARD:
|
|
this.Input.androidScroll("backward", win);
|
|
break;
|
|
case GECKOVIEW_MESSAGE.CURSOR_TO_FOCUSED:
|
|
this.autoMove({ moveToFocused: true }, win);
|
|
break;
|
|
case GECKOVIEW_MESSAGE.BY_GRANULARITY:
|
|
this.Input.moveByGranularity(data, win);
|
|
break;
|
|
case GECKOVIEW_MESSAGE.EXPLORE_BY_TOUCH:
|
|
this.Input.moveToPoint("Simple", ...data.coordinates, win);
|
|
break;
|
|
case GECKOVIEW_MESSAGE.SET_SELECTION:
|
|
this.Input.setSelection(data, win);
|
|
break;
|
|
case GECKOVIEW_MESSAGE.CLIPBOARD:
|
|
this.Input.clipboard(data, win);
|
|
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, aWindow) {
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:AutoMove", aOptions);
|
|
},
|
|
|
|
// So we don't enable/disable twice
|
|
_enabled: false,
|
|
|
|
// Layerview is focused
|
|
_focused: false,
|
|
|
|
_eventDispatcherListeners: new WeakMap(),
|
|
|
|
/**
|
|
* 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, aWindow) {
|
|
Logger.debug("moveToPoint", aX, aY);
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:MoveToPoint",
|
|
{rule: aRule, x: aX, y: aY, origin: "top"});
|
|
},
|
|
|
|
moveCursor: function moveCursor(aAction, aRule, aInputType, aWindow) {
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:MoveCursor",
|
|
{ action: aAction, rule: aRule,
|
|
origin: "top", inputType: aInputType });
|
|
},
|
|
|
|
androidScroll: function androidScroll(aDirection, aWindow) {
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:AndroidScroll",
|
|
{ direction: aDirection, origin: "top" });
|
|
},
|
|
|
|
moveByGranularity: function moveByGranularity(aDetails, aWindow) {
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:MoveByGranularity", aDetails);
|
|
},
|
|
|
|
setSelection: function setSelection(aDetails, aWindow) {
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:SetSelection", aDetails);
|
|
},
|
|
|
|
clipboard: function clipboard(aDetails, aWindow) {
|
|
const mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:Clipboard", aDetails);
|
|
},
|
|
|
|
activateCurrent: function activateCurrent(aData, aWindow) {
|
|
let mm = Utils.getCurrentMessageManager(aWindow);
|
|
mm.sendAsyncMessage("AccessFu:Activate", { offset: 0 });
|
|
},
|
|
|
|
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;
|