blob: 4e6b8728807d3a888b3f99598b0e9bcd76b96927 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview ChromeVox pointer handler. A pointer, in this context, is
* either user touch or mouse input.
*/
import {AutomationPredicate} from '../../common/automation_predicate.js';
import {EventGenerator} from '../../common/event_generator.js';
import {LocalStorage} from '../../common/local_storage.js';
import {CustomAutomationEvent} from '../common/custom_automation_event.js';
import {EarconId} from '../common/earcon_id.js';
import {QueueMode} from '../common/tts_types.js';
import {BaseAutomationHandler} from './base_automation_handler.js';
import {ChromeVox} from './chromevox.js';
import {ChromeVoxState} from './chromevox_state.js';
import {DesktopAutomationInterface} from './desktop_automation_interface.js';
import {Output} from './output/output.js';
const AutomationNode = chrome.automation.AutomationNode;
const AutomationEvent = chrome.automation.AutomationEvent;
const EventType = chrome.automation.EventType;
const RoleType = chrome.automation.RoleType;
export class PointerHandler extends BaseAutomationHandler {
constructor() {
super(null);
/** @private {number|undefined} */
this.mouseX_;
/** @private {number|undefined} */
this.mouseY_;
/** @private {!Date} */
this.lastNoPointerAnchorEarconPlayedTime_ = new Date();
/** @private {number} */
this.expectingHoverCount_ = 0;
/** @private {boolean} */
this.isChromebox_ = false;
/** @private {!Date} */
this.lastHoverRequested_ = new Date();
chrome.automation.getDesktop(desktop => {
this.node_ = desktop;
this.addListener_(EventType.MOUSE_MOVED, this.onMouseMove);
// This is needed for ARC++ and Lacros. They send mouse move and hit test
// respectively. Each responds with hover.
this.addListener_(EventType.HOVER, evt => {
if (this.expectingHoverCount_ === 0) {
return;
}
// Stop honoring expectingHoverCount_ if it comes far after its
// corresponding requested hit test.
if (new Date() - this.lastHoverRequested_ > 500) {
this.expectingHoverCount_ = 0;
}
this.expectingHoverCount_--;
this.handleHitTestResult(evt.target);
});
this.mouseX_ = 0;
this.mouseY_ = 0;
});
if (LocalStorage.get('speakTextUnderMouse')) {
chrome.accessibilityPrivate.enableMouseEvents(true);
}
chrome.chromeosInfoPrivate.get(['deviceType'], result => {
this.isChromebox_ = result['deviceType'] ===
chrome.chromeosInfoPrivate.DeviceType.CHROMEBOX;
});
}
/**
* Performs a hit test using the most recent mouse coordinates received in
* onMouseMove or onMove (a e.g. for touch explore).
* @param {boolean} isTouch
* @param {AutomationNode} specificNode
*/
runHitTest(isTouch = false, specificNode = null) {
if (this.mouseX_ === undefined || this.mouseY_ === undefined) {
return;
}
if (isTouch && this.isChromebox_) {
// TODO(accessibility): hit testing seems to be broken in some cases e.g.
// on the main CFM UI. Synthesize mouse moves with the touch
// accessibility flag for now for touch-based user gestures. Eliminate
// this branch once hit testing is fixed.
this.synthesizeMouseMove();
return;
}
const actOnNode = specificNode ? specificNode : this.node_;
actOnNode.hitTestWithReply(this.mouseX_, this.mouseY_, target => {
this.handleHitTestResult(target);
});
}
/**
* Handles mouse move events.
* @param {AutomationEvent} evt The mouse move event to process.
*/
onMouseMove(evt) {
this.onMove(evt.mouseX, evt.mouseY);
}
/**
* Handles touch move events.
* @param {number} x
* @param {number} y
*/
onTouchMove(x, y) {
this.onMove(x, y, true);
}
/**
* Inform this handler of a move to (x, y).
* @param {number} x
* @param {number} y
* @param {boolean} isTouch
*/
onMove(x, y, isTouch = false) {
this.mouseX_ = x;
this.mouseY_ = y;
this.runHitTest(isTouch);
}
/**
* Synthesizes a mouse move on the current mouse location.
*/
synthesizeMouseMove() {
if (this.mouseX_ === undefined || this.mouseY_ === undefined) {
return;
}
this.expectingHoverCount_++;
this.lastHoverRequested_ = new Date();
EventGenerator.sendMouseMove(
this.mouseX_, this.mouseY_, true /* touchAccessibility */);
}
/**
* Handles the result of a test test e.g. speaking the node.
* @param {chrome.automation.AutomationNode} result
*/
handleHitTestResult(result) {
if (!result) {
return;
}
let target = result;
// The target is in an ExoSurface, which hosts remote content.
if (target.role === RoleType.WINDOW &&
target.className.indexOf('ExoSurface') === 0) {
// We're in ARC++, which still requires a synthesized mouse
// event.
this.synthesizeMouseMove();
return;
}
let targetLeaf = null;
let targetObject = null;
while (target && target !== target.root) {
if (!targetObject && AutomationPredicate.touchObject(target)) {
targetObject = target;
}
if (AutomationPredicate.touchLeaf(target)) {
targetLeaf = target;
}
target = target.parent;
}
target = targetLeaf || targetObject;
if (!target) {
// This clears the anchor point in the TouchExplorationController (so when
// a user touch explores back to the previous range, it will be announced
// again).
ChromeVoxState.instance.setCurrentRange(null);
// Play a earcon to let the user know they're in the middle of nowhere.
if ((new Date() - this.lastNoPointerAnchorEarconPlayedTime_) >
PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS) {
ChromeVox.earcons.playEarcon(EarconId.NO_POINTER_ANCHOR);
this.lastNoPointerAnchorEarconPlayedTime_ = new Date();
}
chrome.tts.stop();
return;
}
if (ChromeVoxState.instance.currentRange &&
target === ChromeVoxState.instance.currentRange.start.node) {
return;
}
Output.forceModeForNextSpeechUtterance(QueueMode.FLUSH);
DesktopAutomationInterface.instance.onEventDefault(
new CustomAutomationEvent(
EventType.HOVER, target,
{eventFromAction: chrome.automation.ActionType.HIT_TEST}));
}
}
/** @const {number} */
PointerHandler.MIN_NO_POINTER_ANCHOR_SOUND_DELAY_MS = 500;