Bug 1226473 - Support AXSelectedTextMarkerRange. r=morgan
authorEitan Isaacson <eitan@monotonous.org>
Mon, 06 Jul 2020 17:32:45 +0000
changeset 538926 92d97e70eb6bd124eb961cf8ec40343d210fb9c5
parent 538925 879f36821e411d4a99399b9f5f4f48feeb7a1c8b
child 538927 e7c1f66e9ff4dfbec8fd14a3ba6f99199b6f54b4
push id37574
push userapavel@mozilla.com
push dateMon, 06 Jul 2020 21:50:07 +0000
treeherdermozilla-central@6cedb9c51fd8 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmorgan
bugs1226473
milestone80.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1226473 - Support AXSelectedTextMarkerRange. r=morgan To do this well we need to cache the text selection in the top level process. Differential Revision: https://phabricator.services.mozilla.com/D82111
accessible/base/Platform.h
accessible/ipc/DocAccessibleParent.cpp
accessible/mac/AccessibleWrap.mm
accessible/mac/MOXAccessibleProtocol.h
accessible/mac/MOXTextMarkerDelegate.h
accessible/mac/MOXTextMarkerDelegate.mm
accessible/mac/Platform.mm
accessible/mac/mozAccessible.mm
accessible/mac/mozTextAccessible.mm
accessible/tests/browser/mac/browser_text_basics.js
--- a/accessible/base/Platform.h
+++ b/accessible/base/Platform.h
@@ -133,12 +133,18 @@ void ProxyBatch(ProxyAccessible* aDocume
                 const nsTArray<ProxyAccessible*>& aAccessibles,
                 const nsTArray<BatchData>& aData);
 
 bool LocalizeString(
     const char* aToken, nsAString& aLocalized,
     const nsTArray<nsString>& aFormatString = nsTArray<nsString>());
 #endif
 
+#ifdef MOZ_WIDGET_COCOA
+class TextRangeData;
+void ProxyTextSelectionChangeEvent(ProxyAccessible* aTarget,
+                                   const nsTArray<TextRangeData>& aSelection);
+#endif
+
 }  // namespace a11y
 }  // namespace mozilla
 
 #endif  // mozilla_a11y_Platform_h
--- a/accessible/ipc/DocAccessibleParent.cpp
+++ b/accessible/ipc/DocAccessibleParent.cpp
@@ -497,20 +497,33 @@ mozilla::ipc::IPCResult DocAccessiblePar
       aAnnouncement, aPriority);
   nsCoreUtils::DispatchAccEvent(std::move(event));
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult DocAccessibleParent::RecvTextSelectionChangeEvent(
     const uint64_t& aID, nsTArray<TextRangeData>&& aSelection) {
-  // XXX: nsIAccessibleTextRange is not e10s friendly, so don't bother
-  // supporting nsIAccessibleTextSelectionChangeEvent for now.
-  // This is a placeholder for potential platform support.
+#  ifdef MOZ_WIDGET_COCOA
+  if (mShutdown) {
+    return IPC_OK();
+  }
+
+  ProxyAccessible* target = GetAccessible(aID);
+  if (!target) {
+    NS_ERROR("no proxy for event!");
+    return IPC_OK();
+  }
+
+  ProxyTextSelectionChangeEvent(target, aSelection);
+
+  return IPC_OK();
+#  else
   return RecvEvent(aID, nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED);
+#  endif
 }
 
 #endif
 
 mozilla::ipc::IPCResult DocAccessibleParent::RecvRoleChangedEvent(
     const a11y::role& aRole) {
   if (mShutdown) {
     return IPC_OK();
--- a/accessible/mac/AccessibleWrap.mm
+++ b/accessible/mac/AccessibleWrap.mm
@@ -1,22 +1,26 @@
-/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format off */
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* clang-format on */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "DocAccessible.h"
 #include "nsObjCExceptions.h"
 
 #include "Accessible-inl.h"
 #include "nsAccUtils.h"
 #include "Role.h"
+#include "TextRange.h"
 #include "gfxPlatform.h"
 
 #import "MOXMathAccessibles.h"
+#import "MOXTextMarkerDelegate.h"
 #import "MOXWebAreaAccessible.h"
 #import "mozAccessible.h"
 #import "mozActionElements.h"
 #import "mozHTMLAccessible.h"
 #import "mozSelectableElements.h"
 #import "mozTableAccessible.h"
 #import "mozTextAccessible.h"
 
@@ -108,18 +112,16 @@ nsresult AccessibleWrap::HandleAccEvent(
   uint32_t eventType = aEvent->GetEventType();
 
   mozAccessible* nativeAcc = nil;
 
   switch (eventType) {
     case nsIAccessibleEvent::EVENT_FOCUS:
     case nsIAccessibleEvent::EVENT_VALUE_CHANGE:
     case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
-    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED:
-    case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED:
     case nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE:
     case nsIAccessibleEvent::EVENT_MENUPOPUP_START:
     case nsIAccessibleEvent::EVENT_MENUPOPUP_END:
     case nsIAccessibleEvent::EVENT_REORDER:
       if (Accessible* accessible = aEvent->GetAccessible()) {
         accessible->GetNativeInterface((void**)&nativeAcc);
         if (!nativeAcc) {
           return NS_ERROR_FAILURE;
@@ -147,16 +149,54 @@ nsresult AccessibleWrap::HandleAccEvent(
           AccStateChangeEvent* event = downcast_accEvent(aEvent);
           [nativeAcc stateChanged:event->GetState() isEnabled:event->IsStateEnabled()];
           return NS_OK;
         } else {
           return NS_ERROR_FAILURE;
         }
       }
       break;
+    case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED:
+      if (Accessible* accessible = aEvent->GetAccessible()) {
+        accessible->GetNativeInterface((void**)&nativeAcc);
+        if (!nativeAcc) {
+          return NS_ERROR_FAILURE;
+        }
+
+        MOXTextMarkerDelegate* delegate =
+            [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()];
+        AccTextSelChangeEvent* event = downcast_accEvent(aEvent);
+        AutoTArray<TextRange, 1> ranges;
+        event->SelectionRanges(&ranges);
+
+        if (ranges.Length()) {
+          // Cache selection in delegate.
+          [delegate setSelectionFrom:ranges[0].StartContainer()
+                                  at:ranges[0].StartOffset()
+                                  to:ranges[0].EndContainer()
+                                  at:ranges[0].EndOffset()];
+        }
+      }
+      break;
+    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED:
+      if (Accessible* accessible = aEvent->GetAccessible()) {
+        accessible->GetNativeInterface((void**)&nativeAcc);
+        if (!nativeAcc) {
+          return NS_ERROR_FAILURE;
+        }
+
+        AccCaretMoveEvent* event = downcast_accEvent(aEvent);
+        if (event->IsSelectionCollapsed()) {
+          // If the selection is collapsed, invalidate our text selection cache.
+          MOXTextMarkerDelegate* delegate =
+              [MOXTextMarkerDelegate getOrCreateForDoc:aEvent->Document()];
+          [delegate invalidateSelection];
+        }
+      }
+      break;
     default:
       break;
   }
 
   if (nativeAcc) {
     [nativeAcc handleAccessibleEvent:eventType];
   }
 
--- a/accessible/mac/MOXAccessibleProtocol.h
+++ b/accessible/mac/MOXAccessibleProtocol.h
@@ -310,16 +310,19 @@
 #pragma mark - TextAttributeGetters
 
 // AXStartTextMarker
 - (id _Nullable)moxStartTextMarker;
 
 // AXEndTextMarker
 - (id _Nullable)moxEndTextMarker;
 
+// AXSelectedTextMarkerRange
+- (id _Nullable)moxSelectedTextMarkerRange;
+
 #pragma mark - ParameterizedTextAttributeGetters
 
 // AXLengthForTextMarkerRange
 - (NSNumber* _Nullable)moxLengthForTextMarkerRange:(id _Nonnull)textMarkerRange;
 
 // AXStringForTextMarkerRange
 - (NSString* _Nullable)moxStringForTextMarkerRange:(id _Nonnull)textMarkerRange;
 
--- a/accessible/mac/MOXTextMarkerDelegate.h
+++ b/accessible/mac/MOXTextMarkerDelegate.h
@@ -6,31 +6,44 @@
 #import <Cocoa/Cocoa.h>
 
 #import "MOXAccessibleProtocol.h"
 
 #include "AccessibleOrProxy.h"
 
 @interface MOXTextMarkerDelegate : NSObject <MOXTextMarkerSupport> {
   mozilla::a11y::AccessibleOrProxy mGeckoDocAccessible;
+  id mSelection;
 }
 
 + (id)getOrCreateForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc;
 
 + (void)destroyForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc;
 
 - (id)initWithDoc:(mozilla::a11y::AccessibleOrProxy)aDoc;
 
+- (void)dealloc;
+
+- (void)setSelectionFrom:(mozilla::a11y::AccessibleOrProxy)startContainer
+                      at:(int32_t)startOffset
+                      to:(mozilla::a11y::AccessibleOrProxy)endContainer
+                      at:(int32_t)endOffset;
+
+- (void)invalidateSelection;
+
 // override
 - (id)moxStartTextMarker;
 
 // override
 - (id)moxEndTextMarker;
 
 // override
+- (id)moxSelectedTextMarkerRange;
+
+// override
 - (NSNumber*)moxLengthForTextMarkerRange:(id)textMarkerRange;
 
 // override
 - (NSString*)moxStringForTextMarkerRange:(id)textMarkerRange;
 
 // override
 - (id)moxTextMarkerRangeForUnorderedTextMarkers:(NSArray*)textMarkers;
 
--- a/accessible/mac/MOXTextMarkerDelegate.mm
+++ b/accessible/mac/MOXTextMarkerDelegate.mm
@@ -40,30 +40,59 @@ static nsDataHashtable<nsUint64HashKey, 
   MOZ_ASSERT(!aDoc.IsNull(), "Cannot init MOXTextDelegate with null");
   if ((self = [super init])) {
     mGeckoDocAccessible = aDoc;
   }
 
   return self;
 }
 
+- (void)dealloc {
+  [self invalidateSelection];
+  [super dealloc];
+}
+
+- (void)setSelectionFrom:(AccessibleOrProxy)startContainer
+                      at:(int32_t)startOffset
+                      to:(AccessibleOrProxy)endContainer
+                      at:(int32_t)endOffset {
+  GeckoTextMarkerRange selection(GeckoTextMarker(startContainer, startOffset),
+                                 GeckoTextMarker(endContainer, endOffset));
+
+  // We store it as an AXTextMarkerRange because it is a safe
+  // way to keep a weak reference - when we need to use the
+  // range we can convert it back to a GeckoTextMarkerRange
+  // and check that it's valid.
+  mSelection = [selection.CreateAXTextMarkerRange() retain];
+}
+
+- (void)invalidateSelection {
+  [mSelection release];
+  mSelection = nil;
+}
+
 - (id)moxStartTextMarker {
   GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0);
   return geckoTextPoint.CreateAXTextMarker();
 }
 
 - (id)moxEndTextMarker {
   uint32_t characterCount =
       mGeckoDocAccessible.IsProxy()
           ? mGeckoDocAccessible.AsProxy()->CharacterCount()
           : mGeckoDocAccessible.AsAccessible()->Document()->AsHyperText()->CharacterCount();
   GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, characterCount);
   return geckoTextPoint.CreateAXTextMarker();
 }
 
+- (id)moxSelectedTextMarkerRange {
+  return mSelection && GeckoTextMarkerRange(mGeckoDocAccessible, mSelection).IsValid() ? mSelection
+                                                                                       : nil;
+}
+
 - (NSString*)moxStringForTextMarkerRange:(id)textMarkerRange {
   if (mGeckoDocAccessible.IsAccessible()) {
     if (!mGeckoDocAccessible.AsAccessible()->AsDoc()->HasLoadState(
             DocAccessible::eTreeConstructed)) {
       // If the accessible tree is still being constructed the text tree
       // is not in a traversable state yet.
       return @"";
     }
--- a/accessible/mac/Platform.mm
+++ b/accessible/mac/Platform.mm
@@ -69,22 +69,20 @@ void ProxyDestroyed(ProxyAccessible* aPr
   if (aProxy->IsDoc()) {
     [MOXTextMarkerDelegate destroyForDoc:aProxy];
   }
 }
 
 void ProxyEvent(ProxyAccessible* aProxy, uint32_t aEventType) {
   // ignore everything but focus-changed, value-changed, caret,
   // selection, and document load complete events for now.
-  NSLog(@"Event type is %u", aEventType);
   if (aEventType != nsIAccessibleEvent::EVENT_FOCUS &&
       aEventType != nsIAccessibleEvent::EVENT_VALUE_CHANGE &&
       aEventType != nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE &&
       aEventType != nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED &&
-      aEventType != nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED &&
       aEventType != nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE &&
       aEventType != nsIAccessibleEvent::EVENT_REORDER)
     return;
 
   mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
   if (wrapper) {
     [wrapper handleAccessibleEvent:aEventType];
   }
@@ -94,31 +92,58 @@ void ProxyStateChangeEvent(ProxyAccessib
   mozAccessible* wrapper = GetNativeFromGeckoAccessible(aProxy);
   if (wrapper) {
     [wrapper stateChanged:aState isEnabled:aEnabled];
   }
 }
 
 void ProxyCaretMoveEvent(ProxyAccessible* aTarget, int32_t aOffset, bool aIsSelectionCollapsed) {
   mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+  if (aIsSelectionCollapsed) {
+    // If selection is collapsed, invalidate selection.
+    MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()];
+    [delegate invalidateSelection];
+  }
+
   if (wrapper) {
     [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED];
   }
 }
 
 void ProxyTextChangeEvent(ProxyAccessible*, const nsString&, int32_t, uint32_t, bool, bool) {}
 
 void ProxyShowHideEvent(ProxyAccessible*, ProxyAccessible*, bool, bool) {}
 
 void ProxySelectionEvent(ProxyAccessible* aTarget, ProxyAccessible* aWidget, uint32_t aEventType) {
   mozAccessible* wrapper = GetNativeFromGeckoAccessible(aWidget);
   if (wrapper) {
     [wrapper handleAccessibleEvent:aEventType];
   }
 }
+
+void ProxyTextSelectionChangeEvent(ProxyAccessible* aTarget,
+                                   const nsTArray<TextRangeData>& aSelection) {
+  if (aSelection.Length()) {
+    MOXTextMarkerDelegate* delegate = [MOXTextMarkerDelegate getOrCreateForDoc:aTarget->Document()];
+    DocAccessibleParent* doc = aTarget->Document();
+    ProxyAccessible* startContainer = doc->GetAccessible(aSelection[0].StartID());
+    ProxyAccessible* endContainer = doc->GetAccessible(aSelection[0].EndID());
+    // Cache the selection.
+    [delegate setSelectionFrom:startContainer
+                            at:aSelection[0].StartOffset()
+                            to:endContainer
+                            at:aSelection[0].EndOffset()];
+  }
+
+  mozAccessible* wrapper = GetNativeFromGeckoAccessible(aTarget);
+  if (wrapper) {
+    [wrapper handleAccessibleEvent:nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED];
+  }
+}
+
 }  // namespace a11y
 }  // namespace mozilla
 
 @interface GeckoNSApplication (a11y)
 - (void)accessibilitySetValue:(id)value forAttribute:(NSString*)attribute;
 @end
 
 @implementation GeckoNSApplication (a11y)
--- a/accessible/mac/mozAccessible.mm
+++ b/accessible/mac/mozAccessible.mm
@@ -810,16 +810,20 @@ struct RoleDescrComparator {
       [self moxPostNotification:@"AXMenuClosed"];
       break;
     case nsIAccessibleEvent::EVENT_SELECTION:
     case nsIAccessibleEvent::EVENT_SELECTION_ADD:
     case nsIAccessibleEvent::EVENT_SELECTION_REMOVE:
     case nsIAccessibleEvent::EVENT_SELECTION_WITHIN:
       [self moxPostNotification:NSAccessibilitySelectedChildrenChangedNotification];
       break;
+    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED:
+    case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED:
+      [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification];
+      break;
   }
 }
 
 - (void)expire {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   [self invalidateState];
 
--- a/accessible/mac/mozTextAccessible.mm
+++ b/accessible/mac/mozTextAccessible.mm
@@ -355,20 +355,16 @@ inline NSString* ToNSString(id aValue) {
 #pragma mark - mozAccessible
 
 - (void)handleAccessibleEvent:(uint32_t)eventType {
   switch (eventType) {
     case nsIAccessibleEvent::EVENT_VALUE_CHANGE:
     case nsIAccessibleEvent::EVENT_TEXT_VALUE_CHANGE:
       [self moxPostNotification:NSAccessibilityValueChangedNotification];
       break;
-    case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED:
-    case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED:
-      [self moxPostNotification:NSAccessibilitySelectedTextChangedNotification];
-      break;
     default:
       [super handleAccessibleEvent:eventType];
       break;
   }
 }
 
 #pragma mark -
 
--- a/accessible/tests/browser/mac/browser_text_basics.js
+++ b/accessible/tests/browser/mac/browser_text_basics.js
@@ -26,9 +26,24 @@ addAccessibleTask(`<p id="p">Hello World
 
   let startMarker = macDoc.getAttributeValue("AXStartTextMarker");
   let endMarker = macDoc.getAttributeValue("AXEndTextMarker");
   let range = macDoc.getParameterizedAttributeValue(
     "AXTextMarkerRangeForUnorderedTextMarkers",
     [startMarker, endMarker]
   );
   is(stringForRange(range), "Hello World");
+
+  let evt = waitForMacEvent("AXSelectedTextChanged");
+  await SpecialPowers.spawn(browser, [], () => {
+    let p = content.document.getElementById("p");
+    let r = new content.Range();
+    r.setStart(p.firstChild, 1);
+    r.setEnd(p.firstChild, 8);
+
+    let s = content.getSelection();
+    s.addRange(r);
+  });
+  await evt;
+
+  range = macDoc.getAttributeValue("AXSelectedTextMarkerRange");
+  is(stringForRange(range), "ello Wo");
 });