Layouting engine rewrite

The layouting was becoming too complex to maintain and to introduce
new features. Was even buggy, the fuzzer was constantly finding
bugs, which took hours to workaround.

Problem with the old layout engine is that there was a catch 22, between
Items driving the separators, and separators driving the anchors.

The new layout is much simpler, both in implementation and conceptually.
There's simply a recursive hierarchy of Item elements. An Item can either
have a QWidget to show, or be a ItemContainer, which contains Item children,
and so forth. Each ItemContainer is either vertical or horizontal. That's enough
to represent the "nested multi-splitter" concept which KDDW uses.

After each item insertion/deletion/resize, the separators are regenerated. They
are essentially dumb now.

TODO:
- Separators are drawn, but are not interactive yet
- There's 5 tests failing
- LayoutSaver scalling functionality
This commit is contained in:
Sergio Martins
2020-05-03 16:06:54 +01:00
parent 86d1a52bbf
commit 97c1ca30fd
47 changed files with 4966 additions and 6033 deletions

View File

@@ -34,7 +34,6 @@
#include "../Frame_p.h"
#include "Anchor_p.h"
#include "AnchorGroup_p.h"
#include "docks_export.h"
#include "KDDockWidgets.h"
#include "Item_p.h"
@@ -45,59 +44,11 @@
namespace KDDockWidgets {
class MultiSplitter;
class Length;
namespace Debug {
class DebugWindow;
}
/**
* Returns the width of the widget if orientation is Vertical, the height otherwise.
*/
template <typename T>
inline int widgetLength(const T *w, Qt::Orientation orientation)
{
return (orientation == Qt::Vertical) ? w->width() : w->height();
}
inline int lengthFromSize(QSize sz, Qt::Orientation orientation)
{
return orientation == Qt::Vertical ? sz.width()
: sz.height();
}
inline Anchor::Side sideForLocation(Location loc)
{
switch (loc) {
case KDDockWidgets::Location_OnLeft:
case KDDockWidgets::Location_OnTop:
return Anchor::Side1;
case KDDockWidgets::Location_OnRight:
case KDDockWidgets::Location_OnBottom:
return Anchor::Side2;
default:
break;
}
return Anchor::Side_None;
}
inline Qt::Orientation orientationForLocation(Location loc)
{
switch (loc) {
case KDDockWidgets::Location_OnLeft:
case KDDockWidgets::Location_OnRight:
return Qt::Vertical;
case KDDockWidgets::Location_OnTop:
case KDDockWidgets::Location_OnBottom:
return Qt::Horizontal;
default:
break;
}
return Qt::Vertical;
}
/**
* A MultiSplitter is like a QSplitter but supports mixing vertical and horizontal splitters in
* any combination.
@@ -124,11 +75,6 @@ public:
explicit MultiSplitterLayout(MultiSplitter *parent);
~MultiSplitterLayout() override;
/**
* @brief No widget can have a minimum size smaller than this, regardless of their minimum size.s
*/
static QSize hardcodedMinimumSize();
/**
* @brief returns the widget that this layout manages
*/
@@ -152,17 +98,17 @@ public:
/**
* @brief Adds the dockwidget but it stays hidden until an explicit show()
*/
void addAsPlaceholder(DockWidgetBase *dw, KDDockWidgets::Location location, Item *relativeTo = nullptr);
void addAsPlaceholder(DockWidgetBase *dw, KDDockWidgets::Location location, Layouting::Item *relativeTo = nullptr);
/**
* @brief Removes an item from this MultiSplitter.
*/
void removeItem(Item *item);
void removeItem(Layouting::Item *item);
/**
* @brief Returns true if this layout contains the specified item.
*/
bool contains(const Item *) const;
bool contains(const Layouting::Item *) const;
/**
* @brief Returns true if this layout contains the specified frame.
@@ -172,20 +118,20 @@ public:
/**
* @brief Returns the visible Item at pos @p p.
*/
Item *itemAt(QPoint p) const;
Layouting::Item *itemAt(QPoint p) const;
/**
* @brief Removes all Items, Anchors and Frames docked in this layout.
* DockWidgets are closed but not deleted.
*/
void clear(bool alsoDeleteStaticAnchors = false);
void clear();
/**
* @brief Returns the number of Item objects in this layout.
* This includes non-visible (placeholder) Items too.
* @sa visibleCount
*/
int count() const { return m_items.size(); }
int count() const { return m_rootItem->count_recursive(); }
/**
* @brief Returns the number of visible Items in this layout.
@@ -201,11 +147,6 @@ public:
*/
int placeholderCount() const;
/**
* @brief Returns true if count is 0.
*/
bool isEmpty() const { return m_items.isEmpty(); }
/**
* @brief Returns whether there's non placeholder items.
*/
@@ -219,7 +160,9 @@ public:
/**
* @brief The list of items in this layout.
*/
const ItemList items() const;
const Layouting::Item::List items() const;
Layouting::ItemContainer *rootItem() const;
/**
* Called by the indicators, so they draw the drop rubber band at the correct place.
@@ -227,38 +170,17 @@ public:
* Excludes the Anchor thickness, result is actually smaller than what needed. In other words,
* the result will be exactly the same as the geometry the widget will get.
*/
QRect rectForDrop(const QWidgetOrQuick *widget, KDDockWidgets::Location location, const Item *relativeTo) const;
QRect rectForDrop(const QWidgetOrQuick *widget, KDDockWidgets::Location location, const Layouting::Item *relativeTo) const;
bool deserialize(const LayoutSaver::MultiSplitterLayout &);
LayoutSaver::MultiSplitterLayout serialize() const;
void setAnchorBeingDragged(Anchor *);
Anchor *anchorBeingDragged() const { return m_anchorBeingDragged; }
void setAnchorBeingDragged(Layouting::Anchor *);
Layouting::Anchor *anchorBeingDragged() const { return m_anchorBeingDragged; }
bool anchorIsBeingDragged() const { return m_anchorBeingDragged != nullptr; }
///@brief returns list of separators
const Anchor::List anchors() const { return m_anchors; }
/**
* @brief Returns the list of anchors that are following @p followee
*/
Anchor::List anchorsFollowing(Anchor *followee) const;
///@brief returns the number of anchors that are following others, just for tests.
int numAchorsFollowing() const;
///@brief returns the number of anchors that are following others, just for tests.
int numVisibleAnchors() const;
///@brief returns either the left, top, right or bottom separator, depending on the @p type
Anchor *staticAnchor(Anchor::Type type) const;
Anchor *staticAnchor(Anchor::Side side, Qt::Orientation orientation) const;
///@brief a function that all code paths adding Items will call.
///It's mostly for code reuse, so we don't duplicate what's done here. But it's also nice to
///have a central place that we know will be called
void addItems_internal(const ItemList &, bool updateConstraints = true, bool emitSignal = true);
Layouting::Anchor::List anchors() const;
/**
* @brief Updates the min size of this layout.
@@ -288,45 +210,30 @@ public:
* @brief returns the contents width.
* Usually it's the same width as the respective parent MultiSplitter.
*/
int width() const { return m_size.width(); }
int width() const { return size().width(); }
/**
* @brief returns the contents height.
* Usually it's the same height as the respective parent MultiSplitter.
*/
int height() const { return m_size.height(); }
int height() const { return size().height(); }
/**
* @brief returns the layout's minimum size
* @ref setMinimumSize
*/
QSize minimumSize() const { return m_minSize; }
QSize minimumSize() const;
/**
* @brief getter for the size
*/
QSize size() const { return m_size; }
QSize size() const { return m_rootItem->size(); }
// For debug/hardening
bool validateInputs(QWidgetOrQuick *widget, KDDockWidgets::Location location, const Frame *relativeToFrame, AddingOption option) const;
// For debug/hardening
enum AnchorSanityOption {
AnchorSanity_Normal = 0,
AnchorSanity_Intersections = 1,
AnchorSanity_WidgetMinSizes = 2,
AnchorSanity_WidgetInvalidSizes = 4,
AnchorSanity_Followers = 8,
AnchorSanity_WidgetGeometry = 16,
AnchorSanity_Visibility = 32,
AnchorSanity_All = AnchorSanity_Intersections | AnchorSanity_WidgetMinSizes | AnchorSanity_WidgetInvalidSizes | AnchorSanity_Followers | AnchorSanity_WidgetGeometry | AnchorSanity_Visibility
};
Q_ENUM(AnchorSanityOption)
bool checkSanity(AnchorSanityOption o = AnchorSanity_All) const;
void maybeCheckSanity();
void restorePlaceholder(Item *item);
bool checkSanity() const;
/**
* @brief Removes unneeded placeholder items when adding new frames.
@@ -343,7 +250,7 @@ public:
/**
* @brief returns the Item that holds @p frame in this layout
*/
Item *itemForFrame(const Frame *frame) const;
Layouting::Item *itemForFrame(const Frame *frame) const;
/**
* @brief returns the frames contained in @p frameOrMultiSplitter
@@ -362,58 +269,21 @@ public:
*/
QVector<DockWidgetBase*> dockWidgets() const;
/**
* @brief Creates an AnchorGroup suited for adding a dockwidget to @location relative to @relativeToItem
*
* Returns the AnchorGroup and a new Anchor, if it was needed.
* If relativeTo is null then it returns the static anchor group.
*/
QPair<AnchorGroup, Anchor *> createTargetAnchorGroup(Location location, Item *relativeToItem);
struct Length {
Length() = default;
Length(int side1, int side2)
: side1Length(side1)
, side2Length(side2)
{}
int side1Length = 0;
int side2Length = 0;
int length() const { return side1Length + side2Length; }
void setLength(int newLength)
{
// Sets the new length, preserving proportion
side1Length = int(side1Factor() * newLength);
side2Length = newLength - side1Length;
}
bool isNull() const
{
return length() <= 0;
}
private:
qreal side1Factor() const
{
return (1.0 * side1Length) / length();
}
};
void restorePlaceholder(DockWidgetBase *dw, Layouting::Item *, int tabIndex);
Q_SIGNALS:
///@brief emitted when the number of widgets changes
///@param count the new widget count
void widgetCountChanged(int count);
void widgetCountChanged();
void visibleWidgetCountChanged(int count);
///@brief emitted when a widget is added
///@param item the item containing the new widget
void widgetAdded(KDDockWidgets::Item *item);
void widgetAdded(Layouting::Item *item);
///@brief emitted when a widget is removed
///@param item the item containing the removed widget
void widgetRemoved(KDDockWidgets::Item *item);
void widgetRemoved(Layouting::Item *item);
///@brief emitted right before dumping debug
///@sa dumpDebug
@@ -428,94 +298,26 @@ Q_SIGNALS:
void minimumSizeChanged(QSize);
public:
bool eventFilter(QObject *o, QEvent *e) override;
AnchorGroup anchorsForPos(QPoint pos) const;
AnchorGroup staticAnchorGroup() const;
Anchor::List anchors(Qt::Orientation, bool includeStatic = false, bool includePlaceholders = true) const;
Anchor *newAnchor(AnchorGroup &group, KDDockWidgets::Location location);
friend QDebug operator<<(QDebug d, const AnchorGroup &group);
static const QString s_magicMarker;
void ensureAnchorsBounded();
Layouting::Anchor::List anchors(Qt::Orientation, bool includeStatic = false, bool includePlaceholders = true) const;
private:
friend struct AnchorGroup;
friend class Item;
friend class Anchor;
friend class TestDocks;
friend class KDDockWidgets::Debug::DebugWindow;
friend class LayoutSaver;
struct AnchorBounds {
Anchor *side1;
Anchor *side2;
};
std::pair<int,int> boundInterval(int newPos1, Anchor* anchor1, int newPos2, Anchor *anchor2) const;
void blockItemPropagateGeo(bool block);
/**
* @brief overload called by the first one. Split-out so it's easier to unit-test the math
*/
QRect rectForDrop(Length lengthForDrop, Location location, QRect relativeToRect) const;
/**
* @brief setter for the minimum size
* @ref minimumSize
*/
void setMinimumSize(QSize);
void setRootItem(Layouting::ItemContainer *);
void emitVisibleWidgetCountChanged();
/**
* @brief Returns the size that the widget will get when dropped at this specific location.
*
* When location is Left or Right then the length represents a width, otherwise an height.
* This function is also called to know the size of the rubberband when hovering over a location.
*/
MultiSplitterLayout::Length lengthForDrop(const QWidgetOrQuick *widget, KDDockWidgets::Location location,
const Item *relativeTo) const;
/**
* @brief Ensures that this layout's size is enough for dropping @p widget to @p location,
* relative to @p relativeToItem.
*
* It may increase size or do notying, never decrease.
*/
void ensureEnoughSize(const QWidgetOrQuick *widget, KDDockWidgets::Location location,
const Item *relativeToItem);
void insertAnchor(Anchor *);
void removeAnchor(Anchor *);
/**
* Returns the min or max position that an anchor can go to (due to minimum size restriction on the widgets).
* For example, if the anchor is vertical and direction is Side1 then it returns the minimum x
* that the anchor can have. If direction is Side2 then it returns the maximum width. If horizontal
* then the height.
*/
int boundPositionForAnchor(Anchor *, Anchor::Side direction) const;
/**
* Similar to boundPositionForAnchor, but returns both the min and the max width (or height)
*/
QPair<int, int> boundPositionsForAnchor(Anchor *) const;
/**
* @brief similar to @ref boundPositionsForAnchor but returns for all anchors
*/
QHash<Anchor*, QPair<int,int>> boundPositionsForAllAnchors() const;
/** Returns how much is available for the new drop. It already counts with the space for new anchor that will be created.
* So it returns this layout's width() (or height), minus the minimum-sizes of all widgets, minus the thickness of all anchors
* minus the thickness of the anchor that would be created.
**/
Length availableLengthForDrop(KDDockWidgets::Location location, const Item *relativeTo) const;
/**
* @brief Like @ref availableLengthForDrop but just returns the total available width or height (depending on @p orientation)
* So no need to receive any location.
* @param orientation If Qt::Vertical then returns the available width. Height otherwise.
* @param orientation If Qt::Vertical then returns the available height. Width otherwise.
*/
int availableLengthForOrientation(Qt::Orientation orientation) const;
@@ -525,126 +327,24 @@ private:
*/
QSize availableSize() const;
/**
* @brief Increases the layout size if @ref availableSize is less than @needed
*/
void ensureHasAvailableSize(QSize needed);
/**
* Removes the widgets associated with oldAnchor and gives them to newAnchor.
* Called when removing a widget results in unneeded anchors.
*/
void updateAnchorsFromTo(Anchor *oldAnchor, Anchor *newAnchor);
void clearAnchorsFollowing();
void updateAnchorFollowing(const AnchorGroup &groupBeingRemoved = {});
QHash<Anchor *, Anchor *> anchorsShouldFollow() const;
/**
* Positions the static anchors at their correct places. Called when the MultiSplitter is resized.
* left and top anchor are at position 0, while right/bottom are at position= width/height.
* (Approx, due to styling margins and whatnot)
*/
void positionStaticAnchors();
/**
* When this MultiSplitter is resized, it gives or steals the less/extra space evenly through
* all widgets.
**/
void redistributeSpace();
void redistributeSpace(QSize oldSize, QSize newSize);
void redistributeSpace_recursive(Anchor *fromAnchor, int minAnchorPos);
/**
* Returns the width (if orientation = Horizontal), or height that is occupied by anchors.
* For example, an horizontal anchor has 2 or 3 px of width, so that's space that can't be
* occupied by child widgets.
*/
int wastedSpacing(Qt::Orientation) const;
/**
* Called by addWidget().
*
* When adding a widget to a layout, it will steal space from the widgets on the left (or top) (@p direction being Anchor::Side1),
* and from the widgets on the right (or bottom) (@p direction being Anchor::Side2).
*
* @param delta the amount of space we're stealing in the specified side
* @param fromAnchor The anchor we're starting from
* @param direction if we're going left/top (Side1) or right/bottom (Side2)
*/
void propagateResize(int delta, Anchor *fromAnchor, Anchor::Side direction);
// Helper function for propagateResize()
void collectPaths(QVector<Anchor::List> &paths, Anchor *fromAnchor, Anchor::Side direction);
// convenience for the unit-tests
// Moves the widget's bottom or right anchor, to resize it.
void resizeItem(Frame *frame, int newSize, Qt::Orientation);
void ensureItemsMinSize();
///@brief returns whether we're inside setSize();
bool isResizing() const { return m_resizing; }
bool isRestoringPlaceholder() const { return m_restoringPlaceholder; }
bool isAddingItem() const { return m_addingItem; }
QString affinityName() const;
MultiSplitter *const m_multiSplitter;
Anchor::List m_anchors;
Anchor *m_leftAnchor = nullptr;
Anchor *m_topAnchor = nullptr;
Anchor *m_rightAnchor = nullptr;
Anchor *m_bottomAnchor = nullptr;
ItemList m_items;
bool m_inCtor = true;
bool m_inDestructor = false;
bool m_beingMergedIntoAnotherMultiSplitter = false;
bool m_beingMergedIntoAnotherMultiSplitter = false; // TODO
bool m_restoringPlaceholder = false;
bool m_resizing = false;
bool m_addingItem = false;
QSize m_minSize = QSize(0, 0);
AnchorGroup m_staticAnchorGroup;
QPointer<Anchor> m_anchorBeingDragged;
QSize m_size;
QPointer<Layouting::Anchor> m_anchorBeingDragged;
Layouting::ItemContainer *m_rootItem = nullptr;
};
inline QDebug operator<<(QDebug d, const AnchorGroup &group) {
d << "AnchorGroup: top=" << group.top << "; left=" << group.left
<< "; right=" << group.right << "; bottom=" << group.bottom;
return d;
}
/**
* Returns the widget's min-width if orientation is Vertical, the min-height otherwise.
*/
inline int widgetMinLength(const QWidgetOrQuick *w, Qt::Orientation orientation)
{
int min = 0;
if (orientation == Qt::Vertical) {
if (w->minimumWidth() > 0)
min = w->minimumWidth();
else
min = w->minimumSizeHint().width();
min = qMax(MultiSplitterLayout::hardcodedMinimumSize().width(), min);
} else {
if (w->minimumHeight() > 0)
min = w->minimumHeight();
else
min = w->minimumSizeHint().height();
min = qMax(MultiSplitterLayout::hardcodedMinimumSize().height(), min);
}
return qMax(min, 0);
}
}
Q_DECLARE_METATYPE(KDDockWidgets::MultiSplitterLayout::Length)
#endif