blob: 34451b098c5748e9198457b8e2dbb69342a4d09c [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {SACache} from './cache.js';
import {Navigator} from './navigator.js';
import {DesktopNode} from './nodes/desktop_node.js';
import {SAChildNode, SARootNode} from './nodes/switch_access_node.js';
import {SwitchAccessPredicate} from './switch_access_predicate.js';
const AutomationNode = chrome.automation.AutomationNode;
/** This class is a structure to store previous state for restoration. */
export class FocusData {
/**
* @param {!SARootNode} group
* @param {!SAChildNode} focus Must be a child of |group|.
*/
constructor(group, focus) {
/** @type {!SARootNode} */
this.group = group;
/** @type {!SAChildNode} */
this.focus = focus;
}
/** @return {boolean} */
isValid() {
if (this.group.isValidGroup()) {
// Ensure it is still valid. Some nodes may have been added
// or removed since this was last used.
this.group.refreshChildren();
}
return this.group.isValidGroup();
}
}
/** This class handles saving and retrieving FocusData. */
export class FocusHistory {
constructor() {
/** @private {!Array<!FocusData>} */
this.dataStack_ = [];
}
/**
* Creates the restore data to get from the desktop node to the specified
* automation node.
* Erases the current history and replaces with the new data.
* @param {!AutomationNode} node
* @return {boolean} Whether the history was rebuilt from the given node.
*/
buildFromAutomationNode(node) {
if (!node.parent) {
// No ancestors, cannot create stack.
return false;
}
const cache = new SACache();
// Create a list of ancestors.
const ancestorStack = [node];
while (node.parent) {
ancestorStack.push(node.parent);
node = node.parent;
}
let group = DesktopNode.build(ancestorStack.pop());
const firstAncestor = ancestorStack[ancestorStack.length - 1];
if (!SwitchAccessPredicate.isInterestingSubtree(firstAncestor, cache)) {
// If the topmost ancestor (other than the desktop) is entirely
// uninteresting, we leave the history as is.
return false;
}
const newDataStack = [];
while (ancestorStack.length > 0) {
const candidate = ancestorStack.pop();
if (!SwitchAccessPredicate.isInteresting(candidate, group, cache)) {
continue;
}
const focus = group.findChild(candidate);
if (!focus) {
continue;
}
newDataStack.push(new FocusData(group, focus));
group = focus.asRootNode();
if (!group) {
break;
}
}
if (newDataStack.length === 0) {
return false;
}
this.dataStack_ = newDataStack;
return true;
}
/**
* @param {!function(!FocusData): boolean} predicate
* @return {boolean}
*/
containsDataMatchingPredicate(predicate) {
for (const data of this.dataStack_) {
if (predicate(data)) {
return true;
}
}
return false;
}
/**
* Returns the most proximal restore data, but does not remove it from the
* history.
* @return {?FocusData}
*/
peek() {
return this.dataStack_[this.dataStack_.length - 1] || null;
}
/** @return {!FocusData} */
retrieve() {
let data = this.dataStack_.pop();
while (data && !data.isValid()) {
data = this.dataStack_.pop();
}
if (data) {
return data;
}
// If we don't have any valid history entries, fallback to the desktop node.
const desktop = new DesktopNode(Navigator.byItem.desktopNode);
return new FocusData(desktop, desktop.firstChild);
}
/** @param {!FocusData} data */
save(data) {
this.dataStack_.push(data);
}
/** Support for this type being used in for..of loops. */
[Symbol.iterator]() {
return this.dataStack_[Symbol.iterator]();
}
}