Files
KDDockWidgets/src/private/multisplitter/MultiSplitterLayout_p.h
Sergio Martins 9904d847f2 LayoutSaver now can save/restore a sub-set of windows
Each subset is identified by its "affinity name".
See DockWidget/MainWindow setAffinityName property.

Patch contributed by Andras Mantia, I've changed it to deal in
terms of affinity names instead of unique names.
2020-03-01 14:25:31 +00:00

651 lines
22 KiB
C++

/*
This file is part of KDDockWidgets.
Copyright (C) 2018-2020 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
Author: Sérgio Martins <sergio.martins@kdab.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file
* @brief A class to layout widgets in any place relative to another widget.
*
* Widgets can be inserted to the left,right,top,bottom in relation to another widget or in relation
* to the window. Each two neighbour widgets have a separator in between, which the user can use
* to resize.
*
* @author Sérgio Martins \<sergio.martins@kdab.com\>
*/
#ifndef KD_MULTISPLITTER_LAYOUT_P_H
#define KD_MULTISPLITTER_LAYOUT_P_H
#include "../Frame_p.h"
#include "Anchor_p.h"
#include "AnchorGroup_p.h"
#include "docks_export.h"
#include "KDDockWidgets.h"
#include "Item_p.h"
#include "LayoutSaver_p.h"
#include <QPointer>
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.
*
* It supports adding a widget to the left/top/bottom/right of the whole MultiSplitter or adding
* relative to a single widget.
*
* A MultiSplitter is simply a list of Anchors, each one of them handling the resizing of widgets.
* See the documentation for Anchor.
*/
class DOCKS_EXPORT_FOR_UNIT_TESTS MultiSplitterLayout : public QObject // clazy:exclude=ctor-missing-parent-argument
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY widgetCountChanged)
Q_PROPERTY(int visibleCount READ visibleCount NOTIFY widgetCountChanged) // This notify isn't ogood enough, but it's just for debug, we're calling QMetaProperty::read to debug
Q_PROPERTY(int placeholderCount READ placeholderCount NOTIFY widgetCountChanged) // This notify isn't ogood enough, but it's just for debug, we're calling QMetaProperty::read to debug
Q_PROPERTY(QSize size READ size NOTIFY sizeChanged)
Q_PROPERTY(QSize minimumSize READ minimumSize NOTIFY minimumSizeChanged)
public:
/**
* @brief Constructor. MultiSplitterLayout is created by MultiSplitter only.
*/
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
*/
MultiSplitter *multiSplitter() const;
/**
* @brief Adds a widget to this MultiSplitter.
*/
void addWidget(QWidgetOrQuick *widget, KDDockWidgets::Location location, Frame *relativeTo = nullptr, AddingOption option = {});
/**
* Adds an entire MultiSplitter into this layout. The donor MultiSplitter will be deleted
* after all its Frames are stolen. All added Frames will preserve their original layout, so,
* if widgetFoo was at the left of widgetBar when in the donor splitter, then it will still be at left
* of widgetBar when the whole splitter is dropped into this one.
*/
void addMultiSplitter(MultiSplitter *splitter, KDDockWidgets::Location location,
Frame *relativeTo = nullptr);
/**
* @brief Adds the dockwidget but it stays hidden until an explicit show()
*/
void addAsPlaceholder(DockWidgetBase *dw, KDDockWidgets::Location location, Item *relativeTo = nullptr);
/**
* @brief Removes an item from this MultiSplitter.
*/
void removeItem(Item *item);
/**
* @brief Returns true if this layout contains the specified item.
*/
bool contains(const Item *) const;
/**
* @brief Returns true if this layout contains the specified frame.
*/
bool contains(const Frame *) const;
/**
* @brief Returns the visible Item at pos @p p.
*/
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);
/**
* @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(); }
/**
* @brief Returns the number of visible Items in this layout.
* Which is @ref count minus @ref placeholderCount
* @sa count
*/
int visibleCount() const;
/**
* @brief Returns the number of placeholder items in this layout.
* This is the same as @ref count minus @ref visibleCount
* @sa count, visibleCount
*/
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.
*/
bool hasVisibleItems() const { return visibleCount() > 0; }
/**
* @brief If @p orientation is Qt::Horizontal, returns the height, otherwise the width.
*/
int length(Qt::Orientation orientation) const;
/**
* @brief The list of items in this layout.
*/
const ItemList items() const;
/**
* Called by the indicators, so they draw the drop rubber band at the correct place.
* The rect for the rubberband when dropping a widget at the specified location.
* 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;
bool deserialize(const LayoutSaver::MultiSplitterLayout &);
LayoutSaver::MultiSplitterLayout serialize() const;
void setAnchorBeingDragged(Anchor *);
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);
/**
* @brief Updates the min size of this layout.
*/
void updateSizeConstraints();
/**
* @brief setter for the contents size
* The "contents size" is just the size() of this layout. However, since resizing
* QWidgets is async and we need it to be sync. As sometimes adding widgets will increase
* the MultiSplitterLayout size (due to widget's min-size constraints).
*/
void setSize(QSize);
/**
* @brief sets either the contents height if @p o is Qt::Horizontal, otherwise sets the contents width
*/
void setContentLength(int value, Qt::Orientation o);
/**
* @brief returns @ref contentsWidth if @p o is Qt::Vertical, otherwise @ref contentsHeight
* @sa contentsHeight, contentsWidth
*/
//int length(Qt::Orientation o) const;
/**
* @brief returns the contents width.
* Usually it's the same width as the respective parent MultiSplitter.
*/
int width() const { return m_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(); }
/**
* @brief returns the layout's minimum size
* @ref setMinimumSize
*/
QSize minimumSize() const { return m_minSize; }
/**
* @brief getter for the size
*/
QSize size() const { return m_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);
/**
* @brief Removes unneeded placeholder items when adding new frames.
*
* A floating frame A might have a placeholder in the main window (for example to remember its position on the Left),
* but then the user might attach it to the right, so the left placeholder is no longer need.
* Right before adding the frame to the right we remove the left placeholder, otherwise it's unrefed while we're adding
* causing a segfault. So what this does is making the unrefing happen a bit earlier.
*/
void unrefOldPlaceholders(const Frame::List &framesBeingAdded) const;
// For debug
void dumpDebug() const;
/**
* @brief returns the Item that holds @p frame in this layout
*/
Item *itemForFrame(const Frame *frame) const;
/**
* @brief returns the frames contained in @p frameOrMultiSplitter
* If frameOrMultiSplitter is a Frame, it returns a list of 1 element, with that frame
* If frameOrMultiSplitter is a MultiSplitterLayout then it returns a list of all frames it contains
*/
Frame::List framesFrom(QWidgetOrQuick *frameOrMultiSplitter) const;
/**
* @brief Returns a list of Frame objects contained in this layout
*/
Frame::List frames() const;
/**
* @brief Returns a list of DockWidget objects contained in this layout
*/
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();
}
};
Q_SIGNALS:
///@brief emitted when the number of widgets changes
///@param count the new widget count
void widgetCountChanged(int count);
void visibleWidgetCountChanged(int count);
///@brief emitted when a widget is added
///@param item the item containing the new widget
void widgetAdded(KDDockWidgets::Item *item);
///@brief emitted when a widget is removed
///@param item the item containing the removed widget
void widgetRemoved(KDDockWidgets::Item *item);
///@brief emitted right before dumping debug
///@sa dumpDebug
void aboutToDumpDebug() const; // clazy:exclude=const-signal-or-slot
///@brief emitted when the size changes
///@sa size
void sizeChanged(QSize sz);
///@brief emitted when the minimumSize changes
///@sa minimumSize
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();
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 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.
*/
int availableLengthForOrientation(Qt::Orientation orientation) const;
/**
* @brief Equivalent to @ref availableLengthForOrientation but returns for both orientations.
* width is for Qt::Vertical.
*/
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_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;
};
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