Fix QDockWidget parenting and dock permissions

Check DockWidgetArea permissions of QDockWidgetGroupWindows with single
dock widget. Obtain a dock widget's tab position from a dock widget
group window if it can't be established otherwise. Remove hardcoded
assumption that a dock widget is in the left dock. Both cases have lead
to inconsistent entries and dangling pointers in
QDockAreaLayoutInfo::item_list.
Remove warning: QMainWindowLayout::tabPosition called with out-of-bounds
value '0', which becomes obsolete by the fix.
Create a QDockWidgetGroup window prepered to become a floating tab,
whenever a dock widget is being hovered over. Store it in item_list so
it can be found and deleted when required.
No longer call e->ignore() after propagating close events to the first
dock widget and thus preventing others from receiving the event.
Add logging category qt.widgets.dockwidgets
Update dock widget autotest with tests to check the fixes mentioned:
plugging, unplugging, hiding, showing, closing and deleting.
Blackist closeAndDelete, floatingTabs test on macos, QEMU, arm, android
due to flaky isFloating() response after a dock widget has been closed
or plugged.
QSKIP dockPermissions and floatingTabs test on Windows due to mouse
simulation malfunction.
QSKIP hideAndShow test on Linux in case of xcb error (QTBUG-82059)

Fixes: QTBUG-99136
Pick-to: 6.3 6.2
Change-Id: Ibd353e0acc9831a0d67c9f682429ab46b94bdbb0
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
This commit is contained in:
Axel Spoerl 2022-03-04 11:05:47 +01:00 committed by Axel Spoerl
parent 908e85cc85
commit 9ff40b59da
9 changed files with 935 additions and 144 deletions

View File

@ -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<int> &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<int> &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<int> 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<QInternal::DockPosition>(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<int> QDockAreaLayout::gapIndex(const QPoint &pos, bool disallowTabs) const
return QList<int>();
}
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<int> QDockAreaLayout::findSeparator(const QPoint &pos) const
{
QList<int> result;

View File

@ -84,7 +84,7 @@ class QTabBar;
// A path indetifies uniquely one object in this tree, the first number being the side and all the following
// indexes into the QDockAreaLayoutInfo::item_list.
struct QDockAreaLayoutItem
struct Q_AUTOTEST_EXPORT QDockAreaLayoutItem
{
enum ItemFlags { NoFlags = 0, GapItem = 1, KeepSize = 2 };
@ -302,6 +302,7 @@ public:
void setGrid(QList<QLayoutStruct> *ver_struct_list, QList<QLayoutStruct> *hor_struct_list);
QRect gapRect(const QList<int> &path) const;
QRect gapRect(QInternal::DockPosition dockPos) const;
void keepSize(QDockWidget *w);
#if QT_CONFIG(tabbar)

View File

@ -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<QDockWidgetGroupWindow*>(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<QDockWidgetGroupWindow *>(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);

View File

@ -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;

View File

@ -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<QDockWidget *>(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<QMainWindow *>(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<QMainWindow *>(parentWidget())->addDockWidget(area, dw);
qCDebug(lcQpaDockWidgets) << "Redocked to Mainwindow:" << area << dw << "by" << this;
}
if (!wasHidden)
dw->show();
@ -1235,8 +1237,9 @@ bool QMainWindowLayoutState::restoreState(QDataStream &_stream,
{
auto dockWidgets = allMyDockWidgets(mainWindow);
QDockWidgetGroupWindow* floatingTab = qt_mainwindow_layout(mainWindow)->createTabbedDockWindow();
*floatingTab->layoutInfo() = QDockAreaLayoutInfo(&dockAreaLayout.sep, QInternal::LeftDock,
Qt::Horizontal, QTabBar::RoundedSouth, mainWindow);
*floatingTab->layoutInfo() = QDockAreaLayoutInfo(
&dockAreaLayout.sep, QInternal::LeftDock, // FIXME: DockWidget doesn't save original docking area
Qt::Horizontal, QTabBar::RoundedSouth, mainWindow);
QRect geometry;
stream >> geometry;
QDockAreaLayoutInfo *info = floatingTab->layoutInfo();
@ -1489,25 +1492,50 @@ static QInternal::DockPosition toDockPos(Qt::DockWidgetArea area)
return QInternal::DockCount;
}
static Qt::DockWidgetArea toDockWidgetArea(QInternal::DockPosition pos)
{
switch (pos) {
case QInternal::LeftDock : return Qt::LeftDockWidgetArea;
case QInternal::RightDock : return Qt::RightDockWidgetArea;
case QInternal::TopDock : return Qt::TopDockWidgetArea;
case QInternal::BottomDock : return Qt::BottomDockWidgetArea;
default:
break;
}
return Qt::NoDockWidgetArea;
}
inline static Qt::DockWidgetArea toDockWidgetArea(int pos)
{
return toDockWidgetArea(static_cast<QInternal::DockPosition>(pos));
return QDockWidgetPrivate::toDockWidgetArea(static_cast<QInternal::DockPosition>(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<int> &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<QDockWidget *>(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<QDockWidgetGroupWindow *>(widget)) {
const QList<QDockWidget *> children = dwgw->findChildren<QDockWidget *>(QString(), Qt::FindDirectChildrenOnly);
if (children.count() == 1) {
// Group window has a single child => read its permissions
const bool allowed = children.at(0)->isAreaAllowed(area);
if (!allowed)
qCDebug(lcQpaDockWidgets) << "No permission for DockWidgetGroupWindow" << widget << "to dock on" << area;
return allowed;
} else {
// Group window has more than one or no children => dock it anywhere
qCDebug(lcQpaDockWidgets) << "DockWidgetGroupWindow" << widget << "has" << children.count() << "children:";
qCDebug(lcQpaDockWidgets) << children;
qCDebug(lcQpaDockWidgets) << "DockWidgetGroupWindow" << widget << "can dock at" << area << "and anywhere else.";
return true;
}
}
qCDebug(lcQpaDockWidgets) << "Docking requested for invalid widget type (coding error)." << widget << area;
return false;
}
#endif
void QMainWindowLayout::setCorner(Qt::Corner corner, Qt::DockWidgetArea area)
{
if (layoutState.dockAreaLayout.corners[corner] == area)
@ -1523,6 +1551,27 @@ Qt::DockWidgetArea QMainWindowLayout::corner(Qt::Corner corner) const
return layoutState.dockAreaLayout.corners[corner];
}
// Returns the rectangle of a dockWidgetArea
// if max is true, the maximum possible rectangle for dropping is returned
// the current visible rectangle otherwise
#if QT_CONFIG(dockwidget)
QRect QMainWindowLayout::dockWidgetAreaRect(const Qt::DockWidgetArea area, DockWidgetAreaSize size) const
{
const QInternal::DockPosition dockPosition = toDockPos(area);
// Called with invalid dock widget area
if (dockPosition == QInternal::DockCount) {
qCDebug(lcQpaDockWidgets) << "QMainWindowLayout::dockWidgetAreaRect called with" << area;
return QRect();
}
const QDockAreaLayout dl = layoutState.dockAreaLayout;
// Return maximum or visible rectangle
return (size == Maximum) ? dl.gapRect(dockPosition) : dl.docks[dockPosition].rect;
}
#endif
void QMainWindowLayout::addDockWidget(Qt::DockWidgetArea area,
QDockWidget *dockwidget,
Qt::Orientation orientation)
@ -1605,7 +1654,7 @@ void QMainWindowLayout::setTabShape(QTabWidget::TabShape tabShape)
QTabWidget::TabPosition QMainWindowLayout::tabPosition(Qt::DockWidgetArea area) const
{
const auto dockPos = toDockPos(area);
const QInternal::DockPosition dockPos = toDockPos(area);
if (dockPos < QInternal::DockCount)
return tabPositions[dockPos];
qWarning("QMainWindowLayout::tabPosition called with out-of-bounds value '%d'", int(area));
@ -2470,7 +2519,6 @@ static bool unplugGroup(QMainWindowLayout *layout, QLayoutItem **item,
return false;
// The QDockWidget is part of a group of tab and we need to unplug them all.
QDockWidgetGroupWindow *floatingTabs = layout->createTabbedDockWindow();
QDockAreaLayoutInfo *info = floatingTabs->layoutInfo();
*info = std::move(*parentItem.subinfo);
@ -2485,6 +2533,30 @@ static bool unplugGroup(QMainWindowLayout *layout, QLayoutItem **item,
}
#endif
#if QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget)
static QTabBar::Shape tabwidgetPositionToTabBarShape(QWidget *w)
{
QTabBar::Shape result = QTabBar::RoundedSouth;
if (qobject_cast<QDockWidget *>(w)) {
switch (static_cast<QDockWidgetPrivate *>(qt_widget_private(w))->tabPosition) {
case QTabWidget::North:
result = QTabBar::RoundedNorth;
break;
case QTabWidget::South:
result = QTabBar::RoundedSouth;
break;
case QTabWidget::West:
result = QTabBar::RoundedWest;
break;
case QTabWidget::East:
result = QTabBar::RoundedEast;
break;
}
}
return result;
}
#endif // QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget)
/*! \internal
Unplug \a widget (QDockWidget or QToolBar) from it's parent container.
@ -2507,22 +2579,87 @@ QLayoutItem *QMainWindowLayout::unplug(QWidget *widget, bool group)
QList<int> groupWindowPath = info->indexOf(widget->parentWidget());
return groupWindowPath.isEmpty() ? nullptr : info->item(groupWindowPath).widgetItem;
}
qCDebug(lcQpaDockWidgets) << "Drag only:" << widget << "Group:" << group;
return nullptr;
}
QList<int> 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<QDockWidget *>(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<QDockWidget *>(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<int> path = layoutState.indexOf(floatingTabs);
QRect r = layoutState.itemRect(path);
savedState = layoutState;
savedState.fitLayout();
// Update gap, fix orientation, raise and show
currentGapPos = path;
currentGapRect = r;
updateGapIndicator();
fixToolBarOrientation(parentItem.widgetItem, currentGapPos.at(1));
floatingTabs->show();
floatingTabs->raise();
qCDebug(lcQpaDockWidgets) << "Unplugged from floating dock:" << widget << "from" << parentItem.widgetItem;
return parentItem.widgetItem;
}
}
#endif
@ -2595,51 +2732,37 @@ void QMainWindowLayout::updateGapIndicator()
gapIndicator->setParent(expectedParent);
}
// Prevent re-entry in case of size change
const bool sigBlockState = gapIndicator->signalsBlocked();
auto resetSignals = qScopeGuard([this, sigBlockState](){ gapIndicator->blockSignals(sigBlockState); });
gapIndicator->blockSignals(true);
#if QT_CONFIG(dockwidget)
if (currentHoveredFloat)
gapIndicator->setGeometry(currentHoveredFloat->currentGapRect);
else
#endif
gapIndicator->setGeometry(currentGapRect);
gapIndicator->show();
gapIndicator->raise();
// Reset signal state
} else if (gapIndicator) {
gapIndicator->hide();
}
#endif // QT_CONFIG(rubberband)
}
#if QT_CONFIG(dockwidget) && QT_CONFIG(tabwidget)
static QTabBar::Shape tabwidgetPositionToTabBarShape(QWidget *w)
{
QTabBar::Shape result = QTabBar::RoundedSouth;
if (qobject_cast<QDockWidget *>(w)) {
switch (static_cast<QDockWidgetPrivate *>(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<QDockWidget*>(widget)
@ -2652,12 +2775,20 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos)
QWidget *w = qobject_cast<QWidget*>(c);
if (!w)
continue;
// Handle only dock widgets and group windows
if (!qobject_cast<QDockWidget*>(w) && !qobject_cast<QDockWidgetGroupWindow *>(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<QDockWidgetGroupWindow *>(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<QDockWidget*>(c)) {
@ -2667,6 +2798,7 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos)
}
}
}
for (QWidget *w : candidates) {
const QScreen *screen1 = qt_widget_private(widget)->associatedScreen();
const QScreen *screen2 = qt_widget_private(w)->associatedScreen();
@ -2677,30 +2809,41 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos)
#if QT_CONFIG(tabwidget)
if (auto dropTo = qobject_cast<QDockWidget *>(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<QMainWindow *>(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<QDockWidgetGroupWindow *>(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<QMainWindow *>(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<QDockWidgetGroupWindow *>(w));
auto group = static_cast<QDockWidgetGroupWindow *>(w);
if (group->hover(widgetItem, group->mapFromGlobal(mousePos))) {
auto group = qobject_cast<QDockWidgetGroupWindow *>(w);
Q_ASSERT(group);
if (group->hover(hoverTarget, group->mapFromGlobal(mousePos))) {
setCurrentHoveredFloat(group);
applyState(layoutState); // update the tabbars
}
@ -2722,21 +2865,7 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos)
bool allowed = false;
#if QT_CONFIG(dockwidget)
if (QDockWidget *dw = qobject_cast<QDockWidget*>(widget))
allowed = dw->isAreaAllowed(toDockWidgetArea(path.at(1)));
// Read permissions from a DockWidgetGroupWindow depending on its DockWidget children
if (QDockWidgetGroupWindow* dwgw = qobject_cast<QDockWidgetGroupWindow *>(widget)) {
const QList<QDockWidget*> children = dwgw->findChildren<QDockWidget*>(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<QToolBar*>(widget))
@ -2752,16 +2881,16 @@ void QMainWindowLayout::hover(QLayoutItem *widgetItem, const QPoint &mousePos)
currentGapPos = path;
if (path.isEmpty()) {
fixToolBarOrientation(widgetItem, 2); // 2 = top dock, ie. horizontal
fixToolBarOrientation(hoverTarget, 2); // 2 = top dock, ie. horizontal
restore(true);
return;
}
fixToolBarOrientation(widgetItem, currentGapPos.at(1));
fixToolBarOrientation(hoverTarget, currentGapPos.at(1));
QMainWindowLayoutState newState = savedState;
if (!newState.insertGap(path, widgetItem)) {
if (!newState.insertGap(path, hoverTarget)) {
restore(true); // not enough space
return;
}

View File

@ -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 <QtCore/qloggingcategory.h>
QT_REQUIRE_CONFIG(mainwindow);
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(lcQpaDockWidgets);
class QToolBar;
class QRubberBand;
@ -334,7 +338,7 @@ bool QMainWindowLayoutSeparatorHelper<Layout>::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<QDockWidget *>())
return dw->minimumSize();
return lay()->minimumSize();
}
QSize maximumSize() const override
{
auto dw = widget()->findChild<QDockWidget *>();
if (dw)
return dw->maximumSize();
return lay()->maximumSize();
}
QSize sizeHint() const override
{
auto dw = widget()->findChild<QDockWidget *>();
if (dw)
return dw->sizeHint();
return lay()->sizeHint();
}
QWidget* widget() const override { return wid; }
private:
QLayout *lay() const { return const_cast<QDockWidgetGroupWindowItem *>(this)->widget()->layout(); }
@ -389,7 +414,7 @@ private:
widgets.
*/
class QMainWindowLayoutState
class Q_AUTOTEST_EXPORT QMainWindowLayoutState
{
public:
QRect rect;
@ -460,22 +485,19 @@ public:
QMainWindow::DockOptions dockOptions;
void setDockOptions(QMainWindow::DockOptions opts);
// status bar
QLayoutItem *statusbar;
// status bar
#if QT_CONFIG(statusbar)
QStatusBar *statusBar() const;
void setStatusBar(QStatusBar *sb);
#endif
// central widget
QWidget *centralWidget() const;
void setCentralWidget(QWidget *cw);
// toolbars
#if QT_CONFIG(toolbar)
void addToolBarBreak(Qt::ToolBarArea area);
void insertToolBarBreak(QToolBar *before);
@ -492,10 +514,11 @@ public:
#endif
// dock widgets
#if QT_CONFIG(dockwidget)
void setCorner(Qt::Corner corner, Qt::DockWidgetArea area);
Qt::DockWidgetArea corner(Qt::Corner corner) const;
enum DockWidgetAreaSize {Visible, Maximum};
QRect dockWidgetAreaRect(Qt::DockWidgetArea area, DockWidgetAreaSize size = Maximum) const;
void addDockWidget(Qt::DockWidgetArea area,
QDockWidget *dockwidget,
Qt::Orientation orientation);
@ -542,7 +565,6 @@ public:
#endif // QT_CONFIG(dockwidget)
// save/restore
enum VersionMarkers { // sentinel values used to validate state data
VersionMarker = 0xff
};
@ -551,7 +573,6 @@ public:
QBasicTimer discardRestoredStateTimer;
// QLayout interface
void addItem(QLayoutItem *item) override;
void setGeometry(const QRect &r) override;
QLayoutItem *itemAt(int index) const override;
@ -565,7 +586,6 @@ public:
void invalidate() override;
// animations
QWidgetAnimator widgetAnimator;
QList<int> currentGapPos;
QRect currentGapRect;
@ -579,7 +599,7 @@ public:
#endif
bool isInApplyState = false;
void hover(QLayoutItem *widgetItem, const QPoint &mousePos);
void hover(QLayoutItem *hoverTarget, const QPoint &mousePos);
bool plug(QLayoutItem *widgetItem);
QLayoutItem *unplug(QWidget *widget, bool group = false);
void revert(QLayoutItem *widgetItem);

View File

@ -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

View File

@ -8,6 +8,7 @@ qt_internal_add_test(tst_qdockwidget
SOURCES
tst_qdockwidget.cpp
PUBLIC_LIBRARIES
Qt::Core
Qt::CorePrivate
Qt::Gui
Qt::GuiPrivate

View File

@ -28,15 +28,24 @@
#include <QTest>
#include <QSignalSpy>
#include <qaction.h>
#include <qdockwidget.h>
#include <qmainwindow.h>
#include "private/qdockwidget_p.h"
#include "private/qmainwindowlayout_p.h"
#include <QAbstractButton>
#include <qlineedit.h>
#include <qtabbar.h>
#include <QScreen>
#include <QTimer>
#include <QtGui/QPainter>
#include "private/qdockwidget_p.h"
#include <QLabel>
#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<QWidget> &cent, QPointer<QDockWidget> &d1, QPointer<QDockWidget> &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<QMainWindowLayout*>(mw->layout())->dockWidgetAreaRect(area, QMainWindowLayout::Maximum).center()); }
bool checkFloatingTabs(QMainWindow* MainWindow, QPointer<QDockWidgetGroupWindow> &ftabs, const QList<QDockWidget*> &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<const Qt::DockWidgetAreas *>(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<const Qt::DockWidgetAreas *>(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<const Qt::DockWidgetAreas *>(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<const Qt::DockWidgetAreas *>(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<const Qt::DockWidgetAreas *>(spy.at(0).value(0).constData()),
dw.allowedAreas());
@ -1046,5 +1142,487 @@ void tst_QDockWidget::setWindowTitle()
QCOMPARE(dock2.windowTitle(), dock2Title);
}
// helpers for dockPermissions, hideAndShow, closeAndDelete
#ifdef QT_BUILD_INTERNAL
void tst_QDockWidget::createTestWidgets(QMainWindow* &mainWindow, QPointer<QWidget> &cent, QPointer<QDockWidget> &d1, QPointer<QDockWidget> &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<QDockWidgetLayout *>(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<QMainWindowLayout*>(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<QDockWidgetGroupWindow> &ftabs, const QList<QDockWidget*> &dwList) const
{
Q_ASSERT(mainWindow);
// Check if mainWindow has a floatingTab child
ftabs = mainWindow->findChild<QDockWidgetGroupWindow*>();
if (ftabs.isNull()) {
qCDebug(lcQpaDockWidgets) << "MainWindow has no DockWidgetGroupWindow" << mainWindow;
return false;
}
QTabBar* tab = ftabs->findChild<QTabBar*>();
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<QDockWidget*> children = ftabs->findChildren<QDockWidget*>(QString(), Qt::FindDirectChildrenOnly);
if (dwList.count() > 0)
{
if (dwList.count() != children.count()) {
qCDebug(lcQpaDockWidgets) << "Expected DockWidgetGroupWindow children:" << dwList.count()
<< "Children found:" << children.count();
qCDebug(lcQpaDockWidgets) << "Expected:" << dwList;
qCDebug(lcQpaDockWidgets) << "Found in" << ftabs << ":" << children.count();
return false;
}
for (const QDockWidget* child : dwList) {
if (!children.contains(child)) {
qCDebug(lcQpaDockWidgets) << "Expected child" << child << "not found in" << children;
return false;
}
}
}
// Always select first tab position
qCDebug(lcQpaDockWidgets) << "click on first tab";
QTest::mouseClick(tab, Qt::LeftButton, Qt::KeyboardModifiers(), tab->tabRect(0).center());
return true;
}
// detect xcb error
// qt.qpa.xcb: internal error: void QXcbWindow::setNetWmStateOnUnmappedWindow() called on mapped window
void tst_QDockWidget::xcbMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
Q_ASSERT(oldMessageHandler);
if (type == QtWarningMsg && QString(context.category) == "qt.qpa.xcb" && msg.contains("internal error")) {
Q_ASSERT(qThis);
qThis->xcbError = true;
}
return oldMessageHandler(type, context, msg);
}
#endif // QT_BUILD_INTERNAL
// test floating tabs and item_tree consistency
void tst_QDockWidget::floatingTabs()
{
#ifdef Q_OS_WIN
QSKIP("Test skipped on Windows platforms");
#endif // Q_OS_WIN
#ifdef QT_BUILD_INTERNAL
// Create a mainwindow with a central widget and two dock widgets
QPointer<QDockWidget> d1;
QPointer<QDockWidget> d2;
QPointer<QWidget> cent;
QMainWindow* mainWindow;
createTestWidgets(mainWindow, cent, d1, d2);
std::unique_ptr<QMainWindow> 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<QMainWindowLayout*>(mainWindow->layout());
const QList<int> path1 = layout->layoutState.indexOf(d1);
const QList<int> 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<QDockWidgetGroupWindow> ftabs;
QTRY_VERIFY(checkFloatingTabs(mainWindow, ftabs, QList<QDockWidget*>() << 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<QAbstractButton*>("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<QDockWidgetGroupWindow*>());
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<int>() << path2 << 0);
#else
QSKIP("test requires -developer-build option");
#endif // QT_BUILD_INTERNAL
}
// test hide & show
void tst_QDockWidget::hideAndShow()
{
#ifdef QT_BUILD_INTERNAL
// Skip test if xcb error is launched
qThis = this;
oldMessageHandler = qInstallMessageHandler(xcbMessageHandler);
auto resetMessageHandler = qScopeGuard([] { qInstallMessageHandler(oldMessageHandler); });
// Create a mainwindow with a central widget and two dock widgets
QPointer<QDockWidget> d1;
QPointer<QDockWidget> d2;
QPointer<QWidget> cent;
QMainWindow* mainWindow;
createTestWidgets(mainWindow, cent, d1, d2);
std::unique_ptr<QMainWindow> up_mainWindow(mainWindow);
// Check hiding of docked widgets
qCDebug(lcQpaDockWidgets) << "Hiding mainWindow with plugged dock widgets" << mainWindow;
mainWindow->hide();
QXCBVERIFY(!mainWindow->isVisible());
QXCBVERIFY(!d1->isVisible());
QXCBVERIFY(!d2->isVisible());
// Check showing everything again
qCDebug(lcQpaDockWidgets) << "Showing mainWindow with plugged dock widgets" << mainWindow;
mainWindow->show();
QXCBVERIFY(QTest::qWaitForWindowActive(mainWindow));
QXCBVERIFY(QTest::qWaitForWindowExposed(mainWindow));
QXCBVERIFY(mainWindow->isVisible());
QXCBVERIFY(QTest::qWaitForWindowActive(d1));
QXCBVERIFY(d1->isVisible());
QXCBVERIFY(QTest::qWaitForWindowActive(d2));
QXCBVERIFY(d2->isVisible());
// in case of XCB errors, unplugAndResize will block and cause the test to time out.
// => force skip
QTest::qWait(waitingTime);
if (xcbError)
QSKIP("Test skipped due to XCB error");
// unplug and resize both dock widgets
unplugAndResize(mainWindow, d1, home1(mainWindow), size1(mainWindow));
unplugAndResize(mainWindow, d2, home2(mainWindow), size2(mainWindow));
// Check hiding of undocked widgets
qCDebug(lcQpaDockWidgets) << "Hiding mainWindow with unplugged dock widgets" << mainWindow;
mainWindow->hide();
QTRY_VERIFY(!mainWindow->isVisible());
QTRY_VERIFY(d1->isVisible());
QTRY_VERIFY(d2->isVisible());
d1->hide();
d2->hide();
QTRY_VERIFY(!d1->isVisible());
QTRY_VERIFY(!d2->isVisible());
qCDebug(lcQpaDockWidgets) << "Waiting" << waitBeforeClose << "ms before closing.";
QTest::qWait(waitBeforeClose);
#else
QSKIP("test requires -developer-build option");
#endif // QT_BUILD_INTERNAL
}
// test closing and deleting consistency
void tst_QDockWidget::closeAndDelete()
{
#ifdef QT_BUILD_INTERNAL
// Create a mainwindow with a central widget and two dock widgets
QPointer<QDockWidget> d1;
QPointer<QDockWidget> d2;
QPointer<QWidget> cent;
QMainWindow* mainWindow;
createTestWidgets(mainWindow, cent, d1, d2);
std::unique_ptr<QMainWindow> up_mainWindow(mainWindow);
// unplug and resize both dock widgets
unplugAndResize(mainWindow, d1, home1(mainWindow), size1(mainWindow));
unplugAndResize(mainWindow, d2, home2(mainWindow), size2(mainWindow));
// Create a floating tab and unplug it again
qCDebug(lcQpaDockWidgets) << "Move d1 over d2";
moveDockWidget(d1, d2->mapToGlobal(d2->rect().center()));
// Both dock widgets must no longer be floating
// disabled due to flakiness on macOS and Windows
//QTRY_VERIFY(!d1->isFloating());
//QTRY_VERIFY(!d2->isFloating());
if (d1->isFloating())
qWarning("OS flakiness: D1 is docked and reports being floating");
if (d2->isFloating())
qWarning("OS flakiness: D2 is docked and reports being floating");
// Close everything with a single shot. Expected behavior: Event loop stops
bool eventLoopStopped = true;
QTimer::singleShot(0, this, [mainWindow, d1, d2] {
mainWindow->close();
QTRY_VERIFY(!mainWindow->isVisible());
QTRY_VERIFY(d1->isVisible());
QTRY_VERIFY(d2->isVisible());
d1->close();
d2->close();
QTRY_VERIFY(!d1->isVisible());
QTRY_VERIFY(!d2->isVisible());
});
// Fallback timer to report event loop still running
QTimer::singleShot(100, this, [&eventLoopStopped] {
qCDebug(lcQpaDockWidgets) << "Last dock widget hasn't shout down event loop!";
eventLoopStopped = false;
QApplication::quit();
});
QApplication::exec();
QTRY_VERIFY(eventLoopStopped);
// Check heap cleanup
qCDebug(lcQpaDockWidgets) << "Deleting mainWindow";
up_mainWindow.reset();
QTRY_VERIFY(d1.isNull());
QTRY_VERIFY(d2.isNull());
QTRY_VERIFY(cent.isNull());
#else
QSKIP("test requires -developer-build option");
#endif // QT_BUILD_INTERNAL
}
// Test dock area permissions
void tst_QDockWidget::dockPermissions()
{
#ifdef Q_OS_WIN
QSKIP("Test skipped on Windows platforms");
#endif // Q_OS_WIN
#ifdef QT_BUILD_INTERNAL
// Create a mainwindow with a central widget and two dock widgets
QPointer<QDockWidget> d1;
QPointer<QDockWidget> d2;
QPointer<QWidget> cent;
QMainWindow* mainWindow;
createTestWidgets(mainWindow, cent, d1, d2);
std::unique_ptr<QMainWindow> 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<QDockWidget*> children = mainWindow->findChildren<QDockWidget*>(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<QDockWidgetGroupWindow*>() == 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"