/* This file is part of KDDockWidgets. SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company Author: Sérgio Martins SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only Contact KDAB at for commercial licensing options. */ /** * @file * @brief Class to save and restore dock widget layouts. * * @author Sérgio Martins \ */ #include "LayoutSaver.h" #include "Config.h" #include "core/ViewFactory.h" #include "core/LayoutSaver_p.h" #include "core/Logging_p.h" #include "core/Position_p.h" #include "core/Utils_p.h" #include "core/View_p.h" #include "core/DockRegistry.h" #include "core/Platform.h" #include "core/Layout.h" #include "core/Group.h" #include "core/FloatingWindow.h" #include "core/DockWidget.h" #include "core/DockWidget_p.h" #include "core/MainWindow.h" #include "core/nlohmann_helpers_p.h" #include "core/layouting/Item_p.h" #include #include #include #include /** * Some implementation details: * * Restoring is done in two phases. From the JSON, we construct an intermediate representation, * which doesn't have any GUI types. Finally we then construct the GUI from the intermediate * representation. * * JSON <-> Intermediate rep (bunch non-gui structs) <-> GUI classes * * This is in contrast to many other dock widget frameworks which just do: * serialized <-> GUI * * The advantage of having the intermediate structs is that we can do validations on them and if * we find some corruption we don't even start messing with the GUI. * * See the LayoutSaver::* structs in LayoutSaver_p.h, those are the intermediate structs. * They have methods to convert to/from JSON. * All other gui classes have methods to convert to/from these structs. For example * FloatingWindow::serialize()/deserialize() */ using namespace KDDockWidgets; using namespace KDDockWidgets::Core; std::map LayoutSaver::DockWidget::s_dockWidgets; LayoutSaver::Layout *LayoutSaver::Layout::s_currentLayoutBeingRestored = nullptr; std::unordered_map> LayoutSaver::Private::s_unrestoredPositions; std::unordered_map LayoutSaver::Private::s_unrestoredProperties; static InternalRestoreOptions internalRestoreOptions(RestoreOptions options) { InternalRestoreOptions ret = {}; if (options.testFlag(RestoreOption_RelativeToMainWindow)) { ret.setFlag(InternalRestoreOption::SkipMainWindowGeometry); ret.setFlag(InternalRestoreOption::RelativeFloatingWindowGeometry); options.setFlag(RestoreOption_RelativeToMainWindow, false); } if (options.testFlag(RestoreOption_AbsoluteFloatingDockWindows)) { ret.setFlag(InternalRestoreOption::RelativeFloatingWindowGeometry, false); options.setFlag(RestoreOption_AbsoluteFloatingDockWindows, false); } if (options != RestoreOption_None) { KDDW_ERROR("Unknown options={}", int(options)); } return ret; } bool LayoutSaver::Private::s_restoreInProgress = false; namespace KDDockWidgets { template static T jsonValue(const nlohmann::json &json, const char *name, const T &defaultValue) { try { // value can throw if the type has "null" value, for instance. return json.value(name, defaultValue); } catch (...) { return defaultValue; } } template static void to_json(nlohmann::json &json, const typename Type::List &list) { for (const auto &l : list) { json.push_back(l); } } static void to_json(nlohmann::json &json, const LayoutSaver::Group &f) { json["id"] = f.id; json["isNull"] = f.isNull; json["objectName"] = f.objectName; json["geometry"] = f.geometry; json["options"] = f.options; json["currentTabIndex"] = f.currentTabIndex; json["mainWindowUniqueName"] = f.mainWindowUniqueName; json["dockWidgets"] = dockWidgetNames(f.dockWidgets); } static void from_json(const nlohmann::json &json, LayoutSaver::Group &f) { f.id = jsonValue(json, "id", QString()); f.isNull = jsonValue(json, "isNull", true); f.objectName = jsonValue(json, "objectName", QString()); f.geometry = jsonValue(json, "geometry", Rect()); f.options = jsonValue(json, "options", QFlags::Int {}); f.currentTabIndex = jsonValue(json, "currentTabIndex", 0); f.mainWindowUniqueName = jsonValue(json, "mainWindowUniqueName", QString()); auto it = json.find("dockWidgets"); if (it == json.end()) return; auto &dockWidgets = *it; f.dockWidgets.reserve(( int )dockWidgets.size()); for (const auto &d : dockWidgets) { LayoutSaver::DockWidget::Ptr dw = LayoutSaver::DockWidget::dockWidgetForName(d.get()); f.dockWidgets.push_back(dw); } } static void to_json(nlohmann::json &json, const LayoutSaver::MultiSplitter &s) { json["layout"] = s.layout; auto &groups = json["frames"]; for (const auto &it : s.groups) { const auto &group = it.second; groups[group.id.toStdString()] = group; } } static void from_json(const nlohmann::json &json, LayoutSaver::MultiSplitter &s) { s.groups.clear(); s.layout = jsonValue(json, "layout", nlohmann::json::object()); auto it = json.find("frames"); if (it == json.end()) return; if (it->is_null()) return; auto &frms = *it; if (!frms.is_object()) KDDW_ERROR("Unexpected not object"); for (const auto &kv : frms.items()) { QString key = QString::fromStdString(kv.key()); auto group = kv.value().get(); s.groups[key] = group; } } static void to_json(nlohmann::json &json, const LayoutSaver::MainWindow &mw) { json["options"] = int(mw.options); json["multiSplitterLayout"] = mw.multiSplitterLayout; json["uniqueName"] = mw.uniqueName; json["geometry"] = mw.geometry; json["normalGeometry"] = mw.normalGeometry; json["screenIndex"] = mw.screenIndex; json["screenSize"] = mw.screenSize; json["isVisible"] = mw.isVisible; json["affinities"] = mw.affinities; json["windowState"] = mw.windowState; for (SideBarLocation loc : { SideBarLocation::North, SideBarLocation::East, SideBarLocation::West, SideBarLocation::South }) { const Vector dockWidgets = mw.dockWidgetsForSideBar(loc); if (!dockWidgets.isEmpty()) { std::string key = std::string("sidebar-") + std::to_string(( int )loc); json[key] = dockWidgets; } } } static void from_json(const nlohmann::json &json, LayoutSaver::MainWindow &mw) { mw.options = static_cast(jsonValue(json, "options", 0)); mw.multiSplitterLayout = jsonValue(json, "multiSplitterLayout", LayoutSaver::MultiSplitter()); mw.uniqueName = jsonValue(json, "uniqueName", QString()); mw.geometry = jsonValue(json, "geometry", Rect()); mw.normalGeometry = jsonValue(json, "normalGeometry", Rect()); mw.screenIndex = jsonValue(json, "screenIndex", 0); mw.screenSize = jsonValue(json, "screenSize", Size(800, 600)); mw.isVisible = jsonValue(json, "isVisible", false); mw.affinities = jsonValue(json, "affinities", Vector()); mw.windowState = ( WindowState )jsonValue(json, "windowState", 0); // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities" // list: if (json.find("affinityName") != json.end()) { QString affinityName = json["affinityName"].get(); if (!mw.affinities.contains(affinityName)) { mw.affinities.push_back(affinityName); } } for (SideBarLocation loc : { SideBarLocation::North, SideBarLocation::East, SideBarLocation::West, SideBarLocation::South }) { std::string key = std::string("sidebar-") + std::to_string(( int )loc); auto it = json.find(key); if (it == json.end()) continue; auto &val = *it; if (val.is_array() && !val.empty()) { mw.dockWidgetsPerSideBar[loc] = val.get>(); } } } static void to_json(nlohmann::json &json, const LayoutSaver::FloatingWindow &window) { json["multiSplitterLayout"] = window.multiSplitterLayout; json["parentIndex"] = window.parentIndex; json["geometry"] = window.geometry; json["normalGeometry"] = window.normalGeometry; json["screenIndex"] = window.screenIndex; json["screenSize"] = window.screenSize; json["flags"] = window.flags; json["isVisible"] = window.isVisible; json["windowState"] = window.windowState; if (!window.affinities.isEmpty()) { json["affinities"] = window.affinities; } } static void from_json(const nlohmann::json &json, LayoutSaver::FloatingWindow &window) { window.multiSplitterLayout = jsonValue(json, "multiSplitterLayout", LayoutSaver::MultiSplitter()); window.parentIndex = jsonValue(json, "parentIndex", -1); window.geometry = jsonValue(json, "geometry", Rect()); window.normalGeometry = jsonValue(json, "normalGeometry", Rect()); window.screenIndex = jsonValue(json, "screenIndex", 0); window.screenSize = jsonValue(json, "screenSize", Size(800, 600)); window.isVisible = jsonValue(json, "isVisible", false); window.flags = jsonValue(json, "flags", int(FloatingWindowFlag::FromGlobalConfig)); window.windowState = ( WindowState )jsonValue(json, "windowState", 0); window.affinities = jsonValue(json, "affinities", Vector()); // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities" // list: const QString affinityName = jsonValue(json, "affinityName", QString()); if (!affinityName.isEmpty() && !window.affinities.contains(affinityName)) { window.affinities.push_back(affinityName); } } static void to_json(nlohmann::json &json, const LayoutSaver::ScreenInfo &screenInfo) { json["index"] = screenInfo.index; json["geometry"] = screenInfo.geometry; json["name"] = screenInfo.name; json["devicePixelRatio"] = screenInfo.devicePixelRatio; } static void from_json(const nlohmann::json &j, LayoutSaver::ScreenInfo &screenInfo) { screenInfo.index = j.value("index", 0); screenInfo.geometry = j.value("geometry", Rect()); screenInfo.name = j.value("name", QString()); screenInfo.devicePixelRatio = j.value("devicePixelRatio", 1.0); } static void to_json(nlohmann::json &json, const LayoutSaver::Placeholder &placeHolder) { json["isFloatingWindow"] = placeHolder.isFloatingWindow; json["itemIndex"] = placeHolder.itemIndex; if (placeHolder.isFloatingWindow) json["indexOfFloatingWindow"] = placeHolder.indexOfFloatingWindow; else json["mainWindowUniqueName"] = placeHolder.mainWindowUniqueName; } static void from_json(const nlohmann::json &json, LayoutSaver::Placeholder &placeHolder) { placeHolder.isFloatingWindow = jsonValue(json, "isFloatingWindow", false); placeHolder.itemIndex = jsonValue(json, "itemIndex", 0); placeHolder.indexOfFloatingWindow = jsonValue(json, "indexOfFloatingWindow", -1); placeHolder.mainWindowUniqueName = jsonValue(json, "mainWindowUniqueName", QString()); } static void to_json(nlohmann::json &json, const LayoutSaver::Position &pos) { json["lastFloatingGeometry"] = pos.lastFloatingGeometry; json["lastOverlayedGeometries"] = pos.lastOverlayedGeometries; json["tabIndex"] = pos.tabIndex; json["wasFloating"] = pos.wasFloating; json["placeholders"] = pos.placeholders; } static void from_json(const nlohmann::json &json, LayoutSaver::Position &pos) { pos.lastFloatingGeometry = jsonValue(json, "lastFloatingGeometry", Rect()); pos.lastOverlayedGeometries = jsonValue(json, "lastOverlayedGeometries", std::unordered_map()); pos.tabIndex = jsonValue(json, "tabIndex", 0); pos.wasFloating = jsonValue(json, "wasFloating", false); pos.placeholders = jsonValue(json, "placeholders", LayoutSaver::Placeholder::List()); } static void to_json(nlohmann::json &json, const LayoutSaver::DockWidget &dw) { if (!dw.affinities.isEmpty()) json["affinities"] = dw.affinities; json["uniqueName"] = dw.uniqueName; json["lastPosition"] = dw.lastPosition; json["lastCloseReason"] = dw.lastCloseReason; #if defined(KDDW_FRONTEND_QT) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) if (!dw.userData.isEmpty()) json["userData"] = dw.userData; #endif } static void from_json(const nlohmann::json &json, LayoutSaver::DockWidget &dw) { auto it = json.find("affinities"); if (it != json.end()) dw.affinities = it->get>(); dw.uniqueName = jsonValue(json, "uniqueName", QString()); if (dw.uniqueName.isEmpty()) KDDW_ERROR("Unexpected no uniqueName for dockWidget"); dw.lastPosition = jsonValue(json, "lastPosition", LayoutSaver::Position()); dw.lastCloseReason = jsonValue(json, "lastCloseReason", CloseReason::Unspecified); #if defined(KDDW_FRONTEND_QT) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) auto userDataIt = json.find("userData"); if (userDataIt != json.end()) { QVariantMap userData; from_json(*userDataIt, userData); dw.userData = userData; } #endif } static void to_json(nlohmann::json &json, const typename LayoutSaver::DockWidget::List &list) { for (const auto &mw : list) { json.push_back(*mw); } } static void from_json(const nlohmann::json &json, typename LayoutSaver::DockWidget::List &list) { list.clear(); for (const auto &v : json) { auto it = v.find("uniqueName"); if (it == v.end()) { KDDW_ERROR("Unexpected no uniqueName"); continue; } QString uniqueName = it->get(); auto dw = LayoutSaver::DockWidget::dockWidgetForName(uniqueName); from_json(v, *dw); list.push_back(dw); } } } LayoutSaver::LayoutSaver(RestoreOptions options) : d(new Private(options)) { d->m_dockRegistry->registerLayoutSaver(); } LayoutSaver::~LayoutSaver() { d->m_dockRegistry->unregisterLayoutSaver(); delete d; } bool LayoutSaver::saveToFile(const QString &jsonFilename) { const QByteArray data = serializeLayout(); std::ofstream file(jsonFilename.toStdString(), std::ios::binary); if (!file.is_open()) { KDDW_ERROR("Failed to open {}", jsonFilename); return false; } file.write(data.constData(), data.size()); file.close(); return true; } bool LayoutSaver::restoreFromFile(const QString &jsonFilename) { bool ok = false; const QByteArray data = Platform::instance()->readFile(jsonFilename, /*by-ref*/ ok); if (!ok) return false; return restoreLayout(data); } QByteArray LayoutSaver::serializeLayout() const { if (!d->m_dockRegistry->isSane()) { KDDW_ERROR("Refusing to serialize this layout. Check previous warnings."); return {}; } LayoutSaver::Layout layout; // Just a simplification. One less type of windows to handle. d->m_dockRegistry->ensureAllFloatingWidgetsAreMorphed(); const auto mainWindows = d->m_dockRegistry->mainwindows(); layout.mainWindows.reserve(mainWindows.size()); for (auto mainWindow : mainWindows) { if (d->matchesAffinity(mainWindow->affinities())) layout.mainWindows.push_back(mainWindow->serialize()); } const Vector floatingWindows = d->m_dockRegistry->floatingWindows(/*includeBeingDeleted=*/false, /*honourSkipped=*/true); layout.floatingWindows.reserve(floatingWindows.size()); for (Core::FloatingWindow *floatingWindow : floatingWindows) { if (d->matchesAffinity(floatingWindow->affinities())) layout.floatingWindows.push_back(floatingWindow->serialize()); } // Closed dock widgets also have interesting things to save, like geometry and placeholder info const Core::DockWidget::List closedDockWidgets = d->m_dockRegistry->closedDockwidgets(/*honourSkipped=*/true); layout.closedDockWidgets.reserve(closedDockWidgets.size()); for (Core::DockWidget *dockWidget : closedDockWidgets) { if (d->matchesAffinity(dockWidget->affinities())) layout.closedDockWidgets.push_back(dockWidget->d->serialize()); } // Save the placeholder info. We do it last, as we also restore it last, since we need all items // to be created before restoring the placeholders const Core::DockWidget::List dockWidgets = d->m_dockRegistry->dockwidgets(); layout.allDockWidgets.reserve(dockWidgets.size()); for (Core::DockWidget *dockWidget : dockWidgets) { if (!dockWidget->skipsRestore() && d->matchesAffinity(dockWidget->affinities())) { auto dw = dockWidget->d->serialize(); dw->lastPosition = dockWidget->d->lastPosition()->serialize(); layout.allDockWidgets.push_back(dw); } } return layout.toJson(); } bool LayoutSaver::restoreLayout(const QByteArray &data) { LayoutSaver::DockWidget::s_dockWidgets.clear(); d->clearRestoredProperty(); if (data.isEmpty()) return true; struct GroupCleanup { explicit GroupCleanup(LayoutSaver *saver) : m_saver(saver) { } ~GroupCleanup() { m_saver->d->deleteEmptyGroups(); } GroupCleanup(const GroupCleanup &) = delete; GroupCleanup &operator=(const GroupCleanup) = delete; LayoutSaver *const m_saver; }; GroupCleanup cleanup(this); LayoutSaver::Layout layout; if (!layout.fromJson(data)) { KDDW_ERROR("Failed to parse json data"); return false; } if (!layout.isValid()) { return false; } 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); // 1. Restore main windows for (const LayoutSaver::MainWindow &mw : std::as_const(layout.mainWindows)) { auto mainWindow = d->m_dockRegistry->mainWindowByName(mw.uniqueName); if (!mainWindow) { if (auto mwFunc = Config::self().mainWindowFactoryFunc()) { mainWindow = mwFunc(mw.uniqueName, mw.options); } else { KDDW_ERROR("Failed to restore layout create MainWindow with name {} first", mw.uniqueName); return false; } } 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); } d->deserializeWindowGeometry(mw, window); window->setWindowState(mw.windowState); } if (!mainWindow->deserialize(mw)) return false; } // 2. Restore FloatingWindows for (LayoutSaver::FloatingWindow &fw : layout.floatingWindows) { if (!d->matchesAffinity(fw.affinities) || fw.skipsRestore()) continue; auto parent = fw.parentIndex == -1 ? nullptr : DockRegistry::self()->mainwindows().at(fw.parentIndex); auto flags = static_cast(fw.flags); flags.setFlag(FloatingWindowFlag::StartsMinimized, int(fw.windowState) & int(WindowState::Minimized)); auto floatingWindow = new Core::FloatingWindow({}, parent, flags); fw.floatingWindowInstance = floatingWindow; d->deserializeWindowGeometry(fw, floatingWindow->view()->window()); if (!floatingWindow->deserialize(fw)) { KDDW_ERROR("Failed to deserialize floating window"); return false; } } // 3. Restore closed dock widgets. They remain closed but acquire geometry and placeholder // properties for (const auto &dw : std::as_const(layout.closedDockWidgets)) { if (d->matchesAffinity(dw->affinities)) { Core::DockWidget::deserialize(dw); } } LayoutSaver::Private::s_unrestoredPositions.clear(); LayoutSaver::Private::s_unrestoredProperties.clear(); // 4. Restore the placeholder info, now that the Items have been created for (const auto &dw : std::as_const(layout.allDockWidgets)) { if (!d->matchesAffinity(dw->affinities)) continue; if (Core::DockWidget *dockWidget = d->m_dockRegistry->dockByName( dw->uniqueName, DockRegistry::DockByNameFlag::ConsultRemapping)) { dockWidget->d->lastPosition()->deserialize(dw->lastPosition); } else { KDDW_INFO("Couldn't find dock widget {}", dw->uniqueName); auto pos = std::make_shared(); pos->deserialize(dw->lastPosition); LayoutSaver::Private::s_unrestoredPositions[dw->uniqueName] = pos; LayoutSaver::Private::s_unrestoredProperties[dw->uniqueName] = dw->lastCloseReason; } } return true; } void LayoutSaver::setAffinityNames(const Vector &affinityNames) { d->m_affinityNames = affinityNames; if (affinityNames.contains(QString())) { // Any window with empty affinity will also be subject to save/restore d->m_affinityNames.push_back(QString()); } } LayoutSaver::Private *LayoutSaver::dptr() const { return d; } Core::DockWidget::List LayoutSaver::restoredDockWidgets() const { const Core::DockWidget::List &allDockWidgets = DockRegistry::self()->dockwidgets(); Core::DockWidget::List result; result.reserve(allDockWidgets.size()); for (Core::DockWidget *dw : allDockWidgets) { if (dw->d->m_wasRestored) result.push_back(dw); } return result; } void LayoutSaver::Private::clearRestoredProperty() { const Core::DockWidget::List &allDockWidgets = DockRegistry::self()->dockwidgets(); for (Core::DockWidget *dw : allDockWidgets) { dw->d->m_wasRestored = false; } } template void LayoutSaver::Private::deserializeWindowGeometry(const T &saved, Window::Ptr window) { // Not simply calling QWidget::setGeometry() here. // For QtQuick we need to modify the QWindow's geometry. Rect geometry = saved.geometry; if (!isNormalWindowState(saved.windowState)) { // The window will be maximized. We first set its geometry to normal // Later it's maximized and will remember this value geometry = saved.normalGeometry; if (saved.screenIndex != Platform::instance()->screenNumberForPoint(geometry.topLeft())) { // Workaround bug #553. Window is maximized on screen 2 but its normal // geometry is on screen 1. Restoring normal geometry would move it to screen 2 // To avoid that, we move its normal geometry to screen 2. Could be fixed // without workarounds if Qt supported QWindow::setNormalGeometry() window->setScreen(saved.screenIndex); if (auto screen = window->screen()) { // center is as good as any geometry.moveCenter(screen->geometry().center()); } } } Core::FloatingWindow::ensureRectIsOnScreen(geometry); window->setGeometry(geometry); window->setVisible(saved.isVisible); } LayoutSaver::Private::Private(RestoreOptions options) : m_dockRegistry(DockRegistry::self()) , m_restoreOptions(internalRestoreOptions(options)) { } /*static*/ void LayoutSaver::Private::restorePendingPositions(Core::DockWidget *dw) { if (!dw) return; { auto it = s_unrestoredPositions.find(dw->uniqueName()); if (it != s_unrestoredPositions.end()) { dw->d->m_lastPositions = it->second; s_unrestoredPositions.erase(it); } } { auto it = s_unrestoredProperties.find(dw->uniqueName()); if (it != s_unrestoredProperties.end()) { dw->d->m_lastCloseReason = it->second; s_unrestoredProperties.erase(it); } } } bool LayoutSaver::Private::matchesAffinity(const Vector &affinities) const { return m_affinityNames.isEmpty() || affinities.isEmpty() || DockRegistry::self()->affinitiesMatch(m_affinityNames, affinities); } void LayoutSaver::Private::floatWidgetsWhichSkipRestore(const Vector &mainWindowNames) { // Widgets with the LayoutSaverOptions::Skip flag skip restore completely. // If they were visible before they need to remain visible now. // If they were previously docked we need to float them, as the main window they were on will // be loading a new layout. const auto mainWindows = DockRegistry::self()->mainWindows(mainWindowNames); for (auto mw : mainWindows) { const Core::DockWidget::List docks = mw->layout()->dockWidgets(); for (auto dw : docks) { if (dw->skipsRestore()) { dw->setFloating(true); } } } } void LayoutSaver::Private::floatUnknownWidgets(const LayoutSaver::Layout &layout) { // An old *.json layout file might have not know about existing dock widgets // When restoring such a file, we need to float any visible dock widgets which it doesn't know // about so we can restore the MainWindow layout properly const auto mainWindows = DockRegistry::self()->mainWindows(layout.mainWindowNames()); for (auto mw : mainWindows) { const Core::DockWidget::List docks = mw->layout()->dockWidgets(); for (Core::DockWidget *dw : docks) { if (!layout.containsDockWidget(dw->uniqueName())) { dw->setFloating(true); } } } } void LayoutSaver::Private::deleteEmptyGroups() const { // After a restore it can happen that some DockWidgets didn't exist, so weren't restored. // Delete their group now. const auto groups = m_dockRegistry->groups(); for (auto group : groups) { if (!group->beingDeletedLater() && group->isEmpty() && !group->isCentralGroup()) { if (auto item = group->layoutItem()) { item->turnIntoPlaceholder(); } else { // This doesn't happen. But the warning will make the tests fail if there's a regression. KDDW_ERROR("Expected item for group"); } delete group; } } } bool LayoutSaver::restoreInProgress() { return Private::s_restoreInProgress; } bool LayoutSaver::Layout::isValid() const { if (serializationVersion != KDDOCKWIDGETS_SERIALIZATION_VERSION) { KDDW_ERROR("Serialization format is too old {}, current={}", serializationVersion, KDDOCKWIDGETS_SERIALIZATION_VERSION); return false; } for (auto &m : mainWindows) { if (!m.isValid()) return false; } for (auto &m : floatingWindows) { if (!m.isValid()) return false; } for (auto &m : allDockWidgets) { if (!m->isValid()) return false; } return true; } Vector LayoutSaver::openedDockWidgetsInLayout(const QString &jsonFilename) { bool ok = false; const QByteArray data = Platform::instance()->readFile(jsonFilename, /*by-ref*/ ok); if (!ok) return {}; return openedDockWidgetsInLayout(data); } Vector LayoutSaver::openedDockWidgetsInLayout(const QByteArray &serialized) { LayoutSaver::Layout layout; if (!layout.fromJson(serialized)) return {}; Vector names; names.reserve(layout.allDockWidgets.size()); // over-reserve so we have a single allocation for (const auto &dock : std::as_const(layout.allDockWidgets)) { auto it = std::find_if(layout.closedDockWidgets.cbegin(), layout.closedDockWidgets.cend(), [&dock](auto closedDock) { return dock->uniqueName == closedDock->uniqueName; }); const bool itsOpen = it == layout.closedDockWidgets.cend(); if (itsOpen) names.push_back(dock->uniqueName); } return names; } Vector LayoutSaver::sideBarDockWidgetsInLayout(const QString &jsonFilename) { bool ok = false; const QByteArray data = Platform::instance()->readFile(jsonFilename, /*by-ref*/ ok); if (!ok) return {}; return sideBarDockWidgetsInLayout(data); } Vector LayoutSaver::sideBarDockWidgetsInLayout(const QByteArray &serialized) { LayoutSaver::Layout layout; if (!layout.fromJson(serialized)) return {}; Vector names; names.reserve(layout.allDockWidgets.size()); // over-reserve so we have a single allocation for (const auto &mainWindow : std::as_const(layout.mainWindows)) { for (const auto &it : mainWindow.dockWidgetsPerSideBar) { for (const auto &name : it.second) names.push_back(name); } } return names; } namespace KDDockWidgets { static void to_json(nlohmann::json &j, const LayoutSaver::Layout &layout) { j["serializationVersion"] = layout.serializationVersion; j["mainWindows"] = layout.mainWindows; j["floatingWindows"] = layout.floatingWindows; j["closedDockWidgets"] = ::dockWidgetNames(layout.closedDockWidgets); j["allDockWidgets"] = layout.allDockWidgets; j["screenInfo"] = layout.screenInfo; } static void from_json(const nlohmann::json &j, LayoutSaver::Layout &layout) { layout.serializationVersion = j.value("serializationVersion", 0); layout.mainWindows = j.value("mainWindows", LayoutSaver::MainWindow::List {}); layout.allDockWidgets = j.value("allDockWidgets", LayoutSaver::DockWidget::List {}); layout.closedDockWidgets.clear(); const auto closedDockWidgets = j.value("closedDockWidgets", Vector()); for (const QString &name : closedDockWidgets) { layout.closedDockWidgets.push_back( LayoutSaver::DockWidget::dockWidgetForName(name)); } layout.floatingWindows = j.value("floatingWindows", LayoutSaver::FloatingWindow::List {}); layout.screenInfo = j.value("screenInfo", LayoutSaver::ScreenInfo::List {}); } } QByteArray LayoutSaver::Layout::toJson() const { nlohmann::json json = *this; return QByteArray::fromStdString(json.dump(4)); } bool LayoutSaver::Layout::fromJson(const QByteArray &jsonData) { nlohmann::json json = nlohmann::json::parse(jsonData, nullptr, /*allow_exceptions=*/false); if (json.is_discarded()) { return false; } try { from_json(json, *this); } catch (const std::exception &e) { KDDW_ERROR("LayoutSaver::Layout::fromJson: Caught exception: {}", e.what()); return false; } catch (...) { KDDW_ERROR("LayoutSaver::Layout::fromJson: Caught exception."); return false; } return true; } void LayoutSaver::Layout::scaleSizes(InternalRestoreOptions options) { if (mainWindows.isEmpty()) return; const bool skipsMainWindowGeometry = options & InternalRestoreOption::SkipMainWindowGeometry; if (!skipsMainWindowGeometry) { // No scaling to do. All windows will be restored with the exact size specified in the // saved JSON layouts. return; } // We won't restore MainWindow's geometry, we use whatever the user has now, meaning // we need to scale all dock widgets inside the layout, as the layout might not have // the same size as specified in the saved JSON layout for (auto &mw : mainWindows) mw.scaleSizes(); // MainWindow has a different size than the one in JSON, so we also restore FloatingWindows // relatively to the user set new MainWindow size const bool useRelativeSizesForFloatingWidgets = options & InternalRestoreOption::RelativeFloatingWindowGeometry; if (useRelativeSizesForFloatingWidgets) { for (auto &fw : floatingWindows) { LayoutSaver::MainWindow mw = mainWindowForIndex(fw.parentIndex); if (mw.scalingInfo.isValid()) fw.scaleSizes(mw.scalingInfo); } } const ScalingInfo firstScalingInfo = mainWindows.constFirst().scalingInfo; if (firstScalingInfo.isValid()) { for (auto &dw : allDockWidgets) { // TODO: Determine the best main window. This only interesting for closed dock // widget geometry which was previously floating. But they still have some other // main window as parent. dw->scaleSizes(firstScalingInfo); } } } LayoutSaver::MainWindow LayoutSaver::Layout::mainWindowForIndex(int index) const { if (index < 0 || index >= mainWindows.size()) return {}; return mainWindows.at(index); } LayoutSaver::FloatingWindow LayoutSaver::Layout::floatingWindowForIndex(int index) const { if (index < 0 || index >= floatingWindows.size()) return {}; return floatingWindows.at(index); } Vector LayoutSaver::Layout::mainWindowNames() const { Vector names; names.reserve(mainWindows.size()); for (const auto &mw : mainWindows) { names.push_back(mw.uniqueName); } return names; } Vector LayoutSaver::Layout::dockWidgetNames() const { Vector names; names.reserve(allDockWidgets.size()); for (const auto &dw : allDockWidgets) { names.push_back(dw->uniqueName); } return names; } Vector LayoutSaver::Layout::dockWidgetsToClose() const { // Before restoring a layout we close all dock widgets, unless they're a floating window with // the DontCloseBeforeRestore flag Vector names; names.reserve(allDockWidgets.size()); auto registry = DockRegistry::self(); for (const auto &dw : allDockWidgets) { if (Core::DockWidget *dockWidget = registry->dockByName(dw->uniqueName)) { bool doClose = true; if (dockWidget->skipsRestore()) { if (auto fw = dockWidget->floatingWindow()) { if (fw->allDockWidgetsHave(LayoutSaverOption::Skip)) { // All dock widgets in this floating window skips float, so we can honour it // for all. doClose = false; } } } if (doClose) names.push_back(dw->uniqueName); } } return names; } bool LayoutSaver::Layout::containsDockWidget(const QString &uniqueName) const { return std::find_if(allDockWidgets.cbegin(), allDockWidgets.cend(), [uniqueName](const std::shared_ptr &dock) { return dock->uniqueName == uniqueName; }) != allDockWidgets.cend(); } bool LayoutSaver::Group::isValid() const { if (isNull) return true; if (!geometry.isValid()) { KDDW_ERROR("Invalid geometry"); return false; } if (id.isEmpty()) { KDDW_ERROR("Invalid id"); return false; } if (!dockWidgets.isEmpty()) { if (currentTabIndex >= dockWidgets.size() || currentTabIndex < 0) { if (dockWidgets.isEmpty() || KDDockWidgets::Config::self().layoutSaverUsesStrictMode()) { KDDW_ERROR("Invalid tab index = {}, size = {}", currentTabIndex, dockWidgets.size()); return false; } // Layout seems corrupted. Use 0 as current tab and let's continue. KDDW_WARN("Invalid tab index = {}, size = {}", currentTabIndex, dockWidgets.size()); const_cast(this)->currentTabIndex = 0; } } for (auto &dw : dockWidgets) { if (!dw->isValid()) return false; } return true; } bool LayoutSaver::Group::hasSingleDockWidget() const { return dockWidgets.size() == 1; } bool LayoutSaver::Group::skipsRestore() const { return std::all_of(dockWidgets.cbegin(), dockWidgets.cend(), [](LayoutSaver::DockWidget::Ptr dw) { return dw->skipsRestore(); }); } LayoutSaver::DockWidget::Ptr LayoutSaver::Group::singleDockWidget() const { if (!hasSingleDockWidget()) return {}; return dockWidgets.first(); } bool LayoutSaver::DockWidget::isValid() const { return !uniqueName.isEmpty(); } void LayoutSaver::DockWidget::scaleSizes(const ScalingInfo &scalingInfo) { lastPosition.scaleSizes(scalingInfo); } bool LayoutSaver::DockWidget::skipsRestore() const { if (Core::DockWidget *dw = DockRegistry::self()->dockByName(uniqueName)) return dw->skipsRestore(); return false; } bool LayoutSaver::FloatingWindow::isValid() const { if (!multiSplitterLayout.isValid()) return false; if (!geometry.isValid()) { KDDW_ERROR("Invalid geometry"); return false; } return true; } bool LayoutSaver::FloatingWindow::hasSingleDockWidget() const { return multiSplitterLayout.hasSingleDockWidget(); } LayoutSaver::DockWidget::Ptr LayoutSaver::FloatingWindow::singleDockWidget() const { return multiSplitterLayout.singleDockWidget(); } bool LayoutSaver::FloatingWindow::skipsRestore() const { return multiSplitterLayout.skipsRestore(); } void LayoutSaver::FloatingWindow::scaleSizes(const ScalingInfo &scalingInfo) { scalingInfo.applyFactorsTo(/*by-ref*/ geometry); } bool LayoutSaver::MainWindow::isValid() const { return multiSplitterLayout.isValid(); } Vector LayoutSaver::MainWindow::dockWidgetsForSideBar(SideBarLocation loc) const { auto it = dockWidgetsPerSideBar.find(loc); return it == dockWidgetsPerSideBar.cend() ? Vector() : it->second; } void LayoutSaver::MainWindow::scaleSizes() { if (scalingInfo.isValid()) { // Doesn't happen, it's called only once assert(false); return; } scalingInfo = ScalingInfo(uniqueName, geometry, screenIndex); } bool LayoutSaver::MultiSplitter::isValid() const { return layout.is_object() && !layout.empty(); } bool LayoutSaver::MultiSplitter::hasSingleDockWidget() const { return groups.size() == 1 && groups.cbegin()->second.hasSingleDockWidget(); } LayoutSaver::DockWidget::Ptr LayoutSaver::MultiSplitter::singleDockWidget() const { if (!hasSingleDockWidget()) return {}; const auto &group = groups.begin()->second; return group.singleDockWidget(); } bool LayoutSaver::MultiSplitter::skipsRestore() const { return std::all_of(groups.cbegin(), groups.cend(), [](auto it) { return it.second.skipsRestore(); }); } void LayoutSaver::Position::scaleSizes(const ScalingInfo &scalingInfo) { scalingInfo.applyFactorsTo(/*by-ref*/ lastFloatingGeometry); } namespace { Core::Screen::Ptr screenForMainWindow(Core::MainWindow *mw) { return mw->view()->d->screen(); } } LayoutSaver::ScalingInfo::ScalingInfo(const QString &mainWindowId, Rect savedMainWindowGeo, int screenIndex) { auto mainWindow = DockRegistry::self()->mainWindowByName(mainWindowId); if (!mainWindow) { KDDW_ERROR("Failed to find main window with name {}", mainWindowId); return; } if (!savedMainWindowGeo.isValid() || savedMainWindowGeo.isNull()) { KDDW_ERROR("Invalid saved main window geometry {}", savedMainWindowGeo); return; } if (!mainWindow->geometry().isValid() || mainWindow->geometry().isNull()) { KDDW_ERROR("Invalid main window geometry {}", mainWindow->geometry()); return; } const int currentScreenIndex = Platform::instance()->screens().indexOf(screenForMainWindow(mainWindow)); this->mainWindowName = mainWindowId; this->savedMainWindowGeometry = savedMainWindowGeo; realMainWindowGeometry = mainWindow->window()->d->windowGeometry(); // window() as our main window might be embedded widthFactor = double(realMainWindowGeometry.width()) / savedMainWindowGeo.width(); heightFactor = double(realMainWindowGeometry.height()) / savedMainWindowGeo.height(); mainWindowChangedScreen = currentScreenIndex != screenIndex; } void LayoutSaver::ScalingInfo::translatePos(Point &pt) const { const int deltaX = pt.x() - savedMainWindowGeometry.x(); const int deltaY = pt.y() - savedMainWindowGeometry.y(); const double newDeltaX = deltaX * widthFactor; const double newDeltaY = deltaY * heightFactor; pt.setX(int(std::ceil(savedMainWindowGeometry.x() + newDeltaX))); pt.setY(int(std::ceil(savedMainWindowGeometry.y() + newDeltaY))); } void LayoutSaver::ScalingInfo::applyFactorsTo(Point &pt) const { translatePos(pt); } void LayoutSaver::ScalingInfo::applyFactorsTo(Size &sz) const { sz.setWidth(int(widthFactor * sz.width())); sz.setHeight(int(heightFactor * sz.height())); } void LayoutSaver::ScalingInfo::applyFactorsTo(Rect &rect) const { if (rect.isEmpty()) return; Point pos = rect.topLeft(); Size size = rect.size(); applyFactorsTo(/*by-ref*/ size); if (!mainWindowChangedScreen) { // Don't play with floating window position if the main window changed screen. // There's too many corner cases that push the floating windows out of bounds, and // we're not even considering monitors with different HDPI. We can support only the simple // case. For complex cases we'll try to guarantee the window is placed somewhere reasonable. applyFactorsTo(/*by-ref*/ pos); } rect.moveTopLeft(pos); rect.setSize(size); } LayoutSaver::Private::RAIIIsRestoring::RAIIIsRestoring() { LayoutSaver::Private::s_restoreInProgress = true; } LayoutSaver::Private::RAIIIsRestoring::~RAIIIsRestoring() { LayoutSaver::Private::s_restoreInProgress = false; }