gecko-dev/accessible/mac/MOXTextMarkerDelegate.mm
James Teh 81d78e645f Bug 1827557 part 2: CachedTextMarker: Differentiate between LeftLine and Line. r=eeejay
Line should return the current line when the start of the line is queried.
Otherwise, VoiceOver reports the previous line if you're on the first character of a line and you move by line with down or up arrow.

LegacyTextMarker behaves inconsistently when you use Line/LeftLine depending on whether you fetched the marker via index or from a selection range.
browser_text_basics.js treated the index behaviour as correct, but it isn't.
The tests have been updated accordingly with expected failures for non-cached.

Differential Revision: https://phabricator.services.mozilla.com/D176608
2023-04-27 08:27:24 +00:00

528 lines
16 KiB
Plaintext

/* 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/. */
#import <Cocoa/Cocoa.h>
#include "DocAccessible.h"
#import "MOXTextMarkerDelegate.h"
#include "mozAccessible.h"
#include "mozilla/Preferences.h"
#include "nsISelectionListener.h"
using namespace mozilla::a11y;
#define PREF_ACCESSIBILITY_MAC_DEBUG "accessibility.mac.debug"
static nsTHashMap<nsPtrHashKey<mozilla::a11y::Accessible>,
MOXTextMarkerDelegate*>
sDelegates;
@implementation MOXTextMarkerDelegate
+ (id)getOrCreateForDoc:(mozilla::a11y::Accessible*)aDoc {
MOZ_ASSERT(aDoc);
MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
if (!delegate) {
delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc];
sDelegates.InsertOrUpdate(aDoc, delegate);
[delegate retain];
}
return delegate;
}
+ (void)destroyForDoc:(mozilla::a11y::Accessible*)aDoc {
MOZ_ASSERT(aDoc);
MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc);
if (delegate) {
sDelegates.Remove(aDoc);
[delegate release];
}
}
- (id)initWithDoc:(Accessible*)aDoc {
MOZ_ASSERT(aDoc, "Cannot init MOXTextDelegate with null");
if ((self = [super init])) {
mGeckoDocAccessible = aDoc;
}
mCaretMoveGranularity = nsISelectionListener::NO_AMOUNT;
return self;
}
- (void)dealloc {
[self invalidateSelection];
[super dealloc];
}
- (void)setSelectionFrom:(Accessible*)startContainer
at:(int32_t)startOffset
to:(Accessible*)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();
CFRetain(mSelection);
}
- (void)setCaretOffset:(mozilla::a11y::Accessible*)container
at:(int32_t)offset
moveGranularity:(int32_t)granularity {
GeckoTextMarker caretMarker(container, offset);
mPrevCaret = mCaret;
mCaret = caretMarker.CreateAXTextMarker();
mCaretMoveGranularity = granularity;
CFRetain(mCaret);
}
mozAccessible* GetEditableNativeFromGeckoAccessible(Accessible* aAcc) {
// The gecko accessible may not have a native accessible so we need
// to walk up the parent chain to find the nearest one.
// This happens when caching is enabled and the text marker's accessible
// may be a text leaf that is pruned from the platform.
for (Accessible* acc = aAcc; acc; acc = acc->Parent()) {
if (mozAccessible* mozAcc = GetNativeFromGeckoAccessible(acc)) {
return [mozAcc moxEditableAncestor];
}
}
return nil;
}
// This returns an info object to pass with AX SelectedTextChanged events.
// It uses the current and previous caret position to make decisions
// regarding which attributes to add to the info object.
- (NSDictionary*)selectionChangeInfo {
GeckoTextMarkerRange selectedGeckoRange =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, mSelection);
int32_t stateChangeType =
selectedGeckoRange.Start() == selectedGeckoRange.End()
? AXTextStateChangeTypeSelectionMove
: AXTextStateChangeTypeSelectionExtend;
// This is the base info object, includes the selected marker range and
// the change type depending on the collapsed state of the selection.
NSMutableDictionary* info = [[@{
@"AXSelectedTextMarkerRange" : selectedGeckoRange.IsValid()
? (__bridge id)mSelection
: [NSNull null],
@"AXTextStateChangeType" : @(stateChangeType),
} mutableCopy] autorelease];
GeckoTextMarker caretMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mCaret);
GeckoTextMarker prevCaretMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, mPrevCaret);
if (!caretMarker.IsValid()) {
// If the current caret is invalid, stop here and return base info.
return info;
}
mozAccessible* caretEditable =
GetEditableNativeFromGeckoAccessible(caretMarker.Acc());
if (!caretEditable && stateChangeType == AXTextStateChangeTypeSelectionMove) {
// If we are not in an editable, VO expects AXTextStateSync to be present
// and true.
info[@"AXTextStateSync"] = @YES;
}
if (!prevCaretMarker.IsValid() || caretMarker == prevCaretMarker) {
// If we have no stored previous marker, stop here.
return info;
}
mozAccessible* prevCaretEditable =
GetEditableNativeFromGeckoAccessible(prevCaretMarker.Acc());
if (prevCaretEditable != caretEditable) {
// If the caret goes in or out of an editable, consider the
// move direction "discontiguous".
info[@"AXTextSelectionDirection"] =
@(AXTextSelectionDirectionDiscontiguous);
if ([[caretEditable moxFocused] boolValue]) {
// If the caret is in a new focused editable, VO expects this attribute to
// be present and to be true.
info[@"AXTextSelectionChangedFocus"] = @YES;
}
return info;
}
bool isForward = prevCaretMarker < caretMarker;
int direction = isForward ? AXTextSelectionDirectionNext
: AXTextSelectionDirectionPrevious;
int32_t granularity = AXTextSelectionGranularityUnknown;
switch (mCaretMoveGranularity) {
case nsISelectionListener::CHARACTER_AMOUNT:
case nsISelectionListener::CLUSTER_AMOUNT:
granularity = AXTextSelectionGranularityCharacter;
break;
case nsISelectionListener::WORD_AMOUNT:
case nsISelectionListener::WORDNOSPACE_AMOUNT:
granularity = AXTextSelectionGranularityWord;
break;
case nsISelectionListener::LINE_AMOUNT:
granularity = AXTextSelectionGranularityLine;
break;
case nsISelectionListener::BEGINLINE_AMOUNT:
direction = AXTextSelectionDirectionBeginning;
granularity = AXTextSelectionGranularityLine;
break;
case nsISelectionListener::ENDLINE_AMOUNT:
direction = AXTextSelectionDirectionEnd;
granularity = AXTextSelectionGranularityLine;
break;
case nsISelectionListener::PARAGRAPH_AMOUNT:
granularity = AXTextSelectionGranularityParagraph;
break;
default:
break;
}
// Determine selection direction with marker comparison.
// If the delta between the two markers is more than one, consider it
// a word. Not accurate, but good enough for VO.
[info addEntriesFromDictionary:@{
@"AXTextSelectionDirection" : @(direction),
@"AXTextSelectionGranularity" : @(granularity)
}];
return info;
}
- (void)invalidateSelection {
CFRelease(mSelection);
CFRelease(mCaret);
CFRelease(mPrevCaret);
mSelection = nil;
}
- (mozilla::a11y::GeckoTextMarkerRange)selection {
return mozilla::a11y::GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, mSelection);
}
- (AXTextMarkerRef)moxStartTextMarker {
GeckoTextMarker geckoTextPoint(mGeckoDocAccessible, 0);
return geckoTextPoint.CreateAXTextMarker();
}
- (AXTextMarkerRef)moxEndTextMarker {
GeckoTextMarker geckoTextPoint(mGeckoDocAccessible,
nsIAccessibleText::TEXT_OFFSET_END_OF_TEXT);
return geckoTextPoint.CreateAXTextMarker();
}
- (AXTextMarkerRangeRef)moxSelectedTextMarkerRange {
return mSelection && GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, mSelection)
.IsValid()
? mSelection
: nil;
}
- (NSString*)moxStringForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
if (!range.IsValid()) {
return @"";
}
return range.Text();
}
- (NSNumber*)moxLengthForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
if (!range.IsValid()) {
return @0;
}
return @(range.Length());
}
- (AXTextMarkerRangeRef)moxTextMarkerRangeForUnorderedTextMarkers:
(NSArray*)textMarkers {
if ([textMarkers count] != 2) {
// Don't allow anything but a two member array.
return nil;
}
GeckoTextMarker p1 = GeckoTextMarker::MarkerFromAXTextMarker(
mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[0]);
GeckoTextMarker p2 = GeckoTextMarker::MarkerFromAXTextMarker(
mGeckoDocAccessible, (__bridge AXTextMarkerRef)textMarkers[1]);
if (!p1.IsValid() || !p2.IsValid()) {
// If either marker is invalid, return nil.
return nil;
}
bool ordered = p1 < p2;
GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1);
return range.CreateAXTextMarkerRange();
}
- (AXTextMarkerRef)moxStartTextMarkerForTextMarkerRange:
(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
return range.IsValid() ? range.Start().CreateAXTextMarker() : nil;
}
- (AXTextMarkerRef)moxEndTextMarkerForTextMarkerRange:
(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
return range.IsValid() ? range.End().CreateAXTextMarker() : nil;
}
- (AXTextMarkerRangeRef)moxLeftWordTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.LeftWordRange().CreateAXTextMarkerRange();
}
- (AXTextMarkerRangeRef)moxRightWordTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.RightWordRange().CreateAXTextMarkerRange();
}
- (AXTextMarkerRangeRef)moxLineTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.LineRange().CreateAXTextMarkerRange();
}
- (AXTextMarkerRangeRef)moxLeftLineTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.LeftLineRange().CreateAXTextMarkerRange();
}
- (AXTextMarkerRangeRef)moxRightLineTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.RightLineRange().CreateAXTextMarkerRange();
}
- (AXTextMarkerRangeRef)moxParagraphTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.ParagraphRange().CreateAXTextMarkerRange();
}
// override
- (AXTextMarkerRangeRef)moxStyleTextMarkerRangeForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.StyleRange().CreateAXTextMarkerRange();
}
- (AXTextMarkerRef)moxNextTextMarkerForTextMarker:(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
if (!geckoTextMarker.Next()) {
return nil;
}
return geckoTextMarker.CreateAXTextMarker();
}
- (AXTextMarkerRef)moxPreviousTextMarkerForTextMarker:
(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
if (!geckoTextMarker.Previous()) {
return nil;
}
return geckoTextMarker.CreateAXTextMarker();
}
- (NSAttributedString*)moxAttributedStringForTextMarkerRange:
(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
if (!range.IsValid()) {
return nil;
}
return range.AttributedText();
}
- (NSValue*)moxBoundsForTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
if (!range.IsValid()) {
return nil;
}
return range.Bounds();
}
- (NSNumber*)moxIndexForTextMarker:(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
GeckoTextMarkerRange range(GeckoTextMarker(mGeckoDocAccessible, 0),
geckoTextMarker);
return @(range.Length());
}
- (AXTextMarkerRef)moxTextMarkerForIndex:(NSNumber*)index {
GeckoTextMarker geckoTextMarker = GeckoTextMarker::MarkerFromIndex(
mGeckoDocAccessible, [index integerValue]);
if (!geckoTextMarker.IsValid()) {
return nil;
}
return geckoTextMarker.CreateAXTextMarker();
}
- (id)moxUIElementForTextMarker:(AXTextMarkerRef)textMarker {
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return nil;
}
Accessible* leaf = geckoTextMarker.Leaf();
if (!leaf) {
return nil;
}
return GetNativeFromGeckoAccessible(leaf);
}
- (AXTextMarkerRangeRef)moxTextMarkerRangeForUIElement:(id)element {
if (![element isKindOfClass:[mozAccessible class]]) {
return nil;
}
GeckoTextMarkerRange range((Accessible*)[element geckoAccessible]);
return range.CreateAXTextMarkerRange();
}
- (NSString*)moxMozDebugDescriptionForTextMarker:(AXTextMarkerRef)textMarker {
if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
return nil;
}
GeckoTextMarker geckoTextMarker =
GeckoTextMarker::MarkerFromAXTextMarker(mGeckoDocAccessible, textMarker);
if (!geckoTextMarker.IsValid()) {
return @"<GeckoTextMarker 0x0 [0]>";
}
return [NSString stringWithFormat:@"<GeckoTextMarker %p [%d]>",
geckoTextMarker.Acc(),
geckoTextMarker.Offset()];
}
- (NSString*)moxMozDebugDescriptionForTextMarkerRange:
(AXTextMarkerRangeRef)textMarkerRange {
if (!mozilla::Preferences::GetBool(PREF_ACCESSIBILITY_MAC_DEBUG)) {
return nil;
}
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
if (!range.IsValid()) {
return @"<GeckoTextMarkerRange 0x0 [0] - 0x0 [0]>";
}
return [NSString stringWithFormat:@"<GeckoTextMarkerRange %p [%d] - %p [%d]>",
range.Start().Acc(), range.Start().Offset(),
range.End().Acc(), range.End().Offset()];
}
- (void)moxSetSelectedTextMarkerRange:(AXTextMarkerRangeRef)textMarkerRange {
mozilla::a11y::GeckoTextMarkerRange range =
GeckoTextMarkerRange::MarkerRangeFromAXTextMarkerRange(
mGeckoDocAccessible, textMarkerRange);
if (range.IsValid()) {
range.Select();
}
}
@end