| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/accessibility/ax_window_obj_wrapper.h" |
| |
| #include <stddef.h> |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/chromeos_buildflags.h" |
| #include "ui/accessibility/aura/aura_window_properties.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/accessibility/ax_tree_id.h" |
| #include "ui/aura/client/focus_client.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/aura/window_tree_host_platform.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/views/accessibility/ax_aura_obj_cache.h" |
| #include "ui/views/widget/widget.h" |
| |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| #include "ui/views/widget/desktop_aura/desktop_window_tree_host_platform.h" |
| #endif |
| |
| namespace views { |
| namespace { |
| |
| Widget* GetWidgetForWindow(aura::Window* window) { |
| Widget* widget = Widget::GetWidgetForNativeView(window); |
| if (!widget) |
| return nullptr; |
| |
| // Under mus/mash both the WindowTreeHost's root aura::Window and the content |
| // aura::Window will return the same Widget for GetWidgetForNativeView(). Only |
| // return the Widget for the content window, not the root, since otherwise |
| // we'll end up with two children in the AX node tree that have the same |
| // parent. |
| if (widget->GetNativeWindow() != window) { |
| DCHECK(window->IsRootWindow()); |
| return nullptr; |
| } |
| |
| return widget; |
| } |
| |
| // Fires |event| on the |window|, and the Widget and RootView associated with |
| // |window|. |
| void FireEventOnWindowChildWidgetAndRootView(aura::Window* window, |
| ax::mojom::Event event, |
| AXAuraObjCache* cache) { |
| cache->FireEvent(cache->GetOrCreate(window), event); |
| Widget* widget = GetWidgetForWindow(window); |
| if (widget) { |
| cache->FireEvent(cache->GetOrCreate(widget), event); |
| |
| views::View* root_view = widget->GetRootView(); |
| if (root_view) |
| root_view->NotifyAccessibilityEvent(event, true); |
| } |
| } |
| |
| // Fires location change events on a window, taking into account its |
| // associated widget, that widget's root view, and descendant windows. |
| void FireLocationChangesRecursively(aura::Window* window, |
| AXAuraObjCache* cache) { |
| FireEventOnWindowChildWidgetAndRootView( |
| window, ax::mojom::Event::kLocationChanged, cache); |
| |
| for (auto* child : window->children()) |
| FireLocationChangesRecursively(child, cache); |
| } |
| |
| std::string GetWindowName(aura::Window* window) { |
| std::string class_name = window->GetName(); |
| if (class_name.empty()) |
| class_name = "aura::Window"; |
| return class_name; |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| std::string GetPlatformWindowId(aura::Window* window) { |
| // Ignore non-top level windows. |
| if (!window->IsRootWindow() || window->parent()) |
| return std::string(); |
| |
| // On desktop aura there is one WindowTreeHost per top-level window. |
| aura::WindowTreeHost* window_tree_host = window->GetHost(); |
| if (!window_tree_host) |
| return std::string(); |
| |
| // Prefer the DesktopWindowTreeHostPlatform if it exists. |
| DesktopWindowTreeHostPlatform* desktop_window_tree_host_platform = |
| DesktopWindowTreeHostPlatform::GetHostForWidget( |
| window_tree_host->GetAcceleratedWidget()); |
| if (!desktop_window_tree_host_platform) |
| return window_tree_host->GetUniqueId(); |
| |
| while (desktop_window_tree_host_platform->window_parent()) { |
| desktop_window_tree_host_platform = |
| desktop_window_tree_host_platform->window_parent(); |
| } |
| |
| return desktop_window_tree_host_platform->GetUniqueId(); |
| } |
| #endif // BUILDFLAG(IS_CHROMEOS_LACROS) |
| |
| } // namespace |
| |
| AXWindowObjWrapper::AXWindowObjWrapper(AXAuraObjCache* aura_obj_cache, |
| aura::Window* window) |
| : AXAuraObjWrapper(aura_obj_cache), |
| window_(window), |
| is_root_window_(window->IsRootWindow()) { |
| observation_.Observe(window); |
| |
| if (is_root_window_) |
| aura_obj_cache_->OnRootWindowObjCreated(window); |
| |
| // This is a top level root window. |
| if (window->IsRootWindow() && !window->parent()) { |
| // On desktop aura there is one WindowTreeHost per top-level window. |
| aura::WindowTreeHost* window_tree_host = window->GetHost(); |
| if (window_tree_host) |
| ime_observation_.Observe(window_tree_host->GetInputMethod()); |
| } |
| } |
| |
| AXWindowObjWrapper::~AXWindowObjWrapper() = default; |
| |
| bool AXWindowObjWrapper::HandleAccessibleAction( |
| const ui::AXActionData& action) { |
| if (action.action == ax::mojom::Action::kFocus) { |
| window_->Focus(); |
| return true; |
| } |
| return false; |
| } |
| |
| AXAuraObjWrapper* AXWindowObjWrapper::GetParent() { |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| const std::string& window_id = GetPlatformWindowId(window_); |
| |
| // In Lacros, the presence of a platform window id means it is parented to the |
| // Ash tree via the app id. |
| if (!window_id.empty()) |
| return nullptr; |
| #endif // BUILDFLAG(IS_CHROMEOS_LACROS) |
| |
| aura::Window* parent = window_->parent(); |
| if (!parent) |
| return nullptr; |
| |
| if (parent->GetProperty(ui::kChildAXTreeID) && |
| ui::AXTreeID::FromString(*(parent->GetProperty(ui::kChildAXTreeID))) != |
| ui::AXTreeIDUnknown()) { |
| return nullptr; |
| } |
| |
| return aura_obj_cache_->GetOrCreate(parent); |
| } |
| |
| void AXWindowObjWrapper::GetChildren( |
| std::vector<AXAuraObjWrapper*>* out_children) { |
| // Ignore this window's descendants if it has a child tree. |
| if (window_->GetProperty(ui::kChildAXTreeID) && |
| ui::AXTreeID::FromString(*(window_->GetProperty(ui::kChildAXTreeID))) != |
| ui::AXTreeIDUnknown()) { |
| return; |
| } |
| |
| // Ignore this window's children if it is forced to be invisible. |
| if (window_->GetProperty(ui::kAXConsiderInvisibleAndIgnoreChildren)) |
| return; |
| |
| for (auto* child : window_->children()) |
| out_children->push_back(aura_obj_cache_->GetOrCreate(child)); |
| |
| // Also consider any associated widgets as children. |
| Widget* widget = GetWidgetForWindow(window_); |
| if (widget && widget->IsVisible()) |
| out_children->push_back(aura_obj_cache_->GetOrCreate(widget)); |
| } |
| |
| void AXWindowObjWrapper::Serialize(ui::AXNodeData* out_node_data) { |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| // This app id connects this node with a node in the Ash tree (an |
| // components/exo shell surface). |
| const std::string& window_id = GetPlatformWindowId(window_); |
| if (!window_id.empty()) |
| out_node_data->AddStringAttribute(ax::mojom::StringAttribute::kAppId, |
| window_id); |
| #endif // BUILDFLAG(IS_CHROMEOS_LACROS) |
| |
| if (window_->IsRootWindow() && !window_->parent() && window_->GetHost()) { |
| ui::TextInputClient* client = |
| window_->GetHost()->GetInputMethod()->GetTextInputClient(); |
| // Only set caret bounds if input caret is in an editable node. |
| if (client && client->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE) { |
| gfx::Rect caret_bounds_in_screen = client->GetCaretBounds(); |
| out_node_data->AddIntListAttribute( |
| ax::mojom::IntListAttribute::kCaretBounds, |
| {caret_bounds_in_screen.x(), caret_bounds_in_screen.y(), |
| caret_bounds_in_screen.width(), caret_bounds_in_screen.height()}); |
| } |
| } |
| |
| out_node_data->id = GetUniqueId(); |
| ax::mojom::Role role = window_->GetProperty(ui::kAXRoleOverride); |
| if (role != ax::mojom::Role::kNone) |
| out_node_data->role = role; |
| else |
| out_node_data->role = ax::mojom::Role::kWindow; |
| out_node_data->AddStringAttribute(ax::mojom::StringAttribute::kName, |
| base::UTF16ToUTF8(window_->GetTitle())); |
| if (!window_->IsVisible() || |
| window_->GetProperty(ui::kAXConsiderInvisibleAndIgnoreChildren)) { |
| out_node_data->AddState(ax::mojom::State::kInvisible); |
| } |
| |
| out_node_data->relative_bounds.bounds = |
| gfx::RectF(window_->GetBoundsInScreen()); |
| |
| out_node_data->AddStringAttribute(ax::mojom::StringAttribute::kClassName, |
| GetWindowName(window_)); |
| |
| std::string* child_ax_tree_id_ptr = window_->GetProperty(ui::kChildAXTreeID); |
| if (child_ax_tree_id_ptr) { |
| ui::AXTreeID child_ax_tree_id = |
| ui::AXTreeID::FromString(*child_ax_tree_id_ptr); |
| if (child_ax_tree_id != ui::AXTreeIDUnknown()) { |
| // Most often, child AX trees are parented to Views. We need to handle |
| // the case where they're not here, but we don't want the same AX tree |
| // to be a child of two different parents. |
| // |
| // To avoid this double-parenting, only add the child tree ID of this |
| // window if the top-level window doesn't have an associated Widget. |
| // |
| // Also, if this window is not visible, its child tree should also be |
| // non-visible so prune it. |
| if (!window_->GetToplevelWindow() || |
| GetWidgetForWindow(window_->GetToplevelWindow()) || |
| !window_->IsVisible() || |
| window_->GetProperty(ui::kAXConsiderInvisibleAndIgnoreChildren)) { |
| return; |
| } |
| |
| out_node_data->AddChildTreeId(child_ax_tree_id); |
| |
| const float scale_factor = |
| window_->GetToplevelWindow()->layer()->device_scale_factor(); |
| out_node_data->AddFloatAttribute( |
| ax::mojom::FloatAttribute::kChildTreeScale, scale_factor); |
| } |
| } |
| } |
| |
| ui::AXNodeID AXWindowObjWrapper::GetUniqueId() const { |
| return unique_id_.Get(); |
| } |
| |
| std::string AXWindowObjWrapper::ToString() const { |
| return GetWindowName(window_); |
| } |
| |
| void AXWindowObjWrapper::OnCaretBoundsChanged( |
| const ui::TextInputClient* client) { |
| FireEvent(ax::mojom::Event::kTreeChanged); |
| } |
| |
| void AXWindowObjWrapper::OnWindowDestroyed(aura::Window* window) { |
| if (is_root_window_) |
| aura_obj_cache_->OnRootWindowObjDestroyed(window_); |
| |
| aura_obj_cache_->Remove(window, nullptr); |
| } |
| |
| void AXWindowObjWrapper::OnWindowDestroying(aura::Window* window) { |
| DCHECK_EQ(window, window_); |
| window_destroying_ = true; |
| } |
| |
| void AXWindowObjWrapper::OnWindowHierarchyChanged( |
| const HierarchyChangeParams& params) { |
| if (params.phase == WindowObserver::HierarchyChangeParams::HIERARCHY_CHANGED) |
| aura_obj_cache_->Remove(params.target, params.old_parent); |
| } |
| |
| void AXWindowObjWrapper::OnWindowBoundsChanged( |
| aura::Window* window, |
| const gfx::Rect& old_bounds, |
| const gfx::Rect& new_bounds, |
| ui::PropertyChangeReason reason) { |
| if (window_destroying_) |
| return; |
| |
| if (window == window_) |
| FireLocationChangesRecursively(window_, aura_obj_cache_); |
| } |
| |
| void AXWindowObjWrapper::OnWindowPropertyChanged(aura::Window* window, |
| const void* key, |
| intptr_t old) { |
| if (window_destroying_ || window != window_) |
| return; |
| |
| if (key == ui::kChildAXTreeID || |
| key == ui::kAXConsiderInvisibleAndIgnoreChildren) { |
| FireEvent(ax::mojom::Event::kChildrenChanged); |
| } |
| } |
| |
| void AXWindowObjWrapper::OnWindowVisibilityChanged(aura::Window* window, |
| bool visible) { |
| if (window_destroying_) |
| return; |
| |
| FireEvent(ax::mojom::Event::kStateChanged); |
| } |
| |
| void AXWindowObjWrapper::OnWindowTransformed(aura::Window* window, |
| ui::PropertyChangeReason reason) { |
| if (window_destroying_) |
| return; |
| |
| if (window == window_) |
| FireLocationChangesRecursively(window_, aura_obj_cache_); |
| } |
| |
| void AXWindowObjWrapper::OnWindowTitleChanged(aura::Window* window) { |
| if (window_destroying_) |
| return; |
| |
| FireEventOnWindowChildWidgetAndRootView( |
| window_, ax::mojom::Event::kTreeChanged, aura_obj_cache_); |
| } |
| |
| void AXWindowObjWrapper::FireEvent(ax::mojom::Event event_type) { |
| aura_obj_cache_->FireEvent(aura_obj_cache_->GetOrCreate(window_), event_type); |
| } |
| |
| } // namespace views |