/* This file is part of KDDockWidgets. SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Sérgio Martins SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only Contact KDAB at for commercial licensing options. */ #include "Item_p.h" #include "ItemFreeContainer_p.h" #include "LayoutingHost_p.h" #include "LayoutingGuest_p.h" #include "LayoutingSeparator_p.h" #include "core/Logging_p.h" #include "core/ObjectGuard_p.h" #include "core/ScopedValueRollback_p.h" #include "core/nlohmann_helpers_p.h" #include #include #include #include #ifdef KDDW_FRONTEND_QT #include "core/Platform_p.h" #include #endif enum { LAYOUT_DUMP_INDENT = 6 }; #ifdef Q_CC_MSVC #pragma warning(push) #pragma warning(disable : 4138) #pragma warning(disable : 4244) #pragma warning(disable : 4457) #pragma warning(disable : 4702) #endif // clazy:excludeall=missing-typeinfo,old-style-connect using namespace KDDockWidgets; using namespace KDDockWidgets::Core; int Core::Item::separatorThickness = 5; int Core::Item::layoutSpacing = 5; bool Core::Item::s_silenceSanityChecks = false; DumpScreenInfoFunc Core::Item::s_dumpScreenInfoFunc = nullptr; CreateSeparatorFunc Core::Item::s_createSeparatorFunc = nullptr; // There are the defaults. They can be changed by the user via Config.h API. Size Core::Item::hardcodedMinimumSize = Size(80, 90); Size Core::Item::hardcodedMaximumSize = Size(16777215, 16777215); bool Core::ItemBoxContainer::s_inhibitSimplify = false; LayoutingSeparator *LayoutingSeparator::s_separatorBeingDragged = nullptr; template void safeEmitSignal(Signal &sig, Args &&...args) { // KDBindings now can throw exceptions. // we emit some signals in destructors, which should never throw. // this makes clang-tidy happy. In practice there's no throwing. try { sig.emit(std::forward(args)...); } catch (...) { KDDW_ERROR("Got exception in signal emit!"); } } static bool locationIsVertical(Location loc) { return loc == Location_OnTop || loc == Location_OnBottom; } static bool locationIsSide1(Location loc) { return loc == Location_OnLeft || loc == Location_OnTop; } static 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; } static Qt::Orientation oppositeOrientation(Qt::Orientation o) { return o == Qt::Vertical ? Qt::Horizontal : Qt::Vertical; } static Rect adjustedRect(Rect 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; } namespace KDDockWidgets::Core { struct LengthOnSide { int length = 0; int minLength = 0; int available() const { return std::max(0, length - minLength); } int missing() const { return std::max(0, minLength - length); } }; static NeighbourSqueezeStrategy defaultNeighbourSqueezeStrategy() { return InitialOption::s_defaultNeighbourSqueezeStrategy; } } ItemBoxContainer *Item::root() const { return m_parent ? m_parent->root() : const_cast(object_cast(this)); } Rect Item::mapToRoot(Rect r) const { const Point topLeft = mapToRoot(r.topLeft()); r.moveTopLeft(topLeft); return r; } Point Item::mapToRoot(Point p) const { if (isRoot()) return p; return p + parentContainer()->mapToRoot(pos()); } int Item::mapToRoot(int p, Qt::Orientation o) const { if (o == Qt::Vertical) return mapToRoot(Point(0, p)).y(); return mapToRoot(Point(p, 0)).x(); } Point Item::mapFromRoot(Point p) const { const Item *it = this; while (it) { p = p - it->pos(); it = it->parentContainer(); } return p; } Rect Item::mapFromRoot(Rect r) const { const Point topLeft = mapFromRoot(r.topLeft()); r.moveTopLeft(topLeft); return r; } Point Item::mapFromParent(Point p) const { if (isRoot()) return p; return p - pos(); } int Item::mapFromRoot(int p, Qt::Orientation o) const { if (o == Qt::Vertical) return mapFromRoot(Point(0, p)).y(); return mapFromRoot(Point(p, 0)).x(); } void Item::setGuest(LayoutingGuest *guest) { assert(!guest || !m_guest); m_guest = guest; m_parentChangedConnection.disconnect(); m_guestDestroyedConnection->disconnect(); m_layoutInvalidatedConnection->disconnect(); if (m_guest) { m_guest->setHost(m_host); m_guest->setLayoutItem(this); m_parentChangedConnection = m_guest->hostChanged.connect([this](LayoutingHost *host) { if (host != this->host()) { // Group was detached into floating window. Turn into placeholder assert(isVisible()); turnIntoPlaceholder(); } }); { ScopedValueRollback guard(m_isSettingGuest, true); setMinSize(guest->minSize()); setMaxSizeHint(guest->maxSizeHint()); } m_guestDestroyedConnection = m_guest->beingDestroyed.connect(&Item::onGuestDestroyed, this); m_layoutInvalidatedConnection = guest->layoutInvalidated.connect(&Item::onWidgetLayoutRequested, this); if (m_sizingInfo.geometry.isEmpty()) { // Use the widgets geometry, but ensure it's at least hardcodedMinimumSize Rect widgetGeo = m_guest->geometry(); widgetGeo.setSize( widgetGeo.size().expandedTo(minSize()).expandedTo(Item::hardcodedMinimumSize)); setGeometry(mapFromRoot(widgetGeo)); } else { updateWidgetGeometries(); } } } void Item::updateWidgetGeometries() { if (m_guest) { m_guest->setGeometry(mapToRoot(rect())); } } void Item::to_json(nlohmann::json &json) const { json["sizingInfo"] = m_sizingInfo; json["isVisible"] = m_isVisible; json["isContainer"] = isContainer(); if (m_guest) json["guestId"] = m_guest->id(); // just for coorelation purposes when restoring } void Item::fillFromJson(const nlohmann::json &j, const std::unordered_map &widgets) { m_sizingInfo = j.value("sizingInfo", SizingInfo()); m_isVisible = j.value("isVisible", false); const QString guestId = j.value("guestId", QString()); if (!guestId.isEmpty()) { auto it = widgets.find(guestId); if (it != widgets.cend()) { setGuest(it->second); m_guest->setHost(host()); } else if (host()) { KDDW_ERROR("Couldn't find group to restore for item={}", ( void * )this); assert(false); } } } Item *Item::createFromJson(LayoutingHost *hostWidget, ItemContainer *parent, const nlohmann::json &json, const std::unordered_map &widgets) { auto item = new Item(hostWidget, parent); item->fillFromJson(json, widgets); return item; } void Item::setDumpScreenInfoFunc(DumpScreenInfoFunc f) { s_dumpScreenInfoFunc = f; } void Item::setCreateSeparatorFunc(CreateSeparatorFunc f) { s_createSeparatorFunc = f; } void Item::ref() { m_refCount++; } void Item::unref() { assert(m_refCount > 0); m_refCount--; if (m_refCount == 0) { assert(!isRoot()); parentContainer()->removeItem(this); } } int Item::refCount() const { return m_refCount; } LayoutingHost *Item::host() const { return m_host; } LayoutingGuest *Item::guest() const { return m_guest; } void Item::restore(LayoutingGuest *guest) { if (isVisible() || m_guest) { KDDW_ERROR("Hitting assert. visible={}, guest={}", isVisible(), ( void * )this); assert(false); } if (isContainer()) { KDDW_ERROR("Containers can't be restored"); } else { setGuest(guest); parentContainer()->restore(this); // When we restore to previous positions, we only still from the immediate neighbours. // It's consistent with closing an item, it also only grows the immediate neighbours // By passing ImmediateNeighboursFirst we can hide/show an item multiple times and it // uses the same place } } Vector Item::pathFromRoot() const { // Returns the list of indexes to get to this item, starting from the root container // Example [0, 1, 3] would mean that the item is the 4th child of the 2nd child of the 1st child // of root // [] would mean 'this' is the root item // [0] would mean the 1st child of root Vector path; path.reserve(10); // random big number, good to bootstrap it const Item *it = this; while (it) { if (auto p = it->parentContainer()) { const auto index = p->indexOfChild(it); path.prepend(index); it = p; } else { break; } } return path; } void Item::setHost(LayoutingHost *host) { if (m_host != host) { m_host = host; if (m_guest) { m_guest->setHost(host); m_guest->setVisible(true); updateWidgetGeometries(); } } } void Item::setSize_recursive(Size newSize, ChildrenResizeStrategy) { setSize(newSize); } Size Item::missingSize() const { Size missing = minSize() - this->size(); missing.setWidth(std::max(missing.width(), 0)); missing.setHeight(std::max(missing.height(), 0)); return missing; } bool Item::isBeingInserted() const { return m_sizingInfo.isBeingInserted; } void Item::setBeingInserted(bool is) { m_sizingInfo.isBeingInserted = is; // Trickle up the hierarchy too, as the parent might be hidden due to not having visible // children if (auto parent = parentContainer()) { if (is) { if (!parent->hasVisibleChildren()) parent->setBeingInserted(true); } else { parent->setBeingInserted(false); } } } void Item::setParentContainer(ItemContainer *parent) { if (parent == m_parent) return; if (m_parent) { m_minSizeChangedHandle.disconnect(); m_visibleChangedHandle.disconnect(); visibleChanged.emit(this, false); } if (auto c = asContainer()) { const bool ceasingToBeRoot = !m_parent && parent; if (ceasingToBeRoot && !c->hasVisibleChildren()) { // Was root but is not root anymore. So, if empty, then it has an empty rect too. // Only root can have a non-empty rect without having children c->setGeometry({}); } } m_parent = parent; connectParent(parent); // Reused by the ctor too setParent(parent); } void Item::connectParent(ItemContainer *parent) { if (parent) { m_minSizeChangedHandle = minSizeChanged.connect(&ItemContainer::onChildMinSizeChanged, parent); m_visibleChangedHandle = visibleChanged.connect(&ItemContainer::onChildVisibleChanged, parent); // These virtuals are fine to be called from Item ctor, as the ItemContainer is still empty at this point // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) setHost(parent->host()); // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) updateWidgetGeometries(); // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall) visibleChanged.emit(this, isVisible()); } } ItemContainer *Item::parentContainer() const { return m_parent; } ItemBoxContainer *Item::parentBoxContainer() const { return object_cast(m_parent); } int Item::indexInAncestor(ItemContainer *ancestor, bool visibleOnly) const { auto it = this; while (auto p = it->parentBoxContainer()) { if (p == ancestor) { // We found the ancestor const auto children = visibleOnly ? ancestor->visibleChildren() : ancestor->childItems(); return children.indexOf(const_cast(it)); } it = p; } return -1; } ItemBoxContainer *Item::ancestorBoxContainerWithOrientation(Qt::Orientation o) const { auto p = parentBoxContainer(); while (p) { if (p->orientation() == o) return p; p = p->parentBoxContainer(); } return nullptr; } const ItemContainer *Item::asContainer() const { return object_cast(this); } ItemContainer *Item::asContainer() { return object_cast(this); } ItemBoxContainer *Item::asBoxContainer() { return object_cast(this); } void Item::setMinSize(Size sz) { if (sz != m_sizingInfo.minSize) { m_sizingInfo.minSize = sz; minSizeChanged.emit(this); if (!m_isSettingGuest) setSize_recursive(size().expandedTo(sz)); } } void Item::setMaxSizeHint(Size sz) { if (sz != m_sizingInfo.maxSizeHint) { m_sizingInfo.maxSizeHint = sz; maxSizeChanged.emit(this); } } Item *Item::outermostNeighbor(Location loc, bool visibleOnly) const { Side side = Side1; Qt::Orientation o = Qt::Vertical; switch (loc) { case Location_None: return nullptr; case Location_OnLeft: side = Side1; o = Qt::Horizontal; break; case Location_OnRight: side = Side2; o = Qt::Horizontal; break; case Location_OnTop: side = Side1; o = Qt::Vertical; break; case Location_OnBottom: side = Side2; o = Qt::Vertical; break; } return outermostNeighbor(side, o, visibleOnly); } Item *Item::outermostNeighbor(Side side, Qt::Orientation o, bool visibleOnly) const { auto p = parentBoxContainer(); if (!p) return nullptr; const auto siblings = visibleOnly ? p->visibleChildren() : p->m_children; const int index = siblings.indexOf(const_cast(this)); if (index == -1 || siblings.isEmpty()) { // Doesn't happen KDDW_ERROR("Item::outermostNeighbor: item not in parent's child list"); return nullptr; } const int lastIndex = siblings.count() - 1; if (p->orientation() == o) { if ((index == 0 && side == Side1) || (index == lastIndex && side == Side2)) { // No item on the sides return nullptr; } else { // outermost sibling auto sibling = siblings.at(side == Side1 ? 0 : lastIndex); if (auto siblingContainer = object_cast(sibling)) { if (siblingContainer->orientation() == o) { // case of 2 sibling containers with the same orientation, it's redundant. return siblingContainer->outermostNeighbor(side, o, visibleOnly); } } return sibling; } } else { if (auto ancestor = p->ancestorBoxContainerWithOrientation(o)) { const int indexInAncestor = this->indexInAncestor(ancestor, visibleOnly); if (indexInAncestor == -1) { // Doesn't happen KDDW_ERROR("Item::outermostNeighbor: item not in ancestor's child list"); return nullptr; } else { return ancestor->childItems().at(indexInAncestor)->outermostNeighbor(side, o, visibleOnly); } } else { return nullptr; } } } Size Item::minSize() const { return m_sizingInfo.minSize; } Size Item::maxSizeHint() const { return m_sizingInfo.maxSizeHint.boundedTo(hardcodedMaximumSize); } void Item::setPos(Point pos) { Rect geo = m_sizingInfo.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(); } int Item::x() const { return m_sizingInfo.geometry.x(); } int Item::y() const { return m_sizingInfo.geometry.y(); } int Item::width() const { return m_sizingInfo.geometry.width(); } int Item::height() const { return m_sizingInfo.geometry.height(); } Size Item::size() const { return m_sizingInfo.geometry.size(); } void Item::setSize(Size sz) { ScopedValueRollback guard(m_inSetSize, true); Rect newGeo = m_sizingInfo.geometry; newGeo.setSize(sz); setGeometry(newGeo); } void Item::requestResize(int left, int top, int right, int bottom) { if (left == 0 && right == 0 && top == 0 && bottom == 0) return; ItemBoxContainer *parent = parentBoxContainer(); if (!parent) { // Can't happen KDDW_ERROR("Item::requestResize: Could not find parent container"); return; } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) auto moveSeparators = [](int side1Delta, int side2Delta, LayoutingSeparator *separator1, LayoutingSeparator *separator2) { if (side1Delta != 0 && separator1) { const auto ancestor = separator1->parentContainer(); const int min = ancestor->minPosForSeparator_global(separator1); const int pos = separator1->position(); const int max = ancestor->maxPosForSeparator_global(separator1); int newPos = pos - side1Delta; newPos = bound(min, newPos, max); const int delta = newPos - pos; ancestor->requestSeparatorMove(separator1, delta); } if (side2Delta != 0 && separator2) { const auto ancestor = separator2->parentContainer(); const int min = ancestor->minPosForSeparator_global(separator2); const int pos = separator2->position(); const int max = ancestor->maxPosForSeparator_global(separator2); int newPos = pos + side2Delta; newPos = bound(min, newPos, max); const int delta = newPos - pos; ancestor->requestSeparatorMove(separator2, delta); } }; { // Here we handle resize along the orientation of the container const int side1Delta = parent->isHorizontal() ? left : top; const int side2Delta = parent->isHorizontal() ? right : bottom; auto separator1 = parent->separatorForChild(this, Side1); auto separator2 = parent->separatorForChild(this, Side2); moveSeparators(side1Delta, side2Delta, separator1, separator2); } { // Here we handle resize against the orientation of the container const int side1Delta = parent->isHorizontal() ? top : left; const int side2Delta = parent->isHorizontal() ? bottom : right; auto separator1 = parent->adjacentSeparatorForChild(this, Side1); auto separator2 = parent->adjacentSeparatorForChild(this, Side2); moveSeparators(side1Delta, side2Delta, separator1, separator2); } } Point Item::pos() const { return m_sizingInfo.geometry.topLeft(); } Rect Item::geometry() const { return isBeingInserted() ? Rect() : m_sizingInfo.geometry; } Rect Item::rect() const { return Rect(0, 0, width(), height()); } bool Item::isContainer() const { return m_isContainer; } int Item::minLength(Qt::Orientation o) const { return Core::length(minSize(), o); } int Item::maxLengthHint(Qt::Orientation o) const { return Core::length(maxSizeHint(), o); } void Item::setLength(int length, Qt::Orientation o) { assert(length > 0); if (o == Qt::Vertical) { const int w = std::max(width(), hardcodedMinimumSize.width()); setSize(Size(w, length)); } else { const int h = std::max(height(), hardcodedMinimumSize.height()); setSize(Size(length, h)); } } void Item::setLength_recursive(int length, Qt::Orientation o) { setLength(length, o); } int Item::length(Qt::Orientation o) const { return Core::length(size(), o); } int Item::availableLength(Qt::Orientation o) const { return length(o) - minLength(o); } bool Item::isPlaceholder() const { return !isVisible(); } bool Item::isVisible(bool excludeBeingInserted) const { return m_isVisible && !(excludeBeingInserted && isBeingInserted()); } void Item::setIsVisible(bool is) { if (is != m_isVisible) { m_isVisible = is; visibleChanged.emit(this, is); } if (is && m_guest) { m_guest->setGeometry(mapToRoot(rect())); m_guest->setVisible(true); // Only set visible when apply*() ? } } void Item::setGeometry_recursive(Rect rect) { // Recursiveness doesn't apply for non-container items setGeometry(rect); } bool Item::checkSanity() { if (!root()) return true; if (minSize().width() > width() || minSize().height() > height()) { root()->dumpLayout(); KDDW_ERROR("Size constraints not honoured this={}, min={}, size={}", ( void * )this, minSize(), size()); return false; } if (m_guest) { if (m_guest->host() != host()) { if (root()) root()->dumpLayout(); KDDW_ERROR("Unexpected host for our guest. m_guest->host()={}, host()={}", ( void * )m_guest->host(), ( void * )host()); return false; } // Reminder: m_guest->geometry() is in the coordspace of the host widget (DropArea) // while Item::m_sizingInfo.geometry is in the coordspace of the parent container if (m_guest->geometry() != mapToRoot(rect())) { root()->dumpLayout(); KDDW_ERROR("Guest widget doesn't have correct geometry. m_guest->guestGeometry={}, item.mapToRoot(rect())={}", m_guest->geometry(), mapToRoot(rect())); return false; } } return true; } bool Item::isMDI() const { return object_cast(parentContainer()) != nullptr; } bool Item::inSetSize() const { return m_inSetSize; } void Item::setGeometry(Rect rect) { Rect &m_geometry = m_sizingInfo.geometry; if (rect != m_geometry) { const Rect oldGeo = m_geometry; m_geometry = rect; if (rect.isEmpty()) { // Just a sanity check... ItemContainer *c = asContainer(); if (c) { if (c->hasVisibleChildren()) { if (auto r = root()) r->dumpLayout(); assert(false); } } else { KDDW_ERROR("Empty rect"); } } const Size minSz = minSize(); if (!s_silenceSanityChecks && (rect.width() < minSz.width() || rect.height() < minSz.height())) { if (auto r = root()) r->dumpLayout(); KDDW_ERROR("Constraints not honoured. this={}, sz={}, min={}, parent={}", ( void * )this, rect.size(), minSz, ( void * )parentContainer()); } geometryChanged.emit(); if (oldGeo.x() != x()) xChanged.emit(); if (oldGeo.y() != y()) yChanged.emit(); if (oldGeo.width() != width()) widthChanged.emit(); if (oldGeo.height() != height()) heightChanged.emit(); updateWidgetGeometries(); } } void Item::dumpLayout(int level, bool) { std::string indent(LAYOUT_DUMP_INDENT * size_t(level), ' '); std::cerr << indent << "- Widget: " << m_sizingInfo.geometry // << "r=" << m_geometry.right() << "b=" << m_geometry.bottom() << "; min=" << minSize(); if (maxSizeHint() != hardcodedMaximumSize) std::cerr << "; max=" << maxSizeHint() << "; "; if (!isVisible()) std::cerr << ";hidden;"; // Reminder: that m_guest->geometry() is in the coordspace of the host widget (DropArea) // while Item::m_sizingInfo.geometry is in the coordspace of the parent container. // We print only if different to save space. if (m_guest && geometry() != m_guest->geometry()) { std::cerr << "; guest geometry=" << m_guest->geometry(); } if (m_sizingInfo.isBeingInserted) std::cerr << ";beingInserted;"; std::cerr << "; item=" << this; if (m_guest) std::cerr << "; m_guest=" << m_guest->toDebugString() << "\n"; std::cerr << "\n"; } Item::Item(LayoutingHost *hostWidget, ItemContainer *parent) : Core::Object(parent) , m_isContainer(false) , m_parent(parent) , m_host(hostWidget) { connectParent(parent); } Item::Item(bool isContainer, LayoutingHost *hostWidget, ItemContainer *parent) : Core::Object(parent) , m_isContainer(isContainer) , m_parent(parent) , m_host(hostWidget) { connectParent(parent); } Item::~Item() { m_inDtor = true; safeEmitSignal(aboutToBeDeleted); m_minSizeChangedHandle.disconnect(); m_visibleChangedHandle.disconnect(); m_parentChangedConnection.disconnect(); safeEmitSignal(deleted); } void Item::turnIntoPlaceholder() { assert(!isContainer()); // Turning into placeholder just means hiding it. So we can show it again in its original // position. Call removeItem() so we share the code for making the neighbours grow into the // space that becomes available after hiding this one parentContainer()->removeItem(this, /*hardRemove=*/false); } void Item::onGuestDestroyed() { m_guest = nullptr; m_parentChangedConnection.disconnect(); m_guestDestroyedConnection->disconnect(); if (m_refCount) { turnIntoPlaceholder(); } else if (!isRoot()) { parentContainer()->removeItem(this); } } void Item::onWidgetLayoutRequested() { if (auto w = guest()) { const Size guestSize = w->geometry().size(); if (guestSize != size() && !isMDI()) { // for MDI we allow user/manual arbitrary resize with // mouse std::cerr << "Item::onWidgetLayoutRequested" << "TODO: Not implemented yet. Widget can't just decide to resize yet" << "View.size=" << guestSize << "Item.size=" << size() << m_sizingInfo.geometry << m_sizingInfo.isBeingInserted << "\n"; } if (w->minSize() != minSize()) { setMinSize(m_guest->minSize()); } setMaxSizeHint(w->maxSizeHint()); } } bool Item::isRoot() const { return m_parent == nullptr; } LayoutBorderLocations Item::adjacentLayoutBorders() const { if (isRoot()) { return LayoutBorderLocation_All; } ItemBoxContainer *c = parentBoxContainer(); if (!c) return LayoutBorderLocation_None; const int indexInParent = c->indexOfVisibleChild(this); const int numVisibleChildren = c->numVisibleChildren(); const bool isFirst = indexInParent == 0; const bool isLast = indexInParent == numVisibleChildren - 1; if (indexInParent == -1) return LayoutBorderLocation_None; LayoutBorderLocations locations = LayoutBorderLocation_None; if (c->isRoot()) { if (c->isVertical()) { locations |= LayoutBorderLocation_West; locations |= LayoutBorderLocation_East; if (isFirst) locations |= LayoutBorderLocation_North; if (isLast) locations |= LayoutBorderLocation_South; } else { locations |= LayoutBorderLocation_North; locations |= LayoutBorderLocation_South; if (isFirst) locations |= LayoutBorderLocation_West; if (isLast) locations |= LayoutBorderLocation_East; } } else { const LayoutBorderLocations parentBorders = c->adjacentLayoutBorders(); if (c->isVertical()) { if (parentBorders & LayoutBorderLocation_West) locations |= LayoutBorderLocation_West; if (parentBorders & LayoutBorderLocation_East) locations |= LayoutBorderLocation_East; if (isFirst && (parentBorders & LayoutBorderLocation_North)) locations |= LayoutBorderLocation_North; if (isLast && (parentBorders & LayoutBorderLocation_South)) locations |= LayoutBorderLocation_South; } else { if (parentBorders & LayoutBorderLocation_North) locations |= LayoutBorderLocation_North; if (parentBorders & LayoutBorderLocation_South) locations |= LayoutBorderLocation_South; if (isFirst && (parentBorders & LayoutBorderLocation_West)) locations |= LayoutBorderLocation_West; if (isLast && (parentBorders & LayoutBorderLocation_East)) locations |= LayoutBorderLocation_East; } } return locations; } int Item::visibleCount_recursive() const { return isVisible() ? 1 : 0; } struct ItemBoxContainer::Private { explicit Private(ItemBoxContainer *qq) : q(qq) { if (!Item::s_createSeparatorFunc) { KDDW_ERROR("Item doesn't know how to create separators! Aborting.\n" "If you're using the layouting engine outside of KDDW, don't forget" " to call KDDockWidgets::Core::Item::createSeparatorFunc()"); std::abort(); } } ~Private() { for (const auto &sep : std::as_const(m_separators)) sep->free(); m_separators.clear(); } // length means height if the container is vertical, otherwise width int defaultLengthFor(Item *item, const InitialOption &option) const; void relayoutIfNeeded(); const Item *itemFromPath(const Vector &path) const; void resizeChildren(Size oldSize, Size newSize, SizingInfo::List &sizes, ChildrenResizeStrategy); void honourMaxSizes(SizingInfo::List &sizes); void scheduleCheckSanity() const; LayoutingSeparator *neighbourSeparator(const Item *item, Side, Qt::Orientation) const; LayoutingSeparator *neighbourSeparator_recursive(const Item *item, Side, Qt::Orientation) const; void updateWidgets_recursive(); /// Returns the positions that each separator should have (x position if Qt::Horizontal, y /// otherwise) Vector requiredSeparatorPositions() const; void updateSeparators(); void deleteSeparators(); LayoutingSeparator *separatorAt(int p) const; Vector childPercentages() const; bool isDummy() const; void deleteSeparators_recursive(); void updateSeparators_recursive(); Size minSize(const Item::List &items) const; int excessLength() const; mutable bool m_checkSanityScheduled = false; Vector m_separators; bool m_convertingItemToContainer = false; bool m_blockUpdatePercentages = false; bool m_isDeserializing = false; bool m_isSimplifying = false; Qt::Orientation m_orientation = Qt::Vertical; ItemBoxContainer *const q; }; ItemBoxContainer::ItemBoxContainer(LayoutingHost *hostWidget, ItemContainer *parent) : ItemContainer(hostWidget, parent) , d(new Private(this)) { assert(parent); } ItemBoxContainer::ItemBoxContainer(LayoutingHost *hostWidget) : ItemContainer(hostWidget, /*parent=*/nullptr) , d(new Private(this)) { } ItemBoxContainer::~ItemBoxContainer() { delete d; } int ItemBoxContainer::numSideBySide_recursive(Qt::Orientation o) const { int num = 0; if (d->m_orientation == o) { // Example: Container is horizontal and we want to know how many layouted horizontally for (Item *child : m_children) { if (ItemBoxContainer *container = child->asBoxContainer()) { num += container->numSideBySide_recursive(o); } else if (!child->isPlaceholder()) { num++; } } } else { // Example: Container is vertical and we want to know how many layouted horizontally for (Item *child : m_children) { if (ItemBoxContainer *container = child->asBoxContainer()) { num = std::max(num, container->numSideBySide_recursive(o)); } else if (!child->isPlaceholder()) { num = std::max(num, 1); } } } return num; } bool ItemBoxContainer::percentagesAreSane() const { const Item::List visibleChildren = this->visibleChildren(); const Vector percentages = d->childPercentages(); const double totalPercentage = std::accumulate(percentages.begin(), percentages.end(), 0.0); const double expectedPercentage = visibleChildren.isEmpty() ? 0.0 : 1.0; if (!fuzzyCompare(totalPercentage, expectedPercentage)) { root()->dumpLayout(); KDDW_ERROR("Percentages don't add up", totalPercentage, percentages, ( void * )this); return false; } return true; } bool ItemBoxContainer::checkSanity() { d->m_checkSanityScheduled = false; #ifdef KDDW_FRONTEND_QT auto plat = Platform::instance(); if (!plat || plat->d->inDestruction()) { // checkSanity() can be called with deleteLater(), so check if we still // have a platform return true; } #endif if (!host()) { /// This is a dummy ItemBoxContainer, just return true return true; } if (!Item::checkSanity()) return false; if (numChildren() == 0 && !isRoot()) { KDDW_ERROR("Container is empty. Should be deleted"); return false; } if (d->m_orientation != Qt::Vertical && d->m_orientation != Qt::Horizontal) { KDDW_ERROR("Invalid orientation={}, this={}", d->m_orientation, ( void * )this); return false; } // Check that the geometries don't overlap int expectedPos = 0; const auto children = childItems(); for (Item *item : children) { if (!item->isVisible()) continue; const int pos = Core::pos(item->pos(), d->m_orientation); if (expectedPos != pos) { root()->dumpLayout(); KDDW_ERROR("Unexpected pos={}, expected={}, item={}, isContainer={}", pos, expectedPos, ( void * )item, item->isContainer()); return false; } expectedPos = pos + Core::length(item->size(), d->m_orientation) + layoutSpacing; } const int h1 = Core::length(size(), oppositeOrientation(d->m_orientation)); for (Item *item : children) { if (item->parentContainer() != this) { KDDW_ERROR("Invalid parent container for item={}, is={}, expected={}", ( void * )item, ( void * )item->parentContainer(), ( void * )this); return false; } if (item->parent() != this) { KDDW_ERROR("Invalid Object parent for item={}, is={}, expected={}", ( void * )item, ( void * )item->parent(), ( void * )this); return false; } if (item->isVisible()) { // Check the children height (if horizontal, and vice-versa) const int h2 = Core::length(item->size(), oppositeOrientation(d->m_orientation)); if (h1 != h2) { root()->dumpLayout(); KDDW_ERROR("Invalid size for item {}, Container.length={}, item.length={}", ( void * )item, h1, h2); return false; } if (!rect().contains(item->geometry())) { root()->dumpLayout(); KDDW_ERROR("Item geo is out of bounds. item={}, geo={}, parent.rect={}", ( void * )item, item->geometry(), rect()); return false; } } if (!item->checkSanity()) return false; } const Item::List visibleChildren = this->visibleChildren(); const bool isEmptyRoot = isRoot() && visibleChildren.isEmpty(); if (!isEmptyRoot) { auto occupied = std::max(0, Item::layoutSpacing * (int(visibleChildren.size()) - 1)); for (Item *item : visibleChildren) { occupied += item->length(d->m_orientation); } if (occupied != length()) { root()->dumpLayout(); KDDW_ERROR("Unexpected length. Expected={}, got={}, this={}", occupied, length(), ( void * )this); return false; } if (!percentagesAreSane()) { // Percentages might be broken due to buggy old layouts. Try to fix them: const_cast(this)->d->updateSeparators_recursive(); if (!percentagesAreSane()) return false; } } const auto numVisibleChildren = int(visibleChildren.size()); if (d->m_separators.size() != std::max(0, numVisibleChildren - 1)) { root()->dumpLayout(); KDDW_ERROR("Unexpected number of separators sz={}, numVisibleChildren={}", d->m_separators.size(), numVisibleChildren); return false; } const Size expectedSeparatorSize = isVertical() ? Size(width(), Item::separatorThickness) : Size(Item::separatorThickness, height()); const int pos2 = Core::pos(mapToRoot(Point(0, 0)), oppositeOrientation(d->m_orientation)); for (int i = 0; i < d->m_separators.size(); ++i) { LayoutingSeparator *separator = d->m_separators.at(i); Item *item = visibleChildren.at(i); const int expectedSeparatorPos = mapToRoot(item->m_sizingInfo.edge(d->m_orientation) + 1, d->m_orientation); if (separator->m_host != host()) { KDDW_ERROR("Invalid host widget for separator this={}", ( void * )this); return false; } if (separator->parentContainer() != this) { KDDW_ERROR("Invalid parent container for separator parent={}, separator={}, this={}", ( void * )separator->parentContainer(), ( void * )separator, ( void * )this); return false; } if (separator->position() != expectedSeparatorPos) { root()->dumpLayout(); KDDW_ERROR("Unexpected separator position, expected={}, separator={}, this={}", separator->position(), expectedSeparatorPos, ( void * )separator, ( void * )this); return false; } const Rect separatorGeometry = separator->geometry(); if (separatorGeometry.size() != expectedSeparatorSize) { KDDW_ERROR("Unexpected separator size={}, expected={}, separator={}, this={}", separatorGeometry.size(), expectedSeparatorSize, ( void * )separator, ( void * )this); return false; } const int separatorPos2 = Core::pos(separatorGeometry.topLeft(), oppositeOrientation(d->m_orientation)); if (Core::pos(separatorGeometry.topLeft(), oppositeOrientation(d->m_orientation)) != pos2) { root()->dumpLayout(); KDDW_ERROR("Unexpected position pos2={}, expected={}, separator={}, this={}", separatorPos2, pos2, ( void * )separator, ( void * )this); return false; } // Check that the separator bounds are correct. We can't always honour widget's max-size // constraints, so only honour min-size const int separatorMinPos = minPosForSeparator_global(separator, /*honourMax=*/false); const int separatorMaxPos = maxPosForSeparator_global(separator, /*honourMax=*/false); const int separatorPos = separator->position(); if (separatorPos < separatorMinPos || separatorPos > separatorMaxPos || separatorMinPos < 0 || separatorMaxPos <= 0) { root()->dumpLayout(); KDDW_ERROR("Invalid bounds for separator, pos={}, min={}, max={}, separator={}", separatorPos, separatorMinPos, separatorMaxPos, ( void * )separator); return false; } } #ifdef DOCKS_DEVELOPER_MODE // Can cause slowdown, so just use it in developer mode. if (isRoot()) { if (!asBoxContainer()->test_suggestedRect()) return false; } #endif return true; } void ItemBoxContainer::Private::scheduleCheckSanity() const { #ifdef KDDW_FRONTEND_QT if (!m_checkSanityScheduled) { m_checkSanityScheduled = true; QTimer::singleShot(0, q->root(), &ItemBoxContainer::checkSanity); } #endif } bool ItemBoxContainer::hasOrientation() const { return isVertical() || isHorizontal(); } int ItemBoxContainer::indexOfVisibleChild(const Item *item) const { const Item::List items = visibleChildren(); return items.indexOf(const_cast(item)); } void ItemBoxContainer::restore(Item *child) { restoreChild(child, false, NeighbourSqueezeStrategy::ImmediateNeighboursFirst); } void ItemBoxContainer::removeItem(Item *item, bool hardRemove) { assert(!item->isRoot()); if (!contains(item)) { if (item->parentContainer() == this) { // This should never happen, but we've seen infinite recursion here before KDDW_ERROR("ItemBoxContainer::removeItem: Could not find item as children, but it has us as parent!"); assert(false); // crash early during debug return; } // Not ours, ask parent item->parentContainer()->removeItem(item, hardRemove); return; } Item *side1Item = visibleNeighbourFor(item, Side1); Item *side2Item = visibleNeighbourFor(item, Side2); const bool isContainer = item->isContainer(); const bool wasVisible = !isContainer && item->isVisible(); if (hardRemove) { m_children.removeOne(item); delete item; if (!isContainer) root()->numItemsChanged.emit(); } else { item->setIsVisible(false); item->setGuest(nullptr); if (!wasVisible && !isContainer) { // Was already hidden return; } } if (wasVisible) { root()->numVisibleItemsChanged.emit(root()->numVisibleChildren()); } if (isEmpty()) { // Empty container is useless, delete it if (auto p = parentContainer()) p->removeItem(this, /*hardRemove=*/true); } else if (!hasVisibleChildren()) { if (auto p = parentContainer()) { p->removeItem(this, /*hardRemove=*/false); setGeometry(Rect()); } } else { // Neighbours will occupy the space of the deleted item growNeighbours(side1Item, side2Item); itemsChanged.emit(); updateSizeConstraints(); d->updateSeparators_recursive(); } } void ItemBoxContainer::setGeometry_recursive(Rect rect) { setPos(rect.topLeft()); // Call resize, which is recursive and will resize the children too setSize_recursive(rect.size()); } ItemBoxContainer *ItemBoxContainer::convertChildToContainer(Item *leaf, const InitialOption &opt) { ScopedValueRollback converting(d->m_convertingItemToContainer, true); const auto index = m_children.indexOf(leaf); assert(index != -1); auto container = new ItemBoxContainer(host(), this); container->setParentContainer(nullptr); container->setParentContainer(this); auto option = opt; option.sizeMode = DefaultSizeMode::NoDefaultSizeMode; insertItem(container, index, option); m_children.removeOne(leaf); container->setGeometry(leaf->isVisible() ? leaf->geometry() : Rect()); if (!leaf->isVisible()) option.visibility = InitialVisibilityOption::StartHidden; container->insertItem(leaf, Location_OnTop, option); itemsChanged.emit(); d->updateSeparators_recursive(); return container; } /** static */ // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void ItemBoxContainer::insertItemRelativeTo(Item *item, Item *relativeTo, Location loc, const KDDockWidgets::InitialOption &option) { assert(item != relativeTo); if (auto asContainer = relativeTo->asBoxContainer()) { asContainer->insertItem(item, loc, option); return; } item->setIsVisible(!option.startsHidden()); assert(!(option.startsHidden() && item->isContainer())); ItemBoxContainer *parent = relativeTo->parentBoxContainer(); if (!parent) { KDDW_ERROR("This method should only be called for box containers parent={}", ( void * )item->parent()); return; } if (parent->hasOrientationFor(loc)) { const bool locIsSide1 = locationIsSide1(loc); auto indexInParent = parent->indexOfChild(relativeTo); if (!locIsSide1) indexInParent++; const Qt::Orientation orientation = orientationForLocation(loc); if (orientation != parent->orientation()) { assert(parent->visibleChildren().size() == 1); // This is the case where the container only has one item, so it's both vertical and // horizontal Now its orientation gets defined parent->setOrientation(orientation); } parent->insertItem(item, indexInParent, option); } else { ItemBoxContainer *container = parent->convertChildToContainer(relativeTo, option); container->insertItem(item, loc, option); } } void ItemBoxContainer::insertItem(Item *item, Location loc, const KDDockWidgets::InitialOption &initialOption) { assert(item != this); if (contains(item)) { KDDW_ERROR("Item already exists"); return; } item->setIsVisible(!initialOption.startsHidden()); assert(!(initialOption.startsHidden() && item->isContainer())); 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 d->m_orientation = locOrientation; } const auto index = locationIsSide1(loc) ? 0 : m_children.size(); insertItem(item, index, initialOption); } else { // Inserting directly in a container ? Only if it's root. assert(isRoot()); auto container = new ItemBoxContainer(host(), this); container->setGeometry(rect()); container->setChildren(m_children, d->m_orientation); m_children.clear(); setOrientation(oppositeOrientation(d->m_orientation)); insertItem(container, 0, {}); // Now we have the correct orientation, we can insert insertItem(item, loc, initialOption); if (!container->hasVisibleChildren()) container->setGeometry(Rect()); } d->updateSeparators_recursive(); d->scheduleCheckSanity(); } void ItemBoxContainer::onChildMinSizeChanged(Item *child) { if (d->m_convertingItemToContainer || d->m_isDeserializing || !child->isVisible()) { // Don't bother our parents, we're converting return; } updateSizeConstraints(); if (child->isBeingInserted()) return; if (numVisibleChildren() == 1 && child->isVisible()) { // The easy case. Child is alone in the layout, occupies everything. child->setGeometry(rect()); updateChildPercentages(); return; } const Size missingForChild = child->missingSize(); if (!missingForChild.isNull()) { // Child has some growing to do. It will grow left and right equally, (and top-bottom), as // needed. growItem(child, Core::length(missingForChild, d->m_orientation), GrowthStrategy::BothSidesEqually, defaultNeighbourSqueezeStrategy()); } updateChildPercentages(); } void ItemBoxContainer::updateSizeConstraints() { const Size missingSize = this->missingSize(); if (!missingSize.isNull()) { if (isRoot()) { // Resize the whole layout setSize_recursive(size() + missingSize); } } // Our min-size changed, notify our parent, and so on until it reaches root() minSizeChanged.emit(this); } void ItemBoxContainer::onChildVisibleChanged(Item *, bool visible) { if (d->m_isDeserializing || isInSimplify()) return; const int numVisible = numVisibleChildren(); if (visible && numVisible == 1) { // Child became visible and there's only 1 visible child. Meaning there were 0 visible // before. visibleChanged.emit(this, true); } else if (!visible && numVisible == 0) { visibleChanged.emit(this, false); } } Rect ItemBoxContainer::suggestedDropRect(const Item *item, const Item *relativeTo, Location loc) const { // Returns the drop rect. This is the geometry used by the rubber band when you hover over an // indicator. It's calculated by copying the layout and inserting the item into the // dummy/invisible copy The we see which geometry the item got. This way the returned geometry // is always what the item will get if you drop it. One exception is if the window doesn't have // enough space and it would grow. In this case we fall back to something reasonable if (relativeTo && !relativeTo->parentContainer()) { KDDW_ERROR("No parent container"); return {}; } if (relativeTo && relativeTo->parentContainer() != this) { KDDW_ERROR("Called on the wrong container"); return {}; } if (relativeTo && !relativeTo->isVisible()) { KDDW_ERROR("relative to isn't visible"); return {}; } if (loc == Location_None) { KDDW_ERROR("Invalid location"); return {}; } const Size availableSize = root()->availableSize(); const Size minSize = item->minSize(); const bool isEmpty = !root()->hasVisibleChildren(); const int extraWidth = (isEmpty || locationIsVertical(loc)) ? 0 : Item::layoutSpacing; const int extraHeight = (isEmpty || !locationIsVertical(loc)) ? 0 : Item::layoutSpacing; const bool windowNeedsGrowing = availableSize.width() < minSize.width() + extraWidth || availableSize.height() < minSize.height() + extraHeight; if (windowNeedsGrowing) return suggestedDropRectFallback(item, relativeTo, loc); nlohmann::json rootSerialized; root()->to_json(rootSerialized); ItemBoxContainer rootCopy(nullptr); rootCopy.fillFromJson(rootSerialized, {}); if (relativeTo) relativeTo = rootCopy.d->itemFromPath(relativeTo->pathFromRoot()); nlohmann::json itemSerialized; item->to_json(itemSerialized); auto itemCopy = new Item(nullptr); itemCopy->fillFromJson(itemSerialized, {}); const InitialOption opt = DefaultSizeMode::FairButFloor; if (relativeTo) { auto r = const_cast(relativeTo); ItemBoxContainer::insertItemRelativeTo(itemCopy, r, loc, opt); } else { rootCopy.insertItem(itemCopy, loc, opt); } if (rootCopy.size() != root()->size()) { // Doesn't happen KDDW_ERROR("The root copy grew ?! copy={}, sz={}, loc={}", rootCopy.size(), root()->size(), loc); return suggestedDropRectFallback(item, relativeTo, loc); } return itemCopy->mapToRoot(itemCopy->rect()); } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) Rect ItemBoxContainer::suggestedDropRectFallback(const Item *item, const Item *relativeTo, Location loc) const { const Size minSize = item->minSize(); const int itemMin = Core::length(minSize, d->m_orientation); const int available = availableLength() - Item::layoutSpacing; if (relativeTo) { int suggestedPos = 0; const Rect relativeToGeo = relativeTo->geometry(); const int suggestedLength = relativeTo->length(orientationForLocation(loc)) / 2; switch (loc) { case Location_OnLeft: suggestedPos = relativeToGeo.x(); break; case Location_OnTop: suggestedPos = relativeToGeo.y(); break; case Location_OnRight: suggestedPos = relativeToGeo.right() - suggestedLength + 1; break; case Location_OnBottom: suggestedPos = relativeToGeo.bottom() - suggestedLength + 1; break; default: assert(false); } Rect rect; if (orientationForLocation(loc) == Qt::Vertical) { rect.setTopLeft(Point(relativeTo->x(), suggestedPos)); rect.setSize(Size(relativeTo->width(), suggestedLength)); } else { rect.setTopLeft(Point(suggestedPos, relativeTo->y())); rect.setSize(Size(suggestedLength, relativeTo->height())); } return mapToRoot(rect); } else if (isRoot()) { // Relative to the window itself Rect rect = this->rect(); const int oneThird = length() / 3; const int suggestedLength = std::max(std::min(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 { KDDW_ERROR("Shouldn't happen"); } return {}; } void ItemBoxContainer::positionItems() { SizingInfo::List sizes = this->sizes(); positionItems(/*by-ref=*/sizes); applyPositions(sizes); d->updateSeparators_recursive(); } void ItemBoxContainer::positionItems_recursive() { positionItems(); for (Item *item : std::as_const(m_children)) { if (item->isVisible()) { if (auto c = item->asBoxContainer()) c->positionItems_recursive(); } } } void ItemBoxContainer::applyPositions(const SizingInfo::List &sizes) { const Item::List items = visibleChildren(); const auto count = items.size(); assert(count == sizes.size()); for (int i = 0; i < count; ++i) { Item *item = items.at(i); const SizingInfo &sizing = sizes[i]; if (sizing.isBeingInserted) { continue; } const Qt::Orientation oppositeOrientation = ::oppositeOrientation(d->m_orientation); // If the layout is horizontal, the item will have the height of the container. And // vice-versa item->setLength_recursive(sizing.length(oppositeOrientation), oppositeOrientation); item->setPos(sizing.geometry.topLeft()); } } Qt::Orientation ItemBoxContainer::orientation() const { return d->m_orientation; } void ItemBoxContainer::positionItems(SizingInfo::List &sizes) { int nextPos = 0; const auto count = sizes.count(); const Qt::Orientation oppositeOrientation = ::oppositeOrientation(d->m_orientation); for (auto i = 0; i < count; ++i) { SizingInfo &sizing = sizes[i]; if (sizing.isBeingInserted) { nextPos += Item::layoutSpacing; continue; } // If the layout is horizontal, the item will have the height of the container. And // vice-versa const int oppositeLength = Core::length(size(), oppositeOrientation); sizing.setLength(oppositeLength, oppositeOrientation); sizing.setPos(0, oppositeOrientation); sizing.setPos(nextPos, d->m_orientation); nextPos += sizing.length(d->m_orientation) + Item::layoutSpacing; } } void ItemBoxContainer::clear() { for (Item *item : std::as_const(m_children)) { if (ItemBoxContainer *container = item->asBoxContainer()) container->clear(); delete item; } m_children.clear(); d->deleteSeparators(); } Item *ItemBoxContainer::itemAt(Point p) const { for (Item *item : std::as_const(m_children)) { if (item->isVisible() && item->geometry().contains(p)) return item; } return nullptr; } Item *ItemBoxContainer::itemAt_recursive(Point p) const { if (Item *item = itemAt(p)) { if (auto c = item->asBoxContainer()) { return c->itemAt_recursive(c->mapFromParent(p)); } else { return item; } } return nullptr; } void ItemBoxContainer::setHost(LayoutingHost *host) { Item::setHost(host); d->deleteSeparators_recursive(); for (Item *item : std::as_const(m_children)) { item->setHost(host); } d->updateSeparators_recursive(); } void ItemBoxContainer::setIsVisible(bool) { // no-op for containers, visibility is calculated } bool ItemBoxContainer::isVisible(bool excludeBeingInserted) const { return hasVisibleChildren(excludeBeingInserted); } void ItemBoxContainer::setLength_recursive(int length, Qt::Orientation o) { Size sz = size(); if (o == Qt::Vertical) { sz.setHeight(length); } else { sz.setWidth(length); } setSize_recursive(sz); } void ItemBoxContainer::insertItem(Item *item, int index, const InitialOption &option) { const bool containerWasVisible = hasVisibleChildren(true); if (option.sizeMode != DefaultSizeMode::NoDefaultSizeMode) { /// Choose a nice main-axis length for the item we're adding /// aka nice height if container is vertical (and vice-versa) const int suggestedLength = d->defaultLengthFor(item, option); item->setLength_recursive(suggestedLength, d->m_orientation); if (!containerWasVisible) { // The container only had hidden items, since it will // be single visible child, we can honour the child's cross-axis length // example: If the container is vertically, we'll honour the new item's // preferred height. But if the container wasn't visible, we can also // honour the child's preferred width. // horizontal if container is vertical, and vice-versa const auto crossAxis = oppositeOrientation(d->m_orientation); // For the scenario where "this" container is vertical, this reads as: // If option has preferred width, set preferred width on item if (option.hasPreferredLength(crossAxis)) { // preferred should not be bigger than minimum const auto l = std::max(item->minLength(crossAxis), option.preferredLength(crossAxis)); item->setLength_recursive(l, crossAxis); } } } m_children.insert(index, item); item->setParentContainer(this); itemsChanged.emit(); if (!d->m_convertingItemToContainer && item->isVisible()) { // Case of inserting with an hidden relativeTo. This container needs to be restored as well. const bool restoreItself = !containerWasVisible && m_children.count() > 1; restoreChild(item, restoreItself, option.neighbourSqueezeStrategy); } const bool shouldEmitVisibleChanged = item->isVisible(); if (!d->m_convertingItemToContainer && !s_inhibitSimplify) simplify(); if (shouldEmitVisibleChanged) root()->numVisibleItemsChanged.emit(root()->numVisibleChildren()); root()->numItemsChanged.emit(); } bool ItemBoxContainer::hasOrientationFor(Location loc) const { if (m_children.size() <= 1) return true; return d->m_orientation == orientationForLocation(loc); } int ItemBoxContainer::usableLength() const { const Item::List children = visibleChildren(); const auto numVisibleChildren = children.size(); if (children.size() <= 1) return Core::length(size(), d->m_orientation); const int separatorWaste = layoutSpacing * (numVisibleChildren - 1); return length() - separatorWaste; } void ItemBoxContainer::setChildren(const List &children, Qt::Orientation o) { m_children = children; for (Item *item : children) item->setParentContainer(this); setOrientation(o); } void ItemBoxContainer::setOrientation(Qt::Orientation o) { if (o != d->m_orientation) { d->m_orientation = o; d->updateSeparators_recursive(); } } Size ItemBoxContainer::Private::minSize(const Item::List &items) const { int minW = 0; int minH = 0; int numVisible = 0; if (!q->m_children.isEmpty()) { for (Item *item : items) { if (!(item->isVisible() || item->isBeingInserted())) continue; numVisible++; if (q->isVertical()) { minW = std::max(minW, item->minSize().width()); minH += item->minSize().height(); } else { minH = std::max(minH, item->minSize().height()); minW += item->minSize().width(); } } const int separatorWaste = std::max(0, (numVisible - 1) * layoutSpacing); if (q->isVertical()) minH += separatorWaste; else minW += separatorWaste; } return Size(minW, minH); } Size ItemBoxContainer::minSize() const { return d->minSize(m_children); } Size ItemBoxContainer::maxSizeHint() const { int maxW = isVertical() ? hardcodedMaximumSize.width() : 0; int maxH = isVertical() ? 0 : hardcodedMaximumSize.height(); const Item::List visibleChildren = this->visibleChildren(/*includeBeingInserted=*/false); if (!visibleChildren.isEmpty()) { for (Item *item : visibleChildren) { if (item->isBeingInserted()) continue; const Size itemMaxSz = item->maxSizeHint(); const int itemMaxWidth = itemMaxSz.width(); const int itemMaxHeight = itemMaxSz.height(); if (isVertical()) { maxW = std::min(maxW, itemMaxWidth); maxH = std::min(maxH + itemMaxHeight, hardcodedMaximumSize.height()); } else { maxH = std::min(maxH, itemMaxHeight); maxW = std::min(maxW + itemMaxWidth, hardcodedMaximumSize.width()); } } const auto separatorWaste = (int(visibleChildren.size()) - 1) * layoutSpacing; if (isVertical()) { maxH = std::min(maxH + separatorWaste, hardcodedMaximumSize.height()); } else { maxW = std::min(maxW + separatorWaste, hardcodedMaximumSize.width()); } } if (maxW == 0) maxW = hardcodedMaximumSize.width(); if (maxH == 0) maxH = hardcodedMaximumSize.height(); return Size(maxW, maxH).expandedTo(d->minSize(visibleChildren)); } void ItemBoxContainer::Private::resizeChildren(Size oldSize, Size newSize, SizingInfo::List &childSizes, ChildrenResizeStrategy strategy) { // This container is being resized to @p newSize, so we must resize our children too, based // on @p strategy. // The new sizes are applied to @p childSizes, which will be applied to the widgets when we're // done const Vector childPercentages = this->childPercentages(); const auto count = childSizes.count(); const bool widthChanged_ = oldSize.width() != newSize.width(); const bool heightChanged_ = oldSize.height() != newSize.height(); const bool lengthChanged_ = (q->isVertical() && heightChanged_) || (q->isHorizontal() && widthChanged_); const int totalNewLength = q->usableLength(); std::function indexOfCentralFrame; indexOfCentralFrame = [&indexOfCentralFrame](const Item::List &children) -> int { for (auto *child : children) { const auto *guest = child->guest(); const bool isCentralFrame = guest && (guest->flags() & IsCentralFrame); if (isCentralFrame) { return children.indexOf(child); } else if (child->isContainer()) { auto container = dynamic_cast(child); int index = indexOfCentralFrame(container->visibleChildren()); if (index != -1) return children.indexOf(child); } } return -1; }; if (strategy == ChildrenResizeStrategy::GiveDropAreaWithCentralFrameAllExtra) { auto children = q->visibleChildren(); int index = children.count() > 1 ? indexOfCentralFrame(children) : -1; if (index == -1) { strategy = ChildrenResizeStrategy::Percentage; } else { int remaining = totalNewLength; for (int i = 0; i < count; ++i) { const bool isCentralFrame = i == index; if (isCentralFrame) continue; const SizingInfo &itemSize = childSizes[i]; remaining -= itemSize.length(q->orientation()); } SizingInfo &itemSize = childSizes[index]; if (q->isVertical()) { itemSize.geometry.setSize({ q->width(), remaining }); } else { itemSize.geometry.setSize({ remaining, q->height() }); } } } if (strategy == ChildrenResizeStrategy::Percentage) { // In this strategy mode, each children will preserve its current relative size. So, if a // child is occupying 50% of this container, then it will still occupy that after the // container resize int remaining = totalNewLength; for (int i = 0; i < count; ++i) { const bool isLast = i == count - 1; SizingInfo &itemSize = childSizes[i]; const double childPercentage = childPercentages.at(i); const int newItemLength = lengthChanged_ ? (isLast ? remaining : int(childPercentage * totalNewLength)) : itemSize.length(m_orientation); if (newItemLength <= 0) { q->root()->dumpLayout(); KDDW_ERROR("Invalid resize newItemLength={}", newItemLength); assert(false); return; } remaining = remaining - newItemLength; if (q->isVertical()) { itemSize.geometry.setSize({ q->width(), newItemLength }); } else { itemSize.geometry.setSize({ newItemLength, q->height() }); } } } else if (strategy == ChildrenResizeStrategy::Side1SeparatorMove || strategy == ChildrenResizeStrategy::Side2SeparatorMove) { int remaining = Core::length( newSize - oldSize, m_orientation); // This is how much we need to give to children (when growing the // container), or to take from them when shrinking the container const bool isGrowing = remaining > 0; remaining = std::abs(remaining); // Easier to deal in positive numbers // We're resizing the container, and need to decide if we start resizing the 1st children or // in reverse order. If the separator is being dragged left or top, then // isSide1SeparatorMove is true. If isSide1SeparatorMove is true and we're growing, then it // means this container is on the right/bottom of the separator, so should resize its first // children first. Same logic for the other 3 cases const bool isSide1SeparatorMove = strategy == ChildrenResizeStrategy::Side1SeparatorMove; bool resizeHeadFirst = false; if (isGrowing && isSide1SeparatorMove) { resizeHeadFirst = true; } else if (isGrowing && !isSide1SeparatorMove) { resizeHeadFirst = false; } else if (!isGrowing && isSide1SeparatorMove) { resizeHeadFirst = false; } else if (!isGrowing && !isSide1SeparatorMove) { resizeHeadFirst = true; } for (int i = 0; i < count; i++) { const auto index = resizeHeadFirst ? i : count - 1 - i; SizingInfo &size = childSizes[index]; if (isGrowing) { // Since we don't honour item max-size yet, it can just grow all it wants size.incrementLength(remaining, m_orientation); remaining = 0; // and we're done, the first one got everything } else { const int availableToGive = size.availableLength(m_orientation); const int took = std::min(availableToGive, remaining); size.incrementLength(-took, m_orientation); remaining -= took; } if (remaining == 0) break; } } honourMaxSizes(childSizes); } void ItemBoxContainer::Private::honourMaxSizes(SizingInfo::List &sizes) { // Reduces the size of all children that are bigger than max-size. // Assuming there's widgets that are willing to grow to occupy that space. int amountNeededToShrink = 0; int amountAvailableToGrow = 0; Vector indexesOfShrinkers; Vector indexesOfGrowers; for (int i = 0; i < sizes.count(); ++i) { SizingInfo &info = sizes[i]; const int neededToShrink = info.neededToShrink(m_orientation); const int availableToGrow = info.availableToGrow(m_orientation); if (neededToShrink > 0) { amountNeededToShrink += neededToShrink; indexesOfShrinkers.push_back(i); // clazy:exclude=reserve-candidates } else if (availableToGrow > 0) { amountAvailableToGrow = std::min(amountAvailableToGrow + availableToGrow, q->length()); indexesOfGrowers.push_back(i); // clazy:exclude=reserve-candidates } } // Don't grow more than what's needed amountAvailableToGrow = std::min(amountNeededToShrink, amountAvailableToGrow); // Don't shrink more than what's available to grow amountNeededToShrink = std::min(amountAvailableToGrow, amountNeededToShrink); if (amountNeededToShrink == 0 || amountAvailableToGrow == 0) return; // We gathered who needs to shrink and who can grow, now try to do it evenly so that all // growers participate, and not just one giving everything. // Do the growing: while (amountAvailableToGrow > 0) { // Each grower will grow a bit (round-robin) auto toGrow = std::max(1, amountAvailableToGrow / int(indexesOfGrowers.size())); // TODO: Use cbegin/cend once we drop Qt 5 for (auto it = indexesOfGrowers.begin(); it != indexesOfGrowers.end();) { const int index = *it; SizingInfo &sizing = sizes[index]; const auto grew = std::min(sizing.availableToGrow(m_orientation), toGrow); sizing.incrementLength(grew, m_orientation); amountAvailableToGrow -= grew; if (amountAvailableToGrow == 0) { // We're done growing break; } if (sizing.availableToGrow(m_orientation) == 0) { // It's no longer a grower it = indexesOfGrowers.erase(it); // clazy:exclude=strict-iterators } else { it++; } } } // Do the shrinking: while (amountNeededToShrink > 0) { // Each shrinker will shrink a bit (round-robin) auto toShrink = std::max(1, amountNeededToShrink / int(indexesOfShrinkers.size())); // TODO: Use cbegin/cend once we drop Qt 5 for (auto it = indexesOfShrinkers.begin(); it != indexesOfShrinkers.end();) { const int index = *it; SizingInfo &sizing = sizes[index]; const auto shrunk = std::min(sizing.neededToShrink(m_orientation), toShrink); sizing.incrementLength(-shrunk, m_orientation); amountNeededToShrink -= shrunk; if (amountNeededToShrink == 0) { // We're done shrinking break; } if (sizing.neededToShrink(m_orientation) == 0) { // It's no longer a shrinker it = indexesOfShrinkers.erase(it); // clazy:exclude=strict-iterators } else { it++; } } } } bool ItemBoxContainer::hostSupportsHonouringLayoutMinSize() const { if (!m_host) { // Corner case. No reason not to honour min-size return true; } return m_host->supportsHonouringLayoutMinSize(); } void ItemBoxContainer::setSize_recursive(Size newSize, ChildrenResizeStrategy strategy) { ScopedValueRollback block(d->m_blockUpdatePercentages, true); const Size minSize = this->minSize(); if (newSize.width() < minSize.width() || newSize.height() < minSize.height()) { if (!s_silenceSanityChecks && hostSupportsHonouringLayoutMinSize()) { root()->dumpLayout(); KDDW_ERROR("New size doesn't respect size constraints new={}, min={}, this={}", newSize, minSize, ( void * )this); } return; } if (newSize == size()) return; const Size oldSize = size(); setSize(newSize); const Item::List children = visibleChildren(); const auto count = children.size(); SizingInfo::List childSizes = sizes(); // #1 Since we changed size, also resize out children. // But apply them to our SizingInfo::List first before setting actual Item/QWidget geometries // Because we need step #2 where we ensure min sizes for each item are respected. We could // calculate and do everything in a single-step, but we already have the code for #2 in // growItem() so doing it in 2 steps will reuse much logic. // the sizes: d->resizeChildren(oldSize, newSize, /*by-ref*/ childSizes, strategy); // the positions: positionItems(/*by-ref*/ childSizes); // #2 Adjust sizes so that each item has at least Item::minSize. for (int i = 0; i < count; ++i) { SizingInfo &size = childSizes[i]; const int missing = size.missingLength(d->m_orientation); if (missing > 0) growItem(i, childSizes, missing, GrowthStrategy::BothSidesEqually, NeighbourSqueezeStrategy::AllNeighbours); } // #3 Sizes are now correct and honour min/max sizes. So apply them to our Items applyGeometries(childSizes, strategy); } int ItemBoxContainer::length() const { return isVertical() ? height() : width(); } void ItemBoxContainer::dumpLayout(int level, bool printSeparators) { if (level == 0 && host() && s_dumpScreenInfoFunc) s_dumpScreenInfoFunc(); std::string indent(LAYOUT_DUMP_INDENT * size_t(level), ' '); const std::string beingInserted = m_sizingInfo.isBeingInserted ? "; beingInserted;" : ""; const std::string visible = !isVisible() ? ";hidden;" : ""; const bool isOverflow = isOverflowing(); const Size missingSize_ = missingSize(); const std::string isOverflowStr = isOverflow ? "; overflowing ;" : ""; const std::string missingSizeStr = missingSize_.isNull() ? "" : (std::string("; missingSize=") + std::to_string(missingSize_.width()) + "x" + std::to_string(missingSize_.height())); const std::string typeStr = isRoot() ? "- Root " : "- Layout "; { const std::string orientationStr = d->m_orientation == Qt::Vertical ? "V" : "H"; std::cerr << indent << typeStr << orientationStr << ": " << m_sizingInfo.geometry /*<< "r=" << m_geometry.right() << "b=" << m_geometry.bottom()*/ << "; min=" << minSize() << "; this=" << this << beingInserted << visible << "; %=" << d->childPercentages(); if (maxSizeHint() != Item::hardcodedMaximumSize) std::cerr << "; max=" << maxSizeHint(); std::cerr << missingSizeStr << isOverflowStr << "\n"; } int i = 0; for (Item *item : std::as_const(m_children)) { item->dumpLayout(level + 1, printSeparators); if (printSeparators && item->isVisible()) { if (i < d->m_separators.size()) { auto separator = d->m_separators.at(i); std::cerr << std::string(LAYOUT_DUMP_INDENT * size_t(level + 1), ' ') << "- Separator: " << "local.geo=" << mapFromRoot(separator->geometry()) << " ; global.geo=" << separator->geometry() << "; separator=" << separator << "\n"; } ++i; } } } void ItemBoxContainer::updateChildPercentages() { if (root()->d->m_blockUpdatePercentages) return; const int usable = usableLength(); for (Item *item : std::as_const(m_children)) { if (item->isVisible() && !item->isBeingInserted()) { item->m_sizingInfo.percentageWithinParent = (1.0 * item->length(d->m_orientation)) / usable; } else { item->m_sizingInfo.percentageWithinParent = 0.0; } } } void ItemBoxContainer::updateChildPercentages_recursive() { updateChildPercentages(); for (Item *item : std::as_const(m_children)) { if (auto c = item->asBoxContainer()) c->updateChildPercentages_recursive(); } } Vector ItemBoxContainer::Private::childPercentages() const { Vector percentages; percentages.reserve(q->m_children.size()); for (Item *item : std::as_const(q->m_children)) { if (item->isVisible() && !item->isBeingInserted()) percentages.push_back(item->m_sizingInfo.percentageWithinParent); } return percentages; } void ItemBoxContainer::restoreChild(Item *item, bool forceRestoreContainer, NeighbourSqueezeStrategy neighbourSqueezeStrategy) { assert(contains(item)); const bool shouldRestoreContainer = forceRestoreContainer || !hasVisibleChildren(/*excludeBeingInserted=*/true); item->setBeingInserted(true); item->setIsVisible(true); const int excessLength = d->excessLength(); if (shouldRestoreContainer) { // This container was hidden and will now be restored too, since a child was restored if (auto c = parentBoxContainer()) { setSize(item->size()); // give it a decent size. Same size as the item being restored // makes sense c->restoreChild(this, false, neighbourSqueezeStrategy); } } // Make sure root() is big enough to respect all item's min-sizes updateSizeConstraints(); item->setBeingInserted(false); if (numVisibleChildren() == 1) { // The easy case. Child is alone in the layout, occupies everything. item->setGeometry_recursive(rect()); d->updateSeparators_recursive(); return; } const int available = availableToSqueezeOnSide(item, Side1) + availableToSqueezeOnSide(item, Side2) - Item::layoutSpacing; const int max = std::min(available, item->maxLengthHint(d->m_orientation)); const int min = item->minLength(d->m_orientation); /* * Regarding the excessLength: * The layout bigger than its own max-size. The new item will get more (if it can), to counter * that excess. There's just 1 case where we have excess length: A layout with items with * max-size, but the layout can't be smaller due to min-size constraints of the higher level * layouts, in the nesting hierarchy. The excess goes away when inserting a widget that can grow * indefinitely, it eats all the current excess. */ const int proposed = std::max(Core::length(item->size(), d->m_orientation), excessLength - Item::layoutSpacing); const int newLength = bound(min, proposed, max); assert(item->isVisible()); // growItem() will make it grow by the same amount it steals from the neighbours, so we can't // start the growing without zeroing it if (isVertical()) { item->m_sizingInfo.geometry.setHeight(0); } else { item->m_sizingInfo.geometry.setWidth(0); } growItem(item, newLength, GrowthStrategy::BothSidesEqually, neighbourSqueezeStrategy, /*accountForNewSeparator=*/true); d->updateSeparators_recursive(); } void ItemBoxContainer::updateWidgetGeometries() { for (Item *item : std::as_const(m_children)) item->updateWidgetGeometries(); } int ItemBoxContainer::oppositeLength() const { return isVertical() ? width() : height(); } void ItemBoxContainer::requestSeparatorMove(LayoutingSeparator *separator, int delta) { const auto separatorIndex = d->m_separators.indexOf(separator); if (separatorIndex == -1) { // Doesn't happen KDDW_ERROR("Unknown separator {}, this={}", ( void * )separator, ( void * )this); root()->dumpLayout(); return; } if (delta == 0) return; const int min = minPosForSeparator_global(separator); const int pos = separator->position(); const int max = maxPosForSeparator_global(separator); if ((pos + delta < min && delta < 0) || // pos can be smaller than min, as long as we're making // the distane to minPos smaller, same for max. (pos + delta > max && delta > 0)) { // pos can be bigger than max already and going left/up // (negative delta, which is fine), just don't increase // if further root()->dumpLayout(); KDDW_ERROR("Separator would have gone out of bounds, separator={}, min={}, pos={}, max={}, deleta={}", ( void * )separator, min, pos, max, delta); return; } const Side moveDirection = delta < 0 ? Side1 : Side2; const Item::List children = visibleChildren(); if (children.size() <= separatorIndex) { // Doesn't happen KDDW_ERROR("Not enough children for separator index", ( void * )separator, ( void * )this, separatorIndex); root()->dumpLayout(); return; } int remainingToTake = std::abs(delta); int tookLocally = 0; Item *side1Neighbour = children[separatorIndex]; Item *side2Neighbour = children[separatorIndex + 1]; Side nextSeparatorDirection = moveDirection; if (moveDirection == Side1) { // Separator is moving left (or top if horizontal) const int availableSqueeze1 = availableToSqueezeOnSide(side2Neighbour, Side1); const int availableGrow2 = availableToGrowOnSide(side1Neighbour, Side2); // This is the available within our container, which we can use without bothering other // separators tookLocally = std::min(availableSqueeze1, remainingToTake); tookLocally = std::min(tookLocally, availableGrow2); if (tookLocally != 0) { growItem(side2Neighbour, tookLocally, GrowthStrategy::Side1Only, NeighbourSqueezeStrategy::ImmediateNeighboursFirst, false, ChildrenResizeStrategy::Side1SeparatorMove); } if (availableGrow2 == tookLocally) nextSeparatorDirection = Side2; } else { const int availableSqueeze2 = availableToSqueezeOnSide(side1Neighbour, Side2); const int availableGrow1 = availableToGrowOnSide(side2Neighbour, Side1); // Separator is moving right (or bottom if horizontal) tookLocally = std::min(availableSqueeze2, remainingToTake); tookLocally = std::min(tookLocally, availableGrow1); if (tookLocally != 0) { growItem(side1Neighbour, tookLocally, GrowthStrategy::Side2Only, NeighbourSqueezeStrategy::ImmediateNeighboursFirst, false, ChildrenResizeStrategy::Side2SeparatorMove); } if (availableGrow1 == tookLocally) nextSeparatorDirection = Side1; } remainingToTake -= tookLocally; if (remainingToTake > 0) { // Go up the hierarchy and move the next separator on the left if (isRoot()) { // Doesn't happen KDDW_ERROR("Not enough space to move separator {}", ( void * )this); } else { LayoutingSeparator *nextSeparator = parentBoxContainer()->d->neighbourSeparator_recursive(this, nextSeparatorDirection, d->m_orientation); if (!nextSeparator) { // Doesn't happen KDDW_ERROR("nextSeparator is null, report a bug"); return; } // nextSeparator might not belong to parentContainer(), due to different orientation const int remainingDelta = moveDirection == Side1 ? -remainingToTake : remainingToTake; nextSeparator->parentContainer()->requestSeparatorMove(nextSeparator, remainingDelta); } } } void ItemBoxContainer::requestEqualSize(LayoutingSeparator *separator) { const auto separatorIndex = d->m_separators.indexOf(separator); if (separatorIndex == -1) { // Doesn't happen KDDW_ERROR("Separator not found {}", ( void * )separator); return; } const Item::List children = visibleChildren(); Item *side1Item = children.at(separatorIndex); Item *side2Item = children.at(separatorIndex + 1); const int length1 = side1Item->length(d->m_orientation); const int length2 = side2Item->length(d->m_orientation); if (std::abs(length1 - length2) <= 1) { // items already have the same length, nothing to do. // We allow for a difference of 1px, since you can't split that. // But if at least 1 item is bigger than its max-size, don't bail out early, as then they // don't deserve equal sizes. if (!(side1Item->m_sizingInfo.isPastMax(d->m_orientation) || side2Item->m_sizingInfo.isPastMax(d->m_orientation))) { return; } } const int newLength = (length1 + length2) / 2; int delta = 0; if (length1 < newLength) { // Let's move separator to the right delta = newLength - length1; } else if (length2 < newLength) { // or left. delta = -(newLength - length2); // negative, since separator is going left } // Do some bounds checking, to respect min-size and max-size const int min = minPosForSeparator_global(separator, true); const int max = maxPosForSeparator_global(separator, true); const int newPos = bound(min, separator->position() + delta, max); // correct the delta delta = newPos - separator->position(); if (delta != 0) requestSeparatorMove(separator, delta); } void ItemBoxContainer::layoutEqually() { SizingInfo::List childSizes = sizes(); if (!childSizes.isEmpty()) { layoutEqually(childSizes); applyGeometries(childSizes); } } void ItemBoxContainer::layoutEqually(SizingInfo::List &sizes) { const auto numItems = sizes.count(); Vector satisfiedIndexes; satisfiedIndexes.reserve(numItems); int lengthToGive = length() - (d->m_separators.size() * Item::layoutSpacing); // clear the sizes before we start distributing for (SizingInfo &size : sizes) { size.setLength(0, d->m_orientation); } while (satisfiedIndexes.count() < sizes.count()) { const int remainingItems = int(sizes.count() - satisfiedIndexes.count()); const int suggestedToGive = std::max(1, lengthToGive / remainingItems); const auto oldLengthToGive = lengthToGive; for (int i = 0; i < numItems; ++i) { if (satisfiedIndexes.contains(i)) continue; SizingInfo &size = sizes[i]; if (size.availableToGrow(d->m_orientation) <= 0) { // Was already satisfied from the beginning satisfiedIndexes.push_back(i); continue; } // Bound the max length. Our max can't be bigger than the remaining space. // The layout's min length minus our own min length is the amount of space that we // need to guarantee. We can't go larger and overwrite that const auto othersMissing = // The size that the others are missing to satisfy their // minimum length std::accumulate(sizes.constBegin(), sizes.constEnd(), 0, [this](size_t sum, const SizingInfo &sz) { return int(sum) + sz.missingLength(d->m_orientation); }) - size.missingLength(d->m_orientation); const auto maxLength = std::min(size.length(d->m_orientation) + lengthToGive - othersMissing, size.maxLengthHint(d->m_orientation)); const auto newItemLenght = bound(size.minLength(d->m_orientation), size.length(d->m_orientation) + suggestedToGive, maxLength); const auto toGive = newItemLenght - size.length(d->m_orientation); if (toGive == 0) { assert(false); satisfiedIndexes.push_back(i); } else { lengthToGive -= toGive; size.incrementLength(toGive, d->m_orientation); if (size.availableToGrow(d->m_orientation) <= 0) { satisfiedIndexes.push_back(i); } if (lengthToGive == 0) return; if (lengthToGive < 0) { KDDW_ERROR("Breaking infinite loop"); return; } } } if (oldLengthToGive == lengthToGive) { // Nothing happened, we can't satisfy more items, due to min/max constraints return; } } } void ItemBoxContainer::layoutEqually_recursive() { layoutEqually(); for (Item *item : std::as_const(m_children)) { if (item->isVisible()) { if (auto c = item->asBoxContainer()) c->layoutEqually_recursive(); } } } Item *ItemBoxContainer::visibleNeighbourFor(const Item *item, Side side) const { // Item might not be visible, so use m_children instead of visibleChildren() const auto index = m_children.indexOf(const_cast(item)); if (side == Side1) { for (auto i = index - 1; i >= 0; --i) { Item *child = m_children.at(i); if (child->isVisible()) return child; } } else { for (auto i = index + 1; i < m_children.size(); ++i) { Item *child = m_children.at(i); if (child->isVisible()) return child; } } return nullptr; } Size ItemBoxContainer::availableSize() const { return size() - this->minSize(); } int ItemBoxContainer::availableLength() const { return isVertical() ? availableSize().height() : availableSize().width(); } LengthOnSide ItemBoxContainer::lengthOnSide(const SizingInfo::List &sizes, int fromIndex, Side side, Qt::Orientation o) const { if (fromIndex < 0) return {}; const auto count = sizes.count(); if (fromIndex >= count) return {}; int start = 0; int end = -1; if (side == Side1) { start = 0; end = fromIndex; } else { start = fromIndex; end = count - 1; } LengthOnSide result; for (int i = start; i <= end; ++i) { const SizingInfo &size = sizes.at(i); result.length += size.length(o); result.minLength += size.minLength(o); } return result; } int ItemBoxContainer::neighboursLengthFor(const Item *item, Side side, Qt::Orientation o) const { const Item::List children = visibleChildren(); const auto index = children.indexOf(const_cast(item)); if (index == -1) { KDDW_ERROR("Couldn't find item {}", ( void * )item); return 0; } if (o == d->m_orientation) { int neighbourLength = 0; int start = 0; int end = -1; if (side == Side1) { start = 0; end = index - 1; } else { start = index + 1; end = children.size() - 1; } for (int i = start; i <= end; ++i) neighbourLength += children.at(i)->length(d->m_orientation); return neighbourLength; } else { // No neighbours in the other orientation. Each container is bidimensional. return 0; } } int ItemBoxContainer::neighboursLengthFor_recursive(const Item *item, Side side, Qt::Orientation o) const { return neighboursLengthFor(item, side, o) + (isRoot() ? 0 : parentBoxContainer()->neighboursLengthFor_recursive(this, side, o)); } int ItemBoxContainer::neighboursMinLengthFor(const Item *item, Side side, Qt::Orientation o) const { const Item::List children = visibleChildren(); const auto index = children.indexOf(const_cast(item)); if (index == -1) { KDDW_ERROR("Couldn't find item {}", ( void * )item); return 0; } if (o == d->m_orientation) { int neighbourMinLength = 0; int start = 0; int end = -1; if (side == Side1) { start = 0; end = index - 1; } else { start = index + 1; end = children.size() - 1; } for (int i = start; i <= end; ++i) neighbourMinLength += children.at(i)->minLength(d->m_orientation); return neighbourMinLength; } else { // No neighbours here return 0; } } int ItemBoxContainer::neighboursMaxLengthFor(const Item *item, Side side, Qt::Orientation o) const { const Item::List children = visibleChildren(); const auto index = children.indexOf(const_cast(item)); if (index == -1) { KDDW_ERROR("Couldn't find item {}", ( void * )item); return 0; } if (o == d->m_orientation) { int neighbourMaxLength = 0; int start = 0; int end = -1; if (side == Side1) { start = 0; end = index - 1; } else { start = index + 1; end = children.size() - 1; } for (int i = start; i <= end; ++i) neighbourMaxLength = std::min(Core::length(root()->size(), d->m_orientation), neighbourMaxLength + children.at(i)->maxLengthHint(d->m_orientation)); return neighbourMaxLength; } else { // No neighbours here return 0; } } int ItemBoxContainer::availableToSqueezeOnSide(const Item *child, Side side) const { const int length = neighboursLengthFor(child, side, d->m_orientation); const int min = neighboursMinLengthFor(child, side, d->m_orientation); const int available = length - min; if (available < 0) { root()->dumpLayout(); assert(false); } return available; } int ItemBoxContainer::availableToGrowOnSide(const Item *child, Side side) const { const int length = neighboursLengthFor(child, side, d->m_orientation); const int max = neighboursMaxLengthFor(child, side, d->m_orientation); return max - length; } int ItemBoxContainer::availableToSqueezeOnSide_recursive(const Item *child, Side side, Qt::Orientation orientation) const { if (orientation == d->m_orientation) { const int available = availableToSqueezeOnSide(child, side); return isRoot() ? available : (available + parentBoxContainer()->availableToSqueezeOnSide_recursive(this, side, orientation)); } else { return isRoot() ? 0 : parentBoxContainer()->availableToSqueezeOnSide_recursive(this, side, orientation); } } int ItemBoxContainer::availableToGrowOnSide_recursive(const Item *child, Side side, Qt::Orientation orientation) const { if (orientation == d->m_orientation) { const int available = availableToGrowOnSide(child, side); return isRoot() ? available : (available + parentBoxContainer()->availableToGrowOnSide_recursive(this, side, orientation)); } else { return isRoot() ? 0 : parentBoxContainer()->availableToGrowOnSide_recursive(this, side, orientation); } } void ItemBoxContainer::growNeighbours(Item *side1Neighbour, Item *side2Neighbour) { if (!side1Neighbour && !side2Neighbour) return; SizingInfo::List childSizes = sizes(); if (side1Neighbour && side2Neighbour) { const int index1 = indexOfVisibleChild(side1Neighbour); const int index2 = indexOfVisibleChild(side2Neighbour); if (index1 == -1 || index2 == -1 || index1 >= childSizes.count() || index2 >= childSizes.count()) { // Doesn't happen KDDW_ERROR("Invalid indexes {} {} {}", index1, index2, childSizes.count()); return; } // Give half/half to each neighbour Rect &geo1 = childSizes[index1].geometry; Rect &geo2 = childSizes[index2].geometry; if (isVertical()) { const int available = geo2.y() - geo1.bottom() - layoutSpacing; geo1.setHeight(geo1.height() + (available / 2)); geo2.setTop(geo1.bottom() + layoutSpacing + 1); } else { const int available = geo2.x() - geo1.right() - layoutSpacing; geo1.setWidth(geo1.width() + (available / 2)); geo2.setLeft(geo1.right() + layoutSpacing + 1); } } else if (side1Neighbour) { const int index1 = indexOfVisibleChild(side1Neighbour); if (index1 == -1 || index1 >= childSizes.count()) { // Doesn't happen KDDW_ERROR("Invalid indexes {} {}", index1, childSizes.count()); return; } // Grow all the way to the right (or bottom if vertical) Rect &geo = childSizes[index1].geometry; if (isVertical()) { geo.setBottom(rect().bottom()); } else { geo.setRight(rect().right()); } } else if (side2Neighbour) { const int index2 = indexOfVisibleChild(side2Neighbour); if (index2 == -1 || index2 >= childSizes.count()) { // Doesn't happen KDDW_ERROR("Invalid indexes {} {}", index2, childSizes.count()); return; } // Grow all the way to the left (or top if vertical) Rect &geo = childSizes[index2].geometry; if (isVertical()) { geo.setTop(0); } else { geo.setLeft(0); } } d->honourMaxSizes(childSizes); positionItems(/*by-ref*/ childSizes); applyGeometries(childSizes); } void ItemBoxContainer::growItem(int index, SizingInfo::List &sizes, int missing, GrowthStrategy growthStrategy, NeighbourSqueezeStrategy neighbourSqueezeStrategy, bool accountForNewSeparator) { int toSteal = missing; // The amount that neighbours of @p index will shrink if (accountForNewSeparator) toSteal += Item::layoutSpacing; assert(index != -1); if (toSteal == 0) return; // #1. Grow our item SizingInfo &sizingInfo = sizes[index]; sizingInfo.setOppositeLength(oppositeLength(), d->m_orientation); const bool isFirst = index == 0; const bool isLast = index == sizes.count() - 1; int side1Growth = 0; int side2Growth = 0; if (growthStrategy == GrowthStrategy::BothSidesEqually) { sizingInfo.setLength(sizingInfo.length(d->m_orientation) + missing, d->m_orientation); const auto count = sizes.count(); if (count == 1) { // There's no neighbours to push, we're alone. Occupy the full container sizingInfo.incrementLength(missing, d->m_orientation); return; } // #2. Now shrink the neighbors by the same amount. Calculate how much to shrink from each // side const LengthOnSide side1Length = lengthOnSide(sizes, index - 1, Side1, d->m_orientation); const LengthOnSide side2Length = lengthOnSide(sizes, index + 1, Side2, d->m_orientation); int available1 = side1Length.available(); int available2 = side2Length.available(); if (toSteal > available1 + available2) { root()->dumpLayout(); assert(false); } while (toSteal > 0) { if (available1 == 0) { assert(available2 >= toSteal); side2Growth += toSteal; break; } else if (available2 == 0) { assert(available1 >= toSteal); side1Growth += toSteal; break; } const int toTake = std::max(1, toSteal / 2); const int took1 = std::min(toTake, available1); toSteal -= took1; available1 -= took1; side1Growth += took1; if (toSteal == 0) break; const int took2 = std::min(toTake, available2); toSteal -= took2; side2Growth += took2; available2 -= took2; } shrinkNeighbours(index, sizes, side1Growth, side2Growth, neighbourSqueezeStrategy); } else if (growthStrategy == GrowthStrategy::Side1Only) { side1Growth = std::min(missing, sizingInfo.availableToGrow(d->m_orientation)); sizingInfo.setLength(sizingInfo.length(d->m_orientation) + side1Growth, d->m_orientation); if (side1Growth > 0) shrinkNeighbours(index, sizes, side1Growth, /*side2Amount=*/0, neighbourSqueezeStrategy); if (side1Growth < missing) { missing = missing - side1Growth; if (isLast) { // Doesn't happen KDDW_ERROR("No more items to grow"); } else { growItem(index + 1, sizes, missing, growthStrategy, neighbourSqueezeStrategy, accountForNewSeparator); } } } else if (growthStrategy == GrowthStrategy::Side2Only) { side2Growth = std::min(missing, sizingInfo.availableToGrow(d->m_orientation)); sizingInfo.setLength(sizingInfo.length(d->m_orientation) + side2Growth, d->m_orientation); if (side2Growth > 0) shrinkNeighbours(index, sizes, /*side1Amount=*/0, side2Growth, neighbourSqueezeStrategy); if (side2Growth < missing) { missing = missing - side2Growth; if (isFirst) { // Doesn't happen KDDW_ERROR("No more items to grow"); } else { growItem(index - 1, sizes, missing, growthStrategy, neighbourSqueezeStrategy, accountForNewSeparator); } } } } void ItemBoxContainer::growItem(Item *item, int amount, GrowthStrategy growthStrategy, NeighbourSqueezeStrategy neighbourSqueezeStrategy, bool accountForNewSeparator, ChildrenResizeStrategy childResizeStrategy) { const Item::List items = visibleChildren(); const auto index = items.indexOf(item); SizingInfo::List sizes = this->sizes(); growItem(index, /*by-ref=*/sizes, amount, growthStrategy, neighbourSqueezeStrategy, accountForNewSeparator); applyGeometries(sizes, childResizeStrategy); } void ItemBoxContainer::applyGeometries(const SizingInfo::List &sizes, ChildrenResizeStrategy strategy) { const Item::List items = visibleChildren(); const auto count = items.size(); assert(count == sizes.size()); for (int i = 0; i < count; ++i) { Item *item = items.at(i); item->setSize_recursive(sizes[i].geometry.size(), strategy); } positionItems(); } SizingInfo::List ItemBoxContainer::sizes(bool ignoreBeingInserted) const { const Item::List children = visibleChildren(ignoreBeingInserted); SizingInfo::List result; result.reserve(children.count()); for (Item *item : children) { if (item->isContainer()) { // Containers have virtual min/maxSize methods, and don't really fill in these // properties So fill them here item->m_sizingInfo.minSize = item->minSize(); item->m_sizingInfo.maxSizeHint = item->maxSizeHint(); } result.push_back(item->m_sizingInfo); } return result; } Vector ItemBoxContainer::calculateSqueezes( SizingInfo::List::const_iterator begin, // clazy:exclude=function-args-by-ref SizingInfo::List::const_iterator end, int needed, // clazy:exclude=function-args-by-ref NeighbourSqueezeStrategy strategy, bool reversed) const { Vector availabilities; availabilities.reserve(std::distance(begin, end)); for (auto it = begin; it < end; ++it) { availabilities.push_back(it->availableLength(d->m_orientation)); } const auto count = availabilities.count(); Vector squeezes; squeezes.resize(count); std::fill(squeezes.begin(), squeezes.end(), 0); int missing = needed; if (strategy == NeighbourSqueezeStrategy::AllNeighbours) { while (missing > 0) { const int numDonors = int(std::count_if(availabilities.cbegin(), availabilities.cend(), [](int num) { return num > 0; })); if (numDonors == 0) { root()->dumpLayout(); assert(false); return {}; } int toTake = missing / numDonors; if (toTake == 0) toTake = missing; for (int i = 0; i < count; ++i) { const int available = availabilities.at(i); if (available == 0) continue; const int took = std::min({ missing, toTake, available }); availabilities[i] -= took; missing -= took; squeezes[i] += took; if (missing == 0) break; } } } else if (strategy == NeighbourSqueezeStrategy::ImmediateNeighboursFirst) { for (int i = 0; i < count; i++) { const auto index = reversed ? count - 1 - i : i; const int available = availabilities.at(index); if (available > 0) { const int took = std::min(missing, available); missing -= took; squeezes[index] += took; } if (missing == 0) break; } } if (missing < 0) { // Doesn't really happen KDDW_ERROR("Missing is negative. missing={}, squeezes={}", missing, squeezes); } return squeezes; } void ItemBoxContainer::shrinkNeighbours(int index, SizingInfo::List &sizes, int side1Amount, int side2Amount, NeighbourSqueezeStrategy strategy) { assert(side1Amount > 0 || side2Amount > 0); assert(side1Amount >= 0 && side2Amount >= 0); // never negative if (side1Amount > 0) { auto begin = sizes.cbegin(); auto end = sizes.cbegin() + index; const bool reversed = strategy == NeighbourSqueezeStrategy::ImmediateNeighboursFirst; const Vector squeezes = calculateSqueezes(begin, end, side1Amount, strategy, reversed); for (int i = 0; i < squeezes.size(); ++i) { const int squeeze = squeezes.at(i); SizingInfo &sizing = sizes[i]; // setSize() or setGeometry() have the same effect here, we don't care about the // position yet. That's done in positionItems() sizing.setSize(adjustedRect(sizing.geometry, d->m_orientation, 0, -squeeze).size()); } } if (side2Amount > 0) { auto begin = sizes.cbegin() + index + 1; auto end = sizes.cend(); const Vector squeezes = calculateSqueezes(begin, end, side2Amount, strategy); for (int i = 0; i < squeezes.size(); ++i) { const int squeeze = squeezes.at(i); SizingInfo &sizing = sizes[i + index + 1]; sizing.setSize(adjustedRect(sizing.geometry, d->m_orientation, squeeze, 0).size()); } } } Vector ItemBoxContainer::Private::requiredSeparatorPositions() const { const int numSeparators = std::max(0, q->numVisibleChildren() - 1); Vector positions; positions.reserve(numSeparators); for (Item *item : std::as_const(q->m_children)) { if (positions.size() == numSeparators) break; if (item->isVisible()) { const int localPos = item->m_sizingInfo.edge(m_orientation) + 1; positions.push_back(q->mapToRoot(localPos, m_orientation)); } } return positions; } void ItemBoxContainer::Private::updateSeparators() { if (!q->host()) return; const Vector positions = requiredSeparatorPositions(); const auto requiredNumSeparators = positions.size(); const bool numSeparatorsChanged = requiredNumSeparators != m_separators.size(); if (numSeparatorsChanged) { // Instead of just creating N missing ones at the end of the list, let's minimize separators // having their position changed, to minimize flicker LayoutingSeparator::List newSeparators; newSeparators.reserve(requiredNumSeparators); for (int position : positions) { LayoutingSeparator *separator = separatorAt(position); if (separator) { // Already existing, reuse newSeparators.push_back(separator); m_separators.removeOne(separator); } else { separator = s_createSeparatorFunc(q->host(), m_orientation, q); newSeparators.push_back(separator); } } // delete what remained, which is unused deleteSeparators(); m_separators = newSeparators; } // Update their positions: const int pos2 = q->isVertical() ? q->mapToRoot(Point(0, 0)).x() : q->mapToRoot(Point(0, 0)).y(); int i = 0; for (int position : positions) { m_separators.at(i)->setGeometry(position, pos2, q->oppositeLength()); i++; } // raise separators as they might be overlapping with dockwidget (supported use case) for (auto sep : std::as_const(m_separators)) sep->raise(); q->updateChildPercentages(); } void ItemBoxContainer::Private::deleteSeparators() { for (const auto &sep : std::as_const(m_separators)) sep->free(); m_separators.clear(); } void ItemBoxContainer::Private::deleteSeparators_recursive() { deleteSeparators(); // recurse into the children: for (Item *item : std::as_const(q->m_children)) { if (auto c = item->asBoxContainer()) c->d->deleteSeparators_recursive(); } } void ItemBoxContainer::Private::updateSeparators_recursive() { updateSeparators(); // recurse into the children: const Item::List items = q->visibleChildren(); for (Item *item : items) { if (auto c = item->asBoxContainer()) c->d->updateSeparators_recursive(); } } int ItemBoxContainer::Private::excessLength() const { // Returns how much bigger this layout is than its max-size return std::max(0, Core::length(q->size(), m_orientation) - q->maxLengthHint(m_orientation)); } void ItemBoxContainer::simplify() { // Removes unneeded nesting. For example, a vertical layout doesn't need to have vertical // layouts inside. It can simply have the contents of said sub-layouts ScopedValueRollback isInSimplify(d->m_isSimplifying, true); Item::List newChildren; newChildren.reserve(m_children.size() + 20); // over-reserve a bit for (Item *child : std::as_const(m_children)) { if (ItemBoxContainer *childContainer = child->asBoxContainer()) { childContainer->simplify(); // recurse down the hierarchy if (childContainer->orientation() == d->m_orientation || childContainer->m_children.size() == 1) { // This sub-container is redundant, as it has the same orientation as its parent // Cannibalize it. const auto children = childContainer->childItems(); for (Item *child2 : children) { child2->setParentContainer(this); newChildren.push_back(child2); } delete childContainer; } else { newChildren.push_back(child); } } else { newChildren.push_back(child); } } if (m_children != newChildren) { m_children = newChildren; positionItems(); updateChildPercentages(); } } LayoutingSeparator *ItemBoxContainer::Private::separatorAt(int p) const { for (auto separator : m_separators) { if (separator->position() == p) return separator; } return nullptr; } bool ItemBoxContainer::isVertical() const { return d->m_orientation == Qt::Vertical; } bool ItemBoxContainer::isHorizontal() const { return d->m_orientation == Qt::Horizontal; } int ItemBoxContainer::indexOf(LayoutingSeparator *separator) const { return d->m_separators.indexOf(separator); } bool ItemBoxContainer::isInSimplify() const { if (d->m_isSimplifying) return true; auto p = parentBoxContainer(); return p && p->isInSimplify(); } int ItemBoxContainer::minPosForSeparator(LayoutingSeparator *separator, bool honourMax) const { const int globalMin = minPosForSeparator_global(separator, honourMax); return mapFromRoot(globalMin, d->m_orientation); } int ItemBoxContainer::maxPosForSeparator(LayoutingSeparator *separator, bool honourMax) const { const int globalMax = maxPosForSeparator_global(separator, honourMax); return mapFromRoot(globalMax, d->m_orientation); } int ItemBoxContainer::minPosForSeparator_global(LayoutingSeparator *separator, bool honourMax) const { const int separatorIndex = indexOf(separator); assert(separatorIndex != -1); const Item::List children = visibleChildren(); assert(separatorIndex + 1 < children.size()); Item *item2 = children.at(separatorIndex + 1); const int availableToSqueeze = availableToSqueezeOnSide_recursive(item2, Side1, d->m_orientation); if (honourMax) { // We can drag the separator left just as much as it doesn't violate max-size constraints of // Side2 Item *item1 = children.at(separatorIndex); const int availabletoGrow = availableToGrowOnSide_recursive(item1, Side2, d->m_orientation); return separator->position() - std::min(availabletoGrow, availableToSqueeze); } return separator->position() - availableToSqueeze; } int ItemBoxContainer::maxPosForSeparator_global(LayoutingSeparator *separator, bool honourMax) const { const int separatorIndex = indexOf(separator); assert(separatorIndex != -1); const Item::List children = visibleChildren(); Item *item1 = children.at(separatorIndex); const int availableToSqueeze = availableToSqueezeOnSide_recursive(item1, Side2, d->m_orientation); if (honourMax) { // We can drag the separator right just as much as it doesn't violate max-size constraints // of Side1 Item *item2 = children.at(separatorIndex + 1); const int availabletoGrow = availableToGrowOnSide_recursive(item2, Side1, d->m_orientation); return separator->position() + std::min(availabletoGrow, availableToSqueeze); } return separator->position() + availableToSqueeze; } void ItemBoxContainer::to_json(nlohmann::json &j) const { Item::to_json(j); j["children"] = m_children; j["orientation"] = d->m_orientation; } void ItemBoxContainer::fillFromJson(const nlohmann::json &j, const std::unordered_map &widgets) { if (!j.is_object()) { KDDW_ERROR("Expected a JSON object"); return; } ScopedValueRollback deserializing(d->m_isDeserializing, true); Item::fillFromJson(j, widgets); d->m_orientation = Qt::Orientation(j.value("orientation", {})); for (const auto &child : j.value("children", nlohmann::json::array())) { const bool isContainer = child.value("isContainer", {}); Item *childItem = isContainer ? new ItemBoxContainer(host(), this) : new Item(host(), this); childItem->fillFromJson(child, widgets); m_children.push_back(childItem); } if (isRoot()) { updateChildPercentages_recursive(); if (host()) { d->updateSeparators_recursive(); d->updateWidgets_recursive(); } d->relayoutIfNeeded(); positionItems_recursive(); minSizeChanged.emit(this); #ifdef DOCKS_DEVELOPER_MODE if (!checkSanity()) KDDW_ERROR("Resulting layout is invalid"); #endif } } bool ItemBoxContainer::Private::isDummy() const { return q->host() == nullptr; } #ifdef DOCKS_DEVELOPER_MODE void ItemBoxContainer::relayoutIfNeeded() { d->relayoutIfNeeded(); } bool ItemBoxContainer::test_suggestedRect() { auto itemToDrop = new Item(host()); const Item::List children = visibleChildren(); for (Item *relativeTo : children) { if (auto c = relativeTo->asBoxContainer()) { c->test_suggestedRect(); } else { std::unordered_map rects; for (Location loc : { Location_OnTop, Location_OnLeft, Location_OnRight, Location_OnBottom }) { const Rect rect = suggestedDropRect(itemToDrop, relativeTo, loc); rects[loc] = rect; if (rect.isEmpty()) { KDDW_ERROR("Empty rect"); return false; } else if (!root()->rect().contains(rect)) { root()->dumpLayout(); KDDW_ERROR("Suggested rect is out of bounds rect={}, loc={}, relativeTo={}", rect, loc, ( void * )relativeTo); return false; } } auto rectFor = [&rects](Location loc) -> Rect { auto it = rects.find(loc); return it == rects.cend() ? Rect() : it->second; }; if (rectFor(Location_OnBottom).y() <= rectFor(Location_OnTop).y() || rectFor(Location_OnRight).x() <= rectFor(Location_OnLeft).x()) { root()->dumpLayout(); KDDW_ERROR("Invalid suggested rects. this={}, relativeTo={}", ( void * )this, ( void * )relativeTo); return false; } } } delete itemToDrop; return true; } #endif Vector ItemBoxContainer::separators_recursive() const { LayoutingSeparator::List separators = d->m_separators; for (Item *item : std::as_const(m_children)) { if (auto c = item->asBoxContainer()) separators.append(c->separators_recursive()); } return separators; } Vector ItemBoxContainer::separators() const { return d->m_separators; } LayoutingSeparator *ItemBoxContainer::separatorForChild(Item *child, Side side) const { if (!child || !child->isVisible()) { KDDW_ERROR("ItemBoxContainer::separatorForChild: Unexpected nullptr or invisible child"); return nullptr; } const Item::List children = visibleChildren(); const int childIndex = children.indexOf(child); if (childIndex == -1) { KDDW_ERROR("ItemBoxContainer::separatorForChild: Could not find child"); return nullptr; } int separatorIndex = -1; if (side == Side1) { // side1 is the separator on the left (or top) if (childIndex == 0) // No left separator for the 1st item return nullptr; separatorIndex = childIndex - 1; } else { // side2 is the separator on the right (or bottom) if (childIndex == children.size() - 1) // No right separator for the last item return nullptr; separatorIndex = childIndex; } if (separatorIndex < 0 || separatorIndex >= d->m_separators.size()) { KDDW_ERROR("ItemBoxContainer::separatorForChild: Not enough separators {} {} {}", d->m_separators.size(), children.size(), childIndex); return nullptr; } return d->m_separators.at(separatorIndex); } LayoutingSeparator *ItemBoxContainer::adjacentSeparatorForChild(Item *child, Side side) const { if (!child || !child->isVisible()) { KDDW_ERROR("ItemBoxContainer::adjacentSeparatorForChild: Unexpected nullptr or invisible child"); return nullptr; } int separatorIndex = -1; // If this container is horizontal, we need to find the parent vertical one (or vice-versa): if (ItemBoxContainer *ancestor = ancestorBoxContainerWithOrientation(oppositeOrientation(orientation()))) { // Since it's not a direct ancestor, we need to use indexInAncestor(). const int childIndex = indexInAncestor(ancestor); const auto children = ancestor->visibleChildren(); const auto separators = ancestor->separators(); if (childIndex == -1) { // Can't happen KDDW_ERROR("ItemBoxContainer::adjacentSeparatorForChild: Could not find index inside ancestor"); return nullptr; } if (side == Side1) { if (childIndex == 0) // No top separator for the 1st item return nullptr; separatorIndex = childIndex - 1; } else { if (childIndex == children.size() - 1) // No bottom separator for the last item return nullptr; separatorIndex = childIndex; } if (separatorIndex < 0 || separatorIndex >= separators.size()) { KDDW_ERROR("ItemBoxContainer::adjacentSeparatorForChild: Not enough separators {} {} {}", separators.size(), children.size(), childIndex); return nullptr; } return separators[separatorIndex]; } /// No grand parent, for example if we are root return nullptr; } bool ItemBoxContainer::isDeserializing() const { return d->m_isDeserializing; } bool ItemBoxContainer::isOverflowing() const { // This never returns true, unless when loading a buggy layout // or if QWidgets now have bigger min-size int contentsLength = 0; int numVisible = 0; for (Item *item : std::as_const(m_children)) { if (item->isVisible()) { contentsLength += item->length(d->m_orientation); numVisible++; } } contentsLength += std::max(0, Item::layoutSpacing * (numVisible - 1)); return contentsLength > length(); } void ItemBoxContainer::Private::relayoutIfNeeded() { // Checks all the child containers if they have the correct min-size, recursively. // When loading a layout from disk the min-sizes for the host QWidgets might have changed, so we // need to adjust { // #1. First, we check if the current container has enough space const Size missing = q->missingSize(); if (!missing.isNull()) q->setSize_recursive(q->size() + missing); } // #2. Make sure there's no child that is missing space for (Item *child : std::as_const(q->m_children)) { const int missingLength = ::length(child->missingSize(), m_orientation); if (!child->isVisible() || missingLength == 0) continue; q->growItem(child, missingLength, GrowthStrategy::BothSidesEqually, defaultNeighbourSqueezeStrategy()); } // #3. Contents is currently bigger. Not sure if this can still happen. if (q->isOverflowing()) { const Size size = q->size(); q->m_sizingInfo.setSize(size + Size(1, 1)); // Just so setSize_recursive() doesn't bail out q->setSize_recursive(size); q->updateChildPercentages(); } // Let's see our children too: for (Item *item : std::as_const(q->m_children)) { if (item->isVisible()) { if (auto c = item->asBoxContainer()) c->d->relayoutIfNeeded(); } } } const Item *ItemBoxContainer::Private::itemFromPath(const Vector &path) const { const ItemBoxContainer *container = q; for (int i = 0; i < path.size(); ++i) { const int index = path[i]; const bool isLast = i == path.size() - 1; if (index < 0 || index >= container->m_children.size()) { // Doesn't happen q->root()->dumpLayout(); KDDW_ERROR("Invalid index {}, this={}, path={}, isRoot={}", index, ( void * )this, path, q->isRoot()); return nullptr; } if (isLast) { return container->m_children.at(index); } else { container = container->m_children.at(index)->asBoxContainer(); if (!container) { KDDW_ERROR("Invalid index path={}", path); return nullptr; } } } return q; } LayoutingSeparator * ItemBoxContainer::Private::neighbourSeparator(const Item *item, Side side, Qt::Orientation orientation) const { Item::List children = q->visibleChildren(); const auto itemIndex = children.indexOf(const_cast(item)); if (itemIndex == -1) { KDDW_ERROR("Item not found item={}, this={}", ( void * )item, ( void * )this); q->root()->dumpLayout(); return nullptr; } if (orientation != q->orientation()) { // Go up if (q->isRoot()) { return nullptr; } else { return q->parentBoxContainer()->d->neighbourSeparator(q, side, orientation); } } const auto separatorIndex = side == Side1 ? itemIndex - 1 : itemIndex; if (separatorIndex < 0 || separatorIndex >= m_separators.size()) return nullptr; return m_separators[separatorIndex]; } LayoutingSeparator * ItemBoxContainer::Private::neighbourSeparator_recursive(const Item *item, Side side, Qt::Orientation orientation) const { LayoutingSeparator *separator = neighbourSeparator(item, side, orientation); if (separator) return separator; if (!q->parentContainer()) return nullptr; return q->parentBoxContainer()->d->neighbourSeparator_recursive(q, side, orientation); } void ItemBoxContainer::Private::updateWidgets_recursive() { for (Item *item : std::as_const(q->m_children)) { if (auto c = item->asBoxContainer()) { c->d->updateWidgets_recursive(); } else { if (item->isVisible()) { if (auto guest = item->guest()) { guest->setGeometry(q->mapToRoot(item->geometry())); guest->setVisible(true); } else { KDDW_ERROR("visible item doesn't have a guest item=", ( void * )item); } } } } } SizingInfo::SizingInfo() : minSize(Core::Item::hardcodedMinimumSize) , maxSizeHint(Core::Item::hardcodedMaximumSize) { } void SizingInfo::setOppositeLength(int l, Qt::Orientation o) { setLength(l, oppositeOrientation(o)); } int SizingInfo::maxLengthHint(Qt::Orientation o) const { return std::max(minLength(o), Core::length(maxSizeHint, o)); } int SizingInfo::availableLength(Qt::Orientation o) const { return std::max(0, length(o) - minLength(o)); } int SizingInfo::missingLength(Qt::Orientation o) const { return std::max(0, minLength(o) - length(o)); } int SizingInfo::neededToShrink(Qt::Orientation o) const { return std::max(0, length(o) - maxLengthHint(o)); } void Core::to_json(nlohmann::json &j, const SizingInfo &info) { j["geometry"] = info.geometry; j["minSize"] = info.minSize; j["maxSizeHint"] = info.maxSizeHint; j["percentageWithinParent"] = info.percentageWithinParent; } void Core::from_json(const nlohmann::json &j, SizingInfo &info) { info.geometry = j.value("geometry", Rect()); info.minSize = j.value("minSize", Size()); info.maxSizeHint = j.value("maxSizeHint", Size()); info.percentageWithinParent = j.value("percentageWithinParent", {}); } void Core::to_json(nlohmann::json &j, Item *item) { if (!item) return; // virtual dispatch item->to_json(j); } int ItemBoxContainer::Private::defaultLengthFor(Item *item, const InitialOption &option) const { int result = 0; if (option.hasPreferredLength(m_orientation) && option.sizeMode != DefaultSizeMode::NoDefaultSizeMode) { result = option.preferredLength(m_orientation); } else { switch (option.sizeMode) { case DefaultSizeMode::NoDefaultSizeMode: break; case DefaultSizeMode::Fair: { const int numVisibleChildren = q->numVisibleChildren() + 1; // +1 so it counts with @p item too, which we're adding const int usableLength = q->length() - (Item::layoutSpacing * (numVisibleChildren - 1)); result = usableLength / numVisibleChildren; break; } case DefaultSizeMode::FairButFloor: { int length = defaultLengthFor(item, DefaultSizeMode::Fair); result = std::min(length, item->length(m_orientation)); break; } case DefaultSizeMode::ItemSize: result = item->length(m_orientation); break; } } result = std::max(item->minLength(m_orientation), result); // bound with max-size too return result; } struct ItemContainer::Private { explicit Private(ItemContainer *qq) : q(qq) { } ~Private() { } ItemContainer *const q; }; ItemContainer::ItemContainer(LayoutingHost *hostWidget, ItemContainer *parent) : Item(true, hostWidget, parent) , d(new Private(this)) { xChanged.connect([this] { for (Item *item : std::as_const(m_children)) { item->xChanged.emit(); } }); yChanged.connect([this] { for (Item *item : std::as_const(m_children)) { item->yChanged.emit(); } }); } ItemContainer::ItemContainer(LayoutingHost *hostWidget) : Item(true, hostWidget, nullptr) , d(new Private(this)) { } ItemContainer::~ItemContainer() { delete d; } Item::List ItemContainer::childItems() const { return m_children; } int ItemContainer::indexOfChild(const Item *child) const { return m_children.indexOf(const_cast(child)); } bool ItemContainer::hasChildren() const { return !m_children.isEmpty(); } bool ItemContainer::hasVisibleChildren(bool excludeBeingInserted) const { for (Item *item : std::as_const(m_children)) { if (item->isVisible(excludeBeingInserted)) return true; } return false; } int ItemContainer::numChildren() const { return m_children.size(); } int ItemContainer::numVisibleChildren() const { int num = 0; for (Item *child : std::as_const(m_children)) { if (child->isVisible()) num++; } return num; } bool ItemContainer::isEmpty() const { return m_children.isEmpty(); } bool ItemContainer::hasSingleVisibleItem() const { return numVisibleChildren() == 1; } bool ItemContainer::contains(const Item *item) const { return m_children.contains(const_cast(item)); } Item *ItemContainer::itemForView(const LayoutingGuest *w) const { for (Item *item : std::as_const(m_children)) { if (item->isContainer()) { if (Item *result = item->asContainer()->itemForView(w)) return result; } else if (item->guest() == w) { return item; } } return nullptr; } Item::List ItemContainer::visibleChildren(bool includeBeingInserted) const { Item::List items; items.reserve(m_children.size()); for (Item *item : std::as_const(m_children)) { if (includeBeingInserted) { if (item->isVisible() || item->isBeingInserted()) items.push_back(item); } else { if (item->isVisible() && !item->isBeingInserted()) items.push_back(item); } } return items; } Item::List ItemContainer::items_recursive() const { Item::List items; items.reserve(30); // sounds like a good upper number to minimize allocations for (Item *item : std::as_const(m_children)) { if (auto c = item->asContainer()) { items.append(c->items_recursive()); } else { items.push_back(item); } } return items; } bool ItemContainer::contains_recursive(const Item *item) const { for (Item *it : std::as_const(m_children)) { if (it == item) { return true; } else if (it->isContainer()) { if (it->asContainer()->contains_recursive(item)) return true; } } return false; } int ItemContainer::visibleCount_recursive() const { int count = 0; for (Item *item : std::as_const(m_children)) { count += item->visibleCount_recursive(); } return count; } int ItemContainer::count_recursive() const { int count = 0; for (Item *item : std::as_const(m_children)) { if (auto c = item->asContainer()) { count += c->count_recursive(); } else { count++; } } return count; } bool ItemContainer::inSetSize() const { return std::any_of(m_children.cbegin(), m_children.cend(), [](Item *child) { return child->inSetSize(); }); } LayoutingHost::~LayoutingHost() = default; LayoutingSeparator::~LayoutingSeparator() = default; LayoutingSeparator::LayoutingSeparator(LayoutingHost *host, Qt::Orientation orientation, Core::ItemBoxContainer *container) : m_host(host) , m_orientation(orientation) , m_parentContainer(container) { } bool LayoutingSeparator::isVertical() const { return m_orientation == Qt::Vertical; } int LayoutingSeparator::position() const { const Point topLeft = geometry().topLeft(); return (isVertical() ? topLeft.y() : topLeft.x()) - offset(); } ItemBoxContainer *LayoutingSeparator::parentContainer() const { return m_parentContainer; } Qt::Orientation LayoutingSeparator::orientation() const { return m_orientation; } // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void LayoutingSeparator::setGeometry(int pos, int pos2, int length) { pos += offset(); Rect newGeo = geometry(); if (isVertical()) { // The separator itself is horizontal newGeo.setSize(Size(length, Core::Item::separatorThickness)); newGeo.moveTo(pos2, pos); } else { // The separator itself is vertical newGeo.setSize(Size(Core::Item::separatorThickness, length)); newGeo.moveTo(pos, pos2); } setGeometry(newGeo); } void LayoutingSeparator::free() { delete this; } bool LayoutingSeparator::isBeingDragged() const { return LayoutingSeparator::s_separatorBeingDragged != nullptr; } void LayoutingSeparator::onMousePress() { LayoutingSeparator::s_separatorBeingDragged = this; } void LayoutingSeparator::onMouseRelease() { LayoutingSeparator::s_separatorBeingDragged = nullptr; } int LayoutingSeparator::onMouseMove(Point pos, bool moveSeparator) { if (!isBeingDragged()) return -1; const int positionToGoTo = Core::pos(pos, m_orientation); const int minPos = m_parentContainer->minPosForSeparator_global(this); const int maxPos = m_parentContainer->maxPosForSeparator_global(this); if ((positionToGoTo > maxPos && position() <= positionToGoTo) || (positionToGoTo < minPos && position() >= positionToGoTo)) { // if current pos is 100, and max is 80, we do allow going to 90. // Would continue to violate, but only by 10, so allow. // On the other hand, if we're already past max-pos, don't make it worse and just // return if positionToGoTo is further away from maxPos. // Same reasoning for minPos return -1; } if (moveSeparator) m_parentContainer->requestSeparatorMove(this, positionToGoTo - position()); return positionToGoTo; } int LayoutingSeparator::offset() const { // almost always 0, unless someone set a spacing different than separator size const int diff = Item::layoutSpacing - Item::separatorThickness; // The separator will be position this much from actual layout position: return diff / 2; } void LayoutingSeparator::raise() { // No raising needed usually, as separators don't overlap with the dockwidgets. // For QtWidgets/QtQuick we do support it though. } class LayoutingGuest::Private { public: ObjectGuard layoutItem; }; Core::Item *LayoutingGuest::layoutItem() const { return d->layoutItem; } void LayoutingGuest::setLayoutItem(Item *item) { if (d->layoutItem == item) return; if (d->layoutItem) d->layoutItem->unref(); if (item) item->ref(); d->layoutItem = item; setLayoutItem_impl(item); } LayoutingGuest::LayoutingGuest() : d(new Private()) { } LayoutingGuest::~LayoutingGuest() { delete d; } /// Inserts a guest widget into the layout, to the specified location with some initial options /// the location is relative to the window, meaning Location_OnBottom will make the widget fill /// the entire bottom void LayoutingHost::insertItem(Core::LayoutingGuest *guest, Location loc, const InitialOption &initialOption) { if (!guest || !guest->layoutItem()) { // qWarning() << "insertItem: Something is null!"; return; } if (auto box = m_rootItem->asBoxContainer()) box->insertItem(guest->layoutItem(), loc, initialOption); } /// Inserts a guest widget into the layout but relative to another widget /// Similar to insertItem() but it's not relative to the window. /// See example in src/core/layouting/examples/qtwidgets/main.cpp void LayoutingHost::insertItemRelativeTo(Core::LayoutingGuest *guest, Core::LayoutingGuest *relativeTo, Location loc, const InitialOption &initialOption) { if (!guest || !relativeTo || !guest->layoutItem() || !relativeTo->layoutItem()) { // qWarning() << "insertItemRelativeTo: Something is null!"; return; } if (m_rootItem->asBoxContainer()) ItemBoxContainer::insertItemRelativeTo(guest->layoutItem(), relativeTo->layoutItem(), loc, initialOption); } #ifdef Q_CC_MSVC #pragma warning(pop) #endif