From db413caf2dcc7eeed194f15662efae3044d29dc0 Mon Sep 17 00:00:00 2001 From: Keith Kyzivat Date: Fri, 5 Jan 2024 19:17:32 -0500 Subject: [PATCH] MAYA-131961: Apply docking fixes from Qt 6.7 This patch includes a series of 14 commits that were cherry-picked from Qt dev branch at the time Qt 6.7 was being developed. The commits were squashed together into one commit to more clearly delineate them as going together. These changes are intended to be included in Maya 2025.1 Here is my working list of things I cherry-picked, with titles and a few notes. --- Starting from the tip of dev, collecting all related commits that should be cherry-picked into Maya's Qt 6.5.3 branch after Senthil's QDockWidget patch is reverted: 3591ea900b720ff48aace14cd81c8ec9d22b5c73 - Not strictly necessary, but improves internal docs for QDockWidgetGroupWindow regarding need for it to not be focusable when only one child QDockWidget Sorta related, but not specifically to this relation chain: d462c7b09c07b366ee5cea00116e32a4b7fedac6 - QMainWindowTabBar: make destructor public e6d85cf28bc4f750b69c33b72c006b067ae1190f -- This is the main fix we want - "QDockWidget: Fix group unplugging" 0b10b7476cf9d41086063ec49555425c6871041c - Refactor tst_QDockWidget::closeAndDelete() 26ded9dedf3f35e93b81bd0281fdd0bd30acb9d4 - tst_QDockWidget: use local context for timer 17372faf3f5e4a9c2577007ed5b38275e64bad89 - Remove bool trap in QDockWidgetPrivate::endDrag() -- NOTE: This one requires manual conflict resolution in QDockWidget::startDrag, drop that change. The change it makes is in a newer commit from Qt 6.6 related to Wayland that we don't really need for this fix. c93ab8c2a015b40b9a487ed9f23a72aebea8d52a - QDockWidget: Remove "group" bool trap -- this one also requires manual conflict resolution, but I can't find out what is different... c02f8b9d4d9a6578bb2017bafbb963d09641383f - Fully export QDockWidget debug operator bbeff2a3350dd3396400865525d509b784c2d93e - Extend qDebug functionality for QDockWidget b6b489db6968fbc3b4aab3755d0166ee09712ff0 - QMainWindowTabBar: Add destructor 2c96f517714eb67a9af821141a90eed68b8714ed - QDockAreaLayout: implement widget based add() and remove() 1ab91b7bdbcced6ea4cc176da1d18e6b789208ae - QDockWidget: call raise() when a dock widget starts to hover 4c60a11d8f935abb762a83b0ab99cefa6db3060c - QDockWidget: Always show dock widgets with the main window 0e435b7f13619106d61c9701e53803d3c488fea3 - QMainWindowLayout: remove redundant #ifdef'ry --- Below you will find the commit messages of each of the cherry-picked commits, starting with the newest (so, for cherry-picking, you would apply in reverse order). At the top of the entry will have the qt dev branch commit hash that was cherry-picked, for easy lookup. Below that will be an indication that manual conflict resolution was needed to apply it to this branch, and what manual changes were needed. ================================= cherry-picked from 3591ea900b720ff48aace14cd81c8ec9d22b5c73 Harden internal documentation of QDockWidgetGroupWindow QDockWidgetGroupWindow was documented by a few lines of code comments, omitting the most critical part of its design: It must not become able to acquire focus (i.e. be dragged or dropped) while having only one QDockWidget child. => replace legacy code comments by structured internal documentation. Pick-to: 6.6 6.5 Change-Id: I410ebf2e4c20c7479038417a4d8448dce8ab995f Reviewed-by: Volker Hilsheimer ================================= cherry-picked from d462c7b09c07b366ee5cea00116e32a4b7fedac6 QMainWindowTabBar: make destructor public Was private by mistake => make it public. Pick-to: 6.6 6.5 Change-Id: I6b07a19687ddf84e8456aa70bc34b1cc714a299e Reviewed-by: Volker Hilsheimer ================================= cherry-picked from e6d85cf28bc4f750b69c33b72c006b067ae1190f QDockWidget: Fix group unplugging A floating dock widget could either be a single QDockWidget object, or a QDockWidgetGroupWindow with a single QDockWidget child. The former could be dropped on the latter. Dropping the latter on the former caused a crash. The existence of QDockWidgetGroupWindows with a single dock widget child was accepted to be by design. Previous fixes, such as 9ff40b59da58160dc26c54204a615a2456e07405, attempted to wrap all single floating dock widgets in QDockWidgetGroupWindows. These attempts fell short, because of the manifold programmatic and manual options to create a floating dock widget: - drag a single dock widget out of a main window dock area - drag a dock widget out of a tab bar on the main window - drag a dock widget out of a floating tab - call `QDockWidget::setFloating(true)` in any situation - create a new QDockWidget, that floats from the beginning Whenever a QDockWidgetGroupWindow with a single QDockWidget child was hovered and/or dropped on a QDockWidget without a group window, crashes or screen artifacts were observed. Previous fixes made them occur less often. QDockWidgetGroupWindow is not designed to hold a single QDockWidget child. Such a state is inconsistent and may only exist, while a QDockWidgetGroupWindow is constructed. The reason why such invalid QDockWidgetGroupWindows started to exist, is a bool trap: QDockWidgetPrivate::mouseMoveEvent() starts a drag operation, when a dock widget is moved by mouse. It called startDrag() with no argument, which defaulted to startDrag(true) and caused a group drag. This assumption is *correct*, when a tabbed group of dock widgets is dragged out of the main dock as a whole, to become floating tabs. *wrong*, when a single dock widget is dragged out of a docked group, to become a single floating dock widget. In the second case, the dock widget was wrapped in a new, floating, invisible QDockWidgetGroupWindow. Looking like a single, floating dock widget, the group window caused a crash, when attempted to be dropped on another dock widget. This patch eliminates all cases, where a QDockWidgetGroupWindow with a single QDockWidget is created: (1) Implement QDockWidgetPrivate::isTabbed(). This enables mouseMoveEvent to determine, whether the move relates to a group of tabbed dock widgets, or to a single dock widget. startDrag() can therefore be called with the right argument. It will no longer create a QDockWidgetGroupWindow with a single QDockWidget child. (2) Change QMainWindowTabBar::mouseReleaseEvent When a dock widget was dragged out of a tab bar and became a single, floating dock widget, it was still parented to the group window. That is wrong, because it has no more relationship with that group window. => Reparent it to the main window, just like any other single floating dock widget. That enables QDockWidgetGroupWindow to detect, that the 2nd last child has gone and no more group window is needed (see next point). (3) React to reparenting, closing and deleting If the second last dock widget in a floating tab gets closed (manually or programmatically), reparented or deleted, also unplug the last one and remove the group window. (4) Amend 9ff40b59da58160dc26c54204a615a2456e07405 Remove the code path where a QDockWidgetGroupWindow with a single QDockWidget child was created 'just in case', to make it compatible others, created by (1), (2) or (3). (5) Change QMainWindowLayout::hover() When the hover ends without a successful drop and a temporary group window with a single dock widget child has been created, remove the group window. The patch fixes smaller inconsistencies, which have not become visible due to assertions and crashes earlier in the chain. The patch finally extends tst_QDockWidget, to cover all 4 cases. - Creation of floating tabs The creation of floating tabs is extracted from floatingTabs() to the helper function createFloatingTabs(). In addition to creating floating tabs, the helper verifies that dragging a dock widget out of the main window doesn't accidently wrap it in a group window. This covers case (1). - tst_QDockWidget::floatingTabs() The test function verifies now, that both test dock widgets have the same path before plugging them together and after unplugging them from the floating tab. This covers case(4). - tst_QDockwidget::deleteFloatingTabWithSingleDockWidget() This test function is added, to cover cases (2) and (3). - tst_QDockWidget::hoverWithoutDrop() This test function hovers two floating dock widgets hover each other, and returns the moved dock widget to its origin before releasing the mouse. This covers case(5). This fixes a lot of long standing bugs, making the author of this patch modestly happy :-) Fixes: QTBUG-118223 Fixes: QTBUG-99136 Fixes: QTBUG-118578 Fixes: QTBUG-118579 Fixes: QTBUG-56799 Fixes: QTBUG-35736 Fixes: QTBUG-63448 Fixes: QTBUG-88329 Fixes: QTBUG-88157 Fixes: QTBUG-94097 Fixes: QTBUG-44540 Fixes: QTBUG-53808 Fixes: QTBUG-72915 Fixes: QTBUG-53438 Found-by: Keith Kyzivat Found-by: Frederic Lefebvre Pick-to: 6.6 6.5 Change-Id: I51b5f9e40cb2dbe55fb14d769541067730538463 Reviewed-by: Volker Hilsheimer ================================= cherry-picked from 0b10b7476cf9d41086063ec49555425c6871041c Refactor tst_QDockWidget::closeAndDelete() The test function was temporarily blacklisted on Ubuntu, but also failing on other Linux platforms (e.g. openSuSE). It tested, whether closing all dock widgets and the main window, would close the application as well. It used one single shot timer, to close the windows and later one to check, whether the application was shut down. While that mechanism must work in an application environment, it is not guaranteed to work in testlib. More specifically, I could happen that the XCB / glib event loop continued to spin and wait for events. => Check the signal QGuiApplication::lastWindowClosed() instead. If the signal is fired, it is proven that all windows have been closed and the application would quit in a production environment. The underlying test case was: Application didn't quit with the last dock widget closed, because there was a dangling QDockWidgetGroupWindow. => finally: Clean up BLACKLIST Pick-to: 6.6 6.5 Change-Id: Ic5fde5967fc8dde70ab64dc30cc7367c908b5c51 Reviewed-by: Volker Hilsheimer ================================= cherry-picked from 26ded9dedf3f35e93b81bd0281fdd0bd30acb9d4 tst_QDockWidget: use local context for timer If we use 'this' as context it might still try to invoke the timer after we have left the function, leading to stack-use-after-return. To avoid doing an in-depth dive if it's okay to use mainWindow as the context or not, I just added a new local QObject. Amends 9ff40b59da58160dc26c54204a615a2456e07405 Pick-to: 6.6 6.5 Change-Id: I2c3bdc1eb06731d9c38979610303876c2748fb73 Reviewed-by: Volker Hilsheimer ================================= cherry-picked from 17372faf3f5e4a9c2577007ed5b38275e64bad89 Manual conflict resolution: This was built ontop of a change made for wayland, which I decided didn't need to be cherry-picked. In the conflict area, just drop the one conflict as the change made was modifying the change introduced by the wayland change. Remove bool trap in QDockWidgetPrivate::endDrag() endDrag(false) meant to end a drag with a dock location change. endDrag(true) meant to abort a drag without a dock location change. Replace this with a meaningful enumeration. Define a dummy enum for builds w/o QDockWidget. Task-number: QTBUG-118578 Task-number: QTBUG-118579 Pick-to: 6.6 6.5 Change-Id: I786f4210f5a3ee67ffcf0dc9285f77a480148569 Reviewed-by: David Faure ================================= cherry-picked from c93ab8c2a015b40b9a487ed9f23a72aebea8d52a Manual conflict resolution: Unclear what changed - all changes in the commit apply, but git stopped for manual conflict resolution. Possibly a whitespace change. QDockWidget: Remove "group" bool trap The unplug() and startDrag() functions of QMainWindowLayout and QDockWidget used a boolean argument specifying whether a single dock widget or a group of dock widgets should be unplugged. The argument defaulted to true. That has lead to inconsistent unplug operations, broken item_lists and crashes, especially when the methods were called without an argument. To improve code readability, replace bool trap with a meaningful enum. Remove default arguments, in order to force explicit calls. This patch does not change behavior, it is just carved out to facilitate reviews. Task-number: QTBUG-118578 Task-number: QTBUG-118579 Pick-to: 6.6 6.5 Change-Id: I50341b055f0bb76c2797b2fb1126a10de1fee7dd Reviewed-by: Volker Hilsheimer ================================= cherry-picked from c02f8b9d4d9a6578bb2017bafbb963d09641383f Fully export QDockWidget debug operator Amends bbeff2a3350dd3396400865525d509b784c2d93e. Task-number: QTBUG-118578 Task-number: QTBUG-118579 Found-by: Friedemann Kleint Pick-to: 6.6 Change-Id: I60d8d11a82ff5de8b9641c86b824969fc9a34b91 Reviewed-by: Friedemann Kleint Reviewed-by: Fabian Kosmale ================================= cherry-picked from bbeff2a3350dd3396400865525d509b784c2d93e Extend qDebug functionality for QDockWidget Add features and floating flag to QDockWidget debugging. Debug dockwidget parents of a group window. Task-number: QTBUG-118578 Task-number: QTBUG-118579 Pick-to: 6.6 Change-Id: If2a6caacf5d02f9018c2a3073fdbc1de39bce1ee Reviewed-by: Santhosh Kumar Reviewed-by: Keith Kyzivat ================================= cherry-picked from b6b489db6968fbc3b4aab3755d0166ee09712ff0 QMainWindowTabBar: Add destructor QMainWindowLayout re-uses tab bars. A QSet and a QList member are kept, to track used and unused tab bars. Corner cases upon application close down leave dangling pointers in those containers. => Add a destructor to QMainWindowTabBar => remove the tab bar from used and unused tab bar containers, if not directly parented to the main window. => No longer reparent unused tab bars of a QDockWidgetGroupWindow to the main window. Let them be destroyed as a group window child, and its destructor remove it from the used/unused tab bar container. Pick-to: 6.6 6.5 Change-Id: If2388cf878553dc89583dbc8585748fad65bbab2 Reviewed-by: Volker Hilsheimer ================================= cherry-picked from 2c96f517714eb67a9af821141a90eed68b8714ed QDockAreaLayout: implement widget based add() and remove() The item_list of a QDockAreaLayoutInfo has abstraction methods for reading the item list. Adding to and removing from the item list is done directly, by using the QList api. Implement an abstraction, that takes a QWidget *. The argument may either be a QDockWidgetGroupWindow or a QDockWidget. Task-number: QTBUG-118578 Task-number: QTBUG-118579 Pick-to: 6.6 6.5 Change-Id: Ib2ccd7557a21a43b68f184fe4575018f2a97004b Reviewed-by: David Faure ================================= cherry-picked from 1ab91b7bdbcced6ea4cc176da1d18e6b789208ae QDockWidget: call raise() when a dock widget starts to hover When dock widget (1) starts to hover over another floating dock widget (2), the latter animates a rubber band, to indicate to the user that it is ready to accept a drop. The creation of a QRubberBand moves (2) one position up in the Z order. The consequence is a visual glitch: While - the mouse cursor dragging (1) is still outside (2) and - the visual rectangle of (1) starts overlapping (2) (1) hides behind (2). As soon as the mouse cursor enters (2), (1) suddenly comes on top and (2) hides behind (1). => raise() 1 as soon as it starts hovering. That brings it on top of the Z order, which is expected behavior. Pick-to: 6.6 6.5 Change-Id: I1140fc6ff109c7a713e7e2617072698467375585 Reviewed-by: Richard Moe Gustavsen Reviewed-by: David Faure ================================= cherry-picked from 4c60a11d8f935abb762a83b0ab99cefa6db3060c QDockWidget: Always show dock widgets with the main window QMainWindow::show() also showed its dock widget children. When a main window with dock widget children consumed a show event for another reason, hidden dock widget children remained hidden. If a dock widget application went to the background, e.g. because it was hidden behind another application gaining focus, a klick on the dock widget application's app icon would not show its dock widget children. Unless the dock widget application provides shows them explicitly, they can never been shown again by the user. => show all dock widget and group window children, when QMainWindow consumes a show event. Pick-to: 6.6 6.5 Change-Id: I7e8b59f021ec4ec5679d0d08d0eeda1e3225a385 Reviewed-by: David Faure ================================= cherry-picked from 0e435b7f13619106d61c9701e53803d3c488fea3 QMainWindowLayout: remove redundant #ifdef'ry Remove #if QT_CONFIG(dockwidget) nested within each other. Pick-to: 6.6 6.5 Change-Id: I6c662909676ffada3ac52e41a9c2d8b9fd491689 Reviewed-by: David Faure --- src/widgets/widgets/qdockarealayout.cpp | 47 ++- src/widgets/widgets/qdockarealayout_p.h | 3 + src/widgets/widgets/qdockwidget.cpp | 61 ++- src/widgets/widgets/qdockwidget.h | 5 + src/widgets/widgets/qdockwidget_p.h | 15 +- src/widgets/widgets/qmainwindow.cpp | 7 + src/widgets/widgets/qmainwindowlayout.cpp | 376 ++++++++++++------ src/widgets/widgets/qmainwindowlayout_p.h | 10 +- src/widgets/widgets/qtoolbar.cpp | 2 +- .../widgets/widgets/qdockwidget/BLACKLIST | 33 +- .../widgets/qdockwidget/tst_qdockwidget.cpp | 261 +++++++++--- 11 files changed, 616 insertions(+), 204 deletions(-) diff --git a/src/widgets/widgets/qdockarealayout.cpp b/src/widgets/widgets/qdockarealayout.cpp index 2f1b7aa6e60..da0e9871715 100644 --- a/src/widgets/widgets/qdockarealayout.cpp +++ b/src/widgets/widgets/qdockarealayout.cpp @@ -172,13 +172,27 @@ QDockAreaLayoutItem } #ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug dbg, const QDockAreaLayoutItem *item) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + return item ? dbg << *item : dbg << "QDockAreaLayoutItem(0x0)"; +} + QDebug operator<<(QDebug dbg, const QDockAreaLayoutItem &item) { QDebugStateSaver saver(dbg); dbg.nospace(); dbg << "QDockAreaLayoutItem(" << static_cast(&item) << "->"; if (item.widgetItem) { - dbg << "widgetItem(" << item.widgetItem->widget() << ")"; + QWidget *widget = item.widgetItem->widget(); + if (auto *dockWidget = qobject_cast(widget)) { + dbg << "widgetItem(" << dockWidget << ")"; + } else if (auto *groupWindow = qobject_cast(widget)) { + dbg << "widgetItem(" << groupWindow << "->(" << groupWindow->dockWidgets() << "))"; + } else { + dbg << "widgetItem(" << widget << ")"; + } } else if (item.subinfo) { dbg << "subInfo(" << item.subinfo << "->(" << item.subinfo->item_list << ")"; } else if (item.placeHolderItem) { @@ -1004,6 +1018,14 @@ void QDockAreaLayoutInfo::remove(const QList &path) } } +void QDockAreaLayoutInfo::remove(QWidget *widget) +{ + const QList path = indexOf(widget); + if (path.isEmpty()) + return; + remove(path); +} + QLayoutItem *QDockAreaLayoutInfo::plug(const QList &path) { Q_ASSERT(!path.isEmpty()); @@ -1134,8 +1156,6 @@ bool QDockAreaLayoutInfo::insertGap(const QList &path, QLayoutItem *dockWid index = -index - 1; } -// dump(qDebug() << "insertGap() before:" << index << tabIndex, *this, QString()); - if (path.size() > 1) { QDockAreaLayoutItem &item = item_list[index]; @@ -1764,6 +1784,26 @@ QLayoutItem *QDockAreaLayoutInfo::takeAt(int *x, int index) return nullptr; } +// Add a dock widget or dock widget group window to the item list +void QDockAreaLayoutInfo::add(QWidget *widget) +{ + // Do not add twice + if (!indexOf(widget).isEmpty()) + return; + + if (auto *dockWidget = qobject_cast(widget)) { + item_list.append(QDockAreaLayoutItem(new QDockWidgetItem(dockWidget))); + return; + } + + if (auto *groupWindow = qobject_cast(widget)) { + item_list.append(QDockAreaLayoutItem(new QDockWidgetGroupWindowItem(groupWindow))); + return; + } + + qFatal("Coding error. Add supports only QDockWidget and QDockWidgetGroupWindow"); +} + void QDockAreaLayoutInfo::deleteAllLayoutItems() { for (int i = 0; i < item_list.size(); ++i) { @@ -1957,6 +1997,7 @@ bool QDockAreaLayoutInfo::restoreState(QDataStream &stream, QList if (testing) { //was it is not really added to the layout, we need to delete the object here delete item.widgetItem; + item.widgetItem = nullptr; } } } else if (nextMarker == SequenceMarker) { diff --git a/src/widgets/widgets/qdockarealayout_p.h b/src/widgets/widgets/qdockarealayout_p.h index 4a5e8901504..bf7e4682e5d 100644 --- a/src/widgets/widgets/qdockarealayout_p.h +++ b/src/widgets/widgets/qdockarealayout_p.h @@ -75,6 +75,7 @@ struct Q_AUTOTEST_EXPORT QDockAreaLayoutItem uint flags; #ifndef QT_NO_DEBUG_STREAM friend Q_AUTOTEST_EXPORT QDebug operator<<(QDebug dbg, const QDockAreaLayoutItem &item); + friend Q_AUTOTEST_EXPORT QDebug operator<<(QDebug dbg, const QDockAreaLayoutItem *item); #endif }; @@ -108,6 +109,7 @@ class Q_AUTOTEST_EXPORT QDockAreaLayoutInfo QList gapIndex(const QPoint &pos, bool nestingEnabled, TabMode tabMode) const; void remove(const QList &path); + void remove(QWidget *widget); void unnest(int index); void split(int index, Qt::Orientation orientation, QLayoutItem *dockWidgetItem); #if QT_CONFIG(tabbar) @@ -155,6 +157,7 @@ class Q_AUTOTEST_EXPORT QDockAreaLayoutInfo QLayoutItem *itemAt(int *x, int index) const; QLayoutItem *takeAt(int *x, int index); + void add(QWidget *widget); void deleteAllLayoutItems(); QMainWindowLayout *mainWindowLayout() const; diff --git a/src/widgets/widgets/qdockwidget.cpp b/src/widgets/widgets/qdockwidget.cpp index 1a672affb18..2e61ba55c54 100644 --- a/src/widgets/widgets/qdockwidget.cpp +++ b/src/widgets/widgets/qdockwidget.cpp @@ -768,7 +768,7 @@ void QDockWidgetPrivate::initDrag(const QPoint &pos, bool nca) tabbed widgets, and false if the dock widget should always be dragged alone. */ -void QDockWidgetPrivate::startDrag(bool group) +void QDockWidgetPrivate::startDrag(DragScope scope) { Q_Q(QDockWidget); @@ -778,7 +778,7 @@ void QDockWidgetPrivate::startDrag(bool group) QMainWindowLayout *layout = qt_mainwindow_layout_from_dock(q); Q_ASSERT(layout != nullptr); - state->widgetItem = layout->unplug(q, group); + state->widgetItem = layout->unplug(q, scope); if (state->widgetItem == nullptr) { /* Dock widget has a QMainWindow parent, but was never inserted with QMainWindow::addDockWidget, so the QMainWindowLayout has no @@ -803,7 +803,7 @@ void QDockWidgetPrivate::startDrag(bool group) The \a abort parameter specifies that it ends because of programmatic state reset rather than mouse release event. */ -void QDockWidgetPrivate::endDrag(bool abort) +void QDockWidgetPrivate::endDrag(EndDragMode mode) { Q_Q(QDockWidget); Q_ASSERT(state != nullptr); @@ -815,7 +815,7 @@ void QDockWidgetPrivate::endDrag(bool abort) Q_ASSERT(mainWindow != nullptr); QMainWindowLayout *mwLayout = qt_mainwindow_layout(mainWindow); - if (abort || !mwLayout->plug(state->widgetItem)) { + if (mode == EndDragMode::Abort || !mwLayout->plug(state->widgetItem)) { if (hasFeature(this, QDockWidget::DockWidgetFloatable)) { // This QDockWidget will now stay in the floating state. if (state->ownWidgetItem) { @@ -847,6 +847,11 @@ void QDockWidgetPrivate::endDrag(bool abort) 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 { @@ -944,6 +949,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; @@ -974,7 +988,8 @@ bool QDockWidgetPrivate::mouseMoveEvent(QMouseEvent *event) } else #endif { - startDrag(); + const DragScope scope = isTabbed() ? DragScope::Group : DragScope::Widget; + startDrag(scope); q->grabMouse(); ret = true; } @@ -1040,7 +1055,7 @@ bool QDockWidgetPrivate::mouseReleaseEvent(QMouseEvent *event) #if QT_CONFIG(mainwindow) if (event->button() == Qt::LeftButton && state && !state->nca) { - endDrag(); + endDrag(EndDragMode::LocationChange); return true; //filter out the event } @@ -1079,22 +1094,21 @@ void QDockWidgetPrivate::nonClientAreaMouseEvent(QMouseEvent *event) break; state->ctrlDrag = (event->modifiers() & Qt::ControlModifier) || (!hasFeature(this, QDockWidget::DockWidgetMovable) && q->isFloating()); - startDrag(); + startDrag(DragScope::Group); break; case QEvent::NonClientAreaMouseMove: if (state == nullptr || !state->dragging) break; #if !defined(Q_OS_MAC) && !defined(Q_OS_WASM) - if (state->nca) { - endDrag(); - } + if (state->nca) + endDrag(EndDragMode::LocationChange); #endif break; case QEvent::NonClientAreaMouseButtonRelease: #if defined(Q_OS_MAC) || defined(Q_OS_WASM) if (state) - endDrag(); + endDrag(EndDragMode::LocationChange); #endif break; case QEvent::NonClientAreaMouseButtonDblClick: @@ -1418,7 +1432,7 @@ void QDockWidget::setFloating(bool floating) // the initial click of a double-click may have started a drag... if (d->state != nullptr) - d->endDrag(true); + d->endDrag(QDockWidgetPrivate::EndDragMode::Abort); QRect r = d->undockedGeometry; // Keep position when undocking for the first time. @@ -1506,7 +1520,7 @@ void QDockWidget::closeEvent(QCloseEvent *event) { Q_D(QDockWidget); if (d->state) - d->endDrag(true); + d->endDrag(QDockWidgetPrivate::EndDragMode::Abort); QWidget::closeEvent(event); } @@ -1782,6 +1796,27 @@ QWidget *QDockWidget::titleBarWidget() const return layout->widgetForRole(QDockWidgetLayout::TitleBar); } +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug dbg, const QDockWidget *dockWidget) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + return dockWidget ? dbg << *dockWidget : dbg << "QDockWidget(0x0)"; +} + +QDebug operator<<(QDebug dbg, const QDockWidget &dockWidget) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QDockWidget(" << static_cast(&dockWidget); + dbg << "->(ObjectName=" << dockWidget.objectName(); + dbg << "; floating=" << dockWidget.isFloating(); + dbg << "; features=" << dockWidget.features(); + dbg << ";))"; + return dbg; +} +#endif // QT_NO_DEBUG_STREAM + QT_END_NAMESPACE #include "qdockwidget.moc" diff --git a/src/widgets/widgets/qdockwidget.h b/src/widgets/widgets/qdockwidget.h index 8123bbb7298..c285b58d52d 100644 --- a/src/widgets/widgets/qdockwidget.h +++ b/src/widgets/widgets/qdockwidget.h @@ -64,6 +64,11 @@ class Q_WIDGETS_EXPORT QDockWidget : public QWidget inline bool isAreaAllowed(Qt::DockWidgetArea area) const { return (allowedAreas() & area) == area; } +#ifndef QT_NO_DEBUG_STREAM + friend Q_WIDGETS_EXPORT QDebug operator<<(QDebug dbg, const QDockWidget &dockWidget); + friend Q_WIDGETS_EXPORT QDebug operator<<(QDebug dbg, const QDockWidget *dockWidget); +#endif + #ifndef QT_NO_ACTION QAction *toggleViewAction() const; #endif diff --git a/src/widgets/widgets/qdockwidget_p.h b/src/widgets/widgets/qdockwidget_p.h index 326c55cc788..10bc2110d94 100644 --- a/src/widgets/widgets/qdockwidget_p.h +++ b/src/widgets/widgets/qdockwidget_p.h @@ -51,6 +51,16 @@ class QDockWidgetPrivate : public QWidgetPrivate }; public: + enum class DragScope { + Group, + Widget + }; + + enum class EndDragMode { + LocationChange, + Abort + }; + void init(); void _q_toggleView(bool); // private slot void _q_toggleTopLevel(); // private slot @@ -86,8 +96,8 @@ class QDockWidgetPrivate : public QWidgetPrivate void setWindowState(bool floating, bool unplug = false, const QRect &rect = QRect()); void nonClientAreaMouseEvent(QMouseEvent *event); void initDrag(const QPoint &pos, bool nca); - void startDrag(bool group = true); - void endDrag(bool abort = false); + void startDrag(DragScope scope); + void endDrag(EndDragMode mode); void moveEvent(QMoveEvent *event); void recalculatePressPos(QResizeEvent *event); @@ -96,6 +106,7 @@ class QDockWidgetPrivate : public QWidgetPrivate void setResizerActive(bool active); bool isAnimating() const; + bool isTabbed() const; private: QWidgetResizeHandler *resizer = nullptr; diff --git a/src/widgets/widgets/qmainwindow.cpp b/src/widgets/widgets/qmainwindow.cpp index 10489f830b4..fec367e9dd5 100644 --- a/src/widgets/widgets/qmainwindow.cpp +++ b/src/widgets/widgets/qmainwindow.cpp @@ -1287,6 +1287,13 @@ bool QMainWindow::event(QEvent *event) return true; #endif // QT_CONFIG(statustip) +#if QT_CONFIG(dockwidget) + case QEvent::Show: + Q_ASSERT(d->layout); + d->layout->showDockWidgets(); + break; +#endif // QT_CONFIG(dockwidget) + case QEvent::StyleChange: #if QT_CONFIG(dockwidget) Q_ASSERT(d->layout); diff --git a/src/widgets/widgets/qmainwindowlayout.cpp b/src/widgets/widgets/qmainwindowlayout.cpp index 5571b9e51cd..9f7bc68357e 100644 --- a/src/widgets/widgets/qmainwindowlayout.cpp +++ b/src/widgets/widgets/qmainwindowlayout.cpp @@ -147,18 +147,57 @@ 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) -/****************************************************************************** - ** QDockWidgetGroupWindow - */ -// QDockWidgetGroupWindow is the floating window containing several QDockWidgets floating together. -// (QMainWindow::GroupedDragging feature) -// QDockWidgetGroupLayout is the layout of that window and use a QDockAreaLayoutInfo to layout -// the QDockWidgets inside it. -// If there is only one QDockWidgets, or all QDockWidgets are tabbed together, it is equivalent -// of a floating QDockWidget (the title of the QDockWidget is the title of the window). But if there -// are nested QDockWidget, an additional title bar is there. + +/*! + \internal + QDockWidgetGroupWindow is a floating window, containing several QDockWidgets floating together. + This requires QMainWindow::GroupedDragging to be enabled. + QDockWidgets floating jointly in a QDockWidgetGroupWindow are considered to be docked. + Their \c isFloating property is \c false. + QDockWidget children of a QDockWidgetGroupWindow are either: + \list + \li tabbed (as long as Qt is compiled with the \c tabbar feature), or + \li arranged next to each other, equivalent to the default on a main window dock. + \endlist + + QDockWidgetGroupWindow uses QDockWidgetGroupLayout to lay out its QDockWidget children. + It stores layout information in a QDockAreaLayoutInfo, including temporary spacer items + and rubber bands. + + If its QDockWidget children are tabbed, the QDockWidgetGroupWindow shows the active QDockWidget's + title as its own window title. + + QDockWidgetGroupWindow is designed to hold more than one QDockWidget. + A QDockWidgetGroupWindow with only one QDockWidget child may occur only temporarily + \list + \li in its construction phase, or + \li during a hover: While QDockWidget A is hovered over B, B is converted into a QDockWidgetGroupWindow. + \endlist + + A QDockWidgetGroupWindow with only one QDockWidget child must never get focus, be dragged or dropped. + To enforce this restriction, QDockWidgetGrouWindow will remove itself after its second QDockWidget + child has been removed. It will make its last QDockWidget child a single, floating QDockWidget. + Eventually, the empty QDockWidgetGroupWindow will call deleteLater() on itself. +*/ + + #if QT_CONFIG(dockwidget) class QDockWidgetGroupLayout : public QLayout, public QMainWindowLayoutSeparatorHelper @@ -399,8 +438,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()); @@ -420,11 +459,6 @@ void QDockWidgetGroupWindow::destroyOrHideIfEmpty() if (!wasHidden) dw->show(); } -#if QT_CONFIG(tabbar) - const auto tabBars = findChildren(Qt::FindDirectChildrenOnly); - for (QTabBar *tb : tabBars) - tb->setParent(parentWidget()); -#endif deleteLater(); } @@ -608,6 +642,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 /****************************************************************************** @@ -1220,7 +1356,7 @@ bool QMainWindowLayoutState::restoreState(QDataStream &_stream, if (info == nullptr) { continue; } - info->item_list.append(QDockAreaLayoutItem(new QDockWidgetItem(w))); + info->add(w); } } } @@ -1490,9 +1626,19 @@ inline static Qt::DockWidgetArea toDockWidgetArea(int pos) return QDockWidgetPrivate::toDockWidgetArea(static_cast(pos)); } +void QMainWindowLayout::showDockWidgets() const +{ + const auto dockWidgets = parent()->findChildren(Qt::FindDirectChildrenOnly); + for (auto *dockWidget : dockWidgets) + dockWidget->show(); + + const auto groupWindows = parent()->findChildren(Qt::FindDirectChildrenOnly); + for (auto *groupWindow : groupWindows) + groupWindow->show(); +} + // 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.size() > 1), "isAreaAllowed", "invalid path size"); @@ -1527,7 +1673,6 @@ static bool isAreaAllowed(QWidget *widget, const QList &path) qCDebug(lcQpaDockWidgets) << "Docking requested for invalid widget type (coding error)." << widget << area; return false; } -#endif void QMainWindowLayout::setCorner(Qt::Corner corner, Qt::DockWidgetArea area) { @@ -1547,7 +1692,6 @@ Qt::DockWidgetArea QMainWindowLayout::corner(Qt::Corner corner) const // 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); @@ -1563,7 +1707,6 @@ QRect QMainWindowLayout::dockWidgetAreaRect(const Qt::DockWidgetArea area, DockW // Return maximum or visible rectangle return (size == Maximum) ? dl.gapRect(dockPosition) : dl.docks[dockPosition].rect; } -#endif void QMainWindowLayout::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget, @@ -1739,10 +1882,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 public: QMainWindowTabBar(QMainWindow *parent); + ~QMainWindowTabBar(); + QDockWidget *dockAt(int index) const; + QList dockWidgets() const; protected: bool event(QEvent *e) override; void mouseReleaseEvent(QMouseEvent*) override; @@ -1756,6 +1903,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. @@ -1769,13 +1939,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); @@ -1788,7 +1953,7 @@ void QMainWindowTabBar::mouseMoveEvent(QMouseEvent *e) QDockWidgetPrivate *dockPriv = static_cast(QObjectPrivate::get(draggingDock)); QDockWidgetLayout *dwlayout = static_cast(draggingDock->layout()); dockPriv->initDrag(dwlayout->titleArea().center(), true); - dockPriv->startDrag(false); + dockPriv->startDrag(QDockWidgetPrivate::DragScope::Widget); if (dockPriv->state) dockPriv->state->ctrlDrag = e->modifiers() & Qt::ControlModifier; } @@ -1806,13 +1971,28 @@ void QMainWindowTabBar::mouseMoveEvent(QMouseEvent *e) QTabBar::mouseMoveEvent(e); } +QMainWindowTabBar::~QMainWindowTabBar() +{ + if (!mainWindow || mainWindow == parentWidget()) + return; + + // tab bar is not parented to the main window + // => can only be a dock widget group window + // => remove itself from used and unused tab bar containers + auto *mwLayout = qt_mainwindow_layout(mainWindow); + if (!mwLayout) + return; + mwLayout->unusedTabBars.removeOne(this); + mwLayout->usedTabBars.remove(this); +} + void QMainWindowTabBar::mouseReleaseEvent(QMouseEvent *e) { if (draggingDock && e->button() == Qt::LeftButton) { QDockWidgetPrivate *dockPriv = static_cast(QObjectPrivate::get(draggingDock)); - if (dockPriv->state && dockPriv->state->dragging) { - dockPriv->endDrag(); - } + if (dockPriv->state && dockPriv->state->dragging) + dockPriv->endDrag(QDockWidgetPrivate::EndDragMode::LocationChange); + draggingDock = nullptr; } QTabBar::mouseReleaseEvent(e); @@ -1836,6 +2016,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) { @@ -2548,25 +2745,25 @@ static QTabBar::Shape tabwidgetPositionToTabBarShape(QWidget *w) Returns the QLayoutItem of the dragged element. The layout item is kept in the layout but set as a gap item. */ -QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, bool group) +QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, QDockWidgetPrivate::DragScope scope) { #if QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget) auto *groupWindow = qobject_cast(widget->parentWidget()); if (!widget->isWindow() && groupWindow) { - if (group && groupWindow->tabLayoutInfo()) { + if (scope == QDockWidgetPrivate::DragScope::Group && groupWindow->tabLayoutInfo()) { // We are just dragging a floating window as it, not need to do anything, we just have to // look up the corresponding QWidgetItem* if it exists if (QDockAreaLayoutInfo *info = dockInfo(widget->parentWidget())) { QList groupWindowPath = info->indexOf(widget->parentWidget()); return groupWindowPath.isEmpty() ? nullptr : info->item(groupWindowPath).widgetItem; } - qCDebug(lcQpaDockWidgets) << "Drag only:" << widget << "Group:" << group; + qCDebug(lcQpaDockWidgets) << "Drag only:" << widget << "Group:" << (scope == QDockWidgetPrivate::DragScope::Group); return nullptr; } QList path = groupWindow->layoutInfo()->indexOf(widget); QDockAreaLayoutItem parentItem = groupWindow->layoutInfo()->item(path); QLayoutItem *item = parentItem.widgetItem; - if (group && path.size() > 1 + if (scope == QDockWidgetPrivate::DragScope::Group && path.size() > 1 && unplugGroup(this, &item, parentItem)) { qCDebug(lcQpaDockWidgets) << "Unplugging:" << widget << "from" << item; return item; @@ -2574,73 +2771,10 @@ QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, bool group) // 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 @@ -2659,7 +2793,7 @@ QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, bool group) if (QDockWidget *dw = qobject_cast(widget)) { Q_ASSERT(path.constFirst() == 1); #if QT_CONFIG(tabwidget) - if (group && (dockOptions & QMainWindow::GroupedDragging) && path.size() > 3 + if (scope == QDockWidgetPrivate::DragScope::Group && (dockOptions & QMainWindow::GroupedDragging) && path.size() > 3 && unplugGroup(this, &item, layoutState.dockAreaLayout.item(path.mid(1, path.size() - 2)))) { path.removeLast(); @@ -2776,6 +2910,7 @@ void QMainWindowLayout::hover(QLayoutItem *hoverTarget, QWidget *widget = hoverTarget->widget(); #if QT_CONFIG(dockwidget) + widget->raise(); if ((dockOptions & QMainWindow::GroupedDragging) && (qobject_cast(widget) || qobject_cast(widget))) { @@ -2792,7 +2927,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; @@ -2830,16 +2965,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; @@ -2852,15 +2997,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) @@ -3033,4 +3184,5 @@ bool QMainWindowLayout::restoreState(QDataStream &stream) 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 4719210e30c..c8f500a0db3 100644 --- a/src/widgets/widgets/qmainwindowlayout_p.h +++ b/src/widgets/widgets/qmainwindowlayout_p.h @@ -29,6 +29,7 @@ #include "QtCore/qset.h" #include "private/qlayoutengine_p.h" #include "private/qwidgetanimator_p.h" +#include "private/qdockwidget_p.h" #if QT_CONFIG(dockwidget) #include "qdockarealayout_p.h" @@ -322,6 +323,10 @@ class Q_AUTOTEST_EXPORT QDockWidgetGroupWindow : public QWidget void updateCurrentGapRect(); void restore(); void apply(); + void childEvent(QChildEvent *event) override; + void reparent(QDockWidget *dockWidget); + void destroyIfSingleItemLeft(); + QList dockWidgets() const { return findChildren(); } QRect currentGapRect; QList currentGapPos; @@ -331,6 +336,7 @@ class Q_AUTOTEST_EXPORT QDockWidgetGroupWindow : public QWidget protected: bool event(QEvent *) override; + bool eventFilter(QObject *obj, QEvent *event) override; void paintEvent(QPaintEvent*) override; private: @@ -492,6 +498,7 @@ class Q_AUTOTEST_EXPORT QMainWindowLayout Qt::Orientation orientation); Qt::DockWidgetArea dockWidgetArea(QWidget* widget) const; bool restoreDockWidget(QDockWidget *dockwidget); + void showDockWidgets() const; #if QT_CONFIG(tabbar) void tabifyDockWidget(QDockWidget *first, QDockWidget *second); void raise(QDockWidget *widget); @@ -561,12 +568,13 @@ class Q_AUTOTEST_EXPORT QMainWindowLayout #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; void hover(QLayoutItem *hoverTarget, const QPoint &mousePos); bool plug(QLayoutItem *widgetItem); - QLayoutItem *unplug(QWidget *widget, bool group = false); + QLayoutItem *unplug(QWidget *widget, QDockWidgetPrivate::DragScope scope); void revert(QLayoutItem *widgetItem); void applyState(QMainWindowLayoutState &newState, bool animate = true); void restore(bool keepSavedState = false); diff --git a/src/widgets/widgets/qtoolbar.cpp b/src/widgets/widgets/qtoolbar.cpp index 8a55b9c4ef2..a4b290cfde9 100644 --- a/src/widgets/widgets/qtoolbar.cpp +++ b/src/widgets/widgets/qtoolbar.cpp @@ -173,7 +173,7 @@ void QToolBarPrivate::startDrag(bool moving) Q_ASSERT(layout != nullptr); if (!moving) { - state->widgetItem = layout->unplug(q); + state->widgetItem = layout->unplug(q, QDockWidgetPrivate::DragScope::Group); Q_ASSERT(state->widgetItem != nullptr); } state->dragging = !moving; diff --git a/tests/auto/widgets/widgets/qdockwidget/BLACKLIST b/tests/auto/widgets/widgets/qdockwidget/BLACKLIST index 8b7a126b4dd..8873589ff45 100644 --- a/tests/auto/widgets/widgets/qdockwidget/BLACKLIST +++ b/tests/auto/widgets/widgets/qdockwidget/BLACKLIST @@ -5,23 +5,32 @@ android # QDockWidget::isFloating() is flaky after state change on these OS [closeAndDelete] macos +b2qt +arm +android + +# QTBUG-103091 [floatingTabs] +arm +android +qnx +macos +b2qt + # QTBUG-103091 +[hoverWithoutDrop] +arm +android qnx macos -[closeAndDelete] b2qt -[floatingTabs] -macos b2qt arm android -[closeAndDelete] + +# 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 -[floatingTabs] arm -[closeAndDelete] -macos b2qt arm android -[floatingTabs] -arm -[closeAndDelete] -android -[floatingTabs] android +macos diff --git a/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp b/tests/auto/widgets/widgets/qdockwidget/tst_qdockwidget.cpp index 61f8bf995bc..6d42394fdea 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(); @@ -80,9 +86,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())); } @@ -102,13 +114,26 @@ private slots: 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: @@ -1172,7 +1197,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); @@ -1189,12 +1214,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 @@ -1242,7 +1277,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 @@ -1304,6 +1339,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 @@ -1320,7 +1420,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); /* @@ -1328,22 +1430,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"); @@ -1400,10 +1486,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); @@ -1421,38 +1506,99 @@ void tst_QDockWidget::floatingTabs() QCOMPARE(d2->windowTitle(), newD2); // 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 } +void tst_QDockWidget::deleteFloatingTabWithSingleDockWidget_data() +{ #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) + 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 +} -// 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() { - Q_ASSERT(oldMessageHandler); + 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 - if (type == QtWarningMsg && QString(context.category) == "qt.qpa.xcb" && msg.contains("internal error")) { - Q_ASSERT(qThis); - qThis->xcbError = true; + 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() @@ -1539,7 +1685,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 @@ -1551,8 +1697,10 @@ void tst_QDockWidget::closeAndDelete() 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] { + QSignalSpy closeSpy(qApp, &QApplication::lastWindowClosed); + QObject localContext; + + QTimer::singleShot(0, &localContext, [&](){ mainWindow->close(); QTRY_VERIFY(!mainWindow->isVisible()); QTRY_VERIFY(d1->isVisible()); @@ -1561,19 +1709,12 @@ void tst_QDockWidget::closeAndDelete() d2->close(); QTRY_VERIFY(!d1->isVisible()); QTRY_VERIFY(!d2->isVisible()); - }); - - // Fallback timer to report event loop still running - QTimer::singleShot(100, this, [&eventLoopStopped] { - qCDebug(lcTestDockWidget) << "Last dock widget hasn't shout down event loop!"; - eventLoopStopped = false; + QTRY_COMPARE(closeSpy.count(), 1); QApplication::quit(); }); QApplication::exec(); - QTRY_VERIFY(eventLoopStopped); - // Check heap cleanup qCDebug(lcTestDockWidget) << "Deleting mainWindow"; up_mainWindow.reset(); @@ -1631,16 +1772,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.";