diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index b2175a3fd..e2fc703e9 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -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 diff --git a/cockatrice/src/client/ui/layouts/flow_layout.cpp b/cockatrice/src/client/ui/layouts/flow_layout.cpp new file mode 100644 index 000000000..f1a776c47 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/flow_layout.cpp @@ -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 +#include +#include +#include + +/** + * @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 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 &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(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(parent); + return pw->style()->pixelMetric(pm, nullptr, pw); + } + + return dynamic_cast(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(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(parent)) { + return scrollArea->viewport()->height(); + } + parent = parent->parentWidget(); + } + + return 0; +} diff --git a/cockatrice/src/client/ui/layouts/flow_layout.h b/cockatrice/src/client/ui/layouts/flow_layout.h new file mode 100644 index 000000000..6fde7b802 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/flow_layout.h @@ -0,0 +1,43 @@ +#ifndef FLOW_LAYOUT_H +#define FLOW_LAYOUT_H + +#include +#include +#include +#include + +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 &rowItems, int x, int y); + [[nodiscard]] QSize sizeHint() const override; + [[nodiscard]] QSize minimumSize() const override; + +protected: + QList items; // List to store layout items + int horizontalMargin; + int verticalMargin; +}; + +#endif // FLOW_LAYOUT_H \ No newline at end of file diff --git a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp new file mode 100644 index 000000000..16f409db7 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.cpp @@ -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 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 &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; + } +} diff --git a/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h new file mode 100644 index 000000000..f6aebb284 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/horizontal_flow_layout.h @@ -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 &colItems, int x, int y); +}; + +#endif // HORIZONTAL_FLOW_LAYOUT_H \ No newline at end of file diff --git a/cockatrice/src/client/ui/layouts/overlap_layout.cpp b/cockatrice/src/client/ui/layouts/overlap_layout.cpp new file mode 100644 index 000000000..da44399a7 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/overlap_layout.cpp @@ -0,0 +1,474 @@ +#include "overlap_layout.h" + +#include + +/** + * @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 layout’s 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(itemList.size()); +} + +/** + * @brief Provides access to a layout item at a specified index. + * + * Allows retrieval of a QLayoutItem from the layout’s 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(maxColumns) : static_cast(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(itemList.size()) / static_cast(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(itemList.size()) / static_cast(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(maxRows) : static_cast(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(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(itemList.size()); + + return qCeil(totalItems / rows); +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/layouts/overlap_layout.h b/cockatrice/src/client/ui/layouts/overlap_layout.h new file mode 100644 index 000000000..1975cce89 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/overlap_layout.h @@ -0,0 +1,44 @@ +#ifndef OVERLAP_LAYOUT_H +#define OVERLAP_LAYOUT_H + +#include +#include +#include + +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 itemList; + int overlapPercentage; + int maxColumns; + int maxRows; + Qt::Orientation direction; + + // Calculate the preferred size of the layout + QSize calculatePreferredSize() const; +}; + +#endif // OVERLAP_LAYOUT_H diff --git a/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp b/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp new file mode 100644 index 000000000..90e154662 --- /dev/null +++ b/cockatrice/src/client/ui/layouts/vertical_flow_layout.cpp @@ -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 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 &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(); + } +} diff --git a/cockatrice/src/client/ui/layouts/vertical_flow_layout.h b/cockatrice/src/client/ui/layouts/vertical_flow_layout.h new file mode 100644 index 000000000..5dcd4945c --- /dev/null +++ b/cockatrice/src/client/ui/layouts/vertical_flow_layout.h @@ -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 &rowItems, int x, int y) override; +}; + +#endif // VERTICAL_FLOW_LAYOUT_H \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp b/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp index 42130b84a..f2e9442d0 100644 --- a/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp +++ b/cockatrice/src/client/ui/widgets/cards/card_info_picture_with_text_overlay_widget.cpp @@ -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(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(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); } /** diff --git a/cockatrice/src/client/ui/widgets/general/display/labeled_input.cpp b/cockatrice/src/client/ui/widgets/general/display/labeled_input.cpp new file mode 100644 index 000000000..ee26c78d9 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/display/labeled_input.cpp @@ -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; +} \ No newline at end of file diff --git a/cockatrice/src/client/ui/widgets/general/display/labeled_input.h b/cockatrice/src/client/ui/widgets/general/display/labeled_input.h new file mode 100644 index 000000000..078db95f8 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/display/labeled_input.h @@ -0,0 +1,36 @@ +#ifndef LABELED_INPUT_H +#define LABELED_INPUT_H + +#include +#include +#include +#include +#include + +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 diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp new file mode 100644 index 000000000..7bcb6ea75 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.cpp @@ -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 +#include +#include +#include + +/** + * @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(); + } + } +} diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h new file mode 100644 index 000000000..a52cca6e2 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/flow_widget.h @@ -0,0 +1,29 @@ +#ifndef FLOW_WIDGET_H +#define FLOW_WIDGET_H +#include "../../../layouts/flow_layout.h" + +#include +#include +#include + +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 diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_control_widget.cpp b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_control_widget.cpp new file mode 100644 index 000000000..2fd6cd6b8 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_control_widget.cpp @@ -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); +} diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_control_widget.h b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_control_widget.h new file mode 100644 index 000000000..eecf304a2 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_control_widget.h @@ -0,0 +1,34 @@ +#ifndef OVERLAP_CONTROL_WIDGET_H +#define OVERLAP_CONTROL_WIDGET_H +#include "../display/labeled_input.h" +#include "overlap_widget.h" + +#include +#include +#include + +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 diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_widget.cpp b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_widget.cpp new file mode 100644 index 000000000..f89399d42 --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_widget.cpp @@ -0,0 +1,194 @@ +#include "overlap_widget.h" + +#include "../../../../../deck/deck_list_model.h" +#include "../../../layouts/flow_layout.h" + +#include + +/** + * @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(); +} diff --git a/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_widget.h b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_widget.h new file mode 100644 index 000000000..0222a386d --- /dev/null +++ b/cockatrice/src/client/ui/widgets/general/layout_containers/overlap_widget.h @@ -0,0 +1,39 @@ +#ifndef OVERLAP_WIDGET_H +#define OVERLAP_WIDGET_H + +#include "../../../layouts/overlap_layout.h" + +#include + +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