* move message_log_widget to game

* move files

* update headers

* fix cmakelists

* oracle fixes

* split implementation out to cpp

* fix recursive import

* fix main file

* format
This commit is contained in:
ebbit1q 2025-09-20 14:35:52 +02:00 committed by GitHub
parent f484c98152
commit 17dcaf9afa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
337 changed files with 728 additions and 721 deletions

View file

@ -0,0 +1,664 @@
/**
* @file flow_layout.cpp
* @brief Implementation of the FlowLayout class, a custom layout for dynamically organizing widgets
* in rows within the constraints of available width or parent scroll areas.
*/
#include "flow_layout.h"
#include "../widgets/general/layout_containers/flow_widget.h"
#include <QDebug>
#include <QLayoutItem>
#include <QScrollArea>
#include <QStyle>
/**
* @brief Constructs a FlowLayout instance with the specified parent widget, margin, and spacing values.
* @param parent The parent widget for this layout.
* @param margin The layout margin.
* @param hSpacing The horizontal spacing between items.
* @param vSpacing The vertical spacing between items.
*/
FlowLayout::FlowLayout(QWidget *parent,
const Qt::Orientation _flowDirection,
const int margin,
const int hSpacing,
const int vSpacing)
: QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing)
{
setContentsMargins(margin, margin, margin, margin);
}
/**
* @brief Destructor for FlowLayout, which cleans up all items in the layout.
*/
FlowLayout::~FlowLayout()
{
QLayoutItem *item;
while ((item = FlowLayout::takeAt(0))) {
delete item;
}
}
/**
* @brief Indicates the layout's support for expansion in both horizontal and vertical directions.
* @return The orientations (Qt::Horizontal | Qt::Vertical) this layout can expand to fill.
*/
Qt::Orientations FlowLayout::expandingDirections() const
{
return Qt::Horizontal | Qt::Vertical;
}
/**
* @brief Indicates that this layout's height depends on its width.
* @return True, as the layout adjusts its height to fit the specified width.
*/
bool FlowLayout::hasHeightForWidth() const
{
return true;
}
/**
* @brief Calculates the required height to display all items within the specified width.
* @param width The available width for arranging items.
* @return The total height needed to fit all items in rows constrained by the specified width.
*/
int FlowLayout::heightForWidth(const int width) const
{
if (flowDirection == Qt::Vertical) {
int height = 0;
int rowWidth = 0;
int rowHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemWidth = item->sizeHint().width() + horizontalSpacing();
if (rowWidth + itemWidth > width) {
height += rowHeight + verticalSpacing();
rowWidth = itemWidth;
rowHeight = item->sizeHint().height();
} else {
rowWidth += itemWidth;
rowHeight = qMax(rowHeight, item->sizeHint().height());
}
}
height += rowHeight; // Add height of the last row
return height;
} else {
int width = 0;
int colWidth = 0;
int colHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemHeight = item->sizeHint().height();
if (colHeight + itemHeight > width) {
width += colWidth;
colHeight = itemHeight;
colWidth = item->sizeHint().width();
} else {
colHeight += itemHeight;
colWidth = qMax(colWidth, item->sizeHint().width());
}
}
width += colWidth; // Add width of the last column
return width;
}
}
/**
* @brief Arranges layout items in rows within the specified rectangle bounds.
* @param rect The area within which to position layout items.
*/
void FlowLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle.
if (flowDirection == Qt::Horizontal) {
// If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setFixedSize(availableWidth, totalHeight);
}
} else {
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setFixedSize(totalWidth, availableHeight);
}
}
}
/**
* @brief Lays out items into rows according to the available width, starting from a given origin.
* Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableWidth The width within which each row is constrained.
* @return The total height after arranging all rows.
*/
int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
{
QVector<QLayoutItem *> rowItems; // Holds items for the current row
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
int rowHeight = 0; // Tracks the maximum height of items in the current row
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the current item
int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing
// Check if the current item fits in the remaining width of the current row
if (currentXPosition + itemWidth > availableWidth) {
// If not, layout the current row and start a new row
layoutSingleRow(rowItems, originX, currentYPosition);
rowItems.clear(); // Reset the list for the new row
currentXPosition = originX; // Reset x-position to the row's start
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row
rowHeight = 0; // Reset row height for the new row
}
// Add the item to the current row
rowItems.append(item);
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item
currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item
}
// Layout the final row if there are any remaining items
layoutSingleRow(rowItems, originX, currentYPosition);
// Return the total height used, including the last row's height
return currentYPosition + rowHeight;
}
/**
* @brief Arranges a single row of items within specified x and y starting positions.
* @param rowItems A list of items to be arranged in the row.
* @param x The starting x-coordinate for the row.
* @param y The starting y-coordinate for the row.
*/
void FlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, const int y)
{
for (QLayoutItem *item : rowItems) {
if (item == nullptr || item->isEmpty()) {
continue;
}
// Get the maximum allowed size for the item
QSize itemMaxSize = item->widget()->maximumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the x-position to the right, leaving space for horizontal spacing
x += itemWidth + horizontalSpacing();
}
}
/**
* @brief Lays out items into columns according to the available height, starting from a given origin.
* Each column is arranged within `availableHeight`, wrapping to a new column as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableHeight The height within which each column is constrained.
* @return The total width after arranging all columns.
*/
int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
{
QVector<QLayoutItem *> colItems; // Holds items for the current column
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column
int colWidth = 0; // Tracks the maximum width of items in the current column
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the current item
// Check if the current item fits in the remaining height of the current column
if (currentYPosition + itemSize.height() > availableHeight) {
// If not, layout the current column and start a new column
layoutSingleColumn(colItems, currentXPosition, originY);
colItems.clear(); // Reset the list for the new column
currentYPosition = originY; // Reset y-position to the column's start
currentXPosition += colWidth; // Move x-position to the next column
colWidth = 0; // Reset column width for the new column
}
// Add the item to the current column
colItems.append(item);
colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item
currentYPosition += itemSize.height(); // Move y-position for the next item
}
// Layout the final column if there are any remaining items
layoutSingleColumn(colItems, currentXPosition, originY);
// Return the total width used, including the last column's width
return currentXPosition + colWidth;
}
/**
* @brief Arranges a single column of items within specified x and y starting positions.
* @param colItems A list of items to be arranged in the column.
* @param x The starting x-coordinate for the column.
* @param y The starting y-coordinate for the column.
*/
void FlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
{
for (QLayoutItem *item : colItems) {
if (item == nullptr) {
qCDebug(FlowLayoutLog) << "Item is null.";
continue;
}
if (item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
// Debugging: Print the item's widget class name and size hint
QWidget *widget = item->widget();
if (widget) {
qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className();
qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint();
qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize();
qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize();
// Debugging: Print child widgets
const QObjectList &children = widget->children();
qCDebug(FlowLayoutLog) << "Child widgets:";
for (QObject *child : children) {
if (QWidget *childWidget = qobject_cast<QWidget *>(child)) {
qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className();
qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint();
qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize();
}
}
} else {
qCDebug(FlowLayoutLog) << "Item does not have a widget.";
}
// Get the maximum allowed size for the item
QSize itemMaxSize = widget->maximumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Debugging: Print the computed geometry
qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth
<< ", height=" << itemHeight;
// Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the y-position down by the item's height to place the next item below
y += itemHeight;
}
}
/**
* @brief Calculates the preferred size of the layout based on the flow direction.
* @return A QSize representing the ideal dimensions of the layout.
*/
QSize FlowLayout::sizeHint() const
{
if (flowDirection == Qt::Horizontal) {
return calculateSizeHintHorizontal();
} else {
return calculateSizeHintVertical();
}
}
/**
* @brief Calculates the minimum size required by the layout based on the flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::minimumSize() const
{
if (flowDirection == Qt::Horizontal) {
return calculateMinimumSizeHorizontal();
} else {
return calculateMinimumSizeVertical();
}
}
/**
* @brief Calculates the size hint for horizontal flow direction.
* @return A QSize representing the preferred dimensions.
*/
QSize FlowLayout::calculateSizeHintHorizontal() const
{
int maxWidth = 0; // Tracks the maximum width needed
int totalHeight = 0; // Tracks the total height across all rows
int rowHeight = 0; // Tracks the height of the current row
int currentWidth = 0; // Tracks the current row's width
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemSize = item->sizeHint();
int itemWidth = itemSize.width() + horizontalSpacing();
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize << "Width with spacing:" << itemWidth;
if (currentWidth + itemWidth > availableWidth) {
qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight + verticalSpacing();
qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
currentWidth = 0;
rowHeight = 0;
}
currentWidth += itemWidth;
rowHeight = qMax(rowHeight, itemSize.height());
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
}
// Account for the final row
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight;
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
return QSize(maxWidth, totalHeight);
}
/**
* @brief Calculates the minimum size for horizontal flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::calculateMinimumSizeHorizontal() const
{
int maxWidth = 0; // Tracks the maximum width of a row
int totalHeight = 0; // Tracks the total height across all rows
int rowHeight = 0; // Tracks the height of the current row
int currentWidth = 0; // Tracks the current row's width
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemMinSize = item->minimumSize();
int itemWidth = itemMinSize.width() + horizontalSpacing();
qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize << "Width with spacing:" << itemWidth;
if (currentWidth + itemWidth > availableWidth) {
qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight;
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight + verticalSpacing();
qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth;
currentWidth = 0;
rowHeight = 0;
}
currentWidth += itemWidth;
rowHeight = qMax(rowHeight, itemMinSize.height());
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
}
// Account for the final row
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight;
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
return QSize(maxWidth, totalHeight);
}
/**
* @brief Calculates the size hint for vertical flow direction.
* @return A QSize representing the preferred dimensions.
*/
QSize FlowLayout::calculateSizeHintVertical() const
{
int totalWidth = 0;
int maxHeight = 0;
int colWidth = 0;
int currentHeight = 0;
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemSize = item->sizeHint();
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize;
if (currentHeight + itemSize.height() > availableHeight) {
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
<< "Column width:" << colWidth;
totalWidth += colWidth + horizontalSpacing();
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
currentHeight = 0;
colWidth = 0;
}
currentHeight += itemSize.height() + verticalSpacing();
colWidth = qMax(colWidth, itemSize.width());
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
}
// Account for the final column
totalWidth += colWidth;
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
return QSize(totalWidth, maxHeight);
}
/**
* @brief Calculates the minimum size for vertical flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::calculateMinimumSizeVertical() const
{
int totalWidth = 0; // Tracks the total width across all columns
int maxHeight = 0; // Tracks the maximum height of a column
int colWidth = 0; // Tracks the width of the current column
int currentHeight = 0; // Tracks the current column's height
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemMinSize = item->minimumSize();
int itemHeight = itemMinSize.height() + verticalSpacing();
qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize
<< "Height with spacing:" << itemHeight;
if (currentHeight + itemHeight > availableHeight) {
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
<< "Column width:" << colWidth;
totalWidth += colWidth + horizontalSpacing();
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
currentHeight = 0;
colWidth = 0;
}
currentHeight += itemHeight;
colWidth = qMax(colWidth, itemMinSize.width());
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
}
// Account for the final column
totalWidth += colWidth;
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
return QSize(totalWidth, maxHeight);
}
/**
* @brief Adds a new item to the layout.
* @param item The layout item to add.
*/
void FlowLayout::addItem(QLayoutItem *item)
{
if (item != nullptr) {
items.append(item);
}
}
void FlowLayout::insertWidgetAtIndex(QWidget *toInsert, int index)
{
addChildWidget(toInsert);
// We don't want to fail on an index that violates the bounds, so we just clamp it.
int boundedIndex = qBound(0, index, qMax(0, static_cast<int>(items.size())));
items.insert(boundedIndex, new QWidgetItem(toInsert));
invalidate();
}
/**
* @brief Retrieves the count of items in the layout.
* @return The number of layout items.
*/
int FlowLayout::count() const
{
return static_cast<int>(items.size());
}
/**
* @brief Returns the layout item at the specified index.
* @param index The index of the item to retrieve.
* @return A pointer to the item at the specified index, or nullptr if out of range.
*/
QLayoutItem *FlowLayout::itemAt(const int index) const
{
return (index >= 0 && index < items.size()) ? items.value(index) : nullptr;
}
/**
* @brief Removes and returns the item at the specified index.
* @param index The index of the item to remove.
* @return A pointer to the removed item, or nullptr if out of range.
*/
QLayoutItem *FlowLayout::takeAt(const int index)
{
return (index >= 0 && index < items.size()) ? items.takeAt(index) : nullptr;
}
/**
* @brief Gets the horizontal spacing between items.
* @return The horizontal spacing if set, otherwise a smart default.
*/
int FlowLayout::horizontalSpacing() const
{
return (horizontalMargin >= 0) ? horizontalMargin : smartSpacing(QStyle::PM_LayoutHorizontalSpacing);
}
/**
* @brief Gets the vertical spacing between items.
* @return The vertical spacing if set, otherwise a smart default.
*/
int FlowLayout::verticalSpacing() const
{
return (verticalMargin >= 0) ? verticalMargin : smartSpacing(QStyle::PM_LayoutVerticalSpacing);
}
/**
* @brief Calculates smart spacing based on the parent widget style.
* @param pm The pixel metric to calculate.
* @return The calculated spacing value.
*/
int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const
{
QObject *parent = this->parent();
if (!parent) {
return -1;
}
if (parent->isWidgetType()) {
const auto *pw = dynamic_cast<QWidget *>(parent);
return pw->style()->pixelMetric(pm, nullptr, pw);
}
return dynamic_cast<QLayout *>(parent)->spacing();
}
/**
* @brief Gets the width of the parent scroll area, if any.
* @return The width of the scroll area's viewport, or 0 if not found.
*/
int FlowLayout::getParentScrollAreaWidth() const
{
QWidget *parent = parentWidget();
while (parent) {
if (const auto *scrollArea = qobject_cast<QScrollArea *>(parent)) {
return scrollArea->viewport()->width();
}
parent = parent->parentWidget();
}
return 0;
}
/**
* @brief Gets the height of the parent scroll area, if any.
* @return The height of the scroll area's viewport, or 0 if not found.
*/
int FlowLayout::getParentScrollAreaHeight() const
{
QWidget *parent = parentWidget();
while (parent) {
if (const auto *scrollArea = qobject_cast<QScrollArea *>(parent)) {
return scrollArea->viewport()->height();
}
parent = parent->parentWidget();
}
return 0;
}

View file

@ -0,0 +1,54 @@
#ifndef FLOW_LAYOUT_H
#define FLOW_LAYOUT_H
#include <QLayout>
#include <QList>
#include <QLoggingCategory>
#include <QWidget>
#include <qstyle.h>
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout", QtInfoMsg);
class FlowLayout : public QLayout
{
public:
explicit FlowLayout(QWidget *parent = nullptr);
FlowLayout(QWidget *parent, Qt::Orientation _flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~FlowLayout() override;
void insertWidgetAtIndex(QWidget *toInsert, int index);
QSize calculateMinimumSizeHorizontal() const;
QSize calculateSizeHintVertical() const;
QSize calculateMinimumSizeVertical() const;
void addItem(QLayoutItem *item) override;
[[nodiscard]] int count() const override;
[[nodiscard]] QLayoutItem *itemAt(int index) const override;
QLayoutItem *takeAt(int index) override;
[[nodiscard]] int horizontalSpacing() const;
[[nodiscard]] Qt::Orientations expandingDirections() const override;
[[nodiscard]] bool hasHeightForWidth() const override;
[[nodiscard]] int heightForWidth(int width) const override;
[[nodiscard]] int verticalSpacing() const;
[[nodiscard]] int doLayout(const QRect &rect, bool testOnly) const;
[[nodiscard]] int smartSpacing(QStyle::PixelMetric pm) const;
[[nodiscard]] int getParentScrollAreaWidth() const;
[[nodiscard]] int getParentScrollAreaHeight() const;
void setGeometry(const QRect &rect) override;
virtual int layoutAllRows(int originX, int originY, int availableWidth);
virtual void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y);
int layoutAllColumns(int originX, int originY, int availableHeight);
void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
[[nodiscard]] QSize sizeHint() const override;
[[nodiscard]] QSize minimumSize() const override;
QSize calculateSizeHintHorizontal() const;
protected:
QList<QLayoutItem *> items; // List to store layout items
Qt::Orientation flowDirection;
int horizontalMargin;
int verticalMargin;
};
#endif // FLOW_LAYOUT_H

View file

@ -0,0 +1,496 @@
#include "overlap_layout.h"
#include <QDebug>
#include <QtMath>
/**
* @class OverlapLayout
* @brief Custom layout class to arrange widgets with overlapping positions.
*
* The OverlapLayout class is a QLayout subclass that arranges child widgets
* in an overlapping configuration, allowing control over the overlap percentage
* and the number of rows or columns based on the chosen layout direction. This
* layout is particularly useful for visualizing elements that need to partially
* stack over one another, either horizontally or vertically.
*/
/**
* @brief Constructs an OverlapLayout with the specified parameters.
*
* Initializes a new OverlapLayout with the given overlap percentage, row or column limit,
* and layout direction. The overlap percentage determines how much each widget will
* overlap with the previous one. If maxColumns or maxRows are set to zero, it implies
* no limit in that respective dimension.
*
* @param overlapPercentage An integer representing the percentage of overlap between items (0-100).
* @param maxColumns The maximum number of columns allowed in the layout when in horizontal orientation (0 for
* unlimited).
* @param maxRows The maximum number of rows allowed in the layout when in vertical orientation (0 for unlimited).
* @param direction The orientation direction of the layout, either Qt::Horizontal or Qt::Vertical.
* @param parent The parent widget of this layout.
*/
OverlapLayout::OverlapLayout(QWidget *parent,
const int overlapPercentage,
const int maxColumns,
const int maxRows,
const Qt::Orientation _overlapDirection,
const Qt::Orientation _flowDirection)
: QLayout(parent), overlapPercentage(overlapPercentage), maxColumns(maxColumns), maxRows(maxRows),
overlapDirection(_overlapDirection), flowDirection(_flowDirection)
{
}
/**
* @brief Destructor for OverlapLayout, ensuring cleanup of all layout items.
*
* Iterates through all layout items and deletes them. This prevents memory
* leaks by removing all child QLayoutItems stored in the layout.
*/
OverlapLayout::~OverlapLayout()
{
QLayoutItem *item;
while ((item = OverlapLayout::takeAt(0))) {
delete item;
}
}
void OverlapLayout::insertWidgetAtIndex(QWidget *toInsert, int index)
{
addChildWidget(toInsert);
int clampedIndex = qBound(0, index, qMax(0, static_cast<int>(itemList.size())));
itemList.insert(clampedIndex, new QWidgetItem(toInsert));
for (int i = clampedIndex; i < itemList.size(); ++i) {
dynamic_cast<QWidgetItem *>(itemList.at(i))->widget()->raise();
}
invalidate();
}
/**
* @brief Adds a new item to the layout.
*
* Appends a QLayoutItem to the internal list, allowing it to be positioned within the
* layout during the next geometry update. This method does not directly arrange the
* items; it merely adds them to the layouts tracking.
*
* @param item Pointer to the QLayoutItem being added to this layout.
*/
void OverlapLayout::addItem(QLayoutItem *item)
{
if (item != nullptr) {
itemList.append(item);
}
}
/**
* @brief Retrieves the total count of items within the layout.
*
* Returns the number of items stored in the layout's internal item list.
* This count reflects how many widgets or spacers the layout is currently managing.
*
* @return Integer count of items in the layout.
*/
int OverlapLayout::count() const
{
return static_cast<int>(itemList.size());
}
/**
* @brief Provides access to a layout item at a specified index.
*
* Allows retrieval of a QLayoutItem from the layouts internal list
* by index. If the index is out of bounds, this function will return nullptr.
*
* @param index The index of the desired item.
* @return Pointer to the QLayoutItem at the specified index, or nullptr if index is invalid.
*/
QLayoutItem *OverlapLayout::itemAt(const int index) const
{
return (index >= 0 && index < itemList.size()) ? itemList.value(index) : nullptr;
}
/**
* @brief Removes and returns a layout item at the specified index.
*
* Removes a QLayoutItem from the layout at the given index, reducing the layout's count.
* If the index is invalid, this function returns nullptr without any effect.
*
* @param index The index of the item to remove.
* @return Pointer to the removed QLayoutItem, or nullptr if index is invalid.
*/
QLayoutItem *OverlapLayout::takeAt(const int index)
{
return (index >= 0 && index < itemList.size()) ? itemList.takeAt(index) : nullptr;
}
/**
* @brief Sets the geometry for the layout items, arranging them with the specified overlap.
* @param rect The rectangle defining the area within which the layout should arrange items.
*/
void OverlapLayout::setGeometry(const QRect &rect)
{
// Call the base class implementation to ensure standard layout behavior.
QLayout::setGeometry(rect);
// If there are no items to layout, exit early.
if (itemList.isEmpty()) {
return;
}
// Get the parent widget for size and margin calculations.
const QWidget *parentWidget = this->parentWidget();
if (!parentWidget) {
return;
}
// Calculate available width and height, subtracting the parent's margins.
int availableWidth = parentWidget->width();
int availableHeight = parentWidget->height();
const QMargins margins = parentWidget->contentsMargins();
availableWidth -= margins.left() + margins.right();
availableHeight -= margins.top() + margins.bottom();
// Determine the maximum item width and height among all layout items.
int maxItemWidth = 0;
int maxItemHeight = 0;
for (QLayoutItem *item : itemList) {
if (item != nullptr && item->widget()) {
QSize itemSize = item->widget()->sizeHint();
maxItemWidth = qMax(maxItemWidth, itemSize.width());
maxItemHeight = qMax(maxItemHeight, itemSize.height());
}
}
// Calculate the overlap offsets based on the layout direction and overlap percentage.
const int overlapOffsetWidth = (overlapDirection == Qt::Horizontal) ? (maxItemWidth * overlapPercentage / 100) : 0;
const int overlapOffsetHeight = (overlapDirection == Qt::Vertical) ? (maxItemHeight * overlapPercentage / 100) : 0;
// Determine the number of columns based on layout constraints and available space.
int columns;
if (flowDirection == Qt::Horizontal) {
if (maxColumns > 0) {
// Calculate the maximum possible columns given the available width and overlap.
const int availableColumns = (availableWidth + overlapOffsetWidth) / (maxItemWidth - overlapOffsetWidth);
// Use the smaller of maxColumns and availableColumns.
qCDebug(OverlapLayoutLog) << " Max Columns " << maxColumns << " available columns " << availableColumns;
columns = qMin(maxColumns, availableColumns);
if (columns < 1) {
columns = 1;
}
} else {
// If no maxColumns constraint, allow as many columns as possible.
columns = INT_MAX;
}
} else {
// If not a horizontal layout, column count is irrelevant.
columns = INT_MAX;
}
// Determine the number of rows based on layout constraints and available space.
int rows;
if (flowDirection == Qt::Vertical) {
if (maxRows > 0) {
// Calculate the maximum possible rows given the available height and overlap.
const int availableRows = (availableHeight + overlapOffsetHeight) / (maxItemHeight - overlapOffsetHeight);
// Use the smaller of maxRows and availableRows.
qCDebug(OverlapLayoutLog) << " Max Rows " << maxRows << " available rows " << availableRows;
rows = qMin(maxRows, availableRows);
if (rows < 1) {
rows = 1;
}
} else {
// If no maxRows constraint, allow as many rows as possible.
rows = INT_MAX;
}
} else {
// If not a vertical layout, row count is irrelevant.
rows = INT_MAX;
}
// Initialize row and column indices.
int currentRow = 0;
int currentColumn = 0;
// Loop through all items and position them based on the calculated offsets.
for (const auto item : itemList) {
if (item == nullptr) {
continue;
}
// Calculate the position of the current item.
const int xPos = rect.left() + currentColumn * (maxItemWidth - overlapOffsetWidth);
const int yPos = rect.top() + currentRow * (maxItemHeight - overlapOffsetHeight);
item->setGeometry(QRect(xPos, yPos, maxItemWidth, maxItemHeight));
// TODO: Figure this out properly or maybe adjust size hint to account for this?
// Update row and column indices based on the layout direction.
if (overlapDirection == Qt::Horizontal) {
currentColumn++;
if (currentColumn > qCeil(itemList.size() / rows)) {
currentColumn = 0;
currentRow++;
}
} else {
currentRow++;
if (currentRow > qCeil(itemList.size() / columns)) {
currentRow = 0;
currentColumn++;
}
}
}
}
/**
* @brief Calculates the preferred size for the layout, considering overlap and orientation.
* @return The preferred layout size as a QSize object.
*/
QSize OverlapLayout::calculatePreferredSize() const
{
// Get the parent widget for size and margin calculations.
const QWidget *parentWidget = this->parentWidget();
if (!parentWidget) {
return QSize(0, 0);
}
if (itemList.isEmpty()) {
return QSize(0, 0);
}
int availableWidth = parentWidget->width();
int availableHeight = parentWidget->height();
const QMargins margins = parentWidget->contentsMargins();
availableWidth -= margins.left() + margins.right();
availableHeight -= margins.top() + margins.bottom();
// Determine the maximum item width and height among all layout items.
int maxItemWidth = 0;
int maxItemHeight = 0;
for (QLayoutItem *item : itemList) {
if (item != nullptr && item->widget()) {
QSize itemSize = item->widget()->sizeHint();
maxItemWidth = qMax(maxItemWidth, itemSize.width());
maxItemHeight = qMax(maxItemHeight, itemSize.height());
}
}
// Calculate the overlap offsets based on the layout direction and overlap percentage.
const int overlapOffsetWidth = (overlapDirection == Qt::Horizontal) ? (maxItemWidth * overlapPercentage / 100) : 0;
const int overlapOffsetHeight = (overlapDirection == Qt::Vertical) ? (maxItemHeight * overlapPercentage / 100) : 0;
// Determine the number of columns based on layout constraints and available space.
int columns;
if (flowDirection == Qt::Horizontal) {
if (maxColumns > 0) {
// Calculate the maximum possible columns given the available width and overlap.
const int availableColumns = (availableWidth + overlapOffsetWidth) / (maxItemWidth - overlapOffsetWidth);
// Use the smaller of maxColumns and availableColumns.
qCDebug(OverlapLayoutLog) << " Max Columns " << maxColumns << " available columns " << availableColumns;
columns = qMin(maxColumns, availableColumns);
if (columns < 1) {
columns = 1;
}
} else {
// If no maxColumns constraint, allow as many columns as possible.
columns = INT_MAX;
}
} else {
// If not a horizontal layout, column count is irrelevant.
columns = INT_MAX;
}
// Determine the number of rows based on layout constraints and available space.
int rows;
if (flowDirection == Qt::Vertical) {
if (maxRows > 0) {
// Calculate the maximum possible rows given the available height and overlap.
const int availableRows = (availableHeight + overlapOffsetHeight) / (maxItemHeight - overlapOffsetHeight);
// Use the smaller of maxRows and availableRows.
qCDebug(OverlapLayoutLog) << " Max Rows " << maxRows << " available rows " << availableRows;
rows = qMin(maxRows, availableRows);
if (rows < 1) {
rows = 1;
}
} else {
// If no maxRows constraint, allow as many rows as possible.
rows = INT_MAX;
}
} else {
// If not a vertical layout, row count is irrelevant.
rows = INT_MAX;
}
if (overlapDirection == Qt::Horizontal) {
return QSize(maxItemWidth + ((qCeil(itemList.size() / rows)) * (maxItemWidth - overlapOffsetWidth)),
rows * maxItemHeight);
} else {
return QSize(columns * maxItemWidth,
maxItemHeight + ((qCeil(itemList.size() / columns)) * (maxItemHeight - overlapOffsetHeight)));
}
}
/**
* @brief Returns the size hint for the layout, based on preferred size calculations.
*
* Provides a recommended size for the layout, useful for layouts that need to fit within
* a specific parent container size. This takes into account the preferred size and
* any specific item size requirements.
*
* @return The layout's recommended QSize.
*/
QSize OverlapLayout::sizeHint() const
{
return calculatePreferredSize();
}
/**
* @brief Provides the minimum size hint for the layout, ensuring functionality within constraints.
*
* Defines a minimum workable size for the layout to prevent excessive compression
* that could distort item arrangement.
*
* @return The minimum QSize for this layout.
*/
QSize OverlapLayout::minimumSize() const
{
return calculatePreferredSize();
}
/**
* @brief Sets the layout's orientation direction.
*
* @param _direction The new orientation direction (Qt::Horizontal or Qt::Vertical).
*/
void OverlapLayout::setDirection(const Qt::Orientation _direction)
{
overlapDirection = _direction;
}
/**
* @brief Sets the maximum number of columns for horizontal orientation.
*
* @param _maxColumns New maximum column count.
*/
void OverlapLayout::setMaxColumns(const int _maxColumns)
{
if (_maxColumns >= 0) {
maxColumns = _maxColumns;
}
}
/**
* @brief Sets the maximum number of rows for vertical orientation.
*
* @param _maxRows New maximum row count.
*/
void OverlapLayout::setMaxRows(const int _maxRows)
{
if (_maxRows >= 0) {
maxRows = _maxRows;
}
}
/**
* @brief Calculates the maximum number of columns for a vertical overlap layout based on the current width.
*
* This function determines the maximum number of columns that can fit within the layout's width
* given the overlap percentage and item size, based on the current layout direction.
*
* @return Maximum number of columns that can fit within the layout width.
*/
int OverlapLayout::calculateMaxColumns() const
{
if (overlapDirection != Qt::Vertical || itemList.isEmpty()) {
return 1; // Only relevant if the layout direction is vertical
}
// Determine maximum item width
int maxItemWidth = 0;
for (QLayoutItem *item : itemList) {
if (item == nullptr || !item->widget()) {
continue;
}
QSize itemSize = item->widget()->sizeHint();
maxItemWidth = qMax(maxItemWidth, itemSize.width());
}
const int availableWidth = parentWidget() ? parentWidget()->width() : 0;
// Determine the maximum number of columns that can fit
const int columns = availableWidth / maxItemWidth;
return qMax(1, columns);
}
/**
* @brief Calculates the maximum number of rows needed for a given number of columns in a vertical overlap layout.
*
* Determines how many rows are required to arrange all items given the calculated or specified number of columns.
*
* @param columns The number of columns available.
* @return The total number of rows required.
*/
int OverlapLayout::calculateRowsForColumns(const int columns) const
{
if (overlapDirection != Qt::Vertical || itemList.isEmpty() || columns <= 0) {
return 1; // Only relevant if the layout direction is vertical and there are items
}
const int totalItems = static_cast<int>(itemList.size());
return qCeil(totalItems / columns);
}
/**
* @brief Calculates the maximum number of rows for a horizontal overlap layout based on the current height.
*
* This function determines the maximum number of rows that can fit within the layout's height
* given the overlap percentage and item size, based on the current layout direction.
*
* @return Maximum number of rows that can fit within the layout height.
*/
int OverlapLayout::calculateMaxRows() const
{
if (overlapDirection != Qt::Horizontal || itemList.isEmpty()) {
return 1; // Only relevant if the layout direction is horizontal
}
// Determine maximum item height
int maxItemHeight = 0;
for (QLayoutItem *item : itemList) {
if (item == nullptr || !item->widget()) {
continue;
}
QSize itemSize = item->widget()->sizeHint();
maxItemHeight = qMax(maxItemHeight, itemSize.height());
}
// Calculate the effective height of each item with the overlap applied
const int overlapOffsetHeight = (maxItemHeight * (100 - overlapPercentage)) / 100;
const int availableHeight = parentWidget() ? parentWidget()->height() : 0;
// Determine the maximum number of rows that can fit
const int rows = availableHeight / overlapOffsetHeight;
return qMax(1, rows);
}
/**
* @brief Calculates the maximum number of columns needed for a given number of rows in a horizontal overlap layout.
*
* Determines how many columns are required to arrange all items given the calculated or specified number of rows.
*
* @param rows The number of rows available.
* @return The total number of columns required.
*/
int OverlapLayout::calculateColumnsForRows(const int rows) const
{
if (overlapDirection != Qt::Horizontal || itemList.isEmpty() || rows <= 0) {
return 1; // Only relevant if the layout direction is horizontal and there are items
}
const int totalItems = static_cast<int>(itemList.size());
return qCeil(totalItems / rows);
}

View file

@ -0,0 +1,50 @@
#ifndef OVERLAP_LAYOUT_H
#define OVERLAP_LAYOUT_H
#include <QLayout>
#include <QList>
#include <QLoggingCategory>
#include <QWidget>
inline Q_LOGGING_CATEGORY(OverlapLayoutLog, "overlap_layout");
class OverlapLayout : public QLayout
{
public:
OverlapLayout(QWidget *parent = nullptr,
int overlapPercentage = 10,
int maxColumns = 2,
int maxRows = 2,
Qt::Orientation overlapDirection = Qt::Vertical,
Qt::Orientation flowDirection = Qt::Horizontal);
~OverlapLayout();
void insertWidgetAtIndex(QWidget *toInsert, int index);
void addItem(QLayoutItem *item) override;
int count() const override;
QLayoutItem *itemAt(int index) const override;
QLayoutItem *takeAt(int index) override;
void setGeometry(const QRect &rect) override;
QSize minimumSize() const override;
QSize sizeHint() const override;
void setMaxColumns(int _maxColumns);
void setMaxRows(int _maxRows);
int calculateMaxColumns() const;
int calculateRowsForColumns(int columns) const;
int calculateMaxRows() const;
int calculateColumnsForRows(int rows) const;
void setDirection(Qt::Orientation _direction);
private:
QList<QLayoutItem *> itemList;
int overlapPercentage;
int maxColumns;
int maxRows;
Qt::Orientation overlapDirection;
Qt::Orientation flowDirection;
// Calculate the preferred size of the layout
QSize calculatePreferredSize() const;
};
#endif // OVERLAP_LAYOUT_H