KDDockWidgets API Documentation 2.0
Loading...
Searching...
No Matches
Item.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
12#include "Item_p.h"
13#include "ItemFreeContainer_p.h"
14#include "LayoutingHost_p.h"
15#include "LayoutingGuest_p.h"
16#include "LayoutingSeparator_p.h"
17
18#include "core/Logging_p.h"
19#include "core/ObjectGuard_p.h"
20#include "core/ScopedValueRollback_p.h"
21#include "core/nlohmann_helpers_p.h"
22
23#include <algorithm>
24#include <iostream>
25#include <cstdlib>
26#include <utility>
27
28#ifdef KDDW_FRONTEND_QT
29#include "core/Platform_p.h"
30#include <QTimer>
31#endif
32
33#define LAYOUT_DUMP_INDENT 6
34
35#ifdef Q_CC_MSVC
36#pragma warning(push)
37#pragma warning(disable : 4138)
38#pragma warning(disable : 4244)
39#pragma warning(disable : 4457)
40#pragma warning(disable : 4702)
41#endif
42
43// clazy:excludeall=missing-typeinfo,old-style-connect
44
45using namespace KDDockWidgets;
46using namespace KDDockWidgets::Core;
47
48int Core::Item::separatorThickness = 5;
49bool Core::Item::s_silenceSanityChecks = false;
50
51DumpScreenInfoFunc Core::Item::s_dumpScreenInfoFunc = nullptr;
52CreateSeparatorFunc Core::Item::s_createSeparatorFunc = nullptr;
53
54// There are the defaults. They can be changed by the user via Config.h API.
55Size Core::Item::hardcodedMinimumSize = Size(80, 90);
56Size Core::Item::hardcodedMaximumSize = Size(16777215, 16777215);
57
58bool Core::ItemBoxContainer::s_inhibitSimplify = false;
59LayoutingSeparator *LayoutingSeparator::s_separatorBeingDragged = nullptr;
60
62{
63 return loc == Location_OnTop || loc == Location_OnBottom;
64}
65
66inline bool locationIsSide1(Location loc)
67{
68 return loc == Location_OnLeft || loc == Location_OnTop;
69}
70
72{
73 switch (loc) {
74 case Location_OnLeft:
76 return Qt::Horizontal;
77 case Location_None:
78 case Location_OnTop:
80 return Qt::Vertical;
81 }
82
83 return Qt::Vertical;
84}
85
90
91inline Rect adjustedRect(Rect r, Qt::Orientation o, int p1, int p2)
92{
93 if (o == Qt::Vertical) {
94 r.adjust(0, p1, 0, p2);
95 } else {
96 r.adjust(p1, 0, p2, 0);
97 }
98
99 return r;
100}
101
102namespace KDDockWidgets::Core {
103struct LengthOnSide
104{
105 int length = 0;
106 int minLength = 0;
107
108 int available() const
109 {
110 return std::max(0, length - minLength);
111 }
112
113 int missing() const
114 {
115 return std::max(0, minLength - length);
116 }
117};
118
119}
120
121ItemBoxContainer *Item::root() const
122{
123 return m_parent ? m_parent->root()
124 : const_cast<ItemBoxContainer *>(object_cast<const ItemBoxContainer *>(this));
125}
126
127Rect Item::mapToRoot(Rect r) const
128{
129 const Point topLeft = mapToRoot(r.topLeft());
130 r.moveTopLeft(topLeft);
131 return r;
132}
133
134Point Item::mapToRoot(Point p) const
135{
136 if (isRoot())
137 return p;
138
139 return p + parentContainer()->mapToRoot(pos());
140}
141
142int Item::mapToRoot(int p, Qt::Orientation o) const
143{
144 if (o == Qt::Vertical)
145 return mapToRoot(Point(0, p)).y();
146 return mapToRoot(Point(p, 0)).x();
147}
148
149Point Item::mapFromRoot(Point p) const
150{
151 const Item *it = this;
152 while (it) {
153 p = p - it->pos();
154 it = it->parentContainer();
155 }
156
157 return p;
158}
159
160Rect Item::mapFromRoot(Rect r) const
161{
162 const Point topLeft = mapFromRoot(r.topLeft());
163 r.moveTopLeft(topLeft);
164 return r;
165}
166
167Point Item::mapFromParent(Point p) const
168{
169 if (isRoot())
170 return p;
171
172 return p - pos();
173}
174
175int Item::mapFromRoot(int p, Qt::Orientation o) const
176{
177 if (o == Qt::Vertical)
178 return mapFromRoot(Point(0, p)).y();
179 return mapFromRoot(Point(p, 0)).x();
180}
181
182void Item::setGuest(LayoutingGuest *guest)
183{
184 assert(!guest || !m_guest);
185
186 m_guest = guest;
187 m_parentChangedConnection.disconnect();
188 m_guestDestroyedConnection->disconnect();
189 m_layoutInvalidatedConnection->disconnect();
190
191 if (m_guest) {
192 m_guest->setHost(m_host);
193 m_guest->setLayoutItem(this);
194
195 m_parentChangedConnection = m_guest->hostChanged.connect([this](LayoutingHost *host) {
196 if (host != this->host()) {
197 // Group was detached into floating window. Turn into placeholder
198 assert(isVisible());
199 turnIntoPlaceholder();
200 }
201 });
202
203 {
204 ScopedValueRollback guard(m_isSettingGuest, true);
205 setMinSize(guest->minSize());
206 setMaxSizeHint(guest->maxSizeHint());
207 }
208
209 m_guestDestroyedConnection =
210 m_guest->beingDestroyed.connect(&Item::onGuestDestroyed, this);
211
212 m_layoutInvalidatedConnection =
213 guest->layoutInvalidated.connect(&Item::onWidgetLayoutRequested, this);
214
215 if (m_sizingInfo.geometry.isEmpty()) {
216 // Use the widgets geometry, but ensure it's at least hardcodedMinimumSize
217 Rect widgetGeo = m_guest->geometry();
218 widgetGeo.setSize(
219 widgetGeo.size().expandedTo(minSize()).expandedTo(Item::hardcodedMinimumSize));
220 setGeometry(mapFromRoot(widgetGeo));
221 } else {
222 updateWidgetGeometries();
223 }
224 }
225
226 updateObjectName();
227}
228
229void Item::updateWidgetGeometries()
230{
231 if (m_guest) {
232 m_guest->setGeometry(mapToRoot(rect()));
233 }
234}
235
236void Item::to_json(nlohmann::json &json) const
237{
238 json["sizingInfo"] = m_sizingInfo;
239 json["isVisible"] = m_isVisible;
240 json["isContainer"] = isContainer();
241 json["objectName"] = objectName();
242 if (m_guest)
243 json["guestId"] = m_guest->id(); // just for coorelation purposes when restoring
244}
245
246void Item::fillFromJson(const nlohmann::json &j,
247 const std::unordered_map<QString, LayoutingGuest *> &widgets)
248{
249 m_sizingInfo = j.value("sizingInfo", SizingInfo());
250 m_isVisible = j.value("isVisible", false);
251 setObjectName(j.value("objectName", QString()));
252 const QString guestId = j.value("guestId", QString());
253 if (!guestId.isEmpty()) {
254 auto it = widgets.find(guestId);
255 if (it != widgets.cend()) {
256 setGuest(it->second);
257 m_guest->setHost(host());
258 } else if (host()) {
259 KDDW_ERROR("Couldn't find group to restore for item={}", ( void * )this);
260 assert(false);
261 }
262 }
263}
264
265Item *Item::createFromJson(LayoutingHost *hostWidget, ItemContainer *parent, const nlohmann::json &json,
266 const std::unordered_map<QString, LayoutingGuest *> &widgets)
267{
268 auto item = new Item(hostWidget, parent);
269 item->fillFromJson(json, widgets);
270 return item;
271}
272
273void Item::setDumpScreenInfoFunc(DumpScreenInfoFunc f)
274{
275 s_dumpScreenInfoFunc = f;
276}
277
278void Item::setCreateSeparatorFunc(CreateSeparatorFunc f)
279{
280 s_createSeparatorFunc = f;
281}
282
283void Item::ref()
284{
285 m_refCount++;
286}
287
288void Item::unref()
289{
290 assert(m_refCount > 0);
291 m_refCount--;
292 if (m_refCount == 0) {
293 assert(!isRoot());
294 parentContainer()->removeItem(this);
295 }
296}
297
298int Item::refCount() const
299{
300 return m_refCount;
301}
302
303LayoutingHost *Item::host() const
304{
305 return m_host;
306}
307
308LayoutingGuest *Item::guest() const
309{
310 return m_guest;
311}
312
313void Item::restore(LayoutingGuest *guest)
314{
315 if (isVisible() || m_guest) {
316 KDDW_ERROR("Hitting assert. visible={}, guest={}", isVisible(), ( void * )this);
317 assert(false);
318 }
319
320 if (isContainer()) {
321 KDDW_ERROR("Containers can't be restored");
322 } else {
323 setGuest(guest);
324 parentContainer()->restore(this);
325
326 // When we restore to previous positions, we only still from the immediate neighbours.
327 // It's consistent with closing an item, it also only grows the immediate neighbours
328 // By passing ImmediateNeighboursFirst we can hide/show an item multiple times and it
329 // uses the same place
330 }
331}
332
333Vector<int> Item::pathFromRoot() const
334{
335 // Returns the list of indexes to get to this item, starting from the root container
336 // Example [0, 1, 3] would mean that the item is the 4th child of the 2nd child of the 1st child
337 // of root
338 // [] would mean 'this' is the root item
339 // [0] would mean the 1st child of root
340
341 Vector<int> path;
342 path.reserve(10); // random big number, good to bootstrap it
343
344 const Item *it = this;
345 while (it) {
346 if (auto p = it->parentContainer()) {
347 const auto index = p->indexOfChild(it);
348 path.prepend(index);
349 it = p;
350 } else {
351 break;
352 }
353 }
354
355 return path;
356}
357
358void Item::setHost(LayoutingHost *host)
359{
360 if (m_host != host) {
361 m_host = host;
362 if (m_guest) {
363 m_guest->setHost(host);
364 m_guest->setVisible(true);
365 updateWidgetGeometries();
366 }
367 }
368}
369
370void Item::setSize_recursive(Size newSize, ChildrenResizeStrategy)
371{
372 setSize(newSize);
373}
374
375Size Item::missingSize() const
376{
377 Size missing = minSize() - this->size();
378 missing.setWidth(std::max(missing.width(), 0));
379 missing.setHeight(std::max(missing.height(), 0));
380
381 return missing;
382}
383
384bool Item::isBeingInserted() const
385{
386 return m_sizingInfo.isBeingInserted;
387}
388
389void Item::setBeingInserted(bool is)
390{
391 m_sizingInfo.isBeingInserted = is;
392
393 // Trickle up the hierarchy too, as the parent might be hidden due to not having visible
394 // children
395 if (auto parent = parentContainer()) {
396 if (is) {
397 if (!parent->hasVisibleChildren())
398 parent->setBeingInserted(true);
399 } else {
400 parent->setBeingInserted(false);
401 }
402 }
403}
404
405void Item::setParentContainer(ItemContainer *parent)
406{
407 if (parent == m_parent)
408 return;
409
410 if (m_parent) {
411 m_minSizeChangedHandle.disconnect();
412 m_visibleChangedHandle.disconnect();
413 visibleChanged.emit(this, false);
414 }
415
416 if (auto c = asContainer()) {
417 const bool ceasingToBeRoot = !m_parent && parent;
418 if (ceasingToBeRoot && !c->hasVisibleChildren()) {
419 // Was root but is not root anymore. So, if empty, then it has an empty rect too.
420 // Only root can have a non-empty rect without having children
421 c->setGeometry({});
422 }
423 }
424
425 m_parent = parent;
426 connectParent(parent); // Reused by the ctor too
427
428 setParent(parent);
429}
430
431void Item::connectParent(ItemContainer *parent)
432{
433 if (parent) {
434 m_minSizeChangedHandle =
435 minSizeChanged.connect(&ItemContainer::onChildMinSizeChanged, parent);
436 m_visibleChangedHandle =
437 visibleChanged.connect(&ItemContainer::onChildVisibleChanged, parent);
438
439 // These virtuals are fine to be called from Item ctor, as the ItemContainer is still empty at this point
440 // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall)
441 setHost(parent->host());
442
443 // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall)
444 updateWidgetGeometries();
445
446 // NOLINTNEXTLINE(clang-analyzer-optin.cplusplus.VirtualCall)
447 visibleChanged.emit(this, isVisible());
448 }
449}
450
451ItemContainer *Item::parentContainer() const
452{
453 return m_parent;
454}
455
456ItemBoxContainer *Item::parentBoxContainer() const
457{
458 return object_cast<ItemBoxContainer *>(m_parent);
459}
460
461int Item::indexInAncestor(ItemContainer *ancestor) const
462{
463 auto it = this;
464 while (auto p = it->parentBoxContainer()) {
465 if (p == ancestor) {
466 // We found the ancestor
467 const auto children = ancestor->visibleChildren();
468 return children.indexOf(const_cast<Item *>(it));
469 }
470 it = p;
471 }
472
473 return -1;
474}
475
476ItemBoxContainer *Item::ancestorBoxContainerWithOrientation(Qt::Orientation o) const
477{
478 auto p = parentBoxContainer();
479 while (p) {
480 if (p->orientation() == o)
481 return p;
482 p = p->parentBoxContainer();
483 }
484
485 return nullptr;
486}
487
488const ItemContainer *Item::asContainer() const
489{
490 return object_cast<const ItemContainer *>(this);
491}
492
493ItemContainer *Item::asContainer()
494{
495 return object_cast<ItemContainer *>(this);
496}
497
498ItemBoxContainer *Item::asBoxContainer()
499{
500 return object_cast<ItemBoxContainer *>(this);
501}
502
503void Item::setMinSize(Size sz)
504{
505 if (sz != m_sizingInfo.minSize) {
506 m_sizingInfo.minSize = sz;
507 minSizeChanged.emit(this);
508 if (!m_isSettingGuest)
509 setSize_recursive(size().expandedTo(sz));
510 }
511}
512
513void Item::setMaxSizeHint(Size sz)
514{
515 if (sz != m_sizingInfo.maxSizeHint) {
516 m_sizingInfo.maxSizeHint = sz;
517 maxSizeChanged.emit(this);
518 }
519}
520
521Size Item::minSize() const
522{
523 return m_sizingInfo.minSize;
524}
525
526Size Item::maxSizeHint() const
527{
528 return m_sizingInfo.maxSizeHint.boundedTo(hardcodedMaximumSize);
529}
530
531void Item::setPos(Point pos)
532{
533 Rect geo = m_sizingInfo.geometry;
534 geo.moveTopLeft(pos);
535 setGeometry(geo);
536}
537
538void Item::setPos(int pos, Qt::Orientation o)
539{
540 if (o == Qt::Vertical) {
541 setPos({ x(), pos });
542 } else {
543 setPos({ pos, y() });
544 }
545}
546
547int Item::pos(Qt::Orientation o) const
548{
549 return o == Qt::Vertical ? y() : x();
550}
551
552int Item::x() const
553{
554 return m_sizingInfo.geometry.x();
555}
556
557int Item::y() const
558{
559 return m_sizingInfo.geometry.y();
560}
561
562int Item::width() const
563{
564 return m_sizingInfo.geometry.width();
565}
566
567int Item::height() const
568{
569 return m_sizingInfo.geometry.height();
570}
571
572Size Item::size() const
573{
574 return m_sizingInfo.geometry.size();
575}
576
577void Item::setSize(Size sz)
578{
579 Rect newGeo = m_sizingInfo.geometry;
580 newGeo.setSize(sz);
581 setGeometry(newGeo);
582}
583
584void Item::requestResize(int left, int top, int right, int bottom)
585{
586 if (left == 0 && right == 0 && top == 0 && bottom == 0)
587 return;
588
589 ItemBoxContainer *parent = parentBoxContainer();
590 if (!parent) {
591 // Can't happen
592 KDDW_ERROR("Item::requestResize: Could not find parent container");
593 return;
594 }
595
596 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
597 auto moveSeparators = [](int side1Delta, int side2Delta, LayoutingSeparator *separator1, LayoutingSeparator *separator2) {
598 if (side1Delta != 0 && separator1) {
599 const auto ancestor = separator1->parentContainer();
600 const int min = ancestor->minPosForSeparator_global(separator1);
601 const int pos = separator1->position();
602 const int max = ancestor->maxPosForSeparator_global(separator1);
603 int newPos = pos - side1Delta;
604 newPos = bound(min, newPos, max);
605 const int delta = newPos - pos;
606
607 ancestor->requestSeparatorMove(separator1, delta);
608 }
609
610 if (side2Delta != 0 && separator2) {
611 const auto ancestor = separator2->parentContainer();
612 const int min = ancestor->minPosForSeparator_global(separator2);
613 const int pos = separator2->position();
614 const int max = ancestor->maxPosForSeparator_global(separator2);
615 int newPos = pos + side2Delta;
616 newPos = bound(min, newPos, max);
617 const int delta = newPos - pos;
618
619 ancestor->requestSeparatorMove(separator2, delta);
620 }
621 };
622
623 {
624 // Here we handle resize along the orientation of the container
625 const int side1Delta = parent->isHorizontal() ? left : top;
626 const int side2Delta = parent->isHorizontal() ? right : bottom;
627 auto separator1 = parent->separatorForChild(this, Side1);
628 auto separator2 = parent->separatorForChild(this, Side2);
629 moveSeparators(side1Delta, side2Delta, separator1, separator2);
630 }
631
632 {
633 // Here we handle resize against the orientation of the container
634 const int side1Delta = parent->isHorizontal() ? top : left;
635 const int side2Delta = parent->isHorizontal() ? bottom : right;
636 auto separator1 = parent->adjacentSeparatorForChild(this, Side1);
637 auto separator2 = parent->adjacentSeparatorForChild(this, Side2);
638
639 if (separator2)
640 KDDW_WARN("SEP {} {}", separator2->position(), separator2->orientation());
641 moveSeparators(side1Delta, side2Delta, separator1, separator2);
642 }
643}
644
645Point Item::pos() const
646{
647 return m_sizingInfo.geometry.topLeft();
648}
649
650Rect Item::geometry() const
651{
652 return isBeingInserted() ? Rect() : m_sizingInfo.geometry;
653}
654
655Rect Item::rect() const
656{
657 return Rect(0, 0, width(), height());
658}
659
660bool Item::isContainer() const
661{
662 return m_isContainer;
663}
664
665int Item::minLength(Qt::Orientation o) const
666{
667 return Core::length(minSize(), o);
668}
669
670int Item::maxLengthHint(Qt::Orientation o) const
671{
672 return Core::length(maxSizeHint(), o);
673}
674
675void Item::setLength(int length, Qt::Orientation o)
676{
677 assert(length > 0);
678 if (o == Qt::Vertical) {
679 const int w = std::max(width(), hardcodedMinimumSize.width());
680 setSize(Size(w, length));
681 } else {
682 const int h = std::max(height(), hardcodedMinimumSize.height());
683 setSize(Size(length, h));
684 }
685}
686
687void Item::setLength_recursive(int length, Qt::Orientation o)
688{
689 setLength(length, o);
690}
691
692int Item::length(Qt::Orientation o) const
693{
694 return Core::length(size(), o);
695}
696
697int Item::availableLength(Qt::Orientation o) const
698{
699 return length(o) - minLength(o);
700}
701
702bool Item::isPlaceholder() const
703{
704 return !isVisible();
705}
706
707bool Item::isVisible(bool excludeBeingInserted) const
708{
709 return m_isVisible && !(excludeBeingInserted && isBeingInserted());
710}
711
712void Item::setIsVisible(bool is)
713{
714 if (is != m_isVisible) {
715 m_isVisible = is;
716 visibleChanged.emit(this, is);
717 }
718
719 if (is && m_guest) {
720 m_guest->setGeometry(mapToRoot(rect()));
721 m_guest->setVisible(true); // Only set visible when apply*() ?
722 }
723
724 updateObjectName();
725}
726
727void Item::setGeometry_recursive(Rect rect)
728{
729 // Recursiveness doesn't apply for non-container items
730 setGeometry(rect);
731}
732
733bool Item::checkSanity()
734{
735 if (!root())
736 return true;
737
738 if (minSize().width() > width() || minSize().height() > height()) {
739 root()->dumpLayout();
740 KDDW_ERROR("Size constraints not honoured this={}, min={}, size={}", ( void * )this, minSize(), size());
741 return false;
742 }
743
744 if (m_guest) {
745 if (m_guest->host() != host()) {
746 if (root())
747 root()->dumpLayout();
748 KDDW_ERROR("Unexpected host for our guest. m_guest->host()={}, host()={}",
749 ( void * )m_guest->host(), ( void * )host());
750 return false;
751 }
752
753#if 0 // if guest is explicitly hidden we're not hiding the item yet. And probably won't
754 if (!m_guest->isVisible() && (!m_guest->parent() || m_guest->parentWidget()->isVisible())) {
755
756 KDDW_ERROR("Guest widget isn't visible {}", this);
757 return false;
758 }
759#endif
760 if (m_guest->geometry() != mapToRoot(rect())) {
761 root()->dumpLayout();
762 KDDW_ERROR("Guest widget doesn't have correct geometry. m_guest->guestGeometry={}, item.mapToRoot(rect())={}", m_guest->geometry(), mapToRoot(rect()));
763 return false;
764 }
765 }
766
767 return true;
768}
769
770bool Item::isMDI() const
771{
772 return object_cast<ItemFreeContainer *>(parentContainer()) != nullptr;
773}
774
775void Item::setGeometry(Rect rect)
776{
777 Rect &m_geometry = m_sizingInfo.geometry;
778
779 if (rect != m_geometry) {
780 const Rect oldGeo = m_geometry;
781
782 m_geometry = rect;
783
784 if (rect.isEmpty()) {
785 // Just a sanity check...
786 ItemContainer *c = asContainer();
787 if (c) {
788 if (c->hasVisibleChildren()) {
789 if (auto r = root())
790 r->dumpLayout();
791 assert(false);
792 }
793 } else {
794 KDDW_ERROR("Empty rect");
795 }
796 }
797
798 const Size minSz = minSize();
799 if (!s_silenceSanityChecks
800 && (rect.width() < minSz.width() || rect.height() < minSz.height())) {
801 if (auto r = root())
802 r->dumpLayout();
803 KDDW_ERROR("Constraints not honoured. this={}, sz={}, min={}, parent={}", ( void * )this, rect.size(), minSz, ( void * )parentContainer());
804 }
805
806 geometryChanged.emit();
807
808 if (oldGeo.x() != x())
809 xChanged.emit();
810 if (oldGeo.y() != y())
811 yChanged.emit();
812 if (oldGeo.width() != width())
813 widthChanged.emit();
814 if (oldGeo.height() != height())
815 heightChanged.emit();
816
817 updateWidgetGeometries();
818 }
819}
820
821void Item::dumpLayout(int level, bool)
822{
823 std::string indent(LAYOUT_DUMP_INDENT * size_t(level), ' ');
824
825 std::cerr << indent << "- Widget: " << m_sizingInfo.geometry // << "r=" << m_geometry.right() << "b=" << m_geometry.bottom()
826 << "; min=" << minSize();
827
828 if (maxSizeHint() != hardcodedMaximumSize)
829 std::cerr << "; max=" << maxSizeHint() << "; ";
830
831 if (!isVisible())
832 std::cerr << ";hidden;";
833
834 if (m_guest && geometry() != m_guest->geometry()) {
835 std::cerr << "; guest geometry=" << m_guest->geometry();
836 }
837
838 if (m_sizingInfo.isBeingInserted)
839 std::cerr << ";beingInserted;";
840
841 std::cerr << "; guest=" << this << "; name=" << objectName().toStdString() << "\n";
842}
843
844Item::Item(LayoutingHost *hostWidget, ItemContainer *parent)
845 : Core::Object(parent)
846 , m_isContainer(false)
847 , m_parent(parent)
848 , m_host(hostWidget)
849{
850 connectParent(parent);
851}
852
853Item::Item(bool isContainer, LayoutingHost *hostWidget, ItemContainer *parent)
854 : Core::Object(parent)
855 , m_isContainer(isContainer)
856 , m_parent(parent)
857 , m_host(hostWidget)
858{
859 connectParent(parent);
860}
861
862Item::~Item()
863{
864 m_inDtor = true;
865 aboutToBeDeleted.emit();
866
867 m_minSizeChangedHandle.disconnect();
868 m_visibleChangedHandle.disconnect();
869 m_parentChangedConnection.disconnect();
870
871 deleted.emit();
872}
873
874void Item::turnIntoPlaceholder()
875{
876 assert(!isContainer());
877
878 // Turning into placeholder just means hiding it. So we can show it again in its original
879 // position. Call removeItem() so we share the code for making the neighbours grow into the
880 // space that becomes available after hiding this one
881 parentContainer()->removeItem(this, /*hardRemove=*/false);
882}
883
884void Item::updateObjectName()
885{
886 if (isContainer())
887 return;
888
889 if (auto w = guest()) {
890 setObjectName(w->debugName().isEmpty() ? QStringLiteral("widget") : w->debugName());
891 } else if (!isVisible()) {
892 setObjectName(QStringLiteral("hidden"));
893 } else if (!m_guest) {
894 setObjectName(QStringLiteral("null"));
895 } else {
896 setObjectName(QStringLiteral("empty"));
897 }
898}
899
900void Item::onGuestDestroyed()
901{
902 m_guest = nullptr;
903 m_parentChangedConnection.disconnect();
904 m_guestDestroyedConnection->disconnect();
905
906 if (m_refCount) {
907 turnIntoPlaceholder();
908 } else if (!isRoot()) {
909 parentContainer()->removeItem(this);
910 }
911}
912
913void Item::onWidgetLayoutRequested()
914{
915 if (auto w = guest()) {
916 const Size guestSize = w->geometry().size();
917 if (guestSize != size() && !isMDI()) { // for MDI we allow user/manual arbitrary resize with
918 // mouse
919 std::cerr << "Item::onWidgetLayoutRequested"
920 << "TODO: Not implemented yet. Widget can't just decide to resize yet"
921 << "View.size=" << guestSize << "Item.size=" << size() << m_sizingInfo.geometry
922 << m_sizingInfo.isBeingInserted << "\n";
923 }
924
925 if (w->minSize() != minSize()) {
926 setMinSize(m_guest->minSize());
927 }
928
929 setMaxSizeHint(w->maxSizeHint());
930 }
931}
932
933bool Item::isRoot() const
934{
935 return m_parent == nullptr;
936}
937
938LayoutBorderLocations Item::adjacentLayoutBorders() const
939{
940 if (isRoot()) {
941 return LayoutBorderLocation_All;
942 }
943
944 ItemBoxContainer *c = parentBoxContainer();
945 if (!c)
946 return LayoutBorderLocation_None;
947
948 const int indexInParent = c->indexOfVisibleChild(this);
949 const int numVisibleChildren = c->numVisibleChildren();
950 const bool isFirst = indexInParent == 0;
951 const bool isLast = indexInParent == numVisibleChildren - 1;
952 if (indexInParent == -1)
953 return LayoutBorderLocation_None;
954
955 LayoutBorderLocations locations = LayoutBorderLocation_None;
956 if (c->isRoot()) {
957 if (c->isVertical()) {
958 locations |= LayoutBorderLocation_West;
959 locations |= LayoutBorderLocation_East;
960
961 if (isFirst)
962 locations |= LayoutBorderLocation_North;
963 if (isLast)
964 locations |= LayoutBorderLocation_South;
965 } else {
966 locations |= LayoutBorderLocation_North;
967 locations |= LayoutBorderLocation_South;
968
969 if (isFirst)
970 locations |= LayoutBorderLocation_West;
971 if (isLast)
972 locations |= LayoutBorderLocation_East;
973 }
974 } else {
975 const LayoutBorderLocations parentBorders = c->adjacentLayoutBorders();
976 if (c->isVertical()) {
977 if (parentBorders & LayoutBorderLocation_West)
978 locations |= LayoutBorderLocation_West;
979
980 if (parentBorders & LayoutBorderLocation_East)
981 locations |= LayoutBorderLocation_East;
982
983 if (isFirst && (parentBorders & LayoutBorderLocation_North))
984 locations |= LayoutBorderLocation_North;
985
986 if (isLast && (parentBorders & LayoutBorderLocation_South))
987 locations |= LayoutBorderLocation_South;
988
989 } else {
990 if (parentBorders & LayoutBorderLocation_North)
991 locations |= LayoutBorderLocation_North;
992
993 if (parentBorders & LayoutBorderLocation_South)
994 locations |= LayoutBorderLocation_South;
995
996 if (isFirst && (parentBorders & LayoutBorderLocation_West))
997 locations |= LayoutBorderLocation_West;
998
999 if (isLast && (parentBorders & LayoutBorderLocation_East))
1000 locations |= LayoutBorderLocation_East;
1001 }
1002 }
1003
1004 return locations;
1005}
1006
1007int Item::visibleCount_recursive() const
1008{
1009 return isVisible() ? 1 : 0;
1010}
1011
1012struct ItemBoxContainer::Private
1013{
1014 explicit Private(ItemBoxContainer *qq)
1015 : q(qq)
1016 {
1017 if (!Item::s_createSeparatorFunc) {
1018 KDDW_ERROR("Item doesn't know how to create separators! Aborting.\n"
1019 "If you're using the layouting engine outside of KDDW, don't forget"
1020 " to call KDDockWidgets::Core::Item::createSeparatorFunc()");
1021 std::abort();
1022 }
1023 }
1024
1025 ~Private()
1026 {
1027 for (const auto &sep : std::as_const(m_separators))
1028 sep->free();
1029 m_separators.clear();
1030 }
1031
1032 int defaultLengthFor(Item *item, InitialOption option) const;
1033 void relayoutIfNeeded();
1034 const Item *itemFromPath(const Vector<int> &path) const;
1035 void resizeChildren(Size oldSize, Size newSize, SizingInfo::List &sizes,
1036 ChildrenResizeStrategy);
1037 void honourMaxSizes(SizingInfo::List &sizes);
1038 void scheduleCheckSanity() const;
1039 LayoutingSeparator *neighbourSeparator(const Item *item, Side,
1040 Qt::Orientation) const;
1041 LayoutingSeparator *neighbourSeparator_recursive(const Item *item, Side,
1042 Qt::Orientation) const;
1043 void updateWidgets_recursive();
1046 Vector<int> requiredSeparatorPositions() const;
1047 void updateSeparators();
1048 void deleteSeparators();
1049 LayoutingSeparator *separatorAt(int p) const;
1050 Vector<double> childPercentages() const;
1051 bool isDummy() const;
1052 void deleteSeparators_recursive();
1053 void updateSeparators_recursive();
1054 Size minSize(const Item::List &items) const;
1055 int excessLength() const;
1056
1057 mutable bool m_checkSanityScheduled = false;
1058 Vector<LayoutingSeparator *> m_separators;
1059 bool m_convertingItemToContainer = false;
1060 bool m_blockUpdatePercentages = false;
1061 bool m_isDeserializing = false;
1062 bool m_isSimplifying = false;
1063 Qt::Orientation m_orientation = Qt::Vertical;
1064 ItemBoxContainer *const q;
1065};
1066
1067ItemBoxContainer::ItemBoxContainer(LayoutingHost *hostWidget, ItemContainer *parent)
1068 : ItemContainer(hostWidget, parent)
1069 , d(new Private(this))
1070{
1071 assert(parent);
1072}
1073
1074ItemBoxContainer::ItemBoxContainer(LayoutingHost *hostWidget)
1075 : ItemContainer(hostWidget, /*parent=*/nullptr)
1076 , d(new Private(this))
1077{
1078}
1079
1080ItemBoxContainer::~ItemBoxContainer()
1081{
1082 delete d;
1083}
1084
1085int ItemBoxContainer::numSideBySide_recursive(Qt::Orientation o) const
1086{
1087 int num = 0;
1088 if (d->m_orientation == o) {
1089 // Example: Container is horizontal and we want to know how many layouted horizontally
1090 for (Item *child : m_children) {
1091 if (ItemBoxContainer *container = child->asBoxContainer()) {
1092 num += container->numSideBySide_recursive(o);
1093 } else if (!child->isPlaceholder()) {
1094 num++;
1095 }
1096 }
1097 } else {
1098 // Example: Container is vertical and we want to know how many layouted horizontally
1099 for (Item *child : m_children) {
1100 if (ItemBoxContainer *container = child->asBoxContainer()) {
1101 num = std::max(num, container->numSideBySide_recursive(o));
1102 } else if (!child->isPlaceholder()) {
1103 num = std::max(num, 1);
1104 }
1105 }
1106 }
1107
1108 return num;
1109}
1110
1111bool ItemBoxContainer::checkSanity()
1112{
1113 d->m_checkSanityScheduled = false;
1114
1115#ifdef KDDW_FRONTEND_QT
1116 auto plat = Platform::instance();
1117 if (!plat || plat->d->inDestruction()) {
1118 // checkSanity() can be called with deleteLater(), so check if we still
1119 // have a platform
1120 return true;
1121 }
1122#endif
1123
1124 if (!host()) {
1126 return true;
1127 }
1128
1129 if (!Item::checkSanity())
1130 return false;
1131
1132 if (numChildren() == 0 && !isRoot()) {
1133 KDDW_ERROR("Container is empty. Should be deleted");
1134 return false;
1135 }
1136
1137 if (d->m_orientation != Qt::Vertical && d->m_orientation != Qt::Horizontal) {
1138 KDDW_ERROR("Invalid orientation={}, this={}", d->m_orientation, ( void * )this);
1139 return false;
1140 }
1141
1142 // Check that the geometries don't overlap
1143 int expectedPos = 0;
1144 const auto children = childItems();
1145 for (Item *item : children) {
1146 if (!item->isVisible())
1147 continue;
1148 const int pos = Core::pos(item->pos(), d->m_orientation);
1149 if (expectedPos != pos) {
1150 root()->dumpLayout();
1151 KDDW_ERROR("Unexpected pos={}, expected={}, item={}, isContainer={}", pos, expectedPos, ( void * )item,
1152 item->isContainer());
1153 return false;
1154 }
1155
1156 expectedPos = pos + Core::length(item->size(), d->m_orientation) + separatorThickness;
1157 }
1158
1159 const int h1 = Core::length(size(), oppositeOrientation(d->m_orientation));
1160 for (Item *item : children) {
1161 if (item->parentContainer() != this) {
1162 KDDW_ERROR("Invalid parent container for item={}, is={}, expected={}", ( void * )item, ( void * )item->parentContainer(), ( void * )this);
1163 return false;
1164 }
1165
1166 if (item->parent() != this) {
1167 KDDW_ERROR("Invalid Object parent for item={}, is={}, expected={}", ( void * )item, ( void * )item->parent(), ( void * )this);
1168 return false;
1169 }
1170
1171 if (item->isVisible()) {
1172 // Check the children height (if horizontal, and vice-versa)
1173 const int h2 = Core::length(item->size(), oppositeOrientation(d->m_orientation));
1174 if (h1 != h2) {
1175 root()->dumpLayout();
1176 KDDW_ERROR("Invalid size for item {}, Container.length={}, item.length={}", ( void * )item, h1, h2);
1177 return false;
1178 }
1179
1180 if (!rect().contains(item->geometry())) {
1181 root()->dumpLayout();
1182 KDDW_ERROR("Item geo is out of bounds. item={}, geo={}, parent.rect={}", ( void * )item, item->geometry(), rect());
1183 return false;
1184 }
1185 }
1186
1187 if (!item->checkSanity())
1188 return false;
1189 }
1190
1191 const Item::List visibleChildren = this->visibleChildren();
1192 const bool isEmptyRoot = isRoot() && visibleChildren.isEmpty();
1193 if (!isEmptyRoot) {
1194 auto occupied = std::max(0, Item::separatorThickness * (int(visibleChildren.size()) - 1));
1195 for (Item *item : visibleChildren) {
1196 occupied += item->length(d->m_orientation);
1197 }
1198
1199 if (occupied != length()) {
1200 root()->dumpLayout();
1201 KDDW_ERROR("Unexpected length. Expected={}, got={}, this={}", occupied, length(), ( void * )this);
1202 return false;
1203 }
1204
1205 const Vector<double> percentages = d->childPercentages();
1206 const double totalPercentage = std::accumulate(percentages.begin(), percentages.end(), 0.0);
1207 const double expectedPercentage = visibleChildren.isEmpty() ? 0.0 : 1.0;
1208 if (!fuzzyCompare(totalPercentage, expectedPercentage)) {
1209 root()->dumpLayout();
1210 KDDW_ERROR("Percentages don't add up", totalPercentage, percentages, ( void * )this);
1211 const_cast<ItemBoxContainer *>(this)->d->updateSeparators_recursive();
1212 KDDW_ERROR("percentages={}", d->childPercentages());
1213 return false;
1214 }
1215 }
1216
1217 const auto numVisibleChildren = int(visibleChildren.size());
1218 if (d->m_separators.size() != std::max(0, numVisibleChildren - 1)) {
1219 root()->dumpLayout();
1220 KDDW_ERROR("Unexpected number of separators sz={}, numVisibleChildren={}", d->m_separators.size(), numVisibleChildren);
1221 return false;
1222 }
1223
1224 const Size expectedSeparatorSize = isVertical() ? Size(width(), Item::separatorThickness)
1225 : Size(Item::separatorThickness, height());
1226
1227 const int pos2 = Core::pos(mapToRoot(Point(0, 0)), oppositeOrientation(d->m_orientation));
1228
1229 for (int i = 0; i < d->m_separators.size(); ++i) {
1230 LayoutingSeparator *separator = d->m_separators.at(i);
1231 Item *item = visibleChildren.at(i);
1232 const int expectedSeparatorPos =
1233 mapToRoot(item->m_sizingInfo.edge(d->m_orientation) + 1, d->m_orientation);
1234
1235 if (separator->m_host != host()) {
1236 KDDW_ERROR("Invalid host widget for separator this={}", ( void * )this);
1237 return false;
1238 }
1239
1240 if (separator->parentContainer() != this) {
1241 KDDW_ERROR("Invalid parent container for separator parent={}, separator={}, this={}", ( void * )separator->parentContainer(), ( void * )separator, ( void * )this);
1242 return false;
1243 }
1244
1245 if (separator->position() != expectedSeparatorPos) {
1246 root()->dumpLayout();
1247 KDDW_ERROR("Unexpected separator position, expected={}, separator={}, this={}", separator->position(), expectedSeparatorPos, ( void * )separator, ( void * )this);
1248 return false;
1249 }
1250 const Rect separatorGeometry = separator->geometry();
1251 if (separatorGeometry.size() != expectedSeparatorSize) {
1252 KDDW_ERROR("Unexpected separator size={}, expected={}, separator={}, this={}", separatorGeometry.size(), expectedSeparatorSize, ( void * )separator, ( void * )this);
1253 return false;
1254 }
1255
1256 const int separatorPos2 = Core::pos(separatorGeometry.topLeft(),
1257 oppositeOrientation(d->m_orientation));
1258 if (Core::pos(separatorGeometry.topLeft(),
1259 oppositeOrientation(d->m_orientation))
1260 != pos2) {
1261 root()->dumpLayout();
1262 KDDW_ERROR("Unexpected position pos2={}, expected={}, separator={}, this={}", separatorPos2, pos2, ( void * )separator, ( void * )this);
1263 return false;
1264 }
1265
1266 // Check that the separator bounds are correct. We can't always honour widget's max-size
1267 // constraints, so only honour min-size
1268 const int separatorMinPos = minPosForSeparator_global(separator, /*honourMax=*/false);
1269 const int separatorMaxPos = maxPosForSeparator_global(separator, /*honourMax=*/false);
1270 const int separatorPos = separator->position();
1271 if (separatorPos < separatorMinPos || separatorPos > separatorMaxPos || separatorMinPos < 0
1272 || separatorMaxPos <= 0) {
1273 root()->dumpLayout();
1274 KDDW_ERROR("Invalid bounds for separator, pos={}, min={}, max={}, separator={}", separatorPos, separatorMinPos, separatorMaxPos, ( void * )separator);
1275 return false;
1276 }
1277 }
1278
1279#ifdef DOCKS_DEVELOPER_MODE
1280 // Can cause slowdown, so just use it in developer mode.
1281 if (isRoot()) {
1282 if (!asBoxContainer()->test_suggestedRect())
1283 return false;
1284 }
1285#endif
1286
1287 return true;
1288}
1289
1290void ItemBoxContainer::Private::scheduleCheckSanity() const
1291{
1292#ifdef KDDW_FRONTEND_QT
1293 if (!m_checkSanityScheduled) {
1294 m_checkSanityScheduled = true;
1295 QTimer::singleShot(0, q->root(), &ItemBoxContainer::checkSanity);
1296 }
1297#endif
1298}
1299
1300bool ItemBoxContainer::hasOrientation() const
1301{
1302 return isVertical() || isHorizontal();
1303}
1304
1305int ItemBoxContainer::indexOfVisibleChild(const Item *item) const
1306{
1307 const Item::List items = visibleChildren();
1308 return items.indexOf(const_cast<Item *>(item));
1309}
1310
1311void ItemBoxContainer::restore(Item *child)
1312{
1313 restoreChild(child, NeighbourSqueezeStrategy::ImmediateNeighboursFirst);
1314}
1315
1316void ItemBoxContainer::removeItem(Item *item, bool hardRemove)
1317{
1318 assert(!item->isRoot());
1319
1320 if (!contains(item)) {
1321 if (item->parentContainer() == this) {
1322 // This should never happen, but we've seen infinite recursion here before
1323 KDDW_ERROR("ItemBoxContainer::removeItem: Could not find item as children, but it has us as parent!");
1324 assert(false); // crash early during debug
1325 return;
1326 }
1327
1328 // Not ours, ask parent
1329 item->parentContainer()->removeItem(item, hardRemove);
1330 return;
1331 }
1332
1333 Item *side1Item = visibleNeighbourFor(item, Side1);
1334 Item *side2Item = visibleNeighbourFor(item, Side2);
1335
1336 const bool isContainer = item->isContainer();
1337 const bool wasVisible = !isContainer && item->isVisible();
1338
1339 if (hardRemove) {
1340 m_children.removeOne(item);
1341 delete item;
1342 if (!isContainer)
1343 root()->numItemsChanged.emit();
1344 } else {
1345 item->setIsVisible(false);
1346 item->setGuest(nullptr);
1347
1348 if (!wasVisible && !isContainer) {
1349 // Was already hidden
1350 return;
1351 }
1352 }
1353
1354 if (wasVisible) {
1355 root()->numVisibleItemsChanged.emit(root()->numVisibleChildren());
1356 }
1357
1358 if (isEmpty()) {
1359 // Empty container is useless, delete it
1360 if (auto p = parentContainer())
1361 p->removeItem(this, /*hardRemove=*/true);
1362 } else if (!hasVisibleChildren()) {
1363 if (auto p = parentContainer()) {
1364 p->removeItem(this, /*hardRemove=*/false);
1365 setGeometry(Rect());
1366 }
1367 } else {
1368 // Neighbours will occupy the space of the deleted item
1369 growNeighbours(side1Item, side2Item);
1370 itemsChanged.emit();
1371
1372 updateSizeConstraints();
1373 d->updateSeparators_recursive();
1374 }
1375}
1376
1377void ItemBoxContainer::setGeometry_recursive(Rect rect)
1378{
1379 setPos(rect.topLeft());
1380
1381 // Call resize, which is recursive and will resize the children too
1382 setSize_recursive(rect.size());
1383}
1384
1385ItemBoxContainer *ItemBoxContainer::convertChildToContainer(Item *leaf)
1386{
1387 ScopedValueRollback converting(d->m_convertingItemToContainer, true);
1388
1389 const auto index = m_children.indexOf(leaf);
1390 assert(index != -1);
1391 auto container = new ItemBoxContainer(host(), this);
1392 container->setParentContainer(nullptr);
1393 container->setParentContainer(this);
1394
1395 insertItem(container, index, DefaultSizeMode::NoDefaultSizeMode);
1396 m_children.removeOne(leaf);
1397 container->setGeometry(leaf->geometry());
1398 container->insertItem(leaf, Location_OnTop, DefaultSizeMode::NoDefaultSizeMode);
1399 itemsChanged.emit();
1400 d->updateSeparators_recursive();
1401
1402 return container;
1403}
1404
1406// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1407void ItemBoxContainer::insertItemRelativeTo(Item *item, Item *relativeTo, Location loc,
1409{
1410 assert(item != relativeTo);
1411
1412 if (auto asContainer = relativeTo->asBoxContainer()) {
1413 asContainer->insertItem(item, loc, option);
1414 return;
1415 }
1416
1417 item->setIsVisible(!option.startsHidden());
1418 assert(!(option.startsHidden() && item->isContainer()));
1419
1420 ItemBoxContainer *parent = relativeTo->parentBoxContainer();
1421 if (!parent) {
1422 KDDW_ERROR("This method should only be called for box containers parent={}", ( void * )item->parent());
1423 return;
1424 }
1425
1426 if (parent->hasOrientationFor(loc)) {
1427 const bool locIsSide1 = locationIsSide1(loc);
1428 auto indexInParent = parent->indexOfChild(relativeTo);
1429 if (!locIsSide1)
1430 indexInParent++;
1431
1432 const Qt::Orientation orientation = orientationForLocation(loc);
1433 if (orientation != parent->orientation()) {
1434 assert(parent->visibleChildren().size() == 1);
1435 // This is the case where the container only has one item, so it's both vertical and
1436 // horizontal Now its orientation gets defined
1437 parent->setOrientation(orientation);
1438 }
1439
1440 parent->insertItem(item, indexInParent, option);
1441 } else {
1442 ItemBoxContainer *container = parent->convertChildToContainer(relativeTo);
1443 container->insertItem(item, loc, option);
1444 }
1445}
1446
1447void ItemBoxContainer::insertItem(Item *item, Location loc,
1448 KDDockWidgets::InitialOption initialOption)
1449{
1450 assert(item != this);
1451 if (contains(item)) {
1452 KDDW_ERROR("Item already exists");
1453 return;
1454 }
1455
1456 item->setIsVisible(!initialOption.startsHidden());
1457 assert(!(initialOption.startsHidden() && item->isContainer()));
1458
1459 const Qt::Orientation locOrientation = orientationForLocation(loc);
1460
1461 if (hasOrientationFor(loc)) {
1462 if (m_children.size() == 1) {
1463 // 2 items is the minimum to know which orientation we're layedout
1464 d->m_orientation = locOrientation;
1465 }
1466
1467 const auto index = locationIsSide1(loc) ? 0 : m_children.size();
1468 insertItem(item, index, initialOption);
1469 } else {
1470 // Inserting directly in a container ? Only if it's root.
1471 assert(isRoot());
1472 auto container = new ItemBoxContainer(host(), this);
1473 container->setGeometry(rect());
1474 container->setChildren(m_children, d->m_orientation);
1475 m_children.clear();
1476 setOrientation(oppositeOrientation(d->m_orientation));
1477 insertItem(container, 0, DefaultSizeMode::NoDefaultSizeMode);
1478
1479 // Now we have the correct orientation, we can insert
1480 insertItem(item, loc, initialOption);
1481
1482 if (!container->hasVisibleChildren())
1483 container->setGeometry(Rect());
1484 }
1485
1486 d->updateSeparators_recursive();
1487 d->scheduleCheckSanity();
1488}
1489
1490void ItemBoxContainer::onChildMinSizeChanged(Item *child)
1491{
1492 if (d->m_convertingItemToContainer || d->m_isDeserializing || !child->isVisible()) {
1493 // Don't bother our parents, we're converting
1494 return;
1495 }
1496
1497 updateSizeConstraints();
1498
1499 if (child->isBeingInserted())
1500 return;
1501
1502 if (numVisibleChildren() == 1 && child->isVisible()) {
1503 // The easy case. Child is alone in the layout, occupies everything.
1504 child->setGeometry(rect());
1505 updateChildPercentages();
1506 return;
1507 }
1508
1509 const Size missingForChild = child->missingSize();
1510 if (!missingForChild.isNull()) {
1511 // Child has some growing to do. It will grow left and right equally, (and top-bottom), as
1512 // needed.
1513 growItem(child, Core::length(missingForChild, d->m_orientation),
1514 GrowthStrategy::BothSidesEqually, NeighbourSqueezeStrategy::AllNeighbours);
1515 }
1516
1517 updateChildPercentages();
1518}
1519
1520void ItemBoxContainer::updateSizeConstraints()
1521{
1522 const Size missingSize = this->missingSize();
1523 if (!missingSize.isNull()) {
1524 if (isRoot()) {
1525 // Resize the whole layout
1526 setSize_recursive(size() + missingSize);
1527 }
1528 }
1529
1530 // Our min-size changed, notify our parent, and so on until it reaches root()
1531 minSizeChanged.emit(this);
1532}
1533
1534void ItemBoxContainer::onChildVisibleChanged(Item *, bool visible)
1535{
1536 if (d->m_isDeserializing || isInSimplify())
1537 return;
1538
1539 const int numVisible = numVisibleChildren();
1540 if (visible && numVisible == 1) {
1541 // Child became visible and there's only 1 visible child. Meaning there were 0 visible
1542 // before.
1543 visibleChanged.emit(this, true);
1544 } else if (!visible && numVisible == 0) {
1545 visibleChanged.emit(this, false);
1546 }
1547}
1548
1549Rect ItemBoxContainer::suggestedDropRect(const Item *item, const Item *relativeTo,
1550 Location loc) const
1551{
1552 // Returns the drop rect. This is the geometry used by the rubber band when you hover over an
1553 // indicator. It's calculated by copying the layout and inserting the item into the
1554 // dummy/invisible copy The we see which geometry the item got. This way the returned geometry
1555 // is always what the item will get if you drop it. One exception is if the window doesn't have
1556 // enough space and it would grow. In this case we fall back to something reasonable
1557
1558
1559 if (relativeTo && !relativeTo->parentContainer()) {
1560 KDDW_ERROR("No parent container");
1561 return {};
1562 }
1563
1564 if (relativeTo && relativeTo->parentContainer() != this) {
1565 KDDW_ERROR("Called on the wrong container");
1566 return {};
1567 }
1568
1569 if (relativeTo && !relativeTo->isVisible()) {
1570 KDDW_ERROR("relative to isn't visible");
1571 return {};
1572 }
1573
1574 if (loc == Location_None) {
1575 KDDW_ERROR("Invalid location");
1576 return {};
1577 }
1578
1579 const Size availableSize = root()->availableSize();
1580 const Size minSize = item->minSize();
1581 const bool isEmpty = !root()->hasVisibleChildren();
1582 const int extraWidth = (isEmpty || locationIsVertical(loc)) ? 0 : Item::separatorThickness;
1583 const int extraHeight = (isEmpty || !locationIsVertical(loc)) ? 0 : Item::separatorThickness;
1584 const bool windowNeedsGrowing = availableSize.width() < minSize.width() + extraWidth
1585 || availableSize.height() < minSize.height() + extraHeight;
1586
1587 if (windowNeedsGrowing)
1588 return suggestedDropRectFallback(item, relativeTo, loc);
1589
1590 nlohmann::json rootSerialized;
1591 root()->to_json(rootSerialized);
1592
1593 ItemBoxContainer rootCopy(nullptr);
1594 rootCopy.fillFromJson(rootSerialized, {});
1595
1596 if (relativeTo)
1597 relativeTo = rootCopy.d->itemFromPath(relativeTo->pathFromRoot());
1598
1599 nlohmann::json itemSerialized;
1600 item->to_json(itemSerialized);
1601 auto itemCopy = new Item(nullptr);
1602 itemCopy->fillFromJson(itemSerialized, {});
1603
1604 if (relativeTo) {
1605 auto r = const_cast<Item *>(relativeTo);
1606 ItemBoxContainer::insertItemRelativeTo(itemCopy, r, loc, DefaultSizeMode::FairButFloor);
1607 } else {
1608 rootCopy.insertItem(itemCopy, loc, DefaultSizeMode::FairButFloor);
1609 }
1610
1611 if (rootCopy.size() != root()->size()) {
1612 // Doesn't happen
1613 KDDW_ERROR("The root copy grew ?! copy={}, sz={}, loc={}", rootCopy.size(), root()->size(), loc);
1614 return suggestedDropRectFallback(item, relativeTo, loc);
1615 }
1616
1617 return itemCopy->mapToRoot(itemCopy->rect());
1618}
1619
1620// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
1621Rect ItemBoxContainer::suggestedDropRectFallback(const Item *item, const Item *relativeTo,
1622 Location loc) const
1623{
1624 const Size minSize = item->minSize();
1625 const int itemMin = Core::length(minSize, d->m_orientation);
1626 const int available = availableLength() - Item::separatorThickness;
1627 if (relativeTo) {
1628 int suggestedPos = 0;
1629 const Rect relativeToGeo = relativeTo->geometry();
1630 const int suggestedLength = relativeTo->length(orientationForLocation(loc)) / 2;
1631 switch (loc) {
1632 case Location_OnLeft:
1633 suggestedPos = relativeToGeo.x();
1634 break;
1635 case Location_OnTop:
1636 suggestedPos = relativeToGeo.y();
1637 break;
1638 case Location_OnRight:
1639 suggestedPos = relativeToGeo.right() - suggestedLength + 1;
1640 break;
1641 case Location_OnBottom:
1642 suggestedPos = relativeToGeo.bottom() - suggestedLength + 1;
1643 break;
1644 default:
1645 assert(false);
1646 }
1647
1648 Rect rect;
1650 rect.setTopLeft(Point(relativeTo->x(), suggestedPos));
1651 rect.setSize(Size(relativeTo->width(), suggestedLength));
1652 } else {
1653 rect.setTopLeft(Point(suggestedPos, relativeTo->y()));
1654 rect.setSize(Size(suggestedLength, relativeTo->height()));
1655 }
1656
1657 return mapToRoot(rect);
1658 } else if (isRoot()) {
1659 // Relative to the window itself
1660 Rect rect = this->rect();
1661 const int oneThird = length() / 3;
1662 const int suggestedLength = std::max(std::min(available, oneThird), itemMin);
1663
1664 switch (loc) {
1665 case Location_OnLeft:
1666 rect.setWidth(suggestedLength);
1667 break;
1668 case Location_OnTop:
1669 rect.setHeight(suggestedLength);
1670 break;
1671 case Location_OnRight:
1672 rect.adjust(rect.width() - suggestedLength, 0, 0, 0);
1673 break;
1674 case Location_OnBottom:
1675 rect.adjust(0, rect.bottom() - suggestedLength, 0, 0);
1676 break;
1677 case Location_None:
1678 return {};
1679 }
1680
1681 return rect;
1682
1683 } else {
1684 KDDW_ERROR("Shouldn't happen");
1685 }
1686
1687 return {};
1688}
1689
1690void ItemBoxContainer::positionItems()
1691{
1692 SizingInfo::List sizes = this->sizes();
1693 positionItems(/*by-ref=*/sizes);
1694 applyPositions(sizes);
1695
1696 d->updateSeparators_recursive();
1697}
1698
1699void ItemBoxContainer::positionItems_recursive()
1700{
1701 positionItems();
1702 for (Item *item : std::as_const(m_children)) {
1703 if (item->isVisible()) {
1704 if (auto c = item->asBoxContainer())
1705 c->positionItems_recursive();
1706 }
1707 }
1708}
1709
1710void ItemBoxContainer::applyPositions(const SizingInfo::List &sizes)
1711{
1712 const Item::List items = visibleChildren();
1713 const auto count = items.size();
1714 assert(count == sizes.size());
1715 for (int i = 0; i < count; ++i) {
1716 Item *item = items.at(i);
1717 const SizingInfo &sizing = sizes[i];
1718 if (sizing.isBeingInserted) {
1719 continue;
1720 }
1721
1723 // If the layout is horizontal, the item will have the height of the container. And
1724 // vice-versa
1725 item->setLength_recursive(sizing.length(oppositeOrientation), oppositeOrientation);
1726
1727 item->setPos(sizing.geometry.topLeft());
1728 }
1729}
1730
1731Qt::Orientation ItemBoxContainer::orientation() const
1732{
1733 return d->m_orientation;
1734}
1735
1736void ItemBoxContainer::positionItems(SizingInfo::List &sizes)
1737{
1738 int nextPos = 0;
1739 const auto count = sizes.count();
1741 for (auto i = 0; i < count; ++i) {
1742 SizingInfo &sizing = sizes[i];
1743 if (sizing.isBeingInserted) {
1744 nextPos += Item::separatorThickness;
1745 continue;
1746 }
1747
1748 // If the layout is horizontal, the item will have the height of the container. And
1749 // vice-versa
1750 const int oppositeLength = Core::length(size(), oppositeOrientation);
1751 sizing.setLength(oppositeLength, oppositeOrientation);
1752 sizing.setPos(0, oppositeOrientation);
1753
1754 sizing.setPos(nextPos, d->m_orientation);
1755 nextPos += sizing.length(d->m_orientation) + Item::separatorThickness;
1756 }
1757}
1758
1759void ItemBoxContainer::clear()
1760{
1761 for (Item *item : std::as_const(m_children)) {
1762 if (ItemBoxContainer *container = item->asBoxContainer())
1763 container->clear();
1764
1765 delete item;
1766 }
1767 m_children.clear();
1768 d->deleteSeparators();
1769}
1770
1771Item *ItemBoxContainer::itemAt(Point p) const
1772{
1773 for (Item *item : std::as_const(m_children)) {
1774 if (item->isVisible() && item->geometry().contains(p))
1775 return item;
1776 }
1777
1778 return nullptr;
1779}
1780
1781Item *ItemBoxContainer::itemAt_recursive(Point p) const
1782{
1783 if (Item *item = itemAt(p)) {
1784 if (auto c = item->asBoxContainer()) {
1785 return c->itemAt_recursive(c->mapFromParent(p));
1786 } else {
1787 return item;
1788 }
1789 }
1790
1791 return nullptr;
1792}
1793
1794void ItemBoxContainer::setHost(LayoutingHost *host)
1795{
1796 Item::setHost(host);
1797 d->deleteSeparators_recursive();
1798 for (Item *item : std::as_const(m_children)) {
1799 item->setHost(host);
1800 }
1801
1802 d->updateSeparators_recursive();
1803}
1804
1805void ItemBoxContainer::setIsVisible(bool)
1806{
1807 // no-op for containers, visibility is calculated
1808}
1809
1810bool ItemBoxContainer::isVisible(bool excludeBeingInserted) const
1811{
1812 return hasVisibleChildren(excludeBeingInserted);
1813}
1814
1815void ItemBoxContainer::setLength_recursive(int length, Qt::Orientation o)
1816{
1817 Size sz = size();
1818 if (o == Qt::Vertical) {
1819 sz.setHeight(length);
1820 } else {
1821 sz.setWidth(length);
1822 }
1823
1824 setSize_recursive(sz);
1825}
1826
1827void ItemBoxContainer::insertItem(Item *item, int index, InitialOption option)
1828{
1829 if (option.sizeMode != DefaultSizeMode::NoDefaultSizeMode) {
1831 const int suggestedLength = d->defaultLengthFor(item, option);
1832 item->setLength_recursive(suggestedLength, d->m_orientation);
1833 }
1834
1835 m_children.insert(index, item);
1836 item->setParentContainer(this);
1837
1838 itemsChanged.emit();
1839
1840 if (!d->m_convertingItemToContainer && item->isVisible())
1841 restoreChild(item);
1842
1843 const bool shouldEmitVisibleChanged = item->isVisible();
1844
1845 if (!d->m_convertingItemToContainer && !s_inhibitSimplify)
1846 simplify();
1847
1848 if (shouldEmitVisibleChanged)
1849 root()->numVisibleItemsChanged.emit(root()->numVisibleChildren());
1850 root()->numItemsChanged.emit();
1851}
1852
1853bool ItemBoxContainer::hasOrientationFor(Location loc) const
1854{
1855 if (m_children.size() <= 1)
1856 return true;
1857
1858 return d->m_orientation == orientationForLocation(loc);
1859}
1860
1861int ItemBoxContainer::usableLength() const
1862{
1863 const Item::List children = visibleChildren();
1864 const auto numVisibleChildren = children.size();
1865
1866 if (children.size() <= 1)
1867 return Core::length(size(), d->m_orientation);
1868
1869 const int separatorWaste = separatorThickness * (numVisibleChildren - 1);
1870 return length() - separatorWaste;
1871}
1872
1873void ItemBoxContainer::setChildren(const List &children, Qt::Orientation o)
1874{
1875 m_children = children;
1876 for (Item *item : children)
1877 item->setParentContainer(this);
1878
1879 setOrientation(o);
1880}
1881
1882void ItemBoxContainer::setOrientation(Qt::Orientation o)
1883{
1884 if (o != d->m_orientation) {
1885 d->m_orientation = o;
1886 d->updateSeparators_recursive();
1887 }
1888}
1889
1890Size ItemBoxContainer::Private::minSize(const Item::List &items) const
1891{
1892 int minW = 0;
1893 int minH = 0;
1894 int numVisible = 0;
1895 if (!q->m_children.isEmpty()) {
1896 for (Item *item : items) {
1897 if (!(item->isVisible() || item->isBeingInserted()))
1898 continue;
1899 numVisible++;
1900 if (q->isVertical()) {
1901 minW = std::max(minW, item->minSize().width());
1902 minH += item->minSize().height();
1903 } else {
1904 minH = std::max(minH, item->minSize().height());
1905 minW += item->minSize().width();
1906 }
1907 }
1908
1909 const int separatorWaste = std::max(0, (numVisible - 1) * separatorThickness);
1910 if (q->isVertical())
1911 minH += separatorWaste;
1912 else
1913 minW += separatorWaste;
1914 }
1915
1916 return Size(minW, minH);
1917}
1918
1919Size ItemBoxContainer::minSize() const
1920{
1921 return d->minSize(m_children);
1922}
1923
1924Size ItemBoxContainer::maxSizeHint() const
1925{
1926 int maxW = isVertical() ? hardcodedMaximumSize.width() : 0;
1927 int maxH = isVertical() ? 0 : hardcodedMaximumSize.height();
1928
1929 const Item::List visibleChildren = this->visibleChildren(/*includeBeingInserted=*/false);
1930 if (!visibleChildren.isEmpty()) {
1931 for (Item *item : visibleChildren) {
1932 if (item->isBeingInserted())
1933 continue;
1934 const Size itemMaxSz = item->maxSizeHint();
1935 const int itemMaxWidth = itemMaxSz.width();
1936 const int itemMaxHeight = itemMaxSz.height();
1937 if (isVertical()) {
1938 maxW = std::min(maxW, itemMaxWidth);
1939 maxH = std::min(maxH + itemMaxHeight, hardcodedMaximumSize.height());
1940 } else {
1941 maxH = std::min(maxH, itemMaxHeight);
1942 maxW = std::min(maxW + itemMaxWidth, hardcodedMaximumSize.width());
1943 }
1944 }
1945
1946 const auto separatorWaste = (int(visibleChildren.size()) - 1) * separatorThickness;
1947 if (isVertical()) {
1948 maxH = std::min(maxH + separatorWaste, hardcodedMaximumSize.height());
1949 } else {
1950 maxW = std::min(maxW + separatorWaste, hardcodedMaximumSize.width());
1951 }
1952 }
1953
1954 if (maxW == 0)
1955 maxW = hardcodedMaximumSize.width();
1956
1957 if (maxH == 0)
1958 maxH = hardcodedMaximumSize.height();
1959
1960 return Size(maxW, maxH).expandedTo(d->minSize(visibleChildren));
1961}
1962
1963void ItemBoxContainer::Private::resizeChildren(Size oldSize, Size newSize,
1964 SizingInfo::List &childSizes,
1965 ChildrenResizeStrategy strategy)
1966{
1967 // This container is being resized to @p newSize, so we must resize our children too, based
1968 // on @p strategy.
1969 // The new sizes are applied to @p childSizes, which will be applied to the widgets when we're
1970 // done
1971
1972 const Vector<double> childPercentages = this->childPercentages();
1973 const auto count = childSizes.count();
1974 const bool widthChanged = oldSize.width() != newSize.width();
1975 const bool heightChanged = oldSize.height() != newSize.height();
1976 const bool lengthChanged =
1977 (q->isVertical() && heightChanged) || (q->isHorizontal() && widthChanged);
1978 const int totalNewLength = q->usableLength();
1979
1980 if (strategy == ChildrenResizeStrategy::Percentage) {
1981 // In this strategy mode, each children will preserve its current relative size. So, if a
1982 // child is occupying 50% of this container, then it will still occupy that after the
1983 // container resize
1984
1985 int remaining = totalNewLength;
1986 for (int i = 0; i < count; ++i) {
1987 const bool isLast = i == count - 1;
1988
1989 SizingInfo &itemSize = childSizes[i];
1990
1991 const double childPercentage = childPercentages.at(i);
1992 const int newItemLength = lengthChanged
1993 ? (isLast ? remaining : int(childPercentage * totalNewLength))
1994 : itemSize.length(m_orientation);
1995
1996 if (newItemLength <= 0) {
1997 q->root()->dumpLayout();
1998 KDDW_ERROR("Invalid resize newItemLength={}", newItemLength);
1999 assert(false);
2000 return;
2001 }
2002
2003 remaining = remaining - newItemLength;
2004
2005 if (q->isVertical()) {
2006 itemSize.geometry.setSize({ q->width(), newItemLength });
2007 } else {
2008 itemSize.geometry.setSize({ newItemLength, q->height() });
2009 }
2010 }
2011 } else if (strategy == ChildrenResizeStrategy::Side1SeparatorMove
2012 || strategy == ChildrenResizeStrategy::Side2SeparatorMove) {
2013 int remaining = Core::length(
2014 newSize - oldSize,
2015 m_orientation); // This is how much we need to give to children (when growing the
2016 // container), or to take from them when shrinking the container
2017 const bool isGrowing = remaining > 0;
2018 remaining = std::abs(remaining); // Easier to deal in positive numbers
2019
2020 // We're resizing the container, and need to decide if we start resizing the 1st children or
2021 // in reverse order. If the separator is being dragged left or top, then
2022 // isSide1SeparatorMove is true. If isSide1SeparatorMove is true and we're growing, then it
2023 // means this container is on the right/bottom of the separator, so should resize its first
2024 // children first. Same logic for the other 3 cases
2025
2026 const bool isSide1SeparatorMove = strategy == ChildrenResizeStrategy::Side1SeparatorMove;
2027 bool resizeHeadFirst = false;
2028 if (isGrowing && isSide1SeparatorMove) {
2029 resizeHeadFirst = true;
2030 } else if (isGrowing && !isSide1SeparatorMove) {
2031 resizeHeadFirst = false;
2032 } else if (!isGrowing && isSide1SeparatorMove) {
2033 resizeHeadFirst = false;
2034 } else if (!isGrowing && !isSide1SeparatorMove) {
2035 resizeHeadFirst = true;
2036 }
2037
2038 for (int i = 0; i < count; i++) {
2039 const auto index = resizeHeadFirst ? i : count - 1 - i;
2040
2041 SizingInfo &size = childSizes[index];
2042
2043 if (isGrowing) {
2044 // Since we don't honour item max-size yet, it can just grow all it wants
2045 size.incrementLength(remaining, m_orientation);
2046 remaining = 0; // and we're done, the first one got everything
2047 } else {
2048 const int availableToGive = size.availableLength(m_orientation);
2049 const int took = std::min(availableToGive, remaining);
2050 size.incrementLength(-took, m_orientation);
2051 remaining -= took;
2052 }
2053
2054 if (remaining == 0)
2055 break;
2056 }
2057 }
2058 honourMaxSizes(childSizes);
2059}
2060
2061void ItemBoxContainer::Private::honourMaxSizes(SizingInfo::List &sizes)
2062{
2063 // Reduces the size of all children that are bigger than max-size.
2064 // Assuming there's widgets that are willing to grow to occupy that space.
2065
2066 int amountNeededToShrink = 0;
2067 int amountAvailableToGrow = 0;
2068 Vector<int> indexesOfShrinkers;
2069 Vector<int> indexesOfGrowers;
2070
2071 for (int i = 0; i < sizes.count(); ++i) {
2072 SizingInfo &info = sizes[i];
2073 const int neededToShrink = info.neededToShrink(m_orientation);
2074 const int availableToGrow = info.availableToGrow(m_orientation);
2075
2076 if (neededToShrink > 0) {
2077 amountNeededToShrink += neededToShrink;
2078 indexesOfShrinkers.push_back(i); // clazy:exclude=reserve-candidates
2079 } else if (availableToGrow > 0) {
2080 amountAvailableToGrow = std::min(amountAvailableToGrow + availableToGrow, q->length());
2081 indexesOfGrowers.push_back(i); // clazy:exclude=reserve-candidates
2082 }
2083 }
2084
2085 // Don't grow more than what's needed
2086 amountAvailableToGrow = std::min(amountNeededToShrink, amountAvailableToGrow);
2087
2088 // Don't shrink more than what's available to grow
2089 amountNeededToShrink = std::min(amountAvailableToGrow, amountNeededToShrink);
2090
2091 if (amountNeededToShrink == 0 || amountAvailableToGrow == 0)
2092 return;
2093
2094 // We gathered who needs to shrink and who can grow, now try to do it evenly so that all
2095 // growers participate, and not just one giving everything.
2096
2097 // Do the growing:
2098 while (amountAvailableToGrow > 0) {
2099 // Each grower will grow a bit (round-robin)
2100 auto toGrow = std::max(1, amountAvailableToGrow / int(indexesOfGrowers.size()));
2101
2102 for (auto it = indexesOfGrowers.begin(); it != indexesOfGrowers.end();) {
2103 const int index = *it;
2104 SizingInfo &sizing = sizes[index];
2105 const auto grew = std::min(sizing.availableToGrow(m_orientation), toGrow);
2106 sizing.incrementLength(grew, m_orientation);
2107 amountAvailableToGrow -= grew;
2108
2109 if (amountAvailableToGrow == 0) {
2110 // We're done growing
2111 break;
2112 }
2113
2114 if (sizing.availableToGrow(m_orientation) == 0) {
2115 // It's no longer a grower
2116 it = indexesOfGrowers.erase(it);
2117 } else {
2118 it++;
2119 }
2120 }
2121 }
2122
2123 // Do the shrinking:
2124 while (amountNeededToShrink > 0) {
2125 // Each shrinker will shrink a bit (round-robin)
2126 auto toShrink = std::max(1, amountNeededToShrink / int(indexesOfShrinkers.size()));
2127
2128 for (auto it = indexesOfShrinkers.begin(); it != indexesOfShrinkers.end();) {
2129 const int index = *it;
2130 SizingInfo &sizing = sizes[index];
2131 const auto shrunk = std::min(sizing.neededToShrink(m_orientation), toShrink);
2132 sizing.incrementLength(-shrunk, m_orientation);
2133 amountNeededToShrink -= shrunk;
2134
2135 if (amountNeededToShrink == 0) {
2136 // We're done shrinking
2137 break;
2138 }
2139
2140 if (sizing.neededToShrink(m_orientation) == 0) {
2141 // It's no longer a shrinker
2142 it = indexesOfShrinkers.erase(it);
2143 } else {
2144 it++;
2145 }
2146 }
2147 }
2148}
2149
2150bool ItemBoxContainer::hostSupportsHonouringLayoutMinSize() const
2151{
2152 if (!m_host) {
2153 // Corner case. No reason not to honour min-size
2154 return true;
2155 }
2156
2157 return m_host->supportsHonouringLayoutMinSize();
2158}
2159
2160void ItemBoxContainer::setSize_recursive(Size newSize, ChildrenResizeStrategy strategy)
2161{
2162 ScopedValueRollback block(d->m_blockUpdatePercentages, true);
2163
2164 const Size minSize = this->minSize();
2165 if (newSize.width() < minSize.width() || newSize.height() < minSize.height()) {
2166 if (!s_silenceSanityChecks && hostSupportsHonouringLayoutMinSize()) {
2167 root()->dumpLayout();
2168 KDDW_ERROR("New size doesn't respect size constraints new={}, min={}, this={}", newSize, minSize, ( void * )this);
2169 }
2170 return;
2171 }
2172
2173 if (newSize == size())
2174 return;
2175
2176 const Size oldSize = size();
2177 setSize(newSize);
2178
2179 const Item::List children = visibleChildren();
2180 const auto count = children.size();
2181 SizingInfo::List childSizes = sizes();
2182
2183 // #1 Since we changed size, also resize out children.
2184 // But apply them to our SizingInfo::List first before setting actual Item/QWidget geometries
2185 // Because we need step #2 where we ensure min sizes for each item are respected. We could
2186 // calculate and do everything in a single-step, but we already have the code for #2 in
2187 // growItem() so doing it in 2 steps will reuse much logic.
2188
2189
2190 // the sizes:
2191 d->resizeChildren(oldSize, newSize, /*by-ref*/ childSizes, strategy);
2192
2193 // the positions:
2194 positionItems(/*by-ref*/ childSizes);
2195
2196 // #2 Adjust sizes so that each item has at least Item::minSize.
2197 for (int i = 0; i < count; ++i) {
2198 SizingInfo &size = childSizes[i];
2199 const int missing = size.missingLength(d->m_orientation);
2200 if (missing > 0)
2201 growItem(i, childSizes, missing, GrowthStrategy::BothSidesEqually,
2202 NeighbourSqueezeStrategy::AllNeighbours);
2203 }
2204
2205 // #3 Sizes are now correct and honour min/max sizes. So apply them to our Items
2206 applyGeometries(childSizes, strategy);
2207}
2208
2209int ItemBoxContainer::length() const
2210{
2211 return isVertical() ? height() : width();
2212}
2213
2214void ItemBoxContainer::dumpLayout(int level, bool printSeparators)
2215{
2216 if (level == 0 && host() && s_dumpScreenInfoFunc)
2217 s_dumpScreenInfoFunc();
2218
2219 std::string indent(LAYOUT_DUMP_INDENT * size_t(level), ' ');
2220 const std::string beingInserted =
2221 m_sizingInfo.isBeingInserted ? "; beingInserted;" : "";
2222 const std::string visible = !isVisible() ? ";hidden;" : "";
2223 const bool isOverflow = isOverflowing();
2224 const Size missingSize_ = missingSize();
2225 const std::string isOverflowStr = isOverflow ? "; overflowing ;" : "";
2226 const std::string missingSizeStr = missingSize_.isNull() ? "" : (std::string("; missingSize=") + std::to_string(missingSize_.width()) + "x" + std::to_string(missingSize_.height()));
2227
2228 const std::string typeStr = isRoot() ? "- Root: " : "- Layout: ";
2229
2230 {
2231 std::cerr << indent << typeStr << "; isVertical=" << (d->m_orientation == Qt::Vertical) << "; "
2232 << m_sizingInfo.geometry /*<< "r=" << m_geometry.right() << "b=" <<
2233 m_geometry.bottom()*/
2234 << "; min=" << minSize() << "; this=" << this << beingInserted << visible
2235 << "; %=" << d->childPercentages();
2236
2237 if (maxSizeHint() != Item::hardcodedMaximumSize)
2238 std::cerr << "; max=" << maxSizeHint();
2239
2240 std::cerr << missingSizeStr << isOverflowStr << "\n";
2241 }
2242
2243 int i = 0;
2244 for (Item *item : std::as_const(m_children)) {
2245 item->dumpLayout(level + 1, printSeparators);
2246 if (printSeparators && item->isVisible()) {
2247 if (i < d->m_separators.size()) {
2248 auto separator = d->m_separators.at(i);
2249 std::cerr << std::string(LAYOUT_DUMP_INDENT * size_t(level + 1), ' ') << "- Separator: "
2250 << "local.geo=" << mapFromRoot(separator->geometry())
2251 << " ; global.geo=" << separator->geometry() << "; separator=" << separator << "\n";
2252 }
2253 ++i;
2254 }
2255 }
2256}
2257
2258void ItemBoxContainer::updateChildPercentages()
2259{
2260 if (root()->d->m_blockUpdatePercentages)
2261 return;
2262
2263 const int usable = usableLength();
2264 for (Item *item : std::as_const(m_children)) {
2265 if (item->isVisible() && !item->isBeingInserted()) {
2266 item->m_sizingInfo.percentageWithinParent =
2267 (1.0 * item->length(d->m_orientation)) / usable;
2268 } else {
2269 item->m_sizingInfo.percentageWithinParent = 0.0;
2270 }
2271 }
2272}
2273
2274void ItemBoxContainer::updateChildPercentages_recursive()
2275{
2276 updateChildPercentages();
2277 for (Item *item : std::as_const(m_children)) {
2278 if (auto c = item->asBoxContainer())
2279 c->updateChildPercentages_recursive();
2280 }
2281}
2282
2283Vector<double> ItemBoxContainer::Private::childPercentages() const
2284{
2285 Vector<double> percentages;
2286 percentages.reserve(q->m_children.size());
2287
2288 for (Item *item : std::as_const(q->m_children)) {
2289 if (item->isVisible() && !item->isBeingInserted())
2290 percentages.push_back(item->m_sizingInfo.percentageWithinParent);
2291 }
2292
2293 return percentages;
2294}
2295
2296void ItemBoxContainer::restoreChild(Item *item, NeighbourSqueezeStrategy neighbourSqueezeStrategy)
2297{
2298 assert(contains(item));
2299
2300 const bool hadVisibleChildren = hasVisibleChildren(/*excludeBeingInserted=*/true);
2301
2302 item->setIsVisible(true);
2303 item->setBeingInserted(true);
2304
2305 const int excessLength = d->excessLength();
2306
2307 if (!hadVisibleChildren) {
2308 // This container was hidden and will now be restored too, since a child was restored
2309 if (auto c = parentBoxContainer()) {
2310 setSize(item->size()); // give it a decent size. Same size as the item being restored
2311 // makes sense
2312 c->restoreChild(this, neighbourSqueezeStrategy);
2313 }
2314 }
2315
2316 // Make sure root() is big enough to respect all item's min-sizes
2317 updateSizeConstraints();
2318
2319 item->setBeingInserted(false);
2320
2321 if (numVisibleChildren() == 1) {
2322 // The easy case. Child is alone in the layout, occupies everything.
2323 item->setGeometry_recursive(rect());
2324 d->updateSeparators_recursive();
2325 return;
2326 }
2327
2328 const int available = availableToSqueezeOnSide(item, Side1)
2329 + availableToSqueezeOnSide(item, Side2) - Item::separatorThickness;
2330
2331 const int max = std::min(available, item->maxLengthHint(d->m_orientation));
2332 const int min = item->minLength(d->m_orientation);
2333
2334 /*
2335 * Regarding the excessLength:
2336 * The layout bigger than its own max-size. The new item will get more (if it can), to counter
2337 * that excess. There's just 1 case where we have excess length: A layout with items with
2338 * max-size, but the layout can't be smaller due to min-size constraints of the higher level
2339 * layouts, in the nesting hierarchy. The excess goes away when inserting a widget that can grow
2340 * indefinitely, it eats all the current excess.
2341 */
2342 const int proposed = std::max(Core::length(item->size(), d->m_orientation),
2343 excessLength - Item::separatorThickness);
2344 const int newLength = bound(min, proposed, max);
2345
2346 assert(item->isVisible());
2347
2348 // growItem() will make it grow by the same amount it steals from the neighbours, so we can't
2349 // start the growing without zeroing it
2350 if (isVertical()) {
2351 item->m_sizingInfo.geometry.setHeight(0);
2352 } else {
2353 item->m_sizingInfo.geometry.setWidth(0);
2354 }
2355
2356 growItem(item, newLength, GrowthStrategy::BothSidesEqually, neighbourSqueezeStrategy,
2357 /*accountForNewSeparator=*/true);
2358 d->updateSeparators_recursive();
2359}
2360
2361void ItemBoxContainer::updateWidgetGeometries()
2362{
2363 for (Item *item : std::as_const(m_children))
2364 item->updateWidgetGeometries();
2365}
2366
2367int ItemBoxContainer::oppositeLength() const
2368{
2369 return isVertical() ? width() : height();
2370}
2371
2372void ItemBoxContainer::requestSeparatorMove(LayoutingSeparator *separator,
2373 int delta)
2374{
2375 const auto separatorIndex = d->m_separators.indexOf(separator);
2376 if (separatorIndex == -1) {
2377 // Doesn't happen
2378 KDDW_ERROR("Unknown separator {}, this={}", ( void * )separator, ( void * )this);
2379 root()->dumpLayout();
2380 return;
2381 }
2382
2383 if (delta == 0)
2384 return;
2385
2386 const int min = minPosForSeparator_global(separator);
2387 const int pos = separator->position();
2388 const int max = maxPosForSeparator_global(separator);
2389
2390 if ((pos + delta < min && delta < 0) || // pos can be smaller than min, as long as we're making
2391 // the distane to minPos smaller, same for max.
2392 (pos + delta > max && delta > 0)) { // pos can be bigger than max already and going left/up
2393 // (negative delta, which is fine), just don't increase
2394 // if further
2395 root()->dumpLayout();
2396 KDDW_ERROR("Separator would have gone out of bounds, separator={}, min={}, pos={}, max={}, deleta={}", ( void * )separator,
2397 min, pos, max, delta);
2398 return;
2399 }
2400
2401 const Side moveDirection = delta < 0 ? Side1 : Side2;
2402 const Item::List children = visibleChildren();
2403 if (children.size() <= separatorIndex) {
2404 // Doesn't happen
2405 KDDW_ERROR("Not enough children for separator index", ( void * )separator, ( void * )this, separatorIndex);
2406 root()->dumpLayout();
2407 return;
2408 }
2409
2410 int remainingToTake = std::abs(delta);
2411 int tookLocally = 0;
2412
2413 Item *side1Neighbour = children[separatorIndex];
2414 Item *side2Neighbour = children[separatorIndex + 1];
2415
2416 Side nextSeparatorDirection = moveDirection;
2417
2418 if (moveDirection == Side1) {
2419 // Separator is moving left (or top if horizontal)
2420 const int availableSqueeze1 = availableToSqueezeOnSide(side2Neighbour, Side1);
2421 const int availableGrow2 = availableToGrowOnSide(side1Neighbour, Side2);
2422
2423 // This is the available within our container, which we can use without bothering other
2424 // separators
2425 tookLocally = std::min(availableSqueeze1, remainingToTake);
2426 tookLocally = std::min(tookLocally, availableGrow2);
2427
2428 if (tookLocally != 0) {
2429 growItem(side2Neighbour, tookLocally, GrowthStrategy::Side1Only,
2430 NeighbourSqueezeStrategy::ImmediateNeighboursFirst, false,
2431 ChildrenResizeStrategy::Side1SeparatorMove);
2432 }
2433
2434 if (availableGrow2 == tookLocally)
2435 nextSeparatorDirection = Side2;
2436
2437 } else {
2438
2439 const int availableSqueeze2 = availableToSqueezeOnSide(side1Neighbour, Side2);
2440 const int availableGrow1 = availableToGrowOnSide(side2Neighbour, Side1);
2441
2442 // Separator is moving right (or bottom if horizontal)
2443 tookLocally = std::min(availableSqueeze2, remainingToTake);
2444 tookLocally = std::min(tookLocally, availableGrow1);
2445
2446 if (tookLocally != 0) {
2447 growItem(side1Neighbour, tookLocally, GrowthStrategy::Side2Only,
2448 NeighbourSqueezeStrategy::ImmediateNeighboursFirst, false,
2449 ChildrenResizeStrategy::Side2SeparatorMove);
2450 }
2451
2452 if (availableGrow1 == tookLocally)
2453 nextSeparatorDirection = Side1;
2454 }
2455
2456 remainingToTake -= tookLocally;
2457
2458 if (remainingToTake > 0) {
2459 // Go up the hierarchy and move the next separator on the left
2460 if (isRoot()) {
2461 // Doesn't happen
2462 KDDW_ERROR("Not enough space to move separator {}", ( void * )this);
2463 } else {
2464 LayoutingSeparator *nextSeparator =
2465 parentBoxContainer()->d->neighbourSeparator_recursive(this, nextSeparatorDirection,
2466 d->m_orientation);
2467 if (!nextSeparator) {
2468 // Doesn't happen
2469 KDDW_ERROR("nextSeparator is null, report a bug");
2470 return;
2471 }
2472
2473 // nextSeparator might not belong to parentContainer(), due to different orientation
2474 const int remainingDelta = moveDirection == Side1 ? -remainingToTake : remainingToTake;
2475 nextSeparator->parentContainer()->requestSeparatorMove(nextSeparator, remainingDelta);
2476 }
2477 }
2478}
2479
2480void ItemBoxContainer::requestEqualSize(LayoutingSeparator *separator)
2481{
2482 const auto separatorIndex = d->m_separators.indexOf(separator);
2483 if (separatorIndex == -1) {
2484 // Doesn't happen
2485 KDDW_ERROR("Separator not found {}", ( void * )separator);
2486 return;
2487 }
2488
2489 const Item::List children = visibleChildren();
2490 Item *side1Item = children.at(separatorIndex);
2491 Item *side2Item = children.at(separatorIndex + 1);
2492
2493 const int length1 = side1Item->length(d->m_orientation);
2494 const int length2 = side2Item->length(d->m_orientation);
2495
2496 if (std::abs(length1 - length2) <= 1) {
2497 // items already have the same length, nothing to do.
2498 // We allow for a difference of 1px, since you can't split that.
2499
2500 // But if at least 1 item is bigger than its max-size, don't bail out early, as then they
2501 // don't deserve equal sizes.
2502 if (!(side1Item->m_sizingInfo.isPastMax(d->m_orientation)
2503 || side2Item->m_sizingInfo.isPastMax(d->m_orientation))) {
2504 return;
2505 }
2506 }
2507
2508 const int newLength = (length1 + length2) / 2;
2509
2510 int delta = 0;
2511 if (length1 < newLength) {
2512 // Let's move separator to the right
2513 delta = newLength - length1;
2514 } else if (length2 < newLength) {
2515 // or left.
2516 delta = -(newLength - length2); // negative, since separator is going left
2517 }
2518
2519 // Do some bounds checking, to respect min-size and max-size
2520 const int min = minPosForSeparator_global(separator, true);
2521 const int max = maxPosForSeparator_global(separator, true);
2522 const int newPos = bound(min, separator->position() + delta, max);
2523
2524 // correct the delta
2525 delta = newPos - separator->position();
2526
2527 if (delta != 0)
2528 requestSeparatorMove(separator, delta);
2529}
2530
2531void ItemBoxContainer::layoutEqually()
2532{
2533 SizingInfo::List childSizes = sizes();
2534 if (!childSizes.isEmpty()) {
2535 layoutEqually(childSizes);
2536 applyGeometries(childSizes);
2537 }
2538}
2539
2540void ItemBoxContainer::layoutEqually(SizingInfo::List &sizes)
2541{
2542 const auto numItems = sizes.count();
2543 Vector<int> satisfiedIndexes;
2544 satisfiedIndexes.reserve(numItems);
2545
2546 int lengthToGive = length() - (d->m_separators.size() * Item::separatorThickness);
2547
2548 // clear the sizes before we start distributing
2549 for (SizingInfo &size : sizes) {
2550 size.setLength(0, d->m_orientation);
2551 }
2552
2553 while (satisfiedIndexes.count() < sizes.count()) {
2554 const int remainingItems = int(sizes.count() - satisfiedIndexes.count());
2555 const int suggestedToGive = std::max(1, lengthToGive / remainingItems);
2556 const auto oldLengthToGive = lengthToGive;
2557
2558 for (int i = 0; i < numItems; ++i) {
2559 if (satisfiedIndexes.contains(i))
2560 continue;
2561
2562 SizingInfo &size = sizes[i];
2563 if (size.availableToGrow(d->m_orientation) <= 0) {
2564 // Was already satisfied from the beginning
2565 satisfiedIndexes.push_back(i);
2566 continue;
2567 }
2568
2569 // Bound the max length. Our max can't be bigger than the remaining space.
2570 // The layout's min length minus our own min length is the amount of space that we
2571 // need to guarantee. We can't go larger and overwrite that
2572
2573 const auto othersMissing = // The size that the others are missing to satisfy their
2574 // minimum length
2575 std::accumulate(sizes.constBegin(), sizes.constEnd(), 0,
2576 [this](size_t sum, const SizingInfo &sz) {
2577 return int(sum) + sz.missingLength(d->m_orientation);
2578 })
2579 - size.missingLength(d->m_orientation);
2580
2581 const auto maxLength =
2582 std::min(size.length(d->m_orientation) + lengthToGive - othersMissing,
2583 size.maxLengthHint(d->m_orientation));
2584
2585 const auto newItemLenght =
2586 bound(size.minLength(d->m_orientation),
2587 size.length(d->m_orientation) + suggestedToGive, maxLength);
2588 const auto toGive = newItemLenght - size.length(d->m_orientation);
2589
2590 if (toGive == 0) {
2591 assert(false);
2592 satisfiedIndexes.push_back(i);
2593 } else {
2594 lengthToGive -= toGive;
2595 size.incrementLength(toGive, d->m_orientation);
2596 if (size.availableToGrow(d->m_orientation) <= 0) {
2597 satisfiedIndexes.push_back(i);
2598 }
2599 if (lengthToGive == 0)
2600 return;
2601
2602 if (lengthToGive < 0) {
2603 KDDW_ERROR("Breaking infinite loop");
2604 return;
2605 }
2606 }
2607 }
2608
2609 if (oldLengthToGive == lengthToGive) {
2610 // Nothing happened, we can't satisfy more items, due to min/max constraints
2611 return;
2612 }
2613 }
2614}
2615
2616void ItemBoxContainer::layoutEqually_recursive()
2617{
2618 layoutEqually();
2619 for (Item *item : std::as_const(m_children)) {
2620 if (item->isVisible()) {
2621 if (auto c = item->asBoxContainer())
2622 c->layoutEqually_recursive();
2623 }
2624 }
2625}
2626
2627Item *ItemBoxContainer::visibleNeighbourFor(const Item *item, Side side) const
2628{
2629 // Item might not be visible, so use m_children instead of visibleChildren()
2630 const auto index = m_children.indexOf(const_cast<Item *>(item));
2631
2632 if (side == Side1) {
2633 for (auto i = index - 1; i >= 0; --i) {
2634 Item *child = m_children.at(i);
2635 if (child->isVisible())
2636 return child;
2637 }
2638 } else {
2639 for (auto i = index + 1; i < m_children.size(); ++i) {
2640 Item *child = m_children.at(i);
2641 if (child->isVisible())
2642 return child;
2643 }
2644 }
2645
2646 return nullptr;
2647}
2648
2649Size ItemBoxContainer::availableSize() const
2650{
2651 return size() - this->minSize();
2652}
2653
2654int ItemBoxContainer::availableLength() const
2655{
2656 return isVertical() ? availableSize().height() : availableSize().width();
2657}
2658
2659LengthOnSide ItemBoxContainer::lengthOnSide(const SizingInfo::List &sizes, int fromIndex, Side side,
2660 Qt::Orientation o) const
2661{
2662 if (fromIndex < 0)
2663 return {};
2664
2665 const auto count = sizes.count();
2666 if (fromIndex >= count)
2667 return {};
2668
2669 int start = 0;
2670 int end = -1;
2671 if (side == Side1) {
2672 start = 0;
2673 end = fromIndex;
2674 } else {
2675 start = fromIndex;
2676 end = count - 1;
2677 }
2678
2679 LengthOnSide result;
2680 for (int i = start; i <= end; ++i) {
2681 const SizingInfo &size = sizes.at(i);
2682 result.length += size.length(o);
2683 result.minLength += size.minLength(o);
2684 }
2685
2686 return result;
2687}
2688
2689int ItemBoxContainer::neighboursLengthFor(const Item *item, Side side, Qt::Orientation o) const
2690{
2691 const Item::List children = visibleChildren();
2692 const auto index = children.indexOf(const_cast<Item *>(item));
2693 if (index == -1) {
2694 KDDW_ERROR("Couldn't find item {}", ( void * )item);
2695 return 0;
2696 }
2697
2698 if (o == d->m_orientation) {
2699 int neighbourLength = 0;
2700 int start = 0;
2701 int end = -1;
2702 if (side == Side1) {
2703 start = 0;
2704 end = index - 1;
2705 } else {
2706 start = index + 1;
2707 end = children.size() - 1;
2708 }
2709
2710 for (int i = start; i <= end; ++i)
2711 neighbourLength += children.at(i)->length(d->m_orientation);
2712
2713 return neighbourLength;
2714 } else {
2715 // No neighbours in the other orientation. Each container is bidimensional.
2716 return 0;
2717 }
2718}
2719
2720int ItemBoxContainer::neighboursLengthFor_recursive(const Item *item, Side side,
2721 Qt::Orientation o) const
2722{
2723 return neighboursLengthFor(item, side, o)
2724 + (isRoot() ? 0 : parentBoxContainer()->neighboursLengthFor_recursive(this, side, o));
2725}
2726
2727int ItemBoxContainer::neighboursMinLengthFor(const Item *item, Side side, Qt::Orientation o) const
2728{
2729 const Item::List children = visibleChildren();
2730 const auto index = children.indexOf(const_cast<Item *>(item));
2731 if (index == -1) {
2732 KDDW_ERROR("Couldn't find item {}", ( void * )item);
2733 return 0;
2734 }
2735
2736 if (o == d->m_orientation) {
2737 int neighbourMinLength = 0;
2738 int start = 0;
2739 int end = -1;
2740 if (side == Side1) {
2741 start = 0;
2742 end = index - 1;
2743 } else {
2744 start = index + 1;
2745 end = children.size() - 1;
2746 }
2747
2748 for (int i = start; i <= end; ++i)
2749 neighbourMinLength += children.at(i)->minLength(d->m_orientation);
2750
2751 return neighbourMinLength;
2752 } else {
2753 // No neighbours here
2754 return 0;
2755 }
2756}
2757
2758int ItemBoxContainer::neighboursMaxLengthFor(const Item *item, Side side, Qt::Orientation o) const
2759{
2760 const Item::List children = visibleChildren();
2761 const auto index = children.indexOf(const_cast<Item *>(item));
2762 if (index == -1) {
2763 KDDW_ERROR("Couldn't find item {}", ( void * )item);
2764 return 0;
2765 }
2766
2767 if (o == d->m_orientation) {
2768 int neighbourMaxLength = 0;
2769 int start = 0;
2770 int end = -1;
2771 if (side == Side1) {
2772 start = 0;
2773 end = index - 1;
2774 } else {
2775 start = index + 1;
2776 end = children.size() - 1;
2777 }
2778
2779 for (int i = start; i <= end; ++i)
2780 neighbourMaxLength =
2781 std::min(Core::length(root()->size(), d->m_orientation),
2782 neighbourMaxLength + children.at(i)->maxLengthHint(d->m_orientation));
2783
2784 return neighbourMaxLength;
2785 } else {
2786 // No neighbours here
2787 return 0;
2788 }
2789}
2790
2791int ItemBoxContainer::availableToSqueezeOnSide(const Item *child, Side side) const
2792{
2793 const int length = neighboursLengthFor(child, side, d->m_orientation);
2794 const int min = neighboursMinLengthFor(child, side, d->m_orientation);
2795
2796 const int available = length - min;
2797 if (available < 0) {
2798 root()->dumpLayout();
2799 assert(false);
2800 }
2801 return available;
2802}
2803
2804int ItemBoxContainer::availableToGrowOnSide(const Item *child, Side side) const
2805{
2806 const int length = neighboursLengthFor(child, side, d->m_orientation);
2807 const int max = neighboursMaxLengthFor(child, side, d->m_orientation);
2808
2809 return max - length;
2810}
2811
2812int ItemBoxContainer::availableToSqueezeOnSide_recursive(const Item *child, Side side,
2813 Qt::Orientation orientation) const
2814{
2815 if (orientation == d->m_orientation) {
2816 const int available = availableToSqueezeOnSide(child, side);
2817 return isRoot()
2818 ? available
2819 : (available
2820 + parentBoxContainer()->availableToSqueezeOnSide_recursive(this, side, orientation));
2821 } else {
2822 return isRoot()
2823 ? 0
2824 : parentBoxContainer()->availableToSqueezeOnSide_recursive(this, side, orientation);
2825 }
2826}
2827
2828int ItemBoxContainer::availableToGrowOnSide_recursive(const Item *child, Side side,
2829 Qt::Orientation orientation) const
2830{
2831 if (orientation == d->m_orientation) {
2832 const int available = availableToGrowOnSide(child, side);
2833 return isRoot()
2834 ? available
2835 : (available
2836 + parentBoxContainer()->availableToGrowOnSide_recursive(this, side, orientation));
2837 } else {
2838 return isRoot()
2839 ? 0
2840 : parentBoxContainer()->availableToGrowOnSide_recursive(this, side, orientation);
2841 }
2842}
2843
2844void ItemBoxContainer::growNeighbours(Item *side1Neighbour, Item *side2Neighbour)
2845{
2846 if (!side1Neighbour && !side2Neighbour)
2847 return;
2848
2849 SizingInfo::List childSizes = sizes();
2850
2851 if (side1Neighbour && side2Neighbour) {
2852 const int index1 = indexOfVisibleChild(side1Neighbour);
2853 const int index2 = indexOfVisibleChild(side2Neighbour);
2854
2855 if (index1 == -1 || index2 == -1 || index1 >= childSizes.count()
2856 || index2 >= childSizes.count()) {
2857 // Doesn't happen
2858 KDDW_ERROR("Invalid indexes {} {} {}", index1, index2, childSizes.count());
2859 return;
2860 }
2861
2862 // Give half/half to each neighbour
2863 Rect &geo1 = childSizes[index1].geometry;
2864 Rect &geo2 = childSizes[index2].geometry;
2865
2866 if (isVertical()) {
2867 const int available = geo2.y() - geo1.bottom() - separatorThickness;
2868 geo1.setHeight(geo1.height() + available / 2);
2869 geo2.setTop(geo1.bottom() + separatorThickness + 1);
2870 } else {
2871 const int available = geo2.x() - geo1.right() - separatorThickness;
2872 geo1.setWidth(geo1.width() + available / 2);
2873 geo2.setLeft(geo1.right() + separatorThickness + 1);
2874 }
2875
2876 } else if (side1Neighbour) {
2877 const int index1 = indexOfVisibleChild(side1Neighbour);
2878 if (index1 == -1 || index1 >= childSizes.count()) {
2879 // Doesn't happen
2880 KDDW_ERROR("Invalid indexes {} {}", index1, childSizes.count());
2881 return;
2882 }
2883
2884 // Grow all the way to the right (or bottom if vertical)
2885 Rect &geo = childSizes[index1].geometry;
2886 if (isVertical()) {
2887 geo.setBottom(rect().bottom());
2888 } else {
2889 geo.setRight(rect().right());
2890 }
2891 } else if (side2Neighbour) {
2892 const int index2 = indexOfVisibleChild(side2Neighbour);
2893 if (index2 == -1 || index2 >= childSizes.count()) {
2894 // Doesn't happen
2895 KDDW_ERROR("Invalid indexes {} {}", index2, childSizes.count());
2896 return;
2897 }
2898
2899 // Grow all the way to the left (or top if vertical)
2900 Rect &geo = childSizes[index2].geometry;
2901 if (isVertical()) {
2902 geo.setTop(0);
2903 } else {
2904 geo.setLeft(0);
2905 }
2906 }
2907
2908 d->honourMaxSizes(childSizes);
2909 positionItems(/*by-ref*/ childSizes);
2910 applyGeometries(childSizes);
2911}
2912
2913void ItemBoxContainer::growItem(int index, SizingInfo::List &sizes, int missing,
2914 GrowthStrategy growthStrategy,
2915 NeighbourSqueezeStrategy neighbourSqueezeStrategy,
2916 bool accountForNewSeparator)
2917{
2918 int toSteal = missing; // The amount that neighbours of @p index will shrink
2919 if (accountForNewSeparator)
2920 toSteal += Item::separatorThickness;
2921
2922 assert(index != -1);
2923 if (toSteal == 0)
2924 return;
2925
2926 // #1. Grow our item
2927 SizingInfo &sizingInfo = sizes[index];
2928 sizingInfo.setOppositeLength(oppositeLength(), d->m_orientation);
2929 const bool isFirst = index == 0;
2930 const bool isLast = index == sizes.count() - 1;
2931
2932 int side1Growth = 0;
2933 int side2Growth = 0;
2934
2935 if (growthStrategy == GrowthStrategy::BothSidesEqually) {
2936 sizingInfo.setLength(sizingInfo.length(d->m_orientation) + missing, d->m_orientation);
2937 const auto count = sizes.count();
2938 if (count == 1) {
2939 // There's no neighbours to push, we're alone. Occupy the full container
2940 sizingInfo.incrementLength(missing, d->m_orientation);
2941 return;
2942 }
2943
2944 // #2. Now shrink the neighbors by the same amount. Calculate how much to shrink from each
2945 // side
2946 const LengthOnSide side1Length = lengthOnSide(sizes, index - 1, Side1, d->m_orientation);
2947 const LengthOnSide side2Length = lengthOnSide(sizes, index + 1, Side2, d->m_orientation);
2948
2949 int available1 = side1Length.available();
2950 int available2 = side2Length.available();
2951
2952 if (toSteal > available1 + available2) {
2953 root()->dumpLayout();
2954 assert(false);
2955 }
2956
2957 while (toSteal > 0) {
2958 if (available1 == 0) {
2959 assert(available2 >= toSteal);
2960 side2Growth += toSteal;
2961 break;
2962 } else if (available2 == 0) {
2963 assert(available1 >= toSteal);
2964 side1Growth += toSteal;
2965 break;
2966 }
2967
2968 const int toTake = std::max(1, toSteal / 2);
2969 const int took1 = std::min(toTake, available1);
2970 toSteal -= took1;
2971 available1 -= took1;
2972 side1Growth += took1;
2973 if (toSteal == 0)
2974 break;
2975
2976 const int took2 = std::min(toTake, available2);
2977 toSteal -= took2;
2978 side2Growth += took2;
2979 available2 -= took2;
2980 }
2981 shrinkNeighbours(index, sizes, side1Growth, side2Growth, neighbourSqueezeStrategy);
2982 } else if (growthStrategy == GrowthStrategy::Side1Only) {
2983 side1Growth = std::min(missing, sizingInfo.availableToGrow(d->m_orientation));
2984 sizingInfo.setLength(sizingInfo.length(d->m_orientation) + side1Growth, d->m_orientation);
2985 if (side1Growth > 0)
2986 shrinkNeighbours(index, sizes, side1Growth, /*side2Amount=*/0,
2987 neighbourSqueezeStrategy);
2988 if (side1Growth < missing) {
2989 missing = missing - side1Growth;
2990
2991 if (isLast) {
2992 // Doesn't happen
2993 KDDW_ERROR("No more items to grow");
2994 } else {
2995 growItem(index + 1, sizes, missing, growthStrategy, neighbourSqueezeStrategy,
2996 accountForNewSeparator);
2997 }
2998 }
2999
3000 } else if (growthStrategy == GrowthStrategy::Side2Only) {
3001 side2Growth = std::min(missing, sizingInfo.availableToGrow(d->m_orientation));
3002 sizingInfo.setLength(sizingInfo.length(d->m_orientation) + side2Growth, d->m_orientation);
3003
3004 if (side2Growth > 0)
3005 shrinkNeighbours(index, sizes, /*side1Amount=*/0, side2Growth,
3006 neighbourSqueezeStrategy);
3007 if (side2Growth < missing) {
3008 missing = missing - side2Growth;
3009
3010 if (isFirst) {
3011 // Doesn't happen
3012 KDDW_ERROR("No more items to grow");
3013 } else {
3014 growItem(index - 1, sizes, missing, growthStrategy, neighbourSqueezeStrategy,
3015 accountForNewSeparator);
3016 }
3017 }
3018 }
3019}
3020
3021void ItemBoxContainer::growItem(Item *item, int amount, GrowthStrategy growthStrategy,
3022 NeighbourSqueezeStrategy neighbourSqueezeStrategy,
3023 bool accountForNewSeparator,
3024 ChildrenResizeStrategy childResizeStrategy)
3025{
3026 const Item::List items = visibleChildren();
3027 const auto index = items.indexOf(item);
3028 SizingInfo::List sizes = this->sizes();
3029
3030 growItem(index, /*by-ref=*/sizes, amount, growthStrategy, neighbourSqueezeStrategy,
3031 accountForNewSeparator);
3032
3033 applyGeometries(sizes, childResizeStrategy);
3034}
3035
3036void ItemBoxContainer::applyGeometries(const SizingInfo::List &sizes,
3037 ChildrenResizeStrategy strategy)
3038{
3039 const Item::List items = visibleChildren();
3040 const auto count = items.size();
3041 assert(count == sizes.size());
3042
3043 for (int i = 0; i < count; ++i) {
3044 Item *item = items.at(i);
3045 item->setSize_recursive(sizes[i].geometry.size(), strategy);
3046 }
3047
3048 positionItems();
3049}
3050
3051SizingInfo::List ItemBoxContainer::sizes(bool ignoreBeingInserted) const
3052{
3053 const Item::List children = visibleChildren(ignoreBeingInserted);
3054 SizingInfo::List result;
3055 result.reserve(children.count());
3056 for (Item *item : children) {
3057 if (item->isContainer()) {
3058 // Containers have virtual min/maxSize methods, and don't really fill in these
3059 // properties So fill them here
3060 item->m_sizingInfo.minSize = item->minSize();
3061 item->m_sizingInfo.maxSizeHint = item->maxSizeHint();
3062 }
3063 result.push_back(item->m_sizingInfo);
3064 }
3065
3066 return result;
3067}
3068
3069Vector<int> ItemBoxContainer::calculateSqueezes(
3070 SizingInfo::List::const_iterator begin, // clazy:exclude=function-args-by-ref
3071 SizingInfo::List::const_iterator end, int needed, // clazy:exclude=function-args-by-ref
3072 NeighbourSqueezeStrategy strategy, bool reversed) const
3073{
3074 Vector<int> availabilities;
3075 for (auto it = begin; it < end; ++it) {
3076 availabilities.push_back(it->availableLength(d->m_orientation));
3077 }
3078
3079 const auto count = availabilities.count();
3080
3081 Vector<int> squeezes;
3082 squeezes.resize(count);
3083 std::fill(squeezes.begin(), squeezes.end(), 0);
3084
3085 int missing = needed;
3086
3087 if (strategy == NeighbourSqueezeStrategy::AllNeighbours) {
3088 while (missing > 0) {
3089 const int numDonors = int(std::count_if(availabilities.cbegin(), availabilities.cend(),
3090 [](int num) { return num > 0; }));
3091
3092 if (numDonors == 0) {
3093 root()->dumpLayout();
3094 assert(false);
3095 return {};
3096 }
3097
3098 int toTake = missing / numDonors;
3099 if (toTake == 0)
3100 toTake = missing;
3101
3102 for (int i = 0; i < count; ++i) {
3103 const int available = availabilities.at(i);
3104 if (available == 0)
3105 continue;
3106 const int took = std::min(missing, std::min(toTake, available));
3107 availabilities[i] -= took;
3108 missing -= took;
3109 squeezes[i] += took;
3110 if (missing == 0)
3111 break;
3112 }
3113 }
3114 } else if (strategy == NeighbourSqueezeStrategy::ImmediateNeighboursFirst) {
3115 for (int i = 0; i < count; i++) {
3116 const auto index = reversed ? count - 1 - i : i;
3117
3118 const int available = availabilities.at(index);
3119 if (available > 0) {
3120 const int took = std::min(missing, available);
3121 missing -= took;
3122 squeezes[index] += took;
3123 }
3124
3125 if (missing == 0)
3126 break;
3127 }
3128 }
3129
3130 if (missing < 0) {
3131 // Doesn't really happen
3132 KDDW_ERROR("Missing is negative. missing={}, squeezes={}", missing, squeezes);
3133 }
3134
3135 return squeezes;
3136}
3137
3138void ItemBoxContainer::shrinkNeighbours(int index, SizingInfo::List &sizes, int side1Amount,
3139 int side2Amount, NeighbourSqueezeStrategy strategy)
3140{
3141 assert(side1Amount > 0 || side2Amount > 0);
3142 assert(side1Amount >= 0 && side2Amount >= 0); // never negative
3143
3144 if (side1Amount > 0) {
3145 auto begin = sizes.cbegin();
3146 auto end = sizes.cbegin() + index;
3147 const bool reversed = strategy == NeighbourSqueezeStrategy::ImmediateNeighboursFirst;
3148 const Vector<int> squeezes =
3149 calculateSqueezes(begin, end, side1Amount, strategy, reversed);
3150 for (int i = 0; i < squeezes.size(); ++i) {
3151 const int squeeze = squeezes.at(i);
3152 SizingInfo &sizing = sizes[i];
3153 // setSize() or setGeometry() have the same effect here, we don't care about the
3154 // position yet. That's done in positionItems()
3155 sizing.setSize(adjustedRect(sizing.geometry, d->m_orientation, 0, -squeeze).size());
3156 }
3157 }
3158
3159 if (side2Amount > 0) {
3160 auto begin = sizes.cbegin() + index + 1;
3161 auto end = sizes.cend();
3162
3163 const Vector<int> squeezes = calculateSqueezes(begin, end, side2Amount, strategy);
3164 for (int i = 0; i < squeezes.size(); ++i) {
3165 const int squeeze = squeezes.at(i);
3166 SizingInfo &sizing = sizes[i + index + 1];
3167 sizing.setSize(adjustedRect(sizing.geometry, d->m_orientation, squeeze, 0).size());
3168 }
3169 }
3170}
3171
3172Vector<int> ItemBoxContainer::Private::requiredSeparatorPositions() const
3173{
3174 const int numSeparators = std::max(0, q->numVisibleChildren() - 1);
3175 Vector<int> positions;
3176 positions.reserve(numSeparators);
3177
3178 for (Item *item : std::as_const(q->m_children)) {
3179 if (positions.size() == numSeparators)
3180 break;
3181
3182 if (item->isVisible()) {
3183 const int localPos = item->m_sizingInfo.edge(m_orientation) + 1;
3184 positions.push_back(q->mapToRoot(localPos, m_orientation));
3185 }
3186 }
3187
3188 return positions;
3189}
3190
3191void ItemBoxContainer::Private::updateSeparators()
3192{
3193 if (!q->host())
3194 return;
3195
3196 const Vector<int> positions = requiredSeparatorPositions();
3197 const auto requiredNumSeparators = positions.size();
3198
3199 const bool numSeparatorsChanged = requiredNumSeparators != m_separators.size();
3200 if (numSeparatorsChanged) {
3201 // Instead of just creating N missing ones at the end of the list, let's minimize separators
3202 // having their position changed, to minimize flicker
3203 LayoutingSeparator::List newSeparators;
3204 newSeparators.reserve(requiredNumSeparators);
3205
3206 for (int position : positions) {
3207 LayoutingSeparator *separator = separatorAt(position);
3208 if (separator) {
3209 // Already existing, reuse
3210 newSeparators.push_back(separator);
3211 m_separators.removeOne(separator);
3212 } else {
3213 separator = s_createSeparatorFunc(q->host(), m_orientation, q);
3214 newSeparators.push_back(separator);
3215 }
3216 }
3217
3218 // delete what remained, which is unused
3219 deleteSeparators();
3220
3221 m_separators = newSeparators;
3222 }
3223
3224 // Update their positions:
3225 const int pos2 =
3226 q->isVertical() ? q->mapToRoot(Point(0, 0)).x() : q->mapToRoot(Point(0, 0)).y();
3227
3228 int i = 0;
3229 for (int position : positions) {
3230 m_separators.at(i)->setGeometry(position, pos2, q->oppositeLength());
3231 i++;
3232 }
3233
3234 q->updateChildPercentages();
3235}
3236
3237void ItemBoxContainer::Private::deleteSeparators()
3238{
3239 for (const auto &sep : std::as_const(m_separators))
3240 sep->free();
3241 m_separators.clear();
3242}
3243
3244void ItemBoxContainer::Private::deleteSeparators_recursive()
3245{
3246 deleteSeparators();
3247
3248 // recurse into the children:
3249 for (Item *item : std::as_const(q->m_children)) {
3250 if (auto c = item->asBoxContainer())
3251 c->d->deleteSeparators_recursive();
3252 }
3253}
3254
3255void ItemBoxContainer::Private::updateSeparators_recursive()
3256{
3257 updateSeparators();
3258
3259 // recurse into the children:
3260 const Item::List items = q->visibleChildren();
3261 for (Item *item : items) {
3262 if (auto c = item->asBoxContainer())
3263 c->d->updateSeparators_recursive();
3264 }
3265}
3266
3267int ItemBoxContainer::Private::excessLength() const
3268{
3269 // Returns how much bigger this layout is than its max-size
3270 return std::max(0, Core::length(q->size(), m_orientation) - q->maxLengthHint(m_orientation));
3271}
3272
3273void ItemBoxContainer::simplify()
3274{
3275 // Removes unneeded nesting. For example, a vertical layout doesn't need to have vertical
3276 // layouts inside. It can simply have the contents of said sub-layouts
3277
3278 ScopedValueRollback isInSimplify(d->m_isSimplifying, true);
3279
3280 Item::List newChildren;
3281 newChildren.reserve(m_children.size() + 20); // over-reserve a bit
3282
3283 for (Item *child : std::as_const(m_children)) {
3284 if (ItemBoxContainer *childContainer = child->asBoxContainer()) {
3285 childContainer->simplify(); // recurse down the hierarchy
3286
3287 if (childContainer->orientation() == d->m_orientation
3288 || childContainer->m_children.size() == 1) {
3289 // This sub-container is redundant, as it has the same orientation as its parent
3290 // Cannibalize it.
3291 const auto children = childContainer->childItems();
3292 for (Item *child2 : children) {
3293 child2->setParentContainer(this);
3294 newChildren.push_back(child2);
3295 }
3296
3297 delete childContainer;
3298 } else {
3299 newChildren.push_back(child);
3300 }
3301 } else {
3302 newChildren.push_back(child);
3303 }
3304 }
3305
3306 if (m_children != newChildren) {
3307 m_children = newChildren;
3308 positionItems();
3309 updateChildPercentages();
3310 }
3311}
3312
3313LayoutingSeparator *ItemBoxContainer::Private::separatorAt(int p) const
3314{
3315 for (auto separator : m_separators) {
3316 if (separator->position() == p)
3317 return separator;
3318 }
3319
3320 return nullptr;
3321}
3322
3323bool ItemBoxContainer::isVertical() const
3324{
3325 return d->m_orientation == Qt::Vertical;
3326}
3327
3328bool ItemBoxContainer::isHorizontal() const
3329{
3330 return d->m_orientation == Qt::Horizontal;
3331}
3332
3333int ItemBoxContainer::indexOf(LayoutingSeparator *separator) const
3334{
3335 return d->m_separators.indexOf(separator);
3336}
3337
3338bool ItemBoxContainer::isInSimplify() const
3339{
3340 if (d->m_isSimplifying)
3341 return true;
3342
3343 auto p = parentBoxContainer();
3344 return p && p->isInSimplify();
3345}
3346
3347int ItemBoxContainer::minPosForSeparator(LayoutingSeparator *separator,
3348 bool honourMax) const
3349{
3350 const int globalMin = minPosForSeparator_global(separator, honourMax);
3351 return mapFromRoot(globalMin, d->m_orientation);
3352}
3353
3354int ItemBoxContainer::maxPosForSeparator(LayoutingSeparator *separator,
3355 bool honourMax) const
3356{
3357 const int globalMax = maxPosForSeparator_global(separator, honourMax);
3358 return mapFromRoot(globalMax, d->m_orientation);
3359}
3360
3361int ItemBoxContainer::minPosForSeparator_global(LayoutingSeparator *separator,
3362 bool honourMax) const
3363{
3364 const int separatorIndex = indexOf(separator);
3365 assert(separatorIndex != -1);
3366
3367 const Item::List children = visibleChildren();
3368 assert(separatorIndex + 1 < children.size());
3369 Item *item2 = children.at(separatorIndex + 1);
3370
3371 const int availableToSqueeze =
3372 availableToSqueezeOnSide_recursive(item2, Side1, d->m_orientation);
3373
3374 if (honourMax) {
3375 // We can drag the separator left just as much as it doesn't violate max-size constraints of
3376 // Side2
3377 Item *item1 = children.at(separatorIndex);
3378 const int availabletoGrow = availableToGrowOnSide_recursive(item1, Side2, d->m_orientation);
3379 return separator->position() - std::min(availabletoGrow, availableToSqueeze);
3380 }
3381
3382 return separator->position() - availableToSqueeze;
3383}
3384
3385int ItemBoxContainer::maxPosForSeparator_global(LayoutingSeparator *separator,
3386 bool honourMax) const
3387{
3388 const int separatorIndex = indexOf(separator);
3389 assert(separatorIndex != -1);
3390
3391 const Item::List children = visibleChildren();
3392 Item *item1 = children.at(separatorIndex);
3393
3394 const int availableToSqueeze =
3395 availableToSqueezeOnSide_recursive(item1, Side2, d->m_orientation);
3396
3397 if (honourMax) {
3398 // We can drag the separator right just as much as it doesn't violate max-size constraints
3399 // of Side1
3400 Item *item2 = children.at(separatorIndex + 1);
3401 const int availabletoGrow = availableToGrowOnSide_recursive(item2, Side1, d->m_orientation);
3402 return separator->position() + std::min(availabletoGrow, availableToSqueeze);
3403 }
3404
3405 return separator->position() + availableToSqueeze;
3406}
3407
3408void ItemBoxContainer::to_json(nlohmann::json &j) const
3409{
3410 Item::to_json(j);
3411
3412 j["children"] = m_children;
3413 j["orientation"] = d->m_orientation;
3414}
3415
3416void ItemBoxContainer::fillFromJson(const nlohmann::json &j,
3417 const std::unordered_map<QString, LayoutingGuest *> &widgets)
3418{
3419 if (!j.is_object()) {
3420 KDDW_ERROR("Expected a JSON object");
3421 return;
3422 }
3423
3424 ScopedValueRollback deserializing(d->m_isDeserializing, true);
3425 Item::fillFromJson(j, widgets);
3426
3427 d->m_orientation = Qt::Orientation(j.value<Qt::Orientation>("orientation", {}));
3428
3429 for (const auto &child : j.value("children", nlohmann::json::array())) {
3430 const bool isContainer = child.value<bool>("isContainer", {});
3431 Item *childItem =
3432 isContainer ? new ItemBoxContainer(host(), this) : new Item(host(), this);
3433 childItem->fillFromJson(child, widgets);
3434 m_children.push_back(childItem);
3435 }
3436
3437 if (isRoot()) {
3438 updateChildPercentages_recursive();
3439 if (host()) {
3440 d->updateSeparators_recursive();
3441 d->updateWidgets_recursive();
3442 }
3443
3444 d->relayoutIfNeeded();
3445 positionItems_recursive();
3446
3447 minSizeChanged.emit(this);
3448#ifdef DOCKS_DEVELOPER_MODE
3449 if (!checkSanity())
3450 KDDW_ERROR("Resulting layout is invalid");
3451#endif
3452 }
3453}
3454
3455bool ItemBoxContainer::Private::isDummy() const
3456{
3457 return q->host() == nullptr;
3458}
3459
3460#ifdef DOCKS_DEVELOPER_MODE
3461void ItemBoxContainer::relayoutIfNeeded()
3462{
3463 d->relayoutIfNeeded();
3464}
3465
3466bool ItemBoxContainer::test_suggestedRect()
3467{
3468 auto itemToDrop = new Item(host());
3469
3470 const Item::List children = visibleChildren();
3471 for (Item *relativeTo : children) {
3472 if (auto c = relativeTo->asBoxContainer()) {
3473 c->test_suggestedRect();
3474 } else {
3475 std::unordered_map<Location, Rect> rects;
3476 for (Location loc :
3478 const Rect rect = suggestedDropRect(itemToDrop, relativeTo, loc);
3479 rects[loc] = rect;
3480 if (rect.isEmpty()) {
3481 KDDW_ERROR("Empty rect");
3482 return false;
3483 } else if (!root()->rect().contains(rect)) {
3484 root()->dumpLayout();
3485 KDDW_ERROR("Suggested rect is out of bounds rect={}, loc={}, relativeTo={}", rect, loc, ( void * )relativeTo);
3486 return false;
3487 }
3488 }
3489
3490 auto rectFor = [&rects](Location loc) -> Rect {
3491 auto it = rects.find(loc);
3492 return it == rects.cend() ? Rect() : it->second;
3493 };
3494
3495 if (rectFor(Location_OnBottom).y() <= rectFor(Location_OnTop).y()
3496 || rectFor(Location_OnRight).x() <= rectFor(Location_OnLeft).x()) {
3497 root()->dumpLayout();
3498 KDDW_ERROR("Invalid suggested rects. this={}, relativeTo={}", ( void * )this, ( void * )relativeTo);
3499 return false;
3500 }
3501 }
3502 }
3503
3504 delete itemToDrop;
3505 return true;
3506}
3507#endif
3508
3509Vector<LayoutingSeparator *> ItemBoxContainer::separators_recursive() const
3510{
3511 LayoutingSeparator::List separators = d->m_separators;
3512
3513 for (Item *item : std::as_const(m_children)) {
3514 if (auto c = item->asBoxContainer())
3515 separators.append(c->separators_recursive());
3516 }
3517
3518 return separators;
3519}
3520
3521Vector<LayoutingSeparator *> ItemBoxContainer::separators() const
3522{
3523 return d->m_separators;
3524}
3525
3526LayoutingSeparator *ItemBoxContainer::separatorForChild(Item *child, Side side) const
3527{
3528 if (!child || !child->isVisible()) {
3529 KDDW_ERROR("ItemBoxContainer::separatorForChild: Unexpected nullptr or invisible child");
3530 return nullptr;
3531 }
3532
3533 const Item::List children = visibleChildren();
3534 const int childIndex = children.indexOf(child);
3535 if (childIndex == -1) {
3536 KDDW_ERROR("ItemBoxContainer::separatorForChild: Could not find child");
3537 return nullptr;
3538 }
3539
3540 int separatorIndex = -1;
3541
3542 if (side == Side1) {
3543 // side1 is the separator on the left (or top)
3544 if (childIndex == 0) // No left separator for the 1st item
3545 return nullptr;
3546
3547 separatorIndex = childIndex - 1;
3548 } else {
3549 // side2 is the separator on the right (or bottom)
3550 if (childIndex == children.size() - 1) // No right separator for the last item
3551 return nullptr;
3552
3553 separatorIndex = childIndex;
3554 }
3555
3556 if (separatorIndex < 0 || separatorIndex >= d->m_separators.size()) {
3557 KDDW_ERROR("ItemBoxContainer::separatorForChild: Not enough separators {} {} {}", d->m_separators.size(), children.size(), childIndex);
3558 return nullptr;
3559 }
3560
3561 return d->m_separators.at(separatorIndex);
3562}
3563
3564LayoutingSeparator *ItemBoxContainer::adjacentSeparatorForChild(Item *child, Side side) const
3565{
3566 if (!child || !child->isVisible()) {
3567 KDDW_ERROR("ItemBoxContainer::adjacentSeparatorForChild: Unexpected nullptr or invisible child");
3568 return nullptr;
3569 }
3570
3571 int separatorIndex = -1;
3572
3573 // If this container is horizontal, we need to find the parent vertical one (or vice-versa):
3574 if (ItemBoxContainer *ancestor = ancestorBoxContainerWithOrientation(oppositeOrientation(orientation()))) {
3575 // Since it's not a direct ancestor, we need to use indexInAncestor().
3576 const int childIndex = indexInAncestor(ancestor);
3577 const auto children = ancestor->visibleChildren();
3578 const auto separators = ancestor->separators();
3579
3580 if (childIndex == -1) {
3581 // Can't happen
3582 KDDW_ERROR("ItemBoxContainer::adjacentSeparatorForChild: Could not find index inside ancestor");
3583 return nullptr;
3584 }
3585
3586 if (side == Side1) {
3587 if (childIndex == 0) // No top separator for the 1st item
3588 return nullptr;
3589
3590 separatorIndex = childIndex - 1;
3591 } else {
3592 if (childIndex == children.size() - 1) // No bottom separator for the last item
3593 return nullptr;
3594
3595 separatorIndex = childIndex;
3596 }
3597
3598 if (separatorIndex < 0 || separatorIndex >= separators.size()) {
3599 KDDW_ERROR("ItemBoxContainer::adjacentSeparatorForChild: Not enough separators {} {} {}", separators.size(), children.size(), childIndex);
3600 return nullptr;
3601 }
3602
3603 return separators[separatorIndex];
3604 }
3605
3607 return nullptr;
3608}
3609
3610bool ItemBoxContainer::isOverflowing() const
3611{
3612 // This never returns true, unless when loading a buggy layout
3613 // or if QWidgets now have bigger min-size
3614
3615 int contentsLength = 0;
3616 int numVisible = 0;
3617 for (Item *item : std::as_const(m_children)) {
3618 if (item->isVisible()) {
3619 contentsLength += item->length(d->m_orientation);
3620 numVisible++;
3621 }
3622 }
3623
3624 contentsLength += std::max(0, Item::separatorThickness * (numVisible - 1));
3625 return contentsLength > length();
3626}
3627
3628void ItemBoxContainer::Private::relayoutIfNeeded()
3629{
3630 // Checks all the child containers if they have the correct min-size, recursively.
3631 // When loading a layout from disk the min-sizes for the host QWidgets might have changed, so we
3632 // need to adjust
3633
3634 {
3635 // #1. First, we check if the current container has enough space
3636 const Size missing = q->missingSize();
3637 if (!missing.isNull())
3638 q->setSize_recursive(q->size() + missing);
3639 }
3640
3641 // #2. Make sure there's no child that is missing space
3642 for (Item *child : std::as_const(q->m_children)) {
3643 const int missingLength = ::length(child->missingSize(), m_orientation);
3644 if (!child->isVisible() || missingLength == 0)
3645 continue;
3646
3647 q->growItem(child, missingLength, GrowthStrategy::BothSidesEqually, NeighbourSqueezeStrategy::AllNeighbours);
3648 }
3649
3650 // #3. Contents is currently bigger. Not sure if this can still happen.
3651 if (q->isOverflowing()) {
3652 const Size size = q->size();
3653 q->m_sizingInfo.setSize(size + Size(1, 1)); // Just so setSize_recursive() doesn't bail out
3654 q->setSize_recursive(size);
3655 q->updateChildPercentages();
3656 }
3657
3658 // Let's see our children too:
3659 for (Item *item : std::as_const(q->m_children)) {
3660 if (item->isVisible()) {
3661 if (auto c = item->asBoxContainer())
3662 c->d->relayoutIfNeeded();
3663 }
3664 }
3665}
3666
3667const Item *ItemBoxContainer::Private::itemFromPath(const Vector<int> &path) const
3668{
3669 const ItemBoxContainer *container = q;
3670
3671 for (int i = 0; i < path.size(); ++i) {
3672 const int index = path[i];
3673 const bool isLast = i == path.size() - 1;
3674 if (index < 0 || index >= container->m_children.size()) {
3675 // Doesn't happen
3676 q->root()->dumpLayout();
3677 KDDW_ERROR("Invalid index {}, this={}, path={}, isRoot={}", index, ( void * )this, path, q->isRoot());
3678 return nullptr;
3679 }
3680
3681 if (isLast) {
3682 return container->m_children.at(index);
3683 } else {
3684 container = container->m_children.at(index)->asBoxContainer();
3685 if (!container) {
3686 KDDW_ERROR("Invalid index path={}", path);
3687 return nullptr;
3688 }
3689 }
3690 }
3691
3692 return q;
3693}
3694
3695LayoutingSeparator *
3696ItemBoxContainer::Private::neighbourSeparator(const Item *item, Side side,
3697 Qt::Orientation orientation) const
3698{
3699 Item::List children = q->visibleChildren();
3700 const auto itemIndex = children.indexOf(const_cast<Item *>(item));
3701 if (itemIndex == -1) {
3702 KDDW_ERROR("Item not found item={}, this={}", ( void * )item, ( void * )this);
3703 q->root()->dumpLayout();
3704 return nullptr;
3705 }
3706
3707 if (orientation != q->orientation()) {
3708 // Go up
3709 if (q->isRoot()) {
3710 return nullptr;
3711 } else {
3712 return q->parentBoxContainer()->d->neighbourSeparator(q, side, orientation);
3713 }
3714 }
3715
3716 const auto separatorIndex = side == Side1 ? itemIndex - 1 : itemIndex;
3717
3718 if (separatorIndex < 0 || separatorIndex >= m_separators.size())
3719 return nullptr;
3720
3721 return m_separators[separatorIndex];
3722}
3723
3724LayoutingSeparator *
3725ItemBoxContainer::Private::neighbourSeparator_recursive(const Item *item, Side side,
3726 Qt::Orientation orientation) const
3727{
3728 LayoutingSeparator *separator = neighbourSeparator(item, side, orientation);
3729 if (separator)
3730 return separator;
3731
3732 if (!q->parentContainer())
3733 return nullptr;
3734
3735 return q->parentBoxContainer()->d->neighbourSeparator_recursive(q, side, orientation);
3736}
3737
3738void ItemBoxContainer::Private::updateWidgets_recursive()
3739{
3740 for (Item *item : std::as_const(q->m_children)) {
3741 if (auto c = item->asBoxContainer()) {
3742 c->d->updateWidgets_recursive();
3743 } else {
3744 if (item->isVisible()) {
3745 if (auto guest = item->guest()) {
3746 guest->setGeometry(q->mapToRoot(item->geometry()));
3747 guest->setVisible(true);
3748 } else {
3749 KDDW_ERROR("visible item doesn't have a guest item=", ( void * )item);
3750 }
3751 }
3752 }
3753 }
3754}
3755
3756SizingInfo::SizingInfo()
3757 : minSize(Core::Item::hardcodedMinimumSize)
3758 , maxSizeHint(Core::Item::hardcodedMaximumSize)
3759{
3760}
3761
3762void SizingInfo::setOppositeLength(int l, Qt::Orientation o)
3763{
3764 setLength(l, oppositeOrientation(o));
3765}
3766
3767int SizingInfo::maxLengthHint(Qt::Orientation o) const
3768{
3769 return std::max(minLength(o), Core::length(maxSizeHint, o));
3770}
3771
3772int SizingInfo::availableLength(Qt::Orientation o) const
3773{
3774 return std::max(0, length(o) - minLength(o));
3775}
3776
3777int SizingInfo::missingLength(Qt::Orientation o) const
3778{
3779 return std::max(0, minLength(o) - length(o));
3780}
3781
3782int SizingInfo::neededToShrink(Qt::Orientation o) const
3783{
3784 return std::max(0, length(o) - maxLengthHint(o));
3785}
3786
3787void Core::to_json(nlohmann::json &j, const SizingInfo &info)
3788{
3789 j["geometry"] = info.geometry;
3790 j["minSize"] = info.minSize;
3791 j["maxSizeHint"] = info.maxSizeHint;
3792 j["percentageWithinParent"] = info.percentageWithinParent;
3793}
3794
3795void Core::from_json(const nlohmann::json &j, SizingInfo &info)
3796{
3797 info.geometry = j.value("geometry", Rect());
3798 info.minSize = j.value("minSize", Size());
3799 info.maxSizeHint = j.value("maxSizeHint", Size());
3800 info.percentageWithinParent = j.value<double>("percentageWithinParent", {});
3801}
3802
3803void Core::to_json(nlohmann::json &j, Item *item)
3804{
3805 if (!item)
3806 return;
3807
3808 // virtual dispatch
3809 item->to_json(j);
3810}
3811
3812int ItemBoxContainer::Private::defaultLengthFor(Item *item, InitialOption option) const
3813{
3814 int result = 0;
3815
3816 if (option.hasPreferredLength(m_orientation)
3817 && option.sizeMode != DefaultSizeMode::NoDefaultSizeMode) {
3818 result = option.preferredLength(m_orientation);
3819 } else {
3820 switch (option.sizeMode) {
3821 case DefaultSizeMode::NoDefaultSizeMode:
3822 break;
3823 case DefaultSizeMode::Fair: {
3824 const int numVisibleChildren =
3825 q->numVisibleChildren() + 1; // +1 so it counts with @p item too, which we're adding
3826 const int usableLength =
3827 q->length() - (Item::separatorThickness * (numVisibleChildren - 1));
3828 result = usableLength / numVisibleChildren;
3829 break;
3830 }
3831 case DefaultSizeMode::FairButFloor: {
3832 int length = defaultLengthFor(item, DefaultSizeMode::Fair);
3833 result = std::min(length, item->length(m_orientation));
3834 break;
3835 }
3836 case DefaultSizeMode::ItemSize:
3837 result = item->length(m_orientation);
3838 break;
3839 }
3840 }
3841
3842 result = std::max(item->minLength(m_orientation), result); // bound with max-size too
3843 return result;
3844}
3845
3846struct ItemContainer::Private
3847{
3848 explicit Private(ItemContainer *qq)
3849 : q(qq)
3850 {
3851 }
3852
3853 ~Private()
3854 {
3855 }
3856 ItemContainer *const q;
3857};
3858
3859ItemContainer::ItemContainer(LayoutingHost *hostWidget, ItemContainer *parent)
3860 : Item(true, hostWidget, parent)
3861 , d(new Private(this))
3862{
3863 xChanged.connect([this] {
3864 for (Item *item : std::as_const(m_children)) {
3865 item->xChanged.emit();
3866 }
3867 });
3868
3869 yChanged.connect([this] {
3870 for (Item *item : std::as_const(m_children)) {
3871 item->yChanged.emit();
3872 }
3873 });
3874}
3875
3876ItemContainer::ItemContainer(LayoutingHost *hostWidget)
3877 : Item(true, hostWidget, nullptr)
3878 , d(new Private(this))
3879{
3880}
3881
3882ItemContainer::~ItemContainer()
3883{
3884 delete d;
3885}
3886
3887Item::List ItemContainer::childItems() const
3888{
3889 return m_children;
3890}
3891
3892int ItemContainer::indexOfChild(const Item *child) const
3893{
3894 return m_children.indexOf(const_cast<Item *>(child));
3895}
3896
3897bool ItemContainer::hasChildren() const
3898{
3899 return !m_children.isEmpty();
3900}
3901
3902bool ItemContainer::hasVisibleChildren(bool excludeBeingInserted) const
3903{
3904 for (Item *item : std::as_const(m_children)) {
3905 if (item->isVisible(excludeBeingInserted))
3906 return true;
3907 }
3908
3909 return false;
3910}
3911
3912int ItemContainer::numChildren() const
3913{
3914 return m_children.size();
3915}
3916
3917int ItemContainer::numVisibleChildren() const
3918{
3919 int num = 0;
3920 for (Item *child : std::as_const(m_children)) {
3921 if (child->isVisible())
3922 num++;
3923 }
3924 return num;
3925}
3926
3927bool ItemContainer::isEmpty() const
3928{
3929 return m_children.isEmpty();
3930}
3931
3932bool ItemContainer::hasSingleVisibleItem() const
3933{
3934 return numVisibleChildren() == 1;
3935}
3936
3937bool ItemContainer::contains(const Item *item) const
3938{
3939 return m_children.contains(const_cast<Item *>(item));
3940}
3941
3942Item *ItemContainer::itemForView(const LayoutingGuest *w) const
3943{
3944 for (Item *item : std::as_const(m_children)) {
3945 if (item->isContainer()) {
3946 if (Item *result = item->asContainer()->itemForView(w))
3947 return result;
3948 } else if (item->guest() == w) {
3949 return item;
3950 }
3951 }
3952
3953 return nullptr;
3954}
3955
3956Item::List ItemContainer::visibleChildren(bool includeBeingInserted) const
3957{
3958 Item::List items;
3959 items.reserve(m_children.size());
3960 for (Item *item : std::as_const(m_children)) {
3961 if (includeBeingInserted) {
3962 if (item->isVisible() || item->isBeingInserted())
3963 items.push_back(item);
3964 } else {
3965 if (item->isVisible() && !item->isBeingInserted())
3966 items.push_back(item);
3967 }
3968 }
3969
3970 return items;
3971}
3972
3973Item::List ItemContainer::items_recursive() const
3974{
3975 Item::List items;
3976 items.reserve(30); // sounds like a good upper number to minimize allocations
3977 for (Item *item : std::as_const(m_children)) {
3978 if (auto c = item->asContainer()) {
3979 items.append(c->items_recursive());
3980 } else {
3981 items.push_back(item);
3982 }
3983 }
3984
3985 return items;
3986}
3987
3988bool ItemContainer::contains_recursive(const Item *item) const
3989{
3990 for (Item *it : std::as_const(m_children)) {
3991 if (it == item) {
3992 return true;
3993 } else if (it->isContainer()) {
3994 if (it->asContainer()->contains_recursive(item))
3995 return true;
3996 }
3997 }
3998
3999 return false;
4000}
4001
4002
4003int ItemContainer::visibleCount_recursive() const
4004{
4005 int count = 0;
4006 for (Item *item : std::as_const(m_children)) {
4007 count += item->visibleCount_recursive();
4008 }
4009
4010 return count;
4011}
4012
4013int ItemContainer::count_recursive() const
4014{
4015 int count = 0;
4016 for (Item *item : std::as_const(m_children)) {
4017 if (auto c = item->asContainer()) {
4018 count += c->count_recursive();
4019 } else {
4020 count++;
4021 }
4022 }
4023
4024 return count;
4025}
4026
4027LayoutingHost::~LayoutingHost() = default;
4028LayoutingSeparator::~LayoutingSeparator() = default;
4029
4030LayoutingSeparator::LayoutingSeparator(LayoutingHost *host, Qt::Orientation orientation, Core::ItemBoxContainer *container)
4031 : m_host(host)
4032 , m_orientation(orientation)
4033 , m_parentContainer(container)
4034{
4035}
4036
4037bool LayoutingSeparator::isVertical() const
4038{
4039 return m_orientation == Qt::Vertical;
4040}
4041
4042int LayoutingSeparator::position() const
4043{
4044 const Point topLeft = geometry().topLeft();
4045 return isVertical() ? topLeft.y() : topLeft.x();
4046}
4047
4048ItemBoxContainer *LayoutingSeparator::parentContainer() const
4049{
4050 return m_parentContainer;
4051}
4052
4053Qt::Orientation LayoutingSeparator::orientation() const
4054{
4055 return m_orientation;
4056}
4057
4058// NOLINTNEXTLINE(bugprone-easily-swappable-parameters)
4059void LayoutingSeparator::setGeometry(int pos, int pos2, int length)
4060{
4061 Rect newGeo = geometry();
4062 if (isVertical()) {
4063 // The separator itself is horizontal
4064 newGeo.setSize(Size(length, Core::Item::separatorThickness));
4065 newGeo.moveTo(pos2, pos);
4066 } else {
4067 // The separator itself is vertical
4068 newGeo.setSize(Size(Core::Item::separatorThickness, length));
4069 newGeo.moveTo(pos, pos2);
4070 }
4071
4072 setGeometry(newGeo);
4073}
4074
4075void LayoutingSeparator::free()
4076{
4077 delete this;
4078}
4079
4080bool LayoutingSeparator::isBeingDragged() const
4081{
4082 return LayoutingSeparator::s_separatorBeingDragged != nullptr;
4083}
4084
4085void LayoutingSeparator::onMousePress()
4086{
4087 LayoutingSeparator::s_separatorBeingDragged = this;
4088}
4089
4090void LayoutingSeparator::onMouseRelease()
4091{
4092 LayoutingSeparator::s_separatorBeingDragged = nullptr;
4093}
4094
4095int LayoutingSeparator::onMouseMove(Point pos, bool moveSeparator)
4096{
4097 if (!isBeingDragged())
4098 return -1;
4099
4100 const int positionToGoTo = Core::pos(pos, m_orientation);
4101 const int minPos = m_parentContainer->minPosForSeparator_global(this);
4102 const int maxPos = m_parentContainer->maxPosForSeparator_global(this);
4103
4104 if ((positionToGoTo > maxPos && position() <= positionToGoTo)
4105 || (positionToGoTo < minPos && position() >= positionToGoTo)) {
4106 // if current pos is 100, and max is 80, we do allow going to 90.
4107 // Would continue to violate, but only by 10, so allow.
4108
4109 // On the other hand, if we're already past max-pos, don't make it worse and just
4110 // return if positionToGoTo is further away from maxPos.
4111
4112 // Same reasoning for minPos
4113 return -1;
4114 }
4115
4116 if (moveSeparator)
4117 m_parentContainer->requestSeparatorMove(this, positionToGoTo - position());
4118
4119 return positionToGoTo;
4120}
4121
4122class LayoutingGuest::Private
4123{
4124public:
4125 ObjectGuard<Core::Item> layoutItem;
4126};
4127
4128Core::Item *LayoutingGuest::layoutItem() const
4129{
4130 return d->layoutItem;
4131}
4132
4133void LayoutingGuest::setLayoutItem(Item *item)
4134{
4135 if (d->layoutItem == item)
4136 return;
4137
4138 if (d->layoutItem)
4139 d->layoutItem->unref();
4140
4141 if (item)
4142 item->ref();
4143
4144 d->layoutItem = item;
4145
4146 setLayoutItem_impl(item);
4147}
4148
4149LayoutingGuest::LayoutingGuest()
4150 : d(new Private())
4151{
4152}
4153
4154LayoutingGuest::~LayoutingGuest()
4155{
4156 delete d;
4157}
4158
4162void LayoutingHost::insertItem(Core::LayoutingGuest *guest, Location loc,
4163 InitialOption initialOption)
4164{
4165 if (!guest || !guest->layoutItem()) {
4166 // qWarning() << "insertItem: Something is null!";
4167 return;
4168 }
4169
4170 if (auto box = m_rootItem->asBoxContainer())
4171 box->insertItem(guest->layoutItem(), loc, initialOption);
4172}
4173
4177void LayoutingHost::insertItemRelativeTo(Core::LayoutingGuest *guest, Core::LayoutingGuest *relativeTo, Location loc,
4178 InitialOption initialOption)
4179{
4180 if (!guest || !relativeTo || !guest->layoutItem() || !relativeTo->layoutItem()) {
4181 // qWarning() << "insertItemRelativeTo: Something is null!";
4182 return;
4183 }
4184
4185 if (auto box = m_rootItem->asBoxContainer())
4186 box->insertItemRelativeTo(guest->layoutItem(), relativeTo->layoutItem(), loc, initialOption);
4187}
4188
4189#ifdef Q_CC_MSVC
4190#pragma warning(pop)
4191#endif
bool locationIsSide1(Location loc)
Definition Item.cpp:66
Qt::Orientation oppositeOrientation(Qt::Orientation o)
Definition Item.cpp:86
#define LAYOUT_DUMP_INDENT
Definition Item.cpp:33
Qt::Orientation orientationForLocation(Location loc)
Definition Item.cpp:71
bool locationIsVertical(Location loc)
Definition Item.cpp:61
Rect adjustedRect(Rect r, Qt::Orientation o, int p1, int p2)
Definition Item.cpp:91
static Platform * instance()
Returns the platform singleton.
Class to abstract QAction, so code still works with QtQuick and Flutter.
@ Location_OnTop
‍Left docking location
@ Location_OnRight
‍Top docking location
@ Location_OnBottom
‍Right docking location
bool fuzzyCompare(double a, double b, double epsilon=0.0001)
QtQuick::Platform * plat()
T bound(T minVal, T value, T maxVal)
Definition utils.h:161
bool isEmpty() const const
Orientation
QTextStream & left(QTextStream &stream)
QTextStream & right(QTextStream &stream)
Struct describing the preferred dock widget size and visibility when adding it to a layout.
int preferredLength(Qt::Orientation o) const
bool hasPreferredLength(Qt::Orientation o) 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