diff --git a/src/widgets/widgets/qdockwidget.cpp b/src/widgets/widgets/qdockwidget.cpp index ff4fda6bad..f76975859f 100644 --- a/src/widgets/widgets/qdockwidget.cpp +++ b/src/widgets/widgets/qdockwidget.cpp @@ -847,6 +847,11 @@ void QDockWidgetPrivate::endDrag(EndDragMode mode) tabPosition = mwLayout->tabPosition(toDockWidgetArea(dwgw->layoutInfo()->dockPos)); } #endif + // Reparent, if the drag was out of a dock widget group window + if (mode == EndDragMode::LocationChange) { + if (auto *groupWindow = qobject_cast(q->parentWidget())) + groupWindow->reparent(q); + } } q->activateWindow(); } else { @@ -948,6 +953,15 @@ bool QDockWidgetPrivate::mouseDoubleClickEvent(QMouseEvent *event) return false; } +bool QDockWidgetPrivate::isTabbed() const +{ + Q_Q(const QDockWidget); + QDockWidget *that = const_cast(q); + auto *mwLayout = qt_mainwindow_layout_from_dock(that); + Q_ASSERT(mwLayout); + return mwLayout->isDockWidgetTabbed(q); +} + bool QDockWidgetPrivate::mouseMoveEvent(QMouseEvent *event) { bool ret = false; @@ -978,7 +992,8 @@ bool QDockWidgetPrivate::mouseMoveEvent(QMouseEvent *event) } else #endif { - startDrag(DragScope::Group); + const DragScope scope = isTabbed() ? DragScope::Group : DragScope::Widget; + startDrag(scope); q->grabMouse(); ret = true; } diff --git a/src/widgets/widgets/qdockwidget_p.h b/src/widgets/widgets/qdockwidget_p.h index b768f11e2c..fa936599c6 100644 --- a/src/widgets/widgets/qdockwidget_p.h +++ b/src/widgets/widgets/qdockwidget_p.h @@ -106,6 +106,7 @@ public: void setResizerActive(bool active); bool isAnimating() const; + bool isTabbed() const; private: QWidgetResizeHandler *resizer = nullptr; diff --git a/src/widgets/widgets/qmainwindowlayout.cpp b/src/widgets/widgets/qmainwindowlayout.cpp index 170c594d0f..6711bff009 100644 --- a/src/widgets/widgets/qmainwindowlayout.cpp +++ b/src/widgets/widgets/qmainwindowlayout.cpp @@ -151,6 +151,21 @@ QDebug operator<<(QDebug debug, const QMainWindowLayout *layout) return debug; } +// Use this to dump item lists of all populated main window docks. +// Use DUMP macro inside QMainWindowLayout +#if 0 +static void dumpItemLists(const QMainWindowLayout *layout, const char *function, const char *comment) +{ + for (int i = 0; i < QInternal::DockCount; ++i) { + const auto &list = layout->layoutState.dockAreaLayout.docks[i].item_list; + if (list.isEmpty()) + continue; + qDebug() << function << comment << "Dock" << i << list; + } +} +#define DUMP(comment) dumpItemLists(this, __FUNCTION__, comment) +#endif // 0 + #endif // QT_CONFIG(dockwidget) && !defined(QT_NO_DEBUG) /****************************************************************************** @@ -403,8 +418,8 @@ 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) { + const auto dockWidgetsList = dockWidgets(); + for (QDockWidget *dw : dockWidgetsList) { const bool wasFloating = dw->isFloating(); const bool wasHidden = dw->isHidden(); dw->setParent(parentWidget()); @@ -607,6 +622,108 @@ void QDockWidgetGroupWindow::apply() layoutInfo()->apply(false); } +void QDockWidgetGroupWindow::childEvent(QChildEvent *event) +{ + switch (event->type()) { + case QEvent::ChildRemoved: + if (auto *dockWidget = qobject_cast(event->child())) + dockWidget->removeEventFilter(this); + destroyIfSingleItemLeft(); + break; + case QEvent::ChildAdded: + if (auto *dockWidget = qobject_cast(event->child())) + dockWidget->installEventFilter(this); + break; + default: + break; + } +} + +bool QDockWidgetGroupWindow::eventFilter(QObject *obj, QEvent *event) +{ + auto *dockWidget = qobject_cast(obj); + if (!dockWidget) + return QWidget::eventFilter(obj, event); + + switch (event->type()) { + case QEvent::Close: + // We don't want closed dock widgets in a floating tab + // => dock it to the main dock, before closing; + reparent(dockWidget); + dockWidget->setFloating(false); + break; + + case QEvent::Hide: + // if the dock widget is not an active tab, it is hidden anyway. + // if it is the active tab, hide the whole group. + if (dockWidget->isVisible()) + hide(); + break; + + default: + break; + } + return QWidget::eventFilter(obj, event); +} + +void QDockWidgetGroupWindow::destroyIfSingleItemLeft() +{ + const auto &dockWidgets = this->dockWidgets(); + + // Handle only the last dock + if (dockWidgets.count() != 1) + return; + + auto *lastDockWidget = dockWidgets.at(0); + + // If the last remaining dock widget is not in the group window's item_list, + // a group window is being docked on a main window docking area. + // => don't interfere + if (layoutInfo()->indexOf(lastDockWidget).isEmpty()) + return; + + auto *mainWindow = qobject_cast(parentWidget()); + QMainWindowLayout *mwLayout = qt_mainwindow_layout(mainWindow); + + // Unplug the last remaining dock widget and hide the group window, to avoid flickering + mwLayout->unplug(lastDockWidget, QDockWidgetPrivate::DragScope::Widget); + lastDockWidget->setGeometry(geometry()); + hide(); + + // Get the layout info for the main window dock, where dock widgets need to go + QDockAreaLayoutInfo &parentInfo = mwLayout->layoutState.dockAreaLayout.docks[layoutInfo()->dockPos]; + + // Re-parent last dock widget + reparent(lastDockWidget); + + // the group window could still have placeholder items => clear everything + layoutInfo()->item_list.clear(); + + // remove the group window and the dock's item_list pointing to it. + parentInfo.remove(this); + destroyOrHideIfEmpty(); +} + +void QDockWidgetGroupWindow::reparent(QDockWidget *dockWidget) +{ + // reparent a dockWidget to the main window + // - remove it from the floating dock's layout info + // - insert it to the main dock's layout info + // Finally, set draggingDock to nullptr, since the drag is finished. + auto *mainWindow = qobject_cast(parentWidget()); + Q_ASSERT(mainWindow); + QMainWindowLayout *mwLayout = qt_mainwindow_layout(mainWindow); + Q_ASSERT(mwLayout); + QDockAreaLayoutInfo &parentInfo = mwLayout->layoutState.dockAreaLayout.docks[layoutInfo()->dockPos]; + dockWidget->removeEventFilter(this); + parentInfo.add(dockWidget); + layoutInfo()->remove(dockWidget); + const bool wasFloating = dockWidget->isFloating(); + const bool wasVisible = dockWidget->isVisible(); + dockWidget->setParent(mainWindow); + dockWidget->setFloating(wasFloating); + dockWidget->setVisible(wasVisible); +} #endif /****************************************************************************** @@ -1745,11 +1862,14 @@ void QMainWindowLayout::keepSize(QDockWidget *w) // Handle custom tooltip, and allow to drag tabs away. class QMainWindowTabBar : public QTabBar { + Q_OBJECT QMainWindow *mainWindow; QPointer draggingDock; // Currently dragging (detached) dock widget ~QMainWindowTabBar(); public: QMainWindowTabBar(QMainWindow *parent); + QDockWidget *dockAt(int index) const; + QList dockWidgets() const; protected: bool event(QEvent *e) override; void mouseReleaseEvent(QMouseEvent*) override; @@ -1763,6 +1883,29 @@ QMainWindowTabBar::QMainWindowTabBar(QMainWindow *parent) setExpanding(false); } +QList QMainWindowTabBar::dockWidgets() const +{ + QList docks; + for (int i = 0; i < count(); ++i) { + if (QDockWidget *dock = dockAt(i)) + docks << dock; + } + return docks; +} + +QDockWidget *QMainWindowTabBar::dockAt(int index) const +{ + QMainWindowTabBar *that = const_cast(this); + QMainWindowLayout* mlayout = qt_mainwindow_layout(mainWindow); + QDockAreaLayoutInfo *info = mlayout->dockInfo(that); + if (!info) + return nullptr; + const int itemIndex = info->tabIndexToListIndex(index); + Q_ASSERT(itemIndex >= 0 && itemIndex < info->item_list.count()); + const QDockAreaLayoutItem &item = info->item_list.at(itemIndex); + return item.widgetItem ? qobject_cast(item.widgetItem->widget()) : nullptr; +} + void QMainWindowTabBar::mouseMoveEvent(QMouseEvent *e) { // The QTabBar handles the moving (reordering) of tabs. @@ -1776,13 +1919,8 @@ void QMainWindowTabBar::mouseMoveEvent(QMouseEvent *e) offset *= 3; QRect r = rect().adjusted(-offset, -offset, offset, offset); if (d->dragInProgress && !r.contains(e->position().toPoint()) && d->validIndex(d->pressedIndex)) { - QMainWindowLayout* mlayout = qt_mainwindow_layout(mainWindow); - QDockAreaLayoutInfo *info = mlayout->dockInfo(this); - Q_ASSERT(info); - int idx = info->tabIndexToListIndex(d->pressedIndex); - const QDockAreaLayoutItem &item = info->item_list.at(idx); - if (item.widgetItem - && (draggingDock = qobject_cast(item.widgetItem->widget()))) { + draggingDock = dockAt(d->pressedIndex); + if (draggingDock) { // We should drag this QDockWidget away by unpluging it. // First cancel the QTabBar's internal move d->moveTabFinished(d->pressedIndex); @@ -1858,6 +1996,23 @@ bool QMainWindowTabBar::event(QEvent *e) return true; } +bool QMainWindowLayout::isDockWidgetTabbed(const QDockWidget *dockWidget) const +{ + for (auto *bar : std::as_const(usedTabBars)) { + // A single dock widget in a tab bar is not considered to be tabbed. + // This is to make sure, we don't drag an empty QDockWidgetGroupWindow around. + // => only consider tab bars with two or more tabs. + if (bar->count() <= 1) + continue; + auto *tabBar = qobject_cast(bar); + Q_ASSERT(tabBar); + const auto dockWidgets = tabBar->dockWidgets(); + if (std::find(dockWidgets.begin(), dockWidgets.end(), dockWidget) != dockWidgets.end()) + return true; + } + return false; +} + QTabBar *QMainWindowLayout::getTabBar() { if (!usedTabBars.isEmpty() && !isInRestoreState) { @@ -2596,73 +2751,10 @@ QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, QDockWidgetPrivate::Drag // 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); - 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; + return item; } } #endif @@ -2815,7 +2907,7 @@ void QMainWindowLayout::hover(QLayoutItem *hoverTarget, continue; // Check permission to dock on another dock widget or floating dock - // FIXME in 6.4 + // FIXME in Qt 7 if (w != widget && w->isWindow() && w->isVisible() && !w->isMinimized()) candidates << w; @@ -2853,16 +2945,26 @@ void QMainWindowLayout::hover(QLayoutItem *hoverTarget, floatingTabs->setGeometry(dropTo->geometry()); QDockAreaLayoutInfo *info = floatingTabs->layoutInfo(); const QTabBar::Shape shape = tabwidgetPositionToTabBarShape(dropTo); - const QInternal::DockPosition dockPosition = toDockPos(dockWidgetArea(dropTo)); + + // dropTo and widget may be in a state where they transition + // from being a group window child to a single floating dock widget. + // In that case, their path to a main window dock may not have been + // updated yet. + // => ask both and fall back to dock 1 (right dock) + QInternal::DockPosition dockPosition = toDockPos(dockWidgetArea(dropTo)); + if (dockPosition == QInternal::DockPosition::DockCount) + dockPosition = toDockPos(dockWidgetArea(widget)); + if (dockPosition == QInternal::DockPosition::DockCount) + dockPosition = QInternal::DockPosition::RightDock; + *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)))); - + info->add(dropTo); + QDockAreaLayoutInfo &parentInfo = layoutState.dockAreaLayout.docks[dockPosition]; + parentInfo.add(floatingTabs); dropTo->setParent(floatingTabs); qCDebug(lcQpaDockWidgets) << "Wrapping" << widget << "into floating tabs" << floatingTabs; w = floatingTabs; @@ -2875,15 +2977,21 @@ void QMainWindowLayout::hover(QLayoutItem *hoverTarget, qCDebug(lcQpaDockWidgets) << "Raising" << widget; } #endif - auto group = qobject_cast(w); - Q_ASSERT(group); - if (group->hover(hoverTarget, group->mapFromGlobal(mousePos))) { - setCurrentHoveredFloat(group); + auto *groupWindow = qobject_cast(w); + Q_ASSERT(groupWindow); + if (groupWindow->hover(hoverTarget, groupWindow->mapFromGlobal(mousePos))) { + setCurrentHoveredFloat(groupWindow); applyState(layoutState); // update the tabbars } return; } } + + // If a temporary group window has been created during a hover, + // remove it, if it has only one dockwidget child + if (currentHoveredFloat) + currentHoveredFloat->destroyIfSingleItemLeft(); + setCurrentHoveredFloat(nullptr); layoutState.dockAreaLayout.fallbackToSizeHints = false; #endif // QT_CONFIG(dockwidget) @@ -3091,4 +3199,5 @@ Qt::DropAction QMainWindowLayout::performPlatformWidgetDrag(QLayoutItem *widgetI QT_END_NAMESPACE +#include "qmainwindowlayout.moc" #include "moc_qmainwindowlayout_p.cpp" diff --git a/src/widgets/widgets/qmainwindowlayout_p.h b/src/widgets/widgets/qmainwindowlayout_p.h index f382d58cf6..72bb9f2384 100644 --- a/src/widgets/widgets/qmainwindowlayout_p.h +++ b/src/widgets/widgets/qmainwindowlayout_p.h @@ -333,6 +333,9 @@ public: void updateCurrentGapRect(); void restore(); void apply(); + void childEvent(QChildEvent *event) override; + void reparent(QDockWidget *dockWidget); + void destroyIfSingleItemLeft(); QList dockWidgets() const { return findChildren(); } QRect currentGapRect; @@ -343,6 +346,7 @@ signals: protected: bool event(QEvent *) override; + bool eventFilter(QObject *obj, QEvent *event) override; void paintEvent(QPaintEvent*) override; private: @@ -574,6 +578,7 @@ public: #if QT_CONFIG(dockwidget) QPointer currentHoveredFloat; // set when dragging over a floating dock widget void setCurrentHoveredFloat(QDockWidgetGroupWindow *w); + bool isDockWidgetTabbed(const QDockWidget *dockWidget) const; #endif bool isInApplyState = false; diff --git a/tests/auto/widgets/widgets/qdockwidget/BLACKLIST b/tests/auto/widgets/widgets/qdockwidget/BLACKLIST index bb9118f9ee..8873589ff4 100644 --- a/tests/auto/widgets/widgets/qdockwidget/BLACKLIST +++ b/tests/auto/widgets/widgets/qdockwidget/BLACKLIST @@ -24,3 +24,13 @@ android qnx macos b2qt + +# OSes are flaky because of unplugging and plugging requires +# precise calculation of the title bar area for mouse emulation +# That's not possible for floating dock widgets. +[deleteFloatingTabWithSingleDockWidget] +qnx +b2qt +arm +android +macos diff --git a/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp b/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp index f5d3a6b6f0..fc48deec0b 100644 --- a/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp +++ b/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp @@ -10,6 +10,7 @@ #include "private/qmainwindowlayout_p.h" #include #include +#include #include #include #include @@ -67,6 +68,11 @@ private slots: // test floating tabs, item_tree and window title consistency void floatingTabs(); + void hoverWithoutDrop(); + + // floating tab gets removed, when last child goes away + void deleteFloatingTabWithSingleDockWidget_data(); + void deleteFloatingTabWithSingleDockWidget(); // test hide & show void hideAndShow(); @@ -81,9 +87,15 @@ private slots: private: // helpers and consts for dockPermissions, hideAndShow, closeAndDelete #ifdef QT_BUILD_INTERNAL - void createTestWidgets(QMainWindow* &MainWindow, QPointer ¢, QPointer &d1, QPointer &d2) const; + void createTestWidgets(QMainWindow* &MainWindow, QPointer ¢, + QPointer &d1, QPointer &d2) const; + void unplugAndResize(QMainWindow* MainWindow, QDockWidget* dw, QPoint home, QSize size) const; + void createFloatingTabs(QMainWindow* &MainWindow, QPointer ¢, + QPointer &d1, QPointer &d2, + QList &path1, QList &path2) 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())); } @@ -103,13 +115,26 @@ private: bool checkFloatingTabs(QMainWindow* MainWindow, QPointer &ftabs, const QList &dwList = {}) const; // move a dock widget - void moveDockWidget(QDockWidget* dw, QPoint to, QPoint from = QPoint()) const; + enum class MoveDockWidgetRule { + Drop, + Abort + }; + + void moveDockWidget(QDockWidget* dw, QPoint to, QPoint from, MoveDockWidgetRule rule) const; #ifdef QT_BUILD_INTERNAL // Message handling for xcb error QTBUG 82059 static void xcbMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg); + + enum class ChildRemovalReason { + Destroyed, + Closed, + Reparented + }; + public: bool xcbError = false; + bool platformSupportingRaise = true; #endif private: @@ -1185,7 +1210,7 @@ QPoint tst_QDockWidget::dragPoint(QDockWidget* dockWidget) return dockWidget->mapToGlobal(dwlayout->titleArea().center()); } -void tst_QDockWidget::moveDockWidget(QDockWidget* dw, QPoint to, QPoint from) const +void tst_QDockWidget::moveDockWidget(QDockWidget* dw, QPoint to, QPoint from, MoveDockWidgetRule rule) const { Q_ASSERT(dw); @@ -1202,12 +1227,22 @@ void tst_QDockWidget::moveDockWidget(QDockWidget* dw, QPoint to, QPoint from) co QTest::mouseMove(dw, target); qCDebug(lcTestDockWidget) << "Move" << dw->objectName() << "to" << target; qCDebug(lcTestDockWidget) << "Move" << dw->objectName() << "to" << to; - QTest::mouseRelease(dw, Qt::LeftButton, Qt::KeyboardModifiers(), target); - QTest::qWait(waitingTime); + if (rule == MoveDockWidgetRule::Drop) { + 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)); + // Verify WindowActive only for floating dock widgets + if (dw->isFloating()) + QTRY_VERIFY(QTest::qWaitForWindowActive(dw)); + return; + } + qCDebug(lcTestDockWidget) << "Aborting move and dropping at origin"; + + // Give animations some time + QTest::qWait(waitingTime); + QTest::mouseMove(dw, from); + QTest::mouseRelease(dw, Qt::LeftButton, Qt::KeyboardModifiers(), from); + QTest::qWait(waitingTime); } void tst_QDockWidget::unplugAndResize(QMainWindow* mainWindow, QDockWidget* dw, QPoint home, QSize size) const @@ -1255,7 +1290,7 @@ void tst_QDockWidget::unplugAndResize(QMainWindow* mainWindow, QDockWidget* dw, QPoint pos1 = dw->mapToGlobal(dw->rect().center()); pos1.rx() += mx; pos1.ry() += my; - moveDockWidget(dw, pos1, dw->mapToGlobal(dw->rect().center())); + moveDockWidget(dw, pos1, dw->mapToGlobal(dw->rect().center()), MoveDockWidgetRule::Drop); QTRY_VERIFY(dw->isFloating()); // Unplugged object's size may differ max. by 2x frame size @@ -1317,6 +1352,71 @@ bool tst_QDockWidget::checkFloatingTabs(QMainWindow* mainWindow, QPointerxcbError = true; + if (msg.contains("does not support raise")) + qThis->platformSupportingRaise = false; + } + + return oldMessageHandler(type, context, msg); +} +#endif + +void tst_QDockWidget::createFloatingTabs(QMainWindow* &mainWindow, QPointer ¢, + QPointer &d1, QPointer &d2, + QList &path1, QList &path2) const +{ + createTestWidgets(mainWindow, cent, d1, d2); + +#ifdef QT_BUILD_INTERNAL + qThis = const_cast(this); + oldMessageHandler = qInstallMessageHandler(xcbMessageHandler); + auto resetMessageHandler = qScopeGuard([] { qInstallMessageHandler(oldMessageHandler); }); +#endif + + // Test will fail if platform doesn't support raise. + mainWindow->windowHandle()->handle()->raise(); + if (!platformSupportingRaise) + QSKIP("Platform not supporting raise(). Floating tab based tests will fail."); + + // remember paths to d1 and d2 + QMainWindowLayout* layout = qobject_cast(mainWindow->layout()); + path1 = layout->layoutState.indexOf(d1); + 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)); + + // docks must be parented to the main window, no group window must exist + QCOMPARE(d1->parentWidget(), mainWindow); + QCOMPARE(d2->parentWidget(), mainWindow); + QVERIFY(mainWindow->findChildren().isEmpty()); + + // Test plugging + qCDebug(lcTestDockWidget) << "*** move d1 dock over d2 dock ***"; + qCDebug(lcTestDockWidget) << "**********(test plugging)*************"; + qCDebug(lcTestDockWidget) << "Move d1 over d2"; + moveDockWidget(d1, d2->mapToGlobal(d2->rect().center()), QPoint(), MoveDockWidgetRule::Drop); + + // Now MainWindow has to have a floatingTab child + QPointer ftabs; + QTRY_VERIFY(checkFloatingTabs(mainWindow, ftabs, QList() << d1 << d2)); +} #endif // QT_BUILD_INTERNAL // test floating tabs and item_tree consistency @@ -1333,7 +1433,9 @@ void tst_QDockWidget::floatingTabs() QPointer d2; QPointer cent; QMainWindow* mainWindow; - createTestWidgets(mainWindow, cent, d1, d2); + QList path1; + QList path2; + createFloatingTabs(mainWindow, cent, d1, d2, path1, path2); std::unique_ptr up_mainWindow(mainWindow); /* @@ -1341,22 +1443,6 @@ void tst_QDockWidget::floatingTabs() * 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(lcTestDockWidget) << "*** move d1 dock over d2 dock ***"; - qCDebug(lcTestDockWidget) << "**********(test plugging)*************"; - qCDebug(lcTestDockWidget) << "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 if (d1->isFloating()) qWarning("OS flakiness: D1 is docked and reports being floating"); @@ -1407,10 +1493,9 @@ void tst_QDockWidget::floatingTabs() // Plug back into dock areas qCDebug(lcTestDockWidget) << "*** test plugging back to dock areas ***"; qCDebug(lcTestDockWidget) << "Move d1 to left dock"; - //moveDockWidget(d1, d1->mapFrom(MainWindow, dockPoint(MainWindow, Qt::LeftDockWidgetArea))); - moveDockWidget(d1, dockPoint(mainWindow, Qt::LeftDockWidgetArea)); + moveDockWidget(d1, dockPoint(mainWindow, Qt::LeftDockWidgetArea), QPoint(), MoveDockWidgetRule::Drop); qCDebug(lcTestDockWidget) << "Move d2 to right dock"; - moveDockWidget(d2, dockPoint(mainWindow, Qt::RightDockWidgetArea)); + moveDockWidget(d2, dockPoint(mainWindow, Qt::RightDockWidgetArea), QPoint(), MoveDockWidgetRule::Drop); qCDebug(lcTestDockWidget) << "Waiting" << waitBeforeClose << "ms before plugging back."; QTest::qWait(waitBeforeClose); @@ -1424,38 +1509,99 @@ void tst_QDockWidget::floatingTabs() QTRY_VERIFY(ftabs.isNull()); // Check if paths are consistent + QMainWindowLayout* layout = qobject_cast(mainWindow->layout()); qCDebug(lcTestDockWidget) << "Checking path consistency" << layout->layoutState.indexOf(d1) << layout->layoutState.indexOf(d2); - // Path1 must be identical - QTRY_COMPARE(path1, layout->layoutState.indexOf(d1)); - - // d1 must have a gap item due to size change - QTRY_COMPARE(layout->layoutState.indexOf(d2), QList() << path2 << 0); + // Paths must be identical + QTRY_COMPARE(layout->layoutState.indexOf(d1), path1); + QTRY_COMPARE(layout->layoutState.indexOf(d2), path2); #else QSKIP("test requires -developer-build option"); #endif // QT_BUILD_INTERNAL } -#ifdef 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) - -// 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) +void tst_QDockWidget::deleteFloatingTabWithSingleDockWidget_data() { - Q_ASSERT(oldMessageHandler); +#ifdef QT_BUILD_INTERNAL + QTest::addColumn("reason"); + QTest::addRow("Delete child") << static_cast(ChildRemovalReason::Destroyed); + QTest::addRow("Close child") << static_cast(ChildRemovalReason::Closed); + QTest::addRow("Reparent child") << static_cast(ChildRemovalReason::Reparented); +#endif +} - if (type == QtWarningMsg && QString(context.category) == "qt.qpa.xcb" && msg.contains("internal error")) { - Q_ASSERT(qThis); - qThis->xcbError = true; +void tst_QDockWidget::deleteFloatingTabWithSingleDockWidget() +{ + if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + QSKIP("Test skipped on Wayland."); +#ifdef Q_OS_WIN + QSKIP("Test skipped on Windows platforms"); +#endif // Q_OS_WIN +#ifdef QT_BUILD_INTERNAL + + QFETCH(int, reason); + const ChildRemovalReason removalReason = static_cast(reason); + + QPointer d1; + QPointer d2; + QPointer cent; + QMainWindow* mainWindow; + QList path1; + QList path2; + createFloatingTabs(mainWindow, cent, d1, d2, path1, path2); + std::unique_ptr up_mainWindow(mainWindow); + + switch (removalReason) { + case ChildRemovalReason::Destroyed: + delete d1; + break; + case ChildRemovalReason::Closed: + d1->close(); + break; + case ChildRemovalReason::Reparented: + // This will create an invalid state, because setParent() doesn't fix the item_list. + // Testing this case anyway, because setParent() includig item_list fixup is executed, + // when the 2nd last dock widget is dragged out of a floating tab. + // => despite of the broken state, the group window has to be gone. + d1->setParent(mainWindow); + break; } - return oldMessageHandler(type, context, msg); -} + QTRY_VERIFY(!qobject_cast(d2->parentWidget())); + QTRY_VERIFY(mainWindow->findChildren().isEmpty()); #endif // QT_BUILD_INTERNAL +} + +void tst_QDockWidget::hoverWithoutDrop() +{ + if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) + QSKIP("Test skipped on Wayland."); +#ifdef QT_BUILD_INTERNAL + + 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)); + + // Test plugging + qCDebug(lcTestDockWidget) << "*** move d1 dock over d2 dock ***"; + qCDebug(lcTestDockWidget) << "*******(test hovering)***********"; + qCDebug(lcTestDockWidget) << "Move d1 over d2, wait and return to origin"; + const QPoint source = d1->mapToGlobal(d1->rect().center()); + const QPoint target = d2->mapToGlobal(d2->rect().center()); + moveDockWidget(d1, target, source, MoveDockWidgetRule::Abort); + auto *groupWindow = mainWindow->findChild(); + QCOMPARE(groupWindow, nullptr); +#else + QSKIP("test requires -developer-build option"); +#endif // QT_BUILD_INTERNAL +} // test hide & show void tst_QDockWidget::hideAndShow() @@ -1542,7 +1688,7 @@ void tst_QDockWidget::closeAndDelete() // Create a floating tab and unplug it again qCDebug(lcTestDockWidget) << "Move d1 over d2"; - moveDockWidget(d1, d2->mapToGlobal(d2->rect().center())); + moveDockWidget(d1, d2->mapToGlobal(d2->rect().center()), QPoint(), MoveDockWidgetRule::Drop); // Both dock widgets must no longer be floating // disabled due to flakiness on macOS and Windows @@ -1647,16 +1793,16 @@ void tst_QDockWidget::dockPermissions() // Move d2 to non allowed dock areas and verify it remains floating qCDebug(lcTestDockWidget) << "Move d2 to top dock"; - moveDockWidget(d2, dockPoint(mainWindow, Qt::TopDockWidgetArea)); + moveDockWidget(d2, dockPoint(mainWindow, Qt::TopDockWidgetArea), QPoint(), MoveDockWidgetRule::Drop); QTRY_VERIFY(d2->isFloating()); qCDebug(lcTestDockWidget) << "Move d2 to left dock"; //moveDockWidget(d2, d2->mapFrom(MainWindow, dockPoint(MainWindow, Qt::LeftDockWidgetArea))); - moveDockWidget(d2, dockPoint(mainWindow, Qt::LeftDockWidgetArea)); + moveDockWidget(d2, dockPoint(mainWindow, Qt::LeftDockWidgetArea), QPoint(), MoveDockWidgetRule::Drop); QTRY_VERIFY(d2->isFloating()); qCDebug(lcTestDockWidget) << "Move d2 to bottom dock"; - moveDockWidget(d2, dockPoint(mainWindow, Qt::BottomDockWidgetArea)); + moveDockWidget(d2, dockPoint(mainWindow, Qt::BottomDockWidgetArea), QPoint(), MoveDockWidgetRule::Drop); QTRY_VERIFY(d2->isFloating()); qCDebug(lcTestDockWidget) << "Waiting" << waitBeforeClose << "ms before closing.";