From 6328dd2d2743ed540efea89742261f24552d7611 Mon Sep 17 00:00:00 2001 From: Gabriel de Dietrich Date: Thu, 2 Jul 2015 18:20:41 +0200 Subject: [PATCH] Add isTabFence private flag When an item has this flag set, the user can't tab-navigate either out of it, or enter it. We use this flag to implement QQuickPanel as an item for platforms that only support one single top-level window. Change-Id: I1f4313912ae1c70217af0d4d21064932b50a9438 Reviewed-by: Mitch Curtis --- src/quick/items/qquickitem.cpp | 89 +++++++++++++++---- src/quick/items/qquickitem_p.h | 8 ++ .../auto/quick/qquickitem2/data/tabFence.qml | 49 ++++++++++ .../auto/quick/qquickitem2/tst_qquickitem.cpp | 72 +++++++++++++++ 4 files changed, 199 insertions(+), 19 deletions(-) create mode 100644 tests/auto/quick/qquickitem2/data/tabFence.qml diff --git a/src/quick/items/qquickitem.cpp b/src/quick/items/qquickitem.cpp index 0c9ee4fe7..80bad4dc2 100644 --- a/src/quick/items/qquickitem.cpp +++ b/src/quick/items/qquickitem.cpp @@ -2422,6 +2422,50 @@ bool QQuickItemPrivate::focusNextPrev(QQuickItem *item, bool forward) return true; } +QQuickItem *QQuickItemPrivate::nextTabChildItem(const QQuickItem *item, int start) +{ + if (!item) { + qWarning() << "QQuickItemPrivate::nextTabChildItem called with null item."; + return Q_NULLPTR; + } + const QList &children = item->childItems(); + const int count = children.count(); + if (start < 0 || start >= count) { + qWarning() << "QQuickItemPrivate::nextTabChildItem: Start index value out of range for item" << item; + return Q_NULLPTR; + } + while (start < count) { + QQuickItem *child = children.at(start); + if (!child->d_func()->isTabFence) + return child; + ++start; + } + return Q_NULLPTR; +} + +QQuickItem *QQuickItemPrivate::prevTabChildItem(const QQuickItem *item, int start) +{ + if (!item) { + qWarning() << "QQuickItemPrivate::prevTabChildItem called with null item."; + return Q_NULLPTR; + } + const QList &children = item->childItems(); + const int count = children.count(); + if (start == -1) + start = count - 1; + if (start < 0 || start >= count) { + qWarning() << "QQuickItemPrivate::prevTabChildItem: Start index value out of range for item" << item; + return Q_NULLPTR; + } + while (start >= 0) { + QQuickItem *child = children.at(start); + if (!child->d_func()->isTabFence) + return child; + --start; + } + return Q_NULLPTR; +} + QQuickItem* QQuickItemPrivate::nextPrevItemInTabFocusChain(QQuickItem *item, bool forward) { Q_ASSERT(item); @@ -2444,7 +2488,6 @@ QQuickItem* QQuickItemPrivate::nextPrevItemInTabFocusChain(QQuickItem *item, boo from = item->parentItem(); } bool skip = false; - const QQuickItem * const originalItem = item; QQuickItem * startItem = item; QQuickItem * firstFromItem = from; QQuickItem *current = item; @@ -2453,46 +2496,53 @@ QQuickItem* QQuickItemPrivate::nextPrevItemInTabFocusChain(QQuickItem *item, boo QQuickItem *last = current; bool hasChildren = !current->childItems().isEmpty() && current->isEnabled() && current->isVisible(); + QQuickItem *firstChild = Q_NULLPTR; + QQuickItem *lastChild = Q_NULLPTR; + if (hasChildren) { + firstChild = nextTabChildItem(current, 0); + if (!firstChild) + hasChildren = false; + else + lastChild = prevTabChildItem(current, -1); + } + bool isTabFence = current->d_func()->isTabFence; // coming from parent: check children if (hasChildren && from == current->parentItem()) { if (forward) { - current = current->childItems().first(); + current = firstChild; } else { - current = current->childItems().last(); + current = lastChild; if (!current->childItems().isEmpty()) skip = true; } - } else if (hasChildren && forward && from != current->childItems().last()) { + } else if (hasChildren && forward && from != lastChild) { // not last child going forwards int nextChild = current->childItems().indexOf(from) + 1; - current = current->childItems().at(nextChild); - } else if (hasChildren && !forward && from != current->childItems().first()) { + current = nextTabChildItem(current, nextChild); + } else if (hasChildren && !forward && from != firstChild) { // not first child going backwards int prevChild = current->childItems().indexOf(from) - 1; - current = current->childItems().at(prevChild); + current = prevTabChildItem(current, prevChild); if (!current->childItems().isEmpty()) skip = true; // back to the parent - } else if (current->parentItem()) { - current = current->parentItem(); + } else if (QQuickItem *parent = !isTabFence ? current->parentItem() : Q_NULLPTR) { // we would evaluate the parent twice, thus we skip if (forward) { skip = true; - } else if (!forward && !current->childItems().isEmpty()) { - if (last != current->childItems().first()) { - skip = true; - } else if (last == current->childItems().first()) { - if (current->isFocusScope() && current->activeFocusOnTab() && current->hasActiveFocus()) + } else if (QQuickItem *firstSibling = !forward ? nextTabChildItem(parent, 0) : Q_NULLPTR) { + if (last != firstSibling + || (parent->isFocusScope() && parent->activeFocusOnTab() && parent->hasActiveFocus())) skip = true; - } } + current = parent; } else if (hasChildren) { // Wrap around after checking all items forward if (forward) { - current = current->childItems().first(); + current = firstChild; } else { - current = current->childItems().last(); + current = lastChild; if (!current->childItems().isEmpty()) skip = true; } @@ -2500,9 +2550,9 @@ QQuickItem* QQuickItemPrivate::nextPrevItemInTabFocusChain(QQuickItem *item, boo from = last; if (current == startItem && from == firstFromItem) { // wrapped around, avoid endless loops - if (originalItem == contentItem) { + if (item == contentItem) { qCDebug(DBG_FOCUS) << "QQuickItemPrivate::nextPrevItemInTabFocusChain: looped, return contentItem"; - return item->window()->contentItem(); + return item; } else { qCDebug(DBG_FOCUS) << "QQuickItemPrivate::nextPrevItemInTabFocusChain: looped, return " << startItem; return startItem; @@ -3026,6 +3076,7 @@ QQuickItemPrivate::QQuickItemPrivate() , activeFocusOnTab(false) , implicitAntialiasing(false) , antialiasingValid(false) + , isTabFence(false) , dirtyAttributes(0) , nextDirtyItem(0) , prevDirtyItem(0) diff --git a/src/quick/items/qquickitem_p.h b/src/quick/items/qquickitem_p.h index 942b51bf6..6670975f2 100644 --- a/src/quick/items/qquickitem_p.h +++ b/src/quick/items/qquickitem_p.h @@ -427,6 +427,12 @@ public: bool activeFocusOnTab:1; bool implicitAntialiasing:1; bool antialiasingValid:1; + // isTabFence: When true, the item acts as a fence within the tab focus chain. + // This means that the item and its children will be skipped from the tab focus + // chain when navigating from its parent or any of its siblings. Similarly, + // when any of the item's descendants gets focus, the item constrains the tab + // focus chain and prevents tabbing outside. + bool isTabFence:1; enum DirtyType { TransformOrigin = 0x00000001, @@ -498,6 +504,8 @@ public: void itemToParentTransform(QTransform &) const; static bool focusNextPrev(QQuickItem *item, bool forward); + static QQuickItem *nextTabChildItem(const QQuickItem *item, int start); + static QQuickItem *prevTabChildItem(const QQuickItem *item, int start); static QQuickItem *nextPrevItemInTabFocusChain(QQuickItem *item, bool forward); static bool canAcceptTabFocus(QQuickItem *item); diff --git a/tests/auto/quick/qquickitem2/data/tabFence.qml b/tests/auto/quick/qquickitem2/data/tabFence.qml new file mode 100644 index 000000000..fcf69b418 --- /dev/null +++ b/tests/auto/quick/qquickitem2/data/tabFence.qml @@ -0,0 +1,49 @@ +import QtQuick 2.1 +import Test 1.0 + +Item { + objectName: "root" + focus: true + width: 800 + height: 600 + + TabFence { + objectName: "fence1" + + TextInput { + objectName: "input11" + activeFocusOnTab: true + } + TextInput { + objectName: "input12" + activeFocusOnTab: true + } + TextInput { + objectName: "input13" + activeFocusOnTab: true + } + } + + TextInput { + objectName: "input1" + activeFocusOnTab: true + } + + TextInput { + objectName: "input2" + activeFocusOnTab: true + } + + TabFence { + objectName: "fence2" + } + + TextInput { + objectName: "input3" + activeFocusOnTab: true + } + + TabFence { + objectName: "fence3" + } +} diff --git a/tests/auto/quick/qquickitem2/tst_qquickitem.cpp b/tests/auto/quick/qquickitem2/tst_qquickitem.cpp index 396f18386..9f3de8292 100644 --- a/tests/auto/quick/qquickitem2/tst_qquickitem.cpp +++ b/tests/auto/quick/qquickitem2/tst_qquickitem.cpp @@ -73,6 +73,8 @@ private slots: void nextItemInFocusChain2(); void nextItemInFocusChain3(); + void tabFence(); + void keys(); void standardKeys_data(); void standardKeys(); @@ -289,6 +291,20 @@ private: QML_DECLARE_TYPE(HollowTestItem); +class TabFenceItem : public QQuickItem +{ + Q_OBJECT + +public: + TabFenceItem(QQuickItem *parent = Q_NULLPTR) + : QQuickItem(parent) + { + QQuickItemPrivate *d = QQuickItemPrivate::get(this); + d->isTabFence = true; + } +}; + +QML_DECLARE_TYPE(TabFenceItem); tst_QQuickItem::tst_QQuickItem() { @@ -299,6 +315,7 @@ void tst_QQuickItem::initTestCase() QQmlDataTest::initTestCase(); qmlRegisterType("Test",1,0,"KeyTestItem"); qmlRegisterType("Test", 1, 0, "HollowTestItem"); + qmlRegisterType("Test", 1, 0, "TabFence"); } void tst_QQuickItem::cleanup() @@ -1120,6 +1137,61 @@ void tst_QQuickItem::nextItemInFocusChain3() QCOMPARE(QGuiApplication::focusWindow(), window); } +void verifyTabFocusChain(QQuickView *window, const char **focusChain, bool forward) +{ + int idx = 0; + for (const char **objectName = focusChain; *objectName; ++objectName, ++idx) { + const QString &descrStr = QString("idx=%1 objectName=\"%2\"").arg(idx).arg(*objectName); + const char *descr = descrStr.toLocal8Bit().data(); + QKeyEvent key(QEvent::KeyPress, Qt::Key_Tab, forward ? Qt::NoModifier : Qt::ShiftModifier); + QGuiApplication::sendEvent(window, &key); + QVERIFY2(key.isAccepted(), descr); + + QQuickItem *item = findItem(window->rootObject(), *objectName); + QVERIFY2(item, descr); + QVERIFY2(item->hasActiveFocus(), descr); + } +} + +void tst_QQuickItem::tabFence() +{ + QQuickView *window = new QQuickView(0); + window->setBaseSize(QSize(800,600)); + + window->setSource(testFileUrl("tabFence.qml")); + window->show(); + window->requestActivate(); + QVERIFY(QTest::qWaitForWindowActive(window)); + QVERIFY(QGuiApplication::focusWindow() == window); + QVERIFY(window->rootObject()->hasActiveFocus()); + + const char *rootTabFocusChain[] = { + "input1", "input2", "input3", "input1", Q_NULLPTR + }; + verifyTabFocusChain(window, rootTabFocusChain, true /* forward */); + + const char *rootBacktabFocusChain[] = { + "input3", "input2", "input1", "input3", Q_NULLPTR + }; + verifyTabFocusChain(window, rootBacktabFocusChain, false /* forward */); + + // Give focus to input11 in fence1 + QQuickItem *item = findItem(window->rootObject(), "input11"); + item->setFocus(true); + QVERIFY(item); + QVERIFY(item->hasActiveFocus()); + + const char *fence1TabFocusChain[] = { + "input12", "input13", "input11", "input12", Q_NULLPTR + }; + verifyTabFocusChain(window, fence1TabFocusChain, true /* forward */); + + const char *fence1BacktabFocusChain[] = { + "input11", "input13", "input12", "input11", Q_NULLPTR + }; + verifyTabFocusChain(window, fence1BacktabFocusChain, false /* forward */); +} + void tst_QQuickItem::keys() { QQuickView *window = new QQuickView(0); -- 2.34.1