Add Utility Layouts and corresponding Widgets (#5177)

* Add FlowWidget class with flexible layout and scroll handling

- Implemented FlowWidget class to organize widgets in a flow layout with scrollable options.
- Integrated QScrollArea to handle overflow with configurable horizontal and vertical scroll policies.
- Incorporated dynamic layout selection (FlowLayout, HorizontalFlowLayout, VerticalFlowLayout) based on scroll policy.

* Add OverlapWidget and OverlapLayout for managing overlapping child widgets

- Implemented the OverlapWidget class to manage child widgets in an overlapping manner, supporting configurable overlap percentage, maximum columns, maximum rows, and layout direction.
- Introduced the OverlapLayout class, which arranges widgets with overlapping positions, allowing flexible stacking based on specified parameters.

* Add OverlapControlWidget.

* Delete FlowLayout items later to allow them to finish their event loop.

* Allow OverlapWidgets to adjust their rows/columns on resize.

* Clamp vertical FlowLayout to any available parent scrollAreas.

* Implement margins and spacing for FlowLayouts.

* Adjust/revert some things.

* Address pull request comments (nullptr checks and additional comments, mostly.)

* Reformat code so the linter will stop yelling at me.

* Remove undefined methods from FlowLayouts.

* Fix the build.

* Revert FlowLayout::takeAt to index check.

* Commits will continue until linter morale improves.

* Fix various warnings in FlowLayout.

* Fix various warnings in FlowLayout.h.

* Fix various warnings in the FlowLayout classes.

* Fix [[nodiscard]] warning.

* Fix more warnings.

* Final round of yellow squiggle fixing.

* Linter formatting.

* Refactor column/row calculation to be more readable.

* Code style.

* Address PR comments.

* Combine if-statements.

* Replace std::math functions with Qt equivalents.

* Fix non-consts and QtMath.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2024-11-17 19:32:31 +01:00 committed by GitHub
parent c8336df49d
commit 8ef92d26c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 1832 additions and 42 deletions

View file

@ -58,6 +58,10 @@ set(cockatrice_SOURCES
src/game/filters/filter_builder.cpp
src/game/filters/filter_tree.cpp
src/game/filters/filter_tree_model.cpp
src/client/ui/layouts/flow_layout.cpp
src/client/ui/layouts/horizontal_flow_layout.cpp
src/client/ui/layouts/vertical_flow_layout.cpp
src/client/ui/widgets/general/layout_containers/flow_widget.cpp
src/game/game_scene.cpp
src/game/game_selector.cpp
src/game/games_model.cpp
@ -74,8 +78,12 @@ set(cockatrice_SOURCES
src/utility/logger.cpp
src/client/ui/widgets/cards/card_info_picture_enlarged_widget.cpp
src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp
src/client/ui/widgets/general/display/labeled_input.cpp
src/main.cpp
src/server/message_log_widget.cpp
src/client/ui/layouts/overlap_layout.cpp
src/client/ui/widgets/general/layout_containers/overlap_widget.cpp
src/client/ui/widgets/general/layout_containers/overlap_control_widget.cpp
src/server/pending_command.cpp
src/game/phase.cpp
src/client/ui/phases_toolbar.cpp

View file

@ -0,0 +1,337 @@
/**
* @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 int margin, const int hSpacing, const int vSpacing)
: QLayout(parent), 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
{
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) { // Start a new row if the row width exceeds available width
height += rowHeight + verticalSpacing();
rowWidth = itemWidth;
rowHeight = item->sizeHint().height() + verticalSpacing();
} else {
rowWidth += itemWidth;
rowHeight = qMax(rowHeight, item->sizeHint().height());
}
}
height += rowHeight; // Add the final row's height
return height;
}
/**
* @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.
int left, top, right, bottom;
getContentsMargins(&left, &top, &right, &bottom); // Retrieves the layout's content margins.
// Adjust the rectangle to exclude margins.
const QRect adjustedRect = rect.adjusted(+left, +top, -right, -bottom);
// Calculate the available width for items, considering either the adjusted rectangle's width
// or the parent scroll area width, if applicable.
const int availableWidth = qMax(adjustedRect.width(), getParentScrollAreaWidth());
// Arrange all rows of items within the available width and get the total height used.
const int totalHeight = layoutAllRows(adjustedRect.x(), adjustedRect.y(), availableWidth);
// If the layout's parent is a QWidget, update its minimum size to ensure it can accommodate
// the arranged items' dimensions.
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setMinimumSize(availableWidth, totalHeight);
}
}
/**
* @brief Arranges items in rows based on the available width.
* Items are added to a row until the row's width exceeds `availableWidth`.
* Then, a new row is started.
* @param originX The starting x-coordinate for the row layout.
* @param originY The starting y-coordinate for the row layout.
* @param availableWidth The available width to lay out items.
* @return The y-coordinate of the final row's end position.
*/
int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
{
QVector<QLayoutItem *> rowItems; // Temporary storage for items in the current row.
int currentXPosition = originX; // Tracks the x-coordinate for placing items in the current row.
int currentYPosition = originY; // Tracks the y-coordinate, updated after each row.
int rowHeight = 0; // Tracks the maximum height of items in the current row.
// Iterate through all layout items to arrange them.
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemSize = item->sizeHint(); // The suggested size for the item.
const int itemWidth = itemSize.width() + horizontalSpacing();
// Check if the item fits in the current row's remaining width.
if (currentXPosition + itemWidth > availableWidth) {
// If not, layout the current row and start a new row.
layoutSingleRow(rowItems, originX, currentYPosition);
rowItems.clear(); // Clear the temporary storage for the new row.
currentXPosition = originX; // Reset x-position to the start of the new row.
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down for the new 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 height to the tallest item.
currentXPosition += itemSize.width() + horizontalSpacing(); // Move x-position for the next item.
}
// Layout the final row if there are remaining items.
layoutSingleRow(rowItems, originX, currentYPosition);
currentYPosition += rowHeight; // Add the final row's height
return currentYPosition;
}
/**
* @brief Helper function for arranging a single row of items within specified bounds.
* @param rowItems Items to be arranged in the row.
* @param x The x-coordinate for starting the row.
* @param y The y-coordinate for starting the row.
*/
void FlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, const int y)
{
// Iterate through each item in the row and position it.
for (QLayoutItem *item : rowItems) {
if (item == nullptr || item->isEmpty()) {
continue;
}
QSize itemMaxSize = item->widget()->maximumSize(); // Get the item's maximum allowable size.
// Constrain the item's width and height to its size hint or maximum size.
int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width());
int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height());
// Set the item's geometry based on the calculated size and position.
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the x-position for the next item, including horizontal spacing.
x += itemWidth + horizontalSpacing();
}
}
/**
* @brief Returns the preferred size for this layout.
* @return The maximum of all item size hints as a QSize.
*/
QSize FlowLayout::sizeHint() const
{
QSize size;
for (const QLayoutItem *item : items) {
if (item != nullptr && !item->isEmpty()) {
size = size.expandedTo(item->sizeHint());
}
}
return size.isValid() ? size : QSize(0, 0);
}
/**
* @brief Returns the minimum size required to display all layout items.
* @return The minimum QSize needed by the layout.
*/
QSize FlowLayout::minimumSize() const
{
QSize size;
for (const QLayoutItem *item : items) {
if (item != nullptr && !item->isEmpty()) {
size = size.expandedTo(item->minimumSize());
}
}
size.setWidth(qMin(size.width(), getParentScrollAreaWidth()));
size.setHeight(qMin(size.height(), getParentScrollAreaHeight()));
return size.isValid() ? size : QSize(0, 0);
}
/**
* @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);
}
}
/**
* @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,43 @@
#ifndef FLOW_LAYOUT_H
#define FLOW_LAYOUT_H
#include <QLayout>
#include <QList>
#include <QWidget>
#include <qstyle.h>
class FlowLayout : public QLayout
{
public:
explicit FlowLayout(QWidget *parent = nullptr);
FlowLayout(QWidget *parent, int margin, int hSpacing, int vSpacing);
~FlowLayout() override;
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);
[[nodiscard]] QSize sizeHint() const override;
[[nodiscard]] QSize minimumSize() const override;
protected:
QList<QLayoutItem *> items; // List to store layout items
int horizontalMargin;
int verticalMargin;
};
#endif // FLOW_LAYOUT_H

View file

@ -0,0 +1,142 @@
#include "horizontal_flow_layout.h"
/**
* @brief Constructs a HorizontalFlowLayout instance with the specified parent widget.
* This layout arranges items in columns within the given height, automatically adjusting its width.
* @param parent The parent widget to which this layout belongs.
* @param margin The layout margin.
* @param hSpacing The horizontal spacing between items.
* @param vSpacing The vertical spacing between items.
*/
HorizontalFlowLayout::HorizontalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
: FlowLayout(parent, margin, hSpacing, vSpacing)
{
}
/**
* @brief Destructor for HorizontalFlowLayout, responsible for cleaning up layout items.
*/
HorizontalFlowLayout::~HorizontalFlowLayout()
{
QLayoutItem *item;
while ((item = FlowLayout::takeAt(0))) {
delete item;
}
}
/**
* @brief Calculates the required width to display all items, given a specified height.
* This method arranges items into columns and determines the total width needed.
* @param height The available height for arranging layout items.
* @return The total width required to fit all items, organized in columns constrained by the given height.
*/
int HorizontalFlowLayout::heightForWidth(const int height) const
{
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 > height) {
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 Sets the geometry of the layout items, arranging them in columns within the given height.
* @param rect The rectangle area defining the layout space.
*/
void HorizontalFlowLayout::setGeometry(const QRect &rect)
{
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setMinimumSize(totalWidth, availableHeight);
}
}
/**
* @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 HorizontalFlowLayout::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 HorizontalFlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
{
for (QLayoutItem *item : colItems) {
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 y-position down by the item's height to place the next item below
y += itemHeight;
}
}

View file

@ -0,0 +1,19 @@
#ifndef HORIZONTAL_FLOW_LAYOUT_H
#define HORIZONTAL_FLOW_LAYOUT_H
#include "flow_layout.h"
class HorizontalFlowLayout : public FlowLayout
{
public:
explicit HorizontalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~HorizontalFlowLayout() override;
[[nodiscard]] int heightForWidth(int height) const override;
void setGeometry(const QRect &rect) override;
int layoutAllColumns(int originX, int originY, int availableHeight);
static void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
};
#endif // HORIZONTAL_FLOW_LAYOUT_H

View file

@ -0,0 +1,474 @@
#include "overlap_layout.h"
#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 direction)
: QLayout(parent), overlapPercentage(overlapPercentage), maxColumns(maxColumns), maxRows(maxRows),
direction(direction)
{
}
/**
* @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;
}
}
/**
* @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 = (direction == Qt::Horizontal) ? (maxItemWidth * overlapPercentage / 100) : 0;
const int overlapOffsetHeight = (direction == Qt::Vertical) ? (maxItemHeight * overlapPercentage / 100) : 0;
// Determine the number of columns based on layout constraints and available space.
int columns;
if (direction == 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.
columns = qMin(maxColumns, availableColumns);
} 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 (direction == 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.
rows = qMin(maxRows, availableRows);
} 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));
// Update row and column indices based on the layout direction.
if (direction == Qt::Horizontal) {
currentColumn++;
if (currentColumn >= columns) {
currentColumn = 0;
currentRow++;
}
} else {
currentRow++;
if (currentRow >= rows) {
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
{
// Determine the maximum item dimensions.
int maxItemWidth = 0;
int maxItemHeight = 0;
for (QLayoutItem *item : itemList) {
if (item == nullptr) {
continue;
}
if (!item->widget()) {
continue;
}
QSize itemSize = item->widget()->sizeHint();
maxItemWidth = qMax(maxItemWidth, itemSize.width());
maxItemHeight = qMax(maxItemHeight, itemSize.height());
}
// Calculate the overlap offsets.
const int extra_for_overlap = (100 - overlapPercentage);
const int overlapOffsetWidth =
(direction == Qt::Horizontal) ? qRound((maxItemWidth / 100.0) * extra_for_overlap) : 0;
const int overlapOffsetHeight =
(direction == Qt::Vertical) ? qRound((maxItemHeight / 100.0) * extra_for_overlap) : 0;
// Variables to hold the total dimensions based on layout orientation.
int totalWidth = 0;
int totalHeight = 0;
// Calculate the total size based on the layout direction and constraints.
if (direction == Qt::Horizontal) {
// Determine the number of columns:
// - Use maxColumns if it is greater than 0 (constraint set by the user).
// - Otherwise, set the number of columns to the total number of items in the layout.
const int numColumns = (maxColumns > 0) ? static_cast<int>(maxColumns) : static_cast<int>(itemList.size());
// Calculate the extra space required for overlaps between columns:
// - Each overlap reduces the effective width of subsequent items.
const int extra_space_for_overlaps = overlapOffsetWidth * (numColumns - 1);
// Total width:
// - The first item's width is fully included (maxItemWidth).
// - Add the space for all overlaps between adjacent items.
totalWidth = maxItemWidth + extra_space_for_overlaps;
// Determine the number of rows:
// - Use maxRows if maxColumns is set (to constrain the number of rows).
// - Otherwise, assume a single row.
const int numRows =
(maxColumns > 0) ? qCeil(static_cast<double>(itemList.size()) / static_cast<double>(numColumns)) : 1;
// Total height:
// - Multiply the number of rows by the item height (maxItemHeight).
// - Subtract the height overlap between rows to avoid double-counting.
totalHeight = maxItemHeight * numRows - (numRows - 1) * overlapOffsetHeight;
} else if (direction == Qt::Vertical) {
// Determine the number of columns:
// - Use maxRows to calculate how many columns are needed if a row constraint exists.
// - Otherwise, assume a single column.
const int numColumns =
(maxRows != 0) ? qCeil(static_cast<double>(itemList.size()) / static_cast<double>(maxRows)) : 1;
// Total width:
// - Multiply the number of columns by the item width (maxItemWidth).
// - Subtract the width overlap between columns to avoid double-counting.
totalWidth = maxItemWidth * numColumns - (numColumns - 1) * overlapOffsetWidth;
// Determine the number of rows:
// - Use maxRows if it is greater than 0 (constraint set by the user).
// - Otherwise, set the number of rows to the total number of items in the layout.
const int numRows = (maxRows > 0) ? static_cast<int>(maxRows) : static_cast<int>(itemList.size());
// Calculate the extra space required for overlaps between rows:
// - Each overlap reduces the effective height of subsequent items.
const int extraSpaceForOverlaps = overlapOffsetHeight * (numRows - 1);
// Total height:
// - The first item's height is fully included (maxItemHeight).
// - Add the space for all overlaps between adjacent items.
totalHeight = maxItemHeight + extraSpaceForOverlaps;
}
return {totalWidth, totalHeight};
}
/**
* @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)
{
direction = _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 (direction != 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 (direction != 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 (direction != 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 (direction != 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,44 @@
#ifndef OVERLAP_LAYOUT_H
#define OVERLAP_LAYOUT_H
#include <QLayout>
#include <QList>
#include <QWidget>
class OverlapLayout : public QLayout
{
public:
OverlapLayout(QWidget *parent = nullptr,
int overlapPercentage = 10,
int maxColumns = 2,
int maxRows = 2,
Qt::Orientation direction = Qt::Horizontal);
~OverlapLayout();
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 direction;
// Calculate the preferred size of the layout
QSize calculatePreferredSize() const;
};
#endif // OVERLAP_LAYOUT_H

View file

@ -0,0 +1,144 @@
#include "vertical_flow_layout.h"
/**
* @brief Constructs a VerticalFlowLayout instance with the specified parent widget.
* This layout arranges items in rows within the given width, automatically adjusting its height.
* @param parent The parent widget to which this layout belongs.
* @param margin The layout margin.
* @param hSpacing The horizontal spacing between items.
* @param vSpacing The vertical spacing between items.
*/
VerticalFlowLayout::VerticalFlowLayout(QWidget *parent, const int margin, const int hSpacing, const int vSpacing)
: FlowLayout(parent, margin, hSpacing, vSpacing)
{
}
/**
* @brief Destructor for VerticalFlowLayout, responsible for cleaning up layout items.
*/
VerticalFlowLayout::~VerticalFlowLayout()
{
QLayoutItem *item;
while ((item = FlowLayout::takeAt(0))) {
delete item;
}
}
/**
* @brief Calculates the required height to display all items, given a specified width.
* This method arranges items into rows and determines the total height needed.
* @param width The available width for arranging layout items.
* @return The total height required to fit all items, organized in rows constrained by the given width.
*/
int VerticalFlowLayout::heightForWidth(const int width) const
{
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;
}
/**
* @brief Sets the geometry of the layout items, arranging them in rows within the given width.
* @param rect The rectangle area defining the layout space.
*/
void VerticalFlowLayout::setGeometry(const QRect &rect)
{
// 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->setMinimumSize(availableWidth, totalHeight);
}
}
/**
* @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 VerticalFlowLayout::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 VerticalFlowLayout::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();
}
}

View file

@ -0,0 +1,19 @@
#ifndef VERTICAL_FLOW_LAYOUT_H
#define VERTICAL_FLOW_LAYOUT_H
#include "flow_layout.h"
class VerticalFlowLayout : public FlowLayout
{
public:
explicit VerticalFlowLayout(QWidget *parent = nullptr, int margin = 0, int hSpacing = 0, int vSpacing = 0);
~VerticalFlowLayout() override;
[[nodiscard]] int heightForWidth(int width) const override;
void setGeometry(const QRect &rect) override;
int layoutAllRows(int originX, int originY, int availableWidth) override;
void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y) override;
};
#endif // VERTICAL_FLOW_LAYOUT_H

View file

@ -66,7 +66,7 @@ void CardInfoPictureWithTextOverlayWidget::setOutlineColor(const QColor &color)
*/
void CardInfoPictureWithTextOverlayWidget::setFontSize(const int size)
{
fontSize = size;
fontSize = size > 0 ? size : 1;
update();
}
@ -109,48 +109,50 @@ void CardInfoPictureWithTextOverlayWidget::paintEvent(QPaintEvent *event)
// Get the pixmap from the base class using the getter
const QPixmap &pixmap = getResizedPixmap();
if (!pixmap.isNull()) {
// Calculate size and position for drawing
const QSize scaledSize = pixmap.size().scaled(size(), Qt::KeepAspectRatio);
const QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
const QRect pixmapRect(topLeft, scaledSize);
// Prepare text wrapping
const QFontMetrics fontMetrics(font);
const int lineHeight = fontMetrics.height();
const int textWidth = pixmapRect.width();
QString wrappedText;
// Break the text into multiple lines to fit within the pixmap width
QString currentLine;
QStringList words = overlayText.split(' ');
for (const QString &word : words) {
if (fontMetrics.horizontalAdvance(currentLine + " " + word) > textWidth) {
wrappedText += currentLine + '\n';
currentLine = word;
} else {
if (!currentLine.isEmpty()) {
currentLine += " ";
}
currentLine += word;
}
}
wrappedText += currentLine;
// Calculate total text block height
const int totalTextHeight = static_cast<int>(wrappedText.count('\n')) * lineHeight + lineHeight;
// Set up the text layout options
QTextOption textOption;
textOption.setAlignment(textAlignment);
// Create a text rectangle centered within the pixmap rect
auto textRect = QRect(pixmapRect.left(), pixmapRect.top(), pixmapRect.width(), totalTextHeight);
textRect.moveTop((pixmapRect.height() - totalTextHeight) / 2 + pixmapRect.top());
// Draw the outlined text
drawOutlinedText(painter, textRect, wrappedText, textOption);
if (pixmap.isNull()) {
return;
}
// Calculate size and position for drawing
const QSize scaledSize = pixmap.size().scaled(size(), Qt::KeepAspectRatio);
const QPoint topLeft{(width() - scaledSize.width()) / 2, (height() - scaledSize.height()) / 2};
const QRect pixmapRect(topLeft, scaledSize);
// Prepare text wrapping
const QFontMetrics fontMetrics(font);
const int lineHeight = fontMetrics.height();
const int textWidth = pixmapRect.width();
QString wrappedText;
// Break the text into multiple lines to fit within the pixmap width
QString currentLine;
QStringList words = overlayText.split(' ');
for (const QString &word : words) {
if (fontMetrics.horizontalAdvance(currentLine + " " + word) > textWidth) {
wrappedText += currentLine + '\n';
currentLine = word;
} else {
if (!currentLine.isEmpty()) {
currentLine += " ";
}
currentLine += word;
}
}
wrappedText += currentLine;
// Calculate total text block height
const int totalTextHeight = static_cast<int>(wrappedText.count('\n')) * lineHeight + lineHeight;
// Set up the text layout options
QTextOption textOption;
textOption.setAlignment(textAlignment);
// Create a text rectangle centered within the pixmap rect
auto textRect = QRect(pixmapRect.left(), pixmapRect.top(), pixmapRect.width(), totalTextHeight);
textRect.moveTop((pixmapRect.height() - totalTextHeight) / 2 + pixmapRect.top());
// Draw the outlined text
drawOutlinedText(painter, textRect, wrappedText, textOption);
}
/**

View file

@ -0,0 +1,39 @@
#include "labeled_input.h"
LabeledInput::LabeledInput(QWidget *parent, const QString &labelText) : QWidget(parent)
{
label = new QLabel(labelText, this);
layout = new QHBoxLayout(this);
layout->addWidget(label);
}
QSpinBox *LabeledInput::addSpinBox(const int minValue, const int maxValue, const int defaultValue)
{
auto *spinBox = new QSpinBox(this);
spinBox->setRange(minValue, maxValue);
spinBox->setValue(defaultValue);
layout->addWidget(spinBox);
connect(spinBox, SIGNAL(valueChanged(int)), this, SIGNAL(spinBoxValueChanged(int)));
return spinBox;
}
// Add a QComboBox (for arbitrary selections)
QComboBox *LabeledInput::addComboBox(const QStringList &items, const QString &defaultItem)
{
auto *comboBox = new QComboBox(this);
comboBox->addItems(items);
if (!defaultItem.isEmpty()) {
comboBox->setCurrentText(defaultItem);
}
layout->addWidget(comboBox);
return comboBox;
}
// Add a QComboBox specifically for Qt Directions
QComboBox *LabeledInput::addDirectionComboBox()
{
const QStringList directions = {"Qt::Horizontal", "Qt::Vertical"};
const auto comboBox = addComboBox(directions, "Qt::Vertical");
connect(comboBox, SIGNAL(currentTextChanged(QString)), this, SIGNAL(directionComboBoxChanged(QString)));
return comboBox;
}

View file

@ -0,0 +1,36 @@
#ifndef LABELED_INPUT_H
#define LABELED_INPUT_H
#include <QComboBox>
#include <QHBoxLayout>
#include <QLabel>
#include <QSpinBox>
#include <QWidget>
class LabeledInput final : public QWidget
{
Q_OBJECT
public:
explicit LabeledInput(QWidget *parent, const QString &labelText);
// Add a QSpinBox (for arbitrary numbers)
QSpinBox *addSpinBox(int minValue, int maxValue, int defaultValue = 0);
// Add a QComboBox (for arbitrary selections)
QComboBox *addComboBox(const QStringList &items, const QString &defaultItem = QString());
// Add a QComboBox specifically for Qt Directions
QComboBox *addDirectionComboBox();
signals:
void spinBoxValueChanged(int newValue); // Declare the valueChanged signal
void comboBoxValueChanged(int newValue);
void directionComboBoxChanged(QString newDirection);
private:
QLabel *label;
QHBoxLayout *layout;
};
#endif // LABELED_INPUT_H

View file

@ -0,0 +1,141 @@
/**
* @file flow_widget.cpp
* @brief Implementation of the FlowWidget class for organizing widgets in a flow layout within a scrollable area.
*/
#include "flow_widget.h"
#include "../../../layouts/flow_layout.h"
#include "../../../layouts/horizontal_flow_layout.h"
#include "../../../layouts/vertical_flow_layout.h"
#include <QHBoxLayout>
#include <QWidget>
#include <qscrollarea.h>
#include <qsizepolicy.h>
/**
* @brief Constructs a FlowWidget with a scrollable layout.
*
* @param parent The parent widget of this FlowWidget.
* @param horizontalPolicy The horizontal scroll bar policy for the scroll area.
* @param verticalPolicy The vertical scroll bar policy for the scroll area.
*/
FlowWidget::FlowWidget(QWidget *parent,
const Qt::ScrollBarPolicy horizontalPolicy,
const Qt::ScrollBarPolicy verticalPolicy)
: QWidget(parent)
{
// Main Widget and Layout
this->setMinimumSize(0, 0);
this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
main_layout = new QHBoxLayout();
this->setLayout(main_layout);
// Flow Layout inside the scroll area
container = new QWidget();
if (horizontalPolicy != Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) {
flow_layout = new HorizontalFlowLayout(container);
} else if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy != Qt::ScrollBarAlwaysOff) {
flow_layout = new VerticalFlowLayout(container);
} else {
flow_layout = new FlowLayout(container, 0, 0, 0);
}
container->setLayout(flow_layout);
// The container should expand as much as possible, trusting the scrollArea to constrain it.
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
container->setMinimumSize(0, 0);
// Scroll Area, which should expand as much as possible, since it should be the only direct child widget.
scrollArea = new QScrollArea();
scrollArea->setWidgetResizable(true);
scrollArea->setMinimumSize(0, 0);
scrollArea->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// Set scrollbar policies
scrollArea->setHorizontalScrollBarPolicy(horizontalPolicy);
scrollArea->setVerticalScrollBarPolicy(verticalPolicy);
// Use the FlowLayout container directly if we disable the ScrollArea
if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) {
main_layout->addWidget(container);
} else {
scrollArea->setWidget(container);
main_layout->addWidget(scrollArea);
}
}
/**
* @brief Adds a widget to the flow layout within the FlowWidget.
*
* Adjusts the widget's size policy based on the scroll bar policies.
*
* @param widget_to_add The widget to add to the flow layout.
*/
void FlowWidget::addWidget(QWidget *widget_to_add) const
{
// Adjust size policy if scrollbars are disabled
if (scrollArea->horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOff) {
widget_to_add->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
}
if (scrollArea->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOff) {
widget_to_add->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
}
// Add the widget to the flow layout
this->flow_layout->addWidget(widget_to_add);
}
/**
* @brief Clears all widgets from the flow layout.
*
* Deletes each widget and layout item, and recreates the flow layout if it was removed.
*/
void FlowWidget::clearLayout()
{
if (flow_layout != nullptr) {
QLayoutItem *item;
while ((item = flow_layout->takeAt(0)) != nullptr) {
item->widget()->deleteLater(); // Delete the widget
delete item; // Delete the layout item
}
} else {
if (scrollArea->horizontalScrollBarPolicy() != Qt::ScrollBarAlwaysOff &&
scrollArea->verticalScrollBarPolicy() == Qt::ScrollBarAlwaysOff) {
flow_layout = new HorizontalFlowLayout(container);
} else if (scrollArea->horizontalScrollBarPolicy() == Qt::ScrollBarAlwaysOff &&
scrollArea->verticalScrollBarPolicy() != Qt::ScrollBarAlwaysOff) {
flow_layout = new VerticalFlowLayout(container);
} else {
flow_layout = new FlowLayout(container, 0, 0, 0);
}
this->container->setLayout(flow_layout);
}
}
/**
* @brief Handles resize events for the FlowWidget.
*
* Triggers layout recalculation and adjusts the scroll area content size.
*
* @param event The resize event containing the new size information.
*/
void FlowWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
// Trigger the layout to recalculate
if (flow_layout != nullptr) {
flow_layout->invalidate(); // Marks the layout as dirty and requires recalculation
flow_layout->activate(); // Recalculate the layout based on the new size
}
// Ensure the scroll area and its content adjust correctly
if (scrollArea != nullptr) {
if (scrollArea->widget() != nullptr) {
scrollArea->widget()->adjustSize();
}
}
}

View file

@ -0,0 +1,29 @@
#ifndef FLOW_WIDGET_H
#define FLOW_WIDGET_H
#include "../../../layouts/flow_layout.h"
#include <QHBoxLayout>
#include <QWidget>
#include <qscrollarea.h>
class FlowWidget final : public QWidget
{
Q_OBJECT
public:
FlowWidget(QWidget *parent, Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy verticalPolicy);
void addWidget(QWidget *widget_to_add) const;
void clearLayout();
QScrollArea *scrollArea;
protected:
void resizeEvent(QResizeEvent *event) override;
private:
QHBoxLayout *main_layout;
FlowLayout *flow_layout;
QWidget *container;
};
#endif // FLOW_WIDGET_H

View file

@ -0,0 +1,46 @@
#include "overlap_control_widget.h"
#include "overlap_widget.h"
OverlapControlWidget::OverlapControlWidget(int overlapPercentage,
int maxColumns,
int maxRows,
Qt::Orientation direction,
QWidget *parent)
: QWidget(parent), overlapPercentage(overlapPercentage), maxColumns(maxColumns), maxRows(maxRows),
direction(direction)
{
// Main Widget and Layout
this->setMinimumSize(0, 100);
// this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
// this->setStyleSheet("border: 10px solid red;");
layout = new QHBoxLayout(this);
this->setLayout(layout);
card_size_slider = new QSlider(Qt::Horizontal);
card_size_slider->setRange(1, 10); // Example range for scaling, adjust as needed
amount_of_items_to_overlap = new LabeledInput(this, tr("Cards to overlap:"));
amount_of_items_to_overlap->addSpinBox(0, 999, 10);
overlap_percentage_input = new LabeledInput(this, tr("Overlap percentage:"));
overlap_percentage_input->addSpinBox(0, 100, 80);
overlap_direction = new LabeledInput(this, tr("Overlap direction:"));
overlap_direction->addDirectionComboBox();
layout->addWidget(card_size_slider);
layout->addWidget(amount_of_items_to_overlap);
layout->addWidget(overlap_percentage_input);
layout->addWidget(overlap_direction);
// TODO probably connect this to the parent
// connect(card_size_slider, &QSlider::valueChanged, display, &CardPicture::setScaleFactor);
}
void OverlapControlWidget::connectOverlapWidget(OverlapWidget *overlap_widget)
{
connect(amount_of_items_to_overlap, &LabeledInput::spinBoxValueChanged, overlap_widget,
&OverlapWidget::maxOverlapItemsChanged);
connect(overlap_direction, &LabeledInput::directionComboBoxChanged, overlap_widget,
&OverlapWidget::overlapDirectionChanged);
}

View file

@ -0,0 +1,34 @@
#ifndef OVERLAP_CONTROL_WIDGET_H
#define OVERLAP_CONTROL_WIDGET_H
#include "../display/labeled_input.h"
#include "overlap_widget.h"
#include <QHBoxLayout>
#include <QSlider>
#include <QWidget>
class OverlapControlWidget final : public QWidget
{
Q_OBJECT
public:
OverlapControlWidget(int overlapPercentage,
int maxColumns,
int maxRows,
Qt::Orientation direction,
QWidget *parent);
void connectOverlapWidget(OverlapWidget *overlap_widget);
private:
QHBoxLayout *layout;
QSlider *card_size_slider;
LabeledInput *amount_of_items_to_overlap;
LabeledInput *overlap_percentage_input;
LabeledInput *overlap_direction;
int overlapPercentage;
int maxColumns;
int maxRows;
Qt::Orientation direction;
};
#endif // OVERLAP_CONTROL_WIDGET_H

View file

@ -0,0 +1,194 @@
#include "overlap_widget.h"
#include "../../../../../deck/deck_list_model.h"
#include "../../../layouts/flow_layout.h"
#include <QWidget>
/**
* @class OverlapWidget
* @brief A widget for managing overlapping child widgets.
*
* The OverlapWidget class is a QWidget subclass that utilizes the OverlapLayout
* to arrange its child widgets in an overlapping manner. This widget allows
* configuration of overlap percentage, maximum columns, maximum rows, and layout
* direction, making it suitable for displaying elements that can partially stack
* over each other. The widget automatically manages resizing and re-layout of its
* child widgets based on the available space and specified parameters.
*/
/**
* @brief Constructs an OverlapWidget with specified layout parameters.
*
* Initializes the OverlapWidget with the given overlap percentage, maximum number
* of columns and rows, and layout direction. Sets size policies to ensure the widget
* can expand as needed. A new OverlapLayout is created and assigned to manage the
* layout of child widgets.
*
* @param overlapPercentage The percentage of overlap between child widgets (0-100).
* @param maxColumns The maximum number of columns for the layout (0 for unlimited).
* @param maxRows The maximum number of rows for the layout (0 for unlimited).
* @param direction The orientation of the layout, either Qt::Horizontal or Qt::Vertical.
* @param adjustOnResize If the overlap widgets should adjust its max columns/rows on resize to fit.
* @param parent The parent widget of this OverlapWidget.
*/
OverlapWidget::OverlapWidget(QWidget *parent,
const int overlapPercentage,
const int maxColumns,
const int maxRows,
const Qt::Orientation direction,
const bool adjustOnResize)
: QWidget(parent), overlapPercentage(overlapPercentage), maxColumns(maxColumns), maxRows(maxRows),
direction(direction), adjustOnResize(adjustOnResize)
{
this->setMinimumSize(0, 0);
overlapLayout = new OverlapLayout(this, overlapPercentage, maxColumns, maxRows, direction);
this->setLayout(overlapLayout);
}
/**
* @brief Adds a widget to the overlap layout.
*
* This method appends the specified widget to the internal OverlapLayout, allowing
* it to be arranged with the existing child widgets. The widget's visibility and
* behavior will be managed by the layout.
*
* @param widgetToAdd A pointer to the QWidget to be added to the layout.
*/
void OverlapWidget::addWidget(QWidget *widgetToAdd) const
{
this->overlapLayout->addWidget(widgetToAdd);
}
/**
* @brief Clears all widgets from the layout and deletes them.
*
* This method removes all child widgets from the OverlapLayout, deleting both the
* widget instances and their corresponding layout items. This ensures that the layout
* is empty and can be reused or refreshed as needed.
*/
void OverlapWidget::clearLayout()
{
if (overlapLayout != nullptr) {
QLayoutItem *item;
while ((item = overlapLayout->takeAt(0)) != nullptr) {
delete item->widget(); // Delete the widget
delete item; // Delete the layout item
}
}
// If layout is null, create a new layout; otherwise, reuse the existing one
if (overlapLayout == nullptr) {
overlapLayout = new OverlapLayout(this, overlapPercentage, maxColumns, maxRows, direction);
this->setLayout(overlapLayout);
}
}
/**
* @brief Handles resizing events for the widget.
*
* This overridden method is called when the widget is resized. It invokes layout
* recalculation to ensure that the child widgets are correctly arranged based on the
* new dimensions. It marks the layout as dirty and activates it to reflect the changes.
*
* @param event The resize event containing the new size information.
*/
void OverlapWidget::resizeEvent(QResizeEvent *event)
{
QWidget::resizeEvent(event);
// Trigger the layout to recalculate
if (overlapLayout != nullptr) {
overlapLayout->invalidate(); // Marks the layout as dirty and requires recalculation
overlapLayout->activate(); // Recalculate the layout based on the new size
}
if (adjustOnResize) {
adjustMaxColumnsAndRows();
}
}
/**
* @brief Dynamically adjusts maxColumns and maxRows based on widget size and layout direction.
*
* This function calculates the maximum number of columns or rows that can fit within
* the widget's width and height, depending on the layout direction. It then updates
* the OverlapLayout with these calculated values to ensure the layout is optimized.
*/
void OverlapWidget::adjustMaxColumnsAndRows()
{
if (direction == Qt::Vertical) {
// Calculate columns based on width for vertical layout
const int calculatedColumns = overlapLayout->calculateMaxColumns();
maxColumns = calculatedColumns;
overlapLayout->setMaxColumns(calculatedColumns);
// Calculate rows based on total item count and columns
const int calculatedRows = overlapLayout->calculateRowsForColumns(calculatedColumns);
maxRows = calculatedRows;
overlapLayout->setMaxRows(calculatedRows);
} else {
// Calculate rows based on height for horizontal layout
const int calculatedRows = overlapLayout->calculateMaxRows();
maxRows = calculatedRows;
overlapLayout->setMaxRows(calculatedRows);
// Calculate columns based on total item count and rows
const int calculatedColumns = overlapLayout->calculateColumnsForRows(calculatedRows);
maxColumns = calculatedColumns;
overlapLayout->setMaxColumns(calculatedColumns);
}
overlapLayout->invalidate();
overlapLayout->activate();
}
/**
* @brief Updates the maximum number of overlapping items based on new value.
*
* This method updates the maximum number of columns or rows for the overlap layout
* based on the given new value. It adjusts the layout direction accordingly and
* triggers a size adjustment for the widget, ensuring the layout reflects the changes.
*
* @param newValue The new maximum number of overlapping items allowed in the layout.
*/
void OverlapWidget::maxOverlapItemsChanged(const int newValue)
{
if (direction == Qt::Horizontal) {
maxRows = 0;
overlapLayout->setMaxRows(0);
maxColumns = newValue;
overlapLayout->setMaxColumns(newValue);
} else {
maxRows = newValue;
overlapLayout->setMaxRows(newValue);
maxColumns = 0;
overlapLayout->setMaxColumns(0);
}
this->adjustSize();
overlapLayout->invalidate();
}
/**
* @brief Changes the layout direction based on the specified new direction.
*
* This method modifies the layout direction of the OverlapLayout based on the input
* string. It updates the direction and triggers a size adjustment for the widget.
* Valid inputs are "Qt::Horizontal" and "Qt::Vertical".
*
* @param newDirection The new layout direction as a QString.
*/
void OverlapWidget::overlapDirectionChanged(const QString &newDirection)
{
if (newDirection.compare("Qt::Horizontal", Qt::CaseInsensitive) == 0) {
direction = Qt::Horizontal;
overlapLayout->setDirection(direction);
} else if (newDirection.compare("Qt::Vertical", Qt::CaseInsensitive) == 0) {
direction = Qt::Vertical;
overlapLayout->setDirection(direction);
}
this->adjustSize();
overlapLayout->invalidate();
}

View file

@ -0,0 +1,39 @@
#ifndef OVERLAP_WIDGET_H
#define OVERLAP_WIDGET_H
#include "../../../layouts/overlap_layout.h"
#include <QWidget>
class OverlapWidget final : public QWidget
{
Q_OBJECT
public:
OverlapWidget(QWidget *parent,
int overlapPercentage,
int maxColumns,
int maxRows,
Qt::Orientation direction,
bool adjustOnResize = false);
void addWidget(QWidget *widgetToAdd) const;
void clearLayout();
void adjustMaxColumnsAndRows();
public slots:
void maxOverlapItemsChanged(int newValue);
void overlapDirectionChanged(const QString &newDirection);
protected:
void resizeEvent(QResizeEvent *event) override;
private:
OverlapLayout *overlapLayout;
int overlapPercentage;
int maxColumns;
int maxRows;
Qt::Orientation direction;
bool adjustOnResize = false;
};
#endif // OVERLAP_WIDGET_H