KDDockWidgets API Documentation 2.0
Loading...
Searching...
No Matches
LayoutSaver.cpp
Go to the documentation of this file.
1/*
2 This file is part of KDDockWidgets.
3
4 SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
5 Author: SĂ©rgio Martins <sergio.martins@kdab.com>
6
7 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
8
9 Contact KDAB at <info@kdab.com> for commercial licensing options.
10*/
11
19#include "LayoutSaver.h"
20#include "Config.h"
21#include "core/ViewFactory.h"
22#include "core/LayoutSaver_p.h"
23#include "core/Logging_p.h"
24#include "core/Position_p.h"
25#include "core/Utils_p.h"
26#include "core/View_p.h"
27
28#include "core/DockRegistry.h"
29#include "core/Platform.h"
30#include "core/Layout.h"
31#include "core/Group.h"
32#include "core/FloatingWindow.h"
33#include "core/DockWidget.h"
34#include "core/DockWidget_p.h"
35#include "core/MainWindow.h"
36#include "core/nlohmann_helpers_p.h"
37#include "core/layouting/Item_p.h"
38
39#include <iostream>
40#include <fstream>
41#include <cmath>
42#include <utility>
43
64using namespace KDDockWidgets;
65using namespace KDDockWidgets::Core;
66
67std::map<QString, LayoutSaver::DockWidget::Ptr> LayoutSaver::DockWidget::s_dockWidgets;
68LayoutSaver::Layout *LayoutSaver::Layout::s_currentLayoutBeingRestored = nullptr;
69std::unordered_map<QString, std::shared_ptr<KDDockWidgets::Position>> LayoutSaver::Private::s_unrestoredPositions;
70std::unordered_map<QString, CloseReason> LayoutSaver::Private::s_unrestoredProperties;
71
72inline InternalRestoreOptions internalRestoreOptions(RestoreOptions options)
73{
74 InternalRestoreOptions ret = {};
75 if (options.testFlag(RestoreOption_RelativeToMainWindow)) {
76 ret.setFlag(InternalRestoreOption::SkipMainWindowGeometry);
77 ret.setFlag(InternalRestoreOption::RelativeFloatingWindowGeometry);
78 options.setFlag(RestoreOption_RelativeToMainWindow, false);
79 }
80 if (options.testFlag(RestoreOption_AbsoluteFloatingDockWindows)) {
81 ret.setFlag(InternalRestoreOption::RelativeFloatingWindowGeometry, false);
82 options.setFlag(RestoreOption_AbsoluteFloatingDockWindows, false);
83 }
84
85 if (options != RestoreOption_None) {
86 KDDW_ERROR("Unknown options={}", int(options));
87 }
88
89 return ret;
90}
91
92bool LayoutSaver::Private::s_restoreInProgress = false;
93
94namespace KDDockWidgets {
95
96template<typename T>
97T jsonValue(const nlohmann::json &json, const char *name, const T &defaultValue)
98{
99 try {
100 // value can throw if the type has "null" value, for instance.
101 return json.value(name, defaultValue);
102 } catch (...) {
103 return defaultValue;
104 }
105}
106
107template<typename Type>
108void to_json(nlohmann::json &json, const typename Type::List &list)
109{
110 for (const auto &l : list) {
111 json.push_back(l);
112 }
113}
114
115void to_json(nlohmann::json &json, const LayoutSaver::Group &f)
116{
117 json["id"] = f.id;
118 json["isNull"] = f.isNull;
119 json["objectName"] = f.objectName;
120 json["geometry"] = f.geometry;
121 json["options"] = f.options;
122 json["currentTabIndex"] = f.currentTabIndex;
123 json["mainWindowUniqueName"] = f.mainWindowUniqueName;
124 json["dockWidgets"] = dockWidgetNames(f.dockWidgets);
125}
126void from_json(const nlohmann::json &json, LayoutSaver::Group &f)
127{
128 f.id = jsonValue(json, "id", QString());
129 f.isNull = jsonValue(json, "isNull", true);
130 f.objectName = jsonValue(json, "objectName", QString());
131 f.geometry = jsonValue(json, "geometry", Rect());
132 f.options = jsonValue(json, "options", QFlags<FrameOption>::Int {});
133 f.currentTabIndex = jsonValue(json, "currentTabIndex", 0);
134 f.mainWindowUniqueName = jsonValue(json, "mainWindowUniqueName", QString());
135
136 auto it = json.find("dockWidgets");
137 if (it == json.end())
138 return;
139
140 auto &dockWidgets = *it;
141 f.dockWidgets.reserve(( int )dockWidgets.size());
142 for (const auto &d : dockWidgets) {
143 LayoutSaver::DockWidget::Ptr dw =
144 LayoutSaver::DockWidget::dockWidgetForName(d.get<QString>());
145 f.dockWidgets.push_back(dw);
146 }
147}
148
149void to_json(nlohmann::json &json, const LayoutSaver::MultiSplitter &s)
150{
151 json["layout"] = s.layout;
152 auto &groups = json["frames"];
153 for (const auto &it : s.groups) {
154 const auto &group = it.second;
155 groups[group.id.toStdString()] = group;
156 }
157}
158
159void from_json(const nlohmann::json &json, LayoutSaver::MultiSplitter &s)
160{
161 s.groups.clear();
162 s.layout = jsonValue(json, "layout", nlohmann::json::object());
163 auto it = json.find("frames");
164 if (it == json.end())
165 return;
166 if (it->is_null())
167 return;
168
169 auto &frms = *it;
170 if (!frms.is_object())
171 KDDW_ERROR("Unexpected not object");
172
173 for (const auto &kv : frms.items()) {
174 QString key = QString::fromStdString(kv.key());
175 auto group = kv.value().get<LayoutSaver::Group>();
176 s.groups[key] = group;
177 }
178}
179
180void to_json(nlohmann::json &json, const LayoutSaver::MainWindow &mw)
181{
182 json["options"] = int(mw.options);
183 json["multiSplitterLayout"] = mw.multiSplitterLayout;
184 json["uniqueName"] = mw.uniqueName;
185 json["geometry"] = mw.geometry;
186 json["normalGeometry"] = mw.normalGeometry;
187 json["screenIndex"] = mw.screenIndex;
188 json["screenSize"] = mw.screenSize;
189 json["isVisible"] = mw.isVisible;
190 json["affinities"] = mw.affinities;
191 json["windowState"] = mw.windowState;
192
195 const Vector<QString> dockWidgets = mw.dockWidgetsForSideBar(loc);
196 if (!dockWidgets.isEmpty()) {
197 std::string key = std::string("sidebar-") + std::to_string(( int )loc);
198 json[key] = dockWidgets;
199 }
200 }
201}
202
203void from_json(const nlohmann::json &json, LayoutSaver::MainWindow &mw)
204{
205 mw.options = static_cast<decltype(mw.options)>(jsonValue(json, "options", 0));
206 mw.multiSplitterLayout = jsonValue(json, "multiSplitterLayout", LayoutSaver::MultiSplitter());
207 mw.uniqueName = jsonValue(json, "uniqueName", QString());
208 mw.geometry = jsonValue(json, "geometry", Rect());
209 mw.normalGeometry = jsonValue(json, "normalGeometry", Rect());
210 mw.screenIndex = jsonValue(json, "screenIndex", 0);
211 mw.screenSize = jsonValue(json, "screenSize", Size(800, 600));
212 mw.isVisible = jsonValue(json, "isVisible", false);
213 mw.affinities = jsonValue(json, "affinities", Vector<QString>());
214 mw.windowState = ( WindowState )jsonValue(json, "windowState", 0);
215
216 // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities"
217 // list:
218 if (json.find("affinityName") != json.end()) {
219 QString affinityName = json["affinityName"].get<QString>();
220 if (!mw.affinities.contains(affinityName)) {
221 mw.affinities.push_back(affinityName);
222 }
223 }
224
227 std::string key = std::string("sidebar-") + std::to_string(( int )loc);
228 auto it = json.find(key);
229 if (it == json.end())
230 continue;
231 auto &val = *it;
232 if (val.is_array() && !val.empty()) {
233 mw.dockWidgetsPerSideBar[loc] = val.get<Vector<QString>>();
234 }
235 }
236}
237
238void to_json(nlohmann::json &json, const LayoutSaver::FloatingWindow &window)
239{
240 json["multiSplitterLayout"] = window.multiSplitterLayout;
241 json["parentIndex"] = window.parentIndex;
242 json["geometry"] = window.geometry;
243 json["normalGeometry"] = window.normalGeometry;
244 json["screenIndex"] = window.screenIndex;
245 json["screenSize"] = window.screenSize;
246 json["flags"] = window.flags;
247 json["isVisible"] = window.isVisible;
248 json["windowState"] = window.windowState;
249
250 if (!window.affinities.isEmpty()) {
251 json["affinities"] = window.affinities;
252 }
253}
254
255void from_json(const nlohmann::json &json, LayoutSaver::FloatingWindow &window)
256{
257 window.multiSplitterLayout = jsonValue(json, "multiSplitterLayout", LayoutSaver::MultiSplitter());
258 window.parentIndex = jsonValue(json, "parentIndex", -1);
259 window.geometry = jsonValue(json, "geometry", Rect());
260 window.normalGeometry = jsonValue(json, "normalGeometry", Rect());
261 window.screenIndex = jsonValue(json, "screenIndex", 0);
262 window.screenSize = jsonValue(json, "screenSize", Size(800, 600));
263 window.isVisible = jsonValue(json, "isVisible", false);
264 window.flags = jsonValue(json, "flags", int(FloatingWindowFlag::FromGlobalConfig));
265 window.windowState = ( WindowState )jsonValue(json, "windowState", 0);
266 window.affinities = jsonValue(json, "affinities", Vector<QString>());
267
268 // Compatibility hack. Old json format had a single "affinityName" instead of an "affinities"
269 // list:
270 const QString affinityName = jsonValue(json, "affinityName", QString());
271 if (!affinityName.isEmpty() && !window.affinities.contains(affinityName)) {
272 window.affinities.push_back(affinityName);
273 }
274}
275
276void to_json(nlohmann::json &json, const LayoutSaver::ScreenInfo &screenInfo)
277{
278 json["index"] = screenInfo.index;
279 json["geometry"] = screenInfo.geometry;
280 json["name"] = screenInfo.name;
281 json["devicePixelRatio"] = screenInfo.devicePixelRatio;
282}
283void from_json(const nlohmann::json &j, LayoutSaver::ScreenInfo &screenInfo)
284{
285 screenInfo.index = j.value("index", 0);
286 screenInfo.geometry = j.value("geometry", Rect());
287 screenInfo.name = j.value("name", QString());
288 screenInfo.devicePixelRatio = j.value("devicePixelRatio", 1.0);
289}
290
291void to_json(nlohmann::json &json, const LayoutSaver::Placeholder &placeHolder)
292{
293 json["isFloatingWindow"] = placeHolder.isFloatingWindow;
294 json["itemIndex"] = placeHolder.itemIndex;
295 if (placeHolder.isFloatingWindow)
296 json["indexOfFloatingWindow"] = placeHolder.indexOfFloatingWindow;
297 else
298 json["mainWindowUniqueName"] = placeHolder.mainWindowUniqueName;
299}
300
301void from_json(const nlohmann::json &json, LayoutSaver::Placeholder &placeHolder)
302{
303 placeHolder.isFloatingWindow = jsonValue(json, "isFloatingWindow", false);
304 placeHolder.itemIndex = jsonValue(json, "itemIndex", 0);
305 placeHolder.indexOfFloatingWindow = jsonValue(json, "indexOfFloatingWindow", -1);
306 placeHolder.mainWindowUniqueName = jsonValue(json, "mainWindowUniqueName", QString());
307}
308
309void to_json(nlohmann::json &json, const LayoutSaver::Position &pos)
310{
311 json["lastFloatingGeometry"] = pos.lastFloatingGeometry;
312 json["lastOverlayedGeometries"] = pos.lastOverlayedGeometries;
313 json["tabIndex"] = pos.tabIndex;
314 json["wasFloating"] = pos.wasFloating;
315 json["placeholders"] = pos.placeholders;
316}
317
318void from_json(const nlohmann::json &json, LayoutSaver::Position &pos)
319{
320 pos.lastFloatingGeometry = jsonValue(json, "lastFloatingGeometry", Rect());
321 pos.lastOverlayedGeometries = jsonValue(json, "lastOverlayedGeometries", std::unordered_map<KDDockWidgets::SideBarLocation, Rect>());
322 pos.tabIndex = jsonValue(json, "tabIndex", 0);
323 pos.wasFloating = jsonValue(json, "wasFloating", false);
324 pos.placeholders = jsonValue(json, "placeholders", LayoutSaver::Placeholder::List());
325}
326
327void to_json(nlohmann::json &json, const LayoutSaver::DockWidget &dw)
328{
329 if (!dw.affinities.isEmpty())
330 json["affinities"] = dw.affinities;
331 json["uniqueName"] = dw.uniqueName;
332 json["lastPosition"] = dw.lastPosition;
333 json["lastCloseReason"] = dw.lastCloseReason;
334}
335void from_json(const nlohmann::json &json, LayoutSaver::DockWidget &dw)
336{
337 auto it = json.find("affinities");
338 if (it != json.end())
339 dw.affinities = it->get<Vector<QString>>();
340
341 dw.uniqueName = jsonValue(json, "uniqueName", QString());
342 if (dw.uniqueName.isEmpty())
343 KDDW_ERROR("Unexpected no uniqueName for dockWidget");
344
345 dw.lastPosition = jsonValue(json, "lastPosition", LayoutSaver::Position());
346 dw.lastCloseReason = jsonValue(json, "lastCloseReason", CloseReason::Unspecified);
347}
348
349void to_json(nlohmann::json &json, const typename LayoutSaver::DockWidget::List &list)
350{
351 for (const auto &mw : list) {
352 json.push_back(*mw);
353 }
354}
355
356void from_json(const nlohmann::json &json, typename LayoutSaver::DockWidget::List &list)
357{
358 list.clear();
359 for (const auto &v : json) {
360 auto it = v.find("uniqueName");
361 if (it == v.end()) {
362 KDDW_ERROR("Unexpected no uniqueName");
363 continue;
364 }
365 QString uniqueName = it->get<QString>();
366 auto dw = LayoutSaver::DockWidget::dockWidgetForName(uniqueName);
367 from_json(v, *dw);
368 list.push_back(dw);
369 }
370}
371
372}
373
374LayoutSaver::LayoutSaver(RestoreOptions options)
375 : d(new Private(options))
376{
377 d->m_dockRegistry->registerLayoutSaver();
378}
379
381{
382 d->m_dockRegistry->unregisterLayoutSaver();
383 delete d;
384}
385
386bool LayoutSaver::saveToFile(const QString &jsonFilename)
387{
388 const QByteArray data = serializeLayout();
389
390 std::ofstream file(jsonFilename.toStdString(), std::ios::binary);
391 if (!file.is_open()) {
392 KDDW_ERROR("Failed to open {}", jsonFilename);
393 return false;
394 }
395
396 file.write(data.constData(), data.size());
397 file.close();
398 return true;
399}
400
401bool LayoutSaver::restoreFromFile(const QString &jsonFilename)
402{
403 bool ok = false;
404 const QByteArray data = Platform::instance()->readFile(jsonFilename, /*by-ref*/ ok);
405
406 if (!ok)
407 return false;
408
409 return restoreLayout(data);
410}
411
413{
414 if (!d->m_dockRegistry->isSane()) {
415 KDDW_ERROR("Refusing to serialize this layout. Check previous warnings.");
416 return {};
417 }
418
419 LayoutSaver::Layout layout;
420
421 // Just a simplification. One less type of windows to handle.
422 d->m_dockRegistry->ensureAllFloatingWidgetsAreMorphed();
423
424 const auto mainWindows = d->m_dockRegistry->mainwindows();
425 layout.mainWindows.reserve(mainWindows.size());
426 for (auto mainWindow : mainWindows) {
427 if (d->matchesAffinity(mainWindow->affinities()))
428 layout.mainWindows.push_back(mainWindow->serialize());
429 }
430
431 const Vector<Core::FloatingWindow *> floatingWindows =
432 d->m_dockRegistry->floatingWindows(/*includeBeingDeleted=*/false, /*honourSkipped=*/true);
433 layout.floatingWindows.reserve(floatingWindows.size());
434 for (Core::FloatingWindow *floatingWindow : floatingWindows) {
435 if (d->matchesAffinity(floatingWindow->affinities()))
436 layout.floatingWindows.push_back(floatingWindow->serialize());
437 }
438
439 // Closed dock widgets also have interesting things to save, like geometry and placeholder info
440 const Core::DockWidget::List closedDockWidgets = d->m_dockRegistry->closedDockwidgets(/*honourSkipped=*/true);
441 layout.closedDockWidgets.reserve(closedDockWidgets.size());
442 for (Core::DockWidget *dockWidget : closedDockWidgets) {
443 if (d->matchesAffinity(dockWidget->affinities()))
444 layout.closedDockWidgets.push_back(dockWidget->d->serialize());
445 }
446
447 // Save the placeholder info. We do it last, as we also restore it last, since we need all items
448 // to be created before restoring the placeholders
449
450 const Core::DockWidget::List dockWidgets = d->m_dockRegistry->dockwidgets();
451 layout.allDockWidgets.reserve(dockWidgets.size());
452 for (Core::DockWidget *dockWidget : dockWidgets) {
453 if (!dockWidget->skipsRestore() && d->matchesAffinity(dockWidget->affinities())) {
454 auto dw = dockWidget->d->serialize();
455 dw->lastPosition = dockWidget->d->lastPosition()->serialize();
456 layout.allDockWidgets.push_back(dw);
457 }
458 }
459
460 return layout.toJson();
461}
462
464{
465 LayoutSaver::DockWidget::s_dockWidgets.clear();
466 d->clearRestoredProperty();
467 if (data.isEmpty())
468 return true;
469
470 struct GroupCleanup
471 {
472 explicit GroupCleanup(LayoutSaver *saver)
473 : m_saver(saver)
474 {
475 }
476
477 ~GroupCleanup()
478 {
479 m_saver->d->deleteEmptyGroups();
480 }
481
482 GroupCleanup(const GroupCleanup &) = delete;
483 GroupCleanup &operator=(const GroupCleanup) = delete;
484
485 LayoutSaver *const m_saver;
486 };
487
488 GroupCleanup cleanup(this);
489 LayoutSaver::Layout layout;
490 if (!layout.fromJson(data)) {
491 KDDW_ERROR("Failed to parse json data");
492 return false;
493 }
494
495 if (!layout.isValid()) {
496 return false;
497 }
498
499 layout.scaleSizes(d->m_restoreOptions);
500
501 d->floatWidgetsWhichSkipRestore(layout.mainWindowNames());
502 d->floatUnknownWidgets(layout);
503
504 Private::RAIIIsRestoring isRestoring;
505
506 // Hide all dockwidgets and unparent them from any layout before starting restore
507 // We only close the stuff that the loaded JSON knows about. Unknown widgets might be newer.
508
509 d->m_dockRegistry->clear(d->m_dockRegistry->dockWidgets(layout.dockWidgetsToClose()),
510 d->m_dockRegistry->mainWindows(layout.mainWindowNames()),
511 d->m_affinityNames);
512
513 // 1. Restore main windows
514 for (const LayoutSaver::MainWindow &mw : std::as_const(layout.mainWindows)) {
515 auto mainWindow = d->m_dockRegistry->mainWindowByName(mw.uniqueName);
516 if (!mainWindow) {
517 if (auto mwFunc = Config::self().mainWindowFactoryFunc()) {
518 mainWindow = mwFunc(mw.uniqueName, mw.options);
519 } else {
520 KDDW_ERROR("Failed to restore layout create MainWindow with name {} first");
521 return false;
522 }
523 }
524
525 if (!d->matchesAffinity(mainWindow->affinities()))
526 continue;
527
528 if (!(d->m_restoreOptions & InternalRestoreOption::SkipMainWindowGeometry)) {
529 Window::Ptr window = mainWindow->view()->window();
530
531 if (window->windowState() == WindowState::Maximized && mw.windowState != WindowState::Maximized) {
532 // Never call deserializeWindowGeometry() on a maximized window.
533 // If window is maximized and we're restoring it to "normal", then 1st change state, then set geometry
534 window->setWindowState(mw.windowState);
535 d->deserializeWindowGeometry(mw, window);
536 } else if (mw.windowState != window->windowState()) {
537 // If window is normal and we're restoring it to maximized, then set geometry 1st
538 // so its normal geometry gets remembered, only then maximize
539
540 d->deserializeWindowGeometry(mw, window);
541 window->setWindowState(mw.windowState);
542 } else {
543 d->deserializeWindowGeometry(mw, window);
544 }
545 }
546
547 if (!mainWindow->deserialize(mw))
548 return false;
549 }
550
551 // 2. Restore FloatingWindows
552 for (LayoutSaver::FloatingWindow &fw : layout.floatingWindows) {
553 if (!d->matchesAffinity(fw.affinities) || fw.skipsRestore())
554 continue;
555
556 auto parent =
557 fw.parentIndex == -1 ? nullptr : DockRegistry::self()->mainwindows().at(fw.parentIndex);
558
559 auto flags = static_cast<FloatingWindowFlags>(fw.flags);
560 flags.setFlag(FloatingWindowFlag::StartsMinimized, int(fw.windowState) & int(WindowState::Minimized));
561
562 auto floatingWindow =
563 new Core::FloatingWindow({}, parent, flags);
564 fw.floatingWindowInstance = floatingWindow;
565 d->deserializeWindowGeometry(fw, floatingWindow->view()->window());
566 if (!floatingWindow->deserialize(fw)) {
567 KDDW_ERROR("Failed to deserialize floating window");
568 return false;
569 }
570 }
571
572 // 3. Restore closed dock widgets. They remain closed but acquire geometry and placeholder
573 // properties
574 for (const auto &dw : std::as_const(layout.closedDockWidgets)) {
575 if (d->matchesAffinity(dw->affinities)) {
577 }
578 }
579
580 LayoutSaver::Private::s_unrestoredPositions.clear();
581 LayoutSaver::Private::s_unrestoredProperties.clear();
582
583 // 4. Restore the placeholder info, now that the Items have been created
584 for (const auto &dw : std::as_const(layout.allDockWidgets)) {
585 if (!d->matchesAffinity(dw->affinities))
586 continue;
587
588 if (Core::DockWidget *dockWidget = d->m_dockRegistry->dockByName(
590 dockWidget->d->lastPosition()->deserialize(dw->lastPosition);
591 } else {
592 KDDW_INFO("Couldn't find dock widget {}", dw->uniqueName);
593 auto pos = std::make_shared<KDDockWidgets::Position>();
594 pos->deserialize(dw->lastPosition);
595 LayoutSaver::Private::s_unrestoredPositions[dw->uniqueName] = pos;
596 LayoutSaver::Private::s_unrestoredProperties[dw->uniqueName] = dw->lastCloseReason;
597 }
598 }
599
600 return true;
601}
602
604{
605 d->m_affinityNames = affinityNames;
606 if (affinityNames.contains(QString())) {
607 // Any window with empty affinity will also be subject to save/restore
608 d->m_affinityNames.push_back(QString());
609 }
610}
611
612LayoutSaver::Private *LayoutSaver::dptr() const
613{
614 return d;
615}
616
618{
619 const Core::DockWidget::List &allDockWidgets = DockRegistry::self()->dockwidgets();
621 result.reserve(allDockWidgets.size());
622 for (Core::DockWidget *dw : allDockWidgets) {
623 if (dw->d->m_wasRestored)
624 result.push_back(dw);
625 }
626
627 return result;
628}
629
630void LayoutSaver::Private::clearRestoredProperty()
631{
632 const Core::DockWidget::List &allDockWidgets = DockRegistry::self()->dockwidgets();
633 for (Core::DockWidget *dw : allDockWidgets) {
634 dw->d->m_wasRestored = false;
635 }
636}
637
638template<typename T>
639void LayoutSaver::Private::deserializeWindowGeometry(const T &saved, Window::Ptr window)
640{
641 // Not simply calling QWidget::setGeometry() here.
642 // For QtQuick we need to modify the QWindow's geometry.
643
644 Rect geometry = saved.geometry;
645 if (!isNormalWindowState(saved.windowState)) {
646 // The window will be maximized. We first set its geometry to normal
647 // Later it's maximized and will remember this value
648 geometry = saved.normalGeometry;
649 }
650
652
653 window->setGeometry(geometry);
654 window->setVisible(saved.isVisible);
655}
656
657LayoutSaver::Private::Private(RestoreOptions options)
658 : m_dockRegistry(DockRegistry::self())
659 , m_restoreOptions(internalRestoreOptions(options))
660{
661}
662
663/*static*/
664void LayoutSaver::Private::restorePendingPositions(Core::DockWidget *dw)
665{
666 if (!dw)
667 return;
668
669 {
670 auto it = s_unrestoredPositions.find(dw->uniqueName());
671 if (it != s_unrestoredPositions.end()) {
672 dw->d->m_lastPosition = it->second;
673 s_unrestoredPositions.erase(it);
674 }
675 }
676
677 {
678 auto it = s_unrestoredProperties.find(dw->uniqueName());
679 if (it != s_unrestoredProperties.end()) {
680 dw->d->m_lastCloseReason = it->second;
681 s_unrestoredProperties.erase(it);
682 }
683 }
684}
685
686bool LayoutSaver::Private::matchesAffinity(const Vector<QString> &affinities) const
687{
688 return m_affinityNames.isEmpty() || affinities.isEmpty()
689 || DockRegistry::self()->affinitiesMatch(m_affinityNames, affinities);
690}
691
692void LayoutSaver::Private::floatWidgetsWhichSkipRestore(const Vector<QString> &mainWindowNames)
693{
694 // Widgets with the LayoutSaverOptions::Skip flag skip restore completely.
695 // If they were visible before they need to remain visible now.
696 // If they were previously docked we need to float them, as the main window they were on will
697 // be loading a new layout.
698
699 const auto mainWindows = DockRegistry::self()->mainWindows(mainWindowNames);
700 for (auto mw : mainWindows) {
701 const Core::DockWidget::List docks = mw->layout()->dockWidgets();
702 for (auto dw : docks) {
703 if (dw->skipsRestore()) {
704 dw->setFloating(true);
705 }
706 }
707 }
708}
709
710void LayoutSaver::Private::floatUnknownWidgets(const LayoutSaver::Layout &layout)
711{
712 // An old *.json layout file might have not know about existing dock widgets
713 // When restoring such a file, we need to float any visible dock widgets which it doesn't know
714 // about so we can restore the MainWindow layout properly
715
716 const auto mainWindows = DockRegistry::self()->mainWindows(layout.mainWindowNames());
717 for (auto mw : mainWindows) {
718 const Core::DockWidget::List docks = mw->layout()->dockWidgets();
719 for (Core::DockWidget *dw : docks) {
720 if (!layout.containsDockWidget(dw->uniqueName())) {
721 dw->setFloating(true);
722 }
723 }
724 }
725}
726
727void LayoutSaver::Private::deleteEmptyGroups() const
728{
729 // After a restore it can happen that some DockWidgets didn't exist, so weren't restored.
730 // Delete their group now.
731
732 const auto groups = m_dockRegistry->groups();
733 for (auto group : groups) {
734 if (!group->beingDeletedLater() && group->isEmpty() && !group->isCentralGroup()) {
735 if (auto item = group->layoutItem()) {
736 item->turnIntoPlaceholder();
737 } else {
738 // This doesn't happen. But the warning will make the tests fail if there's a regression.
739 KDDW_ERROR("Expected item for group");
740 }
741 delete group;
742 }
743 }
744}
745
747{
748 return Private::s_restoreInProgress;
749}
750
751bool LayoutSaver::Layout::isValid() const
752{
753 if (serializationVersion != KDDOCKWIDGETS_SERIALIZATION_VERSION) {
754 KDDW_ERROR("Serialization format is too old {}, current={}", serializationVersion, KDDOCKWIDGETS_SERIALIZATION_VERSION);
755 return false;
756 }
757
758 for (auto &m : mainWindows) {
759 if (!m.isValid())
760 return false;
761 }
762
763 for (auto &m : floatingWindows) {
764 if (!m.isValid())
765 return false;
766 }
767
768 for (auto &m : allDockWidgets) {
769 if (!m->isValid())
770 return false;
771 }
772
773 return true;
774}
775
777{
778 bool ok = false;
779 const QByteArray data = Platform::instance()->readFile(jsonFilename, /*by-ref*/ ok);
780
781 if (!ok)
782 return {};
783
784 return openedDockWidgetsInLayout(data);
785}
786
788{
789 LayoutSaver::Layout layout;
790 if (!layout.fromJson(serialized))
791 return {};
792
793 Vector<QString> names;
794 names.reserve(layout.allDockWidgets.size()); // over-reserve so we have a single allocation
795
796 for (const auto &dock : std::as_const(layout.allDockWidgets)) {
797 auto it = std::find_if(layout.closedDockWidgets.cbegin(), layout.closedDockWidgets.cend(), [&dock](auto closedDock) {
798 return dock->uniqueName == closedDock->uniqueName;
799 });
800
801 const bool itsOpen = it == layout.closedDockWidgets.cend();
802 if (itsOpen)
803 names.push_back(dock->uniqueName);
804 }
805
806 return names;
807}
808
810{
811 bool ok = false;
812 const QByteArray data = Platform::instance()->readFile(jsonFilename, /*by-ref*/ ok);
813
814 if (!ok)
815 return {};
816
817 return sideBarDockWidgetsInLayout(data);
818}
819
821{
822 LayoutSaver::Layout layout;
823 if (!layout.fromJson(serialized))
824 return {};
825
826 Vector<QString> names;
827 names.reserve(layout.allDockWidgets.size()); // over-reserve so we have a single allocation
828
829 for (const auto &mainWindow : std::as_const(layout.mainWindows)) {
830 for (const auto &it : mainWindow.dockWidgetsPerSideBar) {
831 for (const auto &name : it.second)
832 names.push_back(name);
833 }
834 }
835
836 return names;
837}
838
839namespace KDDockWidgets {
840void to_json(nlohmann::json &j, const LayoutSaver::Layout &layout)
841{
842 j["serializationVersion"] = layout.serializationVersion;
843 j["mainWindows"] = layout.mainWindows;
844 j["floatingWindows"] = layout.floatingWindows;
845 j["closedDockWidgets"] = ::dockWidgetNames(layout.closedDockWidgets);
846 j["allDockWidgets"] = layout.allDockWidgets;
847 j["screenInfo"] = layout.screenInfo;
848}
849
850void from_json(const nlohmann::json &j, LayoutSaver::Layout &layout)
851{
852 layout.serializationVersion = j.value("serializationVersion", 0);
853 layout.mainWindows = j.value("mainWindows", LayoutSaver::MainWindow::List {});
854 layout.allDockWidgets = j.value("allDockWidgets", LayoutSaver::DockWidget::List {});
855
856 layout.closedDockWidgets.clear();
857
858 const auto closedDockWidgets = j.value("closedDockWidgets", Vector<QString>());
859 for (const QString &name : closedDockWidgets) {
860 layout.closedDockWidgets.push_back(
861 LayoutSaver::DockWidget::dockWidgetForName(name));
862 }
863
864 layout.floatingWindows = j.value("floatingWindows", LayoutSaver::FloatingWindow::List {});
865 layout.screenInfo = j.value("screenInfo", LayoutSaver::ScreenInfo::List {});
866}
867}
868
869QByteArray LayoutSaver::Layout::toJson() const
870{
871 nlohmann::json json = *this;
872 return QByteArray::fromStdString(json.dump(4));
873}
874
875bool LayoutSaver::Layout::fromJson(const QByteArray &jsonData)
876{
877 nlohmann::json json = nlohmann::json::parse(jsonData, nullptr, /*allow_exceptions=*/false);
878 if (json.is_discarded()) {
879 return false;
880 }
881
882 try {
883 from_json(json, *this);
884 } catch (const std::exception &e) {
885 KDDW_ERROR("LayoutSaver::Layout::fromJson: Caught exception: {}", e.what());
886 return false;
887 } catch (...) {
888 KDDW_ERROR("LayoutSaver::Layout::fromJson: Caught exception.");
889 return false;
890 }
891
892 return true;
893}
894
895void LayoutSaver::Layout::scaleSizes(InternalRestoreOptions options)
896{
897 if (mainWindows.isEmpty())
898 return;
899
900 const bool skipsMainWindowGeometry = options & InternalRestoreOption::SkipMainWindowGeometry;
901 if (!skipsMainWindowGeometry) {
902 // No scaling to do. All windows will be restored with the exact size specified in the
903 // saved JSON layouts.
904 return;
905 }
906
907 // We won't restore MainWindow's geometry, we use whatever the user has now, meaning
908 // we need to scale all dock widgets inside the layout, as the layout might not have
909 // the same size as specified in the saved JSON layout
910 for (auto &mw : mainWindows)
911 mw.scaleSizes();
912
913
914 // MainWindow has a different size than the one in JSON, so we also restore FloatingWindows
915 // relatively to the user set new MainWindow size
916 const bool useRelativeSizesForFloatingWidgets =
917 options & InternalRestoreOption::RelativeFloatingWindowGeometry;
918
919 if (useRelativeSizesForFloatingWidgets) {
920 for (auto &fw : floatingWindows) {
921 LayoutSaver::MainWindow mw = mainWindowForIndex(fw.parentIndex);
922 if (mw.scalingInfo.isValid())
923 fw.scaleSizes(mw.scalingInfo);
924 }
925 }
926
927 const ScalingInfo firstScalingInfo = mainWindows.constFirst().scalingInfo;
928 if (firstScalingInfo.isValid()) {
929 for (auto &dw : allDockWidgets) {
930 // TODO: Determine the best main window. This only interesting for closed dock
931 // widget geometry which was previously floating. But they still have some other
932 // main window as parent.
933 dw->scaleSizes(firstScalingInfo);
934 }
935 }
936}
937
938LayoutSaver::MainWindow LayoutSaver::Layout::mainWindowForIndex(int index) const
939{
940 if (index < 0 || index >= mainWindows.size())
941 return {};
942
943 return mainWindows.at(index);
944}
945
946LayoutSaver::FloatingWindow LayoutSaver::Layout::floatingWindowForIndex(int index) const
947{
948 if (index < 0 || index >= floatingWindows.size())
949 return {};
950
951 return floatingWindows.at(index);
952}
953
954Vector<QString> LayoutSaver::Layout::mainWindowNames() const
955{
956 Vector<QString> names;
957 names.reserve(mainWindows.size());
958 for (const auto &mw : mainWindows) {
959 names.push_back(mw.uniqueName);
960 }
961
962 return names;
963}
964
965Vector<QString> LayoutSaver::Layout::dockWidgetNames() const
966{
967 Vector<QString> names;
968 names.reserve(allDockWidgets.size());
969 for (const auto &dw : allDockWidgets) {
970 names.push_back(dw->uniqueName);
971 }
972
973 return names;
974}
975
976Vector<QString> LayoutSaver::Layout::dockWidgetsToClose() const
977{
978 // Before restoring a layout we close all dock widgets, unless they're a floating window with
979 // the DontCloseBeforeRestore flag
980
981 Vector<QString> names;
982 names.reserve(allDockWidgets.size());
983 auto registry = DockRegistry::self();
984 for (const auto &dw : allDockWidgets) {
985 if (Core::DockWidget *dockWidget = registry->dockByName(dw->uniqueName)) {
986
987 bool doClose = true;
988
989 if (dockWidget->skipsRestore()) {
990 if (auto fw = dockWidget->floatingWindow()) {
991 if (fw->allDockWidgetsHave(LayoutSaverOption::Skip)) {
992 // All dock widgets in this floating window skips float, so we can honour it
993 // for all.
994 doClose = false;
995 }
996 }
997 }
998
999 if (doClose)
1000 names.push_back(dw->uniqueName);
1001 }
1002 }
1003
1004 return names;
1005}
1006
1007bool LayoutSaver::Layout::containsDockWidget(const QString &uniqueName) const
1008{
1009 return std::find_if(allDockWidgets.cbegin(), allDockWidgets.cend(),
1010 [uniqueName](const std::shared_ptr<LayoutSaver::DockWidget> &dock) {
1011 return dock->uniqueName == uniqueName;
1012 })
1013 != allDockWidgets.cend();
1014}
1015
1016bool LayoutSaver::Group::isValid() const
1017{
1018 if (isNull)
1019 return true;
1020
1021 if (!geometry.isValid()) {
1022 KDDW_ERROR("Invalid geometry");
1023 return false;
1024 }
1025
1026 if (id.isEmpty()) {
1027 KDDW_ERROR("Invalid id");
1028 return false;
1029 }
1030
1031 if (!dockWidgets.isEmpty()) {
1032 if (currentTabIndex >= dockWidgets.size() || currentTabIndex < 0) {
1033
1034 if (dockWidgets.isEmpty() || KDDockWidgets::Config::self().layoutSaverUsesStrictMode()) {
1035 KDDW_ERROR("Invalid tab index = {}, size = {}", currentTabIndex, dockWidgets.size());
1036 return false;
1037 }
1038
1039 // Layout seems corrupted. Use 0 as current tab and let's continue.
1040 KDDW_WARN("Invalid tab index = {}, size = {}", currentTabIndex, dockWidgets.size());
1041 const_cast<LayoutSaver::Group *>(this)->currentTabIndex = 0;
1042 }
1043 }
1044
1045 for (auto &dw : dockWidgets) {
1046 if (!dw->isValid())
1047 return false;
1048 }
1049
1050 return true;
1051}
1052
1053bool LayoutSaver::Group::hasSingleDockWidget() const
1054{
1055 return dockWidgets.size() == 1;
1056}
1057
1058bool LayoutSaver::Group::skipsRestore() const
1059{
1060 return std::all_of(dockWidgets.cbegin(), dockWidgets.cend(),
1061 [](LayoutSaver::DockWidget::Ptr dw) { return dw->skipsRestore(); });
1062}
1063
1064LayoutSaver::DockWidget::Ptr LayoutSaver::Group::singleDockWidget() const
1065{
1066 if (!hasSingleDockWidget())
1067 return {};
1068
1069 return dockWidgets.first();
1070}
1071
1072bool LayoutSaver::DockWidget::isValid() const
1073{
1074 return !uniqueName.isEmpty();
1075}
1076
1077void LayoutSaver::DockWidget::scaleSizes(const ScalingInfo &scalingInfo)
1078{
1079 lastPosition.scaleSizes(scalingInfo);
1080}
1081
1082bool LayoutSaver::DockWidget::skipsRestore() const
1083{
1084 if (Core::DockWidget *dw = DockRegistry::self()->dockByName(uniqueName))
1085 return dw->skipsRestore();
1086
1087 return false;
1088}
1089
1090bool LayoutSaver::FloatingWindow::isValid() const
1091{
1092 if (!multiSplitterLayout.isValid())
1093 return false;
1094
1095 if (!geometry.isValid()) {
1096 KDDW_ERROR("Invalid geometry");
1097 return false;
1098 }
1099
1100 return true;
1101}
1102
1103bool LayoutSaver::FloatingWindow::hasSingleDockWidget() const
1104{
1105 return multiSplitterLayout.hasSingleDockWidget();
1106}
1107
1108LayoutSaver::DockWidget::Ptr LayoutSaver::FloatingWindow::singleDockWidget() const
1109{
1110 return multiSplitterLayout.singleDockWidget();
1111}
1112
1113bool LayoutSaver::FloatingWindow::skipsRestore() const
1114{
1115 return multiSplitterLayout.skipsRestore();
1116}
1117
1118void LayoutSaver::FloatingWindow::scaleSizes(const ScalingInfo &scalingInfo)
1119{
1120 scalingInfo.applyFactorsTo(/*by-ref*/ geometry);
1121}
1122
1123bool LayoutSaver::MainWindow::isValid() const
1124{
1125 return multiSplitterLayout.isValid();
1126}
1127
1128Vector<QString> LayoutSaver::MainWindow::dockWidgetsForSideBar(SideBarLocation loc) const
1129{
1130 auto it = dockWidgetsPerSideBar.find(loc);
1131 return it == dockWidgetsPerSideBar.cend() ? Vector<QString>() : it->second;
1132}
1133
1134void LayoutSaver::MainWindow::scaleSizes()
1135{
1136 if (scalingInfo.isValid()) {
1137 // Doesn't happen, it's called only once
1138 assert(false);
1139 return;
1140 }
1141
1142 scalingInfo = ScalingInfo(uniqueName, geometry, screenIndex);
1143}
1144
1145bool LayoutSaver::MultiSplitter::isValid() const
1146{
1147 return layout.is_object() && !layout.empty();
1148}
1149
1150bool LayoutSaver::MultiSplitter::hasSingleDockWidget() const
1151{
1152 return groups.size() == 1 && groups.cbegin()->second.hasSingleDockWidget();
1153}
1154
1155LayoutSaver::DockWidget::Ptr LayoutSaver::MultiSplitter::singleDockWidget() const
1156{
1157 if (!hasSingleDockWidget())
1158 return {};
1159
1160 const auto &group = groups.begin()->second;
1161 return group.singleDockWidget();
1162}
1163
1164bool LayoutSaver::MultiSplitter::skipsRestore() const
1165{
1166 return std::all_of(groups.cbegin(), groups.cend(),
1167 [](auto it) { return it.second.skipsRestore(); });
1168}
1169
1170void LayoutSaver::Position::scaleSizes(const ScalingInfo &scalingInfo)
1171{
1172 scalingInfo.applyFactorsTo(/*by-ref*/ lastFloatingGeometry);
1173}
1174
1175namespace {
1176
1177Core::Screen::Ptr screenForMainWindow(Core::MainWindow *mw)
1178{
1179 return mw->view()->d->screen();
1180}
1181
1182}
1183
1184LayoutSaver::ScalingInfo::ScalingInfo(const QString &mainWindowId, Rect savedMainWindowGeo,
1185 int screenIndex)
1186{
1187 auto mainWindow = DockRegistry::self()->mainWindowByName(mainWindowId);
1188 if (!mainWindow) {
1189 KDDW_ERROR("Failed to find main window with name {}", mainWindowId);
1190 return;
1191 }
1192
1193 if (!savedMainWindowGeo.isValid() || savedMainWindowGeo.isNull()) {
1194 KDDW_ERROR("Invalid saved main window geometry {}", savedMainWindowGeo);
1195 return;
1196 }
1197
1198 if (!mainWindow->geometry().isValid() || mainWindow->geometry().isNull()) {
1199 KDDW_ERROR("Invalid main window geometry {}", mainWindow->geometry());
1200 return;
1201 }
1202
1203 const int currentScreenIndex =
1204 Platform::instance()->screens().indexOf(screenForMainWindow(mainWindow));
1205
1206 this->mainWindowName = mainWindowId;
1207 this->savedMainWindowGeometry = savedMainWindowGeo;
1208 realMainWindowGeometry =
1209 mainWindow->window()->d->windowGeometry(); // window() as our main window might be embedded
1210 widthFactor = double(realMainWindowGeometry.width()) / savedMainWindowGeo.width();
1211 heightFactor = double(realMainWindowGeometry.height()) / savedMainWindowGeo.height();
1212 mainWindowChangedScreen = currentScreenIndex != screenIndex;
1213}
1214
1215void LayoutSaver::ScalingInfo::translatePos(Point &pt) const
1216{
1217 const int deltaX = pt.x() - savedMainWindowGeometry.x();
1218 const int deltaY = pt.y() - savedMainWindowGeometry.y();
1219
1220 const double newDeltaX = deltaX * widthFactor;
1221 const double newDeltaY = deltaY * heightFactor;
1222
1223 pt.setX(int(std::ceil(savedMainWindowGeometry.x() + newDeltaX)));
1224 pt.setY(int(std::ceil(savedMainWindowGeometry.y() + newDeltaY)));
1225}
1226
1227void LayoutSaver::ScalingInfo::applyFactorsTo(Point &pt) const
1228{
1229 translatePos(pt);
1230}
1231
1232void LayoutSaver::ScalingInfo::applyFactorsTo(Size &sz) const
1233{
1234 sz.setWidth(int(widthFactor * sz.width()));
1235 sz.setHeight(int(heightFactor * sz.height()));
1236}
1237
1238void LayoutSaver::ScalingInfo::applyFactorsTo(Rect &rect) const
1239{
1240 if (rect.isEmpty())
1241 return;
1242
1243 Point pos = rect.topLeft();
1244 Size size = rect.size();
1245
1246 applyFactorsTo(/*by-ref*/ size);
1247
1248
1249 if (!mainWindowChangedScreen) {
1250 // Don't play with floating window position if the main window changed screen.
1251 // There's too many corner cases that push the floating windows out of bounds, and
1252 // we're not even considering monitors with different HDPI. We can support only the simple
1253 // case. For complex cases we'll try to guarantee the window is placed somewhere reasonable.
1254 applyFactorsTo(/*by-ref*/ pos);
1255 }
1256
1257 rect.moveTopLeft(pos);
1258 rect.setSize(size);
1259}
1260
1261LayoutSaver::Private::RAIIIsRestoring::RAIIIsRestoring()
1262{
1263 LayoutSaver::Private::s_restoreInProgress = true;
1264}
1265
1266LayoutSaver::Private::RAIIIsRestoring::~RAIIIsRestoring()
1267{
1268 LayoutSaver::Private::s_restoreInProgress = false;
1269}
Application-wide config to tune certain behaviours of the framework.
InternalRestoreOptions internalRestoreOptions(RestoreOptions options)
Class to save and restore dockwidget layouts.
A widget that supports an arbitrary number of splitters (called Separators) in any combination of ver...
MainWindowFactoryFunc mainWindowFactoryFunc() const
Returns the MainWindowFactoryFunc. nullptr by default.
Definition Config.cpp:170
static Config & self()
returns the singleton Config instance
Definition Config.cpp:88
bool layoutSaverUsesStrictMode() const
Definition Config.cpp:429
View * view() const
Returns the view associated with this controller, if any.
The DockWidget base-class. DockWidget and Core::DockWidget are only split in two so we can share some...
bool setFloating(bool floats)
setter to make the dock widget float or dock.
QString uniqueName() const
the dock widget's unique name.
static DockWidget * deserialize(const std::shared_ptr< LayoutSaver::DockWidget > &)
Constructs a dock widget from its serialized form.
bool skipsRestore() const
Returns whether this widget has the LayoutSaverOption::Skip flag.
static void ensureRectIsOnScreen(Rect &geometry)
The MainWindow base-class. MainWindow and MainWindowBase are only split in two so we can share some c...
virtual QByteArray readFile(const QString &, bool &ok) const
static Platform * instance()
Returns the platform singleton.
virtual Vector< std::shared_ptr< Screen > > screens() const =0
Returns all available screens.
bool affinitiesMatch(const Vector< QString > &affinities1, const Vector< QString > &affinities2) const
Q_INVOKABLE KDDockWidgets::Core::MainWindow * mainWindowByName(const QString &) const
static DockRegistry * self()
Vector< Core::MainWindow * > mainWindows(const Vector< QString > &names)
overload returning only the ones with the specified names
Vector< Core::DockWidget * > dockwidgets() const
returns all DockWidget instances
Vector< Core::MainWindow * > mainwindows() const
returns all MainWindow instances
LayoutSaver allows to save or restore layouts.
Definition LayoutSaver.h:56
static Vector< QString > openedDockWidgetsInLayout(const QString &jsonFilename)
Returns the list of opened dock widgets in the specified layout.
static Vector< QString > sideBarDockWidgetsInLayout(const QString &jsonFilename)
static bool restoreInProgress()
returns whether a restore (restoreLayout) is in progress
void setAffinityNames(const Vector< QString > &affinityNames)
Sets the list of affinity names for which restore and save will be applied on. Allows to save/restore...
bool saveToFile(const QString &jsonFilename)
saves the layout to JSON file
QByteArray serializeLayout() const
saves the layout into a byte array
Vector< Core::DockWidget * > restoredDockWidgets() const
returns a list of dock widgets which were restored since the last restoreLayout() or restoreFromFile(...
bool restoreLayout(const QByteArray &)
restores the layout from a byte array All MainWindows and DockWidgets should have been created before...
bool restoreFromFile(const QString &jsonFilename)
restores the layout from a JSON file
LayoutSaver(RestoreOptions options=RestoreOption_None)
Constructor. Construction on the stack is suggested.
The DockWidget controller that's shared between QtWidgets and QtQuick frontends.
The MainWindow base-class that's shared between QtWidgets and QtQuick stack.
Class to abstract QAction, so code still works with QtQuick and Flutter.
@ RestoreOption_RelativeToMainWindow
@ RestoreOption_AbsoluteFloatingDockWindows
Skips scaling of floating dock windows relative to the main window.
SideBarLocation
Each main window supports 4 sidebars.
@ Skip
The dock widget won't participate in save/restore.
void from_json(const nlohmann::json &json, LayoutSaver::Group &f)
T jsonValue(const nlohmann::json &json, const char *name, const T &defaultValue)
void to_json(nlohmann::json &json, const typename Type::List &list)
const char * constData() const const
QByteArray fromStdString(const std::string &str)
bool isEmpty() const const
int size() const const
QString fromStdString(const std::string &str)
bool isEmpty() const const
void push_back(QChar ch)
std::string toStdString() const const

© Klarälvdalens Datakonsult AB (KDAB)
"The Qt, C++ and OpenGL Experts"
https://www.kdab.com/
KDDockWidgets
Advanced Dock Widget Framework for Qt
https://www.kdab.com/development-resources/qt-tools/kddockwidgets/
Generated by doxygen 1.9.8