Abstract QWidget focus chain management

The focus chain in widgets is implemented as a double-linked list,
using QWidgetPrivate::focus_next and focus_prev as pointers to the
next/previous widget in the focus chain.

These pointers are manipulated directly at many places, which is error
prone and difficult to read.

Build an abstraction layer and remove direct usage of focus_next and
focus_prev. Provide functions to insert and remove widgets to/from the
focus chain.

Add a test function to tst_QWidget.

Task-number: QTBUG-121478
Change-Id: Ide6379c0197137e420352a2976912f2de8a8b338
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
bb10
Axel Spoerl 2024-01-26 11:42:17 +01:00
parent f7a809ab73
commit 58d5d4b7c2
3 changed files with 542 additions and 136 deletions

View File

@ -6,6 +6,7 @@
#include "qapplication_p.h"
#include "qbrush.h"
#include "qcursor.h"
#include "private/qduplicatetracker_p.h"
#include "qevent.h"
#include "qlayout.h"
#if QT_CONFIG(menu)
@ -84,6 +85,7 @@ using namespace Qt::StringLiterals;
Q_LOGGING_CATEGORY(lcWidgetPainting, "qt.widgets.painting", QtWarningMsg);
Q_LOGGING_CATEGORY(lcWidgetShowHide, "qt.widgets.showhide", QtWarningMsg);
Q_LOGGING_CATEGORY(lcWidgetWindow, "qt.widgets.window", QtWarningMsg);
Q_LOGGING_CATEGORY(lcFocus, "qt.widgets.focus")
#ifndef QT_NO_DEBUG_STREAM
namespace {
@ -818,12 +820,7 @@ struct QWidgetExceptionCleaner
Q_UNUSED(d);
#else
QWidgetPrivate::allWidgets->remove(that);
if (d->focus_next != that) {
if (d->focus_next)
d->focus_next->d_func()->focus_prev = d->focus_prev;
if (d->focus_prev)
d->focus_prev->d_func()->focus_next = d->focus_next;
}
d->removeFromFocusChain();
#endif
}
};
@ -991,7 +988,7 @@ void QWidgetPrivate::init(QWidget *parentWidget, Qt::WindowFlags f)
//give potential windows a bigger "pre-initial" size; create() will give them a new size later
data.crect = parentWidget ? QRect(0,0,100,30) : QRect(0,0,640,480);
focus_next = focus_prev = q;
initFocusChain();
if ((f & Qt::WindowType_Mask) == Qt::Desktop)
q->create();
@ -1477,17 +1474,9 @@ QWidget::~QWidget()
// delete layout while we still are a valid widget
delete d->layout;
d->layout = nullptr;
// Remove myself from focus list
Q_ASSERT(d->focus_next->d_func()->focus_prev == this);
Q_ASSERT(d->focus_prev->d_func()->focus_next == this);
if (d->focus_next != this) {
d->focus_next->d_func()->focus_prev = d->focus_prev;
d->focus_prev->d_func()->focus_next = d->focus_next;
d->focus_next = d->focus_prev = nullptr;
}
// Remove this from focus list
d->removeFromFocusChain(QWidgetPrivate::FocusChainRemovalRule::AssertConsistency);
QT_TRY {
#if QT_CONFIG(graphicsview)
@ -6406,39 +6395,18 @@ void QWidget::setFocusProxy(QWidget * w)
break;
}
Q_ASSERT(firstChild); // can't be nullptr since w is a child
QWidget *oldNext = d->focus_next;
QWidget *oldPrev = d->focus_prev;
oldNext->d_func()->focus_prev = oldPrev;
oldPrev->d_func()->focus_next = oldNext;
oldPrev = firstChild->d_func()->focus_prev;
d->focus_next = firstChild;
d->focus_prev = oldPrev;
oldPrev->d_func()->focus_next = this;
firstChild->d_func()->focus_prev = this;
d->insertIntoFocusChainBefore(firstChild);
} else if (w && w->isAncestorOf(this)) {
// If the focus proxy is a parent, 'this' has to be inserted directly after its parent in the focus chain
// remove it from the chain and insert this into the focus chain after its parent
// is this the case already?
QWidget *parentsNext = w->d_func()->focus_next;
QWidget *parentsNext = w->nextInFocusChain();
if (parentsNext == this) {
// nothing to do.
Q_ASSERT(d->focus_prev == w);
Q_ASSERT(previousInFocusChain() == w);
} else {
// Remove 'this' from the focus chain by making prev and next point directly to each other
QWidget *myOldNext = d->focus_next;
QWidget *myOldPrev = d->focus_prev;
if (myOldNext && myOldPrev) {
myOldNext->d_func()->focus_prev = myOldPrev;
myOldPrev->d_func()->focus_next = myOldNext;
}
// Insert 'this' behind the parent
w->d_func()->focus_next = this;
d->focus_prev = w;
d->focus_next = parentsNext;
parentsNext->d_func()->focus_prev = this;
d->QWidgetPrivate::insertIntoFocusChainAfter(w);
}
}
@ -6871,7 +6839,8 @@ QObject *QWidgetPrivate::focusObject()
*/
QWidget *QWidget::nextInFocusChain() const
{
return const_cast<QWidget *>(d_func()->focus_next);
Q_D(const QWidget);
return d->nextPrevElementInFocusChain(QWidgetPrivate::FocusDirection::Next);
}
/*!
@ -6884,7 +6853,8 @@ QWidget *QWidget::nextInFocusChain() const
*/
QWidget *QWidget::previousInFocusChain() const
{
return const_cast<QWidget *>(d_func()->focus_prev);
Q_D(const QWidget);
return d->nextPrevElementInFocusChain(QWidgetPrivate::FocusDirection::Previous);
}
/*!
@ -7039,9 +7009,9 @@ void QWidget::setTabOrder(QWidget* first, QWidget *second)
}
} else if (target->isAncestorOf(focusProxy)) {
lastFocusChild = focusProxy;
for (QWidget *focusNext = lastFocusChild->d_func()->focus_next;
for (QWidget *focusNext = lastFocusChild->nextInFocusChain();
focusNext != focusProxy && target->isAncestorOf(focusNext) && focusNext->window() == focusProxy->window();
focusNext = focusNext->d_func()->focus_next) {
focusNext = focusNext->nextInFocusChain()) {
if (focusNext == noFurtherThan)
break;
if (focusNext->focusPolicy() != Qt::NoFocus)
@ -7050,13 +7020,6 @@ void QWidget::setTabOrder(QWidget* first, QWidget *second)
}
return lastFocusChild;
};
auto setPrev = [](QWidget *w, QWidget *prev) {
w->d_func()->focus_prev = prev;
};
auto setNext = [](QWidget *w, QWidget *next) {
w->d_func()->focus_next = next;
};
// detect inflection in case we have compound widgets
QWidget *lastFocusChildOfFirst = determineLastFocusChild(first, second);
if (lastFocusChildOfFirst == second)
@ -7065,28 +7028,15 @@ void QWidget::setTabOrder(QWidget* first, QWidget *second)
if (lastFocusChildOfSecond == first)
lastFocusChildOfSecond = second;
// remove the second widget from the chain
{
QWidget *oldPrev = second->d_func()->focus_prev;
QWidget *prevWithFocus = oldPrev;
while (prevWithFocus->focusPolicy() == Qt::NoFocus)
prevWithFocus = prevWithFocus->d_func()->focus_prev;
// only widgets between first and second -> all is fine
if (prevWithFocus == first)
return;
QWidget *oldNext = lastFocusChildOfSecond->d_func()->focus_next;
setPrev(oldNext, oldPrev);
setNext(oldPrev, oldNext);
}
// insert the second widget into the chain
{
QWidget *oldNext = lastFocusChildOfFirst->d_func()->focus_next;
setPrev(second, lastFocusChildOfFirst);
setNext(lastFocusChildOfFirst, second);
setPrev(oldNext, lastFocusChildOfSecond);
setNext(lastFocusChildOfSecond, oldNext);
}
// Return if only NoFocus widgets are between first and second
QWidget *oldPrev = second->previousInFocusChain();
QWidget *prevWithFocus = oldPrev;
while (prevWithFocus->focusPolicy() == Qt::NoFocus)
prevWithFocus = prevWithFocus->previousInFocusChain();
if (prevWithFocus == first)
return;
const QWidgetList chain = QWidgetPrivate::takeFromFocusChain(second, lastFocusChildOfSecond);
QWidgetPrivate::insertIntoFocusChain(chain, QWidgetPrivate::FocusDirection::Next, lastFocusChildOfFirst);
}
void QWidget::setTabOrder(std::initializer_list<QWidget *> widgets)
@ -7124,67 +7074,8 @@ void QWidgetPrivate::reparentFocusWidgets(QWidget * oldtlw)
if (focus_child)
focus_child->clearFocus();
// separate the focus chain into new (children of myself) and old (the rest)
QWidget *firstOld = nullptr;
//QWidget *firstNew = q; //invariant
QWidget *o = nullptr; // last in the old list
QWidget *n = q; // last in the new list
bool prevWasNew = true;
QWidget *w = focus_next;
//Note: for efficiency, we do not maintain the list invariant inside the loop
//we append items to the relevant list, and we optimize by not changing pointers
//when subsequent items are going into the same list.
while (w != q) {
bool currentIsNew = q->isAncestorOf(w);
if (currentIsNew) {
if (!prevWasNew) {
//prev was old -- append to new list
n->d_func()->focus_next = w;
w->d_func()->focus_prev = n;
}
n = w;
} else {
if (prevWasNew) {
//prev was new -- append to old list, if there is one
if (o) {
o->d_func()->focus_next = w;
w->d_func()->focus_prev = o;
} else {
// "create" the old list
firstOld = w;
}
}
o = w;
}
w = w->d_func()->focus_next;
prevWasNew = currentIsNew;
}
//repair the old list:
if (firstOld) {
o->d_func()->focus_next = firstOld;
firstOld->d_func()->focus_prev = o;
}
if (!q->isWindow()) {
QWidget *topLevel = q->window();
//insert new chain into toplevel's chain
QWidget *prev = topLevel->d_func()->focus_prev;
topLevel->d_func()->focus_prev = n;
prev->d_func()->focus_next = q;
focus_prev = prev;
n->d_func()->focus_next = topLevel;
} else {
//repair the new list
n->d_func()->focus_next = q;
focus_prev = n;
}
insertIntoFocusChain(QWidgetPrivate::FocusDirection::Previous, q->window());
reparentFocusChildren(QWidgetPrivate::FocusDirection::Next);
}
/*!
@ -13371,6 +13262,399 @@ QDebug operator<<(QDebug debug, const QWidget *widget)
}
#endif // !QT_NO_DEBUG_STREAM
// *************************** Focus abstraction ************************************
#define FOCUS_NEXT(w) w->d_func()->focus_next
#define FOCUS_PREV(w) w->d_func()->focus_prev
/*!
\internal
\return next or previous element in the focus chain, depending on
\param direction, irrespective of focus proxies or widgets with Qt::NoFocus.
*/
QWidget *QWidgetPrivate::nextPrevElementInFocusChain(FocusDirection direction) const
{
Q_Q(const QWidget);
return direction == FocusDirection::Next ? FOCUS_NEXT(q) : FOCUS_PREV(q);
}
/*!
\internal
Removes a widget from the focus chain, respecting the flags set in \param rules.
\list
\li EnsureFocusOut: If the widget has input focus, transfer focus to the next or previous widget
in the focus chain, depending on \param direction.
\li RemoveInconsistent: Remove the widget, even if its focus chain is inconsistent.
\li AssertConsistency: qFatal, if the focus chain is inconsistent. This is used in the QWidget destructor.
\endlist
\return \c true if the widget has been removed, otherwise \c false.
*/
bool QWidgetPrivate::removeFromFocusChain(FocusChainRemovalRules rules, FocusDirection direction)
{
Q_Q(QWidget);
if (!isFocusChainConsistent()) {
#ifdef QT_DEBUG
if (rules.testFlag(FocusChainRemovalRule::AssertConsistency))
qFatal() << q << "has inconsistent focus chain.";
#endif
qCDebug(lcFocus) << q << "wasn't removed, because of inconsistent focus chain.";
return false;
}
if (!isInFocusChain()) {
qCDebug(lcFocus) << q << "wasn't removed, because it is not part of a focus chain.";
return false;
}
if (rules.testFlag(FocusChainRemovalRule::EnsureFocusOut))
q->focusNextPrevChild(direction == FocusDirection::Next);
FOCUS_NEXT(FOCUS_PREV(q)) = FOCUS_NEXT(q);
FOCUS_PREV(FOCUS_NEXT(q)) = FOCUS_PREV(q);
initFocusChain();
qCDebug(lcFocus) << q << "removed from focus chain.";
return true;
}
/*!
\internal
Initialises the focus chain by making the widget point to itself.
*/
void QWidgetPrivate::initFocusChain()
{
Q_Q(QWidget);
qCDebug(lcFocus) << "Initializing focus chain of" << q;
FOCUS_PREV(q) = q;
FOCUS_NEXT(q) = q;
}
/*!
\internal
Reads QWidget children, which are not part of a focus chain yet.
Inserts them into the focus chain before or after the widget,
depending on \param direction and in the order of their creation.
This is used, when QWidget::setParent() causes a widget to change toplevel windows.
*/
void QWidgetPrivate::reparentFocusChildren(FocusDirection direction)
{
Q_Q(QWidget);
QWidgetList focusChildrenInsideChain;
QDuplicateTracker<QWidget *> seen;
QWidget *widget = q->nextInFocusChain();
while (q->isAncestorOf(widget)
&& !seen.hasSeen(widget)
&& widget != q->window()) {
if (widget->focusPolicy() != Qt::NoFocus)
focusChildrenInsideChain << widget;
widget = direction == FocusDirection::Next ? widget->nextInFocusChain()
: widget->previousInFocusChain();
}
const QWidgetList children = q->findChildren<QWidget *>(Qt::FindDirectChildrenOnly);
QWidgetList focusChildrenOutsideChain;
for (auto *child : children) {
if (!focusChildrenInsideChain.contains(child))
focusChildrenOutsideChain << child;
}
if (focusChildrenOutsideChain.isEmpty())
return;
QWidget *previous = q;
for (auto *child : focusChildrenOutsideChain) {
child->d_func()->insertIntoFocusChain(direction, previous);
previous = child;
}
}
/*!
\internal
Inserts a widget into the focus chain before or after \param position, depending on
\param direction.
\return \c true, if the insertion has changed the focus chain, otherwise \c false.
*/
bool QWidgetPrivate::insertIntoFocusChain(FocusDirection direction, QWidget *position)
{
Q_Q(QWidget);
Q_ASSERT(position);
QWidget *next = FOCUS_NEXT(q);
QWidget *previous = FOCUS_PREV(q);
switch (direction) {
case FocusDirection::Next:
if (previous == position) {
qCDebug(lcFocus) << "No-op insertion." << q << "is already before" << position;
return false;
}
removeFromFocusChain(FocusChainRemovalRule::AssertConsistency);
FOCUS_NEXT(q) = FOCUS_NEXT(position);
FOCUS_PREV(FOCUS_NEXT(position)) = q;
FOCUS_NEXT(position) = q;
FOCUS_PREV(q) = position;
qCDebug(lcFocus) << q << "inserted after" << position;
break;
case FocusDirection::Previous:
if (next == position) {
qCDebug(lcFocus) << "No-op insertion." << q << "is already after" << position;
return false;
}
removeFromFocusChain(FocusChainRemovalRule::AssertConsistency);
FOCUS_PREV(q) = FOCUS_PREV(position);
FOCUS_NEXT(FOCUS_PREV(position)) = q;
FOCUS_PREV(position) = q;
FOCUS_NEXT(q) = position;
qCDebug(lcFocus) << q << "inserted before" << position;
break;
}
Q_ASSERT(isFocusChainConsistent());
return true;
}
/*!
\internal
Convenience override to insert a QWidgetList \param toBeInserted into the focus chain
before or after \param position, depending on \param direction.
\return \c true, if the insertion has changed the focus chain, otherwise \c false.
\note
\param toBeInserted must be a consistent focus chain.
*/
bool QWidgetPrivate::insertIntoFocusChain(const QWidgetList &toBeInserted,
FocusDirection direction, QWidget *position)
{
if (toBeInserted.isEmpty()) {
qCDebug(lcFocus) << "No-op insertion of an empty list";
return false;
}
Q_ASSERT_X(!toBeInserted.contains(position),
Q_FUNC_INFO,
"Coding error: toBeInserted contains position");
QWidget *first = toBeInserted.constFirst();
QWidget *last = toBeInserted.constLast();
// Call QWidget override to log accordingly
if (toBeInserted.count() == 1)
return first->d_func()->insertIntoFocusChain(direction, position);
Q_ASSERT(first != last);
switch (direction) {
case FocusDirection::Previous:
if (FOCUS_PREV(position) == last) {
qCDebug(lcFocus) << "No-op insertion." << toBeInserted << "is already before"
<< position;
return false;
}
FOCUS_NEXT(FOCUS_PREV(position)) = first;
FOCUS_PREV(first) = FOCUS_PREV(position);
FOCUS_NEXT(last) = position;
FOCUS_PREV(position) = last;
qCDebug(lcFocus) << toBeInserted << "inserted before" << position;
break;
case FocusDirection::Next:
if (FOCUS_PREV(position) == last) {
qCDebug(lcFocus) << "No-op insertion." << toBeInserted << "is already after"
<< position;
return false;
}
FOCUS_PREV(FOCUS_NEXT(position)) = last;
FOCUS_NEXT(last) = FOCUS_NEXT(position);
FOCUS_PREV(first) = position;
FOCUS_NEXT(position) = first;
qCDebug(lcFocus) << toBeInserted << "inserted after" << position;
break;
}
Q_ASSERT(position->d_func()->isFocusChainConsistent());
return true;
}
/*!
\internal
\return a QWidgetList, representing the part of the focus chain,
starting with \param from and ending with \param to, in \param direction.
*/
QWidgetList focusPath(QWidget *from, QWidget *to, QWidgetPrivate::FocusDirection direction)
{
QWidgetList path({from});
if (from == to)
return path;
QWidget *current = from;
do {
switch (direction) {
case QWidgetPrivate::FocusDirection::Previous:
current = current->previousInFocusChain();
break;
case QWidgetPrivate::FocusDirection::Next:
current = current->nextInFocusChain();
break;
}
if (path.contains(current))
return QWidgetList();
path << current;
} while (current != to);
return path;
}
/*!
\internal
Removes the part from the focus chain starting with \param from and ending with \param to,
in \param direction.
\return removed part as a QWidgetList.
*/
QWidgetList QWidgetPrivate::takeFromFocusChain(QWidget *from,
QWidget *to,
FocusDirection direction)
{
// Check if there is a path from->to in direction
const QWidgetList path = focusPath(from, to , direction);
if (path.isEmpty()) {
qCDebug(lcFocus) << "No-op removal. Focus chain from" << from << "doesn't lead to " << to;
return QWidgetList();
}
QWidget *first = path.constFirst();
QWidget *last = path.constLast();
if (first == last) {
first->d_func()->removeFromFocusChain();
return QWidgetList({first});
}
FOCUS_NEXT(FOCUS_PREV(first)) = FOCUS_NEXT(last);
FOCUS_PREV(FOCUS_NEXT(last)) = FOCUS_PREV(first);
FOCUS_PREV(first) = last;
FOCUS_NEXT(last) = first;
qCDebug(lcFocus) << path << "removed from focus chain";
return path;
}
/*!
\internal
\return The last focus child of the widget, traversing the focus chain no further than
\param noFurtherThan.
*/
QWidget *QWidgetPrivate::determineLastFocusChild(QWidget *noFurtherThan)
{
Q_Q(QWidget);
// Since we need to repeat the same logic for both 'first' and 'second', we add a function
// that determines the last focus child for a widget, taking proxies and compound widgets into
// account. If the target is not a compound widget (it doesn't have a focus proxy that points
// to a child), 'lastFocusChild' will be set to the target itself.
QWidget *lastFocusChild = q;
QWidget *focusProxy = deepestFocusProxy();
if (!focusProxy) {
// QTBUG-81097: Another case is possible here. We can have a child
// widget, that sets its focusProxy() to the parent (target).
// An example of such widget is a QLineEdit, nested into
// a QAbstractSpinBox. In this case such widget should be considered
// the last focus child.
for (auto *object : std::as_const(q->children())) {
QWidget *w = qobject_cast<QWidget *>(object);
if (w && w->focusProxy() == q) {
lastFocusChild = w;
break;
}
}
} else if (q->isAncestorOf(focusProxy)) {
lastFocusChild = focusProxy;
for (QWidget *focusNext = lastFocusChild->nextInFocusChain();
focusNext != focusProxy && q->isAncestorOf(focusNext)
&& focusNext->window() == focusProxy->window();
focusNext = focusNext->nextInFocusChain()) {
if (focusNext == noFurtherThan)
break;
if (focusNext->focusPolicy() != Qt::NoFocus)
lastFocusChild = focusNext;
}
}
return lastFocusChild;
};
/*!
\internal
\return \c true, if the widget is part of a focus chain and \c false otherwise.
A widget is considered to be part of a focus chain, neither FOCUS_NEXT, nor FOCUS_PREV
are pointing to the widget itself.
\note
This method doesn't check the consistency of the focus chain.
If multiple widgets have been removed from the focus chain by takeFromFocusChain(),
isInFocusChain() will return \c true for all of those widgets, even if they represent
an inconsistent focus chain.
*/
bool QWidgetPrivate::isInFocusChain() const
{
Q_Q(const QWidget);
return !(FOCUS_NEXT(q) == q && FOCUS_PREV(q) == q);
}
/*!
\internal
A focus chain is consistent, when it is circular: Following the chain in either direction
has to return to the beginning. This is why a newly constructed widget points to itself,
when the focus chain has been initialized. A newly constructed widget is considered to have
a consistent focus chain, while not being part of a focus chain.
This method returns \c true early, if a widget is pointing to itself.
It returns \c false, if one of the following is detected:
\list
\li nullptr found in a previous/next pointer.
\li broken chain: widget A is B's previous, but B isn't A's next.
\li chain isn't closed: starting at A doesn't lead back to A.
\endlist
It return \c true, if none of the above is observed.
\note
The focus chain is checked only in forward direction.
This is sufficient, because the check for a broken chain asserts consistent paths
in both directions.
*/
bool QWidgetPrivate::isFocusChainConsistent() const
{
Q_Q(const QWidget);
if (!isInFocusChain())
return true;
const QWidget *position = q;
for (int i = 0; i < QApplication::allWidgets().count(); ++i) {
if (!FOCUS_PREV(position) || !FOCUS_NEXT(position)) {
qCDebug(lcFocus) << "Nullptr found at:" << position
<< "Previous pointing to" << FOCUS_PREV(position)
<< "Next pointing to" << FOCUS_NEXT(position);
return false;
}
if (!(FOCUS_PREV(FOCUS_NEXT(position)) == position
&& FOCUS_NEXT(FOCUS_PREV(position)) == position)) {
qCDebug(lcFocus) << "Inconsistent focus chain at:" << position
<< "Previous pointing to" << FOCUS_PREV(FOCUS_NEXT(position))
<< "Next pointing to" << FOCUS_NEXT(FOCUS_PREV(position));
return false;
}
position = FOCUS_NEXT(position);
if (position == q)
return true;
}
qCDebug(lcFocus) << "Focus chain leading from" << q << "to" << position << "is not closed.";
return false;
}
#undef FOCUS_NEXT
#undef FOCUS_PREV
QT_END_NAMESPACE
#include "moc_qwidget.cpp"

View File

@ -41,6 +41,7 @@
#include <private/qgesture_p.h>
#include <qpa/qplatformbackingstore.h>
#include <QtGui/private/qbackingstorerhisupport_p.h>
#include <private/qapplication_p.h>
#include <QtCore/qpointer.h>
@ -733,6 +734,40 @@ public:
uint childrenHiddenByWState : 1;
uint childrenShownByExpose : 1;
// *************************** Focus abstraction ************************************
enum class FocusDirection {
Previous,
Next,
};
enum class FocusChainRemovalRule {
EnsureFocusOut = 0x01,
AssertConsistency = 0x02,
};
Q_DECLARE_FLAGS(FocusChainRemovalRules, FocusChainRemovalRule)
// Getters
QWidget *nextPrevElementInFocusChain(FocusDirection direction) const;
// manipulators
bool removeFromFocusChain(FocusChainRemovalRules rules = FocusChainRemovalRules(),
FocusDirection direction = FocusDirection::Next);
bool insertIntoFocusChain(FocusDirection direction, QWidget *position);
static bool insertIntoFocusChain(const QWidgetList &toBeInserted, FocusDirection direction, QWidget *position);
bool insertIntoFocusChainBefore(QWidget *position)
{ return insertIntoFocusChain(FocusDirection::Previous, position); }
bool insertIntoFocusChainAfter(QWidget *position)
{ return insertIntoFocusChain(FocusDirection::Next, position); }
static QWidgetList takeFromFocusChain(QWidget *from, QWidget *to,
FocusDirection direction = FocusDirection::Next);
void reparentFocusChildren(FocusDirection direction);
QWidget *determineLastFocusChild(QWidget *noFurtherThan);
// Initialization and tests
void initFocusChain();
bool isInFocusChain() const;
bool isFocusChainConsistent() const;
// *************************** Platform specific ************************************
#if defined(Q_OS_WIN)
uint noPaintOnScreen : 1; // see qwidget.cpp ::paintEngine()

View File

@ -191,6 +191,7 @@ private slots:
void mapFromAndTo();
void focusChainOnHide();
void focusChainOnReparent();
void focusAbstraction();
void defaultTabOrder();
void reverseTabOrder();
void tabOrderWithProxy();
@ -2016,6 +2017,92 @@ static QList<QWidget *> getFocusChain(QWidget *start, bool bForward)
return ret;
}
void tst_QWidget::focusAbstraction()
{
QWidget *widget1 = new QWidget;
widget1->setObjectName("Widget 1");
QWidget *widget2 = new QWidget;
widget2->setObjectName("Widget 2");
QWidget *widget3 = new QWidget;
widget3->setObjectName("Widget 3");
QWidgetPrivate *priv1 = QWidgetPrivate::get(widget1);
QWidgetPrivate *priv2 = QWidgetPrivate::get(widget2);
QWidgetPrivate *priv3 = QWidgetPrivate::get(widget3);
// Verify initialization
QVERIFY(!priv1->isInFocusChain());
QVERIFY(!priv2->isInFocusChain());
QVERIFY(!priv3->isInFocusChain());
// Verify, that parenting builds a focus chain.
QWidget parent;
parent.setObjectName("Parent");
widget1->setParent(&parent);
widget2->setParent(&parent);
widget3->setParent(&parent);
QVERIFY(priv1->isInFocusChain());
QVERIFY(priv2->isInFocusChain());
QVERIFY(priv3->isInFocusChain());
QWidgetList expected{widget1, widget2, widget3, &parent};
QCOMPARE(getFocusChain(widget1, true), expected);
// Verify, that reparented focus children end up behind parent.
widget1->setParent(widget2);
priv2->insertIntoFocusChainAfter(widget3);
priv2->reparentFocusChildren(QWidgetPrivate::FocusDirection::Next);
expected = {widget1, &parent, widget3, widget2};
QCOMPARE(getFocusChain(widget1, true), expected);
QVERIFY(priv1->isInFocusChain());
QVERIFY(priv2->isInFocusChain());
QVERIFY(priv3->isInFocusChain());
// Check removal
priv3->removeFromFocusChain(QWidgetPrivate::FocusChainRemovalRule::AssertConsistency);
expected.removeOne(widget3);
QCOMPARE(getFocusChain(widget1, true), expected);
QVERIFY(priv1->isInFocusChain());
QVERIFY(priv2->isInFocusChain());
QVERIFY(!priv3->isInFocusChain());
// Check insert
priv3->insertIntoFocusChain(QWidgetPrivate::FocusDirection::Previous, widget1);
expected = {widget3, widget1, &parent, widget2};
QCOMPARE(getFocusChain(widget3, true), expected);
// Verify, that take doesn't break
const QWidgetList taken = QWidgetPrivate::takeFromFocusChain(widget1, widget2);
QVERIFY(priv1->isFocusChainConsistent());
expected = {widget1, &parent, widget2};
QCOMPARE(taken, expected);
QVERIFY(priv1->isInFocusChain());
QVERIFY(priv2->isInFocusChain());
QVERIFY(!priv3->isInFocusChain());
// Verify insertion of multiple widgets
QWidgetPrivate::insertIntoFocusChain(taken, QWidgetPrivate::FocusDirection::Next, widget3);
expected = {widget3, widget1, &parent, widget2};
QCOMPARE(getFocusChain(widget3, true), expected);
QVERIFY(priv1->isInFocusChain());
QVERIFY(priv2->isInFocusChain());
QVERIFY(priv2->isInFocusChain());
// Verify broken chain identification
// d'tor asserts chain consistency => repair before going out of scope
auto guard = qScopeGuard([priv2, widget3]{ priv2->focus_next = widget3; });
// Nullptr is not allowed
priv2->focus_next = nullptr;
QVERIFY(!priv1->isFocusChainConsistent());
// Chain looping back in the middle
priv2->focus_next = widget1;
QVERIFY(!priv1->isFocusChainConsistent());
// "last" element pointing to itself
priv2->focus_next = widget2;
QVERIFY(!priv1->isFocusChainConsistent());
}
void tst_QWidget::defaultTabOrder()
{
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive))