blob: 9faee21564a5f9c039663a2d8d92de2c1b7161ab [file] [log] [blame]
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "cobalt/accessibility/screen_reader.h"
#include <string>
#include "base/stringprintf.h"
#include "cobalt/accessibility/internal/live_region.h"
#include "cobalt/accessibility/text_alternative.h"
#include "cobalt/dom/element.h"
#include "cobalt/dom/html_body_element.h"
#include "cobalt/dom/node_list.h"
namespace cobalt {
namespace accessibility {
namespace {
// When text is removed from a live region, add the word "Removed" to indicate
// that the text has been removed.
// TODO: Support other languages.
const char kRemovedFormatString[] = "Removed. %s";
}
ScreenReader::ScreenReader(dom::Document* document, TTSEngine* tts_engine,
dom::MutationObserverTaskManager* task_manager)
: enabled_(true), document_(document), tts_engine_(tts_engine),
focus_changed_(false) {
document_->AddObserver(this);
live_region_observer_ = new dom::MutationObserver(
base::Bind(&ScreenReader::MutationObserverCallback,
base::Unretained(this)),
task_manager);
}
ScreenReader::~ScreenReader() {
live_region_observer_->Disconnect();
document_->RemoveObserver(this);
}
void ScreenReader::set_enabled(bool value) {
enabled_ = value;
}
void ScreenReader::OnLoad() {
dom::MutationObserverInit init;
init.set_subtree(true);
// Track character data and node addition/removal for live regions.
init.set_character_data(true);
init.set_child_list(true);
if (document_->body()) {
live_region_observer_->Observe(document_->body(), init);
}
}
void ScreenReader::OnFocusChanged() {
if (focus_changed_) {
return;
}
focus_changed_ = true;
MessageLoop::current()->PostTask(
FROM_HERE,
base::Bind(&ScreenReader::FocusChangedCallback, base::Unretained(this)));
}
void ScreenReader::FocusChangedCallback() {
focus_changed_ = false;
if (!enabled_) {
return;
}
scoped_refptr<dom::Element> element = document_->active_element();
if (element) {
std::string text_alternative = ComputeTextAlternative(element);
tts_engine_->SpeakNow(text_alternative);
}
}
// MutationObserver callback for tracking the creation and destruction of
// live regions.
void ScreenReader::MutationObserverCallback(
const MutationRecordSequence& sequence,
const scoped_refptr<dom::MutationObserver>& observer) {
// TODO: Only check live regions if text-to-speech is enabled.
if (!enabled_) {
return;
}
DCHECK_EQ(observer, live_region_observer_);
for (size_t i = 0; i < sequence.size(); ++i) {
scoped_refptr<dom::MutationRecord> record = sequence.at(i);
// Check if the target node is part of a live region.
scoped_ptr<internal::LiveRegion> live_region =
internal::LiveRegion::GetLiveRegionForNode(record->target());
if (!live_region) {
continue;
}
const char* format_string = NULL;
// Get a list of changed nodes, based on the type of change this was and
// whether or not the live region cares about this type of change.
scoped_refptr<dom::NodeList> changed_nodes;
if (record->type() == base::Tokens::characterData() &&
live_region->IsMutationRelevant(
internal::LiveRegion::kMutationTypeText)) {
changed_nodes = new dom::NodeList();
changed_nodes->AppendNode(record->target());
} else if (record->type() == base::Tokens::childList()) {
if (record->added_nodes() &&
live_region->IsMutationRelevant(
internal::LiveRegion::kMutationTypeAddition)) {
changed_nodes = record->added_nodes();
}
if (record->removed_nodes() &&
live_region->IsMutationRelevant(
internal::LiveRegion::kMutationTypeRemoval)) {
changed_nodes = record->removed_nodes();
format_string = kRemovedFormatString;
}
}
// If we don't have changed nodes, that means that the change was not
// relevant to this live region.
if (!changed_nodes || (changed_nodes->length() <= 0)) {
continue;
}
// Check if any of the changed nodes should update the live region
// atomically.
bool is_atomic = false;
for (size_t i = 0; i < changed_nodes->length(); ++i) {
if (live_region->IsAtomic(changed_nodes->Item(i))) {
is_atomic = true;
break;
}
}
std::string text_alternative;
if (is_atomic) {
// If the live region updates atomically, then ignore changed_nodes
// and just get the text alternative from the entire region.
text_alternative = ComputeTextAlternative(live_region->root());
} else {
// Otherwise append all the text alternatives for each node.
for (size_t i = 0; i < changed_nodes->length(); ++i) {
text_alternative += ComputeTextAlternative(changed_nodes->Item(i));
}
}
// If we still don't have a text_alternative to speak, continue to the next
// mutation.
if (text_alternative.empty()) {
continue;
}
// Provide additional context through a format string, if necessary.
if (format_string) {
text_alternative = StringPrintf(format_string, text_alternative.c_str());
}
// Utter the text according to whether this live region is assertive or not.
if (live_region->IsAssertive()) {
tts_engine_->SpeakNow(text_alternative);
} else {
tts_engine_->Speak(text_alternative);
}
}
}
} // namespace accessibility
} // namespace cobalt