From 3ac5b75fc206ade7c666ed4f9b3ae2df23cc2485 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 14 Jul 2026 08:51:07 +0300 Subject: [PATCH 1/2] top toolbar layout fix Replaced reactive geometryChange handling (pendingWidth, forced sync resize, deferred QTimer fixup) with one idempotent pass in updatePolish(), extracted it into TopLevelToolBarsLayout. --- framework/dockwindow_v2/CMakeLists.txt | 4 + framework/dockwindow_v2/internal/dockbase.h | 1 + .../qml/Muse/Dock/CMakeLists.txt | 2 + .../qml/Muse/Dock/dockframemodel.cpp | 2 +- .../qml/Muse/Dock/dockwindow.cpp | 285 +--------- .../dockwindow_v2/qml/Muse/Dock/dockwindow.h | 19 +- .../qml/Muse/Dock/topleveltoolbarslayout.cpp | 290 ++++++++++ .../qml/Muse/Dock/topleveltoolbarslayout.h | 64 +++ framework/dockwindow_v2/tests/CMakeLists.txt | 40 ++ .../tests/topleveltoolbarslayout_tests.cpp | 530 ++++++++++++++++++ 10 files changed, 949 insertions(+), 288 deletions(-) create mode 100644 framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.cpp create mode 100644 framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.h create mode 100644 framework/dockwindow_v2/tests/CMakeLists.txt create mode 100644 framework/dockwindow_v2/tests/topleveltoolbarslayout_tests.cpp diff --git a/framework/dockwindow_v2/CMakeLists.txt b/framework/dockwindow_v2/CMakeLists.txt index cfe8cb2d61..c9d5d16edd 100644 --- a/framework/dockwindow_v2/CMakeLists.txt +++ b/framework/dockwindow_v2/CMakeLists.txt @@ -61,4 +61,8 @@ target_include_directories(muse_dockwindow SYSTEM PRIVATE if (MUSE_MODULE_DOCKWINDOW_QML) add_subdirectory(qml/Muse/Dock) + + if (MUSE_MODULE_DOCKWINDOW_TESTS) + add_subdirectory(tests) + endif() endif() diff --git a/framework/dockwindow_v2/internal/dockbase.h b/framework/dockwindow_v2/internal/dockbase.h index c26d775bf3..fe2dec4d25 100644 --- a/framework/dockwindow_v2/internal/dockbase.h +++ b/framework/dockwindow_v2/internal/dockbase.h @@ -189,6 +189,7 @@ public slots: protected: friend class DockWindow; friend class DropController; + friend class TopLevelToolBarsLayout; void componentComplete() override; diff --git a/framework/dockwindow_v2/qml/Muse/Dock/CMakeLists.txt b/framework/dockwindow_v2/qml/Muse/Dock/CMakeLists.txt index 03c25e0c57..2bb44a3971 100644 --- a/framework/dockwindow_v2/qml/Muse/Dock/CMakeLists.txt +++ b/framework/dockwindow_v2/qml/Muse/Dock/CMakeLists.txt @@ -24,6 +24,8 @@ qt_add_qml_module(muse_dockwindow_qml dockwindow.h foreign.cpp foreign.h + topleveltoolbarslayout.cpp + topleveltoolbarslayout.h QML_FILES DockFloatingWindow.qml DockFrame.qml diff --git a/framework/dockwindow_v2/qml/Muse/Dock/dockframemodel.cpp b/framework/dockwindow_v2/qml/Muse/Dock/dockframemodel.cpp index 2dda680f50..b48c2453fa 100644 --- a/framework/dockwindow_v2/qml/Muse/Dock/dockframemodel.cpp +++ b/framework/dockwindow_v2/qml/Muse/Dock/dockframemodel.cpp @@ -23,7 +23,7 @@ #include "dockframemodel.h" #include -#include +#include #include "kddockwidgets/src/core/Group.h" #include "kddockwidgets/src/qtquick/views/Group.h" diff --git a/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.cpp b/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.cpp index d2f54f7b6f..bf443d3433 100644 --- a/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.cpp +++ b/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.cpp @@ -22,10 +22,7 @@ #include "dockwindow.h" -#include - #include "kddockwidgets/src/LayoutSaver.h" -#include "kddockwidgets/src/Config.h" #include "kddockwidgets/src/core/DockRegistry.h" #include "kddockwidgets/src/core/Layout.h" #include "kddockwidgets/src/core/MainWindow.h" @@ -44,6 +41,7 @@ #include "docktoolbarview.h" #include "dockingholderview.h" #include "dockwindow.h" +#include "topleveltoolbarslayout.h" #include "muse_framework_config.h" @@ -53,9 +51,6 @@ using namespace muse::dock; using namespace muse::async; namespace muse::dock { -//! NOTE: Toolbar's maximum lenght, see DockToolBar.qml -static constexpr int UNCONSTRAINED_TOOLBAR_WIDTH = 16777215; - static const QList POSSIBLE_LOCATIONS { Location::Left, Location::Right, @@ -103,22 +98,6 @@ static void clearRegistry(int ctx) } } -class DockWindow::UniqueConnectionHolder : public QObject -{ - Q_OBJECT -public: - UniqueConnectionHolder(DockPageView* page, DockWindow* parent) - : QObject(parent), m_page(page) {} - - void alignTopLevelToolBars() - { - static_cast(parent())->alignTopLevelToolBars(m_page); - } - -private: - DockPageView* m_page = nullptr; -}; - DockWindow::DockWindow(QQuickItem* parent) : QQuickItem(parent), muse::Contextable(muse::iocCtxForQmlObject(this)), m_toolBars(this), @@ -129,12 +108,6 @@ DockWindow::DockWindow(QQuickItem* parent) DockWindow::~DockWindow() { dockWindowProvider()->deinit(); - - // Without this, the connections would be deleted by the QObject destructor, - // because they are child objects of this. But since they use DockWindow- - // specific code (rather than QObject-specific), we need to delete them - // before the end of the DockWindow destructor. - qDeleteAll(m_pageConnections); } void DockWindow::componentComplete() @@ -149,91 +122,28 @@ void DockWindow::componentComplete() KDDockWidgets::MainWindowOption_None, this); + m_topLevelToolBarsLayout = new TopLevelToolBarsLayout(m_mainWindow, this); + connect(qApp, &QCoreApplication::aboutToQuit, this, &DockWindow::onQuit); connect(this, &QQuickItem::windowChanged, this, &DockWindow::windowPropertyChanged); } void DockWindow::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { - const bool widthChanged = m_currentPage && !qFuzzyCompare(newGeometry.width(), oldGeometry.width()); - const bool shrinking = widthChanged && newGeometry.width() < oldGeometry.width(); - - if (widthChanged) { - m_pendingWidth = int(newGeometry.width()); - - if (shrinking) { - //! NOTE: Unpin the paddings first so compact mode can actually shrink the toolbars' content - for (DockToolBarView* toolBar : topLevelToolBars(m_currentPage)) { - toolBar->setMinimumWidth(toolBar->contentWidth()); - } - } - - adjustContentForAvailableSpace(m_currentPage); - alignTopLevelToolBars(m_currentPage); - - //! NOTE: Apply the new size to the layout synchronously - applyLayoutSizeToFitWindow(); - - m_pendingWidth = -1; - - //! NOTE: The synchronous resize above can be partial when KDDockWidgets' async item-size sync - //! hasn't settled yet. So let's shedule another layout - scheduleDeferredLayoutFix(); - } - QQuickItem::geometryChange(newGeometry, oldGeometry); -} - -void DockWindow::applyLayoutSizeToFitWindow() -{ - KDDockWidgets::Core::MainWindow* mw = m_mainWindow ? m_mainWindow->mainWindow() : nullptr; - KDDockWidgets::Core::Layout* layout = mw ? mw->layout() : nullptr; - if (!layout) { - return; - } - - const int targetWidth = m_pendingWidth >= 0 ? m_pendingWidth : int(width()); - const int layoutMin = layout->layoutMinimumSize().width(); - - const int applyWidth = std::max(targetWidth, layoutMin); - if (applyWidth != layout->layoutSize().width()) { - layout->setLayoutSize(QSize(applyWidth, layout->layoutSize().height())); + if (m_currentPage && !qFuzzyCompare(newGeometry.width(), oldGeometry.width())) { + polish(); } } -void DockWindow::scheduleDeferredLayoutFix() +void DockWindow::updatePolish() { - if (m_layoutFixScheduled) { - return; - } + QQuickItem::updatePolish(); - m_layoutFixScheduled = true; - - QTimer::singleShot(0, this, [this]() { - runDeferredLayoutFix(); - }); -} - -void DockWindow::runDeferredLayoutFix() -{ - m_layoutFixScheduled = false; - - if (!m_currentPage) { - return; - } - - adjustContentForAvailableSpace(m_currentPage); - alignTopLevelToolBars(m_currentPage); - - //! NOTE: Force the layouting items to pick up the current dock minimums - for (DockBase* dock : m_currentPage->allDocks()) { - if (dock) { - dock->syncLayoutItemMinSize(); - } + if (m_topLevelToolBarsLayout) { + m_topLevelToolBarsLayout->relayout(); } - - applyLayoutSizeToFitWindow(); } void DockWindow::onQuit() @@ -492,95 +402,6 @@ void DockWindow::loadPanels(const DockPageView* page) } } -void DockWindow::alignTopLevelToolBars(const DockPageView* page) -{ - const QList topToolBars = topLevelToolBars(page); - - //! NOTE: Reset paddings/pins first, so that widths from a previous - //! configuration do not accumulate - for (DockToolBarView* toolBar : topToolBars) { - toolBar->setMaximumWidth(UNCONSTRAINED_TOOLBAR_WIDTH); - toolBar->setMinimumWidth(toolBar->contentWidth()); - } - - DockToolBarView* lastLeftToolBar = nullptr; - DockToolBarView* lastCentralToolBar = nullptr; - - int leftBlockWidth = 0; - int centralBlockWidth = 0; - int rightBlockWidth = 0; - - int leftCount = 0; - int centralCount = 0; - int rightCount = 0; - - const int separator = KDDockWidgets::Config::self(iocContext()->id).separatorThickness(); - - for (DockToolBarView* toolBar : topToolBars) { - if (toolBar->floating() || !toolBar->isVisible()) { - continue; - } - - switch (static_cast(toolBar->alignment())) { - case DockToolBarAlignment::Left: - lastLeftToolBar = toolBar; - leftBlockWidth += toolBar->contentWidth(); - ++leftCount; - break; - case DockToolBarAlignment::Center: - lastCentralToolBar = toolBar; - centralBlockWidth += toolBar->contentWidth(); - ++centralCount; - break; - case DockToolBarAlignment::Right: - rightBlockWidth += toolBar->contentWidth(); - ++rightCount; - break; - } - } - - leftBlockWidth += separator * std::max(0, leftCount - 1); - centralBlockWidth += separator * std::max(0, centralCount - 1); - rightBlockWidth += separator * std::max(0, rightCount - 1); - - const int separatorLeftCentral = (leftCount > 0 && centralCount > 0) ? separator : 0; - const int separatorCentralRight = (centralCount > 0 && rightCount > 0) ? separator : 0; - const int separatorLeftRight = (leftCount > 0 && centralCount == 0 && rightCount > 0) ? separator : 0; - - const int occupied = leftBlockWidth + centralBlockWidth + rightBlockWidth - + separatorLeftCentral + separatorCentralRight + separatorLeftRight; - - //! NOTE: During a resize the new width isn't propagated to width yet - const int availableWidth = m_pendingWidth >= 0 ? m_pendingWidth : int(width()); - - const int freeSpace = availableWidth - occupied; - if (freeSpace <= 0) { - return; - } - - DockToolBarView* grower = lastLeftToolBar ? lastLeftToolBar : lastCentralToolBar; - - int paddingForLastCentral = 0; - if (lastCentralToolBar && lastCentralToolBar != grower) { - paddingForLastCentral = (availableWidth - centralBlockWidth) / 2 - rightBlockWidth - separatorCentralRight; - paddingForLastCentral = std::min(std::max(paddingForLastCentral, 0), freeSpace); - } - - for (DockToolBarView* toolBar : topToolBars) { - if (toolBar->floating() || !toolBar->isVisible() || toolBar == grower) { - continue; - } - - const int pinnedWidth = (toolBar == lastCentralToolBar) - ? toolBar->contentWidth() + paddingForLastCentral - : toolBar->contentWidth(); - - //! NOTE: max before min - toolBar->setMaximumWidth(pinnedWidth); - toolBar->setMinimumWidth(pinnedWidth); - } -} - void DockWindow::addDock(DockBase* dock, Location location, const DockBase* relativeTo) { TRACEFUNC; @@ -850,7 +671,7 @@ void DockWindow::initDocks(DockPageView* page) TRACEFUNC; //! before init we should correct toolbars sizes - adjustContentForAvailableSpace(page); + m_topLevelToolBarsLayout->adjustContentForAvailableSpace(page); for (DockToolBarView* toolbar : m_toolBars.list()) { toolbar->setParentItem(this); @@ -862,88 +683,8 @@ void DockWindow::initDocks(DockPageView* page) page->init(); } - alignTopLevelToolBars(page); - - if (!m_pageConnections.contains(page)) { - m_pageConnections[page] = new UniqueConnectionHolder(page, this); - } - - UniqueConnectionHolder* holder = m_pageConnections[page]; - - for (DockToolBarView* toolbar : topLevelToolBars(page)) { - connect(toolbar, &DockToolBarView::floatingChanged, - this, &DockWindow::scheduleDeferredLayoutFix, Qt::UniqueConnection); - - connect(toolbar, &DockToolBarView::contentSizeChanged, - holder, &UniqueConnectionHolder::alignTopLevelToolBars, Qt::UniqueConnection); - - connect(toolbar, &DockToolBarView::visibleChanged, - holder, &UniqueConnectionHolder::alignTopLevelToolBars, Qt::UniqueConnection); - } -} - -void DockWindow::adjustContentForAvailableSpace(DockPageView* page) -{ - if (!page) { - return; - } - - int spaceWidth = m_pendingWidth >= 0 ? m_pendingWidth : int(width()); - - auto adjustDocks = [&spaceWidth](QList docks) { - int width = 0; - for (DockBase* dock : docks) { - width += dock->contentWidth(); - } - - docks.erase(std::remove_if(docks.begin(), docks.end(), [](const DockBase* dock){ - return dock->compactPriorityOrder() == -1; - }), docks.end()); - - if (docks.empty()) { - return; - } - - std::sort(docks.begin(), docks.end(), [](const DockBase* dock1, DockBase* dock2) { - return dock1->compactPriorityOrder() < dock2->compactPriorityOrder(); - }); - - if (width >= spaceWidth) { - for (DockBase* dock : docks) { - if (!dock->isCompact()) { - dock->setIsCompact(true); - - width -= dock->nonCompactWidth(); - width += dock->width(); - } - } - } else { - for (int i = docks.size() - 1; i >= 0; i--) { - DockBase* dock = docks.at(i); - if (!dock->isCompact()) { - continue; - } - - int actualWidth = dock->contentWidth(); - int nonCompactWidth = dock->nonCompactWidth(); - if (width - actualWidth + nonCompactWidth < spaceWidth) { - dock->setIsCompact(false); - } - - break; - } - } - }; - - QList topLevelToolBarsDocks; - - for (DockToolBarView* toolBar : topLevelToolBars(page)) { - if (!toolBar->dockWidget()->isFloating() && toolBar->isVisible()) { - topLevelToolBarsDocks << toolBar; - } - } - - adjustDocks(topLevelToolBarsDocks); + m_topLevelToolBarsLayout->setUpPageConnections(page); + m_topLevelToolBarsLayout->scheduleRelayout(); } void DockWindow::notifyAboutDocksOpenStatus() @@ -985,5 +726,3 @@ QList DockWindow::topLevelToolBars(const DockPageView* page) c return toolBars; } - -#include "dockwindow.moc" diff --git a/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.h b/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.h index 491e29d4d9..09ebddff56 100644 --- a/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.h +++ b/framework/dockwindow_v2/qml/Muse/Dock/dockwindow.h @@ -48,6 +48,7 @@ class DockToolBarView; class DockingHolderView; class DockPageView; class DockPanelView; +class TopLevelToolBarsLayout; class DockWindow : public QQuickItem, public IDockWindow, public muse::Contextable, public async::Asyncable { Q_OBJECT @@ -100,6 +101,8 @@ class DockWindow : public QQuickItem, public IDockWindow, public muse::Contextab void restoreDefaultLayout() override; + QList topLevelToolBars(const DockPageView* page) const; + signals: void pageLoaded(); void currentPageUriChanged(const QString& uri); @@ -115,12 +118,12 @@ private slots: void componentComplete() override; void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; void loadPageContent(const DockPageView* page); void loadToolBars(const DockPageView* page); void loadPanels(const DockPageView* page); void loadTopLevelToolBars(const DockPageView* page); - void alignTopLevelToolBars(const DockPageView* page); void addDock(DockBase* dock, Location location = Location::Left, const DockBase* relativeTo = nullptr); void addPanelAsTab(DockPanelView* panel, DockPanelView* destinationPanel); @@ -141,29 +144,17 @@ private slots: void initDocks(DockPageView* page); - void adjustContentForAvailableSpace(DockPageView* page); - - void applyLayoutSizeToFitWindow(); - void scheduleDeferredLayoutFix(); - void runDeferredLayoutFix(); - void notifyAboutDocksOpenStatus(); - QList topLevelToolBars(const DockPageView* page) const; - KDDockWidgets::QtQuick::MainWindow* m_mainWindow = nullptr; DockPageView* m_currentPage = nullptr; uicomponents::QmlListProperty m_toolBars; uicomponents::QmlListProperty m_pages; async::Channel m_docksOpenStatusChanged; - class UniqueConnectionHolder; - QHash m_pageConnections; + TopLevelToolBarsLayout* m_topLevelToolBarsLayout = nullptr; bool m_hasGeometryBeenRestored = false; bool m_reloadCurrentPageAllowed = false; - - bool m_layoutFixScheduled = false; - int m_pendingWidth = -1; }; } diff --git a/framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.cpp b/framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.cpp new file mode 100644 index 0000000000..837cc26764 --- /dev/null +++ b/framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.cpp @@ -0,0 +1,290 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "topleveltoolbarslayout.h" + +#include "kddockwidgets/src/Config.h" +#include "kddockwidgets/src/core/DockWidget.h" +#include "kddockwidgets/src/core/Layout.h" +#include "kddockwidgets/src/core/MainWindow.h" +#include "kddockwidgets/src/qtquick/views/MainWindow.h" + +#include "dockpageview.h" +#include "docktoolbarview.h" +#include "dockwindow.h" + +using namespace muse::dock; + +namespace muse::dock { +//! NOTE: Toolbar's maximum lenght, see DockToolBar.qml +static constexpr int UNCONSTRAINED_TOOLBAR_WIDTH = 16777215; +} + +TopLevelToolBarsLayout::TopLevelToolBarsLayout(KDDockWidgets::QtQuick::MainWindow* mainWindow, DockWindow* window) + : QObject(window), m_mainWindow(mainWindow), m_window(window) +{ +} + +void TopLevelToolBarsLayout::setUpPageConnections(const DockPageView* page) +{ + for (DockToolBarView* toolBar : topLevelToolBars(page)) { + connect(toolBar, &DockToolBarView::floatingChanged, + this, &TopLevelToolBarsLayout::scheduleRelayout, Qt::UniqueConnection); + + connect(toolBar, &DockToolBarView::contentSizeChanged, + this, &TopLevelToolBarsLayout::scheduleRelayout, Qt::UniqueConnection); + + connect(toolBar, &DockToolBarView::visibleChanged, + this, &TopLevelToolBarsLayout::scheduleRelayout, Qt::UniqueConnection); + } +} + +void TopLevelToolBarsLayout::scheduleRelayout() +{ + m_window->polish(); +} + +void TopLevelToolBarsLayout::relayout() +{ + DockPageView* page = currentPage(); + if (!page) { + return; + } + + const int width = availableWidth(); + + adjustContentForAvailableSpace(page); + alignToolBars(page, width); + + //! NOTE: Force the layouting items to pick up the current dock minimums + for (DockBase* dock : page->allDocks()) { + if (dock) { + dock->syncLayoutItemMinSize(); + } + } + + if (!applyLayoutSizeToFitWindow(width)) { + scheduleRelayout(); + } +} + +void TopLevelToolBarsLayout::adjustContentForAvailableSpace(const DockPageView* page) +{ + if (!page) { + return; + } + + const int spaceWidth = availableWidth(); + const int separator = separatorThickness(); + + auto adjustDocks = [spaceWidth, separator](QList docks) { + int width = separator * std::max(0, int(docks.size()) - 1); + for (DockBase* dock : docks) { + width += dock->contentWidth(); + } + + docks.erase(std::remove_if(docks.begin(), docks.end(), [](const DockBase* dock){ + return dock->compactPriorityOrder() == -1; + }), docks.end()); + + if (docks.empty()) { + return; + } + + std::sort(docks.begin(), docks.end(), [](const DockBase* dock1, DockBase* dock2) { + return dock1->compactPriorityOrder() < dock2->compactPriorityOrder(); + }); + + if (width > spaceWidth) { + for (DockBase* dock : docks) { + if (width <= spaceWidth) { + break; + } + + if (!dock->isCompact()) { + dock->setIsCompact(true); + + width -= dock->nonCompactWidth(); + width += dock->width(); + } + } + } else { + for (int i = docks.size() - 1; i >= 0; i--) { + DockBase* dock = docks.at(i); + if (!dock->isCompact()) { + continue; + } + + int actualWidth = dock->contentWidth(); + int nonCompactWidth = dock->nonCompactWidth(); + if (width - actualWidth + nonCompactWidth <= spaceWidth) { + dock->setIsCompact(false); + } + + break; + } + } + }; + + QList topLevelToolBarsDocks; + + for (DockToolBarView* toolBar : topLevelToolBars(page)) { + if (!isToolBarFloating(toolBar) && toolBar->isVisible()) { + topLevelToolBarsDocks << toolBar; + } + } + + adjustDocks(topLevelToolBarsDocks); +} + +void TopLevelToolBarsLayout::alignToolBars(const DockPageView* page, int availableWidth) +{ + const QList topToolBars = topLevelToolBars(page); + + //! NOTE: Reset paddings/pins first, so that widths from a previous + //! configuration do not accumulate + for (DockToolBarView* toolBar : topToolBars) { + toolBar->setMaximumWidth(UNCONSTRAINED_TOOLBAR_WIDTH); + toolBar->setMinimumWidth(toolBar->contentWidth()); + } + + DockToolBarView* lastLeftToolBar = nullptr; + DockToolBarView* lastCentralToolBar = nullptr; + + int leftBlockWidth = 0; + int centralBlockWidth = 0; + int rightBlockWidth = 0; + + int leftCount = 0; + int centralCount = 0; + int rightCount = 0; + + const int separator = separatorThickness(); + + for (DockToolBarView* toolBar : topToolBars) { + if (toolBar->floating() || !toolBar->isVisible()) { + continue; + } + + switch (static_cast(toolBar->alignment())) { + case DockToolBarAlignment::Left: + lastLeftToolBar = toolBar; + leftBlockWidth += toolBar->contentWidth(); + ++leftCount; + break; + case DockToolBarAlignment::Center: + lastCentralToolBar = toolBar; + centralBlockWidth += toolBar->contentWidth(); + ++centralCount; + break; + case DockToolBarAlignment::Right: + rightBlockWidth += toolBar->contentWidth(); + ++rightCount; + break; + } + } + + leftBlockWidth += separator * std::max(0, leftCount - 1); + centralBlockWidth += separator * std::max(0, centralCount - 1); + rightBlockWidth += separator * std::max(0, rightCount - 1); + + const int separatorLeftCentral = (leftCount > 0 && centralCount > 0) ? separator : 0; + const int separatorCentralRight = (centralCount > 0 && rightCount > 0) ? separator : 0; + const int separatorLeftRight = (leftCount > 0 && centralCount == 0 && rightCount > 0) ? separator : 0; + + const int occupied = leftBlockWidth + centralBlockWidth + rightBlockWidth + + separatorLeftCentral + separatorCentralRight + separatorLeftRight; + + const int freeSpace = availableWidth - occupied; + if (freeSpace <= 0) { + return; + } + + DockToolBarView* grower = lastLeftToolBar ? lastLeftToolBar : lastCentralToolBar; + if (!grower) { + //! NOTE: No toolbar to absorb the free space; leave all the toolbars + //! unpinned so the layout can distribute it + return; + } + + int paddingForLastCentral = 0; + if (lastCentralToolBar && lastCentralToolBar != grower) { + paddingForLastCentral = (availableWidth - centralBlockWidth) / 2 - rightBlockWidth - separatorCentralRight; + paddingForLastCentral = std::min(std::max(paddingForLastCentral, 0), freeSpace); + } + + for (DockToolBarView* toolBar : topToolBars) { + if (toolBar->floating() || !toolBar->isVisible() || toolBar == grower) { + continue; + } + + const int pinnedWidth = (toolBar == lastCentralToolBar) + ? toolBar->contentWidth() + paddingForLastCentral + : toolBar->contentWidth(); + + //! NOTE: max before min + toolBar->setMaximumWidth(pinnedWidth); + toolBar->setMinimumWidth(pinnedWidth); + } +} + +bool TopLevelToolBarsLayout::applyLayoutSizeToFitWindow(int targetWidth) +{ + KDDockWidgets::Core::MainWindow* mw = m_mainWindow ? m_mainWindow->mainWindow() : nullptr; + KDDockWidgets::Core::Layout* layout = mw ? mw->layout() : nullptr; + if (!layout) { + return true; + } + + const int applyWidth = std::max(targetWidth, layout->layoutMinimumSize().width()); + + if (applyWidth != layout->layoutSize().width()) { + layout->setLayoutSize(QSize(applyWidth, layout->layoutSize().height())); + } + + return layout->layoutSize().width() == applyWidth; +} + +DockPageView* TopLevelToolBarsLayout::currentPage() const +{ + return m_window->currentPage(); +} + +QList TopLevelToolBarsLayout::topLevelToolBars(const DockPageView* page) const +{ + return m_window->topLevelToolBars(page); +} + +int TopLevelToolBarsLayout::availableWidth() const +{ + return int(m_window->width()); +} + +int TopLevelToolBarsLayout::separatorThickness() const +{ + return KDDockWidgets::Config::self(m_window->iocContext()->id).separatorThickness(); +} + +bool TopLevelToolBarsLayout::isToolBarFloating(const DockToolBarView* toolBar) const +{ + return toolBar->dockWidget()->isFloating(); +} diff --git a/framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.h b/framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.h new file mode 100644 index 0000000000..e6f7389757 --- /dev/null +++ b/framework/dockwindow_v2/qml/Muse/Dock/topleveltoolbarslayout.h @@ -0,0 +1,64 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include + +namespace KDDockWidgets::QtQuick { +class MainWindow; +} + +namespace muse::dock { +class DockWindow; +class DockPageView; +class DockToolBarView; + +class TopLevelToolBarsLayout : public QObject +{ + Q_OBJECT + +public: + TopLevelToolBarsLayout(KDDockWidgets::QtQuick::MainWindow* mainWindow, DockWindow* window); + + void setUpPageConnections(const DockPageView* page); + + void adjustContentForAvailableSpace(const DockPageView* page); + void relayout(); + virtual void scheduleRelayout(); + +protected: + //! NOTE: Virtual for tests + virtual DockPageView* currentPage() const; + virtual QList topLevelToolBars(const DockPageView* page) const; + virtual int availableWidth() const; + virtual int separatorThickness() const; + virtual bool isToolBarFloating(const DockToolBarView* toolBar) const; + virtual bool applyLayoutSizeToFitWindow(int targetWidth); + +private: + void alignToolBars(const DockPageView* page, int availableWidth); + + KDDockWidgets::QtQuick::MainWindow* m_mainWindow = nullptr; + DockWindow* m_window = nullptr; +}; +} diff --git a/framework/dockwindow_v2/tests/CMakeLists.txt b/framework/dockwindow_v2/tests/CMakeLists.txt new file mode 100644 index 0000000000..fcaebbcaea --- /dev/null +++ b/framework/dockwindow_v2/tests/CMakeLists.txt @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-3.0-only +# MuseScore-CLA-applies +# +# MuseScore Studio +# Music Composition & Notation +# +# Copyright (C) 2026 MuseScore Limited and others +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(MODULE_TEST muse_dockwindow_tests) + +set(MODULE_TEST_SRC + ${CMAKE_CURRENT_LIST_DIR}/topleveltoolbarslayout_tests.cpp + ) + +set(MODULE_TEST_INCLUDE + ${CMAKE_CURRENT_LIST_DIR}/.. + ${CMAKE_CURRENT_LIST_DIR}/../qml/Muse/Dock + ) + +set(MODULE_TEST_LINK + muse_dockwindow + muse_dockwindow_qml + muse_ui_qml + muse_uicomponents_qml + Qt::Quick + ) + +include(SetupGTest) diff --git a/framework/dockwindow_v2/tests/topleveltoolbarslayout_tests.cpp b/framework/dockwindow_v2/tests/topleveltoolbarslayout_tests.cpp new file mode 100644 index 0000000000..95085459f0 --- /dev/null +++ b/framework/dockwindow_v2/tests/topleveltoolbarslayout_tests.cpp @@ -0,0 +1,530 @@ +/* + * SPDX-License-Identifier: GPL-3.0-only + * MuseScore-CLA-applies + * + * MuseScore Studio + * Music Composition & Notation + * + * Copyright (C) 2021 MuseScore Limited and others + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include + +#include + +#include "dockpageview.h" +#include "docktoolbarview.h" +#include "topleveltoolbarslayout.h" + +using namespace muse; +using namespace muse::dock; + +//! NOTE: Toolbar's maximum lenght, see DockToolBar.qml +static constexpr int UNCONSTRAINED_TOOLBAR_WIDTH = 16777215; + +class Dock_TopLevelToolBarsLayoutTests : public ::testing::Test +{ +public: + + class ToolBarView : public DockToolBarView + { + public: + void setFloatingState(bool floating) { doSetFloating(floating); } + }; + + class TestTopLevelToolBarsLayout : public TopLevelToolBarsLayout + { + public: + TestTopLevelToolBarsLayout() + : TopLevelToolBarsLayout(nullptr, nullptr) {} + + void scheduleRelayout() override + { + ++scheduledRelayoutCount; + } + + DockPageView* currentPage() const override { return page; } + QList topLevelToolBars(const DockPageView*) const override { return toolBars; } + int availableWidth() const override { return windowWidth; } + int separatorThickness() const override { return separator; } + bool isToolBarFloating(const DockToolBarView* toolBar) const override { return toolBar->floating(); } + + bool applyLayoutSizeToFitWindow(int targetWidth) override + { + appliedLayoutWidth = targetWidth; + return layoutSizeFitsWindow; + } + + QList toolBars; + DockPageView* page = nullptr; + int windowWidth = 0; + int separator = 0; + bool layoutSizeFitsWindow = true; + + int scheduledRelayoutCount = 0; + int appliedLayoutWidth = -1; + }; + + void SetUp() override + { + m_layout = std::make_shared(); + + m_page = new DockPageView(); + m_layout->page = m_page; + } + + void TearDown() override + { + qDeleteAll(m_layout->toolBars); + delete m_page; + } + + ToolBarView* addToolBar(int contentWidth, DockToolBarAlignment::Type alignment = DockToolBarAlignment::Left, + int compactPriorityOrder = -1) + { + ToolBarView* toolBar = new ToolBarView(); + toolBar->setContentWidth(contentWidth); + toolBar->setWidth(contentWidth); + toolBar->setAlignment(alignment); + toolBar->setCompactPriorityOrder(compactPriorityOrder); + + m_layout->toolBars << toolBar; + + return toolBar; + } + + void simulateCompactResize(ToolBarView* toolBar, int compactWidth) + { + QObject::connect(toolBar, &DockBase::isCompactChanged, toolBar, [toolBar, compactWidth]() { + if (toolBar->isCompact()) { + toolBar->setWidth(compactWidth); + } + }); + } + + void expectPinned(const DockToolBarView* toolBar, int width) + { + EXPECT_EQ(toolBar->minimumWidth(), width); + EXPECT_EQ(toolBar->maximumWidth(), width); + } + + void expectUnpinned(const DockToolBarView* toolBar) + { + EXPECT_EQ(toolBar->minimumWidth(), toolBar->contentWidth()); + EXPECT_EQ(toolBar->maximumWidth(), UNCONSTRAINED_TOOLBAR_WIDTH); + } + + std::shared_ptr m_layout; + DockPageView* m_page = nullptr; +}; + +TEST_F(Dock_TopLevelToolBarsLayoutTests, ScheduleRelayoutOnToolBarChanged) +{ + //! [GIVEN] A page with a toolbar + ToolBarView* toolBar = addToolBar(100); + + //! [WHEN] Connections are set up + m_layout->setUpPageConnections(m_page); + + //! [THEN] No relayout has been scheduled yet + EXPECT_EQ(m_layout->scheduledRelayoutCount, 0); + + //! [WHEN] The toolbar's content size changes + toolBar->setContentWidth(200); + + //! [THEN] A relayout is scheduled + EXPECT_EQ(m_layout->scheduledRelayoutCount, 1); + + //! [WHEN] The toolbar's floating state changes + toolBar->setFloatingState(true); + + //! [THEN] A relayout is scheduled + EXPECT_EQ(m_layout->scheduledRelayoutCount, 2); + + //! [WHEN] The toolbar's visibility changes + toolBar->setVisible(false); + + //! [THEN] A relayout is scheduled + EXPECT_EQ(m_layout->scheduledRelayoutCount, 3); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, PageConnectionsAreUnique) +{ + //! [GIVEN] A page with a toolbar + ToolBarView* toolBar = addToolBar(100); + + //! [WHEN] Connections are set up twice + m_layout->setUpPageConnections(m_page); + m_layout->setUpPageConnections(m_page); + + //! [WHEN] The toolbar's content size changes + toolBar->setContentWidth(200); + + //! [THEN] Only one relayout is scheduled + EXPECT_EQ(m_layout->scheduledRelayoutCount, 1); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, RelayoutDoesNothingWithoutPage) +{ + //! [GIVEN] No current page + m_layout->page = nullptr; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] The layout size has not been touched + EXPECT_EQ(m_layout->appliedLayoutWidth, -1); + EXPECT_EQ(m_layout->scheduledRelayoutCount, 0); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, RelayoutAppliesLayoutSizeToFitWindow) +{ + //! [GIVEN] A window + m_layout->windowWidth = 500; + + //! [WHEN] Relayout is requested and the layout can be resized to the window + m_layout->relayout(); + + //! [THEN] The layout size has been applied, no extra relayout is scheduled + EXPECT_EQ(m_layout->appliedLayoutWidth, 500); + EXPECT_EQ(m_layout->scheduledRelayoutCount, 0); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, RelayoutIsRescheduledIfLayoutSizeDoesNotFitWindow) +{ + //! [GIVEN] A window and a layout that cannot be resized to the window yet + m_layout->windowWidth = 500; + m_layout->layoutSizeFitsWindow = false; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] Another relayout is scheduled + EXPECT_EQ(m_layout->appliedLayoutWidth, 500); + EXPECT_EQ(m_layout->scheduledRelayoutCount, 1); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, CompactToolBarsThatDoNotFit) +{ + //! [GIVEN] Toolbars that do not fit into the window + ToolBarView* first = addToolBar(300, DockToolBarAlignment::Left, 0); + ToolBarView* second = addToolBar(300, DockToolBarAlignment::Left, 1); + ToolBarView* nonCompactable = addToolBar(300, DockToolBarAlignment::Left); + + //! [GIVEN] The toolbars shrink to 50 px when they become compact + simulateCompactResize(first, 50); + simulateCompactResize(second, 50); + + m_layout->windowWidth = 700; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] Only the toolbar with the highest compact priority became compact, + //! since that freed enough space: 900 - 300 + 50 = 650 <= 700 + EXPECT_TRUE(first->isCompact()); + EXPECT_FALSE(second->isCompact()); + EXPECT_FALSE(nonCompactable->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, NotCompactToolBarsThatFit) +{ + //! [GIVEN] Toolbars that fit into the window + ToolBarView* first = addToolBar(300, DockToolBarAlignment::Left, 0); + ToolBarView* second = addToolBar(300, DockToolBarAlignment::Left, 1); + + m_layout->windowWidth = 700; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] No toolbar became compact + EXPECT_FALSE(first->isCompact()); + EXPECT_FALSE(second->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, NotCompactToolBarsThatFitExactly) +{ + //! [GIVEN] Toolbars that exactly fit into the window + ToolBarView* first = addToolBar(300, DockToolBarAlignment::Left, 0); + ToolBarView* second = addToolBar(300, DockToolBarAlignment::Left, 1); + + m_layout->windowWidth = 600; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] No toolbar became compact + EXPECT_FALSE(first->isCompact()); + EXPECT_FALSE(second->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, SeparatorsAreCountedWhenCompacting) +{ + //! [GIVEN] Two toolbars whose contents fit into the window, + //! but not together with the separator between them + ToolBarView* first = addToolBar(300, DockToolBarAlignment::Left, 0); + ToolBarView* second = addToolBar(300, DockToolBarAlignment::Left, 1); + + m_layout->separator = 10; + m_layout->windowWidth = 605; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] The toolbars became compact + EXPECT_TRUE(first->isCompact()); + EXPECT_TRUE(second->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, FloatingAndHiddenToolBarsAreIgnoredWhenCompacting) +{ + //! [GIVEN] A floating and a hidden toolbar that would not fit into the window + ToolBarView* floating = addToolBar(500, DockToolBarAlignment::Left, 0); + floating->setFloatingState(true); + + ToolBarView* hidden = addToolBar(500, DockToolBarAlignment::Left, 1); + hidden->setVisible(false); + + //! [GIVEN] A docked toolbar that fits + ToolBarView* docked = addToolBar(300, DockToolBarAlignment::Left, 2); + + m_layout->windowWidth = 400; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] Only the docked toolbar is taken into account, nothing became compact + EXPECT_FALSE(floating->isCompact()); + EXPECT_FALSE(hidden->isCompact()); + EXPECT_FALSE(docked->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, ExpandCompactToolBarWhenItFitsAgain) +{ + //! [GIVEN] A compact toolbar with a non-compact width of 300 + ToolBarView* toolBar = addToolBar(300, DockToolBarAlignment::Left, 0); + toolBar->setIsCompact(true); + toolBar->setContentWidth(50); + + //! [GIVEN] The window has enough space for the non-compact width + m_layout->windowWidth = 700; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] The toolbar is no longer compact + EXPECT_FALSE(toolBar->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, ExpandCompactToolBarWhenItFitsExactly) +{ + //! [GIVEN] A compact toolbar whose non-compact width of 300 + //! exactly matches the window width + ToolBarView* toolBar = addToolBar(300, DockToolBarAlignment::Left, 0); + toolBar->setIsCompact(true); + toolBar->setContentWidth(50); + + m_layout->windowWidth = 300; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] The toolbar is no longer compact + EXPECT_FALSE(toolBar->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, KeepToolBarCompactWhenExpansionWouldNotFit) +{ + //! [GIVEN] A compact toolbar with a non-compact width of 300 + ToolBarView* toolBar = addToolBar(300, DockToolBarAlignment::Left, 0); + toolBar->setIsCompact(true); + toolBar->setContentWidth(50); + + //! [GIVEN] Another toolbar that occupies most of the window + addToolBar(300, DockToolBarAlignment::Left); + + //! [GIVEN] The window has no space for the non-compact width + m_layout->windowWidth = 500; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] The toolbar remains compact + EXPECT_TRUE(toolBar->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, ExpandOnlyLastCompactedToolBarPerPass) +{ + //! [GIVEN] Two compact toolbars + ToolBarView* first = addToolBar(300, DockToolBarAlignment::Left, 0); + first->setIsCompact(true); + first->setContentWidth(50); + + ToolBarView* second = addToolBar(300, DockToolBarAlignment::Left, 1); + second->setIsCompact(true); + second->setContentWidth(50); + + //! [GIVEN] The window has enough space for both non-compact widths + m_layout->windowWidth = 1000; + + //! [WHEN] The content is adjusted for the available space + m_layout->adjustContentForAvailableSpace(m_page); + + //! [THEN] Only the toolbar with the highest compact priority order is expanded; + //! the other one will be expanded on a next pass + EXPECT_TRUE(first->isCompact()); + EXPECT_FALSE(second->isCompact()); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, LastLeftToolBarAbsorbsFreeSpace) +{ + //! [GIVEN] Two left toolbars and a right toolbar in a window with free space + ToolBarView* left1 = addToolBar(100, DockToolBarAlignment::Left); + ToolBarView* left2 = addToolBar(100, DockToolBarAlignment::Left); + ToolBarView* right = addToolBar(100, DockToolBarAlignment::Right); + + m_layout->windowWidth = 1000; + m_layout->separator = 4; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] All the toolbars except the last left one are pinned to their content width, + //! the last left toolbar absorbs the free space + expectPinned(left1, 100); + expectPinned(right, 100); + expectUnpinned(left2); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, CentralToolBarIsPaddedToStayCentered) +{ + //! [GIVEN] Left, central and right toolbars in a window with free space + ToolBarView* left = addToolBar(100, DockToolBarAlignment::Left); + ToolBarView* central = addToolBar(200, DockToolBarAlignment::Center); + ToolBarView* right = addToolBar(100, DockToolBarAlignment::Right); + + m_layout->windowWidth = 1000; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] The left toolbar absorbs the free space + expectUnpinned(left); + + //! [THEN] The right toolbar is pinned to its content width + expectPinned(right, 100); + + //! [THEN] The central toolbar is padded so that its content stays centered: + //! (windowWidth - centralWidth) / 2 - rightWidth = (1000 - 200) / 2 - 100 = 300 + expectPinned(central, 200 + 300); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, LastCentralToolBarAbsorbsFreeSpaceWhenNoLeftToolBars) +{ + //! [GIVEN] Two central toolbars and a right toolbar in a window with free space + ToolBarView* central1 = addToolBar(100, DockToolBarAlignment::Center); + ToolBarView* central2 = addToolBar(100, DockToolBarAlignment::Center); + ToolBarView* right = addToolBar(100, DockToolBarAlignment::Right); + + m_layout->windowWidth = 1000; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] The last central toolbar absorbs the free space, the others are pinned + expectPinned(central1, 100); + expectPinned(right, 100); + expectUnpinned(central2); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, AllToolBarsAreUnpinnedWhenThereIsNoGrower) +{ + //! [GIVEN] Only right toolbars + ToolBarView* right1 = addToolBar(100, DockToolBarAlignment::Right); + ToolBarView* right2 = addToolBar(150, DockToolBarAlignment::Right); + + m_layout->windowWidth = 1000; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] No toolbar is pinned, the layout distributes the free space + expectUnpinned(right1); + expectUnpinned(right2); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, AllToolBarsAreUnpinnedWhenThereIsNoFreeSpace) +{ + //! [GIVEN] Toolbars that occupy more than the window width + ToolBarView* left = addToolBar(600, DockToolBarAlignment::Left); + ToolBarView* right = addToolBar(500, DockToolBarAlignment::Right); + + m_layout->windowWidth = 1000; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] No toolbar is pinned + expectUnpinned(left); + expectUnpinned(right); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, FloatingAndHiddenToolBarsAreNotPinned) +{ + //! [GIVEN] A floating and a hidden left toolbar + ToolBarView* floating = addToolBar(100, DockToolBarAlignment::Left); + floating->setFloatingState(true); + + ToolBarView* hidden = addToolBar(100, DockToolBarAlignment::Left); + hidden->setVisible(false); + + //! [GIVEN] Docked left and right toolbars + ToolBarView* left = addToolBar(100, DockToolBarAlignment::Left); + ToolBarView* right = addToolBar(100, DockToolBarAlignment::Right); + + m_layout->windowWidth = 1000; + + //! [WHEN] Relayout is requested + m_layout->relayout(); + + //! [THEN] The floating and hidden toolbars are not pinned + expectUnpinned(floating); + expectUnpinned(hidden); + + //! [THEN] The docked toolbars are laid out as usual + expectUnpinned(left); + expectPinned(right, 100); +} + +TEST_F(Dock_TopLevelToolBarsLayoutTests, RepeatedRelayoutDoesNotAccumulatePinnedWidths) +{ + //! [GIVEN] Left, central and right toolbars + ToolBarView* central = addToolBar(200, DockToolBarAlignment::Center); + addToolBar(100, DockToolBarAlignment::Left); + addToolBar(100, DockToolBarAlignment::Right); + + //! [GIVEN] The layout has already been done once + m_layout->windowWidth = 1000; + m_layout->relayout(); + expectPinned(central, 200 + 300); + + //! [WHEN] The window shrinks and relayout is requested again + m_layout->windowWidth = 800; + m_layout->relayout(); + + //! [THEN] The central toolbar is pinned according to the new window width: + //! (800 - 200) / 2 - 100 = 200 + expectPinned(central, 200 + 200); +} From ae677c03d1a3e53ffe6ad23d66db6d4db552b5c5 Mon Sep 17 00:00:00 2001 From: Eism Date: Tue, 14 Jul 2026 08:54:59 +0300 Subject: [PATCH 2/2] skip applying floating size constraints for undocked frame Skip applying floating size constraints while the dock's group is still in the main layout - fixes the top row growing taller on undock. --- framework/dockwindow_v2/internal/dockbase.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/framework/dockwindow_v2/internal/dockbase.cpp b/framework/dockwindow_v2/internal/dockbase.cpp index 69b0f74157..6e2ce26c1d 100644 --- a/framework/dockwindow_v2/internal/dockbase.cpp +++ b/framework/dockwindow_v2/internal/dockbase.cpp @@ -712,6 +712,10 @@ void DockBase::applySizeConstraints() KDDockWidgets::Core::Group* group = groupForDockWidget(m_dockWidget); + if (m_floating && group && group->isInMainWindow()) { + return; + } + if (group) { group->view()->setMinimumSize(minimumSize); group->view()->setMaximumSize(maximumSize); @@ -835,8 +839,8 @@ void DockBase::updateFloatingStatus() void DockBase::onIsInMainWindowChanged() { - applySizeConstraints(); updateFloatingStatus(); + applySizeConstraints(); } void DockBase::doSetFloating(bool floating)