diff --git a/cockatrice/CMakeLists.txt b/cockatrice/CMakeLists.txt index 1d07371d8..1017f1247 100644 --- a/cockatrice/CMakeLists.txt +++ b/cockatrice/CMakeLists.txt @@ -327,6 +327,8 @@ set(cockatrice_SOURCES src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_bracket_navigation_widget.h src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.cpp src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h + src/interface/widgets/utility/compact_push_button.cpp + src/interface/widgets/utility/compact_push_button.h ) add_subdirectory(sounds) diff --git a/cockatrice/src/interface/layouts/flow_layout.cpp b/cockatrice/src/interface/layouts/flow_layout.cpp index f7ebbfb79..36edd2c21 100644 --- a/cockatrice/src/interface/layouts/flow_layout.cpp +++ b/cockatrice/src/interface/layouts/flow_layout.cpp @@ -1,7 +1,16 @@ /** * @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. + * @brief Implementation of FlowLayout — a QLayout that wraps child widgets into rows + * (Qt::Horizontal flow) or columns (Qt::Vertical flow). + * + * Design contract (following Qt layout conventions): + * - setGeometry() places children inside the given rect. Nothing else. + * - sizeHint() reports the unconstrained preferred size (all items in one line). + * - minimumSize() reports the minimum size (largest single item). + * - heightForWidth() reports the height needed for a given width (horizontal flow only). + * + * The layout never calls setFixedSize() or adjustSize() on its parent widget; + * that is the responsibility of the parent widget / scroll area. */ #include "flow_layout.h" @@ -12,27 +21,18 @@ #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 Qt::Orientation _flowDirection, + const Qt::Orientation flowDirection, const int margin, const int hSpacing, const int vSpacing) - : QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing) + : QLayout(parent), flowDirection(flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing) { setContentsMargins(margin, margin, margin, margin); } -/** - * @brief Destructor for FlowLayout, which cleans up all items in the layout. - */ FlowLayout::~FlowLayout() { QLayoutItem *item; @@ -42,499 +42,349 @@ FlowLayout::~FlowLayout() } /** - * @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. + * @brief Reports which axis the layout can expand along. + * + * A horizontally-flowing layout expands horizontally (and wraps vertically, + * but that is governed by heightForWidth, not by this flag). + * A vertically-flowing layout expands vertically. */ Qt::Orientations FlowLayout::expandingDirections() const { - return Qt::Horizontal | Qt::Vertical; + return (flowDirection == Qt::Horizontal) ? 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. + * @brief Height-for-width is only meaningful for horizontal (wrapping) flow. */ bool FlowLayout::hasHeightForWidth() const { - return true; + return flowDirection == Qt::Horizontal; } /** - * @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. + * @brief Returns the height required to display all items within @p width. + * + * Only valid for horizontal flow; returns -1 otherwise so Qt ignores it. + * Spacing is counted once between adjacent items, never before the first + * or after the last. */ int FlowLayout::heightForWidth(const int width) const { - if (flowDirection == Qt::Vertical) { - int height = 0; - int rowWidth = 0; - int rowHeight = 0; + if (flowDirection != Qt::Horizontal) + return -1; - for (const QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } + int totalHeight = 0; + int rowUsedWidth = 0; + int rowHeight = 0; - int itemWidth = item->sizeHint().width() + horizontalSpacing(); - if (rowWidth + itemWidth > width) { - height += rowHeight + verticalSpacing(); - rowWidth = itemWidth; - rowHeight = item->sizeHint().height(); - } else { - rowWidth += itemWidth; - rowHeight = qMax(rowHeight, item->sizeHint().height()); - } - } - height += rowHeight; // Add height of the last row - return height; - } else { - int width = 0; - int colWidth = 0; - int colHeight = 0; - - for (const QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { - continue; - } - - int itemHeight = item->sizeHint().height(); - if (colHeight + itemHeight > width) { - width += colWidth; - colHeight = itemHeight; - colWidth = item->sizeHint().width(); - } else { - colHeight += itemHeight; - colWidth = qMax(colWidth, item->sizeHint().width()); - } - } - width += colWidth; // Add width of the last column - return width; - } -} - -/** - * @brief Arranges layout items in rows within the specified rectangle bounds. - * @param rect The area within which to position layout items. - */ -void FlowLayout::setGeometry(const QRect &rect) -{ - QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle. - - if (flowDirection == Qt::Horizontal) { - // If we have a parent scroll area, we're clamped to that, else we use our own rectangle. - const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth(); - - const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth); - - if (QWidget *parentWidgetPtr = parentWidget()) { - parentWidgetPtr->setFixedSize(availableWidth, totalHeight); - } - } else { - const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight()); - - const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight); - - if (QWidget *parentWidgetPtr = parentWidget()) { - parentWidgetPtr->setFixedSize(totalWidth, availableHeight); - } - } -} - -/** - * @brief Lays out items into rows according to the available width, starting from a given origin. - * Each row is arranged within `availableWidth`, wrapping to a new row as necessary. - * @param originX The x-coordinate for the layout start position. - * @param originY The y-coordinate for the layout start position. - * @param availableWidth The width within which each row is constrained. - * @return The total height after arranging all rows. - */ -int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth) -{ - QVector 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()) { + for (const QLayoutItem *item : items) { + if (!item || item->isEmpty()) { continue; } - QSize itemSize = item->sizeHint(); // The suggested size for the current item - int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing + const QSize itemSize = item->sizeHint(); + // Spacing is only inserted between items, not before the first. + const int spaceX = (rowUsedWidth > 0) ? horizontalSpacing() : 0; - // 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 + if (rowUsedWidth > 0 && rowUsedWidth + spaceX + itemSize.width() > width) { + // This item overflows the current row — commit the row and start a new one. + totalHeight += rowHeight + verticalSpacing(); + rowUsedWidth = itemSize.width(); + rowHeight = itemSize.height(); + } else { + rowUsedWidth += spaceX + itemSize.width(); + rowHeight = qMax(rowHeight, itemSize.height()); + } + } + + return totalHeight + rowHeight; // Include the final (possibly only) row. +} + +/** + * @brief Places all children within @p rect. + * + * This is the only method that may move/resize children. It does NOT resize + * the parent widget; that would break Qt's layout protocol. + */ +void FlowLayout::setGeometry(const QRect &rect) +{ + QLayout::setGeometry(rect); + + if (flowDirection == Qt::Horizontal) { + layoutAllRows(rect.x(), rect.y(), rect.width()); + } else { + layoutAllColumns(rect.x(), rect.y(), rect.height()); + } +} + +QSize FlowLayout::sizeHint() const +{ + return (flowDirection == Qt::Horizontal) ? calculateSizeHintHorizontal() : calculateSizeHintVertical(); +} + +QSize FlowLayout::minimumSize() const +{ + return (flowDirection == Qt::Horizontal) ? calculateMinimumSizeHorizontal() : calculateMinimumSizeVertical(); +} + +// ─── Row layout (horizontal flow) ──────────────────────────────────────────── + +/** + * @brief Places all items into wrapping rows within @p availableWidth. + * @return The y-coordinate of the bottom edge of the last row. + */ +int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth) +{ + QVector rowItems; + int rowUsedWidth = 0; // Width consumed by items already in the current row. + int currentY = originY; + int rowHeight = 0; + + for (QLayoutItem *item : items) { + if (!item || item->isEmpty()) { + continue; + } + + const QSize itemSize = item->sizeHint(); + // No leading space for the first item in a row. + const int spaceX = rowItems.isEmpty() ? 0 : horizontalSpacing(); + + if (!rowItems.isEmpty() && rowUsedWidth + spaceX + itemSize.width() > availableWidth) { + // Current item does not fit — flush the current row, begin a new one. + layoutSingleRow(rowItems, originX, currentY, availableWidth); + rowItems.clear(); + currentY += rowHeight + verticalSpacing(); + rowUsedWidth = 0; + rowHeight = 0; } // 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 + // `rowItems.size() > 1` is equivalent to "this is not the first item in the row" + // because we just appended above. + rowUsedWidth += (rowItems.size() > 1 ? horizontalSpacing() : 0) + itemSize.width(); + rowHeight = qMax(rowHeight, itemSize.height()); } - // 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; + layoutSingleRow(rowItems, originX, currentY, availableWidth); // Flush the final row. + return currentY + 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. + * @brief Sets the geometry for every item in @p rowItems, starting at (@p x, @p y). + * + * Items whose horizontal size policy includes the Expand or MinimumExpanding flag + * share the leftover row width proportionally (like QHBoxLayout stretch), so that + * e.g. a QLineEdit can fill remaining space while fixed-size buttons stay compact. + * + * Items without an expanding policy are placed at their sizeHint, clamped to maximumSize. */ -void FlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y) +void FlowLayout::layoutSingleRow(const QVector &rowItems, int x, const int y, const int availableWidth) { + if (rowItems.isEmpty()) + return; + + // ── Pass 1: measure fixed width and count expanding items ──────────────── + int fixedWidth = 0; + int expandingCount = 0; + int spacingTotal = (rowItems.size() - 1) * horizontalSpacing(); + for (QLayoutItem *item : rowItems) { - if (item == nullptr || item->isEmpty()) { + if (!item || 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 + QWidget *widget = item->widget(); + const QSizePolicy::Policy hPolicy = widget ? widget->sizePolicy().horizontalPolicy() : QSizePolicy::Fixed; + + if (hPolicy & QSizePolicy::ExpandFlag) { + ++expandingCount; + } else { + const int maxW = widget ? widget->maximumWidth() : QWIDGETSIZE_MAX; + fixedWidth += qMin(item->sizeHint().width(), maxW); + } + } + + // Extra pixels to share among expanding items (never negative). + const int extra = qMax(0, availableWidth - spacingTotal - fixedWidth); + const int expandingShare = (expandingCount > 0) ? extra / expandingCount : 0; + + // ── Pass 2: place items ────────────────────────────────────────────────── + for (QLayoutItem *item : rowItems) { + if (!item || item->isEmpty()) + continue; + + QWidget *widget = item->widget(); + if (!widget) + continue; + + const QSizePolicy::Policy hPolicy = widget->sizePolicy().horizontalPolicy(); + const QSize maxSize = widget->maximumSize(); + const bool expands = hPolicy & QSizePolicy::ExpandFlag; + + const int itemWidth = + expands ? qMin(expandingShare, maxSize.width()) : qMin(item->sizeHint().width(), maxSize.width()); + const int itemHeight = qMin(item->sizeHint().height(), maxSize.height()); + item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); - // Move the x-position to the right, leaving space for horizontal spacing x += itemWidth + horizontalSpacing(); } } +// ─── Column layout (vertical flow) ─────────────────────────────────────────── + /** - * @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. + * @brief Places all items into wrapping columns within @p availableHeight. + * @return The x-coordinate of the right edge of the last column. */ int FlowLayout::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 + QVector colItems; + int colUsedHeight = 0; // Height consumed by items already in the current column. + int currentX = originX; + int colWidth = 0; for (QLayoutItem *item : items) { - if (item == nullptr || item->isEmpty()) { + if (!item || item->isEmpty()) { continue; } - QSize itemSize = item->sizeHint(); // The suggested size for the current item + const QSize itemSize = item->sizeHint(); + // No leading space for the first item in a column. + const int spaceY = colItems.isEmpty() ? 0 : verticalSpacing(); - // 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 + if (!colItems.isEmpty() && colUsedHeight + spaceY + itemSize.height() > availableHeight) { + // Current item does not fit — flush the current column, begin a new one. + layoutSingleColumn(colItems, currentX, originY); + colItems.clear(); + currentX += colWidth + horizontalSpacing(); + colUsedHeight = 0; + colWidth = 0; } - // 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 + colUsedHeight += (colItems.size() > 1 ? verticalSpacing() : 0) + itemSize.height(); + colWidth = qMax(colWidth, itemSize.width()); } - // 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; + layoutSingleColumn(colItems, currentX, originY); // Flush the final column. + return currentX + 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. + * @brief Sets the geometry for every item in @p colItems, starting at (@p x, @p y). + * + * Each item is placed at its sizeHint, clamped to its maximumSize. */ void FlowLayout::layoutSingleColumn(const QVector &colItems, const int x, int y) { for (QLayoutItem *item : colItems) { - if (item == nullptr) { - qCDebug(FlowLayoutLog) << "Item is null."; + if (!item || item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping null or empty item in column."; continue; } - if (item->isEmpty()) { - qCDebug(FlowLayoutLog) << "Skipping empty item."; - continue; - } - - // Debugging: Print the item's widget class name and size hint QWidget *widget = item->widget(); - if (widget) { - qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className(); - qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint(); - qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize(); - qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize(); - - // Debugging: Print child widgets - const QObjectList &children = widget->children(); - qCDebug(FlowLayoutLog) << "Child widgets:"; - for (QObject *child : children) { - if (QWidget *childWidget = qobject_cast(child)) { - qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className(); - qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint(); - qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize(); - } - } - } else { - qCDebug(FlowLayoutLog) << "Item does not have a widget."; + if (!widget) { + qCDebug(FlowLayoutLog) << "Item has no widget; skipping."; + continue; } - // Get the maximum allowed size for the item - QSize itemMaxSize = widget->maximumSize(); - // Constrain the item's width and height to its size hint or maximum size - const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); - const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); - // Debugging: Print the computed geometry - qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth - << ", height=" << itemHeight; + qCDebug(FlowLayoutLog) << "Widget:" << widget->metaObject()->className() << "sizeHint:" << widget->sizeHint() + << "maximumSize:" << widget->maximumSize() << "minimumSize:" << widget->minimumSize(); + + const QSize maxSize = widget->maximumSize(); + const int itemWidth = qMin(item->sizeHint().width(), maxSize.width()); + const int itemHeight = qMin(item->sizeHint().height(), maxSize.height()); + + qCDebug(FlowLayoutLog) << "Placing at x=" << x << "y=" << y << "w=" << itemWidth << "h=" << itemHeight; // Set the item's geometry based on the computed size and position item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); - - // Move the y-position down by the item's height to place the next item below - y += itemHeight; + y += itemHeight + verticalSpacing(); } } /** - * @brief Calculates the preferred size of the layout based on the flow direction. - * @return A QSize representing the ideal dimensions of the layout. - */ -QSize FlowLayout::sizeHint() const -{ - if (flowDirection == Qt::Horizontal) { - return calculateSizeHintHorizontal(); - } else { - return calculateSizeHintVertical(); - } -} - -/** - * @brief Calculates the minimum size required by the layout based on the flow direction. - * @return A QSize representing the minimum required dimensions. - */ -QSize FlowLayout::minimumSize() const -{ - if (flowDirection == Qt::Horizontal) { - return calculateMinimumSizeHorizontal(); - } else { - return calculateMinimumSizeVertical(); - } -} - -/** - * @brief Calculates the size hint for horizontal flow direction. - * @return A QSize representing the preferred dimensions. + * @brief Preferred size for horizontal flow: all items in a single row (unconstrained). + * + * The actual displayed height is determined by heightForWidth() once Qt knows the + * real available width. */ QSize FlowLayout::calculateSizeHintHorizontal() const { - int maxWidth = 0; // Tracks the maximum width needed - int totalHeight = 0; // Tracks the total height across all rows - int rowHeight = 0; // Tracks the height of the current row - int currentWidth = 0; // Tracks the current row's width - - const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth(); - - qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth; + int totalWidth = 0; + int maxHeight = 0; for (const QLayoutItem *item : items) { if (!item || item->isEmpty()) { - qCDebug(FlowLayoutLog) << "Skipping empty item."; continue; } - - QSize itemSize = item->sizeHint(); - int itemWidth = itemSize.width() + horizontalSpacing(); - qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize << "Width with spacing:" << itemWidth; - - if (currentWidth + itemWidth > availableWidth) { - qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight; - maxWidth = qMax(maxWidth, currentWidth); - totalHeight += rowHeight + verticalSpacing(); - qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth; - - currentWidth = 0; - rowHeight = 0; + const QSize s = item->sizeHint(); + if (totalWidth > 0) { + totalWidth += horizontalSpacing(); } - - currentWidth += itemWidth; - rowHeight = qMax(rowHeight, itemSize.height()); - qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight; + totalWidth += s.width(); + maxHeight = qMax(maxHeight, s.height()); } - - // Account for the final row - maxWidth = qMax(maxWidth, currentWidth); - totalHeight += rowHeight; - qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth; - - return QSize(maxWidth, totalHeight); + return QSize(totalWidth, maxHeight); } /** - * @brief Calculates the minimum size for horizontal flow direction. - * @return A QSize representing the minimum required dimensions. + * @brief Minimum size for horizontal flow: the largest single item. + * + * This guarantees we can always display at least one item per row. */ QSize FlowLayout::calculateMinimumSizeHorizontal() const { - int maxWidth = 0; // Tracks the maximum width of a row - int totalHeight = 0; // Tracks the total height across all rows - int rowHeight = 0; // Tracks the height of the current row - int currentWidth = 0; // Tracks the current row's width + QSize size(0, 0); + for (const QLayoutItem *item : items) { + if (!item || item->isEmpty()) { + qCDebug(FlowLayoutLog) << "Skipping empty item."; + continue; + } + size = size.expandedTo(item->minimumSize()); + } + return size; +} - const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth(); - - qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth; +/** + * @brief Preferred size for vertical flow: all items in a single column (unconstrained). + */ +QSize FlowLayout::calculateSizeHintVertical() const +{ + int maxWidth = 0; + int totalHeight = 0; for (const QLayoutItem *item : items) { if (!item || item->isEmpty()) { qCDebug(FlowLayoutLog) << "Skipping empty item."; continue; } - - QSize itemMinSize = item->minimumSize(); - int itemWidth = itemMinSize.width() + horizontalSpacing(); - qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize << "Width with spacing:" << itemWidth; - - if (currentWidth + itemWidth > availableWidth) { - qCDebug(FlowLayoutLog) << "Row overflow. Current width:" << currentWidth << "Row height:" << rowHeight; - maxWidth = qMax(maxWidth, currentWidth); - totalHeight += rowHeight + verticalSpacing(); - qCDebug(FlowLayoutLog) << "Updated total height:" << totalHeight << "Max width so far:" << maxWidth; - - currentWidth = 0; - rowHeight = 0; + const QSize s = item->sizeHint(); + if (totalHeight > 0) { + totalHeight += verticalSpacing(); } - - currentWidth += itemWidth; - rowHeight = qMax(rowHeight, itemMinSize.height()); - qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight; + totalHeight += s.height(); + maxWidth = qMax(maxWidth, s.width()); } - - // Account for the final row - maxWidth = qMax(maxWidth, currentWidth); - totalHeight += rowHeight; - qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth; - return QSize(maxWidth, totalHeight); } /** - * @brief Calculates the size hint for vertical flow direction. - * @return A QSize representing the preferred dimensions. - */ -QSize FlowLayout::calculateSizeHintVertical() const -{ - int totalWidth = 0; - int maxHeight = 0; - int colWidth = 0; - int currentHeight = 0; - - const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight()); - - qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight; - - for (const QLayoutItem *item : items) { - if (!item || item->isEmpty()) { - qCDebug(FlowLayoutLog) << "Skipping empty item."; - continue; - } - - QSize itemSize = item->sizeHint(); - qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize; - - if (currentHeight + itemSize.height() > availableHeight) { - qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight - << "Column width:" << colWidth; - totalWidth += colWidth + horizontalSpacing(); - maxHeight = qMax(maxHeight, currentHeight); - qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight; - - currentHeight = 0; - colWidth = 0; - } - - currentHeight += itemSize.height() + verticalSpacing(); - colWidth = qMax(colWidth, itemSize.width()); - qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth; - } - - // Account for the final column - totalWidth += colWidth; - maxHeight = qMax(maxHeight, currentHeight); - qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight; - - return QSize(totalWidth, maxHeight); -} - -/** - * @brief Calculates the minimum size for vertical flow direction. - * @return A QSize representing the minimum required dimensions. + * @brief Minimum size for vertical flow: the largest single item. */ QSize FlowLayout::calculateMinimumSizeVertical() const { - int totalWidth = 0; // Tracks the total width across all columns - int maxHeight = 0; // Tracks the maximum height of a column - int colWidth = 0; // Tracks the width of the current column - int currentHeight = 0; // Tracks the current column's height - - const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight()); - - qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight; - + QSize size(0, 0); for (const QLayoutItem *item : items) { if (!item || item->isEmpty()) { qCDebug(FlowLayoutLog) << "Skipping empty item."; continue; } - - QSize itemMinSize = item->minimumSize(); - int itemHeight = itemMinSize.height() + verticalSpacing(); - qCDebug(FlowLayoutLog) << "Processing item. Minimum size:" << itemMinSize - << "Height with spacing:" << itemHeight; - - if (currentHeight + itemHeight > availableHeight) { - qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight - << "Column width:" << colWidth; - totalWidth += colWidth + horizontalSpacing(); - maxHeight = qMax(maxHeight, currentHeight); - qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight; - - currentHeight = 0; - colWidth = 0; - } - - currentHeight += itemHeight; - colWidth = qMax(colWidth, itemMinSize.width()); - qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth; + size = size.expandedTo(item->minimumSize()); } - - // Account for the final column - totalWidth += colWidth; - maxHeight = qMax(maxHeight, currentHeight); - qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight; - - return QSize(totalWidth, maxHeight); + return size; } /** @@ -543,7 +393,7 @@ QSize FlowLayout::calculateMinimumSizeVertical() const */ void FlowLayout::addItem(QLayoutItem *item) { - if (item != nullptr) { + if (item) { items.append(item); } } @@ -551,11 +401,8 @@ void FlowLayout::addItem(QLayoutItem *item) void FlowLayout::insertWidgetAtIndex(QWidget *toInsert, int index) { addChildWidget(toInsert); - - // We don't want to fail on an index that violates the bounds, so we just clamp it. - int boundedIndex = qBound(0, index, qMax(0, static_cast(items.size()))); - items.insert(boundedIndex, new QWidgetItem(toInsert)); - + const int bounded = qBound(0, index, static_cast(items.size())); + items.insert(bounded, new QWidgetItem(toInsert)); invalidate(); } @@ -613,52 +460,13 @@ int FlowLayout::verticalSpacing() const */ int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const { - QObject *parent = this->parent(); - - if (!parent) { + QObject *p = parent(); + if (!p) { return -1; } - - if (parent->isWidgetType()) { - const auto *pw = dynamic_cast(parent); + if (p->isWidgetType()) { + const auto *pw = static_cast(p); 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; -} + return static_cast(p)->spacing(); +} \ No newline at end of file diff --git a/cockatrice/src/interface/layouts/flow_layout.h b/cockatrice/src/interface/layouts/flow_layout.h index cf109d260..cc206afae 100644 --- a/cockatrice/src/interface/layouts/flow_layout.h +++ b/cockatrice/src/interface/layouts/flow_layout.h @@ -1,7 +1,8 @@ /** * @file flow_layout.h * @ingroup UI - * @brief TODO: Document this. + * @brief A QLayout subclass that arranges child widgets in wrapping rows (horizontal flow) + * or wrapping columns (vertical flow). */ #ifndef FLOW_LAYOUT_H @@ -10,8 +11,8 @@ #include #include #include +#include #include -#include inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout", QtInfoMsg); @@ -19,42 +20,55 @@ class FlowLayout : public QLayout { public: explicit FlowLayout(QWidget *parent = nullptr); - FlowLayout(QWidget *parent, Qt::Orientation _flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0); + FlowLayout(QWidget *parent, Qt::Orientation flowDirection, int margin = 0, int hSpacing = 0, int vSpacing = 0); ~FlowLayout() override; + void insertWidgetAtIndex(QWidget *toInsert, int index); - [[nodiscard]] QSize calculateMinimumSizeHorizontal() const; - [[nodiscard]] QSize calculateSizeHintVertical() const; - [[nodiscard]] QSize calculateMinimumSizeVertical() const; + // QLayout interface 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; + void setGeometry(const QRect &rect) override; + // Size negotiation [[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); - int layoutAllColumns(int originX, int originY, int availableHeight); - void layoutSingleColumn(const QVector &colItems, int x, int y); [[nodiscard]] QSize sizeHint() const override; [[nodiscard]] QSize minimumSize() const override; - [[nodiscard]] QSize calculateSizeHintHorizontal() const; + + // Spacing helpers + void setHorizontalMargin(int margin) + { + horizontalMargin = margin; + } + [[nodiscard]] int horizontalSpacing() const; + void setVerticalMargin(int margin) + { + verticalMargin = margin; + } + [[nodiscard]] int verticalSpacing() const; + [[nodiscard]] int smartSpacing(QStyle::PixelMetric pm) const; + + // Layout passes (virtual so subclasses can override placement logic) + virtual int layoutAllRows(int originX, int originY, int availableWidth); + virtual void layoutSingleRow(const QVector &rowItems, int x, int y, int availableWidth); + int layoutAllColumns(int originX, int originY, int availableHeight); + void layoutSingleColumn(const QVector &colItems, int x, int y); protected: - QList items; // List to store layout items + // Size-hint helpers split by direction + [[nodiscard]] QSize calculateSizeHintHorizontal() const; + [[nodiscard]] QSize calculateMinimumSizeHorizontal() const; + [[nodiscard]] QSize calculateSizeHintVertical() const; + [[nodiscard]] QSize calculateMinimumSizeVertical() const; + + QList items; Qt::Orientation flowDirection; - int horizontalMargin; - int verticalMargin; + int horizontalMargin; ///< Horizontal spacing between items (-1 = use style default) + int verticalMargin; ///< Vertical spacing between items (-1 = use style default) }; #endif // FLOW_LAYOUT_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp index 4931aeaa4..0107294c7 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.cpp @@ -20,12 +20,11 @@ DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) : AbstractAnalyticsPanelWidget(parent, analyzer) { - controls = new QWidget(this); - controlLayout = new QHBoxLayout(controls); - controlLayout->setContentsMargins(11, 0, 11, 0); + controls = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); + controls->setSpacing(4, 4); labelPrefix = new QLabel(this); - controlLayout->addWidget(labelPrefix); + controls->addWidget(labelPrefix); criteriaCombo = new QComboBox(this); // Give these things item-data so we can translate the actual user-facing strings @@ -33,33 +32,32 @@ DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatistics criteriaCombo->addItem(QString(), "type"); criteriaCombo->addItem(QString(), "subtype"); criteriaCombo->addItem(QString(), "cmc"); - controlLayout->addWidget(criteriaCombo); + controls->addWidget(criteriaCombo); exactnessCombo = new QComboBox(this); exactnessCombo->addItem(QString(), true); // At least exactnessCombo->addItem(QString(), false); // Exactly - controlLayout->addWidget(exactnessCombo); + controls->addWidget(exactnessCombo); quantitySpin = new QSpinBox(this); quantitySpin->setRange(1, 60); - controlLayout->addWidget(quantitySpin); + controls->addWidget(quantitySpin); labelMiddle = new QLabel(this); - controlLayout->addWidget(labelMiddle); + controls->addWidget(labelMiddle); drawnSpin = new QSpinBox(this); drawnSpin->setRange(1, 60); drawnSpin->setValue(7); - controlLayout->addWidget(drawnSpin); + controls->addWidget(drawnSpin); labelSuffix = new QLabel(this); - controlLayout->addWidget(labelSuffix); + controls->addWidget(labelSuffix); labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - controlLayout->addStretch(1); layout->addWidget(controls); // Table diff --git a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h index 80015999f..9f7b971b1 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/analyzer_modules/draw_probability/draw_probability_widget.h @@ -1,6 +1,8 @@ #ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H #define COCKATRICE_DRAW_PROBABILITY_WIDGET_H +#include "../../../../layouts/flow_layout.h" +#include "../../../general/layout_containers/flow_widget.h" #include "../../abstract_analytics_panel_widget.h" #include "../../deck_list_statistics_analyzer.h" #include "draw_probability_config.h" @@ -31,8 +33,7 @@ private slots: private: DrawProbabilityConfig config; - QWidget *controls; - QHBoxLayout *controlLayout; + FlowWidget *controls; QLabel *labelPrefix; QLabel *labelMiddle; QLabel *labelSuffix; diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp index 147675e21..76552ea2f 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.cpp @@ -23,8 +23,8 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal layout = new QVBoxLayout(this); // Controls - controlContainer = new QWidget(this); - controlLayout = new QHBoxLayout(controlContainer); + controlContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); + controlContainer->setSpacing(4, 4); addButton = new QPushButton(this); removeButton = new QPushButton(this); saveButton = new QPushButton(this); @@ -32,11 +32,11 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal includeSideboardCheckBox = new QCheckBox(this); includeSideboardCheckBox->setChecked(false); - controlLayout->addWidget(addButton); - controlLayout->addWidget(removeButton); - controlLayout->addWidget(saveButton); - controlLayout->addWidget(loadButton); - controlLayout->addWidget(includeSideboardCheckBox); + controlContainer->addWidget(addButton); + controlContainer->addWidget(removeButton); + controlContainer->addWidget(saveButton); + controlContainer->addWidget(loadButton); + controlContainer->addWidget(includeSideboardCheckBox); layout->addWidget(controlContainer); diff --git a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h index 09618c3f8..3c73deca2 100644 --- a/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h +++ b/cockatrice/src/interface/widgets/deck_analytics/deck_analytics_widget.h @@ -7,6 +7,7 @@ #ifndef DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H +#include "../general/layout_containers/flow_widget.h" #include "abstract_analytics_panel_widget.h" #include "deck_list_statistics_analyzer.h" #include "resizable_panel.h" @@ -51,8 +52,7 @@ private: void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {}); QVBoxLayout *layout; - QWidget *controlContainer; - QHBoxLayout *controlLayout; + FlowWidget *controlContainer; QPushButton *addButton; QPushButton *removeButton; diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp index 75ab56b34..59c657724 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.cpp @@ -1,42 +1,52 @@ /** * @file flow_widget.cpp - * @brief Implementation of the FlowWidget class for organizing widgets in a flow layout within a scrollable area. + * @brief Implementation of FlowWidget — a QWidget hosting a FlowLayout inside an + * optional QScrollArea. */ #include "flow_widget.h" #include #include +#include +#include #include -#include -#include /** - * @brief Constructs a FlowWidget with a scrollable layout. + * @brief Constructs a FlowWidget. * - * @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. + * When both scroll policies are Qt::ScrollBarAlwaysOff the scroll area is + * omitted entirely and the container is placed directly in the main layout. + * + * @param parent Parent widget. + * @param _flowDirection Qt::Horizontal for row-wrapping, Qt::Vertical for column-wrapping. + * @param horizontalPolicy Horizontal scroll-bar policy. + * @param verticalPolicy Vertical scroll-bar policy. */ FlowWidget::FlowWidget(QWidget *parent, const Qt::Orientation _flowDirection, const Qt::ScrollBarPolicy horizontalPolicy, const Qt::ScrollBarPolicy verticalPolicy) - : QWidget(parent), flowDirection(_flowDirection) + : QWidget(parent), scrollArea(nullptr), flowDirection(_flowDirection) + { - // Main Widget and Layout - if (_flowDirection == Qt::Horizontal) { - setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); - setMinimumWidth(0); + // Top-level size policy + // Horizontal flow: expand horizontally, let height be determined by wrapping. + // Vertical flow: expand vertically, let width be determined by wrapping. + if (flowDirection == Qt::Horizontal) { + setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); } else { - setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); - setMinimumHeight(0); + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); } + mainLayout = new QHBoxLayout(this); + mainLayout->setContentsMargins(0, 0, 0, 0); setLayout(mainLayout); - if (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff) { - // Scroll Area, which should expand as much as possible, since it should be the only direct child widget. + const bool useScrollArea = (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff); + + // Scroll area (optional) + if (useScrollArea) { scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setMinimumSize(0, 0); @@ -48,39 +58,28 @@ FlowWidget::FlowWidget(QWidget *parent, scrollArea = nullptr; } - // Flow Layout inside the scroll area - if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) { - container = new QWidget(this); - } else { - container = new QWidget(scrollArea); - } + // Container widget (holds the FlowLayout) + container = new QWidget(useScrollArea ? static_cast(scrollArea) : this); + + // The container should be willing to grow in both axes; its actual size is + // governed by the FlowLayout's sizeHint / heightForWidth, not by a fixed policy. + container->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); + container->setMinimumSize(0, 0); flowLayout = new FlowLayout(container, flowDirection); - container->setLayout(flowLayout); - // The container should expand as much as possible, trusting the scrollArea to constrain it. - if (_flowDirection == Qt::Horizontal) { - container->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - container->setMinimumWidth(0); - } else { - container->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - container->setMinimumHeight(0); - } - // Use the FlowLayout container directly if we disable the ScrollArea - if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) { - mainLayout->addWidget(container); - } else { + if (useScrollArea) { scrollArea->setWidget(container); mainLayout->addWidget(scrollArea); + } else { + mainLayout->addWidget(container); } } /** * @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 @@ -100,77 +99,74 @@ void FlowWidget::removeWidget(QWidget *widgetToRemove) const } /** - * @brief Clears all widgets from the flow layout. + * @brief Removes all widgets from the flow layout and deletes them. * - * Deletes each widget and layout item, and recreates the flow layout if it was removed. + * If the layout pointer has somehow been lost it is recreated before returning. */ void FlowWidget::clearLayout() { - if (flowLayout != nullptr) { + if (flowLayout) { QLayoutItem *item; - while ((item = flowLayout->takeAt(0)) != nullptr) { - item->widget()->deleteLater(); // Delete the widget - delete item; // Delete the layout item + while ((item = flowLayout->takeAt(0))) { + if (item->widget()) + item->widget()->deleteLater(); + delete item; } } else { + // Defensive fallback: recreate the layout if it was deleted externally. flowLayout = new FlowLayout(container, flowDirection); container->setLayout(flowLayout); } } /** - * @brief Handles resize events for the FlowWidget. + * @brief Marks the flow layout as dirty so Qt recomputes item positions. * - * Triggers layout recalculation and adjusts the scroll area content size. - * - * @param event The resize event containing the new size information. + * We do NOT call adjustSize() or activate() here: + * - adjustSize() would freeze geometry by calling setFixedSize internally. + * - activate() called inside a resize event can cause synchronous re-entrancy. + * Qt automatically calls setGeometry on the layout after a resize, so simply + * invalidating is sufficient. */ void FlowWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); + qCDebug(FlowWidgetSizeLog) << "resizeEvent:" << event->size(); - qCDebug(FlowWidgetSizeLog) << event->size(); - - // Trigger the layout to recalculate - if (flowLayout != nullptr) { - flowLayout->invalidate(); // Marks the layout as dirty and requires recalculation - flowLayout->activate(); // Recalculate the layout based on the new size - } - - // Ensure the scroll area and its content adjust correctly - if (scrollArea != nullptr && scrollArea->widget() != nullptr) { - qCDebug(FlowWidgetSizeLog) << "Got a scrollarea: " << scrollArea->widget()->size(); - scrollArea->widget()->adjustSize(); - } else { - container->adjustSize(); + if (flowLayout) { + flowLayout->invalidate(); } } +void FlowWidget::setSpacing(int hSpacing, int vSpacing) +{ + flowLayout->setHorizontalMargin(hSpacing); + flowLayout->setVerticalMargin(vSpacing); + flowLayout->invalidate(); +} + /** - * @brief Sets the minimum size for all widgets inside the FlowWidget to the maximum sizeHint of all of them. + * @brief Sets every child widget's minimum size to the largest sizeHint in the layout. + * + * Useful for toolbars or button bars where all items should be the same size. */ void FlowWidget::setMinimumSizeToMaxSizeHint() { - QSize maxSize(0, 0); // Initialize to a zero size + QSize maxSize(0, 0); // Iterate over all widgets in the flow layout to find the maximum sizeHint for (int i = 0; i < flowLayout->count(); ++i) { - if (QLayoutItem *item = flowLayout->itemAt(i)) { - if (QWidget *widget = item->widget()) { - // Update the max size based on the sizeHint of each widget - QSize widgetSizeHint = widget->sizeHint(); - maxSize.setWidth(qMax(maxSize.width(), widgetSizeHint.width())); - maxSize.setHeight(qMax(maxSize.height(), widgetSizeHint.height())); - } + QLayoutItem *item = flowLayout->itemAt(i); + if (item && item->widget()) { + maxSize = maxSize.expandedTo(item->widget()->sizeHint()); } } // Set the minimum size for all widgets to the max sizeHint for (int i = 0; i < flowLayout->count(); ++i) { - if (QLayoutItem *item = flowLayout->itemAt(i)) { - if (QWidget *widget = item->widget()) { - widget->setMinimumSize(maxSize); - } + QLayoutItem *item = flowLayout->itemAt(i); + if (item && item->widget()) { + item->widget()->setMinimumSize(maxSize); } } } diff --git a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h index d9fa49937..1932ce70a 100644 --- a/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h +++ b/cockatrice/src/interface/widgets/general/layout_containers/flow_widget.h @@ -1,22 +1,23 @@ /** * @file flow_widget.h * @ingroup UI - * @brief TODO: Document this. + * @brief A QWidget that wraps a FlowLayout inside an optional QScrollArea. */ #ifndef FLOW_WIDGET_H #define FLOW_WIDGET_H + #include "../../../layouts/flow_layout.h" #include #include +#include #include -#include inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget", QtInfoMsg); inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size", QtInfoMsg); -class FlowWidget final : public QWidget +class FlowWidget : public QWidget { Q_OBJECT @@ -25,17 +26,20 @@ public: Qt::Orientation orientation, Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy verticalPolicy); + void addWidget(QWidget *widget_to_add) const; void insertWidgetAtIndex(QWidget *toInsert, int index); void removeWidget(QWidget *widgetToRemove) const; void clearLayout(); + [[nodiscard]] int count() const; [[nodiscard]] QLayoutItem *itemAt(int index) const; - QScrollArea *scrollArea; + QScrollArea *scrollArea; ///< Null when both scroll policies are AlwaysOff. public slots: void setMinimumSizeToMaxSizeHint(); + void setSpacing(int hSpacing, int vSpacing); protected: void resizeEvent(QResizeEvent *event) override; @@ -47,4 +51,4 @@ private: QWidget *container; }; -#endif // FLOW_WIDGET_H +#endif // FLOW_WIDGET_H \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp index 81812104a..badc437ee 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.cpp @@ -35,19 +35,33 @@ void SettingsButtonWidget::setButtonIcon(QPixmap iconMap) button->setIcon(iconMap); } -void SettingsButtonWidget::setButtonText(const QString &buttonText) +void SettingsButtonWidget::setButtonText(const QString &text) { - // 🔓 unlock size constraints + buttonText = text; + button->setMinimumSize(QSize(0, 0)); button->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)); - button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - button->setText(buttonText); - + button->setText(text); button->setFixedHeight(32); button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - button->setMinimumWidth(button->sizeHint().width()); + button->setMinimumWidth(32); // icon-only fallback minimum +} + +void SettingsButtonWidget::setCompact(bool _compact) +{ + compact = _compact; + if (compact) { + button->setToolButtonStyle(Qt::ToolButtonIconOnly); + button->setFixedWidth(32); + } else { + button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + button->setText(buttonText); + button->setFixedWidth(QWIDGETSIZE_MAX); // release fixed width + button->setMinimumWidth(32); + button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + } } void SettingsButtonWidget::togglePopup() diff --git a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h index 36f01ac38..43de8183f 100644 --- a/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h +++ b/cockatrice/src/interface/widgets/quick_settings/settings_button_widget.h @@ -23,6 +23,11 @@ public: void removeSettingsWidget(QWidget *toRemove) const; void setButtonIcon(QPixmap iconMap); void setButtonText(const QString &buttonText); + void setCompact(bool compact); + bool isCompact() const + { + return compact; + }; protected: void mousePressEvent(QMouseEvent *event) override; @@ -34,6 +39,8 @@ private slots: private: QHBoxLayout *layout; QToolButton *button; + QString buttonText; + bool compact; public: SettingsPopupWidget *popup; diff --git a/cockatrice/src/interface/widgets/utility/compact_push_button.cpp b/cockatrice/src/interface/widgets/utility/compact_push_button.cpp new file mode 100644 index 000000000..cf98bfede --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/compact_push_button.cpp @@ -0,0 +1,73 @@ +#include "compact_push_button.h" + +CompactPushButton::CompactPushButton(QWidget *parent) : QPushButton(parent) +{ + setCheckable(true); + setFixedHeight(32); + + // default sizing + setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + connect(this, &QPushButton::clicked, this, [] { + // your popup logic here + }); +} + +void CompactPushButton::setButtonText(const QString &text) +{ + fullText = text; + + if (!compact) { + setText(fullText); + } + + updateGeometryState(); +} + +void CompactPushButton::setButtonIcon(const QIcon &icon) +{ + setIcon(icon); +} + +void CompactPushButton::setCompact(bool enabled) +{ + compact = enabled; + + if (compact) { + setText(QString()); // icon only + } else { + setText(fullText); + } + + updateGeometryState(); +} + +void CompactPushButton::updateGeometryState() +{ + const int buttonHeight = 32; + + setMinimumHeight(buttonHeight); + setMaximumHeight(buttonHeight); + + if (compact) { + setMinimumWidth(buttonHeight); + setMaximumWidth(buttonHeight); + } else { + setMinimumWidth(0); + setMaximumWidth(QWIDGETSIZE_MAX); + } + + updateGeometry(); +} + +int CompactPushButton::expandedWidth() const +{ + QFontMetrics fm(font()); + + return fm.horizontalAdvance(fullText) + 48; // icon + padding +} + +int CompactPushButton::compactWidth() const +{ + return 32; +} \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/utility/compact_push_button.h b/cockatrice/src/interface/widgets/utility/compact_push_button.h new file mode 100644 index 000000000..b0017beee --- /dev/null +++ b/cockatrice/src/interface/widgets/utility/compact_push_button.h @@ -0,0 +1,31 @@ +#ifndef COCKATRICE_COMPACT_PUSH_BUTTON_H +#define COCKATRICE_COMPACT_PUSH_BUTTON_H + +#include + +class CompactPushButton : public QPushButton +{ + Q_OBJECT + +public: + explicit CompactPushButton(QWidget *parent = nullptr); + + void setButtonText(const QString &text); + + void setButtonIcon(const QIcon &icon); + + void setCompact(bool enabled); + + int expandedWidth() const; + + int compactWidth() const; + +private: + void updateGeometryState(); + +private: + QString fullText; + bool compact = false; +}; + +#endif // COCKATRICE_COMPACT_PUSH_BUTTON_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp index 790ac771a..885925694 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.cpp @@ -5,16 +5,9 @@ #include VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent) - : QWidget(_parent), visualDatabaseDisplay(_parent) + : FlowWidget(_parent, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff), + visualDatabaseDisplay(_parent) { - filterContainerLayout = new QHBoxLayout(this); - filterContainerLayout->setContentsMargins(11, 0, 11, 0); - filterContainerLayout->setSpacing(2); - setLayout(filterContainerLayout); - filterContainerLayout->setAlignment(Qt::AlignLeft); - - setMaximumHeight(80); - connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay, &VisualDatabaseDisplayWidget::onSearchModelChanged); @@ -131,10 +124,18 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() filterLayout->addWidget(quickFilterFormatLegalityWidget); // put everything into main layout - filterContainerLayout->addWidget(sortGroupBox); - filterContainerLayout->addWidget(filterGroupBox); - filterContainerLayout->addStretch(); - filterContainerLayout->addWidget(quickFilterSaveLoadWidget); + addWidget(sortGroupBox); + addWidget(filterGroupBox); + auto *spacer = new QWidget(this); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + spacer->setAttribute(Qt::WA_TransparentForMouseEvents); + addWidget(spacer); + addWidget(quickFilterSaveLoadWidget); + addWidget(quickFilterSaveLoadWidget); + + // Force a layout pass so sizeHint() is accurate + layout()->activate(); + fullWidthHint = sizeHint().width(); } void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi() @@ -155,4 +156,25 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi() quickFilterSubTypeWidget->setButtonText(tr("Sub Type")); quickFilterSetWidget->setButtonText(tr("Sets")); quickFilterFormatLegalityWidget->setButtonText(tr("Formats")); +} + +void VisualDatabaseDisplayFilterToolbarWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + updateCompactMode(event->size().width()); +} + +void VisualDatabaseDisplayFilterToolbarWidget::updateCompactMode(int availableWidth) +{ + const bool compact = availableWidth < fullWidthHint; + + const QList filterButtons = { + quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget, + quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterFormatLegalityWidget, + }; + + for (auto *btn : filterButtons) { + if (btn->isCompact() != compact) // only act on transitions + btn->setCompact(compact); + } } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h index 5cca5187a..5b55f4ba6 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_filter_toolbar_widget.h @@ -10,7 +10,7 @@ class VisualDatabaseDisplayWidget; -class VisualDatabaseDisplayFilterToolbarWidget : public QWidget +class VisualDatabaseDisplayFilterToolbarWidget : public FlowWidget { Q_OBJECT @@ -32,7 +32,6 @@ private: QGroupBox *filterGroupBox; QLabel *filterByLabel; - QHBoxLayout *filterContainerLayout; SettingsButtonWidget *quickFilterSaveLoadWidget; VisualDatabaseDisplayFilterSaveLoadWidget *saveLoadWidget; SettingsButtonWidget *quickFilterNameWidget; @@ -45,6 +44,12 @@ private: VisualDatabaseDisplaySetFilterWidget *setFilterWidget; SettingsButtonWidget *quickFilterFormatLegalityWidget; VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget; + + int fullWidthHint = 0; + void updateCompactMode(int availableWidth); + +protected: + void resizeEvent(QResizeEvent *event) override; }; #endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_TOOLBAR_WIDGET_H diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp index 9c4432042..f9a783a3c 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.cpp @@ -51,9 +51,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance(), &SettingsCache::setVisualDatabaseDisplayCardSize); - searchContainer = new QWidget(this); - searchLayout = new QHBoxLayout(searchContainer); - searchContainer->setLayout(searchLayout); + searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); searchEdit = new SearchLineEdit(); searchEdit->setObjectName("searchEdit"); @@ -152,10 +150,10 @@ void VisualDatabaseDisplayWidget::initialize() filterContainer->initialize(); filterContainer->setVisible(true); - searchLayout->addWidget(colorFilterWidget); - searchLayout->addWidget(clearFilterWidget); - searchLayout->addWidget(searchEdit); - searchLayout->addWidget(displayModeButton); + searchContainer->addWidget(colorFilterWidget); + searchContainer->addWidget(clearFilterWidget); + searchContainer->addWidget(searchEdit); + searchContainer->addWidget(displayModeButton); mainLayout->addWidget(searchContainer); diff --git a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h index 190ea1f6d..1db953b26 100644 --- a/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h +++ b/cockatrice/src/interface/widgets/visual_database_display/visual_database_display_widget.h @@ -91,8 +91,7 @@ protected slots: void onDisplayModeChanged(bool checked); private: - QWidget *searchContainer; - QHBoxLayout *searchLayout; + FlowWidget *searchContainer; SearchLineEdit *searchEdit; QPushButton *displayModeButton; FilterTreeModel *filterModel; diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp index 79a98fda6..f44c9c3ef 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.cpp @@ -72,7 +72,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent) sortCriteriaButton->addSettingsWidget(sortLabel); sortCriteriaButton->addSettingsWidget(sortByListWidget); - displayTypeButton = new QPushButton(this); + displayTypeButton = new CompactPushButton(this); connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckDisplayOptionsWidget::updateDisplayType); groupAndSortLayout->addWidget(groupByLabel); @@ -91,7 +91,8 @@ void VisualDeckDisplayOptionsWidget::retranslateUi() sortByLabel->setText(tr("Sort by:")); sortLabel->setText(tr("Click and drag to change the sort order within the groups")); sortCriteriaButton->setToolTip(tr("Configure how cards are sorted within their groups")); - displayTypeButton->setText(tr("Toggle Layout: Overlap")); + displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); + displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); displayTypeButton->setToolTip( tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); } @@ -115,11 +116,32 @@ void VisualDeckDisplayOptionsWidget::updateDisplayType() // Update UI and emit signal switch (currentDisplayType) { case DisplayType::Flat: - displayTypeButton->setText(tr("Toggle Layout: Flat")); + displayTypeButton->setButtonText(tr("Toggle Layout: Flat")); + displayTypeButton->setButtonIcon(QPixmap("theme:icons/scroll")); break; case DisplayType::Overlap: - displayTypeButton->setText(tr("Toggle Layout: Overlap")); + displayTypeButton->setButtonText(tr("Toggle Layout: Overlap")); + displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales")); break; } emit displayTypeChanged(currentDisplayType); } + +void VisualDeckDisplayOptionsWidget::updateCompactMode(bool mode) +{ + displayTypeButton->setCompact(mode); +} + +int VisualDeckDisplayOptionsWidget::expandedWidth() const +{ + return groupByLabel->sizeHint().width() + groupByComboBox->sizeHint().width() + sortByLabel->sizeHint().width() + + sortCriteriaButton->sizeHint().width() + displayTypeButton->expandedWidth() + + (groupAndSortLayout->spacing() * 4); +} + +int VisualDeckDisplayOptionsWidget::compactWidth() const +{ + return groupByLabel->sizeHint().width() + groupByComboBox->sizeHint().width() + sortByLabel->sizeHint().width() + + sortCriteriaButton->sizeHint().width() + displayTypeButton->compactWidth() + + (groupAndSortLayout->spacing() * 4); +} diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h index 7a447753f..9930ff50b 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_display_options_widget.h @@ -53,6 +53,9 @@ public slots: * Called when the application language changes. */ void retranslateUi(); + void updateCompactMode(bool mode); + int expandedWidth() const; + int compactWidth() const; public: /** @@ -108,7 +111,7 @@ private: DisplayType currentDisplayType = DisplayType::Overlap; /// Button used to toggle the display layout. - QPushButton *displayTypeButton; + CompactPushButton *displayTypeButton; /// Label for the group-by selector. QLabel *groupByLabel; diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp index e957eb304..19a99d3e9 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.cpp @@ -9,6 +9,7 @@ #include "../general/layout_containers/flow_widget.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual.h" #include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.h" +#include "../utility/compact_push_button.h" #include "visual_deck_display_options_widget.h" #include @@ -69,7 +70,13 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent, void VisualDeckEditorWidget::initializeSearchBarAndCompleter() { - searchBar = new QLineEdit(this); + searchContainer = new QWidget(this); + searchContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + searchLayout = new QHBoxLayout(searchContainer); + searchContainer->setLayout(searchLayout); + + searchBar = new QLineEdit(searchContainer); + searchContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); connect(searchBar, &QLineEdit::returnPressed, this, [=, this]() { if (!searchBar->hasFocus()) return; @@ -80,6 +87,8 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() } }); + searchLayout->addWidget(searchBar); + setFocusProxy(searchBar); setFocusPolicy(Qt::ClickFocus); @@ -133,13 +142,16 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter() }); // Search button functionality - searchPushButton = new QPushButton(this); + searchPushButton = new CompactPushButton(searchContainer); + searchPushButton->setButtonIcon(QPixmap("theme:icons/search")); connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()}); if (card) { emit cardAdditionRequested(card); } }); + + searchLayout->addWidget(searchPushButton); } void VisualDeckEditorWidget::initializeDisplayOptionsWidget() @@ -156,18 +168,14 @@ void VisualDeckEditorWidget::initializeDisplayOptionsWidget() void VisualDeckEditorWidget::initializeDisplayOptionsAndSearchWidget() { initializeSearchBarAndCompleter(); - initializeDisplayOptionsWidget(); - displayOptionsAndSearch = new QWidget(this); - displayOptionsAndSearchLayout = new QHBoxLayout(displayOptionsAndSearch); - displayOptionsAndSearchLayout->setContentsMargins(0, 0, 0, 0); - displayOptionsAndSearchLayout->setAlignment(Qt::AlignLeft); - displayOptionsAndSearch->setLayout(displayOptionsAndSearchLayout); + displayOptionsAndSearch = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff); - displayOptionsAndSearchLayout->addWidget(displayOptionsWidget); - displayOptionsAndSearchLayout->addWidget(searchBar); - displayOptionsAndSearchLayout->addWidget(searchPushButton); + // We split into two sub-widgets here so that the searchBar and button wrap together. At this point, we've done + // pretty much all we can and have reached our minimum size. + displayOptionsAndSearch->addWidget(displayOptionsWidget); + displayOptionsAndSearch->addWidget(searchContainer); // Expanding — fills remainder of its row } void VisualDeckEditorWidget::initializeScrollAreaAndZoneContainer() @@ -205,7 +213,7 @@ void VisualDeckEditorWidget::connectDeckListModel() void VisualDeckEditorWidget::retranslateUi() { searchBar->setPlaceholderText(tr("Type a card name here for suggestions from the database...")); - searchPushButton->setText(tr("Quick search and add card")); + searchPushButton->setButtonText(tr("Quick search and add card")); searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add " "preferred printing to the deck on pressing enter")); @@ -214,6 +222,44 @@ void VisualDeckEditorWidget::retranslateUi() } } +void VisualDeckEditorWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + updateCompactMode(); +} + +void VisualDeckEditorWidget::updateCompactMode() +{ + const int spacing = displayOptionsAndSearch->layout()->spacing(); + + const int available = displayOptionsAndSearch->width(); + + const int searchExpanded = + searchBar->sizeHint().width() + searchPushButton->expandedWidth() + searchLayout->spacing(); + + const int fullWidth = displayOptionsWidget->expandedWidth() + spacing + searchExpanded; + + const int displayCompactWidth = displayOptionsWidget->compactWidth() + spacing + searchExpanded; + + // everything expanded + if (available >= fullWidth) { + displayOptionsWidget->updateCompactMode(false); + searchPushButton->setCompact(false); + return; + } + + // only display compact + if (available >= displayCompactWidth) { + displayOptionsWidget->updateCompactMode(true); + searchPushButton->setCompact(false); + return; + } + + // both compact + displayOptionsWidget->updateCompactMode(true); + searchPushButton->setCompact(true); +} + void VisualDeckEditorWidget::updatePlaceholderVisibility() { if (placeholderWidget) { diff --git a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h index 13065d623..3b90ee4e2 100644 --- a/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h +++ b/cockatrice/src/interface/widgets/visual_deck_editor/visual_deck_editor_widget.h @@ -11,6 +11,7 @@ #include "../cards/card_size_widget.h" #include "../general/layout_containers/overlap_control_widget.h" #include "../quick_settings/settings_button_widget.h" +#include "../utility/compact_push_button.h" #include "visual_deck_editor_placeholder_widget.h" #include @@ -39,6 +40,7 @@ class VisualDeckEditorWidget : public QWidget public: explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel); void retranslateUi(); + void updateCompactMode(); void clearAllDisplayWidgets(); void setDeckList(const DeckList &_deckListModel); @@ -82,19 +84,23 @@ protected slots: void onHover(const ExactCard &hoveredCard); void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); void decklistModelReset(); + void resizeEvent(QResizeEvent *event) override; private: + int expandedWidthAll = -1; + int expandedWidthDisplayCompact = -1; DeckListModel *deckListModel; QItemSelectionModel *selectionModel; QVBoxLayout *mainLayout; CardDatabaseModel *cardDatabaseModel; CardDatabaseDisplayModel *cardDatabaseDisplayModel; CardCompleterProxyModel *proxyModel; + QWidget *searchContainer; + QHBoxLayout *searchLayout; QCompleter *completer; - QWidget *displayOptionsAndSearch; - QHBoxLayout *displayOptionsAndSearchLayout; + FlowWidget *displayOptionsAndSearch; VisualDeckDisplayOptionsWidget *displayOptionsWidget; - QPushButton *searchPushButton; + CompactPushButton *searchPushButton; QScrollArea *scrollArea; QWidget *zoneContainer; QVBoxLayout *zoneContainerLayout;