From 9ff40b59da58160dc26c54204a615a2456e07405 Mon Sep 17 00:00:00 2001 From: Axel Spoerl Date: Fri, 4 Mar 2022 11:05:47 +0100 Subject: [PATCH] Fix QDockWidget parenting and dock permissions Check DockWidgetArea permissions of QDockWidgetGroupWindows with single dock widget. Obtain a dock widget's tab position from a dock widget group window if it can't be established otherwise. Remove hardcoded assumption that a dock widget is in the left dock. Both cases have lead to inconsistent entries and dangling pointers in QDockAreaLayoutInfo::item_list. Remove warning: QMainWindowLayout::tabPosition called with out-of-bounds value '0', which becomes obsolete by the fix. Create a QDockWidgetGroup window prepered to become a floating tab, whenever a dock widget is being hovered over. Store it in item_list so it can be found and deleted when required. No longer call e->ignore() after propagating close events to the first dock widget and thus preventing others from receiving the event. Add logging category qt.widgets.dockwidgets Update dock widget autotest with tests to check the fixes mentioned: plugging, unplugging, hiding, showing, closing and deleting. Blackist closeAndDelete, floatingTabs test on macos, QEMU, arm, android due to flaky isFloating() response after a dock widget has been closed or plugged. QSKIP dockPermissions and floatingTabs test on Windows due to mouse simulation malfunction. QSKIP hideAndShow test on Linux in case of xcb error (QTBUG-82059) Fixes: QTBUG-99136 Pick-to: 6.3 6.2 Change-Id: Ibd353e0acc9831a0d67c9f682429ab46b94bdbb0 Reviewed-by: Volker Hilsheimer --- src/widgets/widgets/qdockarealayout.cpp | 59 +- src/widgets/widgets/qdockarealayout_p.h | 3 +- src/widgets/widgets/qdockwidget.cpp | 28 +- src/widgets/widgets/qdockwidget_p.h | 1 + src/widgets/widgets/qmainwindowlayout.cpp | 333 +++++++--- src/widgets/widgets/qmainwindowlayout_p.h | 50 +- .../widgets/widgets/qdockwidget/BLACKLIST | 22 + .../widgets/qdockwidget/CMakeLists.txt | 1 + .../widgets/qdockwidget/tst_qdockwidget.cpp | 582 +++++++++++++++++- 9 files changed, 935 insertions(+), 144 deletions(-) diff --git a/src/widgets/widgets/qdockarealayout.cpp b/src/widgets/widgets/qdockarealayout.cpp index 626710cd54..8d9d81aa83 100644 --- a/src/widgets/widgets/qdockarealayout.cpp +++ b/src/widgets/widgets/qdockarealayout.cpp @@ -60,6 +60,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaDockWidgets, "qt.widgets.dockwidgets"); + // qmainwindow.cpp extern QMainWindowLayout *qt_mainwindow_layout(const QMainWindow *window); @@ -1223,8 +1225,10 @@ bool QDockAreaLayoutInfo::insertGap(const QList &path, QLayoutItem *dockWid const QDockAreaLayoutItem &item = item_list.at(i); if (item.skip()) continue; - Q_ASSERT(!(item.flags & QDockAreaLayoutItem::GapItem)); + Q_ASSERT_X(!(item.flags & QDockAreaLayoutItem::GapItem), + "QDockAreaLayoutInfo::insertGap", "inserting two gaps after each other"); space += item.size - pick(o, item.minimumSize()); + qCDebug(lcQpaDockWidgets) << "Item space:" << item.flags << this; } } @@ -1249,8 +1253,7 @@ bool QDockAreaLayoutInfo::insertGap(const QList &path, QLayoutItem *dockWid // finally, insert the gap item_list.insert(index, gap_item); - -// dump(qDebug() << "insertGap() after:" << index << tabIndex, *this, QString()); + qCDebug(lcQpaDockWidgets) << "Insert gap after:" << index << this; return true; } @@ -2429,23 +2432,7 @@ QList QDockAreaLayout::gapIndex(const QPoint &pos, bool disallowTabs) const const QDockAreaLayoutInfo &info = docks[i]; if (info.isEmpty()) { - QRect r; - switch (i) { - case QInternal::LeftDock: - r = QRect(rect.left(), rect.top(), EmptyDropAreaSize, rect.height()); - break; - case QInternal::RightDock: - r = QRect(rect.right() - EmptyDropAreaSize, rect.top(), - EmptyDropAreaSize, rect.height()); - break; - case QInternal::TopDock: - r = QRect(rect.left(), rect.top(), rect.width(), EmptyDropAreaSize); - break; - case QInternal::BottomDock: - r = QRect(rect.left(), rect.bottom() - EmptyDropAreaSize, - rect.width(), EmptyDropAreaSize); - break; - } + const QRect r = gapRect(static_cast(i)); if (r.contains(pos)) { if (opts & QMainWindow::ForceTabbedDocks && !info.item_list.isEmpty()) { //in case of ForceTabbedDocks, we pass -1 in order to force the gap to be tabbed @@ -2461,6 +2448,38 @@ QList QDockAreaLayout::gapIndex(const QPoint &pos, bool disallowTabs) const return QList(); } +QRect QDockAreaLayout::gapRect(QInternal::DockPosition dockPos) const +{ + Q_ASSERT_X(mainWindow, "QDockAreaLayout::gapRect", "Called without valid mainWindow pointer."); + + // Warn if main window is too small to create proper docks. + // Do not fail because this can be triggered by the user. + if (mainWindow->height() < (2 * EmptyDropAreaSize)) { + qCWarning(lcQpaDockWidgets, "QDockAreaLayout::gapRect: Main window height %i is too small. Docking will not be possible.", + mainWindow->height()); + + } + if (mainWindow->width() < (2 * EmptyDropAreaSize)) { + qCWarning(lcQpaDockWidgets, "QDockAreaLayout::gapRect: Main window width %i is too small. Docking will not be possible.", + mainWindow->width()); + } + + // Calculate rectangle of requested dock + switch (dockPos) { + case QInternal::LeftDock: + return QRect(rect.left(), rect.top(), EmptyDropAreaSize, rect.height()); + case QInternal::RightDock: + return QRect(rect.right() - EmptyDropAreaSize, rect.top(), EmptyDropAreaSize, rect.height()); + case QInternal::TopDock: + return QRect(rect.left(), rect.top(), rect.width(), EmptyDropAreaSize); + case QInternal::BottomDock: + return QRect(rect.left(), rect.bottom() - EmptyDropAreaSize, rect.width(), EmptyDropAreaSize); + case QInternal::DockCount: + break; + } + return QRect(); +} + QList QDockAreaLayout::findSeparator(const QPoint &pos) const { QList result; diff --git a/src/widgets/widgets/qdockarealayout_p.h b/src/widgets/widgets/qdockarealayout_p.h index 57572bcfa1..829a24dce3 100644 --- a/src/widgets/widgets/qdockarealayout_p.h +++ b/src/widgets/widgets/qdockarealayout_p.h @@ -84,7 +84,7 @@ class QTabBar; // A path indetifies uniquely one object in this tree, the first number being the side and all the following // indexes into the QDockAreaLayoutInfo::item_list. -struct QDockAreaLayoutItem +struct Q_AUTOTEST_EXPORT QDockAreaLayoutItem { enum ItemFlags { NoFlags = 0, GapItem = 1, KeepSize = 2 }; @@ -302,6 +302,7 @@ public: void setGrid(QList *ver_struct_list, QList *hor_struct_list); QRect gapRect(const QList &path) const; + QRect gapRect(QInternal::DockPosition dockPos) const; void keepSize(QDockWidget *w); #if QT_CONFIG(tabbar) diff --git a/src/widgets/widgets/qdockwidget.cpp b/src/widgets/widgets/qdockwidget.cpp index e08f64a660..66f92af051 100644 --- a/src/widgets/widgets/qdockwidget.cpp +++ b/src/widgets/widgets/qdockwidget.cpp @@ -811,10 +811,10 @@ void QDockWidgetPrivate::startDrag(bool group) state->widgetItem = layout->unplug(q, group); if (state->widgetItem == nullptr) { - /* I have a QMainWindow parent, but I was never inserted with + /* Dock widget has a QMainWindow parent, but was never inserted with QMainWindow::addDockWidget, so the QMainWindowLayout has no - widget item for me. :( I have to create it myself, and then - delete it if I don't get dropped into a dock area. */ + widget item for it. It will be newly created and deleted if it doesn't + get dropped into a dock area. */ QDockWidgetGroupWindow *floatingTab = qobject_cast(parent); if (floatingTab && !q->isFloating()) state->widgetItem = new QDockWidgetGroupWindowItem(floatingTab); @@ -868,7 +868,15 @@ void QDockWidgetPrivate::endDrag(bool abort) if (q->isFloating()) { // Might not be floating when dragging a QDockWidgetGroupWindow undockedGeometry = q->geometry(); #if QT_CONFIG(tabwidget) - tabPosition = mwLayout->tabPosition(mainWindow->dockWidgetArea(q)); + // is the widget located within the mainwindow? + const Qt::DockWidgetArea area = mainWindow->dockWidgetArea(q); + if (area != Qt::NoDockWidgetArea) { + tabPosition = mwLayout->tabPosition(area); + } else if (auto dwgw = qobject_cast(q->parent())) { + // DockWidget wasn't found in one of the docks within mainwindow + // => derive tabPosition from parent + tabPosition = mwLayout->tabPosition(toDockWidgetArea(dwgw->layoutInfo()->dockPos)); + } #endif } q->activateWindow(); @@ -883,6 +891,18 @@ void QDockWidgetPrivate::endDrag(bool abort) state = nullptr; } +Qt::DockWidgetArea QDockWidgetPrivate::toDockWidgetArea(QInternal::DockPosition pos) +{ + switch (pos) { + case QInternal::LeftDock: return Qt::LeftDockWidgetArea; + case QInternal::RightDock: return Qt::RightDockWidgetArea; + case QInternal::TopDock: return Qt::TopDockWidgetArea; + case QInternal::BottomDock: return Qt::BottomDockWidgetArea; + default: break; + } + return Qt::NoDockWidgetArea; +} + void QDockWidgetPrivate::setResizerActive(bool active) { Q_Q(QDockWidget); diff --git a/src/widgets/widgets/qdockwidget_p.h b/src/widgets/widgets/qdockwidget_p.h index ca9bf9dfcc..0b338c8baa 100644 --- a/src/widgets/widgets/qdockwidget_p.h +++ b/src/widgets/widgets/qdockwidget_p.h @@ -90,6 +90,7 @@ public: void _q_toggleTopLevel(); // private slot void updateButtons(); + static Qt::DockWidgetArea toDockWidgetArea(QInternal::DockPosition pos); #if QT_CONFIG(tabwidget) QTabWidget::TabPosition tabPosition = QTabWidget::North; diff --git a/src/widgets/widgets/qmainwindowlayout.cpp b/src/widgets/widgets/qmainwindowlayout.cpp index e6c885512f..06a252bb95 100644 --- a/src/widgets/widgets/qmainwindowlayout.cpp +++ b/src/widgets/widgets/qmainwindowlayout.cpp @@ -294,7 +294,6 @@ bool QDockWidgetGroupWindow::event(QEvent *e) #if QT_CONFIG(tabbar) // Forward the close to the QDockWidget just as if its close button was pressed if (QDockWidget *dw = activeTabbedDockWidget()) { - e->ignore(); dw->close(); adjustFlags(); } @@ -420,12 +419,13 @@ QDockWidget *QDockWidgetGroupWindow::activeTabbedDockWidget() const */ void QDockWidgetGroupWindow::destroyOrHideIfEmpty() { - if (!layoutInfo()->isEmpty()) { + const QDockAreaLayoutInfo *info = layoutInfo(); + if (!info->isEmpty()) { show(); // It might have been hidden, return; } // There might still be placeholders - if (!layoutInfo()->item_list.isEmpty()) { + if (!info->item_list.isEmpty()) { hide(); return; } @@ -433,9 +433,10 @@ void QDockWidgetGroupWindow::destroyOrHideIfEmpty() // Make sure to reparent the possibly floating or hidden QDockWidgets to the parent const auto dockWidgets = findChildren(Qt::FindDirectChildrenOnly); for (QDockWidget *dw : dockWidgets) { - bool wasFloating = dw->isFloating(); - bool wasHidden = dw->isHidden(); + const bool wasFloating = dw->isFloating(); + const bool wasHidden = dw->isHidden(); dw->setParent(parentWidget()); + qCDebug(lcQpaDockWidgets) << "Reparented:" << dw << "to" << parentWidget() << "by" << this; if (wasFloating) { dw->setFloating(true); } else { @@ -444,8 +445,9 @@ void QDockWidgetGroupWindow::destroyOrHideIfEmpty() qt_mainwindow_layout(static_cast(parentWidget())); Qt::DockWidgetArea area = ml->dockWidgetArea(this); if (area == Qt::NoDockWidgetArea) - area = Qt::LeftDockWidgetArea; + area = Qt::LeftDockWidgetArea; // FIXME: DockWidget doesn't save original docking area static_cast(parentWidget())->addDockWidget(area, dw); + qCDebug(lcQpaDockWidgets) << "Redocked to Mainwindow:" << area << dw << "by" << this; } if (!wasHidden) dw->show(); @@ -1235,8 +1237,9 @@ bool QMainWindowLayoutState::restoreState(QDataStream &_stream, { auto dockWidgets = allMyDockWidgets(mainWindow); QDockWidgetGroupWindow* floatingTab = qt_mainwindow_layout(mainWindow)->createTabbedDockWindow(); - *floatingTab->layoutInfo() = QDockAreaLayoutInfo(&dockAreaLayout.sep, QInternal::LeftDock, - Qt::Horizontal, QTabBar::RoundedSouth, mainWindow); + *floatingTab->layoutInfo() = QDockAreaLayoutInfo( + &dockAreaLayout.sep, QInternal::LeftDock, // FIXME: DockWidget doesn't save original docking area + Qt::Horizontal, QTabBar::RoundedSouth, mainWindow); QRect geometry; stream >> geometry; QDockAreaLayoutInfo *info = floatingTab->layoutInfo(); @@ -1489,25 +1492,50 @@ static QInternal::DockPosition toDockPos(Qt::DockWidgetArea area) return QInternal::DockCount; } -static Qt::DockWidgetArea toDockWidgetArea(QInternal::DockPosition pos) -{ - switch (pos) { - case QInternal::LeftDock : return Qt::LeftDockWidgetArea; - case QInternal::RightDock : return Qt::RightDockWidgetArea; - case QInternal::TopDock : return Qt::TopDockWidgetArea; - case QInternal::BottomDock : return Qt::BottomDockWidgetArea; - default: - break; - } - - return Qt::NoDockWidgetArea; -} - inline static Qt::DockWidgetArea toDockWidgetArea(int pos) { - return toDockWidgetArea(static_cast(pos)); + return QDockWidgetPrivate::toDockWidgetArea(static_cast(pos)); } +// Checks if QDockWidgetGroupWindow or QDockWidget can be plugged the area indicated by path. +// Returns false if called with invalid widget type or if compiled without dockwidget support. +#if QT_CONFIG(dockwidget) +static bool isAreaAllowed(QWidget *widget, const QList &path) +{ + Q_ASSERT_X((path.count() > 1), "isAreaAllowed", "invalid path size"); + const Qt::DockWidgetArea area = toDockWidgetArea(path[1]); + + // Read permissions directly from a single dock widget + if (QDockWidget *dw = qobject_cast(widget)) { + const bool allowed = dw->isAreaAllowed(area); + if (!allowed) + qCDebug(lcQpaDockWidgets) << "No permission for single DockWidget" << widget << "to dock on" << area; + return allowed; + } + + // Read permissions from a DockWidgetGroupWindow depending on its DockWidget children + if (QDockWidgetGroupWindow *dwgw = qobject_cast(widget)) { + const QList children = dwgw->findChildren(QString(), Qt::FindDirectChildrenOnly); + + if (children.count() == 1) { + // Group window has a single child => read its permissions + const bool allowed = children.at(0)->isAreaAllowed(area); + if (!allowed) + qCDebug(lcQpaDockWidgets) << "No permission for DockWidgetGroupWindow" << widget << "to dock on" << area; + return allowed; + } else { + // Group window has more than one or no children => dock it anywhere + qCDebug(lcQpaDockWidgets) << "DockWidgetGroupWindow" << widget << "has" << children.count() << "children:"; + qCDebug(lcQpaDockWidgets) << children; + qCDebug(lcQpaDockWidgets) << "DockWidgetGroupWindow" << widget << "can dock at" << area << "and anywhere else."; + return true; + } + } + qCDebug(lcQpaDockWidgets) << "Docking requested for invalid widget type (coding error)." << widget << area; + return false; +} +#endif + void QMainWindowLayout::setCorner(Qt::Corner corner, Qt::DockWidgetArea area) { if (layoutState.dockAreaLayout.corners[corner] == area) @@ -1523,6 +1551,27 @@ Qt::DockWidgetArea QMainWindowLayout::corner(Qt::Corner corner) const return layoutState.dockAreaLayout.corners[corner]; } +// Returns the rectangle of a dockWidgetArea +// if max is true, the maximum possible rectangle for dropping is returned +// the current visible rectangle otherwise +#if QT_CONFIG(dockwidget) +QRect QMainWindowLayout::dockWidgetAreaRect(const Qt::DockWidgetArea area, DockWidgetAreaSize size) const +{ + const QInternal::DockPosition dockPosition = toDockPos(area); + + // Called with invalid dock widget area + if (dockPosition == QInternal::DockCount) { + qCDebug(lcQpaDockWidgets) << "QMainWindowLayout::dockWidgetAreaRect called with" << area; + return QRect(); + } + + const QDockAreaLayout dl = layoutState.dockAreaLayout; + + // Return maximum or visible rectangle + return (size == Maximum) ? dl.gapRect(dockPosition) : dl.docks[dockPosition].rect; +} +#endif + void QMainWindowLayout::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget, Qt::Orientation orientation) @@ -1605,7 +1654,7 @@ void QMainWindowLayout::setTabShape(QTabWidget::TabShape tabShape) QTabWidget::TabPosition QMainWindowLayout::tabPosition(Qt::DockWidgetArea area) const { - const auto dockPos = toDockPos(area); + const QInternal::DockPosition dockPos = toDockPos(area); if (dockPos < QInternal::DockCount) return tabPositions[dockPos]; qWarning("QMainWindowLayout::tabPosition called with out-of-bounds value '%d'", int(area)); @@ -2470,7 +2519,6 @@ static bool unplugGroup(QMainWindowLayout *layout, QLayoutItem **item, return false; // The QDockWidget is part of a group of tab and we need to unplug them all. - QDockWidgetGroupWindow *floatingTabs = layout->createTabbedDockWindow(); QDockAreaLayoutInfo *info = floatingTabs->layoutInfo(); *info = std::move(*parentItem.subinfo); @@ -2485,6 +2533,30 @@ static bool unplugGroup(QMainWindowLayout *layout, QLayoutItem **item, } #endif +#if QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget) +static QTabBar::Shape tabwidgetPositionToTabBarShape(QWidget *w) +{ + QTabBar::Shape result = QTabBar::RoundedSouth; + if (qobject_cast(w)) { + switch (static_cast(qt_widget_private(w))->tabPosition) { + case QTabWidget::North: + result = QTabBar::RoundedNorth; + break; + case QTabWidget::South: + result = QTabBar::RoundedSouth; + break; + case QTabWidget::West: + result = QTabBar::RoundedWest; + break; + case QTabWidget::East: + result = QTabBar::RoundedEast; + break; + } + } + return result; +} +#endif // QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget) + /*! \internal Unplug \a widget (QDockWidget or QToolBar) from it's parent container. @@ -2507,22 +2579,87 @@ QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, bool group) QList groupWindowPath = info->indexOf(widget->parentWidget()); return groupWindowPath.isEmpty() ? nullptr : info->item(groupWindowPath).widgetItem; } + qCDebug(lcQpaDockWidgets) << "Drag only:" << widget << "Group:" << group; return nullptr; } QList path = groupWindow->layoutInfo()->indexOf(widget); - QLayoutItem *item = groupWindow->layoutInfo()->item(path).widgetItem; + QDockAreaLayoutItem parentItem = groupWindow->layoutInfo()->item(path); + QLayoutItem *item = parentItem.widgetItem; if (group && path.size() > 1 - && unplugGroup(this, &item, - groupWindow->layoutInfo()->item(path.mid(0, path.size() - 1)))) { + && unplugGroup(this, &item, parentItem)) { + qCDebug(lcQpaDockWidgets) << "Unplugging:" << widget << "from" << item; return item; } else { - // We are unplugging a dock widget from a floating window. - QDockWidget *dw = qobject_cast(widget); - Q_ASSERT(dw); // cannot be a QDockWidgetGroupWindow because it's not floating. - dw->d_func()->unplug(widget->geometry()); + // We are unplugging a single dock widget from a floating window. + QDockWidget *dockWidget = qobject_cast(widget); + Q_ASSERT(dockWidget); // cannot be a QDockWidgetGroupWindow because it's not floating. + + // unplug the widget first + dockWidget->d_func()->unplug(widget->geometry()); + + // Create a floating tab, copy properties and generate layout info + QDockWidgetGroupWindow *floatingTabs = createTabbedDockWindow(); + const QInternal::DockPosition dockPos = groupWindow->layoutInfo()->dockPos; + QDockAreaLayoutInfo *info = floatingTabs->layoutInfo(); + + const QTabBar::Shape shape = tabwidgetPositionToTabBarShape(dockWidget); + + // Populate newly created DockAreaLayoutInfo of floating tabs + *info = QDockAreaLayoutInfo(&layoutState.dockAreaLayout.sep, dockPos, + Qt::Horizontal, shape, + layoutState.mainWindow); + + // Create tab and hide it as group window contains only one widget + info->tabbed = true; + info->tabBar = getTabBar(); + info->tabBar->hide(); + updateGapIndicator(); + + // Reparent it to a QDockWidgetGroupLayout + floatingTabs->setGeometry(dockWidget->geometry()); + + // Append reference to floatingTabs to the dock's item_list + parentItem.widgetItem = new QDockWidgetGroupWindowItem(floatingTabs); + layoutState.dockAreaLayout.docks[dockPos].item_list.append(parentItem); + + // use populated parentItem to set reference to dockWidget as the first item in own list + parentItem.widgetItem = new QDockWidgetItem(dockWidget); + info->item_list = {parentItem}; + + // Add non-gap items of the dock to the tab bar + for (const auto &listItem : layoutState.dockAreaLayout.docks[dockPos].item_list) { + if (listItem.GapItem || !listItem.widgetItem) + continue; + info->tabBar->addTab(listItem.widgetItem->widget()->objectName()); + } + + // Re-parent and fit + floatingTabs->setParent(layoutState.mainWindow); + floatingTabs->layoutInfo()->fitItems(); + floatingTabs->layoutInfo()->apply(dockOptions & QMainWindow::AnimatedDocks); groupWindow->layoutInfo()->fitItems(); groupWindow->layoutInfo()->apply(dockOptions & QMainWindow::AnimatedDocks); - return item; + dockWidget->d_func()->tabPosition = layoutState.mainWindow->tabPosition(toDockWidgetArea(dockPos)); + info->reparentWidgets(floatingTabs); + dockWidget->setParent(floatingTabs); + info->updateTabBar(); + + // Show the new item + const QList path = layoutState.indexOf(floatingTabs); + QRect r = layoutState.itemRect(path); + savedState = layoutState; + savedState.fitLayout(); + + // Update gap, fix orientation, raise and show + currentGapPos = path; + currentGapRect = r; + updateGapIndicator(); + fixToolBarOrientation(parentItem.widgetItem, currentGapPos.at(1)); + floatingTabs->show(); + floatingTabs->raise(); + + qCDebug(lcQpaDockWidgets) << "Unplugged from floating dock:" << widget << "from" << parentItem.widgetItem; + return parentItem.widgetItem; } } #endif @@ -2595,51 +2732,37 @@ void QMainWindowLayout::updateGapIndicator() gapIndicator->setParent(expectedParent); } + // Prevent re-entry in case of size change + const bool sigBlockState = gapIndicator->signalsBlocked(); + auto resetSignals = qScopeGuard([this, sigBlockState](){ gapIndicator->blockSignals(sigBlockState); }); + gapIndicator->blockSignals(true); + #if QT_CONFIG(dockwidget) if (currentHoveredFloat) gapIndicator->setGeometry(currentHoveredFloat->currentGapRect); else #endif gapIndicator->setGeometry(currentGapRect); + gapIndicator->show(); gapIndicator->raise(); + + // Reset signal state + } else if (gapIndicator) { gapIndicator->hide(); } + #endif // QT_CONFIG(rubberband) } -#if QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget) -static QTabBar::Shape tabwidgetPositionToTabBarShape(QWidget *w) -{ - QTabBar::Shape result = QTabBar::RoundedSouth; - if (qobject_cast(w)) { - switch (static_cast(qt_widget_private(w))->tabPosition) { - case QTabWidget::North: - result = QTabBar::RoundedNorth; - break; - case QTabWidget::South: - result = QTabBar::RoundedSouth; - break; - case QTabWidget::West: - result = QTabBar::RoundedWest; - break; - case QTabWidget::East: - result = QTabBar::RoundedEast; - break; - } - } - return result; -} -#endif // QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget) +void QMainWindowLayout::hover(QLayoutItem *hoverTarget, + const QPoint &mousePos) { + if (!parentWidget()->isVisible() || parentWidget()->isMinimized() || + pluggingWidget != nullptr || hoverTarget == nullptr) + return; -void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos) -{ - if (!parentWidget()->isVisible() || parentWidget()->isMinimized() - || pluggingWidget != nullptr || widgetItem == nullptr) - return; - - QWidget *widget = widgetItem->widget(); + QWidget *widget = hoverTarget->widget(); #if QT_CONFIG(dockwidget) if ((dockOptions & QMainWindow::GroupedDragging) && (qobject_cast(widget) @@ -2652,12 +2775,20 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos) QWidget *w = qobject_cast(c); if (!w) continue; + + // Handle only dock widgets and group windows if (!qobject_cast(w) && !qobject_cast(w)) continue; + + // Check permission to dock on another dock widget or floating dock + // FIXME in 6.4 + if (w != widget && w->isWindow() && w->isVisible() && !w->isMinimized()) candidates << w; + if (QDockWidgetGroupWindow *group = qobject_cast(w)) { - // Sometimes, there are floating QDockWidget that have a QDockWidgetGroupWindow as a parent. + // floating QDockWidgets have a QDockWidgetGroupWindow as a parent, + // if they have been hovered over const auto groupChildren = group->children(); for (QObject *c : groupChildren) { if (QDockWidget *dw = qobject_cast(c)) { @@ -2667,6 +2798,7 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos) } } } + for (QWidget *w : candidates) { const QScreen *screen1 = qt_widget_private(widget)->associatedScreen(); const QScreen *screen2 = qt_widget_private(w)->associatedScreen(); @@ -2677,30 +2809,41 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos) #if QT_CONFIG(tabwidget) if (auto dropTo = qobject_cast(w)) { - // dropping to a normal widget, we mutate it in a QDockWidgetGroupWindow with two - // tabs - QDockWidgetGroupWindow *floatingTabs = createTabbedDockWindow(); // FIXME - floatingTabs->setGeometry(dropTo->geometry()); - QDockAreaLayoutInfo *info = floatingTabs->layoutInfo(); - const QTabBar::Shape shape = tabwidgetPositionToTabBarShape(dropTo); - *info = QDockAreaLayoutInfo(&layoutState.dockAreaLayout.sep, QInternal::LeftDock, - Qt::Horizontal, shape, - static_cast(parentWidget())); - info->tabbed = true; - QLayout *parentLayout = dropTo->parentWidget()->layout(); - info->item_list.append( - QDockAreaLayoutItem(parentLayout->takeAt(parentLayout->indexOf(dropTo)))); - dropTo->setParent(floatingTabs); + // w is the drop target's widget + w = dropTo->widget(); + + // Create a floating tab, unless already existing + if (!qobject_cast(w)) { + QDockWidgetGroupWindow *floatingTabs = createTabbedDockWindow(); + floatingTabs->setGeometry(dropTo->geometry()); + QDockAreaLayoutInfo *info = floatingTabs->layoutInfo(); + const QTabBar::Shape shape = tabwidgetPositionToTabBarShape(dropTo); + const QInternal::DockPosition dockPosition = toDockPos(dockWidgetArea(dropTo)); + *info = QDockAreaLayoutInfo(&layoutState.dockAreaLayout.sep, dockPosition, + Qt::Horizontal, shape, + static_cast(parentWidget())); + info->tabBar = getTabBar(); + info->tabbed = true; + QLayout *parentLayout = dropTo->parentWidget()->layout(); + info->item_list.append( + QDockAreaLayoutItem(parentLayout->takeAt(parentLayout->indexOf(dropTo)))); + + dropTo->setParent(floatingTabs); + qCDebug(lcQpaDockWidgets) << "Wrapping" << w << "into floating tabs" << floatingTabs; + w = floatingTabs; + } + + // Show the drop target and raise widget to foreground dropTo->show(); - dropTo->d_func()->plug(QRect()); - w = floatingTabs; - widget->raise(); // raise, as our newly created drop target is now on top + qCDebug(lcQpaDockWidgets) << "Showing" << dropTo; + widget->raise(); + qCDebug(lcQpaDockWidgets) << "Raising" << widget; } #endif - Q_ASSERT(qobject_cast(w)); - auto group = static_cast(w); - if (group->hover(widgetItem, group->mapFromGlobal(mousePos))) { + auto group = qobject_cast(w); + Q_ASSERT(group); + if (group->hover(hoverTarget, group->mapFromGlobal(mousePos))) { setCurrentHoveredFloat(group); applyState(layoutState); // update the tabbars } @@ -2722,21 +2865,7 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos) bool allowed = false; #if QT_CONFIG(dockwidget) - if (QDockWidget *dw = qobject_cast(widget)) - allowed = dw->isAreaAllowed(toDockWidgetArea(path.at(1))); - - // Read permissions from a DockWidgetGroupWindow depending on its DockWidget children - if (QDockWidgetGroupWindow* dwgw = qobject_cast(widget)) { - const QList children = dwgw->findChildren(QString(), Qt::FindDirectChildrenOnly); - - if (children.count() == 1) { - // Group window has a single child => read its permissions - allowed = children.at(0)->isAreaAllowed(toDockWidgetArea(path.at(1))); - } else { - // Group window has more than one or no children => dock it anywhere - allowed = true; - } - } + allowed = isAreaAllowed(widget, path); #endif #if QT_CONFIG(toolbar) if (QToolBar *tb = qobject_cast(widget)) @@ -2752,16 +2881,16 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos) currentGapPos = path; if (path.isEmpty()) { - fixToolBarOrientation(widgetItem, 2); // 2 = top dock, ie. horizontal + fixToolBarOrientation(hoverTarget, 2); // 2 = top dock, ie. horizontal restore(true); return; } - fixToolBarOrientation(widgetItem, currentGapPos.at(1)); + fixToolBarOrientation(hoverTarget, currentGapPos.at(1)); QMainWindowLayoutState newState = savedState; - if (!newState.insertGap(path, widgetItem)) { + if (!newState.insertGap(path, hoverTarget)) { restore(true); // not enough space return; } diff --git a/src/widgets/widgets/qmainwindowlayout_p.h b/src/widgets/widgets/qmainwindowlayout_p.h index bcb814fb20..8a1aba5f70 100644 --- a/src/widgets/widgets/qmainwindowlayout_p.h +++ b/src/widgets/widgets/qmainwindowlayout_p.h @@ -68,15 +68,19 @@ #if QT_CONFIG(dockwidget) #include "qdockarealayout_p.h" +#include "qdockwidget.h" #endif #if QT_CONFIG(toolbar) #include "qtoolbararealayout_p.h" #endif +#include QT_REQUIRE_CONFIG(mainwindow); QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaDockWidgets); + class QToolBar; class QRubberBand; @@ -334,7 +338,7 @@ bool QMainWindowLayoutSeparatorHelper::endSeparatorMove(const QPoint &) return true; } -class QDockWidgetGroupWindow : public QWidget +class Q_AUTOTEST_EXPORT QDockWidgetGroupWindow : public QWidget { Q_OBJECT public: @@ -369,14 +373,35 @@ private: }; // This item will be used in the layout for the gap item. We cannot use QWidgetItem directly -// because QWidgetItem functions return an empty size for widgets that are are floating. +// because QWidgetItem functions return an empty size for widgets that are floating. class QDockWidgetGroupWindowItem : public QWidgetItem { public: explicit QDockWidgetGroupWindowItem(QDockWidgetGroupWindow *parent) : QWidgetItem(parent) {} - QSize minimumSize() const override { return lay()->minimumSize(); } - QSize maximumSize() const override { return lay()->maximumSize(); } - QSize sizeHint() const override { return lay()->sizeHint(); } + + // when the item contains a dock widget, obtain its size (to prevent infinite loop) + // ask the layout otherwise + QSize minimumSize() const override + { + if (auto dw = widget()->findChild()) + return dw->minimumSize(); + return lay()->minimumSize(); + } + QSize maximumSize() const override + { + auto dw = widget()->findChild(); + if (dw) + return dw->maximumSize(); + return lay()->maximumSize(); + } + QSize sizeHint() const override + { + auto dw = widget()->findChild(); + if (dw) + return dw->sizeHint(); + return lay()->sizeHint(); + } + QWidget* widget() const override { return wid; } private: QLayout *lay() const { return const_cast(this)->widget()->layout(); } @@ -389,7 +414,7 @@ private: widgets. */ -class QMainWindowLayoutState +class Q_AUTOTEST_EXPORT QMainWindowLayoutState { public: QRect rect; @@ -460,22 +485,19 @@ public: QMainWindow::DockOptions dockOptions; void setDockOptions(QMainWindow::DockOptions opts); - // status bar - QLayoutItem *statusbar; + // status bar #if QT_CONFIG(statusbar) QStatusBar *statusBar() const; void setStatusBar(QStatusBar *sb); #endif // central widget - QWidget *centralWidget() const; void setCentralWidget(QWidget *cw); // toolbars - #if QT_CONFIG(toolbar) void addToolBarBreak(Qt::ToolBarArea area); void insertToolBarBreak(QToolBar *before); @@ -492,10 +514,11 @@ public: #endif // dock widgets - #if QT_CONFIG(dockwidget) void setCorner(Qt::Corner corner, Qt::DockWidgetArea area); Qt::DockWidgetArea corner(Qt::Corner corner) const; + enum DockWidgetAreaSize {Visible, Maximum}; + QRect dockWidgetAreaRect(Qt::DockWidgetArea area, DockWidgetAreaSize size = Maximum) const; void addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget, Qt::Orientation orientation); @@ -542,7 +565,6 @@ public: #endif // QT_CONFIG(dockwidget) // save/restore - enum VersionMarkers { // sentinel values used to validate state data VersionMarker = 0xff }; @@ -551,7 +573,6 @@ public: QBasicTimer discardRestoredStateTimer; // QLayout interface - void addItem(QLayoutItem *item) override; void setGeometry(const QRect &r) override; QLayoutItem *itemAt(int index) const override; @@ -565,7 +586,6 @@ public: void invalidate() override; // animations - QWidgetAnimator widgetAnimator; QList currentGapPos; QRect currentGapRect; @@ -579,7 +599,7 @@ public: #endif bool isInApplyState = false; - void hover(QLayoutItem *widgetItem, const QPoint &mousePos); + void hover(QLayoutItem *hoverTarget, const QPoint &mousePos); bool plug(QLayoutItem *widgetItem); QLayoutItem *unplug(QWidget *widget, bool group = false); void revert(QLayoutItem *widgetItem); diff --git a/tests/auto/widgets/widgets/qdockwidget/BLACKLIST b/tests/auto/widgets/widgets/qdockwidget/BLACKLIST index 6a3b189939..769bc1bd5c 100644 --- a/tests/auto/widgets/widgets/qdockwidget/BLACKLIST +++ b/tests/auto/widgets/widgets/qdockwidget/BLACKLIST @@ -1,3 +1,25 @@ # QTBUG-87415 [task169808_setFloating] android +# +# QDockWidget::isFloating() is flaky after state change on these OS +[closeAndDelete] +macos +[floatingTabs] +macos +[closeAndDelete] +b2qt +[floatingTabs] +macos b2qt arm android +[closeAndDelete] +b2qt +[floatingTabs] +arm +[closeAndDelete] +macos b2qt arm android +[floatingTabs] +arm +[closeAndDelete] +android +[floatingTabs] +android diff --git a/tests/auto/widgets/widgets/qdockwidget/CMakeLists.txt b/tests/auto/widgets/widgets/qdockwidget/CMakeLists.txt index adf48ddb11..6d54deca0b 100644 --- a/tests/auto/widgets/widgets/qdockwidget/CMakeLists.txt +++ b/tests/auto/widgets/widgets/qdockwidget/CMakeLists.txt @@ -8,6 +8,7 @@ qt_internal_add_test(tst_qdockwidget SOURCES tst_qdockwidget.cpp PUBLIC_LIBRARIES + Qt::Core Qt::CorePrivate Qt::Gui Qt::GuiPrivate diff --git a/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp b/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp index d726807b7c..c74b29ad4b 100644 --- a/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp +++ b/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp @@ -28,15 +28,24 @@ #include #include - #include #include #include +#include "private/qdockwidget_p.h" +#include "private/qmainwindowlayout_p.h" +#include #include #include #include +#include #include -#include "private/qdockwidget_p.h" +#include + +#ifdef QT_BUILD_INTERNAL +QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaDockWidgets, "qt.widgets.dockwidgets"); +QT_END_NAMESPACE +#endif bool hasFeature(QDockWidget *dockwidget, QDockWidget::DockWidgetFeature feature) { return (dockwidget->features() & feature) == feature; } @@ -70,6 +79,7 @@ private slots: void restoreDockWidget(); void restoreStateWhileStillFloating(); void setWindowTitle(); + // task specific tests: void task165177_deleteFocusWidget(); void task169808_setFloating(); @@ -78,8 +88,75 @@ private slots: void task258459_visibilityChanged(); void taskQTBUG_1665_closableChanged(); void taskQTBUG_9758_undockedGeometry(); + + // Dock area permissions for DockWidgets and DockWidgetGroupWindows + void dockPermissions(); + + // test floating tabs and item_tree consistency + void floatingTabs(); + + // test hide & show + void hideAndShow(); + + // test closing and deleting consistency + void closeAndDelete(); + +private: + // helpers and consts for dockPermissions, hideAndShow, closeAndDelete +#ifdef QT_BUILD_INTERNAL + void createTestWidgets(QMainWindow* &MainWindow, QPointer ¢, QPointer &d1, QPointer &d2) const; + void unplugAndResize(QMainWindow* MainWindow, QDockWidget* dw, QPoint home, QSize size) const; + + static inline QPoint dragPoint(QDockWidget* dockWidget); + static inline QPoint home1(QMainWindow* MainWindow) + { return MainWindow->mapToGlobal(MainWindow->rect().topLeft() + QPoint(0.1 * MainWindow->width(), 0.1 * MainWindow->height())); } + + static inline QPoint home2(QMainWindow* MainWindow) + { return MainWindow->mapToGlobal(MainWindow->rect().topLeft() + QPoint(0.6 * MainWindow->width(), 0.15 * MainWindow->height())); } + + static inline QSize size1(QMainWindow* MainWindow) + { return QSize (0.2 * MainWindow->width(), 0.25 * MainWindow->height()); } + + static inline QSize size2(QMainWindow* MainWindow) + { return QSize (0.1 * MainWindow->width(), 0.15 * MainWindow->height()); } + + static inline QPoint dockPoint(QMainWindow* mw, Qt::DockWidgetArea area) + { return mw->mapToGlobal(qobject_cast(mw->layout())->dockWidgetAreaRect(area, QMainWindowLayout::Maximum).center()); } + + bool checkFloatingTabs(QMainWindow* MainWindow, QPointer &ftabs, const QList &dwList = {}) const; + + // move a dock widget + void moveDockWidget(QDockWidget* dw, QPoint to, QPoint from = QPoint()) const; + + // Message handling for xcb error QTBUG 82059 + static void xcbMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); +public: + bool xcbError = false; +private: + +#ifdef QT_DEBUG + // Grace time between mouse events. Set to 400 for debugging. + const int waitingTime = 400; + + // Waiting time before closing widgets successful test. Set to 20000 for debugging. + const int waitBeforeClose = 0; + + // Enable logging + const bool dockWidgetLog = true; +#else + const int waitingTime = 15; + const int waitBeforeClose = 0; + const bool dockWidgetLog = false; +#endif // QT_DEBUG +#endif // QT_BUILD_INTERNAL + }; +// Statics for xcb error / msg handler +static tst_QDockWidget *qThis = nullptr; +static void (*oldMessageHandler)(QtMsgType, const QMessageLogContext&, const QString&); +#define QXCBVERIFY(cond) do { if (xcbError) QSKIP("Test skipped due to XCB error"); QVERIFY(cond); } while (0) + // Testing get/set functions void tst_QDockWidget::getSetCheck() { @@ -446,6 +523,7 @@ void tst_QDockWidget::allowedAreas() QVERIFY(!dw.isAreaAllowed(Qt::RightDockWidgetArea)); QVERIFY(dw.isAreaAllowed(Qt::TopDockWidgetArea)); QVERIFY(dw.isAreaAllowed(Qt::BottomDockWidgetArea)); + //QVERIFY(!dw.isAreaAllowed(Qt::FloatingDockWidgetArea)); QCOMPARE(spy.count(), 1); QCOMPARE(*static_cast(spy.at(0).value(0).constData()), dw.allowedAreas()); @@ -459,6 +537,7 @@ void tst_QDockWidget::allowedAreas() QVERIFY(dw.isAreaAllowed(Qt::RightDockWidgetArea)); QVERIFY(!dw.isAreaAllowed(Qt::TopDockWidgetArea)); QVERIFY(!dw.isAreaAllowed(Qt::BottomDockWidgetArea)); + //QVERIFY(!dw.isAreaAllowed(Qt::FloatingDockWidgetArea)); QCOMPARE(spy.count(), 1); QCOMPARE(*static_cast(spy.at(0).value(0).constData()), dw.allowedAreas()); @@ -472,6 +551,22 @@ void tst_QDockWidget::allowedAreas() QVERIFY(!dw.isAreaAllowed(Qt::RightDockWidgetArea)); QVERIFY(dw.isAreaAllowed(Qt::TopDockWidgetArea)); QVERIFY(!dw.isAreaAllowed(Qt::BottomDockWidgetArea)); + //QVERIFY(!dw.isAreaAllowed(Qt::FloatingDockWidgetArea)); + QCOMPARE(spy.count(), 1); + QCOMPARE(*static_cast(spy.at(0).value(0).constData()), + dw.allowedAreas()); + spy.clear(); + dw.setAllowedAreas(dw.allowedAreas()); + QCOMPARE(spy.count(), 0); + + //dw.setAllowedAreas(Qt::BottomDockWidgetArea | Qt::FloatingDockWidgetArea); + dw.setAllowedAreas(Qt::BottomDockWidgetArea); + //QCOMPARE(dw.allowedAreas(), Qt::BottomDockWidgetArea | Qt::FloatingDockWidgetArea); + QVERIFY(!dw.isAreaAllowed(Qt::LeftDockWidgetArea)); + QVERIFY(!dw.isAreaAllowed(Qt::RightDockWidgetArea)); + QVERIFY(!dw.isAreaAllowed(Qt::TopDockWidgetArea)); + QVERIFY(dw.isAreaAllowed(Qt::BottomDockWidgetArea)); + //QVERIFY(dw.isAreaAllowed(Qt::FloatingDockWidgetArea)); QCOMPARE(spy.count(), 1); QCOMPARE(*static_cast(spy.at(0).value(0).constData()), dw.allowedAreas()); @@ -485,6 +580,7 @@ void tst_QDockWidget::allowedAreas() QVERIFY(dw.isAreaAllowed(Qt::RightDockWidgetArea)); QVERIFY(!dw.isAreaAllowed(Qt::TopDockWidgetArea)); QVERIFY(dw.isAreaAllowed(Qt::BottomDockWidgetArea)); + //QVERIFY(!dw.isAreaAllowed(Qt::FloatingDockWidgetArea)); QCOMPARE(spy.count(), 1); QCOMPARE(*static_cast(spy.at(0).value(0).constData()), dw.allowedAreas()); @@ -1046,5 +1142,487 @@ void tst_QDockWidget::setWindowTitle() QCOMPARE(dock2.windowTitle(), dock2Title); } +// helpers for dockPermissions, hideAndShow, closeAndDelete +#ifdef QT_BUILD_INTERNAL +void tst_QDockWidget::createTestWidgets(QMainWindow* &mainWindow, QPointer ¢, QPointer &d1, QPointer &d2) const +{ + // Enable logging if required + if (dockWidgetLog) + QLoggingCategory::setFilterRules("qt.widgets.dockwidgets=true"); + + // Derive sizes and positions from primary screen + const QRect screen = QApplication::primaryScreen()->availableGeometry(); + const QPoint m_topLeft = screen.topLeft(); + const QSize s_mwindow = QApplication::primaryScreen()->availableSize() * 0.7; + + mainWindow = new QMainWindow; + mainWindow->setMouseTracking(true); + mainWindow->setFixedSize(s_mwindow); + cent = new QWidget; + mainWindow->setCentralWidget(cent); + cent->setLayout(new QGridLayout); + cent->layout()->setContentsMargins(0, 0, 0, 0); + cent->setMinimumSize(0, 0); + mainWindow->setDockOptions(QMainWindow::AllowTabbedDocks | QMainWindow::GroupedDragging); + mainWindow->move(m_topLeft); + + d1 = new QDockWidget(mainWindow); + d1->setWindowTitle("I am D1"); + d1->setObjectName("D1"); + d1->setFeatures(QDockWidget::DockWidgetFeatureMask); + d1->setAllowedAreas(Qt::DockWidgetArea::AllDockWidgetAreas); + + d2 = new QDockWidget(mainWindow); + d2->setWindowTitle("I am D2"); + d2->setObjectName("D2"); + d2->setFeatures(QDockWidget::DockWidgetFeatureMask); + d2->setAllowedAreas(Qt::DockWidgetArea::RightDockWidgetArea); + + mainWindow->addDockWidget(Qt::DockWidgetArea::LeftDockWidgetArea, d1); + mainWindow->addDockWidget(Qt::DockWidgetArea::RightDockWidgetArea, d2); + d1->show(); + d2->show(); + mainWindow->show(); + QApplication::setActiveWindow(mainWindow); + +} + +QPoint tst_QDockWidget::dragPoint(QDockWidget* dockWidget) +{ + Q_ASSERT(dockWidget); + QDockWidgetLayout *dwlayout = qobject_cast(dockWidget->layout()); + Q_ASSERT(dwlayout); + return dockWidget->mapToGlobal(dwlayout->titleArea().center()); +} + +void tst_QDockWidget::moveDockWidget(QDockWidget* dw, QPoint to, QPoint from) const +{ + Q_ASSERT(dw); + + // If no from point is given, use the drag point + if (from.isNull()) + from = dragPoint(dw); + + // move and log + const QPoint source = dw->mapFromGlobal(from); + const QPoint target = dw->mapFromGlobal(to); + qCDebug(lcQpaDockWidgets) << "Move" << dw->objectName() << "from" << source; + qCDebug(lcQpaDockWidgets) << "Move" << dw->objectName() << "from" << from; + QTest::mousePress(dw, Qt::LeftButton, Qt::KeyboardModifiers(), source); + QTest::mouseMove(dw, target); + qCDebug(lcQpaDockWidgets) << "Move" << dw->objectName() << "to" << target; + qCDebug(lcQpaDockWidgets) << "Move" << dw->objectName() << "to" << to; + QTest::mouseRelease(dw, Qt::LeftButton, Qt::KeyboardModifiers(), target); + QTest::qWait(waitingTime); + + // Verify WindowActive only for floating dock widgets + if (dw->isFloating()) + QTRY_VERIFY(QTest::qWaitForWindowActive(dw)); +} + +void tst_QDockWidget::unplugAndResize(QMainWindow* mainWindow, QDockWidget* dw, QPoint home, QSize size) const +{ + Q_ASSERT(mainWindow); + Q_ASSERT(dw); + + // Return if floating + if (dw->isFloating()) + return; + + QMainWindowLayout* layout = qobject_cast(mainWindow->layout()); + Qt::DockWidgetArea area = layout->dockWidgetArea(dw); + + // calculate minimum lateral move to unplug a dock widget + const int unplugMargin = 80; + int my = 0; + int mx = 0; + + switch (area) { + case Qt::LeftDockWidgetArea: + mx = unplugMargin; + break; + case Qt::TopDockWidgetArea: + my = unplugMargin; + break; + case Qt::RightDockWidgetArea: + mx = -unplugMargin; + break; + case Qt::BottomDockWidgetArea: + my = -unplugMargin; + break; + default: + return; + } + + // unplug and resize a dock Widget + qCDebug(lcQpaDockWidgets) << "*** unplug and resize" << dw->objectName(); + QPoint pos1 = dw->mapToGlobal(dw->rect().center()); + pos1.rx() += mx; + pos1.ry() += my; + moveDockWidget(dw, pos1, dw->mapToGlobal(dw->rect().center())); + //QTest::mousePress(dw, Qt::LeftButton, Qt::KeyboardModifiers(), dw->mapFromGlobal(pos1)); + QTRY_VERIFY(dw->isFloating()); + + qCDebug(lcQpaDockWidgets) << "Resizing" << dw->objectName() << "to" << size; + dw->setFixedSize(size); + QTest::qWait(waitingTime); + qCDebug(lcQpaDockWidgets) << "Move" << dw->objectName() << "to its home" << dw->mapFromGlobal(home); + dw->move(home); + //moveDockWidget(dw, home); +} + +bool tst_QDockWidget::checkFloatingTabs(QMainWindow* mainWindow, QPointer &ftabs, const QList &dwList) const +{ + Q_ASSERT(mainWindow); + + // Check if mainWindow has a floatingTab child + ftabs = mainWindow->findChild(); + if (ftabs.isNull()) { + qCDebug(lcQpaDockWidgets) << "MainWindow has no DockWidgetGroupWindow" << mainWindow; + return false; + } + + QTabBar* tab = ftabs->findChild(); + if (!tab) { + qCDebug(lcQpaDockWidgets) << "DockWidgetGroupWindow has no tab bar" << ftabs; + return false; + } + + // both dock widgets must be direct children of the main window + const QList children = ftabs->findChildren(QString(), Qt::FindDirectChildrenOnly); + if (dwList.count() > 0) + { + if (dwList.count() != children.count()) { + qCDebug(lcQpaDockWidgets) << "Expected DockWidgetGroupWindow children:" << dwList.count() + << "Children found:" << children.count(); + + qCDebug(lcQpaDockWidgets) << "Expected:" << dwList; + qCDebug(lcQpaDockWidgets) << "Found in" << ftabs << ":" << children.count(); + return false; + } + + for (const QDockWidget* child : dwList) { + if (!children.contains(child)) { + qCDebug(lcQpaDockWidgets) << "Expected child" << child << "not found in" << children; + return false; + } + } + } + + // Always select first tab position + qCDebug(lcQpaDockWidgets) << "click on first tab"; + QTest::mouseClick(tab, Qt::LeftButton, Qt::KeyboardModifiers(), tab->tabRect(0).center()); + return true; +} + +// detect xcb error +// qt.qpa.xcb: internal error: void QXcbWindow::setNetWmStateOnUnmappedWindow() called on mapped window +void tst_QDockWidget::xcbMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) +{ + Q_ASSERT(oldMessageHandler); + + if (type == QtWarningMsg && QString(context.category) == "qt.qpa.xcb" && msg.contains("internal error")) { + Q_ASSERT(qThis); + qThis->xcbError = true; + } + + return oldMessageHandler(type, context, msg); +} + +#endif // QT_BUILD_INTERNAL + +// test floating tabs and item_tree consistency +void tst_QDockWidget::floatingTabs() +{ +#ifdef Q_OS_WIN + QSKIP("Test skipped on Windows platforms"); +#endif // Q_OS_WIN +#ifdef QT_BUILD_INTERNAL + // Create a mainwindow with a central widget and two dock widgets + QPointer d1; + QPointer d2; + QPointer cent; + QMainWindow* mainWindow; + createTestWidgets(mainWindow, cent, d1, d2); + std::unique_ptr up_mainWindow(mainWindow); + + /* + * unplug both dockwidgets, resize them and plug them into a joint floating tab + * expected behavior: QDOckWidgetGroupWindow with both widgets is created + */ + + // remember paths to d1 and d2 + QMainWindowLayout* layout = qobject_cast(mainWindow->layout()); + const QList path1 = layout->layoutState.indexOf(d1); + const QList path2 = layout->layoutState.indexOf(d2); + + // unplug and resize both dock widgets + unplugAndResize(mainWindow, d1, home1(mainWindow), size1(mainWindow)); + unplugAndResize(mainWindow, d2, home2(mainWindow), size2(mainWindow)); + + // Test plugging + qCDebug(lcQpaDockWidgets) << "*** move d1 dock over d2 dock ***"; + qCDebug(lcQpaDockWidgets) << "**********(test plugging)*************"; + qCDebug(lcQpaDockWidgets) << "Move d1 over d2"; + moveDockWidget(d1, d2->mapToGlobal(d2->rect().center())); + + // Both dock widgets must no longer be floating + // disabled due to flakiness on macOS and Windows + //QTRY_VERIFY(!d1->isFloating()); + //QTRY_VERIFY(!d2->isFloating()); + if (d1->isFloating()) + qWarning("OS flakiness: D1 is docked and reports being floating"); + if (d2->isFloating()) + qWarning("OS flakiness: D2 is docked and reports being floating"); + + // Now MainWindow has to have a floatingTab child + QPointer ftabs; + QTRY_VERIFY(checkFloatingTabs(mainWindow, ftabs, QList() << d1 << d2)); + + /* + * replug both dock widgets into their initial position + * expected behavior: both docks are plugged and no longer floating + */ + + + // limitation: QTest cannot handle drag to unplug. + // reason: Object under mouse mutates from QTabBar::tab to QDockWidget. QTest cannot handle that. + // => click float button to unplug + qCDebug(lcQpaDockWidgets) << "*** test unplugging from floating dock ***"; + + // QDockWidget must have a QAbstractButton with object name "qt_dockwidget_floatbutton" + QAbstractButton* floatButton = d1->findChild("qt_dockwidget_floatbutton", Qt::FindDirectChildrenOnly); + QTRY_VERIFY(floatButton != nullptr); + QPoint pos1 = floatButton->rect().center(); + qCDebug(lcQpaDockWidgets) << "unplug d1" << pos1; + QTest::mouseClick(floatButton, Qt::LeftButton, Qt::KeyboardModifiers(), pos1); + QTest::qWait(waitingTime); + + // d1 must be floating again, while d2 is still in its GroupWindow + QTRY_VERIFY(d1->isFloating()); + QTRY_VERIFY(!d2->isFloating()); + + // Plug back into dock areas + qCDebug(lcQpaDockWidgets) << "*** test plugging back to dock areas ***"; + qCDebug(lcQpaDockWidgets) << "Move d1 to left dock"; + //moveDockWidget(d1, d1->mapFrom(MainWindow, dockPoint(MainWindow, Qt::LeftDockWidgetArea))); + moveDockWidget(d1, dockPoint(mainWindow, Qt::LeftDockWidgetArea)); + qCDebug(lcQpaDockWidgets) << "Move d2 to right dock"; + moveDockWidget(d2, dockPoint(mainWindow, Qt::RightDockWidgetArea)); + + qCDebug(lcQpaDockWidgets) << "Waiting" << waitBeforeClose << "ms before plugging back."; + QTest::qWait(waitBeforeClose); + + // Both dock widgets must no longer be floating + QTRY_VERIFY(!d1->isFloating()); + QTRY_VERIFY(!d2->isFloating()); + + // check if QDockWidgetGroupWindow has been removed from mainWindowLayout and properly deleted + QTRY_VERIFY(!mainWindow->findChild()); + QTRY_VERIFY(ftabs.isNull()); + + // Check if paths are consistent + qCDebug(lcQpaDockWidgets) << "Checking path consistency" << layout->layoutState.indexOf(d1) << layout->layoutState.indexOf(d2); + + // Path1 must be identical + QTRY_VERIFY(path1 == layout->layoutState.indexOf(d1)); + + // d1 must have a gap item due to size change + QTRY_VERIFY(layout->layoutState.indexOf(d2) == QList() << path2 << 0); +#else + QSKIP("test requires -developer-build option"); +#endif // QT_BUILD_INTERNAL +} + +// test hide & show +void tst_QDockWidget::hideAndShow() +{ +#ifdef QT_BUILD_INTERNAL + // Skip test if xcb error is launched + qThis = this; + oldMessageHandler = qInstallMessageHandler(xcbMessageHandler); + auto resetMessageHandler = qScopeGuard([] { qInstallMessageHandler(oldMessageHandler); }); + + // Create a mainwindow with a central widget and two dock widgets + QPointer d1; + QPointer d2; + QPointer cent; + QMainWindow* mainWindow; + createTestWidgets(mainWindow, cent, d1, d2); + std::unique_ptr up_mainWindow(mainWindow); + + // Check hiding of docked widgets + qCDebug(lcQpaDockWidgets) << "Hiding mainWindow with plugged dock widgets" << mainWindow; + mainWindow->hide(); + QXCBVERIFY(!mainWindow->isVisible()); + QXCBVERIFY(!d1->isVisible()); + QXCBVERIFY(!d2->isVisible()); + + // Check showing everything again + qCDebug(lcQpaDockWidgets) << "Showing mainWindow with plugged dock widgets" << mainWindow; + mainWindow->show(); + QXCBVERIFY(QTest::qWaitForWindowActive(mainWindow)); + QXCBVERIFY(QTest::qWaitForWindowExposed(mainWindow)); + QXCBVERIFY(mainWindow->isVisible()); + QXCBVERIFY(QTest::qWaitForWindowActive(d1)); + QXCBVERIFY(d1->isVisible()); + QXCBVERIFY(QTest::qWaitForWindowActive(d2)); + QXCBVERIFY(d2->isVisible()); + + // in case of XCB errors, unplugAndResize will block and cause the test to time out. + // => force skip + QTest::qWait(waitingTime); + if (xcbError) + QSKIP("Test skipped due to XCB error"); + + // unplug and resize both dock widgets + unplugAndResize(mainWindow, d1, home1(mainWindow), size1(mainWindow)); + unplugAndResize(mainWindow, d2, home2(mainWindow), size2(mainWindow)); + + // Check hiding of undocked widgets + qCDebug(lcQpaDockWidgets) << "Hiding mainWindow with unplugged dock widgets" << mainWindow; + mainWindow->hide(); + QTRY_VERIFY(!mainWindow->isVisible()); + QTRY_VERIFY(d1->isVisible()); + QTRY_VERIFY(d2->isVisible()); + d1->hide(); + d2->hide(); + QTRY_VERIFY(!d1->isVisible()); + QTRY_VERIFY(!d2->isVisible()); + + qCDebug(lcQpaDockWidgets) << "Waiting" << waitBeforeClose << "ms before closing."; + QTest::qWait(waitBeforeClose); +#else + QSKIP("test requires -developer-build option"); +#endif // QT_BUILD_INTERNAL +} + +// test closing and deleting consistency +void tst_QDockWidget::closeAndDelete() +{ +#ifdef QT_BUILD_INTERNAL + // Create a mainwindow with a central widget and two dock widgets + QPointer d1; + QPointer d2; + QPointer cent; + QMainWindow* mainWindow; + createTestWidgets(mainWindow, cent, d1, d2); + std::unique_ptr up_mainWindow(mainWindow); + + // unplug and resize both dock widgets + unplugAndResize(mainWindow, d1, home1(mainWindow), size1(mainWindow)); + unplugAndResize(mainWindow, d2, home2(mainWindow), size2(mainWindow)); + + // Create a floating tab and unplug it again + qCDebug(lcQpaDockWidgets) << "Move d1 over d2"; + moveDockWidget(d1, d2->mapToGlobal(d2->rect().center())); + + // Both dock widgets must no longer be floating + // disabled due to flakiness on macOS and Windows + //QTRY_VERIFY(!d1->isFloating()); + //QTRY_VERIFY(!d2->isFloating()); + if (d1->isFloating()) + qWarning("OS flakiness: D1 is docked and reports being floating"); + if (d2->isFloating()) + qWarning("OS flakiness: D2 is docked and reports being floating"); + + // Close everything with a single shot. Expected behavior: Event loop stops + bool eventLoopStopped = true; + QTimer::singleShot(0, this, [mainWindow, d1, d2] { + mainWindow->close(); + QTRY_VERIFY(!mainWindow->isVisible()); + QTRY_VERIFY(d1->isVisible()); + QTRY_VERIFY(d2->isVisible()); + d1->close(); + d2->close(); + QTRY_VERIFY(!d1->isVisible()); + QTRY_VERIFY(!d2->isVisible()); + }); + + // Fallback timer to report event loop still running + QTimer::singleShot(100, this, [&eventLoopStopped] { + qCDebug(lcQpaDockWidgets) << "Last dock widget hasn't shout down event loop!"; + eventLoopStopped = false; + QApplication::quit(); + }); + + QApplication::exec(); + + QTRY_VERIFY(eventLoopStopped); + + // Check heap cleanup + qCDebug(lcQpaDockWidgets) << "Deleting mainWindow"; + up_mainWindow.reset(); + QTRY_VERIFY(d1.isNull()); + QTRY_VERIFY(d2.isNull()); + QTRY_VERIFY(cent.isNull()); +#else + QSKIP("test requires -developer-build option"); +#endif // QT_BUILD_INTERNAL +} + +// Test dock area permissions +void tst_QDockWidget::dockPermissions() +{ +#ifdef Q_OS_WIN + QSKIP("Test skipped on Windows platforms"); +#endif // Q_OS_WIN +#ifdef QT_BUILD_INTERNAL + // Create a mainwindow with a central widget and two dock widgets + QPointer d1; + QPointer d2; + QPointer cent; + QMainWindow* mainWindow; + createTestWidgets(mainWindow, cent, d1, d2); + std::unique_ptr up_mainWindow(mainWindow); + + /* + * Unplug both dock widgets from their dock areas and hover them over each other + * expected behavior: + * - d2 hovering over d1 does nothing as d2 can only use right dock + * - hovering d2 over top, left and bottom dock area will do nothing due to lacking permissions + * - d1 hovering over d2 will create floating tabs as d1 has permission for DockWidgetArea::FloatingDockWidgetArea + * - resizing and tab creation will add two gap items in the right dock (d2) + */ + + // unplug and resize both dock widgets + unplugAndResize(mainWindow, d1, home1(mainWindow), size1(mainWindow)); + unplugAndResize(mainWindow, d2, home2(mainWindow), size2(mainWindow)); + + // both dock widgets must be direct children of the main window + { + const QList children = mainWindow->findChildren(QString(), Qt::FindDirectChildrenOnly); + QTRY_VERIFY(children.count() == 2); + for (const QDockWidget* child : children) + QTRY_VERIFY(child == d1 || child == d2); + } + + // The main window must not contain floating tabs + QTRY_VERIFY(mainWindow->findChild() == nullptr); + + // Test unpermitted dock areas with d2 + qCDebug(lcQpaDockWidgets) << "*** move d2 to forbidden docks ***"; + + // Move d2 to non allowed dock areas and verify it remains floating + qCDebug(lcQpaDockWidgets) << "Move d2 to top dock"; + moveDockWidget(d2, dockPoint(mainWindow, Qt::TopDockWidgetArea)); + QTRY_VERIFY(d2->isFloating()); + + qCDebug(lcQpaDockWidgets) << "Move d2 to left dock"; + //moveDockWidget(d2, d2->mapFrom(MainWindow, dockPoint(MainWindow, Qt::LeftDockWidgetArea))); + moveDockWidget(d2, dockPoint(mainWindow, Qt::LeftDockWidgetArea)); + QTRY_VERIFY(d2->isFloating()); + + qCDebug(lcQpaDockWidgets) << "Move d2 to bottom dock"; + moveDockWidget(d2, dockPoint(mainWindow, Qt::BottomDockWidgetArea)); + QTRY_VERIFY(d2->isFloating()); + + qCDebug(lcQpaDockWidgets) << "Waiting" << waitBeforeClose << "ms before closing."; + QTest::qWait(waitBeforeClose); +#else + QSKIP("test requires -developer-build option"); +#endif // QT_BUILD_INTERNAL +} + QTEST_MAIN(tst_QDockWidget) #include "tst_qdockwidget.moc"