blob: 3b2eec22ac54f2c40b16c2d98a908797f3737c84 [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Handles automation events on the currently focused node.
*/
import {CursorRange} from '../../common/cursors/range.js';
import {ChromeVoxEvent} from '../common/custom_automation_event.js';
import {QueueMode, TtsSpeechProperties} from '../common/tts_interface.js';
import {BaseAutomationHandler} from './base_automation_handler.js';
import {ChromeVoxState} from './chromevox_state.js';
import {Output} from './output/output.js';
import {OutputEventType} from './output/output_types.js';
const AutomationEvent = chrome.automation.AutomationEvent;
const AutomationNode = chrome.automation.AutomationNode;
const Dir = constants.Dir;
const EventType = chrome.automation.EventType;
const RoleType = chrome.automation.RoleType;
const StateType = chrome.automation.StateType;
export class FocusAutomationHandler extends BaseAutomationHandler {
/** @private */
constructor() {
super(null);
/** @private {AutomationNode|undefined} */
this.previousActiveDescendant_;
chrome.automation.getDesktop(desktop => {
desktop.addEventListener(EventType.FOCUS, this.onFocus.bind(this), false);
});
}
static init() {
if (FocusAutomationHandler.instance) {
throw 'Error: Trying to create two instances of singleton FocusAutomationHandler';
}
FocusAutomationHandler.instance = new FocusAutomationHandler();
}
/**
* @param {!AutomationEvent} evt
*/
onFocus(evt) {
this.removeAllListeners();
// Events on roots and web views can be very noisy due to bubling. Ignore
// these.
if (evt.target.root === evt.target ||
evt.target.role === RoleType.WEB_VIEW) {
return;
}
this.previousActiveDescendant_ = evt.target.activeDescendant;
this.node_ = evt.target;
this.addListener_(
EventType.ACTIVE_DESCENDANT_CHANGED, this.onActiveDescendantChanged);
this.addListener_(EventType.DETAILS_CHANGED, this.onDetailsChanged);
this.addListener_(
EventType.MENU_LIST_ITEM_SELECTED, this.onEventIfSelected);
this.addListener_(
EventType.SELECTED_VALUE_CHANGED, this.onSelectedValueChanged_);
}
/**
* Handles active descendant changes.
* @param {!AutomationEvent} evt
*/
onActiveDescendantChanged(evt) {
if (!evt.target.activeDescendant) {
return;
}
let skipFocusCheck = false;
chrome.automation.getFocus(focus => {
if (focus.role === RoleType.POP_UP_BUTTON) {
skipFocusCheck = true;
}
});
if (!skipFocusCheck && !evt.target.state.focused) {
return;
}
// Various events might come before a key press (which forces flushed
// speech) and this handler. Force output to be at least category flushed.
Output.forceModeForNextSpeechUtterance(QueueMode.CATEGORY_FLUSH);
const prev = this.previousActiveDescendant_ ?
CursorRange.fromNode(this.previousActiveDescendant_) :
ChromeVoxState.instance.currentRange;
new Output()
.withoutHints()
.withRichSpeechAndBraille(
CursorRange.fromNode(evt.target.activeDescendant), prev,
OutputEventType.NAVIGATE)
.go();
this.previousActiveDescendant_ = evt.target.activeDescendant;
}
/**
* Informs users that details are now available.
* @param {!ChromeVoxEvent} evt
*/
onDetailsChanged(evt) {
const range = ChromeVoxState.instance.currentRange;
let node = range.start ? range.start.node : null;
while (node && (!node.details || !node.details.length)) {
node = node.parent;
}
if (!node) {
return;
}
// Note that we only output speech. Braille output shows the entire line, so
// details output should not be based on an announcement like this. Don't
// allow interruption of this announcement which can occur in a slew of
// events (e.g. typing).
new Output()
.withInitialSpeechProperties(
new TtsSpeechProperties({doNotInterrupt: true}))
.formatForSpeech('@hint_details')
.go();
}
/**
* @param {!ChromeVoxEvent} evt
*/
onEventIfSelected(evt) {
if (evt.target.selected) {
this.onEventDefault(evt);
}
}
/**
* @param {!ChromeVoxEvent} evt
*/
onSelectedValueChanged_(evt) {
if (evt.target.role !== RoleType.POP_UP_BUTTON ||
evt.target.state.editable) {
return;
}
// Focus might be on a container above the popup button.
if (this.node_ !== evt.target) {
return;
}
// If it has children, that means a menu is showing.
if (evt.target.firstChild) {
return;
}
if (evt.target.value) {
const output = new Output();
output.format('$value @describe_index($posInSet, $setSize)', evt.target);
output.go();
return;
}
}
}
/** @type {FocusAutomationHandler} */
FocusAutomationHandler.instance;