diff --git a/Userland/Applications/Browser/InspectorWidget.cpp b/Userland/Applications/Browser/InspectorWidget.cpp index 773eec9b06..b578804b3a 100644 --- a/Userland/Applications/Browser/InspectorWidget.cpp +++ b/Userland/Applications/Browser/InspectorWidget.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,11 @@ InspectorWidget::InspectorWidget() set_selection(index); }; + auto& accessibility_tree_container = top_tab_widget.add_tab("Accessibility"); + accessibility_tree_container.set_layout(); + accessibility_tree_container.layout()->set_margins({ 4, 4, 4, 4 }); + m_accessibility_tree_view = accessibility_tree_container.add(); + auto& bottom_tab_widget = splitter.add(); auto& computed_style_table_container = bottom_tab_widget.add_tab("Computed"); @@ -212,4 +218,11 @@ void InspectorWidget::clear_style_json() m_element_size_view->set_node_content_height(0); } +void InspectorWidget::set_accessibility_json(StringView json) +{ + m_accessibility_tree_view->set_model(WebView::AccessibilityTreeModel::create(json, *m_accessibility_tree_view)); + + // TODO: Support selections from accessibility tab +} + } diff --git a/Userland/Applications/Browser/InspectorWidget.h b/Userland/Applications/Browser/InspectorWidget.h index 16c85e39fb..095b2a4f0a 100644 --- a/Userland/Applications/Browser/InspectorWidget.h +++ b/Userland/Applications/Browser/InspectorWidget.h @@ -43,6 +43,7 @@ public: void set_dom_json(StringView); void clear_dom_json(); void set_dom_node_properties_json(Selection, StringView computed_values_json, StringView resolved_values_json, StringView custom_properties_json, StringView node_box_sizing_json); + void set_accessibility_json(StringView); void set_selection(Selection); void select_default_node(); @@ -59,6 +60,7 @@ private: RefPtr m_web_view; RefPtr m_dom_tree_view; + RefPtr m_accessibility_tree_view; RefPtr m_computed_style_table_view; RefPtr m_resolved_style_table_view; RefPtr m_custom_properties_table_view; diff --git a/Userland/Applications/Browser/Tab.cpp b/Userland/Applications/Browser/Tab.cpp index 9c989b20c4..e9ba11f7a0 100644 --- a/Userland/Applications/Browser/Tab.cpp +++ b/Userland/Applications/Browser/Tab.cpp @@ -244,8 +244,10 @@ Tab::Tab(BrowserWindow& window) update_status(); - if (m_dom_inspector_widget) + if (m_dom_inspector_widget) { m_web_content_view->inspect_dom_tree(); + m_web_content_view->inspect_accessibility_tree(); + } }; view().on_navigate_back = [this]() { @@ -421,6 +423,11 @@ Tab::Tab(BrowserWindow& window) m_dom_inspector_widget->set_dom_node_properties_json({ node_id }, specified, computed, custom_properties, node_box_sizing); }; + view().on_get_accessibility_tree = [this](auto& accessibility_tree) { + if (m_dom_inspector_widget) + m_dom_inspector_widget->set_accessibility_json(accessibility_tree); + }; + view().on_js_console_new_message = [this](auto message_index) { if (m_console_widget) m_console_widget->notify_about_new_console_message(message_index); @@ -666,6 +673,7 @@ void Tab::show_inspector_window(Browser::Tab::InspectorTarget inspector_target) m_dom_inspector_widget = window->set_main_widget().release_value_but_fixme_should_propagate_errors(); m_dom_inspector_widget->set_web_view(*m_web_content_view); m_web_content_view->inspect_dom_tree(); + m_web_content_view->inspect_accessibility_tree(); } if (inspector_target == InspectorTarget::HoveredElement) { diff --git a/Userland/Libraries/LibWebView/AccessibilityTreeModel.cpp b/Userland/Libraries/LibWebView/AccessibilityTreeModel.cpp new file mode 100644 index 0000000000..ca0302ae1d --- /dev/null +++ b/Userland/Libraries/LibWebView/AccessibilityTreeModel.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2022, Jonah Shafran + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include + +namespace WebView { + +AccessibilityTreeModel::AccessibilityTreeModel(JsonObject accessibility_tree, GUI::TreeView* tree_view) + : m_tree_view(tree_view) + , m_accessibility_tree(move(accessibility_tree)) +{ + map_accessibility_nodes_to_parent(nullptr, &m_accessibility_tree); +} + +AccessibilityTreeModel::~AccessibilityTreeModel() = default; + +GUI::ModelIndex AccessibilityTreeModel::index(int row, int column, GUI::ModelIndex const& parent) const +{ + if (!parent.is_valid()) { + return create_index(row, column, &m_accessibility_tree); + } + + auto const& parent_node = *static_cast(parent.internal_data()); + auto const* children = get_children(parent_node); + if (!children) + return create_index(row, column, &m_accessibility_tree); + + auto const& child_node = children->at(row).as_object(); + return create_index(row, column, &child_node); +} + +GUI::ModelIndex AccessibilityTreeModel::parent_index(GUI::ModelIndex const& index) const +{ + if (!index.is_valid()) + return {}; + + auto const& node = *static_cast(index.internal_data()); + + auto const* parent_node = get_parent(node); + if (!parent_node) + return {}; + + // If the parent is the root document, we know it has index 0, 0 + if (parent_node == &m_accessibility_tree) { + return create_index(0, 0, parent_node); + } + + // Otherwise, we need to find the grandparent, to find the index of parent within that + auto const* grandparent_node = get_parent(*parent_node); + VERIFY(grandparent_node); + + auto const* grandparent_children = get_children(*grandparent_node); + if (!grandparent_children) + return {}; + + for (size_t grandparent_child_index = 0; grandparent_child_index < grandparent_children->size(); ++grandparent_child_index) { + auto const& child = grandparent_children->at(grandparent_child_index).as_object(); + if (&child == parent_node) + return create_index(grandparent_child_index, 0, parent_node); + } + + return {}; +} + +int AccessibilityTreeModel::row_count(GUI::ModelIndex const& index) const +{ + if (!index.is_valid()) + return 1; + + auto const& node = *static_cast(index.internal_data()); + auto const* children = get_children(node); + return children ? children->size() : 0; +} + +int AccessibilityTreeModel::column_count(const GUI::ModelIndex&) const +{ + return 1; +} + +GUI::Variant AccessibilityTreeModel::data(GUI::ModelIndex const& index, GUI::ModelRole role) const +{ + auto const& node = *static_cast(index.internal_data()); + auto type = node.get("type"sv).as_string_or("unknown"sv); + + if (role == GUI::ModelRole::Display) { + if (type == "text") + return node.get("text"sv).as_string(); + + auto node_role = node.get("role"sv).as_string(); + if (type != "element") + return node_role; + + StringBuilder builder; + builder.append(node_role.to_lowercase()); + return builder.to_deprecated_string(); + } + return {}; +} + +void AccessibilityTreeModel::map_accessibility_nodes_to_parent(JsonObject const* parent, JsonObject const* node) +{ + m_accessibility_node_to_parent_map.set(node, parent); + + auto const* children = get_children(*node); + if (!children) + return; + + children->for_each([&](auto const& child) { + auto const& child_node = child.as_object(); + map_accessibility_nodes_to_parent(node, &child_node); + }); +} + +} diff --git a/Userland/Libraries/LibWebView/AccessibilityTreeModel.h b/Userland/Libraries/LibWebView/AccessibilityTreeModel.h new file mode 100644 index 0000000000..7cac5c85b1 --- /dev/null +++ b/Userland/Libraries/LibWebView/AccessibilityTreeModel.h @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2022, Jonah Shafran + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include +#include +#include + +namespace WebView { + +class AccessibilityTreeModel final : public GUI::Model { +public: + static NonnullRefPtr create(StringView accessibility_tree, GUI::TreeView& tree_view) + { + auto json_or_error = JsonValue::from_string(accessibility_tree).release_value_but_fixme_should_propagate_errors(); + return adopt_ref(*new AccessibilityTreeModel(json_or_error.as_object(), &tree_view)); + } + + static NonnullRefPtr create(StringView accessibility_tree) + { + auto json_or_error = JsonValue::from_string(accessibility_tree).release_value_but_fixme_should_propagate_errors(); + return adopt_ref(*new AccessibilityTreeModel(json_or_error.as_object(), nullptr)); + } + + virtual ~AccessibilityTreeModel() override; + + virtual int row_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override; + virtual int column_count(GUI::ModelIndex const& = GUI::ModelIndex()) const override; + virtual GUI::Variant data(GUI::ModelIndex const&, GUI::ModelRole) const override; + virtual GUI::ModelIndex index(int row, int column, GUI::ModelIndex const& parent = GUI::ModelIndex()) const override; + virtual GUI::ModelIndex parent_index(GUI::ModelIndex const&) const override; + +private: + AccessibilityTreeModel(JsonObject, GUI::TreeView*); + + ALWAYS_INLINE JsonObject const* get_parent(JsonObject const& o) const + { + auto parent_node = m_accessibility_node_to_parent_map.get(&o); + VERIFY(parent_node.has_value()); + return *parent_node; + } + + ALWAYS_INLINE static JsonArray const* get_children(JsonObject const& o) + { + if (auto const* maybe_children = o.get_ptr("children"sv); maybe_children) + return &maybe_children->as_array(); + return nullptr; + } + + void map_accessibility_nodes_to_parent(JsonObject const* parent, JsonObject const* child); + + GUI::TreeView* m_tree_view { nullptr }; + JsonObject m_accessibility_tree; + HashMap m_accessibility_node_to_parent_map; +}; + +} diff --git a/Userland/Libraries/LibWebView/CMakeLists.txt b/Userland/Libraries/LibWebView/CMakeLists.txt index ee25b4463e..707bb835ee 100644 --- a/Userland/Libraries/LibWebView/CMakeLists.txt +++ b/Userland/Libraries/LibWebView/CMakeLists.txt @@ -1,4 +1,5 @@ set(SOURCES + AccessibilityTreeModel.cpp DOMTreeModel.cpp OutOfProcessWebView.cpp RequestServerAdapter.cpp