/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQuick module 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 The Qt Company. For licensing terms ** and conditions see https://www.qt.io/terms-conditions. For further ** information use the contact form at https://www.qt.io/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 3 as published by the Free Software ** Foundation and appearing in the file LICENSE.LGPL3 included in the ** packaging of this file. Please review the following information to ** ensure the GNU Lesser General Public License version 3 requirements ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. ** ** GNU General Public License Usage ** Alternatively, this file may be used under the terms of the GNU ** General Public License version 2.0 or (at your option) the GNU General ** Public license version 3 or any later version approved by the KDE Free ** Qt Foundation. The licenses are as published by the Free Software ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 ** included in the packaging of this file. Please review the following ** information to ensure the GNU General Public License requirements will ** be met: https://www.gnu.org/licenses/gpl-2.0.html and ** https://www.gnu.org/licenses/gpl-3.0.html. ** ** $QT_END_LICENSE$ ** ****************************************************************************/ #include "qaccessiblequickitem_p.h" #include #include "QtQuick/private/qquickitem_p.h" #include "QtQuick/private/qquicktext_p.h" #include "QtQuick/private/qquicktextinput_p.h" #include "QtQuick/private/qquickaccessibleattached_p.h" #include "QtQuick/qquicktextdocument.h" QT_BEGIN_NAMESPACE #if QT_CONFIG(accessibility) QAccessibleQuickItem::QAccessibleQuickItem(QQuickItem *item) : QAccessibleObject(item), m_doc(textDocument()) { } QWindow *QAccessibleQuickItem::window() const { return item()->window(); } int QAccessibleQuickItem::childCount() const { return childItems().count(); } QRect QAccessibleQuickItem::rect() const { const QRect r = itemScreenRect(item()); return r; } QRect QAccessibleQuickItem::viewRect() const { // ### no window in some cases. if (!item()->window()) { return QRect(); } QQuickWindow *window = item()->window(); QPoint screenPos = window->mapToGlobal(QPoint(0,0)); return QRect(screenPos, window->size()); } bool QAccessibleQuickItem::clipsChildren() const { return static_cast(item())->clip(); } QAccessibleInterface *QAccessibleQuickItem::childAt(int x, int y) const { if (item()->clip()) { if (!rect().contains(x, y)) return nullptr; } const QList kids = accessibleUnignoredChildren(item(), true); for (int i = kids.count() - 1; i >= 0; --i) { QAccessibleInterface *childIface = QAccessible::queryAccessibleInterface(kids.at(i)); if (QAccessibleInterface *childChild = childIface->childAt(x, y)) return childChild; if (childIface && !childIface->state().invisible) { if (childIface->rect().contains(x, y)) return childIface; } } return nullptr; } QAccessibleInterface *QAccessibleQuickItem::parent() const { QQuickItem *parent = item()->parentItem(); QQuickWindow *window = item()->window(); QQuickItem *ci = window ? window->contentItem() : nullptr; while (parent && !QQuickItemPrivate::get(parent)->isAccessible && parent != ci) parent = parent->parentItem(); if (parent) { if (parent == ci) { // Jump out to the scene widget if the parent is the root item. // There are two root items, QQuickWindow::rootItem and // QQuickView::declarativeRoot. The former is the true root item, // but is not a part of the accessibility tree. Check if we hit // it here and return an interface for the scene instead. return QAccessible::queryAccessibleInterface(window); } else { while (parent && !parent->d_func()->isAccessible) parent = parent->parentItem(); return QAccessible::queryAccessibleInterface(parent); } } return nullptr; } QAccessibleInterface *QAccessibleQuickItem::child(int index) const { QList children = childItems(); if (index < 0 || index >= children.count()) return nullptr; QQuickItem *child = children.at(index); return QAccessible::queryAccessibleInterface(child); } int QAccessibleQuickItem::indexOfChild(const QAccessibleInterface *iface) const { QList kids = childItems(); return kids.indexOf(static_cast(iface->object())); } static void unignoredChildren(QQuickItem *item, QList *items, bool paintOrder) { const QList childItems = paintOrder ? QQuickItemPrivate::get(item)->paintOrderChildItems() : item->childItems(); for (QQuickItem *child : childItems) { if (QQuickItemPrivate::get(child)->isAccessible) { items->append(child); } else { unignoredChildren(child, items, paintOrder); } } } QList accessibleUnignoredChildren(QQuickItem *item, bool paintOrder) { QList items; unignoredChildren(item, &items, paintOrder); return items; } QList QAccessibleQuickItem::childItems() const { return accessibleUnignoredChildren(item()); } QAccessible::State QAccessibleQuickItem::state() const { QQuickAccessibleAttached *attached = QQuickAccessibleAttached::attachedProperties(item()); if (!attached) return QAccessible::State(); QAccessible::State state = attached->state(); QRect viewRect_ = viewRect(); QRect itemRect = rect(); if (viewRect_.isNull() || itemRect.isNull() || !item()->window() || !item()->window()->isVisible() ||!item()->isVisible() || qFuzzyIsNull(item()->opacity())) state.invisible = true; if (!viewRect_.intersects(itemRect)) state.offscreen = true; if ((role() == QAccessible::CheckBox || role() == QAccessible::RadioButton) && object()->property("checked").toBool()) state.checked = true; if (item()->activeFocusOnTab() || role() == QAccessible::EditableText) state.focusable = true; if (item()->hasActiveFocus()) state.focused = true; if (role() == QAccessible::EditableText) if (auto ti = qobject_cast(item())) state.passwordEdit = ti->echoMode() != QQuickTextInput::Normal; return state; } QAccessible::Role QAccessibleQuickItem::role() const { // Workaround for setAccessibleRole() not working for // Text items. Text items are special since they are defined // entirely from C++ (setting the role from QML works.) QAccessible::Role role = QAccessible::NoRole; if (item()) role = QQuickItemPrivate::get(item())->accessibleRole(); if (role == QAccessible::NoRole) { if (qobject_cast(const_cast(item()))) role = QAccessible::StaticText; else role = QAccessible::Client; } return role; } bool QAccessibleQuickItem::isAccessible() const { return item()->d_func()->isAccessible; } QStringList QAccessibleQuickItem::actionNames() const { QStringList actions; switch (role()) { case QAccessible::PushButton: actions << QAccessibleActionInterface::pressAction(); break; case QAccessible::RadioButton: case QAccessible::CheckBox: actions << QAccessibleActionInterface::toggleAction() << QAccessibleActionInterface::pressAction(); break; case QAccessible::Slider: case QAccessible::SpinBox: case QAccessible::ScrollBar: actions << QAccessibleActionInterface::increaseAction() << QAccessibleActionInterface::decreaseAction(); break; default: break; } if (state().focusable) actions.append(QAccessibleActionInterface::setFocusAction()); // ### The following can lead to duplicate action names. if (QQuickAccessibleAttached *attached = QQuickAccessibleAttached::attachedProperties(item())) attached->availableActions(&actions); return actions; } void QAccessibleQuickItem::doAction(const QString &actionName) { bool accepted = false; if (actionName == QAccessibleActionInterface::setFocusAction()) { item()->forceActiveFocus(); accepted = true; } if (QQuickAccessibleAttached *attached = QQuickAccessibleAttached::attachedProperties(item())) accepted = attached->doAction(actionName); if (accepted) return; // Look for and call the accessible[actionName]Action() function on the item. // This allows for overriding the default action handling. const QByteArray functionName = "accessible" + actionName.toLatin1() + "Action"; if (object()->metaObject()->indexOfMethod(QByteArray(functionName + "()")) != -1) { QMetaObject::invokeMethod(object(), functionName); return; } // Role-specific default action handling follows. Items are expected to provide // properties according to role conventions. These will then be read and/or updated // by the accessibility system. // Checkable roles : checked // Value-based roles : (via the value interface: value, minimumValue, maximumValue), stepSize switch (role()) { case QAccessible::RadioButton: case QAccessible::CheckBox: { QVariant checked = object()->property("checked"); if (checked.isValid()) { if (actionName == QAccessibleActionInterface::toggleAction() || actionName == QAccessibleActionInterface::pressAction()) { object()->setProperty("checked", QVariant(!checked.toBool())); } } break; } case QAccessible::Slider: case QAccessible::SpinBox: case QAccessible::Dial: case QAccessible::ScrollBar: { if (actionName != QAccessibleActionInterface::increaseAction() && actionName != QAccessibleActionInterface::decreaseAction()) break; // Update the value using QAccessibleValueInterface, respecting // the minimum and maximum value (if set). Also check for and // use the "stepSize" property on the item if (QAccessibleValueInterface *valueIface = valueInterface()) { QVariant valueV = valueIface->currentValue(); qreal newValue = valueV.toReal(); QVariant stepSizeV = object()->property("stepSize"); qreal stepSize = stepSizeV.isValid() ? stepSizeV.toReal() : qreal(1.0); if (actionName == QAccessibleActionInterface::increaseAction()) { newValue += stepSize; } else { newValue -= stepSize; } QVariant minimumValueV = valueIface->minimumValue(); if (minimumValueV.isValid()) { newValue = qMax(newValue, minimumValueV.toReal()); } QVariant maximumValueV = valueIface->maximumValue(); if (maximumValueV.isValid()) { newValue = qMin(newValue, maximumValueV.toReal()); } valueIface->setCurrentValue(QVariant(newValue)); } break; } default: break; } } QStringList QAccessibleQuickItem::keyBindingsForAction(const QString &actionName) const { Q_UNUSED(actionName); return QStringList(); } QString QAccessibleQuickItem::text(QAccessible::Text textType) const { // handles generic behavior not specific to an item switch (textType) { case QAccessible::Name: { QVariant accessibleName = QQuickAccessibleAttached::property(object(), "name"); if (!accessibleName.isNull()) return accessibleName.toString(); break;} case QAccessible::Description: { QVariant accessibleDecription = QQuickAccessibleAttached::property(object(), "description"); if (!accessibleDecription.isNull()) return accessibleDecription.toString(); break;} #ifdef Q_ACCESSIBLE_QUICK_ITEM_ENABLE_DEBUG_DESCRIPTION case QAccessible::DebugDescription: { QString debugString; debugString = QString::fromLatin1(object()->metaObject()->className()) + QLatin1Char(' '); debugString += isAccessible() ? QLatin1String("enabled") : QLatin1String("disabled"); return debugString; break; } #endif case QAccessible::Value: case QAccessible::Help: case QAccessible::Accelerator: default: break; } // the following block handles item-specific behavior if (role() == QAccessible::EditableText) { if (textType == QAccessible::Value) { if (QTextDocument *doc = textDocument()) { return doc->toPlainText(); } QVariant text = object()->property("text"); return text.toString(); } } return QString(); } void QAccessibleQuickItem::setText(QAccessible::Text textType, const QString &text) { if (role() != QAccessible::EditableText) return; if (textType != QAccessible::Value) return; if (QTextDocument *doc = textDocument()) { doc->setPlainText(text); return; } auto textPropertyName = "text"; if (object()->metaObject()->indexOfProperty(textPropertyName) >= 0) object()->setProperty(textPropertyName, text); } void *QAccessibleQuickItem::interface_cast(QAccessible::InterfaceType t) { QAccessible::Role r = role(); if (t == QAccessible::ActionInterface) return static_cast(this); if (t == QAccessible::ValueInterface && (r == QAccessible::Slider || r == QAccessible::SpinBox || r == QAccessible::Dial || r == QAccessible::ScrollBar)) return static_cast(this); if (t == QAccessible::TextInterface && (r == QAccessible::EditableText)) return static_cast(this); return QAccessibleObject::interface_cast(t); } QVariant QAccessibleQuickItem::currentValue() const { return item()->property("value"); } void QAccessibleQuickItem::setCurrentValue(const QVariant &value) { item()->setProperty("value", value); } QVariant QAccessibleQuickItem::maximumValue() const { return item()->property("maximumValue"); } QVariant QAccessibleQuickItem::minimumValue() const { return item()->property("minimumValue"); } QVariant QAccessibleQuickItem::minimumStepSize() const { return item()->property("stepSize"); } /*! \internal Shared between QAccessibleQuickItem and QAccessibleQuickView */ QRect itemScreenRect(QQuickItem *item) { // ### no window in some cases. // ### Should we really check for 0 opacity? if (!item->window() ||!item->isVisible() || qFuzzyIsNull(item->opacity())) { return QRect(); } QSize itemSize((int)item->width(), (int)item->height()); // ### If the bounding rect fails, we first try the implicit size, then we go for the // parent size. WE MIGHT HAVE TO REVISIT THESE FALLBACKS. if (itemSize.isEmpty()) { itemSize = QSize((int)item->implicitWidth(), (int)item->implicitHeight()); if (itemSize.isEmpty() && item->parentItem()) // ### Seems that the above fallback is not enough, fallback to use the parent size... itemSize = QSize((int)item->parentItem()->width(), (int)item->parentItem()->height()); } QPointF scenePoint = item->mapToScene(QPointF(0, 0)); QPoint screenPos = item->window()->mapToGlobal(scenePoint.toPoint()); return QRect(screenPos, itemSize); } QTextDocument *QAccessibleQuickItem::textDocument() const { QVariant docVariant = item()->property("textDocument"); if (docVariant.canConvert()) { QQuickTextDocument *qqdoc = docVariant.value(); return qqdoc->textDocument(); } return nullptr; } int QAccessibleQuickItem::characterCount() const { if (m_doc) { QTextCursor cursor = QTextCursor(m_doc); cursor.movePosition(QTextCursor::End); return cursor.position(); } return text(QAccessible::Value).size(); } int QAccessibleQuickItem::cursorPosition() const { QVariant pos = item()->property("cursorPosition"); return pos.toInt(); } void QAccessibleQuickItem::setCursorPosition(int position) { item()->setProperty("cursorPosition", position); } QString QAccessibleQuickItem::text(int startOffset, int endOffset) const { if (m_doc) { QTextCursor cursor = QTextCursor(m_doc); cursor.setPosition(startOffset); cursor.setPosition(endOffset, QTextCursor::KeepAnchor); return cursor.selectedText(); } return text(QAccessible::Value).mid(startOffset, endOffset - startOffset); } QString QAccessibleQuickItem::textBeforeOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const { Q_ASSERT(startOffset); Q_ASSERT(endOffset); if (m_doc) { QTextCursor cursor = QTextCursor(m_doc); cursor.setPosition(offset); QPair boundaries = QAccessible::qAccessibleTextBoundaryHelper(cursor, boundaryType); cursor.setPosition(boundaries.first - 1); boundaries = QAccessible::qAccessibleTextBoundaryHelper(cursor, boundaryType); *startOffset = boundaries.first; *endOffset = boundaries.second; return text(boundaries.first, boundaries.second); } else { return QAccessibleTextInterface::textBeforeOffset(offset, boundaryType, startOffset, endOffset); } } QString QAccessibleQuickItem::textAfterOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const { Q_ASSERT(startOffset); Q_ASSERT(endOffset); if (m_doc) { QTextCursor cursor = QTextCursor(m_doc); cursor.setPosition(offset); QPair boundaries = QAccessible::qAccessibleTextBoundaryHelper(cursor, boundaryType); cursor.setPosition(boundaries.second); boundaries = QAccessible::qAccessibleTextBoundaryHelper(cursor, boundaryType); *startOffset = boundaries.first; *endOffset = boundaries.second; return text(boundaries.first, boundaries.second); } else { return QAccessibleTextInterface::textAfterOffset(offset, boundaryType, startOffset, endOffset); } } QString QAccessibleQuickItem::textAtOffset(int offset, QAccessible::TextBoundaryType boundaryType, int *startOffset, int *endOffset) const { Q_ASSERT(startOffset); Q_ASSERT(endOffset); if (m_doc) { QTextCursor cursor = QTextCursor(m_doc); cursor.setPosition(offset); QPair boundaries = QAccessible::qAccessibleTextBoundaryHelper(cursor, boundaryType); *startOffset = boundaries.first; *endOffset = boundaries.second; return text(boundaries.first, boundaries.second); } else { return QAccessibleTextInterface::textAtOffset(offset, boundaryType, startOffset, endOffset); } } void QAccessibleQuickItem::selection(int selectionIndex, int *startOffset, int *endOffset) const { if (selectionIndex == 0) { *startOffset = item()->property("selectionStart").toInt(); *endOffset = item()->property("selectionEnd").toInt(); } else { *startOffset = 0; *endOffset = 0; } } int QAccessibleQuickItem::selectionCount() const { if (item()->property("selectionStart").toInt() != item()->property("selectionEnd").toInt()) return 1; return 0; } void QAccessibleQuickItem::addSelection(int /* startOffset */, int /* endOffset */) { } void QAccessibleQuickItem::removeSelection(int /* selectionIndex */) { } void QAccessibleQuickItem::setSelection(int /* selectionIndex */, int /* startOffset */, int /* endOffset */) { } #endif // accessibility QT_END_NAMESPACE