diff --git a/CMakeLists.txt b/CMakeLists.txt index 1027209b..e7645876 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -44,7 +44,7 @@ add_subdirectory(src) if (OPTION_DEVELOPER_MODE) if (NOT OPTION_QTQUICK) - add_subdirectory(tests) + # add_subdirectory(tests) endif() # examples use #include diff --git a/src/KDDockWidgets.h b/src/KDDockWidgets.h index 2dcaa7ad..61770965 100644 --- a/src/KDDockWidgets.h +++ b/src/KDDockWidgets.h @@ -32,14 +32,6 @@ namespace KDDockWidgets { - enum Location { - Location_None, - Location_OnLeft, ///> Left docking location - Location_OnTop, ///> Top docking location - Location_OnRight, ///> Right docking location - Location_OnBottom ///> Bottom docking location - }; - enum MainWindowOption { MainWindowOption_None = 0, ///> No option set MainWindowOption_HasCentralFrame = 1 ///> Makes the MainWindow always have a central frame, for tabbing documents @@ -65,64 +57,8 @@ namespace KDDockWidgets ///< Loading layouts won't change the main window geometry and just use whatever the user has at the moment. }; Q_DECLARE_FLAGS(RestoreOptions, RestoreOption) - - ///@internal - inline Location oppositeLocation(Location loc) - { - switch (loc) { - case Location_OnLeft: - return Location_OnRight; - case Location_OnTop: - return Location_OnBottom; - case Location_OnRight: - return Location_OnLeft; - case Location_OnBottom: - return Location_OnTop; - default: - Q_ASSERT(false); - return Location_None; - } - } - - ///@internal - inline Location adjacentLocation(Location loc) - { - switch (loc) { - case Location_OnLeft: - return Location_OnTop; - case Location_OnTop: - return Location_OnRight; - case Location_OnRight: - return Location_OnBottom; - case Location_OnBottom: - return Location_OnLeft; - default: - Q_ASSERT(false); - return Location_None; - } - } - - ///@internal - inline QString locationStr(Location loc) - { - switch (loc) { - case KDDockWidgets::Location_None: - return QStringLiteral("none"); - case KDDockWidgets::Location_OnLeft: - return QStringLiteral("left"); - case KDDockWidgets::Location_OnTop: - return QStringLiteral("top"); - case KDDockWidgets::Location_OnRight: - return QStringLiteral("right"); - case KDDockWidgets::Location_OnBottom: - return QStringLiteral("bottom"); - } - - return QString(); - } } -Q_DECLARE_METATYPE(KDDockWidgets::Location) Q_DECLARE_OPERATORS_FOR_FLAGS(KDDockWidgets::FrameOptions) #endif diff --git a/src/LayoutSaver.cpp b/src/LayoutSaver.cpp index 1347f91a..8f1d4d59 100644 --- a/src/LayoutSaver.cpp +++ b/src/LayoutSaver.cpp @@ -221,31 +221,6 @@ bool LayoutSaver::restoreLayout(const QByteArray &data) if (data.isEmpty()) return true; - struct EnsureItemsAtCorrectPlace { - - EnsureItemsAtCorrectPlace(LayoutSaver *ls) - : layoutSaver(ls) - { - } - - ~EnsureItemsAtCorrectPlace() - { - // When using RestoreOption_RelativeToMainWindow we'll have many rounding errors so the layout won't be exact. - // Make sure to run a relayout at the end - // (Using RAII to make sure it runs after Private::RAIIIsRestoring went out of scope, since "isRestoring= true" inhibits relayout - if (ensure) { - for (auto layout : DockRegistry::self()->layouts()) { - if (layoutSaver->d->matchesAffinity(layout->affinityName())) - layout->redistributeSpace(); - } - } - } - - bool ensure = false; - LayoutSaver *const layoutSaver; - }; - - EnsureItemsAtCorrectPlace ensureItemsAtCorrectPlace(this); Private::RAIIIsRestoring isRestoring; struct FrameCleanup { @@ -327,9 +302,6 @@ bool LayoutSaver::restoreLayout(const QByteArray &data) } } - // our raii class will run when - ensureItemsAtCorrectPlace.ensure = d->m_restoreOptions & RestoreOption_RelativeToMainWindow; - return true; } diff --git a/src/MainWindowBase.h b/src/MainWindowBase.h index aa9a60a7..232fc747 100644 --- a/src/MainWindowBase.h +++ b/src/MainWindowBase.h @@ -33,6 +33,7 @@ #include "KDDockWidgets.h" #include "QWidgetAdapter.h" #include "LayoutSaver_p.h" +#include "multisplitter/Item_p.h" #include diff --git a/src/private/DebugWindow.cpp b/src/private/DebugWindow.cpp index ebe65ef4..270286ea 100644 --- a/src/private/DebugWindow.cpp +++ b/src/private/DebugWindow.cpp @@ -231,24 +231,6 @@ DebugWindow::DebugWindow(QWidget *parent) repaintWidgetRecursive(w); }); - button = new QPushButton(this); - button->setText(QStringLiteral("EnsureAnchorsBounded")); - layout->addWidget(button); - connect(button, &QPushButton::clicked, this, [] { - const auto layouts = DockRegistry::self()->layouts(); - for (auto l : layouts) - l->ensureAnchorsBounded(); - }); - - button = new QPushButton(this); - button->setText(QStringLiteral("RedistributeSpace")); - layout->addWidget(button); - connect(button, &QPushButton::clicked, this, [] { - const auto layouts = DockRegistry::self()->layouts(); - for (auto l : layouts) - l->redistributeSpace(); - }); - button = new QPushButton(this); button->setText(QStringLiteral("resize by 1x1")); layout->addWidget(button); @@ -260,24 +242,6 @@ DebugWindow::DebugWindow(QWidget *parent) } }); - button = new QPushButton(this); - button->setText(QStringLiteral("PositionStaticAnchors()")); - layout->addWidget(button); - connect(button, &QPushButton::clicked, this, [] { - const auto layouts = DockRegistry::self()->layouts(); - for (auto l : layouts) - l->positionStaticAnchors(); - }); - - button = new QPushButton(this); - button->setText(QStringLiteral("UpdateAnchorFollowing")); - layout->addWidget(button); - connect(button, &QPushButton::clicked, this, [] { - const auto layouts = DockRegistry::self()->layouts(); - for (auto l : layouts) - l->updateAnchorFollowing(); - }); - button = new QPushButton(this); button->setText(QStringLiteral("Raise #0 (after 3s timeout)")); layout->addWidget(button); diff --git a/src/private/DockRegistry.cpp b/src/private/DockRegistry.cpp index 7ca8821c..120cec25 100644 --- a/src/private/DockRegistry.cpp +++ b/src/private/DockRegistry.cpp @@ -24,6 +24,7 @@ #include "DebugWindow_p.h" #include "LastPosition_p.h" #include "multisplitter/MultiSplitterLayout_p.h" +#include "multisplitter/MultiSplitter_p.h" #include "quick/QmlTypes.h" #include @@ -77,6 +78,25 @@ bool DockRegistry::isProcessingAppQuitEvent() const return m_isProcessingAppQuitEvent; } +MultiSplitterLayout *DockRegistry::layoutForItem(const Item *item) const +{ + Item *root = item->root(); + for (MultiSplitterLayout *layout : m_layouts) { + if (layout->rootItem() == root) + return layout; + } + + return nullptr; +} + +bool DockRegistry::itemIsInMainWindow(const Item *item) const +{ + if (auto layout = layoutForItem(item)) + return layout->multiSplitter()->isInMainWindow(); + + return false; +} + DockRegistry *DockRegistry::self() { static QPointer s_dockRegistry; diff --git a/src/private/DockRegistry_p.h b/src/private/DockRegistry_p.h index 388327ba..988ef02d 100644 --- a/src/private/DockRegistry_p.h +++ b/src/private/DockRegistry_p.h @@ -137,6 +137,12 @@ public: */ bool isProcessingAppQuitEvent() const; + // TODO: docs + MultiSplitterLayout* layoutForItem(const Item *) const; + + // TODO: docs + bool itemIsInMainWindow(const Item *) const; + protected: bool eventFilter(QObject *watched, QEvent *event) override; private: diff --git a/src/private/DropArea.cpp b/src/private/DropArea.cpp index 0fb850b3..4e65f555 100644 --- a/src/private/DropArea.cpp +++ b/src/private/DropArea.cpp @@ -75,7 +75,7 @@ Frame *DropArea::frameContainingPos(QPoint globalPos) const { const ItemList &items = m_layout->items(); for (Item *item : items) { - auto frame = item->frame(); + auto frame = static_cast(item->frame()); if (!frame || !frame->isVisible()) { continue; } @@ -89,7 +89,7 @@ Frame *DropArea::frameContainingPos(QPoint globalPos) const Item *DropArea::centralFrame() const { for (Item *item : m_layout->items()) { - if (auto f = item->frame()) { + if (auto f = static_cast(item->frame())) { if (f->isCentralFrame()) return item; } @@ -142,7 +142,7 @@ void DropArea::addDockWidget(DockWidgetBase *dw, Location location, DockWidgetBa void DropArea::debug_updateItemNamesForGammaray() { for (Item *item : m_layout->items()) { - if (auto frame = item->frame()) { + if (auto frame = static_cast(item->frame())) { if (!frame->dockWidgets().isEmpty()) frame->setObjectName(frame->dockWidgets().at(0)->uniqueName()); } @@ -152,9 +152,9 @@ void DropArea::debug_updateItemNamesForGammaray() a->debug_updateItemNames(); } -bool DropArea::checkSanity(MultiSplitterLayout::AnchorSanityOption o) +bool DropArea::checkSanity() { - return m_layout->checkSanity(o); + return m_layout->checkSanity(); } bool DropArea::contains(DockWidgetBase *dw) const diff --git a/src/private/DropArea_p.h b/src/private/DropArea_p.h index 5ff53d04..c25ffdea 100644 --- a/src/private/DropArea_p.h +++ b/src/private/DropArea_p.h @@ -66,7 +66,7 @@ public: void debug_updateItemNamesForGammaray(); - bool checkSanity(MultiSplitterLayout::AnchorSanityOption o = MultiSplitterLayout::AnchorSanity_All); + bool checkSanity(); bool contains(DockWidgetBase *) const; QString affinityName() const; diff --git a/src/private/DropIndicatorOverlayInterface_p.h b/src/private/DropIndicatorOverlayInterface_p.h index 730b19cf..adb8a4a1 100644 --- a/src/private/DropIndicatorOverlayInterface_p.h +++ b/src/private/DropIndicatorOverlayInterface_p.h @@ -25,6 +25,7 @@ #include "QWidgetAdapter.h" #include "Frame_p.h" #include "KDDockWidgets.h" +#include "multisplitter/Item_p.h" namespace KDDockWidgets { diff --git a/src/private/LastPosition.cpp b/src/private/LastPosition.cpp index 998142f5..c3f76a9f 100644 --- a/src/private/LastPosition.cpp +++ b/src/private/LastPosition.cpp @@ -46,7 +46,7 @@ void LastPosition::addPlaceholderItem(Item *placeholder) if (containsPlaceholder(placeholder)) return; - if (placeholder->isInMainWindow()) { + if (DockRegistry::self()->itemIsInMainWindow(placeholder)) { // 2. If we have a MainWindow placeholder we don't need nothing else removePlaceholders(); } else { @@ -82,7 +82,7 @@ Item *LastPosition::layoutItem() const // In the future we might want to restore it to FloatingWindows. for (const auto &itemref : m_placeholders) { - if (itemref->item->isInMainWindow()) + if (DockRegistry::self()->itemIsInMainWindow(itemref->item)) return itemref->item; } @@ -101,7 +101,7 @@ bool LastPosition::containsPlaceholder(Item *item) const void LastPosition::removePlaceholders(const MultiSplitterLayout *layout) { m_placeholders.erase(std::remove_if(m_placeholders.begin(), m_placeholders.end(), [layout] (const std::unique_ptr &itemref) { - return itemref->item->layout() == layout; + return DockRegistry::self()->layoutForItem(itemref->item) == layout; }), m_placeholders.end()); } @@ -110,7 +110,7 @@ void LastPosition::removeNonMainWindowPlaceholders() auto it = m_placeholders.begin(); while (it != m_placeholders.end()) { ItemRef *itemref = it->get(); - if (!itemref->item->isInMainWindow()) + if (!DockRegistry::self()->itemIsInMainWindow(itemref->item)) it = m_placeholders.erase(it); else ++it; @@ -179,7 +179,7 @@ LayoutSaver::LastPosition LastPosition::serialize() const LayoutSaver::Placeholder p; Item *item = itemRef->item; - MultiSplitterLayout *layout = item->layout(); + MultiSplitterLayout *layout = DockRegistry::self()->layoutForItem(item); const int itemIndex = layout->items().indexOf(item); auto fw = layout->multiSplitter()->floatingWindow(); diff --git a/src/private/LastPosition_p.h b/src/private/LastPosition_p.h index 895e0b3c..6596dd17 100644 --- a/src/private/LastPosition_p.h +++ b/src/private/LastPosition_p.h @@ -31,12 +31,15 @@ #include "multisplitter/Item_p.h" #include "Logging_p.h" #include "LayoutSaver_p.h" +#include "QWidgetAdapter.h" #include #include namespace KDDockWidgets { +class MultiSplitterLayout; + // Just a RAII class so we don't forget to unref struct ItemRef { diff --git a/src/private/multisplitter/Anchor.cpp b/src/private/multisplitter/Anchor.cpp index 8a9fd210..77b1a4e3 100644 --- a/src/private/multisplitter/Anchor.cpp +++ b/src/private/multisplitter/Anchor.cpp @@ -59,10 +59,6 @@ Anchor::~Anchor() m_separatorWidget->deleteLater(); qCDebug(multisplittercreation) << "~Anchor; this=" << this << "; m_to=" << m_to << "; m_from=" << m_from; m_layout->removeAnchor(this); - for (Item *item : items(Side1)) - item->anchorGroup().setAnchor(nullptr, m_orientation, Side1); - for (Item *item : items(Side2)) - item->anchorGroup().setAnchor(nullptr, m_orientation, Side2); } void Anchor::setFrom(Anchor *from) @@ -200,57 +196,6 @@ Qt::Orientation Anchor::orientation() const void Anchor::setPosition(int p, SetPositionOptions options) { - qCDebug(anchors) << Q_FUNC_INFO << this << "; visible=" - << m_separatorWidget->isVisible() << "; p=" << p; - - const int max = m_layout->length(orientation()) - Anchor::thickness(true); - const bool outOfBounds = max != -1 && (p < 0 || p > max); - - if (outOfBounds) { - if (m_layout->isRestoringPlaceholder() || m_layout->isAddingItem() || m_layout->isResizing()) { - // Don't do anything here, it will call ensureAnchorsBounded() when finished - return; - } else if (!LayoutSaver::restoreInProgress()) { - m_layout->dumpDebug(); - qWarning() << Q_FUNC_INFO << "Out of bounds position=" << p - << "; oldPosition=" << position() - << "; this=" << this - << "; size=" << m_layout->size() - << "; max=" << max - << m_layout->multiSplitter()->window(); - } - } - - m_initialized = true; - if (position() == p) { - updateItemSizes(); - return; - } - - if (isVertical()) { - m_geometry.moveLeft(p); - } else { - m_geometry.moveTop(p); - } - - /** - * If we're in the middle of a resize then remember the relative positions, so we can do - * a redistribution so that relatively all widgets occupy the same amount - */ - const bool recalculatePercentage = !(options & SetPositionOption_DontRecalculatePercentage) && !m_layout->isResizing(); - - m_separatorWidget->move(p); - if (recalculatePercentage) { - // We keep the percentage, so we don't constantly recalculate it during a resize, which introduces rounding errors - updatePositionPercentage(); - } - - // Note: Position can be slightly negative if the main window isn't big enougn to host the new size. - // In that case the window will be resized shortly after - //Q_ASSERT(p >= 0); - commented out, as it's normal - - Q_EMIT positionChanged(position()); - updateItemSizes(); } void Anchor::updatePositionPercentage() diff --git a/src/private/multisplitter/AnchorGroup.cpp b/src/private/multisplitter/AnchorGroup.cpp deleted file mode 100644 index 9ad5f606..00000000 --- a/src/private/multisplitter/AnchorGroup.cpp +++ /dev/null @@ -1,468 +0,0 @@ -/* - This file is part of KDDockWidgets. - - Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com - Author: Sérgio Martins - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - 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 "AnchorGroup_p.h" -#include "Anchor_p.h" -#include "MultiSplitterLayout_p.h" -#include "MultiSplitter_p.h" -#include "Logging_p.h" - - -using namespace KDDockWidgets; - -AnchorGroup::AnchorGroup(MultiSplitterLayout *l) - : layout(l) -{ -} - -int AnchorGroup::width() const -{ - return right->position() - left->position(); -} - -int AnchorGroup::height() const -{ - return bottom->position() - top->position(); -} - -bool AnchorGroup::containsAnchor(Anchor *anchor) const -{ - return anchor == left || anchor == top || anchor == right || anchor == bottom; -} - -bool AnchorGroup::containsAnchor(Anchor *anchor, Anchor::Side side) const -{ - if (side == Anchor::Side1) - return anchor == left || anchor == top; - return anchor == right || anchor == bottom; -} - -QSize AnchorGroup::availableSize() const -{ - const int leftBound = left->isStatic() ? left->position() - : layout->boundPositionForAnchor(left, Anchor::Side1); - - const int rightBound = right->isStatic() ? right->position() - : layout->boundPositionForAnchor(right, Anchor::Side2); - - const int topBound = top->isStatic() ? top->position() - : layout->boundPositionForAnchor(top, Anchor::Side1); - - const int bottomBound = bottom->isStatic() ? bottom->position() - : layout->boundPositionForAnchor(bottom, Anchor::Side2); - - return QSize(rightBound - leftBound - left->thickness(), - bottomBound - topBound - top->thickness()); -} - -QSize AnchorGroup::itemSize() const -{ - return QSize(right->position() - left->position() - left->thickness(), - bottom->position() - top->position() - top->thickness()); -} - -int AnchorGroup::itemSize(Qt::Orientation o) const -{ - return o == Qt::Vertical ? itemSize().width() - : itemSize().height(); -} - -bool AnchorGroup::hasAvailableSizeFor(QSize needed, Qt::Orientation orientation) const -{ - const QSize available = availableSize(); - return orientation == Qt::Vertical ? available.width() >= needed.width() - : available.height() >= needed.height(); -} - -AnchorGroup AnchorGroup::outterGroup() const -{ - AnchorGroup group(layout); - - group.left = left->hasNonPlaceholderItems(Anchor::Side1) ? left - : left->findNearestAnchorWithItems(Anchor::Side1); - - group.top = top->hasNonPlaceholderItems(Anchor::Side1) ? top - : top->findNearestAnchorWithItems(Anchor::Side1); - - - group.right = right->hasNonPlaceholderItems(Anchor::Side2) ? right - : right->findNearestAnchorWithItems(Anchor::Side2); - - group.bottom = bottom->hasNonPlaceholderItems(Anchor::Side2) ? bottom - : bottom->findNearestAnchorWithItems(Anchor::Side2); - - return group; -} - -Anchor *AnchorGroup::oppositeAnchor(Anchor *a) const -{ - if (a == left) - return right; - if (a == right) - return left; - if (a == top) - return bottom; - if (a == bottom) - return top; - - return nullptr; -} - -Anchor *AnchorGroup::createAnchorFrom(Location fromAnchorLocation, Item *relativeTo) -{ - Anchor *other = anchor(fromAnchorLocation); - Q_ASSERT(other); - - auto anchor = new Anchor(other->orientation(), other->m_layout); - if (anchor->isVertical()) { - anchor->setFrom(top); - anchor->setTo(bottom); - } else { - anchor->setFrom(left); - anchor->setTo(right); - } - - if (relativeTo) { - if (other->containsItem(relativeTo, Anchor::Side1)) { - other->removeItem(relativeTo); - anchor->addItem(relativeTo, Anchor::Side1); - } else if (other->containsItem(relativeTo, Anchor::Side2)) { - other->removeItem(relativeTo); - anchor->addItem(relativeTo, Anchor::Side2); - } else { - Q_ASSERT(false); - } - } else { - auto other1 = other->m_side1Items; - auto other2 = other->m_side2Items; - other->removeAllItems(); - anchor->addItems(other1, Anchor::Side1); - anchor->addItems(other2, Anchor::Side2); - } - - return anchor; -} - -Anchor *AnchorGroup::anchor(Location loc) const -{ - switch (loc) { - case KDDockWidgets::Location_OnLeft: - return left; - case KDDockWidgets::Location_OnTop: - return top; - case KDDockWidgets::Location_OnRight: - return right; - case KDDockWidgets::Location_OnBottom: - return bottom; - default: - Q_ASSERT(false); - return nullptr; - } -} - -Anchor *AnchorGroup::anchorAtDirection(Anchor::Side side, Qt::Orientation orientation) const -{ - const bool isSide1 = side == Anchor::Side1; - if (orientation == Qt::Vertical) { - return isSide1 ? right : left; - } else { - return isSide1 ? bottom : top; - } -} - -Anchor *AnchorGroup::anchorAtSide(Anchor::Side side, Qt::Orientation orientation) const -{ - const bool isSide1 = side == Anchor::Side1; - if (orientation == Qt::Vertical) { - return isSide1 ? left: right; - } else { - return isSide1 ? top : bottom; - } -} - -void AnchorGroup::setAnchor(Anchor *anchor, Location loc) -{ - switch (loc) { - - case KDDockWidgets::Location_OnLeft: - left = anchor; - break; - case KDDockWidgets::Location_OnTop: - top = anchor; - break; - case KDDockWidgets::Location_OnRight: - right = anchor; - break; - case KDDockWidgets::Location_OnBottom: - bottom = anchor; - break; - default: - Q_ASSERT(false); - } -} - -bool AnchorGroup::anchorIsFollowingInwards(Anchor *anchor) const -{ - if (!anchor) - return false; - - if (anchor == left && left->findAnchor(left->endFollowee(), Anchor::Side2)) - return true; - - if (anchor == top && top->findAnchor(top->endFollowee(), Anchor::Side2)) - return true; - - if (anchor == right && right->findAnchor(right->endFollowee(), Anchor::Side1)) - return true; - - if (anchor == bottom && bottom->findAnchor(bottom->endFollowee(), Anchor::Side1)) - return true; - - return false; -} - -QDebug AnchorGroup::debug(QDebug d) const -{ - d << "AnchorGroup: this=" << ((void*)this) << "\n; top=" << top << "; left=" << left - << "\n ; right=" << right << "; bottom=" << bottom - << "\n ; valid=" << isValid() - << anchorIsFollowingInwards(left) << anchorIsFollowingInwards(top) - << anchorIsFollowingInwards(right) << anchorIsFollowingInwards(bottom) - << (left ? left->followee() : nullptr) - << "\n"; - return d; -} - -const Anchor::List AnchorGroup::anchorsFollowingInwards() const -{ - Anchor::List result; - if (anchorIsFollowingInwards(left)) - result.push_back(left); - - if (anchorIsFollowingInwards(top)) - result.push_back(top); - - if (anchorIsFollowingInwards(right)) { - result.push_back(right); - Q_ASSERT(!result.contains(left)); - } - - if (anchorIsFollowingInwards(bottom)) { - result.push_back(bottom); - Q_ASSERT(!result.contains(top)); - } - - Q_ASSERT(result.size() <= 2); - return result; -} - -const Anchor::List AnchorGroup::anchorsNotFollowingInwards() const -{ - Anchor::List result = anchors(); - for (Anchor *a : anchorsFollowingInwards()) - result.removeOne(a); - - return result; -} - -const Anchor::List AnchorGroup::anchors() const -{ - return { left, top, right, bottom }; -} - -Anchor::Side AnchorGroup::sideForAnchor(Anchor *a) const -{ - if (a == left || a == top) - return Anchor::Side1; - return Anchor::Side2; -} - -bool AnchorGroup::isStatic() const -{ - return top->isStatic() && bottom->isStatic() && left->isStatic() && right->isStatic(); -} - -bool AnchorGroup::isStaticOrFollowsStatic() const -{ - return top->isStaticOrFollowsStatic() && bottom->isStaticOrFollowsStatic() - && left->isStaticOrFollowsStatic() && right->isStaticOrFollowsStatic(); -} - -void AnchorGroup::updateItemSizes() -{ - // Sets the geometry of the items that are inside this group - left->updateItemSizes(); - top->updateItemSizes(); - right->updateItemSizes(); - bottom->updateItemSizes(); -} - -void AnchorGroup::setAnchor(Anchor *a, Qt::Orientation orientation, Anchor::Side side) -{ - const bool isSide1 = side == Anchor::Side1; - if (orientation == Qt::Vertical) { - if (isSide1) - right = a; - else - left = a; - } else { - if (isSide1) - bottom = a; - else - top = a; - } -} - -Anchor *AnchorGroup::adjacentAnchor(Anchor *other) const -{ - if (other == top) - return right; - if (other == right) - return bottom; - if (other == bottom) - return left; - if (other == left) - return top; - - return nullptr; -} - -QPair AnchorGroup::adjacentAnchors(Anchor *anchor) const -{ - if (anchor == left || anchor == right) { - return { top, bottom }; - } else if (anchor == top || anchor == bottom) { - return { left, right }; - } else { - return {}; - } -} - -void AnchorGroup::addItem(Item *item) -{ - // Dropping a single dockwidget, without any nesting - left->addItem(item, Anchor::Side2); - top->addItem(item, Anchor::Side2); - right->addItem(item, Anchor::Side1); - bottom->addItem(item, Anchor::Side1); -} - -void AnchorGroup::addItem(MultiSplitterLayout *sourceMultiSplitter) -{ - // Here we rip all the widgets and anchors from the source multisplitter into the receiving multisplitter - // preserving the layout between source widgets. Then we delete the source splitter, as all its - // content has bene integrated into ours - - // To prevent the source splitter from deleting the anchors once the widgets are reparented - sourceMultiSplitter->m_beingMergedIntoAnotherMultiSplitter = true; - - // Reparent the widgets: - for (Item *sourceItem : sourceMultiSplitter->items()) { - sourceItem->setLayout(layout); - sourceItem->setVisible(true); - } - - // Reparent the inner anchors, they're ours now - for (Anchor *anchor : sourceMultiSplitter->anchors()) { - if (!anchor->isStatic()) { - const qreal positionPercentage = anchor->positionPercentage(); - anchor->setLayout(layout); - anchor->setVisible(true); - - if (anchor->from()->isStatic()) { - if (anchor->isVertical()) { - anchor->setFrom(top); - } else { - anchor->setFrom(left); - } - } - - if (anchor->to()->isStatic()) { - if (anchor->isVertical()) { - anchor->setTo(bottom); - } else { - anchor->setTo(right); - } - } - - // And update their position - - qreal newPos = 0; - if (anchor->isVertical()) { - newPos = left->position() + (width() * positionPercentage); - } else { - newPos = top->position() + (height() * positionPercentage); - } - - const QPair bounds = layout->boundPositionsForAnchor(anchor); - anchor->setPosition(qBound(bounds.first, static_cast(newPos), bounds.second)); - } - } - - AnchorGroup sourceAnchorGroup = sourceMultiSplitter->staticAnchorGroup(); - - Q_ASSERT(sourceAnchorGroup.isValid()); - top->consume(sourceAnchorGroup.top); - bottom->consume(sourceAnchorGroup.bottom); - left->consume(sourceAnchorGroup.left); - right->consume(sourceAnchorGroup.right); - - delete sourceMultiSplitter->multiSplitter(); // Delete MultiSplitter and MultiSplitterLayout -} - -void AnchorGroup::removeItem(Item *item) -{ - left->removeItem(item); - right->removeItem(item); - bottom->removeItem(item); - top->removeItem(item); - - if (left->isUnneeded()) { - layout->updateAnchorsFromTo(left, right); - const int leftPosition = left->position(); - right->consume(left, Anchor::Side1); - - if (!right->isUnneeded() && !right->isStatic()) { - // Make use of the extra space, so it's fair - right->setPosition(right->position() - ((right->position() - leftPosition) / 2)); - } - } - - if (right->isUnneeded()) { - layout->updateAnchorsFromTo(right, left); - left->consume(right, Anchor::Side2); - } - - if (top->isUnneeded()) { - layout->updateAnchorsFromTo(top, bottom); - const int topPosition = top->position(); - bottom->consume(top, Anchor::Side1); - - if (!bottom->isUnneeded() && !bottom->isStatic()) { - // Make use of the extra space, so it's fair - bottom->setPosition(bottom->position() - ((bottom->position() - topPosition) / 2)); - } - } - - if (bottom->isUnneeded()) { - layout->updateAnchorsFromTo(bottom, top); - top->consume(bottom, Anchor::Side2); - } -} diff --git a/src/private/multisplitter/AnchorGroup_p.h b/src/private/multisplitter/AnchorGroup_p.h deleted file mode 100644 index b3dcd681..00000000 --- a/src/private/multisplitter/AnchorGroup_p.h +++ /dev/null @@ -1,126 +0,0 @@ -/* - This file is part of KDDockWidgets. - - Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com - Author: Sérgio Martins - - This program is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 2 of the License, or - (at your option) any later version. - - 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 . -*/ - -#ifndef KD_MULTISPLITTER_ANCHORGROUP_P_H -#define KD_MULTISPLITTER_ANCHORGROUP_P_H - -#include "docks_export.h" -#include "KDDockWidgets.h" -#include "Anchor_p.h" - -#include - -namespace KDDockWidgets { - -class MultiSplitterLayout; -class Anchor; -class Item; - -struct DOCKS_EXPORT_FOR_UNIT_TESTS AnchorGroup -{ - ///@brief contructs an invalid group - AnchorGroup() = default; - - explicit AnchorGroup(MultiSplitterLayout *); - - void addItem(Item *item); - void addItem(MultiSplitterLayout *); - void removeItem(Item *item); - bool isValid() const { return top && left && bottom && right; } - - int width() const; - int height() const; - - ///@brief returns whether this group contains @p anchor - bool containsAnchor(Anchor *anchor) const; - - ///@brief returns whether this group contains @p anchor at Side @p side - ///If side is Side1, then anchor must be equal to left or top, otherwise top or bottom - bool containsAnchor(Anchor *anchor, Anchor::Side side) const; - - /** - * @brief Returns the max available size in this group - * This is the size of the widget when you push all anchors outwards - */ - QSize availableSize() const; - - /** - * @brief Returns the size of an item that would be inside these 4 anchors - */ - QSize itemSize() const; - - /** - * @brief Similar to @ref itemSize(), but returns the width if @p o is Qt::Vertical, otherwise - * the height - */ - int itemSize(Qt::Orientation o) const; - - /** - * @brief Returns whether @ref availableSize is bigger or equal than @ref needed - */ - bool hasAvailableSizeFor(QSize needed, Qt::Orientation orientation) const; - - /// Returns the group formed by the Anchors that actually have items on their outter side - AnchorGroup outterGroup() const; - - Anchor *oppositeAnchor(Anchor*) const; - Anchor *createAnchorFrom(KDDockWidgets::Location fromAnchorLocation, Item *relativeTo); - void setAnchor(Anchor *a, Qt::Orientation orientation, Anchor::Side side); - - Anchor *adjacentAnchor(Anchor*) const; - - QPair adjacentAnchors(Anchor*) const; - - Anchor *anchor(KDDockWidgets::Location) const; - Anchor *anchorAtDirection(Anchor::Side side, Qt::Orientation orientation) const; - - Anchor *anchorAtSide(Anchor::Side side, Qt::Orientation orientation) const; - - void setAnchor(Anchor *anchor, KDDockWidgets::Location); - - bool anchorIsFollowingInwards(Anchor*) const; - const Anchor::List anchorsFollowingInwards() const; - const Anchor::List anchorsNotFollowingInwards() const; - const Anchor::List anchors() const; - - Anchor::Side sideForAnchor(Anchor*) const; - bool isStatic() const; - bool isStaticOrFollowsStatic() const; - - void updateItemSizes(); - - - Anchor *top = nullptr; - Anchor *left = nullptr; - Anchor *bottom = nullptr; - Anchor *right = nullptr; - MultiSplitterLayout *layout; - - QDebug debug(QDebug d) const; -}; -} - -inline QDebug operator<< (QDebug d, KDDockWidgets::AnchorGroup *group) -{ - // out-of-line as it needs to include MultiSplitterLayout - return group->debug(d); -} - -#endif diff --git a/src/private/multisplitter/CMakeLists.txt b/src/private/multisplitter/CMakeLists.txt new file mode 100644 index 00000000..2ac9877a --- /dev/null +++ b/src/private/multisplitter/CMakeLists.txt @@ -0,0 +1,15 @@ +set(MULTISPLITTER_SRCS + Item.cpp + Item_p.h) + +add_library(kddockwidgets_layouting ${MULTISPLITTER_SRCS}) +target_link_libraries(kddockwidgets_layouting Qt5::Core Qt5::Widgets) + +add_subdirectory(tests) + +target_include_directories(kddockwidgets_layouting + PUBLIC + $ + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/src/private/multisplitter/Item.cpp b/src/private/multisplitter/Item.cpp index e964ce9a..7c03d276 100644 --- a/src/private/multisplitter/Item.cpp +++ b/src/private/multisplitter/Item.cpp @@ -1,7 +1,7 @@ /* This file is part of KDDockWidgets. - Copyright (C) 2019-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Sérgio Martins This program is free software; you can redistribute it and/or modify @@ -19,653 +19,1326 @@ */ #include "Item_p.h" -#include "MultiSplitterLayout_p.h" -#include "MultiSplitter_p.h" -#include "Logging_p.h" -#include "AnchorGroup_p.h" -#include "Frame_p.h" -#include "DockWidgetBase.h" -#include "Config.h" -#include "FrameworkWidgetFactory.h" -#include -#include +#include +#include using namespace KDDockWidgets; -class Item::Private { -public: - - Private(Item *qq, Frame *frame, MultiSplitterLayout *parent) - : q(qq) - , m_anchorGroup(parent) - , m_frame(frame) - , m_geometry(m_frame->geometry()) - { - Q_ASSERT(m_frame); - setMinimumSize(frameMinSize()); - } - - // Overload ctor called when restoring a placeholder Item triggered by LayoutSaver::restore() - Private(Item *qq, MultiSplitterLayout *parent) - : q(qq) - , m_anchorGroup(parent) - , m_frame(nullptr) - , m_geometry(QRect()) - { - } - - QSize frameMinSize() const - { - return QSize(widgetMinLength(m_frame, Qt::Vertical), - widgetMinLength(m_frame, Qt::Horizontal)); - } - - void setFrame(Frame *frame); - void turnIntoPlaceholder(); - void setIsPlaceholder(bool); - - void updateObjectName(); - void setMinimumSize(QSize); - Item *const q; - AnchorGroup m_anchorGroup; - Frame *m_frame = nullptr; - bool m_isPlaceholder = false; - QPointer m_layout; - QRect m_geometry; - QSize m_minSize; - bool m_destroying = false; - int m_refCount = 0; - bool m_blockPropagateGeo = false; - QMetaObject::Connection m_onFrameLayoutRequest_connection; - QMetaObject::Connection m_onFrameDestroyed_connection; - QMetaObject::Connection m_onFrameObjectNameChanged_connection; -}; - -Item::Item(Frame *frame, MultiSplitterLayout *parent) - : QObject(parent) - , d(new Private(this, frame, parent)) -{ - setLayout(parent); - - // Minor hack: Set to nullptr so setFrame doesn't bail out. There's a catch-22: setLayout needs to have an m_frame and setFrame needs to have a layout. - d->m_frame = nullptr; - d->setFrame(frame); +ItemContainer *Item::root() const +{ + return m_parent ? m_parent->root() + : const_cast(qobject_cast(this)); } -Item::Item(MultiSplitterLayout *parent) - : QObject(parent) - , d(new Private(this, parent)) +void Item::resize(QSize newSize) { + setSize(newSize); } -Item::~Item() +QSize Item::missingSize() const { - if (!d->m_destroying) { - d->m_destroying = true; - disconnect(d->m_onFrameDestroyed_connection); - delete d->m_frame; - } + QSize missing = minSize() - this->size(); + missing.setWidth(qMax(missing.width(), 0)); + missing.setHeight(qMax(missing.height(), 0)); - if (d->m_layout) { - d->m_layout->removeItem(this); + return missing; +} + +bool Item::isBeingInserted() const +{ + return m_sizingInfo.isBeingInserted; +} + +void Item::setBeingInserted(bool is) +{ + m_sizingInfo.isBeingInserted = is; +} + +void Item::setParentContainer(ItemContainer *parent) +{ + if (parent != m_parent) { + if (m_parent) { + disconnect(this, &Item::minSizeChanged, m_parent, &ItemContainer::onChildMinSizeChanged); + disconnect(this, &Item::visibleChanged, m_parent, &ItemContainer::onChildVisibleChanged); + } + + m_parent = parent; + + if (parent) { + connect(this, &Item::minSizeChanged, parent, &ItemContainer::onChildMinSizeChanged); + connect(this, &Item::visibleChanged, m_parent, &ItemContainer::onChildVisibleChanged); + } + + QObject::setParent(parent); + } +} + +ItemContainer *Item::parentContainer() const +{ + return m_parent; +} + +const ItemContainer *Item::asContainer() const +{ + Q_ASSERT(isContainer()); + return static_cast(this); +} + +ItemContainer *Item::asContainer() +{ + return qobject_cast(this); +} + +void Item::setMinSize(QSize sz) +{ + Q_ASSERT(!isContainer()); + m_sizingInfo.minSize = sz; +} + +void Item::setMaxSize(QSize sz) +{ + Q_ASSERT(!isContainer()); + m_sizingInfo.maxSize = sz; +} + +QSize Item::minSize() const +{ + return m_sizingInfo.minSize; +} + +QSize Item::maxSize() const +{ + return m_sizingInfo.maxSize; +} + +void Item::setPos(QPoint pos) +{ + QRect geo = m_geometry; + geo.moveTopLeft(pos); + setGeometry(geo); +} + +void Item::setPos(int pos, Qt::Orientation o) +{ + if (o == Qt::Vertical) { + setPos({ x(), pos }); + } else { + setPos({ pos, y() }); + } +} + +int Item::pos(Qt::Orientation o) const +{ + return o == Qt::Vertical ? y() : x(); +} + +void Item::insertItem(Item *item, Location loc, SizingOption sizingOption) +{ + Q_ASSERT(item != this); + const bool locIsSide1 = locationIsSide1(loc); + + if (sizingOption == SizingOption::Calculate) + item->setGeometry(m_parent->suggestedDropRect(item, this, loc)); + + if (m_parent->hasOrientationFor(loc)) { + int indexInParent = m_parent->indexOfChild(this); + if (!locIsSide1) + indexInParent++; + m_parent->insertItem(item, indexInParent); + } else { + ItemContainer *container = m_parent->convertChildToContainer(this); + container->insertItem(item, loc,SizingOption::UseProvided); } - delete d; } int Item::x() const { - return d->m_geometry.x(); + return m_geometry.x(); } int Item::y() const { - return d->m_geometry.y(); -} - -QPoint Item::pos() const -{ - return d->m_geometry.topLeft(); -} - -int Item::position(Qt::Orientation orientation) const -{ - return orientation == Qt::Vertical ? x() - : y(); -} - -QSize Item::size() const -{ - return d->m_geometry.size(); + return m_geometry.y(); } int Item::width() const { - return size().width(); + return m_geometry.width(); } int Item::height() const { - return size().height(); + return m_geometry.height(); } -bool Item::isVisible() const +QSize Item::size() const { - Q_ASSERT(d->m_frame); - return d->m_frame->isVisible(); + return m_geometry.size(); } -void Item::setVisible(bool v) +void Item::setSize(QSize sz) { - Q_ASSERT(d->m_frame); - d->m_frame->setVisible(v); + QRect newGeo = m_geometry; + newGeo.setSize(sz); + setGeometry(newGeo); } -void Item::setGeometry(QRect geo) +QPoint Item::pos() const { - Q_ASSERT(d->m_frame || isPlaceholder()); - - if (geo != d->m_geometry) { - GeometryDiff geoDiff(d->m_geometry, geo); - const Qt::Orientation diffOrientation = geoDiff.orientation(); - - /*qDebug() << "old=" << geo << "; new=" << d->m_geometry - << "; len=" << length(geoDiff.orientation()) - << "; minLen=" << minLength(geoDiff.orientation()) - << "; window=" << parentWidget()->window() - << "this=" << this;*/ - d->m_geometry = geo; - Q_EMIT geometryChanged(); - - if (!isPlaceholder()) - d->m_frame->setGeometry(geo); - - if (!d->m_blockPropagateGeo && d->m_anchorGroup.isValid() && geoDiff.onlyOneSideChanged) { - // If we're being squeezed to the point where it reaches less then our min size, then we drag the opposite separator, to preserve size - Anchor *anchorThatMoved = anchor(geoDiff); - Q_ASSERT(anchorThatMoved); - Anchor *anchorToMove = d->m_anchorGroup.oppositeAnchor(anchorThatMoved); - ensureMinSize(diffOrientation, anchorGroup().sideForAnchor(anchorToMove)); - } - } + return m_geometry.topLeft(); } -void Item::ensureMinSize(Qt::Orientation orientation, Anchor::Side side) +int Item::position(Qt::Orientation o) const { - if (isPlaceholder()) - return; - - const int delta = length(orientation) - minLength(orientation); - if (delta >= 0) // Our size is just fine - return; - - Anchor *anchorToMove = d->m_anchorGroup.anchorAtSide(side, orientation); - if (anchorToMove->isFollowing()) - anchorToMove = anchorToMove->endFollowee(); - - const bool movingSide1 = side == Anchor::Side1; // if true we're going to move left or top. - const int signess = movingSide1 ? 1 : -1; - const int newPosition = anchorToMove->position() + (delta * signess); - - // Note: Position can be slightly negative if the main window isn't big enougn to host the new size. - // In that case the window will be resized shortly after - Q_ASSERT(!anchorToMove->isFollowing()); - - // When dropping a MultiSplitter into a MultiSplitter there's an instant where some anchors of the group are from the source MultiSplitter, as they weren't consumed yet. - if (anchorToMove->parent() == parentWidget()) - anchorToMove->setPosition(newPosition); - - // Anchor::setPosition() will call Item::ensureMinSize_recursive() again on the next items -} - -void Item::ensureMinSize(Qt::Orientation orientation) -{ - if (isPlaceholder()) - return; - - const int minLength = this->minLength(orientation); - const int delta = length(orientation) - minLength; - if (delta >= 0) // Our size is just fine - return; - - const int newLength = minLength; - - Anchor *anchor1 = anchorGroup().anchorAtSide(Anchor::Side1, orientation); - Anchor *anchor2 = anchorGroup().anchorAtSide(Anchor::Side2, orientation); - - anchor1 = anchor1->isFollowing() ? anchor1->endFollowee() : anchor1; - anchor2 = anchor2->isFollowing() ? anchor2->endFollowee() : anchor2; - - const int bound1 = d->m_layout->boundPositionForAnchor(anchor1, Anchor::Side1); - const int bound2 = d->m_layout->boundPositionForAnchor(anchor2, Anchor::Side2); - - // If vertical, anchor1 is the left separator and anchor2 is the right one. We'll push anchor1 - // further left and anchor2 further right. - - const int suggestedDelta1 = qMin(delta, qCeil(delta / 2) + anchor1->thickness() + 1); - const int maxPos1 = bound2 - newLength - anchor1->thickness(); - const int newPosition1 = qMin(anchor1->position(), qMax(qMin(maxPos1, anchor1->position() - suggestedDelta1), bound1)); // Honour the bound - const int newPosition2 = newPosition1 + anchor1->thickness() + newLength; // No need to check bound2, we have enough space afterall - - if (!anchor1->isStatic()) - anchor1->setPosition(newPosition1); - if (!anchor2->isStatic()) - anchor2->setPosition(newPosition2); -} - -void Item::beginBlockPropagateGeo() -{ - Q_ASSERT(!d->m_blockPropagateGeo); - d->m_blockPropagateGeo = true; -} - -void Item::endBlockPropagateGeo() -{ - Q_ASSERT(d->m_blockPropagateGeo); - d->m_blockPropagateGeo = false; + return o == Qt::Vertical ? y() + : x(); } QRect Item::geometry() const { - return d->m_geometry; + return isBeingInserted() ? QRect() + : m_geometry; } -bool Item::eventFilter(QObject *o, QEvent *e) +bool Item::isContainer() const { - if (!d->m_layout) - return false; - - if (e->type() == QEvent::ParentChange && !d->m_layout->m_beingMergedIntoAnotherMultiSplitter) { - if (o->parent() != d->m_layout->multiSplitter()) { - // Frame was detached into a floating window - Q_ASSERT(!isPlaceholder()); - d->turnIntoPlaceholder(); - } - } else if (e->type() == QEvent::Show || e->type() == QEvent::Hide) { - //d->m_layout->emitVisibleWidgetCountChanged(); REMOVE - } - return false; + return m_isContainer; } -Frame *Item::frame() const +Qt::Orientation Item::orientation() const { - return d->m_frame; + return m_orientation; } -QWidgetOrQuick *Item::window() const +int Item::minLength(Qt::Orientation o) const { - Q_ASSERT(d->m_layout); - Q_ASSERT(d->m_layout->multiSplitter()); - return d->m_layout->multiSplitter()->window(); + return KDDockWidgets::length(minSize(), o); } -QWidgetOrQuick *Item::parentWidget() const +void Item::setLength(int length, Qt::Orientation o) { - return d->m_frame ? d->m_frame->parentWidget() - : nullptr; + Q_ASSERT(length > 0); + if (o == Qt::Vertical) + setSize({ width(), length }); + else + setSize({ length, height() }); } -MultiSplitterLayout *Item::layout() const +int Item::length(Qt::Orientation o) const { - return d->m_layout; + return KDDockWidgets::length(size(), o); } -void Item::setLayout(MultiSplitterLayout *m) +int Item::availableLength(Qt::Orientation o) const { - Q_ASSERT(m); - if (m != d->m_layout) { - d->m_layout = m; - d->m_anchorGroup.layout = m; - setParent(m); - if (d->m_frame) - d->m_frame->setParent(m->multiSplitter()); - } -} - -int Item::length(Qt::Orientation orientation) const -{ - return KDDockWidgets::widgetLength(this, orientation); -} - -int Item::minLength(Qt::Orientation orientation) const -{ - return lengthFromSize(minimumSize(), orientation); -} - -Anchor *Item::anchorAtSide(Anchor::Side side, Qt::Orientation orientation) const -{ - if (!d->m_anchorGroup.isValid()) - qWarning() << Q_FUNC_INFO << "Invalid anchor group" << &d->m_anchorGroup - << "in" << this << "; window=" << (parentWidget() ? parentWidget()->window() : nullptr); - - return d->m_anchorGroup.anchorAtSide(side, orientation); -} - -Anchor *Item::anchor(const GeometryDiff &geoDiff) const -{ - if (!geoDiff.onlyOneSideChanged) - return nullptr; - if (geoDiff.leftDiff) - return d->m_anchorGroup.left; - if (geoDiff.topDiff) - return d->m_anchorGroup.top; - if (geoDiff.bottomDiff) - return d->m_anchorGroup.bottom; - if (geoDiff.rightDiff) - return d->m_anchorGroup.right; - - Q_ASSERT(false); - return nullptr; -} - -AnchorGroup& Item::anchorGroup() -{ - return d->m_anchorGroup; -} - -const AnchorGroup &Item::anchorGroup() const -{ - return d->m_anchorGroup; -} - -QSize Item::minimumSize() const -{ - return isPlaceholder() ? QSize(0, 0) - : d->m_minSize; -} - -QSize Item::actualMinSize() const -{ - return d->m_minSize; + return length(o) - minLength(o); } bool Item::isPlaceholder() const { - return d->m_isPlaceholder; + return !m_isVisible; } -void Item::setIsPlaceholder(bool is) +bool Item::isVisible() const { - d->setIsPlaceholder(is); + return m_isVisible; } -bool Item::isInMainWindow() const +void Item::setIsVisible(bool is) { - if (MultiSplitterLayout *l = layout()) { - if (MultiSplitter *msw = l->multiSplitter()) { - return msw->isInMainWindow(); - } + if (is != m_isVisible) { + m_isVisible = is; + Q_EMIT minSizeChanged(this); // min-size is 0x0 when hidden + Q_EMIT visibleChanged(this, is); } +} + +void Item::setGeometry_recursive(QRect rect) +{ + // Recursiveness doesn't apply for for non-container items + setGeometry(rect); +} + +Item *Item::neighbour(Side side) const +{ + return m_parent ? m_parent->neighbourFor(this, side) + : nullptr; +} + +int Item::separatorThickness() +{ + return 5; +} + +bool Item::checkSanity() const +{ + if (minSize().width() > width() || minSize().height() > height()) { + qWarning() << Q_FUNC_INFO << "Size constraints not honoured" + << "; min=" << minSize() << "; size=" << size(); + return false; + } + + return true; +} + +void Item::setGeometry(QRect rect) +{ + if (rect != m_geometry) { + const QRect oldGeo = m_geometry; + m_geometry = rect; + + Q_ASSERT(rect.width() > 0); + Q_ASSERT(rect.height() > 0); + + Q_EMIT geometryChanged(); + + if (oldGeo.x() != x()) + Q_EMIT xChanged(); + if (oldGeo.y() != y()) + Q_EMIT yChanged(); + if (oldGeo.width() != width()) + Q_EMIT widthChanged(); + if (oldGeo.height() != height()) + Q_EMIT heightChanged(); + } +} + +void Item::dumpLayout(int level) +{ + QString indent; + indent.fill(QLatin1Char(' '), level); + const QString beingInserted = m_sizingInfo.isBeingInserted ? QStringLiteral(";beingInserted;") + : QString(); + const QString visible = !isVisible() ? QStringLiteral(";hidden;") + : QString(); + qDebug().noquote() << indent << "- Widget: " << objectName() << m_geometry << visible << beingInserted; +} + +Item::Item(ItemContainer *parent) + : QObject(parent) + , m_isContainer(false) + , m_parent(parent) +{ +} + +Item::Item(bool isContainer, ItemContainer *parent) + : QObject(parent) + , m_isContainer(isContainer) + , m_parent(parent) +{ +} + +bool Item::isRoot() const +{ + return m_parent == nullptr; +} + +bool Item::isVertical() const +{ + return m_orientation == Qt::Vertical; +} + +bool Item::isHorizontal() const +{ + return m_orientation == Qt::Horizontal; +} + +int Item::availableOnSide(Side, Qt::Orientation o) const +{ + if (isRoot()) + return 0; + + ItemContainer *container = parentContainer(); + + if (o == container->orientation()) { + + } else { + + } + return 0; +} + +ItemContainer::ItemContainer(ItemContainer *parent) + : Item(true, parent) +{ + connect(this, &Item::xChanged, this, [this] { + for (Item *item : qAsConst(m_children)) { + Q_EMIT item->xChanged(); + } + }); + + connect(this, &Item::yChanged, this, [this] { + for (Item *item : qAsConst(m_children)) { + Q_EMIT item->yChanged(); + } + }); +} + +bool ItemContainer::checkSanity() const +{ + if (!Item::checkSanity()) + return false; + + if (numChildren() == 0 && !isRoot()) { + qWarning() << Q_FUNC_INFO << "Container is empty. Should be deleted"; + return false; + } + + // Check that the geometries don't overlap + int expectedPos = KDDockWidgets::pos(pos(), m_orientation); + for (Item *item : m_children) { + const int pos = KDDockWidgets::pos(item->pos(), m_orientation); + if (expectedPos != pos) { + qWarning() << Q_FUNC_INFO << "Unexpected pos" << pos << "; expected=" << expectedPos + << "; for item=" << item + << "; isContainer=" << item->isContainer(); + return false; + } + + expectedPos = pos + KDDockWidgets::length(item->size(), m_orientation) + separatorThickness(); + } + + const int h1 = KDDockWidgets::length(size(), oppositeOrientation(m_orientation)); + for (Item *item : m_children) { + if (item->parentContainer() != this) { + qWarning() << "Invalid parent container for" << item + << "; is=" << item->parentContainer() << "; expected=" << this; + return false; + } + + if (item->parent() != this) { + qWarning() << "Invalid QObject parent for" << item + << "; is=" << item->parent() << "; expected=" << this; + return false; + } + + // Check the children height (if horizontal, and vice-versa) + const int h2 = KDDockWidgets::length(item->size(), oppositeOrientation(m_orientation)); + if (h1 != h2) { + qWarning() << Q_FUNC_INFO << "Invalid size for item." << item + << "Container.length=" << h1 << "; item.length=" << h2; + root()->dumpLayout(); + return false; + } + + if (!rect().contains(item->geometry())) { + qWarning() << Q_FUNC_INFO << "Item geo is out of bounds. item=" << item << "; geo=" + << item->geometry() << "; container.rect=" << rect(); + root()->dumpLayout(); + return false; + } + + if (!item->checkSanity()) + return false; + } + + return true; +} + +bool ItemContainer::hasOrientation() const +{ + return isVertical() || isHorizontal(); +} + +int ItemContainer::numChildren() const +{ + return m_children.size(); +} + +int ItemContainer::numVisibleChildren() const +{ + int num = 0; + for (Item *child : m_children) { + if (child->isVisible()) + num++; + } + return num; +} + +int ItemContainer::indexOfChild(const Item *item) const +{ + return m_children.indexOf(const_cast(item)); +} + +void ItemContainer::removeItem(Item *item) +{ + Q_ASSERT(!item->isRoot()); + if (contains(item)) { + Item *side1Item = visibleNeighbourFor(item, Side1); + Item *side2Item = visibleNeighbourFor(item, Side2); + const bool removed = m_children.removeOne(item); + Q_ASSERT(removed); + delete item; + m_childPercentages.clear(); + if (isEmpty() && !isRoot()) { + parentContainer()->removeItem(this); + } else { + // Neighbours will occupy the space of the deleted item + growNeighbours(side1Item, side2Item); + Q_EMIT itemsChanged(); + } + } else { + item->parentContainer()->removeItem(item); + } +} + +bool ItemContainer::isEmpty() const +{ + return m_children.isEmpty(); +} + +void ItemContainer::setGeometry_recursive(QRect rect) +{ + setPos(rect.topLeft()); + + // Call resize, which is recursive and will resize the children too + resize(rect.size()); +} + +ItemContainer *ItemContainer::convertChildToContainer(Item *leaf) +{ + const int index = indexOfChild(leaf); + Q_ASSERT(index != -1); + auto container = new ItemContainer(this); + insertItem(container, index, /*growItem=*/false); + m_children.removeOne(leaf); + container->setGeometry(leaf->geometry()); + container->insertItem(leaf, Location_OnTop, SizingOption::UseProvided); + Q_EMIT itemsChanged(); + + return container; +} + +void ItemContainer::insertItem(Item *item, Location loc, SizingOption sizingOption) +{ + item->setIsVisible(false); + + if (isRoot() && sizingOption == SizingOption::Calculate) + item->setGeometry(suggestedDropRect(item, nullptr, loc)); + + Q_ASSERT(item != this); + if (contains(item)) { + qWarning() << Q_FUNC_INFO << "Item already exists"; + return; + } + + const Qt::Orientation locOrientation = orientationForLocation(loc); + + if (hasOrientationFor(loc)) { + if (m_children.size() == 1) { + // 2 items is the minimum to know which orientation we're layedout + m_orientation = locOrientation; + } + + const int index = locationIsSide1(loc) ? 0 : m_children.size(); + insertItem(item, index); + } else { + // Inserting directly in a container ? Only if it's root. + Q_ASSERT(isRoot()); + auto container = new ItemContainer(this); + container->setChildren(m_children); + container->m_orientation = m_orientation; + m_children.clear(); + m_orientation = oppositeOrientation(m_orientation); + insertItem(container, 0, /*grow=*/ false); + container->setGeometry(rect()); + container->setIsVisible(container->numVisibleChildren() > 0); + + // Now we have the correct orientation, we can insert + insertItem(item, loc); + } +} + +void ItemContainer::onChildMinSizeChanged(Item *child) +{ + const QSize missingSize = this->missingSize(); + if (!missingSize.isNull()) { + QScopedValueRollback resizing(m_isResizing, true); + + if (isRoot()) { + // Resize the whole layout + resize(size() + missingSize); + Item::List childs = visibleChildren(); + Item *lastChild = nullptr; + for (int i = childs.size() - 1; i >= 0; i--) { + if (!childs[i]->isBeingInserted()) { + lastChild = childs[i]; + break; + } + } + + if (lastChild) { + QRect r = lastChild->geometry(); + r.adjust(0, 0, missingSize.width(), missingSize.height()); + lastChild->setGeometry(r); + } + } + + // Our min-size changed, notify our parent, and so on until it reaches root() + Q_EMIT minSizeChanged(this); + } + + if (numVisibleChildren() == 1) { + // The easy case. Child is alone in the layout, occupies everything. + child->setGeometry(rect()); + return; + } + + const QSize missingForChild = child->missingSize(); + if (missingForChild.isNull()) { + // The child changed its minSize. Thanks for letting us know, but there's nothing needed doing. + // Item is bigger than its minimum. + //Q_ASSERT(false); // Never happens I think. Remove this if! + return; + } + + // Child has some growing to do. It will grow left and right equally, (and top-bottom), as needed. + growItem(child, KDDockWidgets::length(missingForChild, m_orientation), GrowthStrategy::BothSidesEqually); +} + +void ItemContainer::onChildVisibleChanged(Item *child, bool visible) +{ + if (visible != isVisible()) { + if (visible) + setIsVisible(true); + else + setIsVisible(numVisibleChildren() > 0); + } + + if (!visible) + return; + + return; + + if (numVisibleChildren() == 1) { + // There's no separator when there's only 1 + return; + } + + // Shift the neighbours by 5px, to make space for the separator + const int available1 = availableOnSide(child, Side1); + const int available2 = availableOnSide(child, Side2); + int needed = Item::separatorThickness(); + + if (available1 > 0) { // The available squeeze on the left (or top) + Item *neighbour1 = visibleNeighbourFor(child, Side1); + Q_ASSERT(neighbour1); + const int side1Shift = qMin(needed, available1); + needed -= side1Shift; + + const QRect geo1 = adjustedRect(neighbour1->geometry(), m_orientation, 0, -side1Shift); + neighbour1->setGeometry_recursive(geo1); + } + + if (needed > 0) { // Squeeze on the right, if needed + Item *neighbour2 = visibleNeighbourFor(child, Side2); + Q_ASSERT(neighbour2); + Q_ASSERT(available2 >= needed); + const QRect geo2 = adjustedRect(neighbour2->geometry(), m_orientation, 0, -needed); + neighbour2->setGeometry_recursive(geo2); + } +} + +QRect ItemContainer::suggestedDropRect(Item *newItem, Item *relativeTo, Location loc) const +{ + Q_ASSERT(newItem); + if (relativeTo && !relativeTo->parentContainer()) { + qWarning() << Q_FUNC_INFO << "No parent container"; + return {}; + } + + if (relativeTo && relativeTo->parentContainer() != this) { + qWarning() << Q_FUNC_INFO << "Called on the wrong container"; + return {}; + } + + if (loc == Location_None) { + qWarning() << Q_FUNC_INFO << "Invalid location"; + return {}; + } + const int itemMin = newItem->minLength(m_orientation); + const int available = availableLength() - Item::separatorThickness(); + + if (relativeTo) { + const int equitativeLength = usableLength() / (m_children.size() + 1); + const int suggestedLength = qMax(qMin(available, equitativeLength), itemMin); + const int indexOfRelativeTo = indexOfChild(relativeTo); + + //int availableSide1 = 0; + //int min1 = 0; TODO + //int max2 = 0; + + //const int availableSide2 = availableOnSide(m_children.at(indexOfRelativeTo), Side2); + //const int relativeToPos = relativeTo->position(m_orientation); + if (locationIsSide1(loc)) { + if (indexOfRelativeTo > 0) { + //availableSide1 = availableOnSide(m_children.at(indexOfRelativeTo - 1), Side1); + // min1 = relativeToPos - availableSide1; TODO + // max2 = relativeToPos + availableSide2; + } + } else { + if (indexOfRelativeTo < m_children.size() - 1) { + //availableSide1 = availableOnSide(m_children.at(indexOfRelativeTo + 1), Side1); + // min1 = relativeToPos + relativeTo->length(m_orientation) - availableSide1; TODO + // max2 = relativeToPos + relativeTo->length(m_orientation) + availableSide2; + } + } + + QRect rect; + + if (orientationForLocation(loc) == Qt::Vertical) { + rect.setX(0); + rect.setSize(QSize(relativeTo->width(), suggestedLength)); + } else { + rect.setY(0); + rect.setSize(QSize(suggestedLength, relativeTo->height())); + } + + return rect; + + } else if (isRoot()) { + // Relative to the window itself + QRect rect = this->rect(); + const int oneThird = length() / 3; + const int suggestedLength = qMax(qMin(available, oneThird), itemMin); + + switch (loc) { + case Location_OnLeft: + rect.setWidth(suggestedLength); + break; + case Location_OnTop: + rect.setHeight(suggestedLength); + break; + case Location_OnRight: + rect.adjust(rect.width() - suggestedLength, 0, 0, 0); + break; + case Location_OnBottom: + rect.adjust(0, rect.bottom() - suggestedLength, 0, 0); + break; + case Location_None: + return {}; + } + + return rect; + + } else { + qWarning() << Q_FUNC_INFO << "Shouldn't happen"; + } + + return {}; +} + +void ItemContainer::positionItems() +{ + Item::List children = visibleChildren(); + int nextPos = 0; + const Qt::Orientation oppositeOrientation = KDDockWidgets::oppositeOrientation(m_orientation); + for (int i = 0; i < children.size(); ++i) { + Item *item = children.at(i); + + // If the layout is horizontal, the item will have the height of the container. And vice-versa + const int oppositeLength = KDDockWidgets::length(size(), oppositeOrientation); + item->setLength(oppositeLength, oppositeOrientation); + + // Update the pos + item->setPos(nextPos, m_orientation); + nextPos += item->length(m_orientation) + Item::separatorThickness(); + } + + m_childPercentages.clear(); +} + +void ItemContainer::insertItem(Item *item, int index, bool growItem) +{ + m_children.insert(index, item); + item->setParentContainer(this); + m_childPercentages.clear(); + Q_EMIT itemsChanged(); + + if (growItem) + restorePlaceholder(item); +} + +bool ItemContainer::hasChildren() const +{ + return !m_children.isEmpty(); +} + +bool ItemContainer::hasVisibleChildren() const +{ + for (Item *item : m_children) + if (item->isVisible()) + return true; return false; } -void Item::restorePlaceholder(DockWidgetBase *dockWidget, int tabIndex) +bool ItemContainer::hasOrientationFor(Location loc) const { - qCDebug(placeholder) << Q_FUNC_INFO << "Restoring to window=" << window(); - if (d->m_isPlaceholder) { - d->setFrame(Config::self().frameworkWidgetFactory()->createFrame(layout()->multiSplitter())); - d->m_frame->setGeometry(d->m_geometry); - } + if (m_children.size() <= 1) + return true; - if (tabIndex != -1 && d->m_frame->dockWidgetCount() >= tabIndex) { - d->m_frame->insertWidget(dockWidget, tabIndex); - } else { - d->m_frame->addWidget(dockWidget); - } - - if (d->m_isPlaceholder) { - // Resize Anchors to their correct places. - d->m_frame->setVisible(true); - d->setMinimumSize(d->frameMinSize()); - d->m_layout->restorePlaceholder(this); - d->setIsPlaceholder(false); - } + return m_orientation == orientationForLocation(loc); } -void Item::restorePlaceholder(Frame *frame) +Item::List ItemContainer::children() const { - Q_ASSERT(d->m_isPlaceholder); - - frame->setParent(layout()->multiSplitter()); - d->setFrame(frame); - d->m_frame->setGeometry(d->m_geometry); - d->m_layout->restorePlaceholder(this); - d->m_frame->setVisible(true); - d->setIsPlaceholder(false); + return m_children; } -void Item::onLayoutRequest() const +Item::List ItemContainer::visibleChildren() const { - if (!d->m_frame || d->m_isPlaceholder) - return; // It's a placeholder, nothing to do. - - if (LayoutSaver::restoreInProgress()) - return; // we don't even have the anchors yet, nothing to do - - if (d->m_frame->geometry() != geometry()) { - // The frame is controlled by the layout, it can't change its geometry on its own. - // Put it back. - d->m_frame->setGeometry(geometry()); + Item::List items; + items.reserve(m_children.size()); + for (Item *item : m_children) { + if (item->isVisible()) + items << item; } - if (d->m_layout->isAddingItem()) - return; // We're adding an item. Constraints will be updated at the *end* During is dangerous. - - const QSize minSize = d->frameMinSize().expandedTo(MultiSplitterLayout::hardcodedMinimumSize()); - if (minSize == d->m_minSize) - return; // Nothing to do - - const int deltaW = qMax(minSize.width() - d->m_minSize.width(), 0); - const int deltaH = qMax(minSize.height() - d->m_minSize.height(), 0); - - qCDebug(sizing) << Q_FUNC_INFO << "Updating minsize from" - << d->m_minSize << minSize << "for" << this; - d->setMinimumSize(minSize); - - if (deltaW == 0 && deltaH == 0) - return; // min size shrunk, nothing to do - - const QSize oldLayoutSize = d->m_layout->size(); - d->m_layout->updateSizeConstraints(); - const bool ranEnsureAnchorsBounded = oldLayoutSize != d->m_layout->size(); - - // setMinimumSize() and setSize() are no-ops if the size didn't change. So run ensureAnchorsBounded() - // ourselves, as the internal widgets changed their constraints - if (!ranEnsureAnchorsBounded) - d->m_layout->ensureAnchorsBounded(); - - if (width() < d->m_minSize.width() || height() < d->m_minSize.height()) { - // Shouldn't happen - qWarning() << Q_FUNC_INFO << "Constraints not honoured size=" << size() - << "; minSize=" << d->m_minSize - << "; layout.size=" << d->m_layout->size() - << "; layout.minSize=" << d->m_layout->minimumSize() - << "; this=" << this - << "; window=" << d->m_frame->window() - << d->m_layout->isAddingItem(); - } + return items; } -void Item::Private::setMinimumSize(QSize sz) +int ItemContainer::usableLength() const { - if (sz != m_minSize) { - m_minSize = sz; - Q_EMIT q->minimumSizeChanged(); - } + const int numVisibleChildren = this->numVisibleChildren(); + if (numVisibleChildren <= 1) + return KDDockWidgets::length(size(), m_orientation); + + const int separatorWaste = separatorThickness() * (numVisibleChildren - 1); + return length() - separatorWaste; } -void Item::restoreSizes(QSize minSize, QRect geometry) +bool ItemContainer::hasSingleVisibleItem() const { - d->m_minSize = minSize; - d->m_geometry = geometry; - if (d->m_frame) - d->m_frame->setGeometry(geometry); + return numVisibleChildren() == 1; } -void Item::Private::setFrame(Frame *frame) +bool ItemContainer::contains(Item *item) const { - Q_ASSERT((m_frame && !frame) || (!m_frame && frame)); + return m_children.contains(item); +} - if (m_frame) { - m_frame->removeEventFilter(q); - QObject::disconnect(m_onFrameDestroyed_connection); - QObject::disconnect(m_onFrameLayoutRequest_connection); - QObject::disconnect(m_onFrameObjectNameChanged_connection); - } +void ItemContainer::setChildren(const Item::List children) +{ + m_children = children; + for (Item *item : children) + item->setParentContainer(this); +} - m_frame = frame; - Q_EMIT q->frameChanged(); +QSize ItemContainer::minSize() const +{ + int minW = 0; + int minH = 0; - if (frame) { - q->onLayoutRequest(); - frame->setLayoutItem(q); - frame->installEventFilter(q); - // auto destruction - m_onFrameDestroyed_connection = q->connect(frame, &QObject::destroyed, q, [this] { - if (!m_layout) { - // Our parent (MultiSplitterLayout) is being destructed, and will delete this Item - // Nothing to do. - return; - } - - // Frame is being deleted, but perhaps the DockWidget was just made floating, so in this case - // we turn the item into a placeholder, so it remembers its previous place if we want to redock it. - if (m_refCount) { - // There's still KDDockWidgets which are floating and were here previously - turnIntoPlaceholder(); + if (!isEmpty()) { + const Item::List visibleChildren = this->visibleChildren(); + for (Item *item : visibleChildren) { + if (isVertical()) { + minW = qMax(minW, item->minSize().width()); + minH += item->minSize().height(); } else { - // Nope, nothing really needs this this Item, destroy it. - if (!m_destroying) { - m_destroying = true; - delete this; - } + minH = qMax(minH, item->minSize().height()); + minW += item->minSize().width(); } - }); + } - m_onFrameLayoutRequest_connection = connect(frame, &Frame::layoutInvalidated, q, &Item::onLayoutRequest); - m_onFrameObjectNameChanged_connection = connect(frame, &QObject::objectNameChanged, q, [this] { updateObjectName(); }); - updateObjectName(); + const int separatorWaste = (visibleChildren.size() - 1) * separatorThickness(); + if (isVertical()) + minH += separatorWaste; + else + minW += separatorWaste; } + + return { minW, minH }; } -void Item::ref() +QSize ItemContainer::maxSize() const { - d->m_refCount++; - qCDebug(placeholder()) << Q_FUNC_INFO << "; new ref=" << d->m_refCount; + int maxW = 0; + int maxH = 0; + + if (!isEmpty()) { + const Item::List visibleChildren = this->visibleChildren(); + for (Item *item : visibleChildren) { + if (isVertical()) { + maxW = qMin(maxW, item->maxSize().width()); + maxH += item->maxSize().height(); + } else { + maxH = qMin(maxH, item->maxSize().height()); + maxW += item->maxSize().width(); + } + } + + const int separatorWaste = (visibleChildren.size() - 1) * separatorThickness(); + if (isVertical()) + maxH += separatorWaste; + else + maxW += separatorWaste; + } + + return { maxW, maxH }; } -void Item::unref() +void ItemContainer::resize(QSize newSize) { - if (d->m_refCount == 0) { - qWarning() << Q_FUNC_INFO << "refcount can't be 0"; + const QSize minSize = this->minSize(); + if (newSize.width() < minSize.width() || newSize.height() < minSize.height()) { + qWarning() << Q_FUNC_INFO << "New size doesn't respect size constraints"; return; } - d->m_refCount--; - qCDebug(placeholder()) << Q_FUNC_INFO << "; new ref=" << d->m_refCount; + const bool widthChanged = width() != newSize.width(); + const bool heightChanged = height() != newSize.height(); + if (!widthChanged && !heightChanged) + return; - if (d->m_refCount == 0) { - if (!d->m_destroying) { - d->m_destroying = true; - delete this; + const bool lengthChanged = (isVertical() && heightChanged) || (isHorizontal() && widthChanged); + + if (m_childPercentages.isEmpty()) + updateChildPercentages(); + + setSize(newSize); + + if (m_isResizing) { + // We're already under a resize, nothing to do + return; + } + + const int totalNewLength = usableLength(); + int remaining = totalNewLength; + + int nextPos = 0; + for (int i = 0, count = m_children.size(); i < count; ++i) { + const bool isLast = i == count - 1; + + Item *item = m_children.at(i); + const qreal childPercentage = m_childPercentages.at(i); + const int newItemLength = lengthChanged ? (isLast ? remaining + : int(childPercentage * totalNewLength)) + : item->length(m_orientation); + item->setPos(nextPos, m_orientation); + nextPos += newItemLength + separatorThickness(); + remaining = remaining - newItemLength; + + if (isVertical()) { + item->resize({ width(), newItemLength }); + } else { + item->resize({ newItemLength, height() }); } } } -int Item::refCount() const +int ItemContainer::length() const { - return d->m_refCount; + return isVertical() ? height() : width(); } -void Item::Private::turnIntoPlaceholder() +QRect ItemContainer::rect() const { - qCDebug(placeholder) << Q_FUNC_INFO << this; - if (q->isPlaceholder()) + QRect rect = m_geometry; + rect.moveTo(QPoint(0, 0)); + return rect; +} + +void ItemContainer::dumpLayout(int level) +{ + QString indent; + indent.fill(QLatin1Char(' '), level); + const QString beingInserted = m_sizingInfo.isBeingInserted ? QStringLiteral("; beingInserted;") + : QString(); + qDebug().noquote() << indent << "* Layout: " << m_orientation << m_geometry << "; this=" + << this << beingInserted; + for (Item *item : qAsConst(m_children)) { + item->dumpLayout(level + 1); + } +} + +void ItemContainer::updateChildPercentages() +{ + m_childPercentages.clear(); + m_childPercentages.reserve(m_children.size()); + const int available = usableLength(); + for (Item *item : m_children) + m_childPercentages << (1.0 * item->length(m_orientation)) / available; +} + +void ItemContainer::restorePlaceholder(Item *item) +{ + Q_ASSERT(contains(item)); + + item->setBeingInserted(true); // TODO: Move into setIsVisible ? + item->setIsVisible(true); + item->setBeingInserted(false); + positionItems(); + + if (numVisibleChildren() == 1) return; - setFrame(nullptr); - setIsPlaceholder(true); + const int available = availableLength(); + const int maxItemLength = item->minLength(m_orientation) + available; + const int proposedItemLength = item->length(m_orientation); + const int newLength = proposedItemLength > maxItemLength ? maxItemLength + : proposedItemLength; + item->setLength(newLength, m_orientation); + growItem(item, newLength, GrowthStrategy::BothSidesEqually); +} - m_layout->clearAnchorsFollowing(); +Item *ItemContainer::visibleNeighbourFor(const Item *item, Side side) const +{ + const Item::List children = visibleChildren(); + const int index = children.indexOf(const_cast(item)); + const int neighbourIndex = side == Side1 ? index - 1 + : index + 1; - AnchorGroup anchorGroup = q->anchorGroup(); - auto layout = m_layout; // copy it, since we're deleting 'q', which deletes 'this' - if (anchorGroup.isValid()) { - layout->emitVisibleWidgetCountChanged(); + return (neighbourIndex >= 0 && neighbourIndex < children.size()) ? children.at(neighbourIndex) + : nullptr; +} + +Item *ItemContainer::neighbourFor(const Item *item, Side side) const +{ + const int index = indexOfChild(item); + const int neighbourIndex = side == Side1 ? index - 1 + : index + 1; + + return (neighbourIndex >= 0 && neighbourIndex < m_children.size()) ? m_children.at(neighbourIndex) + : nullptr; +} + +QSize ItemContainer::availableSize() const +{ + return size() - this->minSize(); +} + +int ItemContainer::availableLength() const +{ + return isVertical() ? availableSize().height() + : availableSize().width(); +} + +int ItemContainer::neighboursLengthFor(const Item *item, Side side, Qt::Orientation o) const +{ + const int index = indexOfChild(item); + if (index == -1) { + qWarning() << Q_FUNC_INFO << "Couldn't find item" << item; + return 0; + } + + if (o == m_orientation) { + int neighbourLength = 0; + int start = 0; + int end = -1; + if (side == Side1) { + start = 0; + end = index - 1; + } else { + start = index + 1; + end = m_children.size() - 1; + } + + for (int i = start; i <= end; ++i) + neighbourLength += m_children.at(i)->length(m_orientation); + + return neighbourLength; } else { - // Auto-destruction, which removes it from the layout - delete q; - } - - layout->updateAnchorFollowing(anchorGroup); -} - -void Item::Private::setIsPlaceholder(bool is) -{ - if (is != m_isPlaceholder) { - m_isPlaceholder = is; - Q_EMIT q->isPlaceholderChanged(); + // No neighbours in the other orientation. Each container is bidimensional. + return 0; } } -void Item::Private::updateObjectName() +int ItemContainer::neighboursLengthFor_recursive(const Item *item, Side side, Qt::Orientation o) const { - if (m_frame && !m_frame->objectName().isEmpty()) { - q->setObjectName(m_frame->objectName()); - } else if (q->isPlaceholder()) { - q->setObjectName(QStringLiteral("placeholder")); - } else if (!m_frame){ - q->setObjectName(QStringLiteral("null frame")); + return neighboursLengthFor(item, side, o) + (isRoot() ? 0 + : parentContainer()->neighboursLengthFor_recursive(this, side, o)); + +} + +int ItemContainer::neighboursMinLengthFor(const Item *item, Side side, Qt::Orientation o) const +{ + const int index = indexOfChild(item); + if (index == -1) { + qWarning() << Q_FUNC_INFO << "Couldn't find item" << item; + return 0; + } + + if (o == m_orientation) { + int neighbourMinLength = 0; + int start = 0; + int end = -1; + if (side == Side1) { + start = 0; + end = index - 1; + } else { + start = index + 1; + end = m_children.size() - 1; + } + + for (int i = start; i <= end; ++i) + neighbourMinLength += m_children.at(i)->minLength(m_orientation); + + return neighbourMinLength; } else { - q->setObjectName(QStringLiteral("frame with no dockwidgets")); + // No neighbours here + return 0; } } -Item *Item::deserialize(const LayoutSaver::Item &i, MultiSplitterLayout *layout) +int ItemContainer::neighboursMinLengthFor_recursive(const Item *item, Side side, Qt::Orientation o) const { - const bool hasFrame = !i.frame.isNull; - auto item = hasFrame ? new Item(Frame::deserialize(i.frame), layout) - : new Item(layout); - - if (hasFrame) { - // Show only after the frame is parented, so we don't flicker - item->frame()->show(); - } - - item->setIsPlaceholder(i.isPlaceholder); - item->setObjectName(i.objectName); - item->restoreSizes(i.minSize, i.geometry); - - item->setProperty("leftIndex", i.indexOfLeftAnchor); - item->setProperty("topIndex", i.indexOfTopAnchor); - item->setProperty("rightIndex", i.indexOfRightAnchor); - item->setProperty("bottomIndex", i.indexOfBottomAnchor); - - return item; + return neighboursMinLengthFor(item, side, o) + (isRoot() ? 0 + : parentContainer()->neighboursMinLengthFor(this, side, o)); } -LayoutSaver::Item Item::serialize() const +int ItemContainer::neighbourSeparatorWaste(const Item *item, Side side, Qt::Orientation o) const { - LayoutSaver::Item item; + const int index = indexOfChild(item); + if (index == -1) { + qWarning() << Q_FUNC_INFO << "Couldn't find item" << item; + return 0; + } - item.objectName = objectName(); - item.isPlaceholder = isPlaceholder(); - item.geometry = geometry(); - item.minSize = actualMinSize(); - - const Anchor::List allAnchors = layout()->anchors(); - item.indexOfLeftAnchor =allAnchors.indexOf(anchorGroup().left); - item.indexOfTopAnchor = allAnchors.indexOf(anchorGroup().top); - item.indexOfRightAnchor = allAnchors.indexOf(anchorGroup().right); - item.indexOfBottomAnchor = allAnchors.indexOf(anchorGroup().bottom); - - auto frame = this->frame(); - if (frame) { - item.frame = frame->serialize(); + if (o == m_orientation) { + if (side == Side1) { + return index * Item::separatorThickness(); + } else { + return (m_children.size() - 1 - index) * Item::separatorThickness(); + } } else { - item.frame.isNull = true; + return 0; + } +} + +int ItemContainer::neighbourSeparatorWaste_recursive(const Item *item, Side side, Qt::Orientation orientation) const +{ + return neighbourSeparatorWaste(item, side, orientation) + (isRoot() ? 0 + : parentContainer()->neighbourSeparatorWaste(item, side, orientation)); +} + +int ItemContainer::availableOnSide(Item *child, Side side) const +{ + const int length = neighboursLengthFor(child, side, m_orientation); + const int min = neighboursMinLengthFor(child, side, m_orientation); + + const int available = length - min; + if (available < 0) { + root()->dumpLayout(); + Q_ASSERT(false); + } + return available; +} + +QSize ItemContainer::missingSizeFor(Item *item, Qt::Orientation o) const +{ + QSize missing = {0, 0}; + const QSize available = availableSize(); + const int anchorWasteW = (o == Qt::Vertical || !hasVisibleChildren()) ? 0 : Item::separatorThickness(); + const int anchorWasteH = (o == Qt::Vertical && hasVisibleChildren()) ? Item::separatorThickness() : 0; + missing.setWidth(qMax(item->minSize().width() - available.width() + anchorWasteW, 0)); + missing.setHeight(qMax(item->minSize().height() - available.height() + anchorWasteH, 0)); + + return missing; +} + +QVariantList ItemContainer::items() const +{ + QVariantList items; + items.reserve(m_children.size()); + + for (auto item : m_children) + items << QVariant::fromValue(item); + + return items; +} + +void ItemContainer::growNeighbours(Item *side1Neighbour, Item *side2Neighbour) +{ + if (!side1Neighbour && !side2Neighbour) + return; + + if (side1Neighbour && side2Neighbour) { + // Give half/half to each neighbour + QRect geo1 = side1Neighbour->geometry(); + QRect geo2 = side2Neighbour->geometry(); + + if (isVertical()) { + const int available = geo2.y() - geo1.bottom() - separatorThickness(); + geo1.setHeight(geo1.height() + available / 2); + geo2.setTop(geo1.bottom() + separatorThickness() + 1); + } else { + const int available = geo2.x() - geo1.right() - separatorThickness(); + geo1.setWidth(geo1.width() + available / 2); + geo2.setLeft(geo1.right() + separatorThickness() + 1); + } + + side1Neighbour->setGeometry_recursive(geo1); + side2Neighbour->setGeometry_recursive(geo2); + + } else if (side1Neighbour) { + // Grow all the way to the right (or bottom if vertical) + QRect geo = side1Neighbour->geometry(); + + if (isVertical()) { + geo.setBottom(rect().bottom()); + } else { + geo.setRight(rect().right()); + } + + side1Neighbour->setGeometry_recursive(geo); + + } else if (side2Neighbour) { + // Grow all the way to the left (or top if vertical) + QRect geo = side2Neighbour->geometry(); + + if (isVertical()) { + geo.setTop(0); + } else { + geo.setLeft(0); + } + + side2Neighbour->setGeometry_recursive(geo); + } +} + +void ItemContainer::growItem(Item *item, int amount, GrowthStrategy growthStrategy) +{ + if (amount == 0) + return; + + Q_ASSERT(growthStrategy == GrowthStrategy::BothSidesEqually); + const Item::List visibleItems = visibleChildren(); + const int index = visibleItems.indexOf(item); + + if (visibleItems.size() == 1) { + Q_ASSERT(index != -1); + //There's no neighbours to push, we're alone. Occupy the full container + item->setLength(item->length(m_orientation) + amount, m_orientation); + positionItems(); + return; } - return item; + const int available1 = availableOnSide(item, Side1); + const int available2 = availableOnSide(item, Side2); + const int neededLength = amount; + + int min1 = 0; + int max2 = length() - 1; + int newPosition = 0; + int side1Growth = 0; + + Item *side1Neighbour = index > 0 ? visibleItems.at(index - 1) + : nullptr; + + if (side1Neighbour) { + Item *side1Neighbour = visibleItems.at(index - 1); + min1 = side1Neighbour->position(m_orientation) + side1Neighbour->length(m_orientation) - available1; + newPosition = side1Neighbour->position(m_orientation) + side1Neighbour->length(m_orientation) - (amount / 2); + } + + if (index < visibleItems.size() - 1) { + max2 = visibleItems.at(index + 1)->position(m_orientation) + available2; + } + + // Now bound the position + if (newPosition < min1) { + newPosition = min1; + } else if (newPosition + neededLength > max2) { + newPosition = max2 - neededLength - Item::separatorThickness() + 1; + } + + if (newPosition > 0) { + side1Growth = side1Neighbour->position(m_orientation) + side1Neighbour->length(m_orientation) - newPosition; + } + + const int side2Growth = neededLength - side1Growth + Item::separatorThickness(); + growItem(item, side1Growth, side2Growth); +} + +QVector ItemContainer::availableLengthPerNeighbour(Item *item, Side side) const +{ + Item::List children = visibleChildren(); + const int indexOfChild = children.indexOf(item); + + int start = 0; + int end = 0; + if (side == Side1) { + start = 0; + end = indexOfChild - 1; + } else { + start = indexOfChild + 1; + end = children.size() - 1; + } + + QVector result; + result.reserve(end - start + 1); + for (int i = start; i <= end; ++i) { + Item *neighbour = children.at(i); + result << neighbour->availableLength(m_orientation); + } + + return result; +} + +/** static */ +QVector ItemContainer::calculateSqueezes(QVector availabilities, int needed) +{ + QVector squeezes(availabilities.size(), 0); + int missing = needed; + while (missing > 0) { + const int numDonors = std::count_if(availabilities.cbegin(), availabilities.cend(), [] (int num) { + return num > 0; + }); + + if (numDonors == 0) { + Q_ASSERT(false); + return {}; + } + + int toTake = missing / numDonors; + if (toTake == 0) + toTake = missing; + + for (int i = 0; i < availabilities.size(); ++i) { + const int available = availabilities.at(i); + if (available == 0) + continue; + const int took = qMin(toTake, availabilities.at(i)); + availabilities[i] -= took; + missing -= took; + squeezes[i] += took; + if (missing == 0) + break; + } + } + + return squeezes; +} + +void ItemContainer::growItem(Item *child, int side1Growth, int side2Growth) +{ + Q_ASSERT(side1Growth > 0 || side2Growth > 0); + + Item::List children = visibleChildren(); + + if (side1Growth > 0) { + const QVector availables = availableLengthPerNeighbour(child, Side1); + const QVector squeezes = calculateSqueezes(availables, side1Growth); + for (int i = 0; i < squeezes.size(); ++i) { + const int squeeze = squeezes.at(i); + Item *neighbour = children[i]; + neighbour->setGeometry_recursive(adjustedRect(neighbour->geometry(), m_orientation, 0, -squeeze)); + } + } + + if (side2Growth > 0) { + const QVector availables = availableLengthPerNeighbour(child, Side2); + const int itemIndex = children.indexOf(child); + const QVector squeezes = calculateSqueezes(availables, side2Growth); + for (int i = 0; i < squeezes.size(); ++i) { + const int squeeze = squeezes.at(i); + Item *neighbour = children[i + itemIndex + 1]; + neighbour->setGeometry_recursive(adjustedRect(neighbour->geometry(), m_orientation, squeeze, 0)); + } + } + + positionItems(); } diff --git a/src/private/multisplitter/Item_p.h b/src/private/multisplitter/Item_p.h index 554f9229..c2fdc85c 100644 --- a/src/private/multisplitter/Item_p.h +++ b/src/private/multisplitter/Item_p.h @@ -1,7 +1,7 @@ /* This file is part of KDDockWidgets. - Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + Copyright (C) 2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: Sérgio Martins This program is free software; you can redistribute it and/or modify @@ -18,177 +18,332 @@ along with this program. If not, see . */ -#ifndef KD_MULTISPLITTER_ITEM_P_H -#define KD_MULTISPLITTER_ITEM_P_H +#pragma once -#include "docks_export.h" -#include "Anchor_p.h" -#include "QWidgetAdapter.h" -#include "LayoutSaver_p.h" - -#include #include +#include +#include +#include +#include // TODO: remove + +#include + +class TestMultiSplitter; -/** - * @brief Implements an item that you put into a multi-splitter. - * For now it just wraps a KDDockWidgets::Frame, but could eventually be used in QML. - */ namespace KDDockWidgets { -struct AnchorGroup; -class MultiSplitterLayout; -class Frame; -class DockWidgetBase; -class TestDocks; - -struct GeometryDiff -{ - explicit GeometryDiff(QRect oldGeo, QRect newGeo) - : leftDiff(newGeo.left() - oldGeo.left()) - , topDiff(newGeo.top() - oldGeo.top()) - , rightDiff(newGeo.right() - oldGeo.right()) - , bottomDiff(newGeo.bottom() - oldGeo.bottom()) - , onlyOneSideChanged([this]{ - int numChanged = 0; - if (leftDiff != 0) - numChanged++; - if (topDiff != 0) - numChanged++; - if (rightDiff != 0) - numChanged++; - if (bottomDiff != 0) - numChanged++; - return numChanged == 1; - }()) // Lambda just so we can have onlyOneChanged as const - { - } - - // Orientation of the Anchor that provoked the geometry diff - Qt::Orientation orientation() const - { - if (leftDiff || rightDiff) - return Qt::Vertical; - - return Qt::Horizontal; - } - - int delta() const - { - // Since we only use GeometryDiff when only 1 side changed, just sum them all - return leftDiff + rightDiff + topDiff + bottomDiff; - } - - int signess() const - { - return delta() > 0 ? 1: -1; - } - - const int leftDiff; - const int topDiff; - const int rightDiff; - const int bottomDiff; - const bool onlyOneSideChanged; +enum Location { + Location_None, + Location_OnLeft, ///> Left docking location + Location_OnTop, ///> Top docking location + Location_OnRight, ///> Right docking location + Location_OnBottom ///> Bottom docking location }; -class DOCKS_EXPORT_FOR_UNIT_TESTS Item : public QObject // clazy:exclude=ctor-missing-parent-argument +///@internal +inline Location oppositeLocation(Location loc) +{ + switch (loc) { + case Location_OnLeft: + return Location_OnRight; + case Location_OnTop: + return Location_OnBottom; + case Location_OnRight: + return Location_OnLeft; + case Location_OnBottom: + return Location_OnTop; + default: + Q_ASSERT(false); + return Location_None; + } +} + +///@internal +inline Location adjacentLocation(Location loc) +{ + switch (loc) { + case Location_OnLeft: + return Location_OnTop; + case Location_OnTop: + return Location_OnRight; + case Location_OnRight: + return Location_OnBottom; + case Location_OnBottom: + return Location_OnLeft; + default: + Q_ASSERT(false); + return Location_None; + } +} + +///@internal +inline QString locationStr(Location loc) +{ + switch (loc) { + case KDDockWidgets::Location_None: + return QStringLiteral("none"); + case KDDockWidgets::Location_OnLeft: + return QStringLiteral("left"); + case KDDockWidgets::Location_OnTop: + return QStringLiteral("top"); + case KDDockWidgets::Location_OnRight: + return QStringLiteral("right"); + case KDDockWidgets::Location_OnBottom: + return QStringLiteral("bottom"); + } + + return QString(); +} + +enum Side { + Side1, + Side2 +}; + +enum class GrowthStrategy { + BothSidesEqually +}; + +enum class SizingOption { + Calculate, + UseProvided +}; + +inline Qt::Orientation oppositeOrientation(Qt::Orientation o) { + return o == Qt::Vertical ? Qt::Horizontal + : Qt::Vertical; +} + +inline int pos(QPoint p, Qt::Orientation o) { + return o == Qt::Vertical ? p.y() + : p.x(); +} + +inline int length(QSize sz, Qt::Orientation o) { + return o == Qt::Vertical ? sz.height() + : sz.width(); +} + +inline bool locationIsVertical(Location loc) +{ + return loc == Location_OnTop || loc == Location_OnBottom; +} + +inline bool locationIsHorizontal(Location loc) +{ + return !locationIsVertical(loc); +} + +inline bool locationIsSide1(Location loc) +{ + return loc == Location_OnLeft || loc == Location_OnTop; +} + +inline bool locationIsSide2(Location loc) +{ + return loc == Location_OnRight || loc == Location_OnBottom; +} + +inline QRect adjustedRect(QRect r, Qt::Orientation o, int p1, int p2) +{ + if (o == Qt::Vertical) { + r.adjust(0, p1, 0, p2); + } else { + r.adjust(p1, 0, p2, 0); + } + + return r; +} + +inline Qt::Orientation orientationForLocation(Location loc) +{ + switch (loc) { + case Location_OnLeft: + case Location_OnRight: + return Qt::Horizontal; + case Location_None: + case Location_OnTop: + case Location_OnBottom: + return Qt::Vertical; + } + + return Qt::Vertical; +} + +struct SizingInfo { + QSize minSize = QSize(40, 40); // TODO: Hardcoded + QSize maxSize = QSize(16777215, 16777215); // TODO: Not supported yet + QSize proposedSize; + bool isBeingInserted = false; +}; + +class ItemContainer; + +class Item : public QObject { Q_OBJECT - Q_PROPERTY(bool isPlaceholder READ isPlaceholder NOTIFY isPlaceholderChanged) + Q_PROPERTY(int x READ x NOTIFY xChanged) + Q_PROPERTY(int y READ y NOTIFY yChanged) + Q_PROPERTY(int width READ width NOTIFY widthChanged) + Q_PROPERTY(int height READ height NOTIFY heightChanged) Q_PROPERTY(QRect geometry READ geometry NOTIFY geometryChanged) - Q_PROPERTY(QSize minimumSize READ minimumSize NOTIFY minimumSizeChanged) + Q_PROPERTY(bool isContainer READ isContainer CONSTANT) public: + typedef QVector List; - /// @brief constructs a new layout item to show @p Frame in the layout @layout - /// @param frame This is never nullptr. - /// @param layout This is never nullptr. - explicit Item(Frame *frame, MultiSplitterLayout *layout); + explicit Item(ItemContainer *parent = nullptr); - /// @brief Constructor overload used when restoring a layout and the Item is a placeholder (no frame) - explicit Item(MultiSplitterLayout *layout); + bool isRoot() const; - /// @brief Destroys its frame too. - ~Item() override; + bool isVertical() const; + bool isHorizontal() const; - static Item* deserialize(const LayoutSaver::Item &, MultiSplitterLayout *layout); - LayoutSaver::Item serialize() const; + virtual void insertItem(Item *item, Location, SizingOption = SizingOption::Calculate); int x() const; int y() const; - QPoint pos() const; - int position(Qt::Orientation) const; - QSize size() const; int width() const; int height() const; - bool isVisible() const; - void setVisible(bool); - - void setGeometry(QRect); - void ensureMinSize(Qt::Orientation orientation, Anchor::Side); - void ensureMinSize(Qt::Orientation orientation); - - void beginBlockPropagateGeo(); - void endBlockPropagateGeo(); - + QSize size() const; + void setSize(QSize); + QPoint pos() const; + int pos(Qt::Orientation) const; QRect geometry() const; - bool eventFilter(QObject *, QEvent *) override; - Frame* frame() const; - QWidgetOrQuick *window() const; - QWidgetOrQuick *parentWidget() const; + bool isContainer() const; + bool isWidget() const { return !isContainer(); } - MultiSplitterLayout *layout() const; - void setLayout(MultiSplitterLayout *w); // TODO: Make the widget children of this one? - - /** - * Returns the width of the widget if orientation is Vertical, the height otherwise. - */ + Qt::Orientation orientation() const; + static int separatorThickness(); + virtual bool checkSanity() const; + void setParentContainer(ItemContainer *parent); // TODO: Make private + ItemContainer *parentContainer() const; + void setPos(QPoint); // TODO: Make private + void setPos(int pos, Qt::Orientation); + int position(Qt::Orientation) const; + const ItemContainer *asContainer() const; + ItemContainer *asContainer(); + void setMinSize(QSize); + void setMaxSize(QSize); + virtual QSize minSize() const; + virtual QSize maxSize() const; + virtual void resize(QSize newSize); + int minLength(Qt::Orientation) const; + void setLength(int length, Qt::Orientation); int length(Qt::Orientation) const; - int minLength(Qt::Orientation orientation) const; - - Anchor *anchorAtSide(Anchor::Side side, Qt::Orientation orientation) const; - Anchor *anchor(const GeometryDiff &) const; - AnchorGroup& anchorGroup(); - const AnchorGroup& anchorGroup() const; - - QSize minimumSize() const; - + int availableLength(Qt::Orientation) const; bool isPlaceholder() const; - void setIsPlaceholder(bool); - /** - * @brief Returns whether this item lives in a @ref MainWindow, as opposed to a @ref FloatingWindow - */ - bool isInMainWindow() const; - ///@brief turns the placeholder into a normal Item again showing @p dockWidget - void restorePlaceholder(DockWidgetBase *dockWidget, int tabIndex); + void ref() {} + void unref() {} - ///@brief turns the placeholder into a normal item again - /// This overload is called when the Frame has more than 1 tab, otherwise we just use the DockWidget overload - void restorePlaceholder(Frame *frame); + bool isVisible() const; + void setIsVisible(bool); + virtual void setGeometry_recursive(QRect rect); + Item* neighbour(Side) const; - /** - * @brief Checks if the minSize is correct. - * The parent widget got a QEvent::LayoutRequest, so the Frame might have changed its constraints. - */ - void onLayoutRequest() const; + virtual void dumpLayout(int level = 0); + void setGeometry(QRect rect); + SizingInfo m_sizingInfo; + int availableOnSide(Side, Qt::Orientation) const; + QSize missingSize() const; + bool isBeingInserted() const; + void setBeingInserted(bool); + ItemContainer *root() const; + + QWidget *frame() const { return m_widget; } // TODO: rename + void setFrame(QWidget *w) { m_widget = w; } // TODO rename + QWidget *window() const { + return m_widget ? m_widget->window() + : nullptr; + } - void ref(); - void unref(); - int refCount() const; // for tests Q_SIGNALS: - void frameChanged(); void geometryChanged(); - void isPlaceholderChanged(); - void minimumSizeChanged(); -private: - friend KDDockWidgets::TestDocks; - QSize actualMinSize() const; // The min size, regardless if it's a placeholder or not, so we can save the actual value while LayoutSaver::saveLayout - void restoreSizes(QSize minSize, QRect geometry); // Just for LayoutSaver::restore + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void visibleChanged(Item *thisItem, bool visible); + void minSizeChanged(Item *thisItem); +protected: + friend class ::TestMultiSplitter; + explicit Item(bool isContainer, ItemContainer *parent); + const bool m_isContainer; + Qt::Orientation m_orientation = Qt::Vertical; - class Private; - Private *const d; + ItemContainer *m_parent = nullptr; + QRect m_geometry; +private: + bool m_isVisible = false; + QWidget *m_widget = nullptr; // TODO: Make generic +}; + +class ItemContainer : public Item { + Q_OBJECT + Q_PROPERTY(QVariantList items READ items NOTIFY itemsChanged) +public: + explicit ItemContainer(ItemContainer *parent = nullptr); + void insertItem(Item *item, int index, bool growItem = true); + bool checkSanity() const override; + bool hasOrientation() const; + int numChildren() const; + int numVisibleChildren() const; + bool hasChildren() const; + bool hasVisibleChildren() const; + int indexOfChild(const Item *) const; + void removeItem(Item *); + bool isEmpty() const; + void setGeometry_recursive(QRect rect) override; + + ItemContainer *convertChildToContainer(Item *leaf); + void insertItem(Item *item, Location, SizingOption = SizingOption::Calculate) override; + bool hasOrientationFor(Location) const; + Item::List children() const; + Item::List visibleChildren() const; + int usableLength() const; + bool hasSingleVisibleItem() const; + bool contains(Item *item) const; + void setChildren(const Item::List children); + QSize minSize() const override; + QSize maxSize() const override; + void resize(QSize newSize) override; + int length() const; + QRect rect() const; + QVariantList items() const; + void dumpLayout(int level = 0); + void updateChildPercentages(); + void restorePlaceholder(Item *); + void growNeighbours(Item *side1Neighbour, Item *side2Neighbour); + void growItem(Item *, int amount, GrowthStrategy); + void growItem(Item *, int side1Growth, int side2Growth); + Item *neighbourFor(const Item *, Side) const; + Item *visibleNeighbourFor(const Item *item, Side side) const; + QSize availableSize() const; + int availableLength() const; + int neighboursLengthFor(const Item *item, Side, Qt::Orientation) const; + int neighboursLengthFor_recursive(const Item *item, Side, Qt::Orientation) const; + int neighboursMinLengthFor(const Item *item, Side, Qt::Orientation) const; + int neighboursMinLengthFor_recursive(const Item *item, Side, Qt::Orientation) const; + int neighbourSeparatorWaste(const Item *item, Side, Qt::Orientation) const; + int neighbourSeparatorWaste_recursive(const Item *item, Side, Qt::Orientation) const; + int availableOnSide(Item *child, Side) const; + QSize missingSizeFor(Item *item, Qt::Orientation) const; + void onChildMinSizeChanged(Item *child); + void onChildVisibleChanged(Item *child, bool visible); + QVector availableLengthPerNeighbour(Item *item, Side) const; + static QVector calculateSqueezes(QVector availabilities, int needed); + QRect suggestedDropRect(Item *newItem, Item *relativeTo, Location) const; + void positionItems(); + bool isResizing() const { return m_isResizing; } +Q_SIGNALS: + void itemsChanged(); +public: + QVector m_childPercentages; + Item::List m_children; + bool m_isResizing = false; }; } -#endif +Q_DECLARE_METATYPE(KDDockWidgets::Location) diff --git a/src/private/multisplitter/MultiSplitterLayout.cpp b/src/private/multisplitter/MultiSplitterLayout.cpp index 120de9ff..c30fa2be 100644 --- a/src/private/multisplitter/MultiSplitterLayout.cpp +++ b/src/private/multisplitter/MultiSplitterLayout.cpp @@ -44,20 +44,11 @@ using namespace KDDockWidgets; const QString MultiSplitterLayout::s_magicMarker = QStringLiteral("bac9948e-5f1b-4271-acc5-07f1708e2611"); -static Qt::Orientation anchorOrientationForLocation(Location l) -{ - return (l == Location_OnLeft || l == Location_OnRight) ? Qt::Vertical - : Qt::Horizontal; -} MultiSplitterLayout::MultiSplitterLayout(MultiSplitter *parent) : QObject(parent) , m_multiSplitter(parent) - , m_leftAnchor(new Anchor(Qt::Vertical, this, Anchor::Type_LeftStatic)) - , m_topAnchor(new Anchor(Qt::Horizontal, this, Anchor::Type_TopStatic)) - , m_rightAnchor(new Anchor(Qt::Vertical, this, Anchor::Type_RightStatic)) - , m_bottomAnchor(new Anchor(Qt::Horizontal, this, Anchor::Type_BottomStatic)) - , m_staticAnchorGroup(this) + , m_rootItem(new Item()) { Q_ASSERT(parent); @@ -70,30 +61,8 @@ MultiSplitterLayout::MultiSplitterLayout(MultiSplitter *parent) Q_EMIT visibleWidgetCountChanged(visibleCount()); }); - m_leftAnchor->setObjectName(QStringLiteral("left")); - m_rightAnchor->setObjectName(QStringLiteral("right")); - m_bottomAnchor->setObjectName(QStringLiteral("bottom")); - m_topAnchor->setObjectName(QStringLiteral("top")); - - m_leftAnchor->setFrom(m_topAnchor); - m_leftAnchor->setTo(m_bottomAnchor); - m_rightAnchor->setFrom(m_topAnchor); - m_rightAnchor->setTo(m_bottomAnchor); - - m_topAnchor->setFrom(m_leftAnchor); - m_topAnchor->setTo(m_rightAnchor); - m_bottomAnchor->setFrom(m_leftAnchor); - m_bottomAnchor->setTo(m_rightAnchor); - - m_staticAnchorGroup.left = m_leftAnchor; - m_staticAnchorGroup.right = m_rightAnchor; - m_staticAnchorGroup.top = m_topAnchor; - m_staticAnchorGroup.bottom = m_bottomAnchor; - clear(); - positionStaticAnchors(); - // Initialize min size updateSizeConstraints(); m_inCtor = false; @@ -105,6 +74,7 @@ MultiSplitterLayout::~MultiSplitterLayout() m_inDestructor = true; const auto anchors = m_anchors; qDeleteAll(anchors); + delete m_rootItem; DockRegistry::self()->unregisterLayout(this); } @@ -230,7 +200,7 @@ void MultiSplitterLayout::addWidget(QWidgetOrQuick *w, Location location, Frame << "; relativeTo=" << relativeToWidget << "; size=" << size() << "; w.size=" << w->size() - << "; w.min=" << KDDockWidgets::widgetMinLength(w, anchorOrientationForLocation(location)) + << "; w.min=" << widgetMinLength(w, orientationForLocation(location)) << "; frame=" << frame << "; option=" << option; @@ -246,157 +216,8 @@ void MultiSplitterLayout::addWidget(QWidgetOrQuick *w, Location location, Frame return; unrefOldPlaceholders(framesFrom(w)); + //Item *relativeToItem = itemForFrame(relativeToWidget); - Item *relativeToItem = itemForFrame(relativeToWidget); - - ensureEnoughSize(w, location, relativeToItem); - - if (option & AddingOption_StartHidden) { - addAsPlaceholder(qobject_cast(w), location, relativeToItem); - return; - } - - Anchor *newAnchor = nullptr; - const QRect dropRect = rectForDrop(w, location, relativeToItem); - - if (dropRect.size().isNull() || dropRect.x() < 0 || dropRect.y() < 0) { - qWarning() << Q_FUNC_INFO << "Invalid drop rect" << dropRect - << "\n size=" << m_multiSplitter->size() << "; size="<< m_size - << "\n location=" << location - << "\n window=" << m_multiSplitter->window() - << "\n this=" << this - << "\n availableHeight=" << availableLengthForOrientation(Qt::Horizontal) - << "\n availableWidth=" << availableLengthForOrientation(Qt::Vertical) - << "\n widget.minSize=" << widgetMinLength(w, anchorOrientationForLocation(location)); - return; - } - - m_addingItem = true; - - auto result = this->createTargetAnchorGroup(location, relativeToItem); - AnchorGroup targetAnchorGroup = result.first; - newAnchor = result.second; - - if (newAnchor && !newAnchor->isFollowing()) { - const int anchorThickness = Anchor::thickness(/*static=*/false); - qCDebug(sizing) << "Drop rect" << dropRect; - - int posForExistingAnchor = 0; - int posForNewAnchor = 0; - Anchor *existingAnchor = targetAnchorGroup.anchor(location); - Anchor *direction1Anchor = nullptr; - Anchor *direction2Anchor = nullptr; - - switch (location) { - case Location_OnLeft: - posForExistingAnchor = dropRect.left() - existingAnchor->thickness(); - posForNewAnchor = dropRect.right() + 1; - break; - case Location_OnTop: - posForExistingAnchor = dropRect.top() - existingAnchor->thickness(); - posForNewAnchor = dropRect.bottom() + 1; - break; - case Location_OnBottom: - posForExistingAnchor = dropRect.bottom() + 1; - posForNewAnchor = dropRect.top() - anchorThickness; - break; - case Location_OnRight: - posForExistingAnchor = dropRect.right() + 1; - posForNewAnchor = dropRect.left() - anchorThickness; - break; - case Location_None: - Q_ASSERT(false); - break; - } - - int delta1 = 0; - int delta2 = 0; - const int originalExistingAnchorPos = existingAnchor->position(); - - switch (location) { - case Location_OnLeft: - case Location_OnTop: - direction1Anchor = existingAnchor; - direction2Anchor = newAnchor; - std::tie(posForExistingAnchor, posForNewAnchor) = boundInterval(posForExistingAnchor, existingAnchor, posForNewAnchor, newAnchor); - delta1 = originalExistingAnchorPos - posForExistingAnchor; - delta2 = posForNewAnchor - posForExistingAnchor; - break; - case Location_OnRight: - case Location_OnBottom: - direction1Anchor = newAnchor; - direction2Anchor = existingAnchor; - std::tie(posForNewAnchor, posForExistingAnchor) = boundInterval(posForNewAnchor, newAnchor, posForExistingAnchor, existingAnchor); - delta1 = posForExistingAnchor - posForNewAnchor; - delta2 = posForExistingAnchor - originalExistingAnchorPos; - break; - case Location_None: - qWarning() << Q_FUNC_INFO << "Location can't be none"; - return; - } - - newAnchor->setPosition(posForNewAnchor); - - if (posForExistingAnchor != originalExistingAnchorPos) { - if (existingAnchor->isStatic()) { - qWarning() << "Trying to move static anchor from" << originalExistingAnchorPos << "to" - << posForExistingAnchor << "; location=" << location - << "; dropRect=" << dropRect - << "; existingAnchor=" << existingAnchor - << "; size=" << m_size - << "; Qt::WA_PendingResizeEvent=" << m_multiSplitter->testAttribute(Qt::WA_PendingResizeEvent) - << "; Qt::WA_WState_Created=" << m_multiSplitter->testAttribute(Qt::WA_WState_Created); - } - existingAnchor->setPosition(posForExistingAnchor); - } - - // Make sure not just the side1/side2 adjacent widgets are contributing space for our new widget - // the ones adjacents to the adjacents (recursive) must also give. - // The code would work fine without this, it's just that it wouldn't look fair. - propagateResize(delta1, direction1Anchor, Anchor::Side1); - propagateResize(delta2, direction2Anchor, Anchor::Side2); - } - - if (newAnchor) { - // Also ensure the widget has a minimum size in the other direction. So, when adding to - // left/right, it will still have its minimum height honoured, and vice-versa. - QPair adjacentAnchors = targetAnchorGroup.adjacentAnchors(newAnchor); - - const int bound1 = boundPositionForAnchor(adjacentAnchors.first, Anchor::Side1); - const int bound2 = boundPositionForAnchor(adjacentAnchors.second, Anchor::Side2); - - const Qt::Orientation otherOrientation = adjacentAnchors.first->orientation(); - const int min = widgetMinLength(w, otherOrientation); - const int has = targetAnchorGroup.itemSize(otherOrientation); - const int needs = min - has; - if (needs > 0) { - const int pos1 = qMax(bound1, adjacentAnchors.first->position() - needs); - const int pos2 = pos1 + adjacentAnchors.first->thickness() + min; - Q_ASSERT(pos2 <= bound2); - adjacentAnchors.first->setPosition(pos1); - adjacentAnchors.second->setPosition(pos2); - } - } - - auto sourceMultiSplitter = qobject_cast(w); - auto sourceMultiSplitterLayout = sourceMultiSplitter ? sourceMultiSplitter->multiSplitterLayout() - : nullptr; - - if (sourceMultiSplitterLayout) { - auto items = sourceMultiSplitterLayout->items(); - targetAnchorGroup.addItem(sourceMultiSplitterLayout); - addItems_internal(items); - } else { - Q_ASSERT(frame); - auto item = new Item(frame, this); - targetAnchorGroup.addItem(item); - addItems_internal(ItemList{ item }); - } - - updateAnchorFollowing(); - m_addingItem = false; - - maybeCheckSanity(); } void MultiSplitterLayout::addItems_internal(const ItemList &items, bool updateConstraints, bool emitSignal) @@ -1015,24 +836,6 @@ Anchor *MultiSplitterLayout::staticAnchor(Anchor::Type type) const return nullptr; } -Anchor *MultiSplitterLayout::staticAnchor(Anchor::Side side, Qt::Orientation orientation) const -{ - if (orientation == Qt::Vertical) { - return side == Anchor::Side1 ? m_leftAnchor : m_rightAnchor; - } else { - return side == Anchor::Side1 ? m_topAnchor : m_bottomAnchor; - } -} - -AnchorGroup MultiSplitterLayout::anchorsForPos(QPoint pos) const -{ - Item *item = itemAt(pos); - if (!item) - return AnchorGroup(const_cast(this)); - - return item->anchorGroup(); -} - void MultiSplitterLayout::dumpDebug() const { Q_EMIT aboutToDumpDebug(); @@ -1193,51 +996,6 @@ Anchor::List MultiSplitterLayout::anchors(Qt::Orientation orientation, bool incl return result; } -Anchor *MultiSplitterLayout::newAnchor(AnchorGroup &group, Location location) -{ - qCDebug(::anchors) << "MultiSplitterLayout::newAnchor" << location; - Anchor *newAnchor = nullptr; - Anchor *donor = nullptr; - switch (location) { - case Location_OnLeft: - donor = group.left; - newAnchor = Anchor::createFrom(donor); - group.right = newAnchor; - break; - case Location_OnTop: - donor = group.top; - newAnchor = Anchor::createFrom(donor); - group.bottom = newAnchor; - break; - case Location_OnRight: - donor = group.right; - newAnchor = Anchor::createFrom(donor); - group.left = newAnchor; - break; - case Location_OnBottom: - donor = group.bottom; - newAnchor = Anchor::createFrom(donor); - group.top = newAnchor; - break; - default: - qWarning() << "MultiSplitterLayout::newAnchor invalid location!"; - return nullptr; - } - - Q_ASSERT(newAnchor); - Q_ASSERT(donor); - Q_ASSERT(donor != newAnchor); - - updateAnchorsFromTo(donor, newAnchor); - - qCDebug(::anchors()) << newAnchor->hasNonPlaceholderItems(Anchor::Side1) - << newAnchor->hasNonPlaceholderItems(Anchor::Side2) - << newAnchor->side1Items() << newAnchor->side2Items() - << "; donor" << donor - << "; follows=" << newAnchor->followee(); - return newAnchor; -} - void MultiSplitterLayout::blockItemPropagateGeo(bool block) { for (Item *item : m_items) { @@ -1300,253 +1058,9 @@ QVector MultiSplitterLayout::dockWidgets() const return result; } -QPair MultiSplitterLayout::createTargetAnchorGroup(KDDockWidgets::Location location, Item *relativeToItem) +bool MultiSplitterLayout::checkSanity() const { - const bool relativeToThis = relativeToItem == nullptr; - AnchorGroup group = relativeToThis ? staticAnchorGroup() - : anchorsForPos(relativeToItem->geometry().center()); - - if (!group.isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid anchor group:" << group - << "; staticAnchorGroup=" << staticAnchorGroup() - << "; relativeTo=" << relativeToItem; - - dumpDebug(); - } - - Anchor *newAnchor = nullptr; - if (relativeToThis) { - if (!isEmpty()) - newAnchor = this->newAnchor(group, location); - } else { - newAnchor = group.createAnchorFrom(location, relativeToItem); - group.setAnchor(newAnchor, KDDockWidgets::oppositeLocation(location)); - } - - return { group, newAnchor }; -} - -bool MultiSplitterLayout::checkSanity(AnchorSanityOption options) const -{ - if (m_inCtor || LayoutSaver::restoreInProgress()) - return true; - - auto check = [this, options] (Item *item, Qt::Orientation orientation) { - int numSide1 = 0; - int numSide2 = 0; - const auto &anchors = this->anchors(orientation, /*includeStatic=*/ true); - for (Anchor *anchor : anchors) { - if (anchor->containsItem(item, Anchor::Side1)) - numSide1++; - if (anchor->containsItem(item, Anchor::Side2)) - numSide2++; - } - - if (numSide1 != 1 || numSide2 != 1) { - dumpDebug(); - qWarning() << "MultiSplitterLayout::checkSanity:" << "Problem detected! while processing" - << orientation << "anchors" - << "; numSide1=" << numSide1 - << "; numSide2=" << numSide2; - for (Anchor *anchor : anchors) { - if (anchor->containsItem(item, Anchor::Side1)) - qDebug() << "Anchor" << anchor << "contains said widget on side1"; - if (anchor->containsItem(item, Anchor::Side2)) - qDebug() << "Anchor" << anchor << "contains said widget on side2"; - } - qWarning() << "MultiSplitterLayout::checkSanity:" << numSide1 << numSide2 << item - << "\n" << m_topAnchor->items(Anchor::Side2) - << "\n" << m_bottomAnchor->items(Anchor::Side1) - << "\n" << m_leftAnchor->items(Anchor::Side2) - << "\n" << m_rightAnchor->items(Anchor::Side1); - return false; - } - - if ((options & AnchorSanity_WidgetInvalidSizes) && !item->isPlaceholder()) { - if (item->width() <= 0 || item->height() <= 0) { - dumpDebug(); - qWarning() << "Invalid size for widget" << item << item->size() << "; isPlaceholder=" << item->isPlaceholder() - << "; minSize=" << item->minimumSize(); - return false; - } - } - - return true; - }; - - if (!m_topAnchor || !m_leftAnchor || !m_rightAnchor || !m_bottomAnchor) { - qWarning() << Q_FUNC_INFO << "Invalid static anchors" - << m_leftAnchor << m_topAnchor << m_rightAnchor << m_bottomAnchor; - return false; - } - - if (m_topAnchor->position() != 0 || m_leftAnchor->position() != 0 || - m_rightAnchor->position() != width() - m_rightAnchor->thickness() || - m_bottomAnchor->position() != height() - m_bottomAnchor->thickness()) { - qWarning() << Q_FUNC_INFO << "Invalid anchor position" - << " left=" << m_leftAnchor->position() - << " top=" << m_topAnchor->position() - << " right=" << m_rightAnchor->position() - << " bottom=" << m_bottomAnchor->position() - << "; size=" << m_size - << "; min=" << m_minSize; - return false; - } - - - for (Anchor *anchor : qAsConst(m_anchors)) { - if (!anchor->isValid()) { - dumpDebug(); - qWarning() << "invalid anchor" << anchor; - return false; - } - - auto checkSides = [this, anchor] (Anchor::Side side) { - for (Item *item : anchor->items(side)) { - if (!contains(item)) { - dumpDebug(); - qWarning() << "MultiSplitterLayout::checkSanity: Anchor has" << item << "but multi splitter does not"; - return false; - } - } - return true; - }; - - if (!checkSides(Anchor::Side1) || !checkSides(Anchor::Side2)) - return false; - - if (anchor->isFollowing() && !qobject_cast(anchor->followee())) { - qWarning() << "Anchor is following but followee was deleted already"; - return false; - } - - if (options & AnchorSanity_Followers) { - const bool hasItemsOnBothSides = anchor->hasNonPlaceholderItems(Anchor::Side1) && anchor->hasNonPlaceholderItems(Anchor::Side2); - if (!anchor->isStatic() && !anchor->isFollowing() && !hasItemsOnBothSides && anchorsFollowing(anchor).isEmpty()) { - qWarning() << "Non static anchor should have items on both sides unless it's following or being followed" << anchor; - } - } - - if (!anchor->isFollowing() &&anchor->geometry() != anchor->separatorWidget()->geometry()) { - qWarning() << Q_FUNC_INFO << anchor << anchor->separatorWidget() - << "Inconsistent anchor geometry" << anchor->geometry() << "; " << anchor->separatorWidget()->geometry(); - return false; - } - - if (options & AnchorSanity_Visibility) { - if (multiSplitter()->isVisible() && !anchor->isFollowing() && !anchor->separatorWidget()->isVisible()) { - qWarning() << Q_FUNC_INFO << "Anchor should be visible" << anchor; - return false; - } - } - } - - for (Item *item : qAsConst(m_items)) { - if (!check(item, Qt::Vertical)) - return false; - - if (!check(item, Qt::Horizontal)) - return false; - } - - // Check that no widget intersects with an anchor - if (options & AnchorSanity_Intersections) { - for (Item *item: items()) { - for (Anchor *a : anchors()) { - if (!item->isPlaceholder() && item->geometry().intersects(a->geometry())) { - dumpDebug(); - qWarning() << "MultiSplitterLayout::checkSanity: Widget" << item << "with rect" << item->geometry() - << "Intersects anchor" << a << "with rect" << a->geometry() - << "; a.visible|following|valid|unneeded=" << a->separatorWidget()->isVisible()<< a->isFollowing() << a->isValid() << a->isUnneeded(); - return false; - } - } - } - } - - if (options & AnchorSanity_WidgetGeometry) { - for (Item *item: items()) { - - if (!item->isPlaceholder() && item->geometry() != item->frame()->geometry()) { - qWarning() << Q_FUNC_INFO << "Invalid geometry for item" << item << item->geometry() << item->frame()->geometry(); - return false; - } - - if (!item->anchorGroup().isValid()) { - qWarning() << Q_FUNC_INFO << "Invalid item group for item" << item->anchorGroup(); - return false; - } - - if (!item->isPlaceholder() && item->anchorGroup().itemSize() != item->size()) { - qWarning() << Q_FUNC_INFO << "Invaild item size=" - << item->size() - << "group size=" - << item->anchorGroup().itemSize(); - return false; - } - } - } - - if (options & AnchorSanity_WidgetMinSizes) { - for (Item *item : items()) { - - if (item->isPlaceholder()) - continue; - - const int minWidth = item->minLength(Qt::Vertical); - const int minHeight = item->minLength(Qt::Horizontal); - - if (item->width() < minWidth) { - qWarning() << "MultiSplitterLayout::checkSanity: Widget has width=" << item->width() - << "but minimum is" << minWidth - << item; - return false; - } - - if (item->height() < minHeight) { - qWarning() << "MultiSplitterLayout::checkSanity: Widget has height=" << item->height() - << "but minimum is" << minHeight - << item; - return false; - } - } - } - - for (DockWidgetBase *dw : DockRegistry::self()->dockwidgets()) { - Frame *frame = dw->frame(); - auto tabWidgetParent = frame ? frame->tabWidget() : nullptr; - const bool shouldBeChecked = dw->isVisible() || tabWidgetParent; - - if (shouldBeChecked != dw->toggleAction()->isChecked()) { - qWarning() << Q_FUNC_INFO << "Invalid state for DockWidgetBase::toggleAction()" - << dw->toggleAction()->isChecked(); - return false; - } - } - -/* TODO: uncomment when all tests pass - if (m_topAnchor->position() != 0 || m_leftAnchor->position() != 0) { - qWarning() << Q_FUNC_INFO << "Invalid top or left anchor position" - << m_topAnchor->position() << m_leftAnchor->position(); - return false; - } - - if (m_rightAnchor->position() != m_size.width() - 1 || m_bottomAnchor->position() != m_size.height() - 1) { - qWarning() << Q_FUNC_INFO << "Invalid right or bottom anchor position" - << m_rightAnchor->position() << m_bottomAnchor->position() - << "; m_size=" << m_size; - return false; - } -*/ - return true; -} - -void MultiSplitterLayout::maybeCheckSanity() -{ -#if defined(DOCKS_DEVELOPER_MODE) - if (!isRestoringPlaceholder() && !checkSanity(AnchorSanityOption(AnchorSanity_All & ~AnchorSanity_Visibility))) - qWarning() << Q_FUNC_INFO << "Sanity check failed"; -#endif + return m_rootItem->checkSanity(); } void MultiSplitterLayout::ensureHasAvailableSize(QSize needed) @@ -1565,132 +1079,6 @@ void MultiSplitterLayout::ensureHasAvailableSize(QSize needed) setSize(newSize); } -void MultiSplitterLayout::restorePlaceholder(Item *item) -{ - QScopedValueRollback restoring(m_restoringPlaceholder, true); - - AnchorGroup anchorGroup = item->anchorGroup(); - - const QSize availableSize = this->availableSize(); - const QSize hardcodedMinSize = MultiSplitterLayout::hardcodedMinimumSize(); - - const QSize widgetMinSize = { qMax(hardcodedMinSize.width(), KDDockWidgets::widgetMinLength(item->frame(), Qt::Vertical)), - qMax(hardcodedMinSize.height(), KDDockWidgets::widgetMinLength(item->frame(), Qt::Horizontal)) }; - - const QSize newSize = {qMax(qMin(item->length(Qt::Vertical), availableSize.width()), widgetMinSize.width()), - qMax(qMin(item->length(Qt::Horizontal), availableSize.height()), widgetMinSize.height()) }; - - // Our layout has enough size for the dock widget - ensureHasAvailableSize(newSize); - - item->setIsPlaceholder(false); - item->beginBlockPropagateGeo(); - updateSizeConstraints(); - - Anchor::List anchorsFollowing = anchorGroup.anchorsFollowingInwards(); - if (anchorsFollowing.isEmpty()) { - // There's no separator to move, it means it's a static anchor group (layout is empty, so the anchors - // are the actual borders of the window - // dumpDebug(); - Q_ASSERT(anchorGroup.isStaticOrFollowsStatic()); - anchorGroup.updateItemSizes(); - maybeCheckSanity(); - item->endBlockPropagateGeo(); - return; - } - - clearAnchorsFollowing(); - QHash anchorsThatWillFollowOthers = anchorsShouldFollow(); - - if (!anchorsFollowing.contains(anchorGroup.top) && !anchorsFollowing.contains(anchorGroup.bottom)) { - anchorGroup.top->updateItemSizes(); - anchorGroup.bottom->updateItemSizes(); - } - if (!anchorsFollowing.contains(anchorGroup.left) && !anchorsFollowing.contains(anchorGroup.right)) { - anchorGroup.left->updateItemSizes(); - anchorGroup.right->updateItemSizes(); - } - - - for (Anchor *anchorFollowingInwards : anchorsFollowing) { - const Qt::Orientation orientation = anchorFollowingInwards->orientation(); - Anchor *side1Anchor = anchorGroup.anchorAtSide(Anchor::Side1, orientation); // returns the left if vertical, otherwise top - Anchor *side2Anchor = anchorGroup.anchorAtSide(Anchor::Side2, orientation); // returns the right if vertical, otherwise bottom - - if (anchorsThatWillFollowOthers.contains(side1Anchor)) { - Anchor *followee = anchorsThatWillFollowOthers.value(side1Anchor); - side1Anchor->setFollowee(followee); - side1Anchor = followee; - } - - if (anchorsThatWillFollowOthers.contains(side2Anchor)) { - Anchor *followee = anchorsThatWillFollowOthers.value(side2Anchor); - side2Anchor->setFollowee(followee); - side2Anchor = followee; - } - - const int oldPosition1 = side1Anchor->position(); - const int oldPosition2 = side2Anchor->position(); - const int boundPosition1 = side1Anchor->isStatic() ? side1Anchor->position() - : boundPositionForAnchor(side1Anchor, Anchor::Side1); - - const int boundPosition2 = side2Anchor->isStatic() ? side2Anchor->position() - : boundPositionForAnchor(side2Anchor, Anchor::Side2); - - const int newLength = anchorFollowingInwards->isVertical() ? newSize.width() : newSize.height(); - // Let's try that each anchor contributes 50%, so that the widget appears centered - const int suggestedLength1 = qMin(newLength, qCeil(newLength / 2) + side1Anchor->thickness() + 1); - const int maxPos1 = boundPosition2 - newLength - side1Anchor->thickness(); - const int newPosition1 = qMax(qMin(maxPos1, oldPosition1 - suggestedLength1), boundPosition1); // Honour the bound - const int newPosition2 = newPosition1 + side1Anchor->thickness() + newLength; // No need to check bound2, we have enough space afterall - - qCDebug(placeholder) << Q_FUNC_INFO - << "; oldPos1=" << oldPosition1 - << "; oldPos2=" << oldPosition2 - << "; newPosition1=" << newPosition1 - << "; newPosition2=" << newPosition2 - << "; bounds1=" << boundPosition1 - << "; bounds2=" << boundPosition2 - << "; item.geo=" << item->geometry() - << "; newSize=" << newSize - << "; side1Anchor=" << side1Anchor - << "; side2Anchor=" << side2Anchor - << side1Anchor->followee() << side2Anchor->followee() - << "; anchorFollowing=" << anchorFollowingInwards - << "; size=" << m_size - << "; minSize=" << m_minSize - << "; widgetMinSize=" << widgetMinSize - << "; available_old=" << availableSize - << "; available_new=" << availableLengthForOrientation(orientation) - << "; item.size=" << item->size(); - - if (newPosition1 < boundPosition1 || newPosition2 > boundPosition2) { - qWarning() << Q_FUNC_INFO << "Out of bounds" - // << "bounds.anchor1=" << boundPositionsForAnchor(side1Anchor) - << "bounds.anchor2=" << boundPositionsForAnchor(side2Anchor) - << "; side1Anchor.thickness" << side1Anchor->thickness() - << "; side2Anchor.thickness" << side2Anchor->thickness(); - } - - // We don't want item to resize the anchors while setting newPosition1, we already calculated it - if (side1Anchor->isStatic()) { - side1Anchor->updateItemSizes(); - } else { - side1Anchor->setPosition(newPosition1); - } - - if (side2Anchor->isStatic()) { - side2Anchor->updateItemSizes(); - } else { - side2Anchor->setPosition(newPosition2); - } - } - item->endBlockPropagateGeo(); - - updateAnchorFollowing(); - maybeCheckSanity(); -} - void MultiSplitterLayout::unrefOldPlaceholders(const Frame::List &framesBeingAdded) const { for (Frame *frame : framesBeingAdded) { @@ -1763,31 +1151,6 @@ void MultiSplitterLayout::setMinimumSize(QSize sz) qCDebug(sizing) << Q_FUNC_INFO << "minSize = " << m_minSize; } -void MultiSplitterLayout::updateAnchorsFromTo(Anchor *oldAnchor, Anchor *newAnchor) -{ - // Update the from/to of other anchors - for (Anchor *other : qAsConst(m_anchors)) { - Q_ASSERT(other); - Q_ASSERT(other->isValid()); - if (!other->isStatic() && other->orientation() != newAnchor->orientation()) { - if (other->to() == oldAnchor) { - other->setTo(newAnchor); - } else if (other->from() == oldAnchor) { - other->setFrom(newAnchor); - } - - if (!other->isValid()) { - qDebug() << "MultiSplitterLayout::updateAnchorsFromTo: anchor is now invalid." - << "\n old=" << oldAnchor - << "\n new=" << newAnchor - << "\n from=" << other->from() - << "\n to=" << other->to() - << "\n other=" << other; - } - } - } -} - void MultiSplitterLayout::clearAnchorsFollowing() { for (Anchor *anchor : qAsConst(m_anchors)) @@ -1894,6 +1257,11 @@ const ItemList MultiSplitterLayout::items() const return m_items; } +Item *MultiSplitterLayout::rootItem() const +{ + return m_rootItem; +} + bool MultiSplitterLayout::eventFilter(QObject *o, QEvent *e) { if (m_inDestructor || e->spontaneous() || !m_multiSplitter) diff --git a/src/private/multisplitter/MultiSplitterLayout_p.h b/src/private/multisplitter/MultiSplitterLayout_p.h index 17d9683f..68e1f295 100644 --- a/src/private/multisplitter/MultiSplitterLayout_p.h +++ b/src/private/multisplitter/MultiSplitterLayout_p.h @@ -34,7 +34,6 @@ #include "../Frame_p.h" #include "Anchor_p.h" -#include "AnchorGroup_p.h" #include "docks_export.h" #include "KDDockWidgets.h" #include "Item_p.h" @@ -45,7 +44,6 @@ namespace KDDockWidgets { class MultiSplitter; -class Length; namespace Debug { class DebugWindow; @@ -82,22 +80,6 @@ inline Anchor::Side sideForLocation(Location loc) return Anchor::Side_None; } -inline Qt::Orientation orientationForLocation(Location loc) -{ - switch (loc) { - case KDDockWidgets::Location_OnLeft: - case KDDockWidgets::Location_OnRight: - return Qt::Vertical; - case KDDockWidgets::Location_OnTop: - case KDDockWidgets::Location_OnBottom: - return Qt::Horizontal; - default: - break; - } - - return Qt::Vertical; -} - /** * A MultiSplitter is like a QSplitter but supports mixing vertical and horizontal splitters in * any combination. @@ -221,6 +203,8 @@ public: */ const ItemList items() const; + Item* rootItem() const; + /** * Called by the indicators, so they draw the drop rubber band at the correct place. * The rect for the rubberband when dropping a widget at the specified location. @@ -311,22 +295,7 @@ public: bool validateInputs(QWidgetOrQuick *widget, KDDockWidgets::Location location, const Frame *relativeToFrame, AddingOption option) const; // For debug/hardening - enum AnchorSanityOption { - AnchorSanity_Normal = 0, - AnchorSanity_Intersections = 1, - AnchorSanity_WidgetMinSizes = 2, - AnchorSanity_WidgetInvalidSizes = 4, - AnchorSanity_Followers = 8, - AnchorSanity_WidgetGeometry = 16, - AnchorSanity_Visibility = 32, - AnchorSanity_All = AnchorSanity_Intersections | AnchorSanity_WidgetMinSizes | AnchorSanity_WidgetInvalidSizes | AnchorSanity_Followers | AnchorSanity_WidgetGeometry | AnchorSanity_Visibility - }; - Q_ENUM(AnchorSanityOption) - - bool checkSanity(AnchorSanityOption o = AnchorSanity_All) const; - void maybeCheckSanity(); - - void restorePlaceholder(Item *item); + bool checkSanity() const; /** * @brief Removes unneeded placeholder items when adding new frames. @@ -362,14 +331,6 @@ public: */ QVector dockWidgets() const; - /** - * @brief Creates an AnchorGroup suited for adding a dockwidget to @location relative to @relativeToItem - * - * Returns the AnchorGroup and a new Anchor, if it was needed. - * If relativeTo is null then it returns the static anchor group. - */ - QPair createTargetAnchorGroup(Location location, Item *relativeToItem); - struct Length { Length() = default; Length(int side1, int side2) @@ -429,15 +390,10 @@ Q_SIGNALS: public: bool eventFilter(QObject *o, QEvent *e) override; - AnchorGroup anchorsForPos(QPoint pos) const; - AnchorGroup staticAnchorGroup() const; Anchor::List anchors(Qt::Orientation, bool includeStatic = false, bool includePlaceholders = true) const; - Anchor *newAnchor(AnchorGroup &group, KDDockWidgets::Location location); - friend QDebug operator<<(QDebug d, const AnchorGroup &group); static const QString s_magicMarker; void ensureAnchorsBounded(); private: - friend struct AnchorGroup; friend class Item; friend class Anchor; friend class TestDocks; @@ -525,36 +481,6 @@ private: */ QSize availableSize() const; - /** - * @brief Increases the layout size if @ref availableSize is less than @needed - */ - void ensureHasAvailableSize(QSize needed); - - /** - * Removes the widgets associated with oldAnchor and gives them to newAnchor. - * Called when removing a widget results in unneeded anchors. - */ - void updateAnchorsFromTo(Anchor *oldAnchor, Anchor *newAnchor); - - void clearAnchorsFollowing(); - void updateAnchorFollowing(const AnchorGroup &groupBeingRemoved = {}); - QHash anchorsShouldFollow() const; - - /** - * Positions the static anchors at their correct places. Called when the MultiSplitter is resized. - * left and top anchor are at position 0, while right/bottom are at position= width/height. - * (Approx, due to styling margins and whatnot) - */ - void positionStaticAnchors(); - - /** - * When this MultiSplitter is resized, it gives or steals the less/extra space evenly through - * all widgets. - **/ - void redistributeSpace(); - void redistributeSpace(QSize oldSize, QSize newSize); - void redistributeSpace_recursive(Anchor *fromAnchor, int minAnchorPos); - /** * Returns the width (if orientation = Horizontal), or height that is occupied by anchors. * For example, an horizontal anchor has 2 or 3 px of width, so that's space that can't be @@ -562,62 +488,32 @@ private: */ int wastedSpacing(Qt::Orientation) const; - /** - * Called by addWidget(). - * - * When adding a widget to a layout, it will steal space from the widgets on the left (or top) (@p direction being Anchor::Side1), - * and from the widgets on the right (or bottom) (@p direction being Anchor::Side2). - * - * @param delta the amount of space we're stealing in the specified side - * @param fromAnchor The anchor we're starting from - * @param direction if we're going left/top (Side1) or right/bottom (Side2) - */ - void propagateResize(int delta, Anchor *fromAnchor, Anchor::Side direction); - - // Helper function for propagateResize() - void collectPaths(QVector &paths, Anchor *fromAnchor, Anchor::Side direction); - // convenience for the unit-tests // Moves the widget's bottom or right anchor, to resize it. void resizeItem(Frame *frame, int newSize, Qt::Orientation); - void ensureItemsMinSize(); - ///@brief returns whether we're inside setSize(); bool isResizing() const { return m_resizing; } bool isRestoringPlaceholder() const { return m_restoringPlaceholder; } - bool isAddingItem() const { return m_addingItem; } QString affinityName() const; MultiSplitter *const m_multiSplitter; Anchor::List m_anchors; - Anchor *m_leftAnchor = nullptr; - Anchor *m_topAnchor = nullptr; - Anchor *m_rightAnchor = nullptr; - Anchor *m_bottomAnchor = nullptr; - ItemList m_items; bool m_inCtor = true; bool m_inDestructor = false; - bool m_beingMergedIntoAnotherMultiSplitter = false; + bool m_beingMergedIntoAnotherMultiSplitter = false; // TODO bool m_restoringPlaceholder = false; bool m_resizing = false; - bool m_addingItem = false; QSize m_minSize = QSize(0, 0); - AnchorGroup m_staticAnchorGroup; QPointer m_anchorBeingDragged; QSize m_size; + Item *const m_rootItem; }; -inline QDebug operator<<(QDebug d, const AnchorGroup &group) { - d << "AnchorGroup: top=" << group.top << "; left=" << group.left - << "; right=" << group.right << "; bottom=" << group.bottom; - return d; -} - /** * Returns the widget's min-width if orientation is Vertical, the min-height otherwise. */ diff --git a/src/private/multisplitter/tests/CMakeLists.txt b/src/private/multisplitter/tests/CMakeLists.txt new file mode 100644 index 00000000..9e1bcf7d --- /dev/null +++ b/src/private/multisplitter/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +find_package(Qt5Test) + + +add_executable(tst_multisplitter tst_multisplitter.cpp) +target_link_libraries(tst_multisplitter kddockwidgets_layouting Qt5::Test) diff --git a/src/private/multisplitter/tests/tst_multisplitter.cpp b/src/private/multisplitter/tests/tst_multisplitter.cpp new file mode 100644 index 00000000..105b1e87 --- /dev/null +++ b/src/private/multisplitter/tests/tst_multisplitter.cpp @@ -0,0 +1,560 @@ +/* + This file is part of KDDockWidgets. + + Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com + Author: Sérgio Martins + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + 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 "Item.h" +#include +#include + + +// TODO: namespace + +using namespace KDDockWidgets; + +static int st = Item::separatorThickness(); + +static std::unique_ptr createRoot() { + auto item = new ItemContainer(); + item->setSize({ 1000, 1000 }); + return std::unique_ptr(item); +} + +static Item* createItem(const QString &objName) { + auto item = new Item(); + item->setObjectName(objName); + return item; +} + +class TestMultiSplitter : public QObject +{ + Q_OBJECT +public Q_SLOTS: + void initTestCase() + { + } + +private Q_SLOTS: + void tst_createRoot(); + void tst_insertOne(); + void tst_insertThreeSideBySide(); + void tst_insertOnWidgetItem1(); + void tst_insertOnWidgetItem2(); + void tst_insertOnWidgetItem1DifferentOrientation(); + void tst_insertOnWidgetItem2DifferentOrientation(); + void tst_insertOnRootDifferentOrientation(); + void tst_removeItem1(); + void tst_removeItem2(); + void tst_minSize(); + void tst_resize(); + void tst_resizeWithConstraints(); + void tst_availableSize(); + void tst_missingSize(); + void tst_ensureEnoughSize(); +}; + +void TestMultiSplitter::tst_createRoot() +{ + auto root = createRoot(); + QVERIFY(root->isRoot()); + QVERIFY(!root->isWidget()); + QVERIFY(root->isContainer()); + QVERIFY(root->hasOrientation()); + QCOMPARE(root->size(), QSize(1000, 1000)); + QVERIFY(root->checkSanity()); +} + +void TestMultiSplitter::tst_insertOne() +{ + auto root = createRoot(); + auto item = createItem("1"); + root->insertItem(item, Location_Top); + QCOMPARE(root->numChildren(), 1); + QVERIFY(item->isWidget()); + QVERIFY(!item->isContainer()); + QCOMPARE(root->size(), QSize(1000, 1000)); + QCOMPARE(item->size(), root->size()); + QCOMPARE(item->pos(), QPoint()); + QCOMPARE(item->pos(), root->pos()); + QVERIFY(root->hasChildren()); + QVERIFY(root->checkSanity()); +} + +void TestMultiSplitter::tst_insertThreeSideBySide() +{ + // Result is [1, 2, 3] + + auto root = createRoot(); + auto item1 = new Item(); + auto item2 = new Item(); + auto item3 = new Item(); + + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + root->insertItem(item3, Location_Right); + + QVERIFY(root->checkSanity()); + QCOMPARE(root->numChildren(), 3); +} + +void TestMultiSplitter::tst_insertOnWidgetItem1() +{ + // We insert into a widget item instead of in a container. It will insert in the container still + // Result is still [1, 2, 3] + + auto root = createRoot(); + auto item1 = new Item(); + auto item2 = new Item(); + auto item3 = new Item(); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Right); + + QVERIFY(item3->x() > item2->x()); + QCOMPARE(item3->y(), item2->y()); + + QVERIFY(root->checkSanity()); + QCOMPARE(root->numChildren(), 3); +} + +void TestMultiSplitter::tst_insertOnWidgetItem2() +{ + // Same, but result [1, 3, 2] + + auto root = createRoot(); + auto item1 = new Item(); + auto item2 = new Item(); + auto item3 = new Item(); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Left); + + QVERIFY(item1->x() < item3->x()); + QVERIFY(item3->x() < item2->x()); + QCOMPARE(item3->y(), item2->y()); + + QVERIFY(root->checkSanity()); + QCOMPARE(root->numChildren(), 3); +} + +void TestMultiSplitter::tst_insertOnWidgetItem1DifferentOrientation() +{ + // Result [1, 2, |3 |] + // |3.1| + + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + auto item31 = createItem("3.2"); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Right); + item3->insertItem(item31, Location_Bottom); + + auto container3 = item3->parentContainer(); + QVERIFY(container3->isContainer()); + QVERIFY(container3 != root.get()); + QVERIFY(root->isHorizontal()); + QVERIFY(container3->isVertical()); + + QCOMPARE(root->numChildren(), 3); + QCOMPARE(container3->numChildren(), 2); + + QVERIFY(item1->x() < item2->x()); + QVERIFY(item3->parentContainer()->x() > item2->x()); + QCOMPARE(item3->x(), 0); + QCOMPARE(item3->y(), item2->y()); + QCOMPARE(item1->y(), item2->y()); + + QVERIFY(item31->y() >= item3->y()); + QCOMPARE(item31->parentContainer(), container3); + QCOMPARE(item3->parentContainer(), container3); + QCOMPARE(container3->parentContainer(), root.get()); + QCOMPARE(QPoint(0, 0), item3->pos()); + QCOMPARE(container3->width(), item3->width()); + QCOMPARE(container3->height(), item3->height() + st + item31->height()); + + QVERIFY(root->checkSanity()); +} + +void TestMultiSplitter::tst_insertOnWidgetItem2DifferentOrientation() +{ + // Result [1, 2, |3 3.2|] + // |3.1 | + + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + auto item31 = createItem("3.1"); + auto item32 = createItem("3.2"); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Right); + item3->insertItem(item31, Location_Bottom); + auto container3Parent = item3->parentContainer(); + item3->insertItem(item32, Location_Right); + auto container3 = item3->parentContainer(); + + QCOMPARE(container3->parentContainer(), container3Parent); + + QVERIFY(container3->isContainer()); + QVERIFY(container3 != root.get()); + QVERIFY(root->isHorizontal()); + QVERIFY(container3->isHorizontal()); + QVERIFY(container3Parent->isVertical()); + + QCOMPARE(root->numChildren(), 3); + QCOMPARE(container3->numChildren(), 2); + QCOMPARE(container3Parent->numChildren(), 2); + + QVERIFY(item1->x() < item2->x()); + QCOMPARE(container3->pos(), QPoint(0, 0l)); + QCOMPARE(item3->pos(), container3->pos()); + QVERIFY(container3Parent->x() > item2->x()); + QCOMPARE(item3->y(), item2->y()); + QCOMPARE(item1->y(), item2->y()); + + QVERIFY(item31->y() >= item3->y()); + QCOMPARE(item31->parentContainer(), container3Parent); + QCOMPARE(item3->parentContainer(), container3); + QCOMPARE(container3Parent->parentContainer(), root.get()); + QCOMPARE(container3->pos(), item3->pos()); + QCOMPARE(container3->width(), item3->width() + item32->width() + st); + QCOMPARE(container3->height(), item3->height()); + QCOMPARE(container3Parent->height(), item3->height() + st + item31->height()); + + QVERIFY(root->checkSanity()); +} + +void TestMultiSplitter::tst_insertOnRootDifferentOrientation() +{ + // [ 4 ] + // Result [1, 2, |3 3.2|] + // |3.1 | + + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + auto item31 = createItem("3.1"); + auto item32 = createItem("3.2"); + auto item4 = createItem("4"); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Right); + item3->insertItem(item31, Location_Bottom); + item3->insertItem(item32, Location_Right); + root->insertItem(item4, Location_Top); + + QCOMPARE(item4->parentContainer(), root.get()); + QCOMPARE(item4->pos(), root->pos()); + QCOMPARE(item4->width(), root->width()); + + QVERIFY(root->checkSanity()); +} + +void TestMultiSplitter::tst_removeItem1() +{ + // [ 4 ] + // Result [1, 2, |3 3.2|] + // |3.1 | + + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + auto item31 = createItem("3.1"); + auto item32 = createItem("3.2"); + auto item4 = createItem("4"); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Right); + item3->insertItem(item31, Location_Bottom); + item3->insertItem(item32, Location_Right); + root->insertItem(item4, Location_Top); + + QCOMPARE(root->numChildren(), 2); + root->removeItem(item4); + QCOMPARE(root->numChildren(), 1); + + auto c1 = item1->parentContainer(); + QCOMPARE(c1->pos(), QPoint(0, 0)); + QCOMPARE(c1->width(), root->width()); + QCOMPARE(c1->height(), item1->height()); + QCOMPARE(c1->height(), root->height()); + + const int item3and32Width = item3->width() + item32->width() + st; + root->removeItem(item32); + QCOMPARE(item3->width(), item3and32Width); + root->checkSanity(); + + root->removeItem(item31); + root->checkSanity(); + + QCOMPARE(item2->height(), item3->height()); + + QPointer c3 = item3->parentContainer(); + root->removeItem(c3); + QVERIFY(c3.isNull()); +} + +void TestMultiSplitter::tst_removeItem2() +{ + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + auto item31 = createItem("3.1"); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item3, Location_Right); + item3->insertItem(item31, Location_Bottom); + item31->parentContainer()->removeItem(item31); + item3->parentContainer()->removeItem(item3); +} + +void TestMultiSplitter::tst_minSize() +{ + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item22 = createItem("2.2"); + + item1->m_sizingInfo.minSize = {101, 150}; + item2->m_sizingInfo.minSize = {200, 300}; + item22->m_sizingInfo.minSize = {100, 100}; + + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + item2->insertItem(item22, Location_Bottom); + + QCOMPARE(item2->minSize(), QSize(200, 300)); + QCOMPARE(item2->parentContainer()->minSize(), QSize(200, 300+100+st)); + + QCOMPARE(root->minSize(), QSize(101+200+st, 300 + 100 + st)); +} + +void TestMultiSplitter::tst_resize() +{ + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + auto item31 = createItem("31"); + + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + root->insertItem(item3, Location_Right); + + const int item1Percentage = item1->width() / root->width(); + const int item2Percentage = item1->width() / root->width(); + const int item3Percentage = item1->width() / root->width(); + + // Now resize: + root->resize({2000, 505}); + + QVERIFY(item1Percentage - (1.0* item1->width() / root->width()) < 0.01); + QVERIFY(item2Percentage - (1.0* item2->width() / root->width()) < 0.01); + QVERIFY(item3Percentage - (1.0* item3->width() / root->width()) < 0.01); + QCOMPARE(root->width(), 2000); + QCOMPARE(root->height(), 505); + QCOMPARE(item1->height(), 505); + QCOMPARE(item2->height(), 505); + QCOMPARE(item3->height(), 505); + + item3->insertItem(item31, Location_Bottom); + + QVERIFY(root->checkSanity()); + root->resize({2500, 505}); + QVERIFY(root->checkSanity()); +} + +void TestMultiSplitter::tst_resizeWithConstraints() +{ + { + // Test that resizing below minSize isn't permitted. + + auto root = createRoot(); + auto item1 = createItem("1"); + item1->setMinSize(QSize(500, 500)); + root->insertItem(item1, Location_Left); + QVERIFY(root->checkSanity()); + + root->resize(item1->minSize()); // Still fits + root->resize(item1->minSize() - QSize(1, 0)); // wouldn't fit + QCOMPARE(root->size(), item1->size()); // still has the old size + } + + { + auto root = createRoot(); + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + root->resize(QSize(2000, 500)); + item1->setMinSize(QSize(500, 500)); + item2->setMinSize(QSize(500, 500)); + item3->setMinSize(QSize(500, 500)); + root->insertItem(item1, Location_Left); + root->insertItem(item2, Location_Right); + root->insertItem(item3, Location_Right); + QVERIFY(root->checkSanity()); + + // TODO: Resize further + } +} + +void TestMultiSplitter::tst_availableSize() +{ + auto root = createRoot(); + QCOMPARE(root->availableSize(), QSize(1000, 1000)); + QCOMPARE(root->minSize(), QSize(0, 0)); + + auto item1 = createItem("1"); + auto item2 = createItem("2"); + auto item3 = createItem("3"); + item1->m_sizingInfo.minSize = {100, 100}; + item2->m_sizingInfo.minSize = {100, 100}; + item3->m_sizingInfo.minSize = {100, 100}; + + root->insertItem(item1, Location_Left); + QCOMPARE(root->availableSize(), QSize(900, 900)); + QCOMPARE(root->minSize(), QSize(100, 100)); + QCOMPARE(root->neighboursLengthFor(item1, Side1, Qt::Horizontal), 0); + QCOMPARE(root->neighboursLengthFor(item1, Side2, Qt::Horizontal), 0); + QCOMPARE(root->neighboursMinLengthFor(item1, Side1, Qt::Horizontal), 0); + QCOMPARE(root->neighboursMinLengthFor(item1, Side2, Qt::Horizontal), 0); + QCOMPARE(root->neighbourSeparatorWaste(item1, Side1, Qt::Vertical), 0); + QCOMPARE(root->neighbourSeparatorWaste(item1, Side2, Qt::Vertical), 0); + QCOMPARE(root->neighbourSeparatorWaste(item1, Side1, Qt::Horizontal), 0); + QCOMPARE(root->neighbourSeparatorWaste(item1, Side2, Qt::Horizontal), 0); + + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side1, Qt::Vertical), 0); + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side2, Qt::Vertical), 0); + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side1, Qt::Horizontal), 0); + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side2, Qt::Horizontal), 0); + + root->insertItem(item2, Location_Left); + QCOMPARE(root->availableSize(), QSize(800 - st, 900)); + QCOMPARE(root->minSize(), QSize(200 + st, 100)); + QCOMPARE(root->neighboursLengthFor(item1, Side1, Qt::Horizontal), item2->width()); + QCOMPARE(root->neighboursLengthFor(item1, Side2, Qt::Horizontal), 0); + QCOMPARE(root->neighboursLengthFor(item2, Side1, Qt::Horizontal), 0); + QCOMPARE(root->neighboursLengthFor(item2, Side2, Qt::Horizontal), item1->width()); + QCOMPARE(root->neighboursMinLengthFor(item1, Side1, Qt::Horizontal), item2->minSize().width()); + QCOMPARE(root->neighboursMinLengthFor(item1, Side2, Qt::Horizontal), 0); + QCOMPARE(root->neighboursMinLengthFor(item2, Side1, Qt::Horizontal), 0); + QCOMPARE(root->neighboursMinLengthFor(item2, Side2, Qt::Horizontal), item1->minSize().width()); + + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side1, Qt::Vertical), 0); + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side2, Qt::Vertical), 0); + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side1, Qt::Horizontal), item2->width()); + QCOMPARE(root->neighboursLengthFor_recursive(item1, Side2, Qt::Horizontal), 0); + + root->insertItem(item3, Location_Bottom); + QCOMPARE(root->availableSize(), QSize(800 - st, 800 - st)); + QCOMPARE(root->minSize(), QSize(200 + st, 100 + 100 + st)); + QCOMPARE(item3->parentContainer()->neighboursMinLengthFor(item3, Side1, Qt::Vertical), item1->minSize().height()); + + auto container2 = item2->parentContainer(); + QCOMPARE(container2->neighboursLengthFor_recursive(item1, Side1, Qt::Vertical), 0); + QCOMPARE(container2->neighboursLengthFor_recursive(item1, Side2, Qt::Vertical), item3->height()); + QCOMPARE(container2->neighboursLengthFor_recursive(item1, Side1, Qt::Horizontal), item2->width()); + QCOMPARE(container2->neighboursLengthFor_recursive(item1, Side2, Qt::Horizontal), 0); + + // More nesting + auto item4 = createItem("4"); + auto item5 = createItem("5"); + item3->insertItem(item4, Location_Right); + item4->insertItem(item5, Location_Bottom); + + auto container4 = item4->parentContainer(); + QCOMPARE(container4->neighboursLengthFor_recursive(item4, Side1, Qt::Vertical), item1->height()); + QCOMPARE(container4->neighboursLengthFor_recursive(item4, Side2, Qt::Vertical), item5->height()); + QCOMPARE(container4->neighboursLengthFor_recursive(item4, Side1, Qt::Horizontal), item3->width()); + QCOMPARE(container4->neighboursLengthFor_recursive(item4, Side2, Qt::Horizontal), 0); + QCOMPARE(container4->neighboursLengthFor_recursive(item5, Side1, Qt::Vertical), item4->height() + item1->height()); + QCOMPARE(container4->neighboursLengthFor_recursive(item5, Side2, Qt::Vertical), 0); + QCOMPARE(container4->neighboursLengthFor_recursive(item5, Side1, Qt::Horizontal), item3->width()); + QCOMPARE(container4->neighboursLengthFor_recursive(item5, Side2, Qt::Horizontal), 0); + + QCOMPARE(container4->neighbourSeparatorWaste(item4, Side1, Qt::Vertical), 0); + QCOMPARE(container4->neighbourSeparatorWaste(item4, Side2, Qt::Vertical), st); + QCOMPARE(container4->neighbourSeparatorWaste(item4, Side1, Qt::Horizontal), 0); + QCOMPARE(container4->neighbourSeparatorWaste(item4, Side2, Qt::Horizontal), 0); + QCOMPARE(container4->neighbourSeparatorWaste(item5, Side1, Qt::Vertical), st); + QCOMPARE(container4->neighbourSeparatorWaste(item5, Side2, Qt::Vertical), 0); + QCOMPARE(container4->neighbourSeparatorWaste(item5, Side1, Qt::Horizontal), 0); + QCOMPARE(container4->neighbourSeparatorWaste(item5, Side2, Qt::Horizontal), 0); + +} + +void TestMultiSplitter::tst_missingSize() +{ + auto root = createRoot(); + QCOMPARE(root->size(), QSize(1000, 1000)); + QCOMPARE(root->availableSize(), QSize(1000, 1000)); + + Item *item1 = createItem("1"); + item1->setMinSize({100, 100}); + + Item *item2 = createItem("2"); + item2->setMinSize(root->size()); + + Item *item3 = createItem("3"); + item3->setMinSize(root->size() + QSize(100, 200)); + + // Test empty root + QCOMPARE(root->missingSizeFor(item1, Qt::Vertical), QSize(0, 0)); + QCOMPARE(root->missingSizeFor(item2, Qt::Vertical), QSize(0, 0)); + QCOMPARE(root->missingSizeFor(item3, Qt::Vertical), QSize(100, 200)); + + // Test with an existing item + root->insertItem(item1, Location_Top); + QCOMPARE(root->missingSizeFor(item2, Qt::Vertical), item1->minSize() + QSize(0, st)); + QCOMPARE(root->missingSizeFor(item3, Qt::Vertical), item1->minSize() + QSize(0, st) + QSize(100, 200)); +} + +void TestMultiSplitter::tst_ensureEnoughSize() +{ + // Tests that the layout's size grows when the item being inserted wouldn't have enough space + + auto root = createRoot(); /// 1000x1000 + Item *item1 = createItem("1"); + item1->setMinSize({2000, 500}); + + // Insert to empty layout: + + root->insertItem(item1, Location_Left); + QCOMPARE(root->size(), QSize(2000, 1000)); + QCOMPARE(item1->size(), QSize(2000, 1000)); + QCOMPARE(item1->minSize(), root->minSize()); + QVERIFY(root->checkSanity()); + + // Insert to non-empty layout + Item *item2 = createItem("2"); + item2->setMinSize({2000, 2000}); + root->insertItem(item2, Location_Right); + QVERIFY(root->checkSanity()); + QCOMPARE(root->size(), QSize(item1->minSize().width() + item2->minSize().width() + st, item2->minSize().height())); +} + +QTEST_MAIN(TestMultiSplitter) + +#include "tst_multisplitter.moc"