Fix accessibility lines on OS X

Make QTextView accessibility on OS X mirror that of NSTextView in terms
of translating between positions and line numbers.

Most significantly, we report softlines (i.e. "lines" resulting from
visual wrapping induced by text view's width) as individual lines, not
just hardlines (i.e. "lines" delimited by newline characters, a.k.a.
paragraphs).  This fixes keyboard echo when using just arrow up and down
(without VO prefix) as now in such case VoiceOver reads the softline
that the text cursor moved to; before this fix it read again the whole
paragraph (which it read no matter to which softline of paragraph we
moved to). This enables the user to search more effectively for the
softline they need (which they do very often when navigating text).

Further, we changed the behavior to report the trailing newline
character of a line as the last character of the line. This is
consistent with how NSTextView (and TextEdit) does things (and makes the
newline character be displayed on a braille display for the user to be
able to distinguish that this is really the end of paragraph), but could
be debated and changed as some important Apple apps do not include the
newline character in the line range (most notably Pages and Mail, both
in the document (or email text) text area). I asked about this here:
http://lists.apple.com/archives/accessibility-dev/2015/Jan/msg00000.html

This also fixes the case where empty line previously returned empty range
(length == 0) for AXRangeForLine and VoiceOver interpreted that as end of
document even if it was in the middle of document (e.g. in
examples/widgets/richtext/textedit, there is an empty line in "Lists" section
just before the last paragraph, if one attempted to move past it e.g with
VO-arrow down after interacting with the text using VO-Shift-arrow down, then
no luck).

The code is currently O(N) as the previous one, which could mean a performance
problem for bigger documents. As it seems QTextLayout has all the information
it needs to do AXLineForIndex and AXRangeForLine efficiently (I would presume
O(log N)), this should be eventually rewritten to take advantage of
that (but the required interfaces are not currently exposed through
QAccessibleTextInterface)

[ChangeLog][QtGui][Accessibility][OS X] QTextEdit now properly reports
to accessibility visual lines (softlines) as lines, instead of whole
paragraphs. This allows better VoiceOver user experience when reading
text line by line using arrows up/down.

Change-Id: Ie7f06eb919de31d0a083b3b30d26ccac4aa27301
Reviewed-by: Frederik Gladhorn <frederik.gladhorn@theqtcompany.com>
This commit is contained in:
Boris Dušek 2014-12-28 23:38:38 +01:00
parent e82d3b1a1e
commit 07cd153c7e

View File

@ -40,6 +40,47 @@
#import <AppKit/NSAccessibility.h> #import <AppKit/NSAccessibility.h>
static void convertLineOffset(QAccessibleTextInterface *text, int &line, int &offset, NSUInteger *start = 0, NSUInteger *end = 0)
{
Q_ASSERT(line == -1 || offset == -1);
Q_ASSERT(line != -1 || offset != -1);
Q_ASSERT(offset <= text->characterCount());
int curLine = -1;
int curStart = 0, curEnd = 0;
do {
curStart = curEnd;
text->textAtOffset(curStart, QAccessible::LineBoundary, &curStart, &curEnd);
++curLine;
{
// check for a case where a single word longer than the text edit's width and gets wrapped
// in the middle of the word; in this case curEnd will be an offset belonging to the next line
// and therefore nextEnd will not be equal to curEnd
int nextStart;
int nextEnd;
text->textAtOffset(curEnd, QAccessible::LineBoundary, &nextStart, &nextEnd);
if (nextEnd == curEnd)
++curEnd;
}
} while ((line == -1 || curLine < line) && (offset == -1 || (curEnd <= offset)) && curEnd <= text->characterCount());
curEnd = qMin(curEnd, text->characterCount());
if (line == -1)
line = curLine;
if (offset == -1)
offset = curStart;
Q_ASSERT(curStart >= 0);
Q_ASSERT(curEnd >= 0);
if (start)
*start = curStart;
if (end)
*end = curEnd;
}
@implementation QMacAccessibilityElement @implementation QMacAccessibilityElement
- (id)initWithId:(QAccessible::Id)anId - (id)initWithId:(QAccessible::Id)anId
@ -284,8 +325,10 @@
} else if ([attribute isEqualToString:NSAccessibilityInsertionPointLineNumberAttribute]) { } else if ([attribute isEqualToString:NSAccessibilityInsertionPointLineNumberAttribute]) {
if (QAccessibleTextInterface *text = iface->textInterface()) { if (QAccessibleTextInterface *text = iface->textInterface()) {
QString textBeforeCursor = text->text(0, text->cursorPosition()); int line = -1;
return [NSNumber numberWithInt: textBeforeCursor.count(QLatin1Char('\n'))]; int position = text->cursorPosition();
convertLineOffset(text, line, position);
return [NSNumber numberWithInt: line];
} }
return nil; return nil;
} else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) { } else if ([attribute isEqualToString:NSAccessibilityMinValueAttribute]) {
@ -340,22 +383,21 @@
} }
if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) { if ([attribute isEqualToString: NSAccessibilityLineForIndexParameterizedAttribute]) {
int index = [parameter intValue]; int index = [parameter intValue];
NSNumber *ln = [QMacAccessibilityElement lineNumberForIndex: index forText: iface->text(QAccessible::Value)]; if (index < 0 || index > iface->textInterface()->characterCount())
return ln; return nil;
int line = -1;
convertLineOffset(iface->textInterface(), line, index);
return [NSNumber numberWithInt:line];
} }
if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) { if ([attribute isEqualToString: NSAccessibilityRangeForLineParameterizedAttribute]) {
int lineNumber = [parameter intValue]; int line = [parameter intValue];
QString text = iface->text(QAccessible::Value); if (line < 0)
int startOffset = 0; return nil;
// skip newlines until we have the one we look for int lineOffset = -1;
for (int i = 0; i < lineNumber; ++i) NSUInteger startOffset = 0;
startOffset = text.indexOf(QLatin1Char('\n'), startOffset) + 1; NSUInteger endOffset = 0;
if (startOffset < 0) // invalid line number, return the first line convertLineOffset(iface->textInterface(), line, lineOffset, &startOffset, &endOffset);
startOffset = 0; return [NSValue valueWithRange:NSMakeRange(startOffset, endOffset - startOffset)];
int endOffset = text.indexOf(QLatin1Char('\n'), startOffset);
if (endOffset == -1)
endOffset = text.length();
return [NSValue valueWithRange:NSMakeRange(quint32(startOffset), quint32(endOffset - startOffset))];
} }
if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) { if ([attribute isEqualToString: NSAccessibilityBoundsForRangeParameterizedAttribute]) {
NSRange range = [parameter rangeValue]; NSRange range = [parameter rangeValue];