Commit 94f13968 authored by Martin Robinson's avatar Martin Robinson Committed by Commit Bot

Add ATK window activation events for popups

In the accessibility tree, each popup menu and its submenus have a
native window. When using popup menus, the AtkWindow for the native
window of the active menu should always have the ATK 'active' property.
We listen to events associated with the lifetime active popup menus and
use them to properly manage the 'active' property on this constellation
of AtkWindows.

Bug: 895921
Bug: 896392
Change-Id: I7a5e148feb8486bab7984f19c7ee2532b1fd16f0
Reviewed-on: https://chromium-review.googlesource.com/c/1335586
Commit-Queue: Martin Robinson <mrobinson@igalia.com>
Reviewed-by: default avatarScott Violet <sky@chromium.org>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarDaniel Cheng <dcheng@chromium.org>
Cr-Commit-Position: refs/heads/master@{#612770}
parent aa3d28f3
......@@ -43,6 +43,7 @@
menuListItemSelected,
menuListValueChanged,
menuPopupEnd,
menuPopupHide,
menuPopupStart,
menuStart,
mouseCanceled,
......
......@@ -82,6 +82,8 @@ api::automation::EventType ToAutomationEvent(ax::mojom::Event event_type) {
return api::automation::EVENT_TYPE_MENULISTVALUECHANGED;
case ax::mojom::Event::kMenuPopupEnd:
return api::automation::EVENT_TYPE_MENUPOPUPEND;
case ax::mojom::Event::kMenuPopupHide:
return api::automation::EVENT_TYPE_MENUPOPUPHIDE;
case ax::mojom::Event::kMenuPopupStart:
return api::automation::EVENT_TYPE_MENUPOPUPSTART;
case ax::mojom::Event::kMenuStart:
......@@ -401,6 +403,7 @@ bool AutomationAXTreeWrapper::IsEventTypeHandledByAXEventGenerator(
case api::automation::EVENT_TYPE_LAYOUTCOMPLETE:
case api::automation::EVENT_TYPE_MENULISTVALUECHANGED:
case api::automation::EVENT_TYPE_MENUPOPUPEND:
case api::automation::EVENT_TYPE_MENUPOPUPHIDE:
case api::automation::EVENT_TYPE_MENUPOPUPSTART:
case api::automation::EVENT_TYPE_SELECTIONADD:
case api::automation::EVENT_TYPE_SELECTIONREMOVE:
......
......@@ -43,6 +43,7 @@
menuListItemSelected,
menuListValueChanged,
menuPopupEnd,
menuPopupHide,
menuPopupStart,
menuStart,
mouseCanceled,
......
......@@ -83,6 +83,8 @@ api::automation::EventType ToAutomationEvent(ax::mojom::Event event_type) {
return api::automation::EVENT_TYPE_MENULISTVALUECHANGED;
case ax::mojom::Event::kMenuPopupEnd:
return api::automation::EVENT_TYPE_MENUPOPUPEND;
case ax::mojom::Event::kMenuPopupHide:
return api::automation::EVENT_TYPE_MENUPOPUPHIDE;
case ax::mojom::Event::kMenuPopupStart:
return api::automation::EVENT_TYPE_MENUPOPUPSTART;
case ax::mojom::Event::kMenuStart:
......@@ -402,6 +404,7 @@ bool AutomationAXTreeWrapper::IsEventTypeHandledByAXEventGenerator(
case api::automation::EVENT_TYPE_LAYOUTCOMPLETE:
case api::automation::EVENT_TYPE_MENULISTVALUECHANGED:
case api::automation::EVENT_TYPE_MENUPOPUPEND:
case api::automation::EVENT_TYPE_MENUPOPUPHIDE:
case api::automation::EVENT_TYPE_MENUPOPUPSTART:
case api::automation::EVENT_TYPE_SELECTIONADD:
case api::automation::EVENT_TYPE_SELECTIONREMOVE:
......
......@@ -51,6 +51,7 @@ chrome.automation.EventType = {
MENU_LIST_ITEM_SELECTED: 'menuListItemSelected',
MENU_LIST_VALUE_CHANGED: 'menuListValueChanged',
MENU_POPUP_END: 'menuPopupEnd',
MENU_POPUP_HIDE: 'menuPopupHide',
MENU_POPUP_START: 'menuPopupStart',
MENU_START: 'menuStart',
MOUSE_CANCELED: 'mouseCanceled',
......
......@@ -70,6 +70,8 @@ const char* ToString(ax::mojom::Event event) {
return "menuListValueChanged";
case ax::mojom::Event::kMenuPopupEnd:
return "menuPopupEnd";
case ax::mojom::Event::kMenuPopupHide:
return "menuPopupHide";
case ax::mojom::Event::kMenuPopupStart:
return "menuPopupStart";
case ax::mojom::Event::kMenuStart:
......@@ -184,6 +186,8 @@ ax::mojom::Event ParseEvent(const char* event) {
return ax::mojom::Event::kMenuListValueChanged;
if (0 == strcmp(event, "menuPopupEnd"))
return ax::mojom::Event::kMenuPopupEnd;
if (0 == strcmp(event, "menuPopupHide"))
return ax::mojom::Event::kMenuPopupHide;
if (0 == strcmp(event, "menuPopupStart"))
return ax::mojom::Event::kMenuPopupStart;
if (0 == strcmp(event, "menuStart"))
......
......@@ -56,8 +56,9 @@ enum Event {
kMenuEnd, // Native / Win
kMenuListItemSelected, // Web
kMenuListValueChanged, // Web
kMenuPopupEnd, // Native / Win
kMenuPopupStart, // Native / Win
kMenuPopupEnd, // Native
kMenuPopupHide, // Native / AuraLinux
kMenuPopupStart, // Native
kMenuStart, // Native / Win
kMouseCanceled,
kMouseDragged,
......
......@@ -6,11 +6,14 @@
#include <stdint.h>
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "base/command_line.h"
#include "base/no_destructor.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
......@@ -358,15 +361,6 @@ static gfx::Point FindAtkObjectParentCoords(AtkObject* atk_object) {
return FindAtkObjectParentCoords(atk_object);
}
static AtkObject* FindAtkObjectParentFrame(AtkObject* atk_object) {
while (atk_object) {
if (atk_object_get_role(atk_object) == ATK_ROLE_FRAME)
return atk_object;
atk_object = atk_object_get_parent(atk_object);
}
return nullptr;
}
static void AXPlatformNodeAuraLinuxGetExtents(AtkComponent* atk_component,
gint* x,
gint* y,
......@@ -1068,6 +1062,32 @@ AXPlatformNodeAuraLinux* g_current_selected = nullptr;
// null if if the AtkObject is destroyed.
AtkObject* g_active_top_level_frame = nullptr;
static AtkObject* FindAtkObjectParentFrame(AtkObject* atk_object) {
while (atk_object) {
if (atk_object_get_role(atk_object) == ATK_ROLE_FRAME)
return atk_object;
atk_object = atk_object_get_parent(atk_object);
}
return nullptr;
}
// Returns a stack of AtkObjects of activated popup menus. Since each popup
// menu and submenu has its own native window, we want to properly manage the
// activated state for their containing frames.
static std::vector<AtkObject*>& GetActiveMenus() {
static base::NoDestructor<std::vector<AtkObject*>> active_menus;
return *active_menus;
}
// The currently active frame is g_active_top_level_frame, unless there is an
// active menu. If there is an active menu the parent frame of the
// most-recently opened active menu should be the currently active frame.
AtkObject* ComputeActiveTopLevelFrame() {
if (!GetActiveMenus().empty())
return FindAtkObjectParentFrame(GetActiveMenus().back());
return g_active_top_level_frame;
}
const char* GetUniqueAccessibilityGTypeName(int interface_mask) {
// 37 characters is enough for "AXPlatformNodeAuraLinux%x" with any integer
// value.
......@@ -1646,8 +1666,14 @@ AtkRole AXPlatformNodeAuraLinux::GetAtkRole() {
void AXPlatformNodeAuraLinux::GetAtkState(AtkStateSet* atk_state_set) {
AXNodeData data = GetData();
if (atk_object_ == g_active_top_level_frame)
bool menu_active = !GetActiveMenus().empty();
if (!menu_active && atk_object_ == g_active_top_level_frame)
atk_state_set_add_state(atk_state_set, ATK_STATE_ACTIVE);
if (menu_active &&
FindAtkObjectParentFrame(GetActiveMenus().back()) == atk_object_)
atk_state_set_add_state(atk_state_set, ATK_STATE_ACTIVE);
if (data.HasState(ax::mojom::State::kCollapsed))
atk_state_set_add_state(atk_state_set, ATK_STATE_EXPANDABLE);
if (data.HasState(ax::mojom::State::kDefault))
......@@ -1831,6 +1857,93 @@ void AXPlatformNodeAuraLinux::OnExpandedStateChanged(bool is_expanded) {
is_expanded);
}
void AXPlatformNodeAuraLinux::OnMenuPopupStart() {
AtkObject* parent_frame = FindAtkObjectParentFrame(atk_object_);
if (!parent_frame)
return;
// Exit early if kMenuPopupStart is sent multiple times for the same menu.
std::vector<AtkObject*>& active_menus = GetActiveMenus();
bool menu_already_open = !active_menus.empty();
if (menu_already_open && active_menus.back() == atk_object_)
return;
// We also want to inform the AT that menu the is now showing. Normally this
// event is not fired because the menu will be created with the
// ATK_STATE_SHOWING already set to TRUE.
atk_object_notify_state_change(atk_object_, ATK_STATE_SHOWING, TRUE);
// We need to compute this before modifying the active menu stack.
AtkObject* previous_active_frame = ComputeActiveTopLevelFrame();
active_menus.push_back(atk_object_);
// We exit early if the newly activated menu has the same AtkWindow as the
// previous one.
if (previous_active_frame == parent_frame)
return;
if (previous_active_frame) {
g_signal_emit_by_name(previous_active_frame, "deactivate");
atk_object_notify_state_change(previous_active_frame, ATK_STATE_ACTIVE,
FALSE);
}
g_signal_emit_by_name(parent_frame, "activate");
atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, TRUE);
}
void AXPlatformNodeAuraLinux::OnMenuPopupHide() {
AtkObject* parent_frame = FindAtkObjectParentFrame(atk_object_);
if (!parent_frame)
return;
atk_object_notify_state_change(atk_object_, ATK_STATE_SHOWING, FALSE);
// kMenuPopupHide may be called multiple times for the same menu, so only
// remove it if our parent frame matches the most recently opened menu.
std::vector<AtkObject*>& active_menus = GetActiveMenus();
if (active_menus.empty())
return;
// When multiple levels of menu are closed at once, they may be hidden out
// of order. When this happens, we just remove the open menu from the stack.
if (active_menus.back() != atk_object_) {
auto it =
std::find(active_menus.rbegin(), active_menus.rend(), atk_object_);
if (it != active_menus.rend()) {
// We used a reverse iterator, so we need to convert it into a normal
// iterator to use it for std::vector::erase(...).
auto to_remove = --(it.base());
active_menus.erase(to_remove);
}
return;
}
active_menus.pop_back();
// We exit early if the newly activated menu has the same AtkWindow as the
// previous one.
AtkObject* new_active_item = ComputeActiveTopLevelFrame();
if (new_active_item == parent_frame)
return;
g_signal_emit_by_name(parent_frame, "deactivate");
atk_object_notify_state_change(parent_frame, ATK_STATE_ACTIVE, FALSE);
if (new_active_item) {
g_signal_emit_by_name(new_active_item, "activate");
atk_object_notify_state_change(new_active_item, ATK_STATE_ACTIVE, TRUE);
}
}
void AXPlatformNodeAuraLinux::OnMenuPopupEnd() {
if (!GetActiveMenus().empty() && g_active_top_level_frame &&
ComputeActiveTopLevelFrame() != g_active_top_level_frame) {
g_signal_emit_by_name(g_active_top_level_frame, "activate");
atk_object_notify_state_change(g_active_top_level_frame, ATK_STATE_ACTIVE,
TRUE);
}
GetActiveMenus().clear();
}
void AXPlatformNodeAuraLinux::OnWindowActivated() {
AtkObject* parent_frame = FindAtkObjectParentFrame(atk_object_);
if (!parent_frame || parent_frame == g_active_top_level_frame)
......@@ -1945,6 +2058,21 @@ void AXPlatformNodeAuraLinux::OnValueChanged() {
void AXPlatformNodeAuraLinux::NotifyAccessibilityEvent(
ax::mojom::Event event_type) {
switch (event_type) {
// There are three types of messages that we receive for popup menus. Each
// time a popup menu is shown, we get a kMenuPopupStart message. This
// includes if the menu is hidden and then re-shown. When a menu is hidden
// we receive the kMenuPopupHide message. Finally, when the entire menu is
// closed we receive the kMenuPopupEnd message for the parent menu and all
// of the submenus that were opened when navigating through the menu.
case ax::mojom::Event::kMenuPopupEnd:
OnMenuPopupEnd();
break;
case ax::mojom::Event::kMenuPopupHide:
OnMenuPopupHide();
break;
case ax::mojom::Event::kMenuPopupStart:
OnMenuPopupStart();
break;
case ax::mojom::Event::kCheckedStateChanged:
OnCheckedStateChanged();
break;
......
......@@ -83,6 +83,9 @@ class AX_EXPORT AXPlatformNodeAuraLinux : public AXPlatformNodeBase {
void OnFocused();
void OnWindowActivated();
void OnWindowDeactivated();
void OnMenuPopupStart();
void OnMenuPopupHide();
void OnMenuPopupEnd();
void OnSelected();
void OnValueChanged();
......
......@@ -8,6 +8,8 @@
#include <atk/atk.h>
#include <utility>
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/accessibility/platform/ax_platform_node_auralinux.h"
#include "ui/accessibility/platform/ax_platform_node_unittest.h"
......@@ -23,14 +25,24 @@ class AXPlatformNodeAuraLinuxTest : public AXPlatformNodeTest {
void SetUp() override {}
protected:
AtkObject* AtkObjectFromNode(AXNode* node) {
AXPlatformNodeAuraLinux* GetPlatformNode(AXNode* node) {
TestAXNodeWrapper* wrapper =
TestAXNodeWrapper::GetOrCreate(tree_.get(), node);
if (!wrapper)
return nullptr;
AXPlatformNode* ax_platform_node = wrapper->ax_platform_node();
AtkObject* atk_object = ax_platform_node->GetNativeViewAccessible();
return atk_object;
return static_cast<AXPlatformNodeAuraLinux*>(wrapper->ax_platform_node());
}
AXPlatformNodeAuraLinux* GetRootPlatformNode() {
return GetPlatformNode(GetRootNode());
}
AtkObject* AtkObjectFromNode(AXNode* node) {
if (AXPlatformNode* ax_platform_node = GetPlatformNode(node)) {
return ax_platform_node->GetNativeViewAccessible();
} else {
return nullptr;
}
}
TestAXNodeWrapper* GetRootWrapper() {
......@@ -38,14 +50,6 @@ class AXPlatformNodeAuraLinuxTest : public AXPlatformNodeTest {
}
AtkObject* GetRootAtkObject() { return AtkObjectFromNode(GetRootNode()); }
AXPlatformNodeAuraLinux* GetRootPlatformNode() {
TestAXNodeWrapper* wrapper = GetRootWrapper();
if (!wrapper)
return nullptr;
AXPlatformNode* ax_platform_node = wrapper->ax_platform_node();
return static_cast<AXPlatformNodeAuraLinux*>(ax_platform_node);
}
};
static void EnsureAtkObjectHasAttributeWithValue(
......@@ -956,6 +960,46 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkTextCharacterGranularity) {
g_object_unref(root_obj);
}
class ActivationTester {
public:
explicit ActivationTester(AtkObject* target) : target_(target) {
auto callback = G_CALLBACK(+[](AtkWindow*, bool* flag) { *flag = true; });
activate_id_ =
g_signal_connect(target, "activate", callback, &saw_activate_);
deactivate_id_ =
g_signal_connect(target, "deactivate", callback, &saw_deactivate_);
DCHECK(activate_id_);
DCHECK(deactivate_id_);
DCHECK(activate_id_ != deactivate_id_);
}
bool IsActivatedInStateSet() {
AtkStateSet* state_set = atk_object_ref_state_set(target_);
EXPECT_TRUE(ATK_IS_STATE_SET(state_set));
bool in_state_set =
atk_state_set_contains_state(state_set, ATK_STATE_ACTIVE);
g_object_unref(state_set);
return in_state_set;
}
void Reset() {
saw_activate_ = false;
saw_deactivate_ = false;
}
virtual ~ActivationTester() {
g_signal_handler_disconnect(target_, activate_id_);
g_signal_handler_disconnect(target_, deactivate_id_);
}
AtkObject* target_;
bool saw_activate_ = false;
bool saw_deactivate_ = false;
gulong activate_id_ = 0;
gulong deactivate_id_ = 0;
};
//
// AtkWindow interface and active state
//
......@@ -972,40 +1016,114 @@ TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkWindowActive) {
EXPECT_TRUE(ATK_IS_WINDOW(root_atk_object));
bool saw_activate = false;
bool saw_deactivate = false;
{
ActivationTester tester(root_atk_object);
EXPECT_FALSE(tester.IsActivatedInStateSet());
static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
->NotifyAccessibilityEvent(ax::mojom::Event::kWindowActivated);
EXPECT_TRUE(tester.saw_activate_);
EXPECT_FALSE(tester.saw_deactivate_);
EXPECT_TRUE(tester.IsActivatedInStateSet());
}
auto callback = G_CALLBACK(+[](AtkWindow*, bool* flag) { *flag = true; });
g_signal_connect(root_atk_object, "activate", callback, &saw_activate);
g_signal_connect(root_atk_object, "deactivate", callback, &saw_deactivate);
{
ActivationTester tester(root_atk_object);
static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
->NotifyAccessibilityEvent(ax::mojom::Event::kWindowDeactivated);
EXPECT_FALSE(tester.saw_activate_);
EXPECT_TRUE(tester.saw_deactivate_);
EXPECT_FALSE(tester.IsActivatedInStateSet());
}
AtkStateSet* state_set = atk_object_ref_state_set(root_atk_object);
EXPECT_TRUE(ATK_IS_STATE_SET(state_set));
EXPECT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_ACTIVE));
g_object_unref(state_set);
g_object_unref(root_atk_object);
}
static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
TEST_F(AXPlatformNodeAuraLinuxTest, TestAtkPopupWindowActive) {
AXNodeData root;
root.id = 1;
root.role = ax::mojom::Role::kApplication;
root.child_ids.push_back(2);
root.child_ids.push_back(3);
AXNodeData window_node_data;
window_node_data.id = 2;
window_node_data.role = ax::mojom::Role::kWindow;
AXNodeData menu_node_data;
menu_node_data.id = 3;
menu_node_data.role = ax::mojom::Role::kWindow;
menu_node_data.child_ids.push_back(4);
AXNodeData menu_item_data;
menu_item_data.id = 4;
Init(root, window_node_data, menu_node_data, menu_item_data);
AtkObject* root_atk_object(GetRootAtkObject());
EXPECT_TRUE(ATK_IS_OBJECT(root_atk_object));
g_object_ref(root_atk_object);
AXNode* window_node = GetRootNode()->children()[0];
AtkObject* window_atk_node(AtkObjectFromNode(window_node));
ActivationTester toplevel_tester(window_atk_node);
GetPlatformNode(window_node)
->NotifyAccessibilityEvent(ax::mojom::Event::kWindowActivated);
EXPECT_TRUE(saw_activate);
EXPECT_FALSE(saw_deactivate);
EXPECT_TRUE(toplevel_tester.saw_activate_);
EXPECT_FALSE(toplevel_tester.saw_deactivate_);
EXPECT_TRUE(toplevel_tester.IsActivatedInStateSet());
state_set = atk_object_ref_state_set(root_atk_object);
EXPECT_TRUE(ATK_IS_STATE_SET(state_set));
EXPECT_TRUE(atk_state_set_contains_state(state_set, ATK_STATE_ACTIVE));
g_object_unref(state_set);
toplevel_tester.Reset();
saw_activate = false;
saw_deactivate = false;
AXNode* menu_node = GetRootNode()->children()[1];
AtkObject* menu_atk_node(AtkObjectFromNode(menu_node));
{
ActivationTester tester(menu_atk_node);
GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuPopupStart);
EXPECT_TRUE(tester.saw_activate_);
EXPECT_FALSE(tester.saw_deactivate_);
EXPECT_TRUE(tester.IsActivatedInStateSet());
}
static_cast<AXPlatformNodeAuraLinux*>(GetRootPlatformNode())
->NotifyAccessibilityEvent(ax::mojom::Event::kWindowDeactivated);
EXPECT_FALSE(saw_activate);
EXPECT_TRUE(saw_deactivate);
EXPECT_FALSE(toplevel_tester.saw_activate_);
EXPECT_TRUE(toplevel_tester.saw_deactivate_);
state_set = atk_object_ref_state_set(root_atk_object);
EXPECT_TRUE(ATK_IS_STATE_SET(state_set));
EXPECT_FALSE(atk_state_set_contains_state(state_set, ATK_STATE_ACTIVE));
g_object_unref(state_set);
toplevel_tester.Reset();
{
ActivationTester tester(menu_atk_node);
GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuPopupHide);
EXPECT_FALSE(tester.saw_activate_);
EXPECT_TRUE(tester.saw_deactivate_);
EXPECT_FALSE(tester.IsActivatedInStateSet());
}
{
ActivationTester tester(menu_atk_node);
GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuPopupEnd);
EXPECT_FALSE(tester.saw_activate_);
EXPECT_FALSE(tester.saw_deactivate_);
EXPECT_FALSE(tester.IsActivatedInStateSet());
}
// Now that the menu is definitively closed, activation should have returned
// to the previously activated toplevel frame.
EXPECT_TRUE(toplevel_tester.saw_activate_);
EXPECT_FALSE(toplevel_tester.saw_deactivate_);
// No we test opening the menu and closing it without hiding any submenus. The
// toplevel should lose and then regain focus.
toplevel_tester.Reset();
GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuPopupStart);
GetPlatformNode(menu_node)->NotifyAccessibilityEvent(
ax::mojom::Event::kMenuPopupEnd);
EXPECT_TRUE(toplevel_tester.saw_activate_);
EXPECT_TRUE(toplevel_tester.saw_deactivate_);
g_object_unref(root_atk_object);
}
......
......@@ -428,8 +428,11 @@ void SubmenuView::Close() {
}
void SubmenuView::Hide() {
if (host_)
if (host_) {
host_->HideMenuHost();
NotifyAccessibilityEvent(ax::mojom::Event::kMenuPopupHide, true);
}
if (scroll_animator_->is_scrolling())
scroll_animator_->Stop();
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment