2026-04-06 00:20:51 -05:00

634 lines
22 KiB
C++

/*
This file is part of KDDockWidgets.
SPDX-FileCopyrightText: 2019 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.com>
Author: Sérgio Martins <sergio.martins@kdab.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
Contact KDAB at <info@kdab.com> for commercial licensing options.
*/
#pragma once
#include "kddockwidgets/docks_export.h"
#include "kddockwidgets/KDDockWidgets.h"
#include "kdbindings/signal.h"
#include "nlohmann/json.hpp"
#include <memory>
#include <unordered_map>
namespace KDDockWidgets {
namespace Core {
class View;
class ItemContainer;
class ItemBoxContainer;
class Item;
struct LengthOnSide;
class LayoutingHost;
class LayoutingGuest;
class LayoutingSeparator;
typedef void (*DumpScreenInfoFunc)();
typedef LayoutingSeparator *(*CreateSeparatorFunc)(Core::LayoutingHost *host, Qt::Orientation, Core::ItemBoxContainer *);
enum Side {
Side1,
Side2
};
enum class GrowthStrategy {
BothSidesEqually,
Side1Only,
Side2Only
};
enum class SeparatorOption {
None = 0,
LazyResize
};
Q_DECLARE_FLAGS(SeparatorOptions, SeparatorOption)
enum class ChildrenResizeStrategy {
Percentage, ///< Resizes the container in a way that all children will keep occupying the same
///< percentage
Side1SeparatorMove, ///< When resizing a container, it takes/adds space from Side1 children
///< first
Side2SeparatorMove, ///< When resizing a container, it takes/adds space from Side2 children first
GiveDropAreaWithCentralFrameAllExtra ///< When resizing a container, it gives all extra space to a drop area with central frame fix, otherwise same as Percentage
};
enum LayoutBorderLocation {
LayoutBorderLocation_None = 0,
LayoutBorderLocation_North = 1,
LayoutBorderLocation_East = 2,
LayoutBorderLocation_West = 4,
LayoutBorderLocation_South = 8,
LayoutBorderLocation_All = LayoutBorderLocation_North | LayoutBorderLocation_East
| LayoutBorderLocation_West | LayoutBorderLocation_South,
LayoutBorderLocation_Verticals = LayoutBorderLocation_West | LayoutBorderLocation_East,
LayoutBorderLocation_Horizontals = LayoutBorderLocation_North | LayoutBorderLocation_South,
};
Q_DECLARE_FLAGS(LayoutBorderLocations, LayoutBorderLocation)
inline int pos(Point p, Qt::Orientation o)
{
return o == Qt::Vertical ? p.y() : p.x();
}
inline int length(Size sz, Qt::Orientation o)
{
return o == Qt::Vertical ? sz.height() : sz.width();
}
struct DOCKS_EXPORT SizingInfo
{
SizingInfo();
Size size() const
{
return geometry.size();
}
void setSize(Size sz)
{
geometry.setSize(sz);
}
int length(Qt::Orientation o) const
{
return Core::length(size(), o);
}
int minLength(Qt::Orientation o) const
{
return Core::length(minSize, o);
}
int maxLengthHint(Qt::Orientation o) const;
int availableLength(Qt::Orientation o) const;
int missingLength(Qt::Orientation o) const;
Point pos() const
{
return geometry.topLeft();
}
int position(Qt::Orientation o) const
{
return Core::pos(pos(), o);
}
int edge(Qt::Orientation o) const
{
return o == Qt::Vertical ? geometry.bottom() : geometry.right();
}
void setLength(int l, Qt::Orientation o)
{
if (o == Qt::Vertical) {
geometry.setHeight(l);
} else {
geometry.setWidth(l);
}
}
void incrementLength(int byAmount, Qt::Orientation o)
{
setLength(length(o) + byAmount, o);
}
void setOppositeLength(int l, Qt::Orientation o);
void setPos(int p, Qt::Orientation o)
{
if (o == Qt::Vertical)
geometry.moveTop(p);
else
geometry.moveLeft(p);
}
bool isNull() const
{
return geometry.isNull();
}
void setGeometry(Rect geo)
{
geometry = geo;
}
int availableToGrow(Qt::Orientation o) const
{
return maxLengthHint(o) - length(o);
}
int neededToShrink(Qt::Orientation o) const;
bool isPastMax(Qt::Orientation o) const
{
return availableToGrow(o) >= 0;
}
typedef Vector<SizingInfo> List;
Rect geometry;
Size minSize;
Size maxSizeHint;
double percentageWithinParent = 0.0;
bool isBeingInserted = false;
};
class DOCKS_EXPORT Item : public Core::Object
{
Q_OBJECT
public:
typedef Vector<Item *> List;
explicit Item(KDDockWidgets::Core::LayoutingHost *hostWidget, ItemContainer *parent = nullptr);
~Item() override;
/// @brief returns whether this item is a root container
bool isRoot() const;
///@brief Returns whether the item is touching the layout's borders.
/// Returns Location_None if it's not touching a border.
LayoutBorderLocations adjacentLayoutBorders() const;
virtual int visibleCount_recursive() const;
/**
* @brief No widget can have a minimum size smaller than this, regardless of their minimum size.
*/
static Size hardcodedMinimumSize;
static Size hardcodedMaximumSize;
/// The width of a vertical separator, or height of horizontal one
/// Usually 5px
static int separatorThickness;
/// The spacing between dock widgets
/// This is by default, the separatorThickness, as the separator is between dockwidgets.
/// If set to a value smaller than separatorThickness, then the separators will overlap on top
/// of the dockwidgets, which can be useful in certain styles.
static int layoutSpacing;
int x() const;
int y() const;
int width() const;
int height() const;
Size size() const;
void setSize(Size);
/// Unlike setSize(), which just stores data, requestResize() asks the parent container
/// to resize this item and all its neighbours. This is convenience API for users wanting
/// to programmatically resize a dock widget. Not really used by the usual layouting.
/// API semantics work like QRect::adjust(), positive values always increase size.
void requestResize(int left, int top, int right, int bottom);
Point pos() const;
int pos(Qt::Orientation) const;
Rect geometry() const;
Rect rect() const;
bool isContainer() const;
ItemContainer *parentContainer() const;
/// Returns the parent container, but casted to ItemBoxContainer
ItemBoxContainer *parentBoxContainer() const;
int indexInAncestor(ItemContainer *, bool visibleOnly = true) const;
/// Returns the 1st ancestor container that has the desired orientation
/// Example:
/// - ItemBoxContainer(Vertical)
/// -- ItemBoxContainer(Horizontal)
/// --- Item // Passing this item to ancestorBoxContainerWithOrientation(Qt::Vertical) would
/// skip the direct parent and return the verticl one.
ItemBoxContainer *ancestorBoxContainerWithOrientation(Qt::Orientation) const;
void setMinSize(Size);
void setMaxSizeHint(Size);
bool isPlaceholder() const;
void setGeometry(Rect rect);
ItemBoxContainer *root() const;
Rect mapToRoot(Rect) const;
Point mapToRoot(Point) const;
int mapToRoot(int p, Qt::Orientation) const;
Point mapFromRoot(Point) const;
Rect mapFromRoot(Rect) const;
Point mapFromParent(Point) const;
int mapFromRoot(int p, Qt::Orientation) const;
LayoutingHost *host() const;
LayoutingGuest *guest() const;
void setGuest(LayoutingGuest *);
void ref();
void unref();
int refCount() const;
void turnIntoPlaceholder();
int minLength(Qt::Orientation) const;
int maxLengthHint(Qt::Orientation) const;
void restore(LayoutingGuest *);
Vector<int> pathFromRoot() const;
Q_REQUIRED_RESULT virtual bool checkSanity();
bool isMDI() const;
virtual bool inSetSize() const;
static bool s_silenceSanityChecks;
Item *outermostNeighbor(Location, bool visibleOnly = true) const;
Item *outermostNeighbor(Side, Qt::Orientation, bool visibleOnly) const;
virtual Size minSize() const;
virtual Size maxSizeHint() const;
virtual void
setSize_recursive(Size newSize,
ChildrenResizeStrategy strategy = ChildrenResizeStrategy::Percentage);
virtual bool isVisible(bool excludeBeingInserted = false) const;
virtual void setGeometry_recursive(Rect rect);
virtual void dumpLayout(int level = 0, bool printSeparators = true);
virtual void setHost(KDDockWidgets::Core::LayoutingHost *);
virtual void to_json(nlohmann::json &) const;
virtual void fillFromJson(const nlohmann::json &,
const std::unordered_map<QString, LayoutingGuest *> &);
static Item *createFromJson(LayoutingHost *hostWidget, ItemContainer *parent,
const nlohmann::json &,
const std::unordered_map<QString, LayoutingGuest *> &);
static void setDumpScreenInfoFunc(DumpScreenInfoFunc);
static void setCreateSeparatorFunc(CreateSeparatorFunc);
KDBindings::Signal<> geometryChanged;
KDBindings::Signal<> xChanged;
KDBindings::Signal<> yChanged;
KDBindings::Signal<> widthChanged;
KDBindings::Signal<> heightChanged;
KDBindings::Signal<Core::Item *, bool> visibleChanged;
KDBindings::Signal<Core::Item *> minSizeChanged;
KDBindings::Signal<Core::Item *> maxSizeChanged;
/// signal emitted when ~Item starts
KDBindings::Signal<> aboutToBeDeleted;
KDBindings::Signal<> deleted;
public:
explicit Item(bool isContainer, KDDockWidgets::Core::LayoutingHost *hostWidget, ItemContainer *parent);
void setParentContainer(ItemContainer *parent);
void connectParent(ItemContainer *parent);
void setPos(Point);
void setPos(int pos, Qt::Orientation);
const ItemContainer *asContainer() const;
ItemContainer *asContainer();
ItemBoxContainer *asBoxContainer();
void setLength(int length, Qt::Orientation);
virtual void setLength_recursive(int length, Qt::Orientation);
int length(Qt::Orientation) const;
int availableLength(Qt::Orientation) const;
Size missingSize() const;
virtual void updateWidgetGeometries();
virtual void setIsVisible(bool);
bool isBeingInserted() const;
void setBeingInserted(bool);
SizingInfo m_sizingInfo;
const bool m_isContainer;
ItemContainer *m_parent = nullptr;
bool m_isSettingGuest = false;
bool m_inDtor = false;
private Q_SLOTS:
void onWidgetLayoutRequested();
private:
friend class ItemContainer;
friend class ItemBoxContainer;
friend class ItemFreeContainer;
int m_refCount = 0;
void onGuestDestroyed();
bool m_isVisible = false;
bool m_inSetSize = false;
LayoutingHost *m_host = nullptr;
LayoutingGuest *m_guest = nullptr;
static DumpScreenInfoFunc s_dumpScreenInfoFunc;
static CreateSeparatorFunc s_createSeparatorFunc;
KDBindings::ConnectionHandle m_parentChangedConnection;
KDBindings::ConnectionHandle m_minSizeChangedHandle;
KDBindings::ConnectionHandle m_visibleChangedHandle;
KDBindings::ScopedConnection m_layoutInvalidatedConnection;
KDBindings::ScopedConnection m_guestDestroyedConnection;
};
/// @brief And Item which can contain other Items
class DOCKS_EXPORT ItemContainer : public Item
{
Q_OBJECT
public:
explicit ItemContainer(LayoutingHost *hostWidget, ItemContainer *parent);
explicit ItemContainer(LayoutingHost *hostWidget);
~ItemContainer();
virtual void removeItem(Item *, bool hardRemove = true) = 0;
virtual void restore(Item *child) = 0;
virtual void onChildMinSizeChanged(Item *child) = 0;
virtual void onChildVisibleChanged(Item *child, bool visible) = 0;
int numVisibleChildren() const;
int numChildren() const;
bool hasChildren() const;
bool hasVisibleChildren(bool excludeBeingInserted = false) const;
List childItems() const;
int indexOfChild(const Item *child) const;
bool isEmpty() const;
bool contains(const Item *item) const;
Item *itemForView(const LayoutingGuest *) const;
Item::List visibleChildren(bool includeBeingInserted = false) const;
Item::List items_recursive() const;
bool contains_recursive(const Item *item) const;
int visibleCount_recursive() const override;
int count_recursive() const;
virtual void clear() = 0;
bool inSetSize() const override;
public:
KDBindings::Signal<> itemsChanged;
KDBindings::Signal<> numItemsChanged;
KDBindings::Signal<int> numVisibleItemsChanged;
protected:
bool hasSingleVisibleItem() const;
Item::List m_children;
private:
struct Private;
Private *const d;
};
/// @brief A container for items which can either be vertical or horizontal
///
/// Similar analogy to QBoxLayout
class DOCKS_EXPORT ItemBoxContainer : public ItemContainer
{
Q_OBJECT
public:
explicit ItemBoxContainer(LayoutingHost *hostWidget, ItemContainer *parent);
explicit ItemBoxContainer(LayoutingHost *hostWidget);
~ItemBoxContainer();
void insertItem(Item *item, int index,
const KDDockWidgets::InitialOption &option = KDDockWidgets::DefaultSizeMode::Fair);
void insertItem(Item *item, KDDockWidgets::Location, const KDDockWidgets::InitialOption & = {});
static void
insertItemRelativeTo(Item *item, Item *relativeTo, KDDockWidgets::Location,
const KDDockWidgets::InitialOption & = KDDockWidgets::DefaultSizeMode::Fair);
void requestSeparatorMove(LayoutingSeparator *separator, int delta);
int minPosForSeparator(LayoutingSeparator *, bool honourMax = true) const;
int maxPosForSeparator(LayoutingSeparator *, bool honourMax = true) const;
int minPosForSeparator_global(LayoutingSeparator *,
bool honourMax = true) const;
int maxPosForSeparator_global(LayoutingSeparator *,
bool honourMax = true) const;
void requestEqualSize(LayoutingSeparator *separator);
void layoutEqually();
void layoutEqually_recursive();
void removeItem(Item *, bool hardRemove = true) override;
Size minSize() const override;
Size maxSizeHint() const override;
Size availableSize() const;
bool percentagesAreSane() const;
Q_REQUIRED_RESULT bool checkSanity() override;
void dumpLayout(int level = 0, bool printSeparators = true) override;
void setSize_recursive(
Size newSize,
ChildrenResizeStrategy strategy = ChildrenResizeStrategy::Percentage) override;
bool hostSupportsHonouringLayoutMinSize() const;
Rect suggestedDropRect(const Item *item, const Item *relativeTo,
KDDockWidgets::Location) const;
void to_json(nlohmann::json &) const override;
void fillFromJson(const nlohmann::json &,
const std::unordered_map<QString, LayoutingGuest *> &) override;
void clear() override;
Qt::Orientation orientation() const;
bool isVertical() const;
bool isHorizontal() const;
int length() const;
bool hasOrientation() const;
bool isOverflowing() const;
bool isDeserializing() const;
/// @brief Returns the number of visible items layed-out horizontally or vertically
/// But honours nesting
int numSideBySide_recursive(Qt::Orientation) const;
int availableLength() const;
LengthOnSide lengthOnSide(const SizingInfo::List &sizes, int fromIndex, Side,
Qt::Orientation) const;
int neighboursLengthFor(const Item *item, Side, Qt::Orientation) const;
int neighboursLengthFor_recursive(const Item *item, Side, Qt::Orientation) const;
int neighboursMinLengthFor(const Item *item, Side, Qt::Orientation) const;
int neighboursMaxLengthFor(const Item *item, Side, Qt::Orientation) const;
int availableToSqueezeOnSide(const Item *child, Side) const;
int availableToGrowOnSide(const Item *child, Side) const;
int availableToSqueezeOnSide_recursive(const Item *child, Side, Qt::Orientation) const;
int availableToGrowOnSide_recursive(const Item *child, Side, Qt::Orientation) const;
private:
int indexOfVisibleChild(const Item *) const;
void restore(Item *) override;
void restoreChild(Item *, bool forceRestoreContainer,
NeighbourSqueezeStrategy neighbourSqueezeStrategy);
void setGeometry_recursive(Rect rect) override;
ItemBoxContainer *convertChildToContainer(Item *leaf, const InitialOption &);
bool hasOrientationFor(KDDockWidgets::Location) const;
int usableLength() const;
void setChildren(const Item::List &children, Qt::Orientation o);
void setOrientation(Qt::Orientation);
void updateChildPercentages();
void updateChildPercentages_recursive();
void updateWidgetGeometries() override;
int oppositeLength() const;
void layoutEqually(SizingInfo::List &sizes);
///@brief Grows the side1Neighbour to the right and the side2Neighbour to the left
/// So they occupy the empty space that's between them (or bottom/top if Qt::Vertical).
/// This is useful when an Item is removed. Its neighbours will occupy its space.
/// side1Neighbour or side2Neighbour are allowed to be null, in which case the non-null one
/// will occupy the entire space.
void growNeighbours(Item *side1Neighbour, Item *side2Neighbour);
///@brief grows an item by @p amount. It calculates how much to grow on side1 and on side2
/// Then calls growItem(item, side1Growth, side2Growth) which will effectively grow it,
/// and shrink the neighbours which are donating the size.
void growItem(Item *, int amount, GrowthStrategy,
NeighbourSqueezeStrategy neighbourSqueezeStrategy,
bool accountForNewSeparator = false,
ChildrenResizeStrategy = ChildrenResizeStrategy::Percentage);
void growItem(int index, SizingInfo::List &sizes, int missing, GrowthStrategy,
NeighbourSqueezeStrategy neighbourSqueezeStrategy,
bool accountForNewSeparator = false);
///@brief Shrinks the neighbours of the item at @p index
///
/// The neighbours at the left/top of the item, will be shrunk by @p side1Amount, while the
/// items at right/bottom will be shrunk by @p side2Amount. Squeezes all the neighbours (not
/// just the immediate ones).
void shrinkNeighbours(int index, SizingInfo::List &sizes, int side1Amount, int side2Amount,
NeighbourSqueezeStrategy);
Item *visibleNeighbourFor(const Item *item, Side side) const;
void onChildMinSizeChanged(Item *child) override;
void onChildVisibleChanged(Item *child, bool visible) override;
void updateSizeConstraints();
SizingInfo::List sizes(bool ignoreBeingInserted = false) const;
Vector<int> calculateSqueezes(SizingInfo::List::const_iterator begin,
SizingInfo::List::const_iterator end, int needed,
NeighbourSqueezeStrategy, bool reversed = false) const;
Rect suggestedDropRectFallback(const Item *item, const Item *relativeTo,
KDDockWidgets::Location) const;
Item *itemAt(Point p) const;
Item *itemAt_recursive(Point p) const;
void setHost(KDDockWidgets::Core::LayoutingHost *) override;
void setIsVisible(bool) override;
bool isVisible(bool excludeBeingInserted = false) const override;
void setLength_recursive(int length, Qt::Orientation) override;
void applyGeometries(const SizingInfo::List &sizes,
ChildrenResizeStrategy = ChildrenResizeStrategy::Percentage);
void applyPositions(const SizingInfo::List &sizes);
int indexOf(LayoutingSeparator *) const;
bool isInSimplify() const;
public:
Vector<LayoutingSeparator *> separators_recursive() const;
Vector<LayoutingSeparator *> separators() const;
/// Returns the separator for the specified child on the specified child
/// Example:
/// - If this layout is horizontal, and side is Side1, then the left separator is returned.
/// - If this layout is horizontal, and side is Side2, then the right separator is returned.
/// - top, bottom if vertical layout.
/// @sa adjacentSeparatorForChild
LayoutingSeparator *separatorForChild(Item *child, Side side) const;
/// Returns the separator for the specified child on the specified child
/// Example:
/// - If this layout is horizontal, and side is Side1, then the top separator is returned.
/// - If this layout is horizontal, and side is Side2, then the bottom separator is returned.
/// - left, right if vertical layout.
/// @sa separatorForChild
LayoutingSeparator *adjacentSeparatorForChild(Item *child, Side side) const;
#ifdef DOCKS_DEVELOPER_MODE
bool
test_suggestedRect();
void relayoutIfNeeded();
#endif
#ifdef DOCKS_DEVELOPER_MODE
public:
#else
private:
#endif
void simplify();
/// Puts the items in their respective places. i.e. lays them sequentially, spaced by 5px (separator thickness)
/// does not resize.
void positionItems();
void positionItems_recursive();
void positionItems(SizingInfo::List &sizes);
static bool s_inhibitSimplify;
friend class Core::Item;
struct Private;
Private *const d;
};
/// QtQuick triggers a lot of resizes due to bindings being updated individually
/// Only check sanity at the end of an operation, and not each time a binding gets evaluated
/// Tests will fail with a warning if anything is wrong.
struct AtomicSanityChecks
{
AtomicSanityChecks(Item *root)
: m_oldValue(Item::s_silenceSanityChecks)
, m_root(root)
{
Item::s_silenceSanityChecks = true;
}
~AtomicSanityChecks()
{
Item::s_silenceSanityChecks = m_oldValue;
#ifdef DOCKS_DEVELOPER_MODE
if (m_root) {
const bool result = m_root->checkSanity();
KDDW_UNUSED(result);
}
#endif
}
const bool m_oldValue;
Item *const m_root;
KDDW_DELETE_COPY_CTOR(AtomicSanityChecks)
};
DOCKS_EXPORT void from_json(const nlohmann::json &, SizingInfo &);
DOCKS_EXPORT void to_json(nlohmann::json &, const SizingInfo &);
DOCKS_EXPORT void to_json(nlohmann::json &, Item *);
} // Core
} // KDDockWidgets