Emit AccesibilityState change event to AT-SPI bridge on state property set 62/316562/3
authorYoungsun Suh <youngsun.suh@samsung.com>
Thu, 22 Aug 2024 05:52:23 +0000 (14:52 +0900)
committerYoungsun Suh <youngsun.suh@samsung.com>
Fri, 23 Aug 2024 03:42:59 +0000 (12:42 +0900)
Change-Id: If63e953db090b700d24da865905542092f4d2fa9

automated-tests/src/dali-toolkit-internal/dali-toolkit-test-utils/accessibility-test-utils.cpp
automated-tests/src/dali-toolkit-internal/dali-toolkit-test-utils/accessibility-test-utils.h
automated-tests/src/dali-toolkit-internal/utc-Dali-Accessibility-Controls-BridgeUp.cpp
dali-toolkit/devel-api/controls/control-accessible.cpp
dali-toolkit/devel-api/controls/control-accessible.h
dali-toolkit/devel-api/controls/control-devel.h
dali-toolkit/internal/controls/control/control-data-impl.cpp

index 9989d4e2bd2777942c1521d3ff9f87f73547636e..98c29513b914171eefe41f9e7df4e18b4e1371a3 100644 (file)
@@ -15,12 +15,20 @@ using MessagePtr = DBusWrapper::MessagePtr;
 static bool gMoveOutedCalled      = false;
 static bool gPropertyChangeCalled = false;
 
+struct StateChangedResult
+{
+  std::string state{};
+  int         value{-1};
+};
+static StateChangedResult gStateChangedResult{};
+
 void TestEnableSC(bool b)
 {
   static bool firstTime = true;
   if(b && firstTime)
   {
     gPropertyChangeCalled  = false;
+    gStateChangedResult    = {};
     firstTime              = false;
     auto        bridge     = Accessibility::Bridge::GetCurrentBridge();
     Dali::Stage stage      = Dali::Stage::GetCurrent();
@@ -65,6 +73,11 @@ void TestEnableSC(bool b)
     };
     wr->testMethods[std::tuple<std::string, std::string, std::string, MethodType>{"/org/a11y/atspi/accessible", "org.a11y.atspi.Event.Object", "StateChanged", MethodType::Method}] =
       [wr](const MessagePtr& m) -> MessagePtr {
+      std::tuple<std::string, int> decoded;
+      wr->Decode(m, decoded);
+      gStateChangedResult.state = std::get<0>(decoded);
+      gStateChangedResult.value = std::get<1>(decoded);
+
       return wr->newReplyMessage(m);
     };
     wr->testMethods[std::tuple<std::string, std::string, std::string, MethodType>{"/org/a11y/atspi/accessible", "org.a11y.atspi.Event.Object", "BoundsChanged", MethodType::Method}] =
@@ -307,6 +320,21 @@ bool TestPropertyChangeCalled()
   return gPropertyChangeCalled;
 }
 
+bool TestStateChangedCalled()
+{
+  return !gStateChangedResult.state.empty();
+}
+
+bool TestStateChangedResult(const std::string_view& expectedState, int expectedValue)
+{
+  return expectedState == gStateChangedResult.state && expectedValue == gStateChangedResult.value;
+}
+
+void TestResetStateChangedResult()
+{
+  gStateChangedResult = {};
+}
+
 void PrintTree(const Address& root, size_t depth)
 {
   auto name = TestGetName(root);
index 826ce25fe79a5e45e7f61bb67f8bfc4dd406dcc3..c00ec49b4fa9cec07a74d1382522396709621f2b 100644 (file)
@@ -40,6 +40,9 @@ std::string                                                                    T
 void                                                                           TestResetMoveOutedCalled();
 bool                                                                           TestGetMoveOutedCalled();
 bool                                                                           TestPropertyChangeCalled();
+bool                                                                           TestStateChangedCalled();
+bool                                                                           TestStateChangedResult(const std::string_view& expectedState, int expectedValue);
+void                                                                           TestResetStateChangedResult();
 } // namespace Accessibility
 } // namespace Dali
 
index ef299e48b12da6806aff924008dec21faecb01f7..92d41d33a59a72954964693bf21d1dc9ffff3d8f 100644 (file)
@@ -1,5 +1,6 @@
 #include <automated-tests/src/dali-toolkit-internal/dali-toolkit-test-utils/accessibility-test-utils.h>
 #include <automated-tests/src/dali-toolkit-internal/dali-toolkit-test-utils/dbus-wrapper.h>
+#include <automated-tests/src/dali-toolkit/dali-toolkit-test-utils/toolkit-timer.h>
 #include <dali-toolkit-test-suite-utils.h>
 #include <dali-toolkit/dali-toolkit.h>
 #include <dali-toolkit/devel-api/controls/buttons/toggle-button.h>
@@ -397,20 +398,37 @@ int UtcDaliControlAccessibilityState(void)
 {
   ToolkitTestApplication application;
 
-  auto control    = Control::New();
+  auto control = Control::New();
+  control.SetProperty(Actor::Property::SIZE, Vector2(100, 100));
+  control.SetProperty(Actor::Property::POSITION, Vector3(10, 10, 100));
+
+  application.GetScene().Add(control);
   auto accessible = Dali::Accessibility::Accessible::Get(control);
 
+  const auto flushCoalescableMessage = [&]() {
+    Dali::Timer timer = Timer::New(0);
+    for(int i = 0; i < 11; ++i)
+    {
+      application.SendNotification();
+      application.Render();
+      timer.MockEmitSignal();
+    }
+  };
+
   Dali::Accessibility::TestEnableSC(true);
+  DALI_TEST_CHECK(!Dali::Accessibility::TestStateChangedCalled());
 
-  // Test AccessibilityState property
+  // Test setting AccessibilityState property updates at-spi states
+  DevelControl::AccessibilityStates inputStates;
   {
-    DevelControl::AccessibilityStates inputStates;
     inputStates[DevelControl::AccessibilityState::ENABLED] = false;
     inputStates[DevelControl::AccessibilityState::CHECKED] = true;
     inputStates[DevelControl::AccessibilityState::BUSY]    = true;
 
     control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
 
+    DALI_TEST_CHECK(!Dali::Accessibility::TestStateChangedCalled());
+
     auto states = DevelControl::GetAccessibilityStates(control);
     DALI_TEST_CHECK(!states[Dali::Accessibility::State::ENABLED]);
     DALI_TEST_CHECK(!states[Dali::Accessibility::State::SELECTED]);
@@ -419,6 +437,122 @@ int UtcDaliControlAccessibilityState(void)
     DALI_TEST_CHECK(!states[Dali::Accessibility::State::EXPANDED]);
   }
 
+  // state-changed:checked event is NOT emitted if the object is not highlighted
+  {
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_ROLE, DevelControl::AccessibilityRole::CHECK_BOX);
+
+    inputStates[DevelControl::AccessibilityState::CHECKED] = false; // CHECKED: true -> false
+
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(!Dali::Accessibility::TestStateChangedCalled());
+
+    auto states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::ENABLED]);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::SELECTED]);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::CHECKED]);
+    DALI_TEST_CHECK(states[Dali::Accessibility::State::BUSY]);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::EXPANDED]);
+  }
+
+  auto component = dynamic_cast<Dali::Accessibility::Component*>(accessible);
+  component->GrabHighlight();
+
+  // state-changed:checked event is emitted if the object is highlighted and checkable
+  const std::array<DevelControl::AccessibilityRole, 3> checkableRoles{DevelControl::AccessibilityRole::CHECK_BOX, DevelControl::AccessibilityRole::RADIO_BUTTON, DevelControl::AccessibilityRole::TOGGLE_BUTTON};
+  for(auto role : checkableRoles)
+  {
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_ROLE, role);
+
+    // CHECKED: false -> true
+    inputStates[DevelControl::AccessibilityState::CHECKED] = true;
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedCalled());
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedResult("checked", 1));
+
+    auto states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(states[Dali::Accessibility::State::CHECKED]);
+
+    Dali::Accessibility::TestResetStateChangedResult();
+    flushCoalescableMessage();
+
+    // CHECKED: true -> false
+    inputStates[DevelControl::AccessibilityState::CHECKED] = false;
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedCalled());
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedResult("checked", 0));
+
+    states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::CHECKED]);
+
+    Dali::Accessibility::TestResetStateChangedResult();
+    flushCoalescableMessage();
+  }
+
+  // state-changed:selected event is emitted if the object is highlighted and selectable
+  const std::array<DevelControl::AccessibilityRole, 3> selectableRoles{DevelControl::AccessibilityRole::BUTTON, DevelControl::AccessibilityRole::LIST_ITEM, DevelControl::AccessibilityRole::MENU_ITEM};
+  for(auto role : selectableRoles)
+  {
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_ROLE, role);
+
+    // SELECTED: false -> true
+    inputStates[DevelControl::AccessibilityState::SELECTED] = true;
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedCalled());
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedResult("selected", 1));
+
+    auto states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(states[Dali::Accessibility::State::SELECTED]);
+
+    Dali::Accessibility::TestResetStateChangedResult();
+    flushCoalescableMessage();
+
+    // SELECTED: true -> false
+    inputStates[DevelControl::AccessibilityState::SELECTED] = false;
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedCalled());
+    DALI_TEST_CHECK(Dali::Accessibility::TestStateChangedResult("selected", 0));
+
+    states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::SELECTED]);
+    Dali::Accessibility::TestResetStateChangedResult();
+    flushCoalescableMessage();
+  }
+
+  // state-changed event is NOT emitted if object is not checkable or selectable
+  {
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_ROLE, DevelControl::AccessibilityRole::CONTAINER);
+
+    inputStates[DevelControl::AccessibilityState::CHECKED]  = true; // CHECKED: false -> true
+    inputStates[DevelControl::AccessibilityState::SELECTED] = true; // SELECTED: false -> true
+
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(!Dali::Accessibility::TestStateChangedCalled());
+
+    auto states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(states[Dali::Accessibility::State::SELECTED]);
+    DALI_TEST_CHECK(states[Dali::Accessibility::State::CHECKED]);
+  }
+
+  // state-changed event is NOT emitted if object is v1 role
+  {
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_ROLE, Dali::Accessibility::Role::CHECK_BOX);
+
+    inputStates[DevelControl::AccessibilityState::CHECKED] = false; // CHECKED: true -> false
+
+    control.SetProperty(DevelControl::Property::ACCESSIBILITY_STATES, static_cast<int32_t>(inputStates.GetRawData32()));
+
+    DALI_TEST_CHECK(!Dali::Accessibility::TestStateChangedCalled());
+
+    auto states = DevelControl::GetAccessibilityStates(control);
+    DALI_TEST_CHECK(!states[Dali::Accessibility::State::CHECKED]);
+  }
+
   // Test bridge behavior
   {
     auto states_by_bridge = Dali::Accessibility::States{TestGetStates(accessible->GetAddress())};
index 0f29c6def4399e6830220d799025d225b0bf8ed9..cc13b19cb61949185799c9e80f3d91d477d5ff67 100644 (file)
@@ -469,6 +469,8 @@ void ControlAccessible::RegisterPropertySetSignal()
   Internal::Control&       internalControl = Toolkit::Internal::GetImplementation(control);
   Internal::Control::Impl& controlImpl     = Internal::Control::Impl::Get(internalControl);
   controlImpl.RegisterAccessibilityPropertySetSignal();
+
+  mStatesSnapshot = controlImpl.mAccessibilityProps.states;
 }
 
 void ControlAccessible::UnregisterPropertySetSignal()
@@ -477,6 +479,8 @@ void ControlAccessible::UnregisterPropertySetSignal()
   Internal::Control&       internalControl = Toolkit::Internal::GetImplementation(control);
   Internal::Control::Impl& controlImpl     = Internal::Control::Impl::Get(internalControl);
   controlImpl.UnregisterAccessibilityPropertySetSignal();
+
+  mStatesSnapshot = {};
 }
 
 bool ControlAccessible::GrabHighlight()
@@ -663,4 +667,31 @@ Vector2 ControlAccessible::GetLastPosition() const
   return mLastPosition;
 }
 
+void ControlAccessible::OnStatePropertySet(AccessibilityStates newStates)
+{
+  int32_t rawRole = Self().GetProperty<int32_t>(Property::ACCESSIBILITY_ROLE);
+  if(IsRoleV2(rawRole))
+  {
+    AccessibilityRole role = static_cast<AccessibilityRole>(rawRole);
+
+    if(newStates[AccessibilityState::CHECKED] != mStatesSnapshot[AccessibilityState::CHECKED] &&
+       (role == AccessibilityRole::CHECK_BOX || role == AccessibilityRole::RADIO_BUTTON || role == AccessibilityRole::TOGGLE_BUTTON))
+    {
+      EmitStateChanged(Accessibility::State::CHECKED, newStates[AccessibilityState::CHECKED]);
+    }
+
+    if(newStates[AccessibilityState::SELECTED] != mStatesSnapshot[AccessibilityState::SELECTED] &&
+       (role == AccessibilityRole::BUTTON || role == AccessibilityRole::LIST_ITEM || role == AccessibilityRole::MENU_ITEM))
+    {
+      EmitStateChanged(Accessibility::State::SELECTED, newStates[AccessibilityState::SELECTED]);
+    }
+  }
+  else
+  {
+    DALI_LOG_INFO(gLogFilter, Debug::Verbose, "With V1 role, state change events are emitted manually by the app component.");
+  }
+
+  mStatesSnapshot = newStates;
+}
+
 } // namespace Dali::Toolkit::DevelControl
index 4f96998fede262cb6c33847eb19719f8b90cb0a0..c3f955162e7125307dcbdcf6d42711d72806cad5 100644 (file)
 
 namespace Dali::Toolkit::DevelControl
 {
+/**
+ * @brief Represents current state of a control.
+ */
+enum class AccessibilityState : uint32_t
+{
+  ENABLED = 0,
+  SELECTED,
+  CHECKED,
+  BUSY,
+  EXPANDED,
+  MAX_COUNT
+};
+using AccessibilityStates = Accessibility::EnumBitSet<AccessibilityState, AccessibilityState::MAX_COUNT>;
+
+constexpr const uint32_t ROLE_START_INDEX = 200;
+/**
+ * @brief AccessibilityRole represents the purpose of a control.
+ */
+enum class AccessibilityRole : uint32_t
+{
+  ADJUSTABLE = ROLE_START_INDEX,
+  ALERT,
+  BUTTON,
+  CHECK_BOX,
+  COMBO_BOX,
+  CONTAINER,
+  DIALOG,
+  ENTRY,
+  HEADER,
+  IMAGE,
+  LINK,
+  LIST,
+  LIST_ITEM,
+  MENU,
+  MENU_BAR,
+  MENU_ITEM,
+  NONE,
+  PASSWORD_TEXT,
+  POPUP_MENU,
+  PROGRESS_BAR,
+  RADIO_BUTTON,
+  SCROLL_BAR,
+  SPIN_BUTTON,
+  TAB,
+  TAB_LIST,
+  TEXT,
+  TOGGLE_BUTTON,
+  TOOL_BAR,
+  MAX_COUNT
+};
+
 /**
  * @brief Represents the Accessible object for Dali::Toolkit::Control and derived classes
  *
@@ -232,8 +283,21 @@ public:
    */
   Vector2 GetLastPosition() const;
 
+  /**
+   * @brief Handles AcessibilityState property change; Only called when the control is highlighted.
+   */
+  void OnStatePropertySet(AccessibilityStates newStates);
+
 private:
+  /**
+   * @brief Appliys relavant accessibility properties to AT-SPI states.
+   */
   void ApplyAccessibilityProps(Dali::Accessibility::States& states);
+
+  /**
+   * @brief Grabs snapshot of previous state when the control is highlighted.
+   */
+  AccessibilityStates mStatesSnapshot;
 };
 
 } // namespace Dali::Toolkit::DevelControl
index 06cf4e392b6ba81bf7a3dc126bcac6d65845fa63..941d293f5d68c9c8dc55fead3e1fc249cdfbf68d 100644 (file)
@@ -78,57 +78,6 @@ enum State
   DISABLED
 };
 
-/**
- * @brief Represents current state of a control.
- */
-enum class AccessibilityState : uint32_t
-{
-  ENABLED = 0,
-  SELECTED,
-  CHECKED,
-  BUSY,
-  EXPANDED,
-  MAX_COUNT
-};
-using AccessibilityStates = Accessibility::EnumBitSet<AccessibilityState, AccessibilityState::MAX_COUNT>;
-
-constexpr const uint32_t ROLE_START_INDEX = 200;
-/**
- * @brief AccessibilityRole represents the purpose of a control.
- */
-enum class AccessibilityRole : uint32_t
-{
-  ADJUSTABLE = ROLE_START_INDEX,
-  ALERT,
-  BUTTON,
-  CHECK_BOX,
-  COMBO_BOX,
-  CONTAINER,
-  DIALOG,
-  ENTRY,
-  HEADER,
-  IMAGE,
-  LINK,
-  LIST,
-  LIST_ITEM,
-  MENU,
-  MENU_BAR,
-  MENU_ITEM,
-  NONE,
-  PASSWORD_TEXT,
-  POPUP_MENU,
-  PROGRESS_BAR,
-  RADIO_BUTTON,
-  SCROLL_BAR,
-  SPIN_BUTTON,
-  TAB,
-  TAB_LIST,
-  TEXT,
-  TOGGLE_BUTTON,
-  TOOL_BAR,
-  MAX_COUNT
-};
-
 namespace Property
 {
 enum
index a60e537c12ec2aaf8ff123d46a549d512a018592..5865e852734cdde863b5ff69204ba5b6700ddc59 100644 (file)
@@ -834,6 +834,13 @@ void Control::Impl::OnAccessibilityPropertySet(Dali::Handle& handle, Dali::Prope
     if(index == DevelControl::Property::ACCESSIBILITY_VALUE)
     {
       accessible->Emit(Dali::Accessibility::ObjectPropertyChangeEvent::VALUE);
+      return;
+    }
+
+    if(index == DevelControl::Property::ACCESSIBILITY_STATES)
+    {
+      accessible->OnStatePropertySet(mAccessibilityProps.states);
+      return;
     }
   }
 }