ladybird/Libraries/LibDevTools/Actors/AccessibilityWalkerActor.cpp
Sam Atkins 41b4292447 LibDevTools+LibWebView: Implement initial accessibility tree view
This is enough for Firefox to display the Accessibility tab, containing
our accessibility tree which can be inspected. Most information is
blank for now.

There's quite a bit of duplication between AccessibilityWalkerActor and
WalkerActor - it might be worth trying to make a base class once the
details are figured out. Frustratingly, the two don't work quite the
same: for a lot of messages that would be sent to WalkerActor, the
accessibility equivalent is sent to the AccessibilityNodeActor instead.

Co-authored-by: Tim Flynn <trflynn89@pm.me>
2025-10-20 10:51:19 +01:00

228 lines
6.9 KiB
C++

/*
* Copyright (c) 2025, Sam Atkins <sam@ladybird.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <LibDevTools/Actors/AccessibilityNodeActor.h>
#include <LibDevTools/Actors/AccessibilityWalkerActor.h>
#include <LibDevTools/Actors/TabActor.h>
#include <LibDevTools/DevToolsServer.h>
namespace DevTools {
NonnullRefPtr<AccessibilityWalkerActor> AccessibilityWalkerActor::create(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, JsonObject accessibility_tree)
{
return adopt_ref(*new AccessibilityWalkerActor(devtools, move(name), move(tab), move(accessibility_tree)));
}
AccessibilityWalkerActor::AccessibilityWalkerActor(DevToolsServer& devtools, String name, WeakPtr<TabActor> tab, JsonObject accessibility_tree)
: Actor(devtools, move(name))
, m_tab(move(tab))
, m_accessibility_tree(move(accessibility_tree))
{
populate_accessibility_tree_cache();
}
AccessibilityWalkerActor::~AccessibilityWalkerActor() = default;
void AccessibilityWalkerActor::handle_message(Message const& message)
{
if (message.type == "children"sv) {
JsonArray children;
MUST(children.append(serialize_root()));
JsonObject response;
response.set("children"sv, move(children));
send_response(message, move(response));
return;
}
if (message.type == "hideTabbingOrder"sv) {
// Blank response is expected.
send_response(message, JsonObject {});
return;
}
if (message.type == "highlightAccessible"sv) {
// FIXME: Highlight things.
JsonObject response;
response.set("value"sv, false);
send_response(message, move(response));
return;
}
if (message.type == "unhighlight"sv) {
// FIXME: Unhighlight things.
send_response(message, JsonObject {});
return;
}
send_unrecognized_packet_type_error(message);
}
bool AccessibilityWalkerActor::is_suitable_for_accessibility_inspection(JsonValue const& node)
{
if (!node.is_object())
return false;
auto const& object = node.as_object();
if (!object.has_string("type"sv) || !object.has_string("role"sv))
return false;
if (!object.has_i64("id"sv))
return false;
if (auto text = object.get_string("text"sv); text.has_value()) {
if (AK::StringUtils::is_whitespace(*text))
return false;
}
return true;
}
JsonValue AccessibilityWalkerActor::serialize_root() const
{
return serialize_node(m_accessibility_tree);
}
JsonValue AccessibilityWalkerActor::serialize_node(JsonObject const& node) const
{
auto tab = m_tab.strong_ref();
if (!tab)
return {};
auto actor = node.get_string("actor"sv);
if (!actor.has_value())
return {};
auto name = node.get_string("name"sv).value_or(""_string);
auto type = node.get_string("type"sv).release_value();
auto role = node.get_string("role"sv).release_value();
auto child_count = 0u;
if (auto children = node.get_array("children"sv); children.has_value())
child_count = children->size();
JsonObject serialized;
serialized.set("actor"sv, actor.release_value());
serialized.set("name"sv, move(name));
serialized.set("role"sv, move(role));
serialized.set("useChildTargetToFetchChildren"sv, false);
serialized.set("childCount"sv, child_count);
serialized.set("checks"sv, JsonObject {});
return serialized;
}
Optional<Node> AccessibilityWalkerActor::accessibility_node_for(WeakPtr<AccessibilityWalkerActor> const& weak_walker, StringView actor)
{
if (auto walker = weak_walker.strong_ref())
return walker->accessibility_node(actor);
return {};
}
Optional<Node> AccessibilityWalkerActor::accessibility_node(StringView actor)
{
auto tab = m_tab.strong_ref();
if (!tab)
return {};
auto maybe_node = m_actor_to_node_map.get(actor);
if (!maybe_node.has_value() || !maybe_node.value())
return {};
auto const& node = *maybe_node.value();
auto identifier = NodeIdentifier::for_node(node);
return Node { .node = node, .identifier = move(identifier), .tab = tab.release_nonnull() };
}
Optional<Node> AccessibilityWalkerActor::parent_of_accessibility_node_for(WeakPtr<AccessibilityWalkerActor> const& weak_walker, Node const& accessibility_node)
{
if (auto walker = weak_walker.strong_ref())
return walker->parent_of_accessibility_node(accessibility_node);
return {};
}
Optional<Node> AccessibilityWalkerActor::parent_of_accessibility_node(Node const& accessibility_node)
{
auto tab = m_tab.strong_ref();
if (!tab)
return {};
auto maybe_parent_node = m_node_to_parent_map.get(&accessibility_node.node);
if (!maybe_parent_node.has_value() || !maybe_parent_node.value())
return {};
auto const& parent_node = *maybe_parent_node.value();
auto identifier = NodeIdentifier::for_node(parent_node);
return Node { .node = parent_node, .identifier = move(identifier), .tab = tab.release_nonnull() };
}
Optional<Node> AccessibilityWalkerActor::root_accessibility_node_for(WeakPtr<AccessibilityWalkerActor> const& weak_walker)
{
if (auto walker = weak_walker.strong_ref())
return walker->root_accessibility_node();
return {};
}
Optional<Node> AccessibilityWalkerActor::root_accessibility_node()
{
auto tab = m_tab.strong_ref();
if (!tab)
return {};
auto identifier = NodeIdentifier::for_node(m_accessibility_tree);
return Node { .node = m_accessibility_tree, .identifier = move(identifier), .tab = tab.release_nonnull() };
}
void AccessibilityWalkerActor::populate_accessibility_tree_cache()
{
m_node_to_parent_map.clear();
m_actor_to_node_map.clear();
m_node_id_to_actor_map.clear();
populate_accessibility_tree_cache(m_accessibility_tree, nullptr);
}
void AccessibilityWalkerActor::populate_accessibility_tree_cache(JsonObject& node, JsonObject const* parent)
{
auto const& node_actor = actor_for_node(node);
node.set("actor"sv, node_actor.name());
m_node_to_parent_map.set(&node, parent);
m_actor_to_node_map.set(node_actor.name(), &node);
m_node_id_to_actor_map.set(node_actor.node_identifier().id, node_actor.name());
auto children = node.get_array("children"sv);
if (!children.has_value())
return;
children->values().remove_all_matching([&](JsonValue const& child) {
return !is_suitable_for_accessibility_inspection(child);
});
children->for_each([&](JsonValue& child) {
populate_accessibility_tree_cache(child.as_object(), &node);
});
}
AccessibilityNodeActor const& AccessibilityWalkerActor::actor_for_node(JsonObject const& node)
{
auto identifier = NodeIdentifier::for_node(node);
if (auto it = m_node_actors.find(identifier); it != m_node_actors.end()) {
if (auto node_actor = it->value.strong_ref())
return *node_actor;
}
auto& node_actor = devtools().register_actor<AccessibilityNodeActor>(identifier, *this);
m_node_actors.set(identifier, node_actor);
return node_actor;
}
}