diff options
Diffstat (limited to 'accessible/mac/MOXTextMarkerDelegate.mm')
-rw-r--r-- | accessible/mac/MOXTextMarkerDelegate.mm | 527 |
1 files changed, 527 insertions, 0 deletions
diff --git a/accessible/mac/MOXTextMarkerDelegate.mm b/accessible/mac/MOXTextMarkerDelegate.mm new file mode 100644 index 0000000000..3e1e451ddd --- /dev/null +++ b/accessible/mac/MOXTextMarkerDelegate.mm @@ -0,0 +1,527 @@ +/* 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 |