/* This file is part of KDDockWidgets. Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com Author: SĂ©rgio Martins This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "MultiSplitterLayout_p.h" #include "Logging_p.h" #include "MultiSplitter_p.h" #include "Frame_p.h" #include "FloatingWindow_p.h" #include "DockWidgetBase.h" #include "LastPosition_p.h" #include "DockRegistry_p.h" #include "Config.h" #include "Separator_p.h" #include "FrameworkWidgetFactory.h" #include "LayoutSaver.h" #include #include #include #include #define INDICATOR_MINIMUM_LENGTH 100 #define KDDOCKWIDGETS_MIN_WIDTH 80 #define KDDOCKWIDGETS_MIN_HEIGHT 90 using namespace KDDockWidgets; const QString MultiSplitterLayout::s_magicMarker = QStringLiteral("bac9948e-5f1b-4271-acc5-07f1708e2611"); static Qt::Orientation anchorOrientationForLocation(Location l) { return (l == Location_OnLeft || l == Location_OnRight) ? Qt::Vertical : Qt::Horizontal; } MultiSplitterLayout::MultiSplitterLayout(MultiSplitter *parent) : QObject(parent) , m_multiSplitter(parent) , m_leftAnchor(new Anchor(Qt::Vertical, this, Anchor::Type_LeftStatic)) , m_topAnchor(new Anchor(Qt::Horizontal, this, Anchor::Type_TopStatic)) , m_rightAnchor(new Anchor(Qt::Vertical, this, Anchor::Type_RightStatic)) , m_bottomAnchor(new Anchor(Qt::Horizontal, this, Anchor::Type_BottomStatic)) , m_staticAnchorGroup(this) { Q_ASSERT(parent); DockRegistry::self()->registerLayout(this); setSize(parent->size()); qCDebug(multisplittercreation()) << "MultiSplitter"; connect(this, &MultiSplitterLayout::widgetCountChanged, this, [this] { Q_EMIT visibleWidgetCountChanged(visibleCount()); }); m_leftAnchor->setObjectName(QStringLiteral("left")); m_rightAnchor->setObjectName(QStringLiteral("right")); m_bottomAnchor->setObjectName(QStringLiteral("bottom")); m_topAnchor->setObjectName(QStringLiteral("top")); m_leftAnchor->setFrom(m_topAnchor); m_leftAnchor->setTo(m_bottomAnchor); m_rightAnchor->setFrom(m_topAnchor); m_rightAnchor->setTo(m_bottomAnchor); m_topAnchor->setFrom(m_leftAnchor); m_topAnchor->setTo(m_rightAnchor); m_bottomAnchor->setFrom(m_leftAnchor); m_bottomAnchor->setTo(m_rightAnchor); m_staticAnchorGroup.left = m_leftAnchor; m_staticAnchorGroup.right = m_rightAnchor; m_staticAnchorGroup.top = m_topAnchor; m_staticAnchorGroup.bottom = m_bottomAnchor; clear(); positionStaticAnchors(); // Initialize min size updateSizeConstraints(); m_inCtor = false; } MultiSplitterLayout::~MultiSplitterLayout() { qCDebug(multisplittercreation) << "~MultiSplitter" << this; m_inDestructor = true; const auto anchors = m_anchors; qDeleteAll(anchors); DockRegistry::self()->unregisterLayout(this); } /**static*/ QSize MultiSplitterLayout::hardcodedMinimumSize() { return QSize(KDDOCKWIDGETS_MIN_WIDTH, KDDOCKWIDGETS_MIN_HEIGHT); } MultiSplitter *MultiSplitterLayout::multiSplitter() const { return m_multiSplitter; } bool MultiSplitterLayout::validateInputs(QWidgetOrQuick *widget, Location location, const Frame *relativeToFrame, AddingOption option) const { if (!widget) { qWarning() << Q_FUNC_INFO << "Widget is null"; return false; } const bool isDockWidget = qobject_cast(widget); const bool isStartHidden = option & AddingOption_StartHidden; if (!qobject_cast(widget) && !qobject_cast(widget) && !isDockWidget) { qWarning() << "Unknown widget type" << widget; return false; } if (isDockWidget != isStartHidden) { qWarning() << "Wrong parameters" << isDockWidget << isStartHidden; return false; } if (relativeToFrame && relativeToFrame == widget) { qWarning() << "widget can't be relative to itself"; return false; } Item *item = itemForFrame(qobject_cast(widget)); if (contains(item)) { qWarning() << "MultiSplitterLayout::addWidget: Already contains" << widget; return false; }// TODO: check for widget changing parent if (location == Location_None) { qWarning() << "MultiSplitterLayout::addWidget: not adding to location None"; return false; } const bool relativeToThis = relativeToFrame == nullptr; Item *relativeToItem = itemForFrame(relativeToFrame); if (!relativeToThis && !contains(relativeToItem)) { qWarning() << "MultiSplitterLayout::addWidget: Doesn't contain relativeTo:" << relativeToFrame << "; options=" << option; return false; } return true; } std::pair MultiSplitterLayout::boundInterval(int newPos1, Anchor* anchor1, int newPos2, Anchor *anchor2) const { const int bound1 = boundPositionForAnchor(anchor1, Anchor::Side1); const int bound2 = boundPositionForAnchor(anchor2, Anchor::Side2); if (newPos1 >= bound1 && newPos2 <= bound2) { // Simplest case, it's bounded. return { newPos1, newPos2 }; } if (newPos1 < bound1) { // the anchor1 is out of bounds const int bythismuch = bound1 - newPos1; newPos1 = bound1; newPos2 = newPos2 + bythismuch; if (newPos2 > bound2) { qWarning() << "Adjusted interval still out of bounds. Not enough space. #1" << "; newPos1=" << newPos1 << "; newPos2=" << newPos2 << "; bounds=" << bound1 << bound2 << "; anchor1=" << anchor1 << "; anchor2=" << anchor2 << "; size=" << size(); } return { newPos1, newPos2 }; } else if (newPos2 > bound2) { // the anchor2 is out of bounds const int bythismuch = newPos2 - bound2; newPos2 = bound2; newPos1 = newPos1 - bythismuch; if (newPos1 < bound1) { qWarning() << "Adjusted interval still out of bounds. Not enough space. #2" << "; newPos1=" << newPos1 << "; newPos2=" << newPos2 << "; bounds=" << bound1 << bound2 << "; anchor1=" << anchor1 << "; anchor2=" << anchor2 << "; size=" << size(); } return { newPos1, newPos2 }; } return { newPos1, newPos2 }; } void MultiSplitterLayout::addWidget(QWidgetOrQuick *w, Location location, Frame *relativeToWidget, AddingOption option) { auto frame = qobject_cast(w); qCDebug(addwidget) << Q_FUNC_INFO << w << "; location=" << locationStr(location) << "; relativeTo=" << relativeToWidget << "; size=" << size() << "; w.size=" << w->size() << "; w.min=" << KDDockWidgets::widgetMinLength(w, anchorOrientationForLocation(location)) << "; frame=" << frame << "; option=" << option; if (itemForFrame(frame) != nullptr) { // Item already exists, remove it. // Changing the frame parent will make the item clean itself up. It turns into a placeholder and is removed by unrefOldPlaceholders frame->setParent(nullptr); // so ~Item doesn't delete it frame->setLayoutItem(nullptr); // so Item is destroyed, as there's no refs to it } // Make some sanity checks: if (!validateInputs(w, location, relativeToWidget, option)) return; unrefOldPlaceholders(framesFrom(w)); Item *relativeToItem = itemForFrame(relativeToWidget); ensureEnoughSize(w, location, relativeToItem); if (option & AddingOption_StartHidden) { addAsPlaceholder(qobject_cast(w), location, relativeToItem); return; } Anchor *newAnchor = nullptr; const QRect dropRect = rectForDrop(w, location, relativeToItem); if (dropRect.size().isNull() || dropRect.x() < 0 || dropRect.y() < 0) { qWarning() << Q_FUNC_INFO << "Invalid drop rect" << dropRect << "\n size=" << m_multiSplitter->size() << "; size="<< m_size << "\n location=" << location << "\n window=" << m_multiSplitter->window() << "\n this=" << this << "\n availableHeight=" << availableLengthForOrientation(Qt::Horizontal) << "\n availableWidth=" << availableLengthForOrientation(Qt::Vertical) << "\n widget.minSize=" << widgetMinLength(w, anchorOrientationForLocation(location)); return; } m_addingItem = true; auto result = this->createTargetAnchorGroup(location, relativeToItem); AnchorGroup targetAnchorGroup = result.first; newAnchor = result.second; if (newAnchor && !newAnchor->isFollowing()) { const int anchorThickness = Anchor::thickness(/*static=*/false); qCDebug(sizing) << "Drop rect" << dropRect; int posForExistingAnchor = 0; int posForNewAnchor = 0; Anchor *existingAnchor = targetAnchorGroup.anchor(location); Anchor *direction1Anchor = nullptr; Anchor *direction2Anchor = nullptr; switch (location) { case Location_OnLeft: posForExistingAnchor = dropRect.left() - existingAnchor->thickness(); posForNewAnchor = dropRect.right() + 1; break; case Location_OnTop: posForExistingAnchor = dropRect.top() - existingAnchor->thickness(); posForNewAnchor = dropRect.bottom() + 1; break; case Location_OnBottom: posForExistingAnchor = dropRect.bottom() + 1; posForNewAnchor = dropRect.top() - anchorThickness; break; case Location_OnRight: posForExistingAnchor = dropRect.right() + 1; posForNewAnchor = dropRect.left() - anchorThickness; break; case Location_None: Q_ASSERT(false); break; } int delta1 = 0; int delta2 = 0; const int originalExistingAnchorPos = existingAnchor->position(); switch (location) { case Location_OnLeft: case Location_OnTop: direction1Anchor = existingAnchor; direction2Anchor = newAnchor; std::tie(posForExistingAnchor, posForNewAnchor) = boundInterval(posForExistingAnchor, existingAnchor, posForNewAnchor, newAnchor); delta1 = originalExistingAnchorPos - posForExistingAnchor; delta2 = posForNewAnchor - posForExistingAnchor; break; case Location_OnRight: case Location_OnBottom: direction1Anchor = newAnchor; direction2Anchor = existingAnchor; std::tie(posForNewAnchor, posForExistingAnchor) = boundInterval(posForNewAnchor, newAnchor, posForExistingAnchor, existingAnchor); delta1 = posForExistingAnchor - posForNewAnchor; delta2 = posForExistingAnchor - originalExistingAnchorPos; break; case Location_None: qWarning() << Q_FUNC_INFO << "Location can't be none"; return; } newAnchor->setPosition(posForNewAnchor); if (posForExistingAnchor != originalExistingAnchorPos) { if (existingAnchor->isStatic()) { qWarning() << "Trying to move static anchor from" << originalExistingAnchorPos << "to" << posForExistingAnchor << "; location=" << location << "; dropRect=" << dropRect << "; existingAnchor=" << existingAnchor << "; size=" << m_size << "; Qt::WA_PendingResizeEvent=" << m_multiSplitter->testAttribute(Qt::WA_PendingResizeEvent) << "; Qt::WA_WState_Created=" << m_multiSplitter->testAttribute(Qt::WA_WState_Created); } existingAnchor->setPosition(posForExistingAnchor); } // Make sure not just the side1/side2 adjacent widgets are contributing space for our new widget // the ones adjacents to the adjacents (recursive) must also give. // The code would work fine without this, it's just that it wouldn't look fair. propagateResize(delta1, direction1Anchor, Anchor::Side1); propagateResize(delta2, direction2Anchor, Anchor::Side2); } if (newAnchor) { // Also ensure the widget has a minimum size in the other direction. So, when adding to // left/right, it will still have its minimum height honoured, and vice-versa. QPair adjacentAnchors = targetAnchorGroup.adjacentAnchors(newAnchor); const int bound1 = boundPositionForAnchor(adjacentAnchors.first, Anchor::Side1); const int bound2 = boundPositionForAnchor(adjacentAnchors.second, Anchor::Side2); const Qt::Orientation otherOrientation = adjacentAnchors.first->orientation(); const int min = widgetMinLength(w, otherOrientation); const int has = targetAnchorGroup.itemSize(otherOrientation); const int needs = min - has; if (needs > 0) { const int pos1 = qMax(bound1, adjacentAnchors.first->position() - needs); const int pos2 = pos1 + adjacentAnchors.first->thickness() + min; Q_ASSERT(pos2 <= bound2); adjacentAnchors.first->setPosition(pos1); adjacentAnchors.second->setPosition(pos2); } } auto sourceMultiSplitter = qobject_cast(w); auto sourceMultiSplitterLayout = sourceMultiSplitter ? sourceMultiSplitter->multiSplitterLayout() : nullptr; if (sourceMultiSplitterLayout) { auto items = sourceMultiSplitterLayout->items(); targetAnchorGroup.addItem(sourceMultiSplitterLayout); addItems_internal(items); } else { Q_ASSERT(frame); auto item = new Item(frame, this); targetAnchorGroup.addItem(item); addItems_internal(ItemList{ item }); } updateAnchorFollowing(); m_addingItem = false; maybeCheckSanity(); } void MultiSplitterLayout::addItems_internal(const ItemList &items, bool updateConstraints, bool emitSignal) { m_items << items; if (updateConstraints) updateSizeConstraints(); for (auto item : items) { item->setLayout(this); if (item->frame()) { item->setVisible(true); item->frame()->installEventFilter(this); Q_EMIT widgetAdded(item); } } if (emitSignal) Q_EMIT widgetCountChanged(m_items.size()); } void MultiSplitterLayout::addAsPlaceholder(DockWidgetBase *dockWidget, Location location, Item *relativeTo) { if (!dockWidget) { qWarning() << Q_FUNC_INFO << "null dockwidget"; return; } dockWidget->setParent(nullptr); auto result = createTargetAnchorGroup(location, relativeTo); AnchorGroup targetAnchorGroup = result.first; auto frame = Config::self().frameworkWidgetFactory()->createFrame(m_multiSplitter); auto item = new Item(frame, this); targetAnchorGroup.addItem(item); addItems_internal(ItemList{ item }, false); dockWidget->addPlaceholderItem(item); delete frame; updateAnchorFollowing(); Q_ASSERT(!dockWidget->isVisible()); maybeCheckSanity(); } void MultiSplitterLayout::ensureEnoughSize(const QWidgetOrQuick *widget, Location location, const Item *relativeToItem) { const int neededAnchorThickness = isEmpty() ? 0 : Anchor::thickness(/*static=*/ false); const QSize available = availableSize(); const QSize widgetMin = { widgetMinLength(widget, Qt::Vertical), widgetMinLength(widget, Qt::Horizontal) }; const QSize oldSize = m_size; const int neededWidth = widgetMin.width() - available.width() + neededAnchorThickness; const int neededHeight = widgetMin.height() - available.height() + neededAnchorThickness; QSize newSize = m_size; if (neededWidth > 0) newSize.setWidth(newSize.width() + neededWidth); if (neededHeight > 0) newSize.setHeight(newSize.height() + neededHeight); if (newSize != m_size) setSize(newSize); // Just to make sure: if (lengthForDrop(widget, location, relativeToItem).isNull()) { qWarning() << Q_FUNC_INFO << "failed! Please report a bug." << "; oldAvailable=" << available << "; newAvailable=" << availableSize() << "; newSize=" << newSize << "; m_size=" << m_size << "; oldSize=" << oldSize << "; widgetMin=" << widgetMin << "; isEmpty=" << isEmpty(); } } void MultiSplitterLayout::ensureAnchorsBounded() { //Ensures all separators are within their bounds, meaning all items obey their min size positionStaticAnchors(); ensureItemsMinSize(); } static Anchor::List removeSmallestPath(QVector &paths) { // Removes and returns the smallest list Anchor::List smallestPath; int indexOfSmallest = 0; for (int i = 0, end = paths.size(); i < end; ++i) { const Anchor::List &path = paths.at(i); if (path.size() <= smallestPath.size() || smallestPath.isEmpty()) { smallestPath = path; indexOfSmallest = i; } } paths.removeAt(indexOfSmallest); return smallestPath; } void MultiSplitterLayout::propagateResize(int delta, Anchor *fromAnchor, Anchor::Side direction) { if (delta < 0) qWarning() << Q_FUNC_INFO << "Invalid delta" << delta << fromAnchor << direction; if (delta <= 0 || fromAnchor->isStatic()) return; QVector paths; collectPaths(paths, fromAnchor, direction); for (const Anchor::List &path : qAsConst(paths)) { qCDebug(sizing) << Q_FUNC_INFO << path; } Anchor::List anchorsThatAlreadyContributed; anchorsThatAlreadyContributed.push_back(fromAnchor); while (!paths.isEmpty()) { // Get smallest path: Anchor::List smallestPath = removeSmallestPath(/*by-ref*/paths); if (smallestPath.size() <= 1) { // Nothing to do, it has a single anchor, which was already adjusted in addWidget() continue; } const bool towardsSide1 = direction == Anchor::Side1; const bool towardsSide2 = !towardsSide1; const int sign = towardsSide1 ? -1 : 1; const int contributionPerAnchor = (delta / (smallestPath.size() - 1)) * sign; // n-1 because the initial anchor already contributed if (qAbs(contributionPerAnchor) < 5) { // Too small, don't bother continue; } // Now make those anchors contribute, skipping the first for (int i = 1, end = smallestPath.size(); i < end; ++i) { Anchor *a = smallestPath.at(i); if (!anchorsThatAlreadyContributed.contains(a)) { // When moving anchors don't allow widgets to go bellow their min size const int bound = boundPositionForAnchor(a, direction); int newPosition = a->position() + contributionPerAnchor; if ((towardsSide1 && newPosition < bound) || (towardsSide2 && newPosition > bound)) { newPosition = bound; } if (a->position() != newPosition) { a->setPosition(newPosition); anchorsThatAlreadyContributed.push_back(a); } } } } } void MultiSplitterLayout::collectPaths(QVector &paths, Anchor *fromAnchor, Anchor::Side direction) { if (fromAnchor->isStatic()) { // We've finally reached a border anchor, we can stop now. return; } if (paths.isEmpty()) paths.push_back({}); int currentPathIndex = paths.size() - 1; // Store the index instead of using "Anchor::List ¤tPath = paths.last();" as the references are stable, as the paths vector reallocates paths[currentPathIndex].push_back(fromAnchor); const ItemList items = fromAnchor->items(direction); for (int i = 0, end = items.size(); i < end; ++i) { Anchor *nextAnchor = items[i]->anchorAtSide(direction, fromAnchor->orientation()); if (i > 0) { Anchor::List newPath = paths[currentPathIndex]; paths.push_back(newPath); } collectPaths(paths, nextAnchor, direction); } } void MultiSplitterLayout::resizeItem(Frame *frame, int newSize, Qt::Orientation orientation) { // Used for unit-tests only Item *item = itemForFrame(frame); Q_ASSERT(item); Anchor *a = item->anchorAtSide(Anchor::Side2, orientation); Q_ASSERT(!a->isStatic()); const int widgLength = item->length(orientation); const int delta = newSize - widgLength; qCDebug(::anchors) << Q_FUNC_INFO << "Old position:" << a->position() << "; old w.geo=" << item->geometry(); a->setPosition(a->position() + delta); qCDebug(::anchors) << Q_FUNC_INFO << "New position:" << a->position() << "; new w.geo=" << item->geometry(); } void MultiSplitterLayout::ensureItemsMinSize() { for (Item *item : qAsConst(m_items)) { item->ensureMinSize(Qt::Vertical); item->ensureMinSize(Qt::Horizontal); } } void MultiSplitterLayout::addMultiSplitter(MultiSplitter *sourceMultiSplitter, Location location, Frame *relativeTo) { qCDebug(addwidget) << Q_FUNC_INFO << sourceMultiSplitter << location << relativeTo; addWidget(sourceMultiSplitter, location, relativeTo); } void MultiSplitterLayout::removeItem(Item *item) { if (!item || m_inDestructor || !m_items.contains(item)) return; maybeCheckSanity(); if (!item->isPlaceholder()) item->frame()->removeEventFilter(this); AnchorGroup anchorGroup = item->anchorGroup(); anchorGroup.removeItem(item); m_items.removeOne(item); updateAnchorFollowing(); Q_EMIT widgetRemoved(item); Q_EMIT widgetCountChanged(m_items.size()); } bool MultiSplitterLayout::contains(const Item *item) const { return m_items.contains(const_cast(item)); } bool MultiSplitterLayout::contains(const Frame *frame) const { return itemForFrame(frame) != nullptr; } Item *MultiSplitterLayout::itemAt(QPoint p) const { for (Item *item : m_items) { if (!item->isPlaceholder() && item->geometry().contains(p)) return item; } return nullptr; } void MultiSplitterLayout::clear(bool alsoDeleteStaticAnchors) { const int oldCount = count(); const int oldVisibleCount = visibleCount(); const auto items = m_items; m_items.clear(); // Clear the item list first, do avoid ~Item() triggering a removal from the list qDeleteAll(items); const auto anchors = m_anchors; m_anchors.clear(); for (Anchor *anchor : qAsConst(anchors)) { anchor->clear(); if (!anchor->isStatic() || alsoDeleteStaticAnchors) { delete anchor; } } if (alsoDeleteStaticAnchors) { m_anchors.clear(); m_topAnchor = nullptr; m_bottomAnchor = nullptr; m_leftAnchor = nullptr; m_rightAnchor = nullptr; m_staticAnchorGroup.left = nullptr; m_staticAnchorGroup.top = nullptr; m_staticAnchorGroup.right = nullptr; m_staticAnchorGroup.bottom = nullptr; } else { m_anchors = { m_topAnchor, m_bottomAnchor, m_leftAnchor, m_rightAnchor }; } if (oldCount > 0) Q_EMIT widgetCountChanged(0); if (oldVisibleCount > 0) Q_EMIT visibleWidgetCountChanged(0); } int MultiSplitterLayout::visibleCount() const { int count = 0; for (auto item : m_items) if (!item->isPlaceholder()) count++; return count; } int MultiSplitterLayout::placeholderCount() const { return count() - visibleCount(); } void MultiSplitterLayout::removeAnchor(Anchor *anchor) { if (!m_inDestructor) m_anchors.removeOne(anchor); } QPair MultiSplitterLayout::boundPositionsForAnchor(Anchor *anchor) const { if (anchor->isStatic()) { if (anchor == m_leftAnchor || anchor == m_topAnchor) { return {0, 0}; } else if (anchor == m_rightAnchor || anchor == m_bottomAnchor) { const int max = length(anchor->orientation()) - Anchor::thickness(true); return {max, max}; } } if (anchor->isFollowing()) anchor = anchor->endFollowee(); const int minSide1Length = anchor->cumulativeMinLength(Anchor::Side1); const int minSide2Length = anchor->cumulativeMinLength(Anchor::Side2); const int length = anchor->isVertical() ? width() : height(); const int bound1 = qMax(0, minSide1Length - anchor->thickness()); const int bound2 = qMax(0, length - minSide2Length); if (bound2 < bound1) { qWarning() << Q_FUNC_INFO << "Invalid bounds" << "; bound1=" << bound1 << "; bound2=" << bound2 << "; layout.size=" << size() << "; layout.min=" << minimumSize() << "; anchor=" << anchor << "; orientation=" << anchor->orientation() << "; minSide1Length=" << minSide1Length << "; minSide2Length=" << minSide2Length << "; side1=" << anchor->side1Items() << "; side2=" << anchor->side2Items() << "; followee=" << anchor->followee() << "; thickness=" << anchor->thickness(); } return { bound1, bound2 }; } QHash > MultiSplitterLayout::boundPositionsForAllAnchors() const { QHash > result; for (Anchor *anchor : m_anchors) result.insert(anchor, boundPositionsForAnchor(anchor)); return result; } int MultiSplitterLayout::boundPositionForAnchor(Anchor *anchor, Anchor::Side direction) const { auto bounds = boundPositionsForAnchor(anchor); return direction == Anchor::Side1 ? bounds.first : bounds.second; } MultiSplitterLayout::Length MultiSplitterLayout::availableLengthForDrop(Location location, const Item *relativeTo) const { Length result; const bool relativeToThis = relativeTo == nullptr; AnchorGroup anchors = relativeToThis ? staticAnchorGroup() : relativeTo->anchorGroup(); Anchor *anchor = nullptr; int thisLength = 0; switch (location) { case KDDockWidgets::Location_None: qWarning() << "MultiSplitterLayout::availableLengthForDrop invalid location for dropping"; return result; case KDDockWidgets::Location_OnLeft: anchor = anchors.left; thisLength = width(); break; case KDDockWidgets::Location_OnTop: anchor = anchors.top; thisLength = height(); break; case KDDockWidgets::Location_OnRight: anchor = anchors.right; thisLength = width(); break; case KDDockWidgets::Location_OnBottom: anchor = anchors.bottom; thisLength = height(); break; } anchor = anchor->isFollowing() ? anchor->endFollowee() : anchor; const int minForAlreadyOccupied1 = anchor->cumulativeMinLength(Anchor::Side1) - anchor->thickness(); // TODO: Check if this is correct, we're discounting the anchor twice const int minForAlreadyOccupied2 = anchor->cumulativeMinLength(Anchor::Side2) - anchor->thickness(); const int side1AvailableLength = anchor->position() - minForAlreadyOccupied1; const int side2AvailableLength = thisLength - (anchor->position() + anchor->thickness()) - minForAlreadyOccupied2; const bool needsNewAnchor = hasVisibleItems(); // If a new anchor is needed then we need space for the drag handle and such. const int newAnchorThickness = needsNewAnchor ? Anchor::thickness(/*static=*/false) : 0; // This useless space doesn't belong to side1 or side2 specifically. So account for it separately. const int unusableSpace = newAnchorThickness; const int usableLength = qMax(0, side1AvailableLength + side2AvailableLength - unusableSpace); if (usableLength > 0) { qreal factor = (side1AvailableLength * 1.0) / (side1AvailableLength + side2AvailableLength); result.side1Length = int(qRound(usableLength * factor)); // rounding not really needed, but makes things more fair probably result.side2Length = usableLength - result.side1Length; } qCDebug(sizing) << Q_FUNC_INFO << "; available=" << result.length() << result.side1Length << result.side2Length << "; side1AvailableLength=" << side1AvailableLength << "; side2AvailableLength=" << side2AvailableLength << "; minForAlreadyOccupied1=" << minForAlreadyOccupied1 << "; minForAlreadyOccupied2=" << minForAlreadyOccupied2 << "; thisLength=" << thisLength << "; anchorPos=" << anchor->position() << "; unusableSpace=" << unusableSpace; return result; } int MultiSplitterLayout::availableLengthForOrientation(Qt::Orientation orientation) const { Length l = availableLengthForDrop(orientation == Qt::Vertical ? Location_OnLeft : Location_OnTop, nullptr); return l.length(); } QSize MultiSplitterLayout::availableSize() const { return { availableLengthForOrientation(Qt::Vertical), availableLengthForOrientation(Qt::Horizontal) }; } /* * Returns the width or height the widget will get when dropped. */ MultiSplitterLayout::Length MultiSplitterLayout::lengthForDrop(const QWidgetOrQuick *widget, Location location, const Item *relativeTo) const { Q_ASSERT(location != Location_None); const Qt::Orientation anchorOrientation = anchorOrientationForLocation(location); const int widgetCurrentLength = widgetLength(widget, anchorOrientation); Length available = availableLengthForDrop(location, relativeTo); const int requiredAtLeast = widgetMinLength(widget, anchorOrientation); if (available.length() < requiredAtLeast) { qCDebug(sizing) << Q_FUNC_INFO << "\n Not enough space. available=" << available.length() << "; required=" << requiredAtLeast << "; m_size=" << m_size; return {}; } const int suggestedLength = qMin(widgetCurrentLength, int(0.4 * length(anchorOrientation))); available.setLength(qBound(requiredAtLeast, suggestedLength, available.length())); qCDebug(sizing) << "MultiSplitterLayout::lengthForDrop length=" << available.length() << "; s1=" << available.side1Length << "; s2="<< available.side2Length << "; relativeTo=" << relativeTo << "; relativeTo.geo=" << (relativeTo ? relativeTo->geometry() : QRect()) << "; widgetCurrentLength=" << widgetCurrentLength; return available; } QRect MultiSplitterLayout::rectForDrop(MultiSplitterLayout::Length lfd, Location location, QRect relativeToRect) const { QRect result; const int widgetLength = lfd.length(); const int newAnchorThickness = isEmpty() ? 0 : Anchor::thickness(/*static=*/false); const int side1Length = lfd.side1Length; const int staticAnchorThickness = Anchor::thickness(/**static=*/true); switch (location) { case Location_OnLeft: result = QRect(qMax(0, relativeToRect.x() - side1Length), relativeToRect.y(), widgetLength, relativeToRect.height()); break; case Location_OnTop: result = QRect(relativeToRect.x(), qMax(0, relativeToRect.y() - side1Length), relativeToRect.width(), widgetLength); break; case Location_OnRight: result = QRect(qMin(relativeToRect.right() + 1 - side1Length + newAnchorThickness, width() - widgetLength - staticAnchorThickness), relativeToRect.y(), widgetLength, relativeToRect.height()); break; case Location_OnBottom: result = QRect(relativeToRect.x(), qMin(relativeToRect.bottom() + 1 - side1Length + newAnchorThickness, height() - widgetLength - staticAnchorThickness), relativeToRect.width(), widgetLength); break; default: break; } qCDebug(sizing) << "MultiSplitterLayout::rectForDrop rect=" << result << "; result.bottomRight=" << result.bottomRight() << "; location=" << location << "; s1=" << side1Length << "; relativeToRect.bottomRight=" << relativeToRect.bottomRight(); return result; } QRect MultiSplitterLayout::rectForDrop(const QWidgetOrQuick *widgetBeingDropped, Location location, const Item *relativeTo) const { Q_ASSERT(widgetBeingDropped); Length lfd = lengthForDrop(widgetBeingDropped, location, relativeTo); const bool needsMoreSpace = lfd.isNull(); if (needsMoreSpace) { // This is the case with the drop indicators. If there's not enough space let's still // draw some indicator drop. The window will resize to accommodate the drop. lfd.side1Length = INDICATOR_MINIMUM_LENGTH / 2; lfd.side2Length = INDICATOR_MINIMUM_LENGTH - lfd.side1Length; } const int staticAnchorThickness = Anchor::thickness(/**static=*/true); const bool relativeToThis = relativeTo == nullptr; const QRect relativeToRect = relativeToThis ? m_multiSplitter->rect().adjusted(staticAnchorThickness, staticAnchorThickness, -staticAnchorThickness, -staticAnchorThickness) : relativeTo->geometry(); // This function is split in two just so we can unit-test the math in the second one, which is more involved QRect result = rectForDrop(lfd, location, relativeToRect); return result; } void MultiSplitterLayout::setAnchorBeingDragged(Anchor *anchor) { m_anchorBeingDragged = anchor; } Anchor::List MultiSplitterLayout::anchorsFollowing(Anchor *followee) const { if (!followee) return {}; Anchor::List followers; for (Anchor *a : m_anchors) { if (a->followee() == followee) followers.push_back(a); } return followers; } int MultiSplitterLayout::numAchorsFollowing() const { int count = 0; for (Anchor *a : m_anchors) { if (a->isFollowing()) count++; } return count; } int MultiSplitterLayout::numVisibleAnchors() const { int count = 0; for (Anchor *a : m_anchors) { if (a->separatorWidget()->isVisible()) count++; } return count; } Anchor *MultiSplitterLayout::staticAnchor(Anchor::Type type) const { if (type == Anchor::Type_TopStatic) return m_topAnchor; if (type == Anchor::Type_BottomStatic) return m_bottomAnchor; if (type == Anchor::Type_LeftStatic) return m_leftAnchor; if (type == Anchor::Type_RightStatic) return m_rightAnchor; return nullptr; } Anchor *MultiSplitterLayout::staticAnchor(Anchor::Side side, Qt::Orientation orientation) const { if (orientation == Qt::Vertical) { return side == Anchor::Side1 ? m_leftAnchor : m_rightAnchor; } else { return side == Anchor::Side1 ? m_topAnchor : m_bottomAnchor; } } AnchorGroup MultiSplitterLayout::anchorsForPos(QPoint pos) const { Item *item = itemAt(pos); if (!item) return AnchorGroup(const_cast(this)); return item->anchorGroup(); } void MultiSplitterLayout::dumpDebug() const { Q_EMIT aboutToDumpDebug(); qDebug() << Q_FUNC_INFO << "m_size=" << m_size << "; minimumSize=" << minimumSize(); qDebug() << "Items:"; for (auto item : items()) { qDebug() <<" " << item << "; min.width=" << item->minLength(Qt::Vertical) << "; min.height=" << item->minLength(Qt::Horizontal) << "; geometry=" << item->geometry() << "; isPlaceholder=" << item->isPlaceholder() << "; refCount=" << item->refCount(); if (Frame *frame = item->frame()) frame->dumpDebug(); } qDebug() << "Anchors:"; for (Anchor *anchor : m_anchors) { auto side1Widgets = anchor->items(Anchor::Side1); auto side2Widgets = anchor->items(Anchor::Side2); auto bounds = anchor->isStatic() ? QPair() : boundPositionsForAnchor(anchor); qDebug() << "\n " << anchor << "; side1=" << side1Widgets << "; side2=" << side2Widgets << "; pos=" << anchor->position() << "; sepWidget.pos=" << (anchor->isVertical() ? anchor->separatorWidget()->x() : anchor->separatorWidget()->y()) << "; sepWidget.visible=" << anchor->separatorWidget()->isVisible() << "; geo=" << anchor->geometry() << "; sep.geo=" << anchor->separatorWidget()->geometry() << "; bounds=" << bounds << "; orientation=" << anchor->orientation() << "; isFollowing=" << anchor->isFollowing() << "; followee=" << anchor->followee() << "; from=" << ((void*)anchor->from()) << "; to=" << ((void*)anchor->to()) << "; positionPercentage=" << anchor->positionPercentage(); } qDebug() << "Num Frame:" << Frame::dbg_numFrames(); qDebug() << "Num FloatingWindow:" << FloatingWindow::dbg_numFrames(); } void MultiSplitterLayout::positionStaticAnchors() { qCDebug(sizing) << Q_FUNC_INFO; m_leftAnchor->setPosition(0); m_topAnchor->setPosition(0); m_bottomAnchor->setPosition(height() - m_bottomAnchor->thickness()); m_rightAnchor->setPosition(width() - m_rightAnchor->thickness()); } void MultiSplitterLayout::redistributeSpace(QSize oldSize, QSize newSize) { positionStaticAnchors(); if (oldSize == newSize || !oldSize.isValid() || !newSize.isValid()) return; qCDebug(sizing) << Q_FUNC_INFO << "old=" << oldSize << "; new=" << newSize; const bool widthChanged = oldSize.width() != newSize.width(); const bool heightChanged = oldSize.height() != newSize.height(); if (widthChanged) redistributeSpace_recursive(m_leftAnchor, 0); if (heightChanged) redistributeSpace_recursive(m_topAnchor, 0); } void MultiSplitterLayout::redistributeSpace_recursive(Anchor *fromAnchor, int minAnchorPos) { for (Item *item : fromAnchor->items(Anchor::Side2)) { Anchor *nextAnchor = item->anchorAtSide(Anchor::Side2, fromAnchor->orientation()); if (nextAnchor->isStatic()) continue; // We use the minPos of the Anchor that had non-placeholder items on its side1. if (nextAnchor->hasNonPlaceholderItems(Anchor::Side1)) minAnchorPos = nextAnchor->minPosition(); if (nextAnchor->hasNonPlaceholderItems(Anchor::Side2) && !nextAnchor->isFollowing()) { const int newPosition = int(nextAnchor->positionPercentage() * length(nextAnchor->orientation())); // But don't let the anchor go out of bounds, it must respect its widgets min sizes auto bounds = boundPositionsForAnchor(nextAnchor); // For the bounding, use Anchor::minPosition, as we're not making the anchors on the left/top shift, which boundsPositionsForAnchor() assumes. const int newPositionBounded = qMax(bounds.first, qBound(minAnchorPos, newPosition, bounds.second)); qCDebug(sizing) << Q_FUNC_INFO << nextAnchor << "bounds.first=" << bounds.first << "; newPosition=" << newPosition << "; bounds.first=" << bounds.first << "; bounds.second=" << bounds.second << "; newPositionBounded=" << newPositionBounded << "; oldPosition=" << nextAnchor->position() << "; size=" << m_size << "; nextAnchor.minPosition=" << minAnchorPos; nextAnchor->setPosition(newPositionBounded, Anchor::SetPositionOption_DontRecalculatePercentage); } redistributeSpace_recursive(nextAnchor, minAnchorPos); } } void MultiSplitterLayout::updateSizeConstraints() { const int minH = m_topAnchor->cumulativeMinLength(Anchor::Side2); const int minW = m_leftAnchor->cumulativeMinLength(Anchor::Side2); const QSize newMinSize = QSize(minW, minH); qCDebug(sizing) << Q_FUNC_INFO << "Updating size constraints from" << m_minSize << "to" << newMinSize; setMinimumSize(newMinSize); } int MultiSplitterLayout::wastedSpacing(Qt::Orientation orientation) const { // Wasted spacing due to using splitters: int numAnchors = 0; for (Anchor *anchor : m_anchors) { if (anchor->orientation() == orientation) numAnchors++; } return (2 * Anchor::thickness(/*static=*/ true)) + ((numAnchors - 2) * Anchor::thickness(/*static=*/ false)); // 2 of the anchors are always static } AnchorGroup MultiSplitterLayout::staticAnchorGroup() const { return m_staticAnchorGroup; } Anchor::List MultiSplitterLayout::anchors(Qt::Orientation orientation, bool includeStatic, bool includePlaceholders) const { Anchor::List result; for (Anchor *anchor : m_anchors) { if ((includeStatic || !anchor->isStatic()) && (includePlaceholders || !anchor->isFollowing()) && anchor->orientation() == orientation) result << anchor; } return result; } Anchor *MultiSplitterLayout::newAnchor(AnchorGroup &group, Location location) { qCDebug(::anchors) << "MultiSplitterLayout::newAnchor" << location; Anchor *newAnchor = nullptr; Anchor *donor = nullptr; switch (location) { case Location_OnLeft: donor = group.left; newAnchor = Anchor::createFrom(donor); group.right = newAnchor; break; case Location_OnTop: donor = group.top; newAnchor = Anchor::createFrom(donor); group.bottom = newAnchor; break; case Location_OnRight: donor = group.right; newAnchor = Anchor::createFrom(donor); group.left = newAnchor; break; case Location_OnBottom: donor = group.bottom; newAnchor = Anchor::createFrom(donor); group.top = newAnchor; break; default: qWarning() << "MultiSplitterLayout::newAnchor invalid location!"; return nullptr; } Q_ASSERT(newAnchor); Q_ASSERT(donor); Q_ASSERT(donor != newAnchor); updateAnchorsFromTo(donor, newAnchor); qCDebug(::anchors()) << newAnchor->hasNonPlaceholderItems(Anchor::Side1) << newAnchor->hasNonPlaceholderItems(Anchor::Side2) << newAnchor->side1Items() << newAnchor->side2Items() << "; donor" << donor << "; follows=" << newAnchor->followee(); return newAnchor; } void MultiSplitterLayout::blockItemPropagateGeo(bool block) { for (Item *item : m_items) { if (block) item->beginBlockPropagateGeo(); else item->endBlockPropagateGeo(); } } void MultiSplitterLayout::emitVisibleWidgetCountChanged() { if (!m_inDestructor) Q_EMIT visibleWidgetCountChanged(visibleCount()); } Item *MultiSplitterLayout::itemForFrame(const Frame *frame) const { if (!frame) return nullptr; for (Item *item : m_items) { if (item->frame() == frame) return item; } return nullptr; } Frame::List MultiSplitterLayout::framesFrom(QWidgetOrQuick *frameOrMultiSplitter) const { if (auto frame = qobject_cast(frameOrMultiSplitter)) return { frame }; if (auto msw = qobject_cast(frameOrMultiSplitter)) return msw->multiSplitterLayout()->frames(); return {}; } Frame::List MultiSplitterLayout::frames() const { Frame::List result; for (Item *item : m_items) { if (Frame *f = item->frame()) result.push_back(f); } return result; } QVector MultiSplitterLayout::dockWidgets() const { DockWidgetBase::List result; const Frame::List frames = this->frames(); for (Frame *frame : frames) result << frame->dockWidgets(); return result; } QPair MultiSplitterLayout::createTargetAnchorGroup(KDDockWidgets::Location location, Item *relativeToItem) { const bool relativeToThis = relativeToItem == nullptr; AnchorGroup group = relativeToThis ? staticAnchorGroup() : anchorsForPos(relativeToItem->geometry().center()); if (!group.isValid()) { qWarning() << Q_FUNC_INFO << "Invalid anchor group:" << group << "; staticAnchorGroup=" << staticAnchorGroup() << "; relativeTo=" << relativeToItem; dumpDebug(); } Anchor *newAnchor = nullptr; if (relativeToThis) { if (!isEmpty()) newAnchor = this->newAnchor(group, location); } else { newAnchor = group.createAnchorFrom(location, relativeToItem); group.setAnchor(newAnchor, KDDockWidgets::oppositeLocation(location)); } return { group, newAnchor }; } bool MultiSplitterLayout::checkSanity(AnchorSanityOption options) const { if (m_inCtor || LayoutSaver::restoreInProgress()) return true; auto check = [this, options] (Item *item, Qt::Orientation orientation) { int numSide1 = 0; int numSide2 = 0; const auto &anchors = this->anchors(orientation, /*includeStatic=*/ true); for (Anchor *anchor : anchors) { if (anchor->containsItem(item, Anchor::Side1)) numSide1++; if (anchor->containsItem(item, Anchor::Side2)) numSide2++; } if (numSide1 != 1 || numSide2 != 1) { dumpDebug(); qWarning() << "MultiSplitterLayout::checkSanity:" << "Problem detected! while processing" << orientation << "anchors" << "; numSide1=" << numSide1 << "; numSide2=" << numSide2; for (Anchor *anchor : anchors) { if (anchor->containsItem(item, Anchor::Side1)) qDebug() << "Anchor" << anchor << "contains said widget on side1"; if (anchor->containsItem(item, Anchor::Side2)) qDebug() << "Anchor" << anchor << "contains said widget on side2"; } qWarning() << "MultiSplitterLayout::checkSanity:" << numSide1 << numSide2 << item << "\n" << m_topAnchor->items(Anchor::Side2) << "\n" << m_bottomAnchor->items(Anchor::Side1) << "\n" << m_leftAnchor->items(Anchor::Side2) << "\n" << m_rightAnchor->items(Anchor::Side1); return false; } if ((options & AnchorSanity_WidgetInvalidSizes) && !item->isPlaceholder()) { if (item->width() <= 0 || item->height() <= 0) { dumpDebug(); qWarning() << "Invalid size for widget" << item << item->size() << "; isPlaceholder=" << item->isPlaceholder() << "; minSize=" << item->minimumSize(); return false; } } return true; }; if (!m_topAnchor || !m_leftAnchor || !m_rightAnchor || !m_bottomAnchor) { qWarning() << Q_FUNC_INFO << "Invalid static anchors" << m_leftAnchor << m_topAnchor << m_rightAnchor << m_bottomAnchor; return false; } if (m_topAnchor->position() != 0 || m_leftAnchor->position() != 0 || m_rightAnchor->position() != width() - m_rightAnchor->thickness() || m_bottomAnchor->position() != height() - m_bottomAnchor->thickness()) { qWarning() << Q_FUNC_INFO << "Invalid anchor position" << " left=" << m_leftAnchor->position() << " top=" << m_topAnchor->position() << " right=" << m_rightAnchor->position() << " bottom=" << m_bottomAnchor->position() << "; size=" << m_size << "; min=" << m_minSize; return false; } for (Anchor *anchor : qAsConst(m_anchors)) { if (!anchor->isValid()) { dumpDebug(); qWarning() << "invalid anchor" << anchor; return false; } auto checkSides = [this, anchor] (Anchor::Side side) { for (Item *item : anchor->items(side)) { if (!contains(item)) { dumpDebug(); qWarning() << "MultiSplitterLayout::checkSanity: Anchor has" << item << "but multi splitter does not"; return false; } } return true; }; if (!checkSides(Anchor::Side1) || !checkSides(Anchor::Side2)) return false; if (anchor->isFollowing() && !qobject_cast(anchor->followee())) { qWarning() << "Anchor is following but followee was deleted already"; return false; } if (options & AnchorSanity_Followers) { const bool hasItemsOnBothSides = anchor->hasNonPlaceholderItems(Anchor::Side1) && anchor->hasNonPlaceholderItems(Anchor::Side2); if (!anchor->isStatic() && !anchor->isFollowing() && !hasItemsOnBothSides && anchorsFollowing(anchor).isEmpty()) { qWarning() << "Non static anchor should have items on both sides unless it's following or being followed" << anchor; } } if (!anchor->isFollowing() &&anchor->geometry() != anchor->separatorWidget()->geometry()) { qWarning() << Q_FUNC_INFO << anchor << anchor->separatorWidget() << "Inconsistent anchor geometry" << anchor->geometry() << "; " << anchor->separatorWidget()->geometry(); return false; } if (options & AnchorSanity_Visibility) { if (multiSplitter()->isVisible() && !anchor->isFollowing() && !anchor->separatorWidget()->isVisible()) { qWarning() << Q_FUNC_INFO << "Anchor should be visible" << anchor; return false; } } } for (Item *item : qAsConst(m_items)) { if (!check(item, Qt::Vertical)) return false; if (!check(item, Qt::Horizontal)) return false; } // Check that no widget intersects with an anchor if (options & AnchorSanity_Intersections) { for (Item *item: items()) { for (Anchor *a : anchors()) { if (!item->isPlaceholder() && item->geometry().intersects(a->geometry())) { dumpDebug(); qWarning() << "MultiSplitterLayout::checkSanity: Widget" << item << "with rect" << item->geometry() << "Intersects anchor" << a << "with rect" << a->geometry() << "; a.visible|following|valid|unneeded=" << a->separatorWidget()->isVisible()<< a->isFollowing() << a->isValid() << a->isUnneeded(); return false; } } } } if (options & AnchorSanity_WidgetGeometry) { for (Item *item: items()) { if (!item->isPlaceholder() && item->geometry() != item->frame()->geometry()) { qWarning() << Q_FUNC_INFO << "Invalid geometry for item" << item << item->geometry() << item->frame()->geometry(); return false; } if (!item->anchorGroup().isValid()) { qWarning() << Q_FUNC_INFO << "Invalid item group for item" << item->anchorGroup(); return false; } if (!item->isPlaceholder() && item->anchorGroup().itemSize() != item->size()) { qWarning() << Q_FUNC_INFO << "Invaild item size=" << item->size() << "group size=" << item->anchorGroup().itemSize(); return false; } } } if (options & AnchorSanity_WidgetMinSizes) { for (Item *item : items()) { if (item->isPlaceholder()) continue; const int minWidth = item->minLength(Qt::Vertical); const int minHeight = item->minLength(Qt::Horizontal); if (item->width() < minWidth) { qWarning() << "MultiSplitterLayout::checkSanity: Widget has width=" << item->width() << "but minimum is" << minWidth << item; return false; } if (item->height() < minHeight) { qWarning() << "MultiSplitterLayout::checkSanity: Widget has height=" << item->height() << "but minimum is" << minHeight << item; return false; } } } for (DockWidgetBase *dw : DockRegistry::self()->dockwidgets()) { Frame *frame = dw->frame(); auto tabWidgetParent = frame ? frame->tabWidget() : nullptr; const bool shouldBeChecked = dw->isVisible() || tabWidgetParent; if (shouldBeChecked != dw->toggleAction()->isChecked()) { qWarning() << Q_FUNC_INFO << "Invalid state for DockWidgetBase::toggleAction()" << dw->toggleAction()->isChecked(); return false; } } /* TODO: uncomment when all tests pass if (m_topAnchor->position() != 0 || m_leftAnchor->position() != 0) { qWarning() << Q_FUNC_INFO << "Invalid top or left anchor position" << m_topAnchor->position() << m_leftAnchor->position(); return false; } if (m_rightAnchor->position() != m_size.width() - 1 || m_bottomAnchor->position() != m_size.height() - 1) { qWarning() << Q_FUNC_INFO << "Invalid right or bottom anchor position" << m_rightAnchor->position() << m_bottomAnchor->position() << "; m_size=" << m_size; return false; } */ return true; } void MultiSplitterLayout::maybeCheckSanity() { #if defined(DOCKS_DEVELOPER_MODE) if (!isRestoringPlaceholder() && !checkSanity(AnchorSanityOption(AnchorSanity_All & ~AnchorSanity_Visibility))) qWarning() << Q_FUNC_INFO << "Sanity check failed"; #endif } void MultiSplitterLayout::ensureHasAvailableSize(QSize needed) { const QSize availableSize = this->availableSize(); qCDebug(placeholder) << Q_FUNC_INFO << "; needed=" << needed << availableSize; const int deltaWidth = needed.width() > availableSize.width() ? (needed.width() - availableSize.width()) : 0; const int deltaHeight = needed.height() > availableSize.height() ? (needed.height() - availableSize.height()) : 0; const QSize newSize = size() + QSize(deltaWidth, deltaHeight); setSize(newSize); } void MultiSplitterLayout::restorePlaceholder(Item *item) { QScopedValueRollback restoring(m_restoringPlaceholder, true); AnchorGroup anchorGroup = item->anchorGroup(); const QSize availableSize = this->availableSize(); const QSize hardcodedMinSize = MultiSplitterLayout::hardcodedMinimumSize(); const QSize widgetMinSize = { qMax(hardcodedMinSize.width(), KDDockWidgets::widgetMinLength(item->frame(), Qt::Vertical)), qMax(hardcodedMinSize.height(), KDDockWidgets::widgetMinLength(item->frame(), Qt::Horizontal)) }; const QSize newSize = {qMax(qMin(item->length(Qt::Vertical), availableSize.width()), widgetMinSize.width()), qMax(qMin(item->length(Qt::Horizontal), availableSize.height()), widgetMinSize.height()) }; // Our layout has enough size for the dock widget ensureHasAvailableSize(newSize); item->setIsPlaceholder(false); item->beginBlockPropagateGeo(); updateSizeConstraints(); Anchor::List anchorsFollowing = anchorGroup.anchorsFollowingInwards(); if (anchorsFollowing.isEmpty()) { // There's no separator to move, it means it's a static anchor group (layout is empty, so the anchors // are the actual borders of the window // dumpDebug(); Q_ASSERT(anchorGroup.isStaticOrFollowsStatic()); anchorGroup.updateItemSizes(); maybeCheckSanity(); item->endBlockPropagateGeo(); return; } clearAnchorsFollowing(); QHash anchorsThatWillFollowOthers = anchorsShouldFollow(); if (!anchorsFollowing.contains(anchorGroup.top) && !anchorsFollowing.contains(anchorGroup.bottom)) { anchorGroup.top->updateItemSizes(); anchorGroup.bottom->updateItemSizes(); } if (!anchorsFollowing.contains(anchorGroup.left) && !anchorsFollowing.contains(anchorGroup.right)) { anchorGroup.left->updateItemSizes(); anchorGroup.right->updateItemSizes(); } for (Anchor *anchorFollowingInwards : anchorsFollowing) { const Qt::Orientation orientation = anchorFollowingInwards->orientation(); Anchor *side1Anchor = anchorGroup.anchorAtSide(Anchor::Side1, orientation); // returns the left if vertical, otherwise top Anchor *side2Anchor = anchorGroup.anchorAtSide(Anchor::Side2, orientation); // returns the right if vertical, otherwise bottom if (anchorsThatWillFollowOthers.contains(side1Anchor)) { Anchor *followee = anchorsThatWillFollowOthers.value(side1Anchor); side1Anchor->setFollowee(followee); side1Anchor = followee; } if (anchorsThatWillFollowOthers.contains(side2Anchor)) { Anchor *followee = anchorsThatWillFollowOthers.value(side2Anchor); side2Anchor->setFollowee(followee); side2Anchor = followee; } const int oldPosition1 = side1Anchor->position(); const int oldPosition2 = side2Anchor->position(); const int boundPosition1 = side1Anchor->isStatic() ? side1Anchor->position() : boundPositionForAnchor(side1Anchor, Anchor::Side1); const int boundPosition2 = side2Anchor->isStatic() ? side2Anchor->position() : boundPositionForAnchor(side2Anchor, Anchor::Side2); const int newLength = anchorFollowingInwards->isVertical() ? newSize.width() : newSize.height(); // Let's try that each anchor contributes 50%, so that the widget appears centered const int suggestedLength1 = qMin(newLength, qCeil(newLength / 2) + side1Anchor->thickness() + 1); const int maxPos1 = boundPosition2 - newLength - side1Anchor->thickness(); const int newPosition1 = qMax(qMin(maxPos1, oldPosition1 - suggestedLength1), boundPosition1); // Honour the bound const int newPosition2 = newPosition1 + side1Anchor->thickness() + newLength; // No need to check bound2, we have enough space afterall qCDebug(placeholder) << Q_FUNC_INFO << "; oldPos1=" << oldPosition1 << "; oldPos2=" << oldPosition2 << "; newPosition1=" << newPosition1 << "; newPosition2=" << newPosition2 << "; bounds1=" << boundPosition1 << "; bounds2=" << boundPosition2 << "; item.geo=" << item->geometry() << "; newSize=" << newSize << "; side1Anchor=" << side1Anchor << "; side2Anchor=" << side2Anchor << side1Anchor->followee() << side2Anchor->followee() << "; anchorFollowing=" << anchorFollowingInwards << "; size=" << m_size << "; minSize=" << m_minSize << "; widgetMinSize=" << widgetMinSize << "; available_old=" << availableSize << "; available_new=" << availableLengthForOrientation(orientation) << "; item.size=" << item->size(); if (newPosition1 < boundPosition1 || newPosition2 > boundPosition2) { qWarning() << Q_FUNC_INFO << "Out of bounds" // << "bounds.anchor1=" << boundPositionsForAnchor(side1Anchor) << "bounds.anchor2=" << boundPositionsForAnchor(side2Anchor) << "; side1Anchor.thickness" << side1Anchor->thickness() << "; side2Anchor.thickness" << side2Anchor->thickness(); } // We don't want item to resize the anchors while setting newPosition1, we already calculated it if (side1Anchor->isStatic()) { side1Anchor->updateItemSizes(); } else { side1Anchor->setPosition(newPosition1); } if (side2Anchor->isStatic()) { side2Anchor->updateItemSizes(); } else { side2Anchor->setPosition(newPosition2); } } item->endBlockPropagateGeo(); updateAnchorFollowing(); maybeCheckSanity(); } void MultiSplitterLayout::unrefOldPlaceholders(const Frame::List &framesBeingAdded) const { for (Frame *frame : framesBeingAdded) { for (DockWidgetBase *dw : frame->dockWidgets()) { if (Item *existingItem = dw->lastPosition()->layoutItem()) { if (contains(existingItem)) { // We're only interested in placeholders from this layout dw->lastPosition()->removePlaceholders(this); } } } } } void MultiSplitterLayout::setSize(QSize size) { if (size != m_size) { m_resizing = true; QSize oldSize = m_size; if (size.width() < m_minSize.width() || size.height() < m_minSize.height()) { qWarning() << Q_FUNC_INFO << "new size is smaller than min size. Size=" << size << "; min=" << m_minSize; return; } #if defined(DOCKS_DEVELOPER_MODE) if (!m_inCtor && false) { // TODO Uncomment when it passes QSize minSizeCalculated = QSize(availableLengthForOrientation(Qt::Vertical), availableLengthForOrientation(Qt::Horizontal)); if (size.width() < minSizeCalculated.width() || size.height() < minSizeCalculated.height()) { qWarning() << Q_FUNC_INFO << "new size is smaller than min size calculated" << size << minSizeCalculated; } } #endif m_size = size; Q_EMIT sizeChanged(size); redistributeSpace(oldSize, size); m_resizing = false; if (!m_restoringPlaceholder) { // ensureAnchorsBounded() is run at the end of restorePlaceholder() already. ensureAnchorsBounded(); } } } void MultiSplitterLayout::setContentLength(int value, Qt::Orientation o) { if (o == Qt::Vertical) { // Setting the width setSize({value, m_size.height()}); } else { // Setting the height setSize({m_size.width(), value}); } } int MultiSplitterLayout::length(Qt::Orientation o) const { return o == Qt::Vertical ? width() : height(); } void MultiSplitterLayout::setMinimumSize(QSize sz) { if (sz != m_minSize) { m_minSize = sz; setSize(m_size.expandedTo(m_minSize)); // Increase size incase we need to Q_EMIT minimumSizeChanged(sz); } qCDebug(sizing) << Q_FUNC_INFO << "minSize = " << m_minSize; } void MultiSplitterLayout::updateAnchorsFromTo(Anchor *oldAnchor, Anchor *newAnchor) { // Update the from/to of other anchors for (Anchor *other : qAsConst(m_anchors)) { Q_ASSERT(other); Q_ASSERT(other->isValid()); if (!other->isStatic() && other->orientation() != newAnchor->orientation()) { if (other->to() == oldAnchor) { other->setTo(newAnchor); } else if (other->from() == oldAnchor) { other->setFrom(newAnchor); } if (!other->isValid()) { qDebug() << "MultiSplitterLayout::updateAnchorsFromTo: anchor is now invalid." << "\n old=" << oldAnchor << "\n new=" << newAnchor << "\n from=" << other->from() << "\n to=" << other->to() << "\n other=" << other; } } } } void MultiSplitterLayout::clearAnchorsFollowing() { for (Anchor *anchor : qAsConst(m_anchors)) anchor->setFollowee(nullptr); } void MultiSplitterLayout::updateAnchorFollowing(const AnchorGroup &groupBeingRemoved) { clearAnchorsFollowing(); QHash newPositionsWhenGroupRemoved; for (Anchor *anchor : qAsConst(m_anchors)) { if (anchor->isStatic()) continue; if (anchor->onlyHasPlaceholderItems(Anchor::Side2)) { Anchor *toFollow = anchor->findNearestAnchorWithItems(Anchor::Side2); if (toFollow->followee() != anchor) { if (!toFollow->isStatic() && groupBeingRemoved.containsAnchor(anchor, Anchor::Side1)) { // A group is being removed, instead of simply shifting the left/top anchor all the way, let's make it use half the space if (toFollow->onlyHasPlaceholderItems(Anchor::Side1)) { // Means it can move! const int delta = toFollow->position() - anchor->position() - anchor->thickness(); const int halfDelta = int(delta / 2.0); if (halfDelta > 0) { newPositionsWhenGroupRemoved.insert(toFollow, toFollow->position() - halfDelta); } } } anchor->setFollowee(toFollow); } } else if (anchor->onlyHasPlaceholderItems(Anchor::Side1)) { Anchor *toFollow = anchor->findNearestAnchorWithItems(Anchor::Side1); if (toFollow->followee() != anchor) { if (!toFollow->isStatic() && groupBeingRemoved.containsAnchor(anchor, Anchor::Side2)) { // A group is being removed, instead of simply shifting the right/bottom anchor all the way, let's make it use half the space if (toFollow->onlyHasPlaceholderItems(Anchor::Side2)) { // Means it can move! const int delta = anchor->position() - toFollow->position() - toFollow->thickness(); const int halfDelta = int(delta / 2.0); if (halfDelta > 0) { newPositionsWhenGroupRemoved.insert(toFollow, toFollow->position() + halfDelta); } } } anchor->setFollowee(toFollow); } } } for (auto it = newPositionsWhenGroupRemoved.begin(), end = newPositionsWhenGroupRemoved.end(); it != end; ++it) { Anchor *anchorToShift = it.key(); const int newPosition = it.value(); const Anchor::Side sideToShiftTo = newPosition < anchorToShift->position() ? Anchor::Side1 : Anchor::Side2; bool doShift = true; for (Anchor *follower : anchorToShift->followers()) { if (follower->hasNonPlaceholderItems(sideToShiftTo) && !groupBeingRemoved.containsAnchor(follower, sideToShiftTo)) { doShift = false; break; } } if (doShift && !anchorToShift->isFollowing()) anchorToShift->setPosition(newPosition); } updateSizeConstraints(); ensureAnchorsBounded(); } QHash MultiSplitterLayout::anchorsShouldFollow() const { QHash followers; for (Anchor *anchor : m_anchors) { if (anchor->isStatic()) continue; if (anchor->onlyHasPlaceholderItems(Anchor::Side2)) { Anchor *toFollow = anchor->findNearestAnchorWithItems(Anchor::Side2); if (followers.value(toFollow) != anchor) followers.insert(anchor, toFollow); } else if (anchor->onlyHasPlaceholderItems(Anchor::Side1)) { Anchor *toFollow = anchor->findNearestAnchorWithItems(Anchor::Side1); if (followers.value(toFollow) != anchor) followers.insert(anchor, toFollow); } } return followers; } void MultiSplitterLayout::insertAnchor(Anchor *anchor) { m_anchors.append(anchor); } const ItemList MultiSplitterLayout::items() const { return m_items; } bool MultiSplitterLayout::eventFilter(QObject *o, QEvent *e) { if (m_inDestructor || e->spontaneous() || !m_multiSplitter) return false; if (!m_multiSplitter->isVisible()) { // The whole MultiSplitter isn't visible, don't bother. It probably even is being hidden by ~QMainWindow(). return false; } QWidget *w = qobject_cast(o); if (!w || !w->testAttribute(Qt::WA_WState_ExplicitShowHide)) { // We only care about explicit show/hide by the developer return false; } return false; } bool MultiSplitterLayout::deserialize(const LayoutSaver::MultiSplitterLayout &msl) { clear(true); ItemList items; items.reserve(msl.items.size()); for (const auto &i : qAsConst(msl.items)) { Item *item = Item::deserialize(i, this); items.push_back(item); } m_items = items; // Set the items, so Anchor::deserialize() can set the side1 and side2 items for (const auto &a : qAsConst(msl.anchors)) { Anchor *anchor = Anchor::deserialize(a, this); // They auto-register into m_anchors if (!anchor) return false; if (anchor->type() == Anchor::Type_LeftStatic) { Q_ASSERT(!m_leftAnchor); m_leftAnchor = anchor; } else if (anchor->type() == Anchor::Type_TopStatic) { Q_ASSERT(!m_topAnchor); m_topAnchor = anchor; } else if (anchor->type() == Anchor::Type_RightStatic) { Q_ASSERT(!m_rightAnchor); m_rightAnchor = anchor; } else if (anchor->type() == Anchor::Type_BottomStatic) { Q_ASSERT(!m_bottomAnchor); m_bottomAnchor = anchor; } } m_staticAnchorGroup.left = m_leftAnchor; m_staticAnchorGroup.top = m_topAnchor; m_staticAnchorGroup.right = m_rightAnchor; m_staticAnchorGroup.bottom = m_bottomAnchor; m_items.clear(); // Now properly set the items, which installs needed event filters, etc. addItems_internal(items, false, false); // Add the items only after we have the static anchors set for (Anchor *anchor : qAsConst(m_anchors)) { int indexFrom = anchor->property("indexFrom").toInt(); int indexTo = anchor->property("indexTo").toInt(); int indexFolowee = anchor->property("indexFolowee").toInt(); anchor->setProperty("indexFrom", QVariant()); anchor->setProperty("indexTo", QVariant()); anchor->setProperty("indexFolowee", QVariant()); anchor->setFrom(m_anchors.at(indexFrom)); anchor->setTo(m_anchors.at(indexTo)); if (indexFolowee != -1) anchor->setFollowee(m_anchors.at(indexFolowee)); } m_size = msl.size; m_minSize = msl.minSize; // Now that the anchors were created we can add them to the items for (Item *item : qAsConst(m_items)) { const int leftIndex = item->property("leftIndex").toInt(); const int topIndex = item->property("topIndex").toInt(); const int rightIndex = item->property("rightIndex").toInt(); const int bottomIndex = item->property("bottomIndex").toInt(); AnchorGroup &group = item->anchorGroup(); group.left = m_anchors.at(leftIndex); group.top = m_anchors.at(topIndex); group.right = m_anchors.at(rightIndex); group.bottom = m_anchors.at(bottomIndex); // Clear helper properties item->setProperty("leftIndex", QVariant()); item->setProperty("topIndex", QVariant()); item->setProperty("rightIndex", QVariant()); item->setProperty("bottomIndex", QVariant()); } if (!m_items.isEmpty()) Q_EMIT widgetCountChanged(m_items.size()); // The main window that we're restoring can have more stuff now (other-toolbars etc), so by // having restored its geometry it can mean our dockwidget layout is now different, so update // its content size if needed Q_EMIT minimumSizeChanged(m_minSize); if (m_size != multiSplitter()->size()) { setSize(multiSplitter()->size()); } return true; } LayoutSaver::MultiSplitterLayout MultiSplitterLayout::serialize() const { LayoutSaver::MultiSplitterLayout l; l.size = size(); l.minSize = minimumSize(); for (Item *item : m_items) l.items.push_back(item->serialize()); for (Anchor *anchor : m_anchors) l.anchors.push_back(anchor->serialize()); return l; }