diff --git a/src/widgets/widgets/qdockarealayout.cpp b/src/widgets/widgets/qdockarealayout.cpp index c089930ab9e..fede75cb00e 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 a3a25c47953..dbd2b84535c 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 }; @@ -299,6 +299,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 8fecab8059d..66147387870 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 0e5999a4564..34800351178 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 69497032986..3b1b38c795e 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(QString(), 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(); @@ -1210,8 +1212,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(); @@ -1464,25 +1467,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) @@ -1498,6 +1526,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) @@ -1580,7 +1629,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)); @@ -2409,7 +2458,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); @@ -2424,6 +2472,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. @@ -2446,22 +2518,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 @@ -2534,51 +2671,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) @@ -2591,12 +2714,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)) { @@ -2606,6 +2737,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(); @@ -2616,30 +2748,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 } @@ -2661,21 +2804,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)) @@ -2691,16 +2820,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 9ae3999ba13..30122f881e1 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; @@ -458,22 +483,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); @@ -490,10 +512,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); @@ -540,7 +563,6 @@ public: #endif // QT_CONFIG(dockwidget) // save/restore - enum VersionMarkers { // sentinel values used to validate state data VersionMarker = 0xff }; @@ -548,7 +570,6 @@ public: bool restoreState(QDataStream &stream); // QLayout interface - void addItem(QLayoutItem *item) override; void setGeometry(const QRect &r) override; QLayoutItem *itemAt(int index) const override; @@ -562,7 +583,6 @@ public: void invalidate() override; // animations - QWidgetAnimator widgetAnimator; QList currentGapPos; QRect currentGapRect; @@ -576,7 +596,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 6a3b1899396..769bc1bd5c3 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 adf48ddb11b..6d54deca0bf 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 d726807b7c6..f479cdf3409 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,499 @@ 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 + + QSKIP("Deactivated on 6.2 due to QTest::mouseMove() incompatibility."); + + // 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 + + QSKIP("Deactivated on 6.2 due to QTest::mouseMove() incompatibility."); + + // 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 + + QSKIP("Deactivated on 6.2 due to QTest::mouseMove() incompatibility."); + + // 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 + + QSKIP("Deactivated on 6.2 due to QTest::mouseMove() incompatibility."); + + // 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"