Accessibility: Improve line boundary helper functions

These functions are supposed to make it easy for third parties (and
QLineEdit) to implement the textAt/Before/AfterOffset functions.
Before the functions were ignoring newlines completely and thus only somewhat
useful.
Change-Id: I7136b9502a7fa6f8ad9ad7236761a34c1a7fd4da
Reviewed-by: Jan Arve Sæther <jan-arve.saether@digia.com>
bb10
Frederik Gladhorn 2014-07-03 11:19:21 +02:00
parent f4ae4f41b0
commit 52f859604d
5 changed files with 357 additions and 56 deletions

View File

@ -1911,17 +1911,66 @@ QDebug operator<<(QDebug d, const QAccessibleEvent &ev)
The \a endOffset is the first character that will not be returned.
*/
/*!
\internal
Helper for finding line breaks in textBeforeOffset/textAtOffset/textAfterOffset.
\a beforeAtAfter is the line we look for. -1 for before, 0 for at and 1 for after.
*/
static QString textLineBoundary(int beforeAtAfter, const QString &text, int offset, int *startOffset, int *endOffset)
{
Q_ASSERT(beforeAtAfter >= -1 && beforeAtAfter <= 1);
Q_ASSERT(*startOffset == -1 && *endOffset == -1);
int length = text.length();
Q_ASSERT(offset >= 0 && offset <= length);
// move offset into the right range (if asking for line before or after
if (beforeAtAfter == 1) {
offset = text.indexOf(QChar::LineFeed, qMin(offset, length - 1));
if (offset < 0)
return QString(); // after the last line comes nothing
++offset; // move after the newline
} else if (beforeAtAfter == -1) {
offset = text.lastIndexOf(QChar::LineFeed, qMax(offset - 1, 0));
if (offset < 0)
return QString(); // before first line comes nothing
}
if (offset > 0)
*startOffset = text.lastIndexOf(QChar::LineFeed, offset - 1);
++*startOffset; // move to the char after the newline (0 if lastIndexOf returned -1)
*endOffset = text.indexOf(QChar::LineFeed, qMin(offset, length - 1)) + 1; // include newline char
if (*endOffset <= 0 || *endOffset > length)
*endOffset = length; // if the text doesn't end with a newline it ends at length
return text.mid(*startOffset, *endOffset - *startOffset);
}
/*!
Returns the text item of type \a boundaryType that is close to offset \a offset
and sets \a startOffset and \a endOffset values to the start and end positions
of that item; returns an empty string if there is no such an item.
Sets \a startOffset and \a endOffset values to -1 on error.
This default implementation is provided for small text edits. A word processor or
text editor should provide their own efficient implementations. This function makes no
distinction between paragraphs and lines.
\note this function can not take the cursor position into account. By convention
an \a offset of -2 means that this function should use the cursor position as offset.
Thus an offset of -2 must be converted to the cursor position before calling this
function.
An offset of -1 is used for the text length and custom implementations of this function
have to return the result as if the length was passed in as offset.
*/
QString QAccessibleTextInterface::textBeforeOffset(int offset, QAccessible::TextBoundaryType boundaryType,
int *startOffset, int *endOffset) const
{
const QString txt = text(0, characterCount());
if (offset == -1)
offset = txt.length();
*startOffset = *endOffset = -1;
if (txt.isEmpty() || offset <= 0 || offset > txt.length())
return QString();
@ -1937,7 +1986,11 @@ QString QAccessibleTextInterface::textBeforeOffset(int offset, QAccessible::Text
case QAccessible::SentenceBoundary:
type = QTextBoundaryFinder::Sentence;
break;
default:
case QAccessible::LineBoundary:
case QAccessible::ParagraphBoundary:
// Lines can not use QTextBoundaryFinder since Line there means any potential line-break.
return textLineBoundary(-1, txt, offset, startOffset, endOffset);
case QAccessible::NoBoundary:
// return empty, this function currently only supports single lines, so there can be no line before
return QString();
}
@ -1969,12 +2022,26 @@ QString QAccessibleTextInterface::textBeforeOffset(int offset, QAccessible::Text
and sets \a startOffset and \a endOffset values to the start and end positions
of that item; returns an empty string if there is no such an item.
Sets \a startOffset and \a endOffset values to -1 on error.
This default implementation is provided for small text edits. A word processor or
text editor should provide their own efficient implementations. This function makes no
distinction between paragraphs and lines.
\note this function can not take the cursor position into account. By convention
an \a offset of -2 means that this function should use the cursor position as offset.
Thus an offset of -2 must be converted to the cursor position before calling this
function.
An offset of -1 is used for the text length and custom implementations of this function
have to return the result as if the length was passed in as offset.
*/
QString QAccessibleTextInterface::textAfterOffset(int offset, QAccessible::TextBoundaryType boundaryType,
int *startOffset, int *endOffset) const
{
const QString txt = text(0, characterCount());
if (offset == -1)
offset = txt.length();
*startOffset = *endOffset = -1;
if (txt.isEmpty() || offset < 0 || offset >= txt.length())
return QString();
@ -1990,7 +2057,11 @@ QString QAccessibleTextInterface::textAfterOffset(int offset, QAccessible::TextB
case QAccessible::SentenceBoundary:
type = QTextBoundaryFinder::Sentence;
break;
default:
case QAccessible::LineBoundary:
case QAccessible::ParagraphBoundary:
// Lines can not use QTextBoundaryFinder since Line there means any potential line-break.
return textLineBoundary(1, txt, offset, startOffset, endOffset);
case QAccessible::NoBoundary:
// return empty, this function currently only supports single lines, so there can be no line after
return QString();
}
@ -2033,12 +2104,26 @@ QString QAccessibleTextInterface::textAfterOffset(int offset, QAccessible::TextB
and sets \a startOffset and \a endOffset values to the start and end positions
of that item; returns an empty string if there is no such an item.
Sets \a startOffset and \a endOffset values to -1 on error.
This default implementation is provided for small text edits. A word processor or
text editor should provide their own efficient implementations. This function makes no
distinction between paragraphs and lines.
\note this function can not take the cursor position into account. By convention
an \a offset of -2 means that this function should use the cursor position as offset.
Thus an offset of -2 must be converted to the cursor position before calling this
function.
An offset of -1 is used for the text length and custom implementations of this function
have to return the result as if the length was passed in as offset.
*/
QString QAccessibleTextInterface::textAtOffset(int offset, QAccessible::TextBoundaryType boundaryType,
int *startOffset, int *endOffset) const
{
const QString txt = text(0, characterCount());
if (offset == -1)
offset = txt.length();
*startOffset = *endOffset = -1;
if (txt.isEmpty() || offset < 0 || offset > txt.length())
return QString();
@ -2057,8 +2142,11 @@ QString QAccessibleTextInterface::textAtOffset(int offset, QAccessible::TextBoun
case QAccessible::SentenceBoundary:
type = QTextBoundaryFinder::Sentence;
break;
default:
// return the whole line
case QAccessible::LineBoundary:
case QAccessible::ParagraphBoundary:
// Lines can not use QTextBoundaryFinder since Line there means any potential line-break.
return textLineBoundary(0, txt, offset, startOffset, endOffset);
case QAccessible::NoBoundary:
*startOffset = 0;
*endOffset = txt.length();
return txt;

View File

@ -729,8 +729,6 @@ QString QAccessibleLineEdit::textBeforeOffset(int offset, QAccessible::TextBound
*startOffset = *endOffset = -1;
return QString();
}
if (offset == -1)
offset = lineEdit()->text().length();
if (offset == -2)
offset = cursorPosition();
return QAccessibleTextInterface::textBeforeOffset(offset, boundaryType, startOffset, endOffset);
@ -743,8 +741,6 @@ QString QAccessibleLineEdit::textAfterOffset(int offset, QAccessible::TextBounda
*startOffset = *endOffset = -1;
return QString();
}
if (offset == -1)
offset = lineEdit()->text().length();
if (offset == -2)
offset = cursorPosition();
return QAccessibleTextInterface::textAfterOffset(offset, boundaryType, startOffset, endOffset);
@ -757,8 +753,6 @@ QString QAccessibleLineEdit::textAtOffset(int offset, QAccessible::TextBoundaryT
*startOffset = *endOffset = -1;
return QString();
}
if (offset == -1)
offset = lineEdit()->text().length();
if (offset == -2)
offset = cursorPosition();
return QAccessibleTextInterface::textAtOffset(offset, boundaryType, startOffset, endOffset);

View File

@ -0,0 +1,167 @@
/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met: http://www.gnu.org/copyleft/gpl.html.
**
**
** $QT_END_LICENSE$
**
****************************************************************************/
#ifndef ACCESSIBLEWIDGETS_H
#define ACCESSIBLEWIDGETS_H
#include <QtWidgets/qaccessiblewidget.h>
#include <QtWidgets/qpushbutton.h>
class QtTestAccessibleWidget: public QWidget
{
Q_OBJECT
public:
QtTestAccessibleWidget(QWidget *parent, const char *name): QWidget(parent)
{
setObjectName(name);
}
};
class QtTestAccessibleWidgetIface: public QAccessibleWidget
{
public:
QtTestAccessibleWidgetIface(QtTestAccessibleWidget *w): QAccessibleWidget(w) {}
QString text(QAccessible::Text t) const Q_DECL_OVERRIDE
{
if (t == QAccessible::Help)
return QString::fromLatin1("Help yourself");
return QAccessibleWidget::text(t);
}
static QAccessibleInterface *ifaceFactory(const QString &key, QObject *o)
{
if (key == "QtTestAccessibleWidget")
return new QtTestAccessibleWidgetIface(static_cast<QtTestAccessibleWidget*>(o));
return 0;
}
};
class QtTestAccessibleWidgetSubclass: public QtTestAccessibleWidget
{
Q_OBJECT
public:
QtTestAccessibleWidgetSubclass(QWidget *parent, const char *name): QtTestAccessibleWidget(parent, name)
{}
};
class KFooButton: public QPushButton
{
Q_OBJECT
public:
KFooButton(const QString &text, QWidget* parent = 0)
: QPushButton(text, parent)
{}
};
class CustomTextWidget : public QWidget
{
Q_OBJECT
public:
int cursorPosition;
QString text;
};
class CustomTextWidgetIface: public QAccessibleWidget, public QAccessibleTextInterface
{
public:
static QAccessibleInterface *ifaceFactory(const QString &key, QObject *o)
{
if (key == "CustomTextWidget")
return new CustomTextWidgetIface(static_cast<CustomTextWidget*>(o));
return 0;
}
CustomTextWidgetIface(CustomTextWidget *w): QAccessibleWidget(w) {}
void *interface_cast(QAccessible::InterfaceType t) {
if (t == QAccessible::TextInterface)
return static_cast<QAccessibleTextInterface*>(this);
return 0;
}
// this is mostly to test the base implementation for textBefore/At/After
QString text(QAccessible::Text t) const Q_DECL_OVERRIDE
{
if (t == QAccessible::Value)
return textWidget()->text;
return QAccessibleWidget::text(t);
}
QString textBeforeOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const
{
if (offset == -2)
offset = textWidget()->cursorPosition;
return QAccessibleTextInterface::textBeforeOffset(offset, boundaryType, startOffset, endOffset);
}
QString textAtOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const
{
if (offset == -2)
offset = textWidget()->cursorPosition;
return QAccessibleTextInterface::textAtOffset(offset, boundaryType, startOffset, endOffset);
}
QString textAfterOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const
{
if (offset == -2)
offset = textWidget()->cursorPosition;
return QAccessibleTextInterface::textAfterOffset(offset, boundaryType, startOffset, endOffset);
}
void selection(int, int *startOffset, int *endOffset) const Q_DECL_OVERRIDE
{ *startOffset = *endOffset = -1; }
int selectionCount() const Q_DECL_OVERRIDE { return 0; }
void addSelection(int, int) Q_DECL_OVERRIDE {}
void removeSelection(int) Q_DECL_OVERRIDE {}
void setSelection(int, int, int) Q_DECL_OVERRIDE {}
int cursorPosition() const Q_DECL_OVERRIDE { return textWidget()->cursorPosition; }
void setCursorPosition(int position) Q_DECL_OVERRIDE { textWidget()->cursorPosition = position; }
QString text(int startOffset, int endOffset) const Q_DECL_OVERRIDE { return textWidget()->text.mid(startOffset, endOffset); }
int characterCount() const Q_DECL_OVERRIDE { return textWidget()->text.length(); }
QRect characterRect(int) const Q_DECL_OVERRIDE { return QRect(); }
int offsetAtPoint(const QPoint &) const Q_DECL_OVERRIDE { return 0; }
void scrollToSubstring(int, int) Q_DECL_OVERRIDE {}
QString attributes(int, int *, int *) const Q_DECL_OVERRIDE
{ return QString(); }
private:
CustomTextWidget *textWidget() const { return qobject_cast<CustomTextWidget *>(widget()); }
};
#endif // ACCESSIBLEWIDGETS_H

View File

@ -2,7 +2,8 @@ CONFIG += testcase
TARGET = tst_qaccessibility
requires(contains(QT_CONFIG,accessibility))
QT += testlib core-private gui-private widgets-private
SOURCES += tst_qaccessibility.cpp
SOURCES += tst_qaccessibility.cpp
HEADERS += accessiblewidgets.h
unix:!mac:LIBS+=-lm

View File

@ -75,6 +75,8 @@
#include <algorithm>
#include "accessiblewidgets.h"
// Make a widget frameless to prevent size constraints of title bars
// from interfering (Windows).
static inline void setFrameless(QWidget *w)
@ -266,6 +268,8 @@ private slots:
void lineEditTest();
void lineEditTextFunctions_data();
void lineEditTextFunctions();
void textInterfaceTest_data();
void textInterfaceTest();
void groupBoxTest();
void dialogButtonBoxTest();
void dialTest();
@ -395,43 +399,6 @@ void tst_QAccessibility::eventTest()
QTestAccessibility::clearEvents();
}
class QtTestAccessibleWidget: public QWidget
{
Q_OBJECT
public:
QtTestAccessibleWidget(QWidget *parent, const char *name): QWidget(parent)
{
setObjectName(name);
}
};
class QtTestAccessibleWidgetIface: public QAccessibleWidget
{
public:
QtTestAccessibleWidgetIface(QtTestAccessibleWidget *w): QAccessibleWidget(w) {}
QString text(QAccessible::Text t) const
{
if (t == QAccessible::Help)
return QString::fromLatin1("Help yourself");
return QAccessibleWidget::text(t);
}
static QAccessibleInterface *ifaceFactory(const QString &key, QObject *o)
{
if (key == "QtTestAccessibleWidget")
return new QtTestAccessibleWidgetIface(static_cast<QtTestAccessibleWidget*>(o));
return 0;
}
};
class QtTestAccessibleWidgetSubclass: public QtTestAccessibleWidget
{
Q_OBJECT
public:
QtTestAccessibleWidgetSubclass(QWidget *parent, const char *name): QtTestAccessibleWidget(parent, name)
{}
};
void tst_QAccessibility::customWidget()
{
{
@ -491,14 +458,6 @@ void tst_QAccessibility::deletedWidget()
// fixme: QVERIFY(!iface->isValid());
}
class KFooButton: public QPushButton
{
Q_OBJECT
public:
KFooButton(const QString &text, QWidget* parent = 0) : QPushButton(text, parent)
{}
};
void tst_QAccessibility::subclassedWidget()
{
KFooButton button("Ploink", 0);
@ -2381,6 +2340,98 @@ void tst_QAccessibility::lineEditTextFunctions()
QTestAccessibility::clearEvents();
}
void tst_QAccessibility::textInterfaceTest_data()
{
lineEditTextFunctions_data();
QString hello = QStringLiteral("hello\nworld\nend");
QTest::newRow("multi line at 0") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 0 << 0 << 6 << "hello\n";
QTest::newRow("multi line at 1") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 1 << 0 << 6 << "hello\n";
QTest::newRow("multi line at 2") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 2 << 0 << 6 << "hello\n";
QTest::newRow("multi line at 5") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 5 << 0 << 6 << "hello\n";
QTest::newRow("multi line at 6") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 6 << 6 << 12 << "world\n";
QTest::newRow("multi line at 7") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 7 << 6 << 12 << "world\n";
QTest::newRow("multi line at 8") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 8 << 6 << 12 << "world\n";
QTest::newRow("multi line at 10") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 10 << 6 << 12 << "world\n";
QTest::newRow("multi line at 11") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 11 << 6 << 12 << "world\n";
QTest::newRow("multi line at 12") << hello << 1 << (int) QAccessible::LineBoundary << 0 << 12 << 12 << 15 << "end";
QTest::newRow("multi line before 0") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 0 << -1 << -1 << "";
QTest::newRow("multi line before 1") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 1 << -1 << -1 << "";
QTest::newRow("multi line before 2") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 2 << -1 << -1 << "";
QTest::newRow("multi line before 5") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 5 << -1 << -1 << "";
QTest::newRow("multi line before 6") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 6 << 0 << 6 << "hello\n";
QTest::newRow("multi line before 7") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 7 << 0 << 6 << "hello\n";
QTest::newRow("multi line before 8") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 8 << 0 << 6 << "hello\n";
QTest::newRow("multi line before 10") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 10 << 0 << 6 << "hello\n";
QTest::newRow("multi line before 11") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 11 << 0 << 6 << "hello\n";
QTest::newRow("multi line before 12") << hello << 0 << (int) QAccessible::LineBoundary << 0 << 12 << 6 << 12 << "world\n";
QTest::newRow("multi line after 0") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 0 << 6 << 12 << "world\n";
QTest::newRow("multi line after 1") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 1 << 6 << 12 << "world\n";
QTest::newRow("multi line after 2") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 2 << 6 << 12 << "world\n";
QTest::newRow("multi line after 5") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 5 << 6 << 12 << "world\n";
QTest::newRow("multi line after 6") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 6 << 12 << 15 << "end";
QTest::newRow("multi line after 7") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 7 << 12 << 15 << "end";
QTest::newRow("multi line after 8") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 8 << 12 << 15 << "end";
QTest::newRow("multi line after 10") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 10 << 12 << 15 << "end";
QTest::newRow("multi line after 11") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 11 << 12 << 15 << "end";
QTest::newRow("multi line after 12") << hello << 2 << (int) QAccessible::LineBoundary << 0 << 12 << -1 << -1 << "";
QTest::newRow("before 4 \\nFoo\\n") << QStringLiteral("\nFoo\n") << 0 << (int) QAccessible::LineBoundary << 0 << 4 << 0 << 1 << "\n";
QTest::newRow("at 4 \\nFoo\\n") << QStringLiteral("\nFoo\n") << 1 << (int) QAccessible::LineBoundary << 0 << 4 << 1 << 5 << "Foo\n";
QTest::newRow("after 4 \\nFoo\\n") << QStringLiteral("\nFoo\n") << 2 << (int) QAccessible::LineBoundary << 0 << 4 << 5 << 5 << "";
QTest::newRow("before 4 Foo\\nBar\\n") << QStringLiteral("Foo\nBar\n") << 0 << (int) QAccessible::LineBoundary << 0 << 7 << 0 << 4 << "Foo\n";
QTest::newRow("at 4 Foo\\nBar\\n") << QStringLiteral("Foo\nBar\n") << 1 << (int) QAccessible::LineBoundary << 0 << 7 << 4 << 8 << "Bar\n";
QTest::newRow("after 4 Foo\\nBar\\n") << QStringLiteral("Foo\nBar\n") << 2 << (int) QAccessible::LineBoundary << 0 << 7 << 8 << 8 << "";
QTest::newRow("at 0 Foo\\n") << QStringLiteral("Foo\n") << 1 << (int) QAccessible::LineBoundary << 0 << 0 << 0 << 4 << "Foo\n";
}
void tst_QAccessibility::textInterfaceTest()
{
QFETCH(QString, text);
QFETCH(int, textFunction);
QFETCH(int, boundaryType);
QFETCH(int, cursorPosition);
QFETCH(int, offset);
QFETCH(int, expectedStart);
QFETCH(int, expectedEnd);
QFETCH(QString, expectedText);
QAccessible::installFactory(CustomTextWidgetIface::ifaceFactory);
CustomTextWidget *w = new CustomTextWidget();
w->text = text;
w->cursorPosition = cursorPosition;
QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(w);
QVERIFY(iface);
QCOMPARE(iface->text(QAccessible::Value), text);
QAccessibleTextInterface *textIface = iface->textInterface();
QVERIFY(textIface);
int start = -33;
int end = -33;
QString result;
switch (textFunction) {
case 0:
result = textIface->textBeforeOffset(offset, (QAccessible::TextBoundaryType) boundaryType, &start, &end);
break;
case 1:
result = textIface->textAtOffset(offset, (QAccessible::TextBoundaryType) boundaryType, &start, &end);
break;
case 2:
result = textIface->textAfterOffset(offset, (QAccessible::TextBoundaryType) boundaryType, &start, &end);
break;
}
QCOMPARE(result, expectedText);
QCOMPARE(start, expectedStart);
QCOMPARE(end, expectedEnd);
delete w;
QAccessible::removeFactory(CustomTextWidgetIface::ifaceFactory);
QTestAccessibility::clearEvents();
}
void tst_QAccessibility::groupBoxTest()
{
{