accessible/mac/MOXTextMarkerDelegate.mm
author Eitan Isaacson <eitan@monotonous.org>
Tue, 21 Jul 2020 23:02:55 +0000
changeset 541524 1d37ebf3d20ce8071c7482fbf7bcdc37834e950c
parent 541355 8dc0120d47e63446d78b76887d53587031621d76
child 541639 6ff900a299dce4f3c02fc2946d91d5514e460f49
permissions -rw-r--r--
Bug 1653421 - Part 2: Add attributed string for range getter. r=morgan This doesn't actually add any attributes yet. VoiceOver uses this instead of the plain string equivalent. Differential Revision: https://phabricator.services.mozilla.com/D84054

/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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>

#import "MOXTextMarkerDelegate.h"

using namespace mozilla::a11y;

static nsDataHashtable<nsUint64HashKey, MOXTextMarkerDelegate*> sDelegates;

@implementation MOXTextMarkerDelegate

+ (id)getOrCreateForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc {
  MOZ_ASSERT(!aDoc.IsNull());

  MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits());
  if (!delegate) {
    delegate = [[MOXTextMarkerDelegate alloc] initWithDoc:aDoc];
    sDelegates.Put(aDoc.Bits(), delegate);
    [delegate retain];
  }

  return delegate;
}

+ (void)destroyForDoc:(mozilla::a11y::AccessibleOrProxy)aDoc {
  MOZ_ASSERT(!aDoc.IsNull());

  MOXTextMarkerDelegate* delegate = sDelegates.Get(aDoc.Bits());
  if (delegate) {
    sDelegates.Remove(aDoc.Bits());
    [delegate release];
  }
}

- (id)initWithDoc:(AccessibleOrProxy)aDoc {
  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 @"";
    }
  } else {
    if (mGeckoDocAccessible.AsProxy()->State() & states::STALE) {
      // In the proxy case we don't have access to load state,
      // so we need to use the less granular generic STALE state
      // this state also includes DOM unloaded, which isn't ideal.
      // Since we really only care if the a11y tree is loaded.
      return @"";
    }
  }

  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, textMarkerRange);
  return range.Text();
}

- (NSNumber*)moxLengthForTextMarkerRange:(id)textMarkerRange {
  return @([[self moxStringForTextMarkerRange:textMarkerRange] length]);
}

- (id)moxTextMarkerRangeForUnorderedTextMarkers:(NSArray*)textMarkers {
  if ([textMarkers count] != 2) {
    // Don't allow anything but a two member array.
    return nil;
  }

  GeckoTextMarker p1(mGeckoDocAccessible, textMarkers[0]);
  GeckoTextMarker p2(mGeckoDocAccessible, textMarkers[1]);

  bool ordered = p1 < p2;
  GeckoTextMarkerRange range(ordered ? p1 : p2, ordered ? p2 : p1);

  return range.CreateAXTextMarkerRange();
}

- (id)moxStartTextMarkerForTextMarkerRange:(id)textMarkerRange {
  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, textMarkerRange);

  return range.IsValid() ? range.mStart.CreateAXTextMarker() : nil;
}

- (id)moxEndTextMarkerForTextMarkerRange:(id)textMarkerRange {
  mozilla::a11y::GeckoTextMarkerRange range(mGeckoDocAccessible, textMarkerRange);

  return range.IsValid() ? range.mEnd.CreateAXTextMarker() : nil;
}

- (id)moxLeftWordTextMarkerRangeForTextMarker:(id)textMarker {
  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
  geckoTextMarker.NormalizePrevious();

  if (geckoTextMarker.mOffset == 0) {
    // We are probably at the start of the root container, normalize next
    // so we get the first word.
    geckoTextMarker.NormalizeNext();
  } else {
    // Go to preceding offset to get "left" word.
    geckoTextMarker.mOffset--;
    geckoTextMarker.NormalizePrevious();
  }

  return geckoTextMarker.WordRange().CreateAXTextMarkerRange();
}

- (id)moxRightWordTextMarkerRangeForTextMarker:(id)textMarker {
  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);
  geckoTextMarker.NormalizeNext();

  GeckoTextMarkerRange range = geckoTextMarker.AtEnd()
                                   ? GeckoTextMarkerRange(geckoTextMarker, geckoTextMarker)
                                   : geckoTextMarker.WordRange();

  return range.CreateAXTextMarkerRange();
}

- (id)moxNextTextMarkerForTextMarker:(id)textMarker {
  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);

  geckoTextMarker.NormalizeNext();
  if (geckoTextMarker.AtEnd()) {
    return nil;
  }

  geckoTextMarker.mOffset++;

  return geckoTextMarker.CreateAXTextMarker();
}

- (id)moxPreviousTextMarkerForTextMarker:(id)textMarker {
  GeckoTextMarker geckoTextMarker(mGeckoDocAccessible, textMarker);

  geckoTextMarker.NormalizePrevious();
  if (geckoTextMarker.mOffset == 0) {
    return nil;
  }

  geckoTextMarker.mOffset--;
  return geckoTextMarker.CreateAXTextMarker();
}

- (NSAttributedString*)moxAttributedStringForTextMarkerRange:(id)textMarkerRange {
  return [[[NSAttributedString alloc]
      initWithString:[self moxStringForTextMarkerRange:textMarkerRange]] autorelease];
}

@end