/* This file is part of KDDockWidgets. SPDX-FileCopyrightText: 2020 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. */ /** * @file * @brief A widget that supports an arbitrary number of splitters (called Separators) in any * combination of vertical/horizontal. * * @author Sérgio Martins \ */ #include "MultiSplitter_p.h" #include "MainWindowBase.h" #include "FloatingWindow_p.h" #include "LayoutSaver.h" #include "Logging_p.h" #include "Frame_p.h" #include "DockWidgetBase.h" #include "Position_p.h" #include "DockRegistry_p.h" #include "Config.h" #include "FrameworkWidgetFactory.h" #include "multisplitter/Widget_qwidget.h" #include "DropArea_p.h" #include "WindowBeingDragged_p.h" #include using namespace KDDockWidgets; MultiSplitter::MultiSplitter(QWidgetOrQuick *parent) : LayoutGuestWidget(parent) { Q_ASSERT(parent); setRootItem(new Layouting::ItemContainer(this)); DockRegistry::self()->registerLayout(this); setLayoutSize(parent->size()); qCDebug(multisplittercreation()) << "MultiSplitter"; // Initialize min size updateSizeConstraints(); setMinimumSize(minimumSize()); } MultiSplitter::~MultiSplitter() { qCDebug(multisplittercreation) << "~MultiSplitter" << this; if (m_rootItem->hostWidget()->asQObject() == this) delete m_rootItem; DockRegistry::self()->unregisterLayout(this); } void MultiSplitter::onLayoutRequest() { updateSizeConstraints(); } bool MultiSplitter::onResize(QSize newSize) { qCDebug(sizing) << Q_FUNC_INFO << "; new=" << newSize << "; window=" << window(); QScopedValueRollback(m_inResizeEvent, true); // to avoid re-entrancy if (!LayoutSaver::restoreInProgress()) { // don't resize anything while we're restoring the layout setLayoutSize(newSize); } return false; // So QWidget::resizeEvent is called } bool MultiSplitter::isInMainWindow() const { return mainWindow() != nullptr; } MainWindowBase *MultiSplitter::mainWindow() const { if (auto pw = QWidgetAdapter::parentWidget()) { // Note that if pw is a FloatingWindow then pw->parentWidget() can be a MainWindow too, as it's parented if (pw->objectName() == QLatin1String("MyCentralWidget")) return qobject_cast(pw->parentWidget()); if (auto mw = qobject_cast(pw)) return mw; } return nullptr; } FloatingWindow *MultiSplitter::floatingWindow() const { return qobject_cast(QWidgetAdapter::parentWidget()); } bool MultiSplitter::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; } Layouting::Item *item = itemForFrame(qobject_cast(widget)); if (contains(item)) { qWarning() << "MultiSplitter::addWidget: Already contains" << widget; return false; } if (location == Location_None) { qWarning() << "MultiSplitter::addWidget: not adding to location None"; return false; } const bool relativeToThis = relativeToFrame == nullptr; Layouting::Item *relativeToItem = itemForFrame(relativeToFrame); if (!relativeToThis && !contains(relativeToItem)) { qWarning() << "MultiSplitter::addWidget: Doesn't contain relativeTo:" << "; relativeToFrame=" << relativeToFrame << "; relativeToItem=" << relativeToItem << "; options=" << option; return false; } return true; } void MultiSplitter::addWidget(QWidgetOrQuick *w, Location location, Frame *relativeToWidget, DefaultSizeMode defaultSizeMode, AddingOption option) { auto frame = qobject_cast(w); qCDebug(addwidget) << Q_FUNC_INFO << w << "; location=" << locationStr(location) << "; relativeTo=" << relativeToWidget << "; size=" << size() << "; w.size=" << w->size() << "; 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->QWidgetAdapter::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; Layouting::Item *relativeTo = itemForFrame(relativeToWidget); if (!relativeTo) relativeTo = m_rootItem; Layouting::Item *newItem = nullptr; Frame::List frames = framesFrom(w); unrefOldPlaceholders(frames); auto dw = qobject_cast(w); if (frame) { newItem = new Layouting::Item(this); newItem->setGuestWidget(frame); } else if (dw) { newItem = new Layouting::Item(this); frame = Config::self().frameworkWidgetFactory()->createFrame(); newItem->setGuestWidget(frame); frame->addWidget(dw, option); } else if (auto ms = qobject_cast(w)) { newItem = ms->rootItem(); newItem->setHostWidget(this); if (FloatingWindow *fw = ms->floatingWindow()) { newItem->setSize_recursive(fw->size()); } delete ms; } else { // This doesn't happen but let's make coverity happy. // Tests will fail if this is ever printed. qWarning() << Q_FUNC_INFO << "Unknown widget added" << w; return; } Q_ASSERT(!newItem->geometry().isEmpty()); relativeTo->insertItem(newItem, Layouting::Item::Location(location), Layouting::Item::DefaultSizeMode(defaultSizeMode), Layouting::Item::AddingOption(option)); if (dw && option && AddingOption_StartHidden) delete frame; } void MultiSplitter::addMultiSplitter(MultiSplitter *sourceMultiSplitter, Location location, Frame *relativeTo, DefaultSizeMode defaultSizeMode) { qCDebug(addwidget) << Q_FUNC_INFO << sourceMultiSplitter << location << relativeTo; addWidget(sourceMultiSplitter, location, relativeTo, defaultSizeMode); } void MultiSplitter::removeItem(Layouting::Item *item) { if (!item) { qWarning() << Q_FUNC_INFO << "nullptr item"; return; } item->parentContainer()->removeItem(item); } bool MultiSplitter::contains(const Layouting::Item *item) const { return m_rootItem->contains_recursive(item); } bool MultiSplitter::contains(const Frame *frame) const { return itemForFrame(frame) != nullptr; } int MultiSplitter::count() const { return m_rootItem->count_recursive(); } int MultiSplitter::visibleCount() const { return m_rootItem->visibleCount_recursive(); } int MultiSplitter::placeholderCount() const { return count() - visibleCount(); } Layouting::Separator::List MultiSplitter::separators() const { return m_rootItem->separators_recursive(); } void MultiSplitter::updateSizeConstraints() { const QSize newMinSize = m_rootItem->minSize(); qCDebug(sizing) << Q_FUNC_INFO << "Updating size constraints from" << minimumSize() << "to" << newMinSize; setLayoutMinimumSize(newMinSize); } int MultiSplitter::availableLengthForOrientation(Qt::Orientation orientation) const { if (orientation == Qt::Vertical) return availableSize().height(); else return availableSize().width(); } QSize MultiSplitter::availableSize() const { return m_rootItem->availableSize(); } Layouting::Item *MultiSplitter::itemForFrame(const Frame *frame) const { if (!frame) return nullptr; return m_rootItem->itemForWidget(frame); } DockWidgetBase::List MultiSplitter::dockWidgets() const { DockWidgetBase::List dockWidgets; const Frame::List frames = this->frames(); for (Frame *frame : frames) dockWidgets << frame->dockWidgets(); return dockWidgets; } Frame::List MultiSplitter::framesFrom(QWidgetOrQuick *frameOrMultiSplitter) const { if (auto frame = qobject_cast(frameOrMultiSplitter)) return { frame }; if (auto msw = qobject_cast(frameOrMultiSplitter)) return msw->frames(); return {}; } Frame::List MultiSplitter::frames() const { const Layouting::Item::List items = m_rootItem->items_recursive(); Frame::List result; result.reserve(items.size()); for (Layouting::Item *item : items) { if (auto f = static_cast(item->guestAsQObject())) result.push_back(f); } return result; } void MultiSplitter::restorePlaceholder(DockWidgetBase *dw, Layouting::Item *item, int tabIndex) { if (item->isPlaceholder()) { Frame *newFrame = Config::self().frameworkWidgetFactory()->createFrame(this); item->restore(newFrame); } auto frame = qobject_cast(item->guestAsQObject()); Q_ASSERT(frame); if (tabIndex != -1 && frame->dockWidgetCount() >= tabIndex) { frame->insertWidget(dw, tabIndex); } else { frame->addWidget(dw); } frame->QWidgetAdapter::setVisible(true); } void MultiSplitter::layoutEqually() { layoutEqually(m_rootItem); } void MultiSplitter::layoutEqually(Layouting::ItemContainer *container) { if (container) { container->layoutEqually_recursive(); } else { qWarning() << Q_FUNC_INFO << "null container"; } } bool MultiSplitter::checkSanity() const { return m_rootItem->checkSanity(); } void MultiSplitter::unrefOldPlaceholders(const Frame::List &framesBeingAdded) const { for (Frame *frame : framesBeingAdded) { for (DockWidgetBase *dw : frame->dockWidgets()) { dw->lastPositions().removePlaceholders(this); } } } void MultiSplitter::dumpLayout() const { m_rootItem->dumpLayout(); } void MultiSplitter::setLayoutSize(QSize size) { if (size != this->size()) { m_rootItem->setSize_recursive(size); if (!m_inResizeEvent && !LayoutSaver::restoreInProgress()) resize(size); } } QSize MultiSplitter::layoutMinimumSize() const { return m_rootItem->minSize(); } QSize MultiSplitter::size() const { return m_rootItem->size(); } void MultiSplitter::setLayoutMinimumSize(QSize sz) { if (sz != m_rootItem->minSize()) { setLayoutSize(size().expandedTo(m_rootItem->minSize())); // Increase size in case we need to m_rootItem->setMinSize(sz); } qCDebug(sizing) << Q_FUNC_INFO << "minSize = " << m_rootItem->minSize(); } void MultiSplitter::setRootItem(Layouting::ItemContainer *root) { delete m_rootItem; m_rootItem = root; connect(m_rootItem, &Layouting::ItemContainer::numVisibleItemsChanged, this, &MultiSplitter::visibleWidgetCountChanged); connect(m_rootItem, &Layouting::ItemContainer::minSizeChanged, this, [this] { setMinimumSize(layoutMinimumSize()); }); } const Layouting::Item::List MultiSplitter::items() const { return m_rootItem->items_recursive(); } Layouting::ItemContainer *MultiSplitter::rootItem() const { return m_rootItem; } QRect MultiSplitter::rectForDrop(const WindowBeingDragged *wbd, Location location, const Layouting::Item *relativeTo) const { Layouting::Item item(nullptr); if (!wbd) return {}; item.setSize(wbd->size().boundedTo(wbd->maxSize())); item.setMinSize(wbd->minSize()); item.setMaxSizeHint(wbd->maxSize()); Layouting::ItemContainer *container = relativeTo ? relativeTo->parentContainer() : m_rootItem; return container->suggestedDropRect(&item, relativeTo, Layouting::Item::Location(location)); } bool MultiSplitter::deserialize(const LayoutSaver::MultiSplitter &l) { setRootItem(new Layouting::ItemContainer(this)); QHash frames; for (const LayoutSaver::Frame &frame : qAsConst(l.frames)) { Frame *f = Frame::deserialize(frame); Q_ASSERT(!frame.id.isEmpty()); frames.insert(frame.id, f); } m_rootItem->fillFromVariantMap(l.layout, frames); updateSizeConstraints(); m_rootItem->setSize_recursive(QWidgetAdapter::size()); return true; } LayoutSaver::MultiSplitter MultiSplitter::serialize() const { LayoutSaver::MultiSplitter l; l.layout = m_rootItem->toVariantMap(); const Layouting::Item::List items = m_rootItem->items_recursive(); l.frames.reserve(items.size()); for (Layouting::Item *item : items) { if (!item->isContainer()) { if (auto frame = qobject_cast(item->guestAsQObject())) l.frames.insert(frame->id(), frame->serialize()); } } return l; }