diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ec27bd3e..82ba47296 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -592,9 +592,11 @@ if(KDDockWidgets_EXAMPLES) add_subdirectory(examples/minimal) add_subdirectory(examples/mdi) add_subdirectory(examples/mdi_with_docking) + add_subdirectory(examples/misc) set_compiler_flags(qtwidgets_dockwidgets) set_compiler_flags(qtwidgets_minimal) set_compiler_flags(qtwidgets_mdi_with_docking) + set_compiler_flags(qtwidgets_documents) # Standalone layouting example add_subdirectory(src/core/layouting/examples/qtwidgets/) diff --git a/code.code-workspace b/code.code-workspace index c2c62f365..b62658439 100644 --- a/code.code-workspace +++ b/code.code-workspace @@ -65,7 +65,9 @@ "type": "lldb", "request": "launch", "program": "${command:cmake.launchTargetPath}", - "args": [], + "args": [ + "tst_affinityWithPersistentCentralGroup" + ], "cwd": "${command:cmake.buildDirectory}", "env": {} }, @@ -74,7 +76,9 @@ "type": "cppdbg", "request": "launch", "program": "${command:cmake.launchTargetPath}", - "args": [], + "args": [ + "tst_affinityWithPersistentCentralGroup" + ], "cwd": "${command:cmake.buildDirectory}", "MIMode": "gdb", "setupCommands": [ @@ -104,7 +108,9 @@ "type": "cppvsdbg", "request": "launch", "program": "${command:cmake.launchTargetPath}", - "args": [], + "args": [ + "tst_affinityWithPersistentCentralGroup" + ], "cwd": "${command:cmake.buildDirectory}", "sourceFileMap": { "C:/work/build/qt5_workdir/w/s": "${QTDIR}/../Src", diff --git a/examples/misc/CMakeLists.txt b/examples/misc/CMakeLists.txt new file mode 100644 index 000000000..14f6356a3 --- /dev/null +++ b/examples/misc/CMakeLists.txt @@ -0,0 +1,11 @@ +# This file is part of KDDockWidgets. +# +# SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company +# Author: Sergio Martins +# +# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +# +# Contact KDAB at for commercial licensing options. +# + +add_subdirectory(documents) diff --git a/examples/misc/documents/CMakeLists.txt b/examples/misc/documents/CMakeLists.txt new file mode 100644 index 000000000..079e40131 --- /dev/null +++ b/examples/misc/documents/CMakeLists.txt @@ -0,0 +1,35 @@ +# This file is part of KDDockWidgets. +# +# SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company +# Author: Sergio Martins +# +# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only +# +# Contact KDAB at for commercial licensing options. +# + +cmake_minimum_required(VERSION 3.15) +project(qtwidgets_documents) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_INCLUDE_CURRENT_DIRS ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if(NOT TARGET kddockwidgets) + # For the purpose of our example, we're looking for Qt5 or Qt6 KDDW. + # For your own purposes, just chose the one you need. + find_package(KDDockWidgets QUIET) + if(NOT KDDockWidgets_FOUND) + find_package(KDDockWidgets-qt6 REQUIRED) + endif() +endif() + +set(RESOURCES_EXAMPLE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/../../dockwidgets/resources_example.qrc) + +add_executable(qtwidgets_documents main.cpp ../../dockwidgets/MyWidget.cpp ${RESOURCES_EXAMPLE_SRC}) + +target_include_directories(qtwidgets_documents PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../dockwidgets) + +target_link_libraries(qtwidgets_documents PRIVATE KDAB::kddockwidgets) diff --git a/examples/misc/documents/main.cpp b/examples/misc/documents/main.cpp new file mode 100644 index 000000000..213ae8d4d --- /dev/null +++ b/examples/misc/documents/main.cpp @@ -0,0 +1,123 @@ +/* + This file is part of KDDockWidgets. + + SPDX-FileCopyrightText: 2026 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 "MyWidget.h" + +#include +#include +#include +#include +#include + +#include +#include +#include + +// clazy:excludeall=qstring-allocations + +using namespace KDDockWidgets; + +int main(int argc, char **argv) +{ +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif + QApplication app(argc, argv); + QCoreApplication::setOrganizationName(QStringLiteral("KDAB")); + QCoreApplication::setApplicationName(QStringLiteral("Documents Example")); + + KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets); + + qApp->setStyle(QStyleFactory::create(QStringLiteral("Fusion"))); + + // 1. Create a main window with a persistent central group + KDDockWidgets::QtWidgets::MainWindow mainWindow(QStringLiteral("MyMainWindow"), + MainWindowOption_HasCentralGroup); + mainWindow.setWindowTitle("Documents Example"); + mainWindow.resize(1200, 800); + mainWindow.show(); + + // 2. Set up affinities: the main window supports both scopes + mainWindow.setAffinities({ "global", "projects" }); + mainWindow.mainWindow()->setDocumentAffinity("projects"); + + // 3. Create project dock widgets (tabbed in the central group) + auto projectDock1 = new KDDockWidgets::QtWidgets::DockWidget(QStringLiteral("project1")); + projectDock1->setWidget(new MyWidget1()); + projectDock1->setAffinities({ "projects" }); + mainWindow.addDockWidgetAsTab(projectDock1); + + auto projectDock2 = new KDDockWidgets::QtWidgets::DockWidget(QStringLiteral("project2")); + projectDock2->setWidget(new MyWidget2()); + projectDock2->setAffinities({ "projects" }); + mainWindow.addDockWidgetAsTab(projectDock2); + + // 4. Create global dock widgets (docked to the sides) + auto dockedGlobal = new KDDockWidgets::QtWidgets::DockWidget(QStringLiteral("dockedGlobal")); + dockedGlobal->setWidget(new MyWidget3()); + dockedGlobal->setAffinities({ "global" }); + mainWindow.mainWindow()->addDockWidgetToSide(dockedGlobal->asDockWidgetController(), KDDockWidgets::Location_OnLeft); + + auto dockedGlobal2 = new KDDockWidgets::QtWidgets::DockWidget(QStringLiteral("dockedGlobal2")); + dockedGlobal2->setWidget(new MyWidget1()); + dockedGlobal2->setAffinities({ "global" }); + mainWindow.mainWindow()->addDockWidgetToSide(dockedGlobal2->asDockWidgetController(), KDDockWidgets::Location_OnRight); + + // 5. Create floating dock widgets + auto floatingGlobal = new KDDockWidgets::QtWidgets::DockWidget(QStringLiteral("floatingGlobal")); + floatingGlobal->setWidget(new MyWidget2()); + floatingGlobal->setAffinities({ "global" }); + floatingGlobal->open(); + + auto floatingProjects = new KDDockWidgets::QtWidgets::DockWidget(QStringLiteral("floatingProjects")); + floatingProjects->setWidget(new MyWidget3()); + floatingProjects->setAffinities({ "projects" }); + floatingProjects->open(); + + // 6. Create menu bar with save/load actions for each affinity scope + QMenuBar *menuBar = mainWindow.menuBar(); + QMenu *layoutMenu = menuBar->addMenu(QStringLiteral("Layout")); + + layoutMenu->addAction(QStringLiteral("Save Project"), [&] { + KDDockWidgets::LayoutSaver saver; + saver.setAffinityNames({ "projects" }); + saver.saveToFile(QStringLiteral("projects_layout.json")); + }); + + layoutMenu->addAction(QStringLiteral("Load Project"), [&] { + KDDockWidgets::LayoutSaver saver; + saver.setAffinityNames({ "projects" }); + saver.restoreFromFile(QStringLiteral("projects_layout.json")); + }); + + layoutMenu->addSeparator(); + + layoutMenu->addAction(QStringLiteral("Save Global"), [&] { + KDDockWidgets::LayoutSaver saver; + saver.setAffinityNames({ "global" }); + saver.saveToFile(QStringLiteral("global_layout.json")); + }); + + layoutMenu->addAction(QStringLiteral("Load Global"), [&] { + KDDockWidgets::LayoutSaver saver; + saver.setAffinityNames({ "global" }); + saver.restoreFromFile(QStringLiteral("global_layout.json")); + }); + + layoutMenu->addSeparator(); + + layoutMenu->addAction(QStringLiteral("Close all"), [&] { + KDDockWidgets::DockRegistry::self()->clear(); + }); + + return app.exec(); +} diff --git a/src/LayoutSaver.cpp b/src/LayoutSaver.cpp index 7c73548d3..7f5485b16 100644 --- a/src/LayoutSaver.cpp +++ b/src/LayoutSaver.cpp @@ -18,6 +18,7 @@ #include "LayoutSaver.h" #include "Config.h" +#include "core/DropArea.h" #include "core/ViewFactory.h" #include "core/LayoutSaver_p.h" #include "core/Logging_p.h" @@ -39,6 +40,7 @@ #include #include #include +#include #include /** @@ -441,13 +443,13 @@ QByteArray LayoutSaver::serializeLayout() const layout.mainWindows.reserve(mainWindows.size()); for (auto mainWindow : mainWindows) { if (d->matchesAffinity(mainWindow->affinities())) - layout.mainWindows.push_back(mainWindow->serialize()); + layout.mainWindows.push_back(mainWindow->serialize(d->m_affinityNames)); } const auto floatingWindows = Core::floatingWindowsForAffinity(d->m_affinityNames); layout.floatingWindows.reserve(floatingWindows.size()); for (Core::FloatingWindow *floatingWindow : floatingWindows) { - layout.floatingWindows.push_back(floatingWindow->serialize()); + layout.floatingWindows.push_back(floatingWindow->serialize(d->m_affinityNames)); } // Closed dock widgets also have interesting things to save, like geometry and placeholder info @@ -474,6 +476,49 @@ QByteArray LayoutSaver::serializeLayout() const return layout.toJson(); } +namespace { + +bool isDocumentMode(const Core::MainWindow::List &mainWindows, + const QVector &affinities) +{ + if (mainWindows.size() != 1) { + // more than 1 window not supported (yet, until someone needs it ?) + return false; + } + + if (affinities.size() != 1) { + // Not supported + return false; + } + + auto mainWindow = mainWindows.first(); + const QString &documentAffinity = mainWindow->documentAffinity(); + if (documentAffinity.isEmpty()) { + return false; + } + + if ((mainWindow->options() & MainWindowOption_HasCentralGroup) == 0) { + // we need to be in document mode + return false; + } + + return true; +} + +bool isRestoringDocuments(const Core::MainWindow::List &mainWindows, + const QVector &affinities) +{ + return isDocumentMode(mainWindows, affinities) && affinities.first() == mainWindows.first()->documentAffinity(); +} + +// bool isRestoringNonDocuments(const Core::MainWindow::List &mainWindows, +// const QVector &affinities) +// { +// return isDocumentMode(mainWindows, affinities) && affinities.first() != mainWindows.first()->documentAffinity(); +// } + +} + bool LayoutSaver::restoreLayout(const QByteArray &data) { LayoutSaver::DockWidget::s_dockWidgets.clear(); @@ -513,18 +558,25 @@ bool LayoutSaver::restoreLayout(const QByteArray &data) layout.scaleSizes(d->m_restoreOptions); d->floatWidgetsWhichSkipRestore(layout.mainWindowNames()); - d->floatUnknownWidgets(layout); Private::RAIIIsRestoring isRestoring; // Hide all dockwidgets and unparent them from any layout before starting restore // We only close the stuff that the loaded JSON knows about. Unknown widgets might be newer. - d->m_dockRegistry->clear(d->m_dockRegistry->dockWidgets(layout.dockWidgetsToClose()), - d->m_dockRegistry->mainWindows(layout.mainWindowNames()), - d->m_affinityNames); + auto dockWidgetsToClose = d->m_dockRegistry->dockWidgets(layout.dockWidgetsToClose()); + auto mainWindowsToConsider = d->m_dockRegistry->mainWindows(layout.mainWindowNames()); + const bool isRestoringDocuments = ::isRestoringDocuments(mainWindowsToConsider, d->m_affinityNames); + + if (!isDocumentMode(mainWindowsToConsider, d->m_affinityNames)) + d->floatUnknownWidgets(layout); + + d->m_dockRegistry->clear(dockWidgetsToClose, + mainWindowsToConsider, + d->m_affinityNames, isRestoringDocuments); // 1. Restore main windows + for (const LayoutSaver::MainWindow &mw : std::as_const(layout.mainWindows)) { auto mainWindow = d->m_dockRegistry->mainWindowByName(mw.uniqueName); if (!mainWindow) { @@ -539,20 +591,32 @@ bool LayoutSaver::restoreLayout(const QByteArray &data) if (!d->matchesAffinity(mainWindow->affinities())) continue; - if (!(d->m_restoreOptions & InternalRestoreOption::SkipMainWindowGeometry) && !mainWindow->isInDockWidget()) { - Window::Ptr window = mainWindow->view()->window(); - if (window->windowState() == WindowState::Maximized) { - // Restoring geometry needs to be done in normal state. - // Qt doesn't support restoring normal geometry on maximized windows. - window->setWindowState(WindowState::None); + if (isRestoringDocuments) { + // In 'document mode' we only restore the central tabs + if (const auto centralGroup = mw.multiSplitterLayout.centralGroup(); !centralGroup.isNull) { + for (const auto &savedDock : centralGroup.dockWidgets) { + Core::DockWidget *dw = d->m_dockRegistry->dockByName( + savedDock->uniqueName, DockRegistry::DockByNameFlag::CreateIfNotFound); + if (dw) + mainWindow->addDockWidgetAsTab(dw); + } } + } else { + if (!(d->m_restoreOptions & InternalRestoreOption::SkipMainWindowGeometry) && !mainWindow->isInDockWidget()) { + Window::Ptr window = mainWindow->view()->window(); + if (window->windowState() == WindowState::Maximized) { + // Restoring geometry needs to be done in normal state. + // Qt doesn't support restoring normal geometry on maximized windows. + window->setWindowState(WindowState::None); + } - d->deserializeWindowGeometry(mw, window); - window->setWindowState(mw.windowState); - } + d->deserializeWindowGeometry(mw, window); + window->setWindowState(mw.windowState); + } - if (!mainWindow->deserialize(mw)) - return false; + if (!mainWindow->deserialize(mw)) + return false; + } } // 2. Restore FloatingWindows @@ -1186,6 +1250,15 @@ bool LayoutSaver::MultiSplitter::skipsRestore() const [](auto it) { return it.second.skipsRestore(); }); } +LayoutSaver::Group LayoutSaver::MultiSplitter::centralGroup() const +{ + for (const auto &[id, group] : groups) { + if (group.options & FrameOption_IsCentralFrame) + return group; + } + return {}; +} + void LayoutSaver::Position::scaleSizes(const ScalingInfo &scalingInfo) { scalingInfo.applyFactorsTo(/*by-ref*/ lastFloatingGeometry); diff --git a/src/core/DockRegistry.cpp b/src/core/DockRegistry.cpp index 0412f77b4..cd6f04d2d 100644 --- a/src/core/DockRegistry.cpp +++ b/src/core/DockRegistry.cpp @@ -673,8 +673,14 @@ void DockRegistry::clear(const QVector &affinities) void DockRegistry::clear(const Core::DockWidget::List &dockWidgets, const Core::MainWindow::List &mainWindows, - const QVector &affinities) + const QVector &affinities, bool documentsOnly) { + if (documentsOnly) { + // Handle special case, "document mode" restore + clearDocuments(dockWidgets, mainWindows.first()); + return; + } + for (auto dw : std::as_const(dockWidgets)) { if (affinities.isEmpty() || affinitiesMatch(affinities, dw->affinities())) { dw->forceClose(); @@ -689,6 +695,31 @@ void DockRegistry::clear(const Core::DockWidget::List &dockWidgets, } } +/// Special case when restoring project documents. Do not touch non-document widgets. +void DockRegistry::clearDocuments(const QVector &dockWidgets, + Core::MainWindow *mainWindow) +{ + const QString documentAffinity = mainWindow->documentAffinity(); + + // Clear tabbed documents: + Core::Group *centralGroup = mainWindow->dropArea()->centralGroup(); + Vector dws = centralGroup->dockWidgets(); + for (Core::DockWidget *dw : dws) { + if (dw->affinities().contains(documentAffinity)) { + dw->forceClose(); + dw->d->lastPosition()->removePlaceholders(); + } + } + + // Clear floating documents: + for (auto dw : std::as_const(dockWidgets)) { + if (dw->isFloating() && dw->affinities().contains(documentAffinity)) { + dw->forceClose(); + dw->d->lastPosition()->removePlaceholders(); + } + } +} + void DockRegistry::ensureAllFloatingWidgetsAreMorphed() { for (Core::DockWidget *dw : std::as_const(m_dockWidgets)) { diff --git a/src/core/DockRegistry.h b/src/core/DockRegistry.h index 6609f80b7..49b17e186 100644 --- a/src/core/DockRegistry.h +++ b/src/core/DockRegistry.h @@ -169,7 +169,7 @@ class DOCKS_EXPORT DockRegistry : public QObject, public Core::EventFilterInterf */ void clear(const QVector &dockWidgets, const QVector &mainWindows, - const QVector &affinities); + const QVector &affinities, bool documentsOnly = false); /** * @brief Ensures that all floating DockWidgets have a FloatingWindow as a window. @@ -255,6 +255,9 @@ class DOCKS_EXPORT DockRegistry : public QObject, public Core::EventFilterInterf void removeSideBarGrouping(const QVector &); QVector sideBarGroupingFor(Core::DockWidget *) const; + void clearDocuments(const QVector &dockWidgets, + Core::MainWindow *mainWindow); + Private *const d; QVector m_dockWidgets; diff --git a/src/core/DockWidget.cpp b/src/core/DockWidget.cpp index c1d8635fe..2f75f6280 100644 --- a/src/core/DockWidget.cpp +++ b/src/core/DockWidget.cpp @@ -947,7 +947,7 @@ Core::DockWidget *DockWidget::deserialize(const LayoutSaver::DockWidget::Ptr &sa #endif if (dw->affinities() != saved->affinities) { - KDDW_ERROR("Affinity name changed from {} to {}", dw->affinities(), "; to", saved->affinities); + KDDW_ERROR("Affinity name changed from {} to {} for dw {}", dw->affinities(), saved->affinities, dw->uniqueName()); dw->d->affinities = saved->affinities; } diff --git a/src/core/FloatingWindow.cpp b/src/core/FloatingWindow.cpp index 5ea311593..08fdf63f6 100644 --- a/src/core/FloatingWindow.cpp +++ b/src/core/FloatingWindow.cpp @@ -603,14 +603,14 @@ bool FloatingWindow::deserialize(const LayoutSaver::FloatingWindow &fw) return false; } -LayoutSaver::FloatingWindow FloatingWindow::serialize() const +LayoutSaver::FloatingWindow FloatingWindow::serialize(const Vector &affinityNames) const { LayoutSaver::FloatingWindow fw; fw.geometry = geometry(); fw.normalGeometry = view()->normalGeometry(); fw.isVisible = isVisible(); - fw.multiSplitterLayout = dropArea()->serialize(); + fw.multiSplitterLayout = dropArea()->serialize(affinityNames); fw.screenIndex = Platform::instance()->screenNumberForView(view()); fw.screenSize = Platform::instance()->screenSizeFor(view()); fw.affinities = affinities(); diff --git a/src/core/FloatingWindow.h b/src/core/FloatingWindow.h index 174e01839..b5dbfb573 100644 --- a/src/core/FloatingWindow.h +++ b/src/core/FloatingWindow.h @@ -44,7 +44,7 @@ class DOCKS_EXPORT FloatingWindow : public Controller, public Draggable virtual ~FloatingWindow() override; bool deserialize(const LayoutSaver::FloatingWindow &); - LayoutSaver::FloatingWindow serialize() const; + LayoutSaver::FloatingWindow serialize(const Vector &affinityNames) const; // Draggable: std::unique_ptr makeWindow() override; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index fb3eb7f3f..efc0efd70 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -260,7 +260,7 @@ void Group::insertWidget(DockWidget *dockWidget, int index, const InitialOption assert(dockWidget); if (containsDockWidget(dockWidget)) { if (!dockWidget->isPersistentCentralDockWidget()) - KDDW_ERROR("Group::addTab dockWidget already exists. this={} ; dockWidget={}", ( void * )this, ( void * )dockWidget); + KDDW_ERROR("Group::addTab dockWidget already exists. this={} ; dockWidget={} ; name={}", ( void * )this, ( void * )dockWidget, dockWidget->uniqueName()); return; } if (d->m_layoutItem) @@ -772,7 +772,7 @@ Group *Group::deserialize(const LayoutSaver::Group &f) return group; } -LayoutSaver::Group Group::serialize() const +LayoutSaver::Group Group::serialize(const Vector &affinityNames) const { LayoutSaver::Group group; group.isNull = false; @@ -789,10 +789,12 @@ LayoutSaver::Group Group::serialize() const group.mainWindowUniqueName = mw->uniqueName(); } - for (DockWidget *dock : docks) - group.dockWidgets.push_back(dock->d->serialize()); + for (DockWidget *dock : docks) { + if (affinityNames.isEmpty() || DockRegistry::self()->affinitiesMatch(affinityNames, dock->affinities())) + group.dockWidgets.push_back(dock->d->serialize()); + } - if (group.currentTabIndex == -1 && !docks.isEmpty()) { + if (group.currentTabIndex == -1 && !group.dockWidgets.isEmpty()) { KDDW_ERROR("Group::serialize: Current index shouldn't be -1. Setting to 0 instead."); group.currentTabIndex = 0; } diff --git a/src/core/Group.h b/src/core/Group.h index 2132e12eb..44f52e03a 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -49,7 +49,7 @@ class DOCKS_EXPORT Group : public Controller, public FocusScope ~Group() override; static Group *deserialize(const LayoutSaver::Group &); - LayoutSaver::Group serialize() const; + LayoutSaver::Group serialize(const Vector &affinityNames) const; ///@brief Adds a widget into the Group's Stack void addTab(DockWidget *, const InitialOption & = {}); diff --git a/src/core/Layout.cpp b/src/core/Layout.cpp index b4bc2f960..25293ab0c 100644 --- a/src/core/Layout.cpp +++ b/src/core/Layout.cpp @@ -329,7 +329,7 @@ bool Layout::onResize(Size newSize) return false; // So QWidget::resizeEvent is called } -LayoutSaver::MultiSplitter Layout::serialize() const +LayoutSaver::MultiSplitter Layout::serialize(const Vector &affinityNames) const { LayoutSaver::MultiSplitter l; d->m_rootItem->to_json(l.layout); @@ -338,7 +338,7 @@ LayoutSaver::MultiSplitter Layout::serialize() const for (Core::Item *item : items) { if (!item->isContainer()) { if (auto group = Group::fromItem(item)) { - l.groups[group->view()->d->id()] = group->serialize(); + l.groups[group->view()->d->id()] = group->serialize(affinityNames); } } } diff --git a/src/core/Layout.h b/src/core/Layout.h index 02e93f1f1..156b2c052 100644 --- a/src/core/Layout.h +++ b/src/core/Layout.h @@ -184,7 +184,7 @@ class DOCKS_EXPORT Layout : public Controller void updateSizeConstraints(); virtual bool deserialize(const LayoutSaver::MultiSplitter &); - LayoutSaver::MultiSplitter serialize() const; + LayoutSaver::MultiSplitter serialize(const Vector &affinityNames) const; Core::DropArea *asDropArea() const; Core::MDILayout *asMDILayout() const; diff --git a/src/core/LayoutSaver_p.h b/src/core/LayoutSaver_p.h index 75749860c..d60f06ff9 100644 --- a/src/core/LayoutSaver_p.h +++ b/src/core/LayoutSaver_p.h @@ -187,6 +187,7 @@ struct DOCKS_EXPORT LayoutSaver::MultiSplitter bool hasSingleDockWidget() const; LayoutSaver::DockWidget::Ptr singleDockWidget() const; bool skipsRestore() const; + LayoutSaver::Group centralGroup() const; nlohmann::json layout; std::unordered_map groups; diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index 361537290..eb37a4c6e 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -231,6 +231,16 @@ void MainWindow::setAffinities(const Vector &affinityNames) d->affinities = affinities; } +void MainWindow::setDocumentAffinity(const QString &affinity) +{ + d->m_documentAffinity = affinity; +} + +QString MainWindow::documentAffinity() const +{ + return d->m_documentAffinity; +} + Vector MainWindow::affinities() const { return d->affinities; @@ -776,7 +786,7 @@ bool MainWindow::deserialize(const LayoutSaver::MainWindow &mw) return success; } -LayoutSaver::MainWindow MainWindow::serialize() const +LayoutSaver::MainWindow MainWindow::serialize(const Vector &affinityNames) const { LayoutSaver::MainWindow m; @@ -789,7 +799,7 @@ LayoutSaver::MainWindow MainWindow::serialize() const m.uniqueName = uniqueName(); m.screenIndex = Platform::instance()->screenNumberForView(view()); m.screenSize = Platform::instance()->screenSizeFor(view()); - m.multiSplitterLayout = layout()->serialize(); + m.multiSplitterLayout = layout()->serialize(affinityNames); m.affinities = d->affinities; m.windowState = window ? window->windowState() : WindowState::None; diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index 447ac7095..3460e08d1 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -152,6 +152,19 @@ class DOCKS_EXPORT MainWindow : public Controller */ void setAffinities(const Vector &names); + /** + * @brief Sets the "document affinity". + * + * Some projects have the concept of "document". For example, in an IDE, each tab in the central area might be a document. + * It's common that when loading a layout you want to restore everything except the documents, which are project specific. + * Conversely, you might want to save/restore the documents but not the rest of the layout. + * + * Use this in conjuection with LayoutSaver::setAffinityNames() to specify that the "document affinity" should be included or excluded from save/restore. + * Set the dockwidget's affinity to the same value via Core::DockWidget::setAffinities() to have it included in the "document affinity" and tab those to center. + */ + void setDocumentAffinity(const QString &affinity); + QString documentAffinity() const; + /** * @brief Returns the list of affinity names. Empty by default. */ @@ -285,7 +298,7 @@ class DOCKS_EXPORT MainWindow : public Controller friend class ::TestDocks; friend class KDDockWidgets::LayoutSaver; bool deserialize(const LayoutSaver::MainWindow &); - LayoutSaver::MainWindow serialize() const; + LayoutSaver::MainWindow serialize(const Vector &affinityNames) const; }; } } diff --git a/src/core/MainWindow_p.h b/src/core/MainWindow_p.h index 39ab1f4b8..14e39c487 100644 --- a/src/core/MainWindow_p.h +++ b/src/core/MainWindow_p.h @@ -110,6 +110,7 @@ class MainWindow::Private QString name; Vector affinities; + QString m_documentAffinity; const MainWindowOptions m_options; MainWindow *const q; ObjectGuard m_overlayedDockWidget; diff --git a/tests/qtwidgets/tst_qtwidgets.cpp b/tests/qtwidgets/tst_qtwidgets.cpp index 47d500894..8bf50c0cb 100644 --- a/tests/qtwidgets/tst_qtwidgets.cpp +++ b/tests/qtwidgets/tst_qtwidgets.cpp @@ -244,6 +244,7 @@ private Q_SLOTS: void tst_complex(); void tst_restoreFloatingMaximizedState(); void tst_findAncestor(); + void tst_affinityWithPersistentCentralGroup(); void tst_affinityFloatingWindowIndexMismatch(); #if defined(KDDW_FRONTEND_QT) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) void tst_userData(); @@ -3137,6 +3138,78 @@ void TestQtWidgets::tst_controllerToView() QCOMPARE(dws[0]->view(), dwView); } +void TestQtWidgets::tst_affinityWithPersistentCentralGroup() +{ + EnsureTopLevelsDeleted e; + + auto m = createMainWindow(QSize(1000, 800), MainWindowOption_HasCentralGroup, "mw1"); + m->setAffinities({ "global", "projects" }); + m->setDocumentAffinity("projects"); + + auto projectDock1 = new QtWidgets::DockWidget("project1"); + projectDock1->asDockWidgetController()->setAffinities({ "projects" }); + m->addDockWidgetAsTab(projectDock1->asDockWidgetController()); + + auto projectDock2 = new QtWidgets::DockWidget("project2"); + projectDock2->asDockWidgetController()->setAffinities({ "projects" }); + m->addDockWidgetAsTab(projectDock2->asDockWidgetController()); + + auto dockedGlobal = new QtWidgets::DockWidget("dockedGlobal"); + dockedGlobal->asDockWidgetController()->setAffinities({ "global" }); + m->addDockWidgetToSide(dockedGlobal->asDockWidgetController(), KDDockWidgets::Location_OnLeft); + + auto dockedGlobal2 = new QtWidgets::DockWidget("dockedGlobal2"); + dockedGlobal2->asDockWidgetController()->setAffinities({ "global" }); + m->addDockWidgetToSide(dockedGlobal2->asDockWidgetController(), KDDockWidgets::Location_OnRight); + + auto floatingGlobal = new QtWidgets::DockWidget("floatingGlobal"); + floatingGlobal->asDockWidgetController()->setAffinities({ "global" }); + floatingGlobal->asDockWidgetController()->open(); + + auto floatingProjects = new QtWidgets::DockWidget("floatingProjects"); + floatingProjects->asDockWidgetController()->setAffinities({ "projects" }); + floatingProjects->asDockWidgetController()->open(); + + LayoutSaver projectSaver; + projectSaver.setAffinityNames({ "projects" }); + + LayoutSaver globalSaver; + globalSaver.setAffinityNames({ "global" }); + + const auto &projectLayout = projectSaver.serializeLayout(); + const auto &globalLayout = globalSaver.serializeLayout(); + + // Make changes to the global state and to the project state + projectDock2->close(); + dockedGlobal2->close(); + floatingGlobal->close(); + floatingProjects->close(); + + QVERIFY(dockedGlobal->isOpen()); + QVERIFY(!dockedGlobal->isFloating()); + + QVERIFY(projectSaver.restoreLayout(projectLayout)); + + // Verify that only the project layout was restored, and the global layout is unaffected + QVERIFY(projectDock2->isOpen()); + QVERIFY(!dockedGlobal2->isOpen()); + QVERIFY(dockedGlobal->isOpen()); + QVERIFY(!dockedGlobal->isFloating()); + QVERIFY(!floatingGlobal->isOpen()); + + projectDock2->close(); // close it again + + QVERIFY(globalSaver.restoreLayout(globalLayout)); + + // verify that the global layout was restored, and the project layout is unaffected + QVERIFY(dockedGlobal->isOpen()); + QVERIFY(dockedGlobal2->isOpen()); + QVERIFY(floatingGlobal->isOpen()); + QVERIFY(projectDock1->isOpen()); + QVERIFY(!projectDock1->isFloating()); + QVERIFY(!projectDock2->isOpen()); +} + void TestQtWidgets::tst_affinityFloatingWindowIndexMismatch() { EnsureTopLevelsDeleted e;