/* 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. */ #include "Layout.h" #include "Layout_p.h" #include "LayoutSaver_p.h" #include "Position_p.h" #include "Config.h" #include "Platform.h" #include "ViewFactory.h" #include "Utils_p.h" #include "View_p.h" #include "Logging_p.h" #include "ScopedValueRollback_p.h" #include "DropArea.h" #include "DockWidget_p.h" #include "Group.h" #include "FloatingWindow.h" #include "MainWindow.h" #include "layouting/Item_p.h" #include using namespace KDDockWidgets; using namespace KDDockWidgets::Core; Layout::Layout(ViewType type, View *view) : Controller(type, view) , d(new Private(this)) { assert(view); view->d->layoutInvalidated.connect([this] { updateSizeConstraints(); }); view->d->resized.connect(&Layout::onResize, this); } Layout::~Layout() { d->m_minSizeChangedHandler.disconnect(); if (d->m_rootItem && !d->m_viewDeleted) viewAboutToBeDeleted(); delete d; } void Layout::viewAboutToBeDeleted() { if (view()) { if (d == d->m_rootItem->host()) { delete d->m_rootItem; d->m_rootItem = nullptr; } d->m_viewDeleted = true; } } bool Layout::isInMainWindow(bool honourNesting) const { return mainWindow(honourNesting) != nullptr; } Core::MainWindow *Layout::mainWindow(bool honourNesting) const { if (honourNesting) { // This layout might be a MDIArea, nested in DropArea, which is main window. if (Controller *c = view()->d->firstParentOfType(ViewType::MainWindow)) return static_cast(c); return nullptr; } else { if (auto pw = view()->parentView()) { // Note that if pw is a FloatingWindow then pw->parentWidget() can be a MainWindow too, // as it's parented if (pw->viewName() == QLatin1String("MyCentralWidget")) return pw->parentView()->asMainWindowController(); if (auto mw = pw->asMainWindowController()) return mw; } } return nullptr; } Core::FloatingWindow *Layout::floatingWindow() const { auto parent = view()->rootView(); return parent ? parent->asFloatingWindowController() : nullptr; } void Layout::setRootItem(Core::ItemContainer *root) { delete d->m_rootItem; d->m_rootItem = root; d->m_rootItem->numVisibleItemsChanged.connect( [this](int count) { d->visibleWidgetCountChanged.emit(count); }); d->m_minSizeChangedHandler = d->m_rootItem->minSizeChanged.connect([this] { view()->setMinimumSize(layoutMinimumSize()); }); } Size Layout::layoutMinimumSize() const { return d->m_rootItem->minSize(); } Size Layout::layoutMaximumSizeHint() const { return d->m_rootItem->maxSizeHint(); } void Layout::setLayoutMinimumSize(Size sz) { if (sz != d->m_rootItem->minSize()) { setLayoutSize(layoutSize().expandedTo(d->m_rootItem->minSize())); // Increase size in case we // need to d->m_rootItem->setMinSize(sz); } } Size Layout::layoutSize() const { return d->m_rootItem->size(); } void Layout::clearLayout() { d->m_rootItem->clear(); } bool Layout::checkSanity() const { return d->m_rootItem->checkSanity(); } void Layout::dumpLayout() const { d->m_rootItem->dumpLayout(); } void Layout::restorePlaceholder(Core::DockWidget *dw, Core::Item *item, int tabIndex) { if (item->isPlaceholder()) { auto newGroup = new Core::Group(view()); item->restore(newGroup->asLayoutingGuest()); } auto group = Group::fromItem(item); assert(group); if (group->inDtor() || group->beingDeletedLater()) { // Known bug. Let's print diagnostics early, as this is usually difficult to debug // further up the stack. Will also fatal the tests. KDDW_ERROR("Layout::restorePlaceholder: Trying to use a group that's being deleted"); } if (auto tabIndexFunc = Config::self().dockWidgetTabIndexOverrideFunc()) { // The user wishes to control the tabIndex himself group->insertWidget(dw, tabIndexFunc(dw, group, tabIndex)); } else { if (tabIndex != -1 && group->dockWidgetCount() >= tabIndex) { group->insertWidget(dw, tabIndex); } else { group->addTab(dw); } } group->Controller::setVisible(true); } void Layout::unrefOldPlaceholders(const Core::Group::List &groupsBeingAdded) const { for (Core::Group *group : groupsBeingAdded) { const auto dws = group->dockWidgets(); for (Core::DockWidget *dw : dws) { dw->d->lastPosition()->removePlaceholders(d); } } } void Layout::setLayoutSize(Size size) { if (size != layoutSize()) { const bool restoreInProgress = LayoutSaver::restoreInProgress(); ChildrenResizeStrategy strategy = ChildrenResizeStrategy::Percentage; const auto *main = mainWindow(); if (!restoreInProgress && main && main->options() & MainWindowOption_CentralWidgetGetsAllExtraSpace) strategy = ChildrenResizeStrategy::GiveDropAreaWithCentralFrameAllExtra; d->m_rootItem->setSize_recursive(size, strategy); if (!d->m_inResizeEvent && !restoreInProgress) view()->resize(size); } } Core::Item::List Layout::items() const { return d->m_rootItem->items_recursive(); } bool Layout::containsItem(const Core::Item *item) const { return d->m_rootItem->contains_recursive(item); } bool Layout::containsGroup(const Core::Group *group) const { return itemForGroup(group) != nullptr; } int Layout::count() const { return d->m_rootItem->count_recursive(); } int Layout::visibleCount() const { return d->m_rootItem->visibleCount_recursive(); } int Layout::placeholderCount() const { return count() - visibleCount(); } Core::Item *Layout::itemForGroup(const Core::Group *group) const { if (!group) return nullptr; return d->m_rootItem->itemForView(group->asLayoutingGuest()); } Core::DockWidget::List Layout::dockWidgets() const { Core::DockWidget::List dockWidgets; const Core::Group::List groups = this->groups(); for (Core::Group *group : groups) dockWidgets.append(group->dockWidgets()); return dockWidgets; } Core::Group::List Layout::groupsFrom(View *groupOrMultiSplitter) const { if (auto group = groupOrMultiSplitter->asGroupController()) return { group }; if (auto msw = groupOrMultiSplitter->asDropAreaController()) return msw->groups(); return {}; } Core::Group::List Layout::groups() const { const Core::Item::List items = d->m_rootItem->items_recursive(); Core::Group::List result; result.reserve(items.size()); for (Core::Item *item : items) { if (auto group = Group::fromItem(item)) { result.push_back(group); } } return result; } void Layout::removeItem(Core::Item *item) { if (!item) { KDDW_ERROR("nullptr item"); return; } item->parentContainer()->removeItem(item); } void Layout::updateSizeConstraints() { const Size newMinSize = d->m_rootItem->minSize(); setLayoutMinimumSize(newMinSize); } bool Layout::deserialize(const LayoutSaver::MultiSplitter &l) { std::unordered_map groups; for (const auto &it : l.groups) { const LayoutSaver::Group &group = it.second; Core::Group *f = Core::Group::deserialize(group); if (!f) return false; assert(!group.id.isEmpty()); groups[group.id] = f->asLayoutingGuest(); } d->m_rootItem->fillFromJson(l.layout, groups); updateSizeConstraints(); const Size newLayoutSize = view()->size().expandedTo(d->m_rootItem->minSize()); d->m_rootItem->setSize_recursive(newLayoutSize); return true; } bool Layout::onResize(Size newSize) { ScopedValueRollback resizeGuard(d->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 } LayoutSaver::MultiSplitter Layout::serialize() const { LayoutSaver::MultiSplitter l; d->m_rootItem->to_json(l.layout); const Core::Item::List items = d->m_rootItem->items_recursive(); l.groups.reserve(size_t(items.size())); for (Core::Item *item : items) { if (!item->isContainer()) { if (auto group = Group::fromItem(item)) { l.groups[group->view()->d->id()] = group->serialize(); } } } return l; } Core::DropArea *Layout::asDropArea() const { return view()->asDropAreaController(); } MDILayout *Layout::asMDILayout() const { if (auto v = view()) return v->asMDILayoutController(); return nullptr; } Core::ItemContainer *Layout::rootItem() const { return d->m_rootItem; } void Layout::onCloseEvent(CloseEvent *e) { e->accept(); // Accepted by default (will close unless ignored) const Core::Group::List groups = this->groups(); for (Core::Group *group : groups) { group->view()->d->requestClose(e); if (!e->isAccepted()) break; // Stop when the first group prevents closing } } LayoutingHost *Layout::asLayoutingHost() const { return d; } Layout::Private *Layout::d_ptr() { return d; } bool Layout::Private::supportsHonouringLayoutMinSize() const { if (auto window = q->view()->window()) { return window->supportsHonouringLayoutMinSize(); } else { // Corner case. No reason not to honour min-size return true; } } Layout::Private::Private(Layout *qq) : q(qq) { } Layout::Private::~Private() = default; /** static */ Layout *Layout::fromLayoutingHost(LayoutingHost *host) { if (auto layoutPriv = dynamic_cast(host)) return layoutPriv->q; return nullptr; }