Add isTabFence private flag
authorGabriel de Dietrich <gabriel.dedietrich@theqtcompany.com>
Thu, 2 Jul 2015 16:20:41 +0000 (18:20 +0200)
committerGabriel de Dietrich <gabriel.dedietrich@theqtcompany.com>
Fri, 21 Aug 2015 12:43:30 +0000 (12:43 +0000)
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 <mitch.curtis@theqtcompany.com>
src/quick/items/qquickitem.cpp
src/quick/items/qquickitem_p.h
tests/auto/quick/qquickitem2/data/tabFence.qml [new file with mode: 0644]
tests/auto/quick/qquickitem2/tst_qquickitem.cpp

index 0c9ee4fe73f5fe77504780f4f2f6cb4549a2567e..80bad4dc27a0dade65b765a1865f806a243224e2 100644 (file)
@@ -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<QQuickItem *> &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<QQuickItem *> &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)
index 942b51bf689e680060697e36b7f90e7311dd1b07..6670975f20a9e283c81a9acbad1254a4c7c4f3cf 100644 (file)
@@ -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 (file)
index 0000000..fcf69b4
--- /dev/null
@@ -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"
+    }
+}
index 396f183860c4e4860e836ee6bfa7f6716768ca63..9f3de8292f5e47c5b7034da1a8790f5101ca5aaa 100644 (file)
@@ -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<KeyTestItem>("Test",1,0,"KeyTestItem");
     qmlRegisterType<HollowTestItem>("Test", 1, 0, "HollowTestItem");
+    qmlRegisterType<TabFenceItem>("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<QQuickItem>(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<QQuickItem>(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);