[VDD] Fix minimum size by adding a compact mode to quickSettingsButtons (#6890)

* [VDD] Fix minimum size by adding a compact mode to quickSettingsButtons

Took 17 minutes

Took 5 seconds

* Fix and use FlowWidget/FlowLayout

Took 35 minutes

Took 4 seconds

* Set spacings.

Took 12 minutes

* Make VDE tools flow

Took 1 hour 23 minutes

Took 5 seconds

* Squeeze and flow even more.

Took 11 minutes

* Make pushbutton compact.

Took 54 minutes

Took 7 seconds

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-05-14 19:07:15 +02:00 committed by GitHub
parent 762e742be0
commit 7153f7d4c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 666 additions and 617 deletions

View file

@ -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_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.cpp
src/interface/widgets/tabs/api/edhrec/display/commander/edhrec_commander_api_response_budget_navigation_widget.h 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) add_subdirectory(sounds)

View file

@ -1,7 +1,16 @@
/** /**
* @file flow_layout.cpp * @file flow_layout.cpp
* @brief Implementation of the FlowLayout class, a custom layout for dynamically organizing widgets * @brief Implementation of FlowLayout a QLayout that wraps child widgets into rows
* in rows within the constraints of available width or parent scroll areas. * (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" #include "flow_layout.h"
@ -12,27 +21,18 @@
#include <QLayoutItem> #include <QLayoutItem>
#include <QScrollArea> #include <QScrollArea>
#include <QStyle> #include <QStyle>
#include <QWidgetItem>
/**
* @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, FlowLayout::FlowLayout(QWidget *parent,
const Qt::Orientation _flowDirection, const Qt::Orientation flowDirection,
const int margin, const int margin,
const int hSpacing, const int hSpacing,
const int vSpacing) const int vSpacing)
: QLayout(parent), flowDirection(_flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing) : QLayout(parent), flowDirection(flowDirection), horizontalMargin(hSpacing), verticalMargin(vSpacing)
{ {
setContentsMargins(margin, margin, margin, margin); setContentsMargins(margin, margin, margin, margin);
} }
/**
* @brief Destructor for FlowLayout, which cleans up all items in the layout.
*/
FlowLayout::~FlowLayout() FlowLayout::~FlowLayout()
{ {
QLayoutItem *item; QLayoutItem *item;
@ -42,499 +42,349 @@ FlowLayout::~FlowLayout()
} }
/** /**
* @brief Indicates the layout's support for expansion in both horizontal and vertical directions. * @brief Reports which axis the layout can expand along.
* @return The orientations (Qt::Horizontal | Qt::Vertical) this layout can expand to fill. *
* 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 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. * @brief Height-for-width is only meaningful for horizontal (wrapping) flow.
* @return True, as the layout adjusts its height to fit the specified width.
*/ */
bool FlowLayout::hasHeightForWidth() const bool FlowLayout::hasHeightForWidth() const
{ {
return true; return flowDirection == Qt::Horizontal;
} }
/** /**
* @brief Calculates the required height to display all items within the specified width. * @brief Returns the height required to display all items within @p 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. * 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 int FlowLayout::heightForWidth(const int width) const
{ {
if (flowDirection == Qt::Vertical) { if (flowDirection != Qt::Horizontal)
int height = 0; return -1;
int rowWidth = 0;
int rowHeight = 0;
for (const QLayoutItem *item : items) { int totalHeight = 0;
if (item == nullptr || item->isEmpty()) { int rowUsedWidth = 0;
continue; int rowHeight = 0;
}
int itemWidth = item->sizeHint().width() + horizontalSpacing(); for (const QLayoutItem *item : items) {
if (rowWidth + itemWidth > width) { if (!item || item->isEmpty()) {
height += rowHeight + verticalSpacing();
rowWidth = itemWidth;
rowHeight = item->sizeHint().height();
} else {
rowWidth += itemWidth;
rowHeight = qMax(rowHeight, item->sizeHint().height());
}
}
height += rowHeight; // Add height of the last row
return height;
} else {
int width = 0;
int colWidth = 0;
int colHeight = 0;
for (const QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue;
}
int itemHeight = item->sizeHint().height();
if (colHeight + itemHeight > width) {
width += colWidth;
colHeight = itemHeight;
colWidth = item->sizeHint().width();
} else {
colHeight += itemHeight;
colWidth = qMax(colWidth, item->sizeHint().width());
}
}
width += colWidth; // Add width of the last column
return width;
}
}
/**
* @brief Arranges layout items in rows within the specified rectangle bounds.
* @param rect The area within which to position layout items.
*/
void FlowLayout::setGeometry(const QRect &rect)
{
QLayout::setGeometry(rect); // Sets the geometry of the layout based on the given rectangle.
if (flowDirection == Qt::Horizontal) {
// If we have a parent scroll area, we're clamped to that, else we use our own rectangle.
const int availableWidth = getParentScrollAreaWidth() == 0 ? rect.width() : getParentScrollAreaWidth();
const int totalHeight = layoutAllRows(rect.x(), rect.y(), availableWidth);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setFixedSize(availableWidth, totalHeight);
}
} else {
const int availableHeight = qMax(rect.height(), getParentScrollAreaHeight());
const int totalWidth = layoutAllColumns(rect.x(), rect.y(), availableHeight);
if (QWidget *parentWidgetPtr = parentWidget()) {
parentWidgetPtr->setFixedSize(totalWidth, availableHeight);
}
}
}
/**
* @brief Lays out items into rows according to the available width, starting from a given origin.
* Each row is arranged within `availableWidth`, wrapping to a new row as necessary.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableWidth The width within which each row is constrained.
* @return The total height after arranging all rows.
*/
int FlowLayout::layoutAllRows(const int originX, const int originY, const int availableWidth)
{
QVector<QLayoutItem *> rowItems; // Holds items for the current row
int currentXPosition = originX; // Tracks the x-coordinate while placing items
int currentYPosition = originY; // Tracks the y-coordinate, moving down after each row
int rowHeight = 0; // Tracks the maximum height of items in the current row
for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) {
continue; continue;
} }
QSize itemSize = item->sizeHint(); // The suggested size for the current item const QSize itemSize = item->sizeHint();
int itemWidth = itemSize.width() + horizontalSpacing(); // Item width plus spacing // 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 (rowUsedWidth > 0 && rowUsedWidth + spaceX + itemSize.width() > width) {
if (currentXPosition + itemWidth > availableWidth) { // This item overflows the current row — commit the row and start a new one.
// If not, layout the current row and start a new row totalHeight += rowHeight + verticalSpacing();
layoutSingleRow(rowItems, originX, currentYPosition); rowUsedWidth = itemSize.width();
rowItems.clear(); // Reset the list for the new row rowHeight = itemSize.height();
currentXPosition = originX; // Reset x-position to the row's start } else {
currentYPosition += rowHeight + verticalSpacing(); // Move y-position down to the next row rowUsedWidth += spaceX + itemSize.width();
rowHeight = 0; // Reset row height for the new row 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<QLayoutItem *> 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 // Add the item to the current row
rowItems.append(item); rowItems.append(item);
rowHeight = qMax(rowHeight, itemSize.height()); // Update the row's height to the tallest item // `rowItems.size() > 1` is equivalent to "this is not the first item in the row"
currentXPosition += itemWidth + horizontalSpacing(); // Move x-position for the next item // 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, currentY, availableWidth); // Flush the final row.
layoutSingleRow(rowItems, originX, currentYPosition); return currentY + rowHeight;
// 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. * @brief Sets the geometry for every item in @p rowItems, starting at (@p x, @p y).
* @param rowItems A list of items to be arranged in the row. *
* @param x The starting x-coordinate for the row. * Items whose horizontal size policy includes the Expand or MinimumExpanding flag
* @param y The starting y-coordinate for the row. * 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<QLayoutItem *> &rowItems, int x, const int y) void FlowLayout::layoutSingleRow(const QVector<QLayoutItem *> &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) { for (QLayoutItem *item : rowItems) {
if (item == nullptr || item->isEmpty()) { if (!item || item->isEmpty()) {
continue; continue;
} }
// Get the maximum allowed size for the item QWidget *widget = item->widget();
QSize itemMaxSize = item->widget()->maximumSize(); const QSizePolicy::Policy hPolicy = widget ? widget->sizePolicy().horizontalPolicy() : QSizePolicy::Fixed;
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); if (hPolicy & QSizePolicy::ExpandFlag) {
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); ++expandingCount;
// Set the item's geometry based on the computed size and position } 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))); item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
// Move the x-position to the right, leaving space for horizontal spacing
x += itemWidth + horizontalSpacing(); x += itemWidth + horizontalSpacing();
} }
} }
// ─── Column layout (vertical flow) ───────────────────────────────────────────
/** /**
* @brief Lays out items into columns according to the available height, starting from a given origin. * @brief Places all items into wrapping columns within @p availableHeight.
* Each column is arranged within `availableHeight`, wrapping to a new column as necessary. * @return The x-coordinate of the right edge of the last column.
* @param originX The x-coordinate for the layout start position.
* @param originY The y-coordinate for the layout start position.
* @param availableHeight The height within which each column is constrained.
* @return The total width after arranging all columns.
*/ */
int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight) int FlowLayout::layoutAllColumns(const int originX, const int originY, const int availableHeight)
{ {
QVector<QLayoutItem *> colItems; // Holds items for the current column QVector<QLayoutItem *> colItems;
int currentXPosition = originX; // Tracks the x-coordinate while placing items int colUsedHeight = 0; // Height consumed by items already in the current column.
int currentYPosition = originY; // Tracks the y-coordinate, resetting for each new column int currentX = originX;
int colWidth = 0;
int colWidth = 0; // Tracks the maximum width of items in the current column
for (QLayoutItem *item : items) { for (QLayoutItem *item : items) {
if (item == nullptr || item->isEmpty()) { if (!item || item->isEmpty()) {
continue; 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 (!colItems.isEmpty() && colUsedHeight + spaceY + itemSize.height() > availableHeight) {
if (currentYPosition + itemSize.height() > availableHeight) { // Current item does not fit — flush the current column, begin a new one.
// If not, layout the current column and start a new column layoutSingleColumn(colItems, currentX, originY);
layoutSingleColumn(colItems, currentXPosition, originY); colItems.clear();
colItems.clear(); // Reset the list for the new column currentX += colWidth + horizontalSpacing();
currentYPosition = originY; // Reset y-position to the column's start colUsedHeight = 0;
currentXPosition += colWidth; // Move x-position to the next column colWidth = 0;
colWidth = 0; // Reset column width for the new column
} }
// Add the item to the current column
colItems.append(item); colItems.append(item);
colWidth = qMax(colWidth, itemSize.width()); // Update the column's width to the widest item colUsedHeight += (colItems.size() > 1 ? verticalSpacing() : 0) + itemSize.height();
currentYPosition += itemSize.height(); // Move y-position for the next item colWidth = qMax(colWidth, itemSize.width());
} }
// Layout the final column if there are any remaining items layoutSingleColumn(colItems, currentX, originY); // Flush the final column.
layoutSingleColumn(colItems, currentXPosition, originY); return currentX + colWidth;
// 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. * @brief Sets the geometry for every item in @p colItems, starting at (@p x, @p y).
* @param colItems A list of items to be arranged in the column. *
* @param x The starting x-coordinate for the column. * Each item is placed at its sizeHint, clamped to its maximumSize.
* @param y The starting y-coordinate for the column.
*/ */
void FlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y) void FlowLayout::layoutSingleColumn(const QVector<QLayoutItem *> &colItems, const int x, int y)
{ {
for (QLayoutItem *item : colItems) { for (QLayoutItem *item : colItems) {
if (item == nullptr) { if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Item is null."; qCDebug(FlowLayoutLog) << "Skipping null or empty item in column.";
continue; 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(); QWidget *widget = item->widget();
if (widget) { if (!widget) {
qCDebug(FlowLayoutLog) << "Widget class:" << widget->metaObject()->className(); qCDebug(FlowLayoutLog) << "Item has no widget; skipping.";
qCDebug(FlowLayoutLog) << "Widget size hint:" << widget->sizeHint(); continue;
qCDebug(FlowLayoutLog) << "Widget maximum size:" << widget->maximumSize();
qCDebug(FlowLayoutLog) << "Widget minimum size:" << widget->minimumSize();
// Debugging: Print child widgets
const QObjectList &children = widget->children();
qCDebug(FlowLayoutLog) << "Child widgets:";
for (QObject *child : children) {
if (QWidget *childWidget = qobject_cast<QWidget *>(child)) {
qCDebug(FlowLayoutLog) << " - Child widget class:" << childWidget->metaObject()->className();
qCDebug(FlowLayoutLog) << " Size hint:" << childWidget->sizeHint();
qCDebug(FlowLayoutLog) << " Maximum size:" << childWidget->maximumSize();
}
}
} else {
qCDebug(FlowLayoutLog) << "Item does not have a widget.";
} }
// Get the maximum allowed size for the item qCDebug(FlowLayoutLog) << "Widget:" << widget->metaObject()->className() << "sizeHint:" << widget->sizeHint()
QSize itemMaxSize = widget->maximumSize(); << "maximumSize:" << widget->maximumSize() << "minimumSize:" << widget->minimumSize();
// Constrain the item's width and height to its size hint or maximum size
const int itemWidth = qMin(item->sizeHint().width(), itemMaxSize.width()); const QSize maxSize = widget->maximumSize();
const int itemHeight = qMin(item->sizeHint().height(), itemMaxSize.height()); const int itemWidth = qMin(item->sizeHint().width(), maxSize.width());
// Debugging: Print the computed geometry const int itemHeight = qMin(item->sizeHint().height(), maxSize.height());
qCDebug(FlowLayoutLog) << "Computed geometry: x=" << x << ", y=" << y << ", width=" << itemWidth
<< ", height=" << itemHeight; qCDebug(FlowLayoutLog) << "Placing at x=" << x << "y=" << y << "w=" << itemWidth << "h=" << itemHeight;
// Set the item's geometry based on the computed size and position // Set the item's geometry based on the computed size and position
item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight))); item->setGeometry(QRect(QPoint(x, y), QSize(itemWidth, itemHeight)));
y += itemHeight + verticalSpacing();
// Move the y-position down by the item's height to place the next item below
y += itemHeight;
} }
} }
/** /**
* @brief Calculates the preferred size of the layout based on the flow direction. * @brief Preferred size for horizontal flow: all items in a single row (unconstrained).
* @return A QSize representing the ideal dimensions of the layout. *
*/ * The actual displayed height is determined by heightForWidth() once Qt knows the
QSize FlowLayout::sizeHint() const * real available width.
{
if (flowDirection == Qt::Horizontal) {
return calculateSizeHintHorizontal();
} else {
return calculateSizeHintVertical();
}
}
/**
* @brief Calculates the minimum size required by the layout based on the flow direction.
* @return A QSize representing the minimum required dimensions.
*/
QSize FlowLayout::minimumSize() const
{
if (flowDirection == Qt::Horizontal) {
return calculateMinimumSizeHorizontal();
} else {
return calculateMinimumSizeVertical();
}
}
/**
* @brief Calculates the size hint for horizontal flow direction.
* @return A QSize representing the preferred dimensions.
*/ */
QSize FlowLayout::calculateSizeHintHorizontal() const QSize FlowLayout::calculateSizeHintHorizontal() const
{ {
int maxWidth = 0; // Tracks the maximum width needed int totalWidth = 0;
int totalHeight = 0; // Tracks the total height across all rows int maxHeight = 0;
int rowHeight = 0; // Tracks the height of the current row
int currentWidth = 0; // Tracks the current row's width
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth();
qCDebug(FlowLayoutLog) << "Calculating horizontal size hint. Available width:" << availableWidth;
for (const QLayoutItem *item : items) { for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) { if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue; continue;
} }
const QSize s = item->sizeHint();
QSize itemSize = item->sizeHint(); if (totalWidth > 0) {
int itemWidth = itemSize.width() + horizontalSpacing(); totalWidth += 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;
} }
totalWidth += s.width();
currentWidth += itemWidth; maxHeight = qMax(maxHeight, s.height());
rowHeight = qMax(rowHeight, itemSize.height());
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
} }
return QSize(totalWidth, maxHeight);
// Account for the final row
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight;
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
return QSize(maxWidth, totalHeight);
} }
/** /**
* @brief Calculates the minimum size for horizontal flow direction. * @brief Minimum size for horizontal flow: the largest single item.
* @return A QSize representing the minimum required dimensions. *
* This guarantees we can always display at least one item per row.
*/ */
QSize FlowLayout::calculateMinimumSizeHorizontal() const QSize FlowLayout::calculateMinimumSizeHorizontal() const
{ {
int maxWidth = 0; // Tracks the maximum width of a row QSize size(0, 0);
int totalHeight = 0; // Tracks the total height across all rows for (const QLayoutItem *item : items) {
int rowHeight = 0; // Tracks the height of the current row if (!item || item->isEmpty()) {
int currentWidth = 0; // Tracks the current row's width qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
size = size.expandedTo(item->minimumSize());
}
return size;
}
const int availableWidth = getParentScrollAreaWidth() == 0 ? parentWidget()->width() : getParentScrollAreaWidth(); /**
* @brief Preferred size for vertical flow: all items in a single column (unconstrained).
qCDebug(FlowLayoutLog) << "Calculating horizontal minimum size. Available width:" << availableWidth; */
QSize FlowLayout::calculateSizeHintVertical() const
{
int maxWidth = 0;
int totalHeight = 0;
for (const QLayoutItem *item : items) { for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) { if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item."; qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue; continue;
} }
const QSize s = item->sizeHint();
QSize itemMinSize = item->minimumSize(); if (totalHeight > 0) {
int itemWidth = itemMinSize.width() + horizontalSpacing(); totalHeight += verticalSpacing();
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;
} }
totalHeight += s.height();
currentWidth += itemWidth; maxWidth = qMax(maxWidth, s.width());
rowHeight = qMax(rowHeight, itemMinSize.height());
qCDebug(FlowLayoutLog) << "Updated current width:" << currentWidth << "Updated row height:" << rowHeight;
} }
// Account for the final row
maxWidth = qMax(maxWidth, currentWidth);
totalHeight += rowHeight;
qCDebug(FlowLayoutLog) << "Final total height:" << totalHeight << "Final max width:" << maxWidth;
return QSize(maxWidth, totalHeight); return QSize(maxWidth, totalHeight);
} }
/** /**
* @brief Calculates the size hint for vertical flow direction. * @brief Minimum size for vertical flow: the largest single item.
* @return A QSize representing the preferred dimensions.
*/
QSize FlowLayout::calculateSizeHintVertical() const
{
int totalWidth = 0;
int maxHeight = 0;
int colWidth = 0;
int currentHeight = 0;
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
qCDebug(FlowLayoutLog) << "Calculating vertical size hint. Available height:" << availableHeight;
for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue;
}
QSize itemSize = item->sizeHint();
qCDebug(FlowLayoutLog) << "Processing item. Size:" << itemSize;
if (currentHeight + itemSize.height() > availableHeight) {
qCDebug(FlowLayoutLog) << "Column overflow. Current height:" << currentHeight
<< "Column width:" << colWidth;
totalWidth += colWidth + horizontalSpacing();
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Updated total width:" << totalWidth << "Max height so far:" << maxHeight;
currentHeight = 0;
colWidth = 0;
}
currentHeight += itemSize.height() + verticalSpacing();
colWidth = qMax(colWidth, itemSize.width());
qCDebug(FlowLayoutLog) << "Updated current height:" << currentHeight << "Updated column width:" << colWidth;
}
// Account for the final column
totalWidth += colWidth;
maxHeight = qMax(maxHeight, currentHeight);
qCDebug(FlowLayoutLog) << "Final total width:" << totalWidth << "Final max height:" << maxHeight;
return QSize(totalWidth, maxHeight);
}
/**
* @brief Calculates the minimum size for vertical flow direction.
* @return A QSize representing the minimum required dimensions.
*/ */
QSize FlowLayout::calculateMinimumSizeVertical() const QSize FlowLayout::calculateMinimumSizeVertical() const
{ {
int totalWidth = 0; // Tracks the total width across all columns QSize size(0, 0);
int maxHeight = 0; // Tracks the maximum height of a column
int colWidth = 0; // Tracks the width of the current column
int currentHeight = 0; // Tracks the current column's height
const int availableHeight = qMax(parentWidget()->height(), getParentScrollAreaHeight());
qCDebug(FlowLayoutLog) << "Calculating vertical minimum size. Available height:" << availableHeight;
for (const QLayoutItem *item : items) { for (const QLayoutItem *item : items) {
if (!item || item->isEmpty()) { if (!item || item->isEmpty()) {
qCDebug(FlowLayoutLog) << "Skipping empty item."; qCDebug(FlowLayoutLog) << "Skipping empty item.";
continue; continue;
} }
size = size.expandedTo(item->minimumSize());
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;
} }
return size;
// 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);
} }
/** /**
@ -543,7 +393,7 @@ QSize FlowLayout::calculateMinimumSizeVertical() const
*/ */
void FlowLayout::addItem(QLayoutItem *item) void FlowLayout::addItem(QLayoutItem *item)
{ {
if (item != nullptr) { if (item) {
items.append(item); items.append(item);
} }
} }
@ -551,11 +401,8 @@ void FlowLayout::addItem(QLayoutItem *item)
void FlowLayout::insertWidgetAtIndex(QWidget *toInsert, int index) void FlowLayout::insertWidgetAtIndex(QWidget *toInsert, int index)
{ {
addChildWidget(toInsert); addChildWidget(toInsert);
const int bounded = qBound(0, index, static_cast<int>(items.size()));
// We don't want to fail on an index that violates the bounds, so we just clamp it. items.insert(bounded, new QWidgetItem(toInsert));
int boundedIndex = qBound(0, index, qMax(0, static_cast<int>(items.size())));
items.insert(boundedIndex, new QWidgetItem(toInsert));
invalidate(); invalidate();
} }
@ -613,52 +460,13 @@ int FlowLayout::verticalSpacing() const
*/ */
int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const int FlowLayout::smartSpacing(const QStyle::PixelMetric pm) const
{ {
QObject *parent = this->parent(); QObject *p = parent();
if (!p) {
if (!parent) {
return -1; return -1;
} }
if (p->isWidgetType()) {
if (parent->isWidgetType()) { const auto *pw = static_cast<QWidget *>(p);
const auto *pw = dynamic_cast<QWidget *>(parent);
return pw->style()->pixelMetric(pm, nullptr, pw); return pw->style()->pixelMetric(pm, nullptr, pw);
} }
return static_cast<QLayout *>(p)->spacing();
return dynamic_cast<QLayout *>(parent)->spacing(); }
}
/**
* @brief Gets the width of the parent scroll area, if any.
* @return The width of the scroll area's viewport, or 0 if not found.
*/
int FlowLayout::getParentScrollAreaWidth() const
{
QWidget *parent = parentWidget();
while (parent) {
if (const auto *scrollArea = qobject_cast<QScrollArea *>(parent)) {
return scrollArea->viewport()->width();
}
parent = parent->parentWidget();
}
return 0;
}
/**
* @brief Gets the height of the parent scroll area, if any.
* @return The height of the scroll area's viewport, or 0 if not found.
*/
int FlowLayout::getParentScrollAreaHeight() const
{
QWidget *parent = parentWidget();
while (parent) {
if (const auto *scrollArea = qobject_cast<QScrollArea *>(parent)) {
return scrollArea->viewport()->height();
}
parent = parent->parentWidget();
}
return 0;
}

View file

@ -1,7 +1,8 @@
/** /**
* @file flow_layout.h * @file flow_layout.h
* @ingroup UI * @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 #ifndef FLOW_LAYOUT_H
@ -10,8 +11,8 @@
#include <QLayout> #include <QLayout>
#include <QList> #include <QList>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QStyle>
#include <QWidget> #include <QWidget>
#include <qstyle.h>
inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout", QtInfoMsg); inline Q_LOGGING_CATEGORY(FlowLayoutLog, "flow_layout", QtInfoMsg);
@ -19,42 +20,55 @@ class FlowLayout : public QLayout
{ {
public: public:
explicit FlowLayout(QWidget *parent = nullptr); 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; ~FlowLayout() override;
void insertWidgetAtIndex(QWidget *toInsert, int index); void insertWidgetAtIndex(QWidget *toInsert, int index);
[[nodiscard]] QSize calculateMinimumSizeHorizontal() const; // QLayout interface
[[nodiscard]] QSize calculateSizeHintVertical() const;
[[nodiscard]] QSize calculateMinimumSizeVertical() const;
void addItem(QLayoutItem *item) override; void addItem(QLayoutItem *item) override;
[[nodiscard]] int count() const override; [[nodiscard]] int count() const override;
[[nodiscard]] QLayoutItem *itemAt(int index) const override; [[nodiscard]] QLayoutItem *itemAt(int index) const override;
QLayoutItem *takeAt(int index) 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]] Qt::Orientations expandingDirections() const override;
[[nodiscard]] bool hasHeightForWidth() const override; [[nodiscard]] bool hasHeightForWidth() const override;
[[nodiscard]] int heightForWidth(int width) const override; [[nodiscard]] int heightForWidth(int width) const override;
[[nodiscard]] int verticalSpacing() const;
[[nodiscard]] int doLayout(const QRect &rect, bool testOnly) const;
[[nodiscard]] int smartSpacing(QStyle::PixelMetric pm) const;
[[nodiscard]] int getParentScrollAreaWidth() const;
[[nodiscard]] int getParentScrollAreaHeight() const;
void setGeometry(const QRect &rect) override;
virtual int layoutAllRows(int originX, int originY, int availableWidth);
virtual void layoutSingleRow(const QVector<QLayoutItem *> &rowItems, int x, int y);
int layoutAllColumns(int originX, int originY, int availableHeight);
void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
[[nodiscard]] QSize sizeHint() const override; [[nodiscard]] QSize sizeHint() const override;
[[nodiscard]] QSize minimumSize() 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<QLayoutItem *> &rowItems, int x, int y, int availableWidth);
int layoutAllColumns(int originX, int originY, int availableHeight);
void layoutSingleColumn(const QVector<QLayoutItem *> &colItems, int x, int y);
protected: protected:
QList<QLayoutItem *> 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<QLayoutItem *> items;
Qt::Orientation flowDirection; Qt::Orientation flowDirection;
int horizontalMargin; int horizontalMargin; ///< Horizontal spacing between items (-1 = use style default)
int verticalMargin; int verticalMargin; ///< Vertical spacing between items (-1 = use style default)
}; };
#endif // FLOW_LAYOUT_H #endif // FLOW_LAYOUT_H

View file

@ -20,12 +20,11 @@
DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer) DrawProbabilityWidget::DrawProbabilityWidget(QWidget *parent, DeckListStatisticsAnalyzer *analyzer)
: AbstractAnalyticsPanelWidget(parent, analyzer) : AbstractAnalyticsPanelWidget(parent, analyzer)
{ {
controls = new QWidget(this); controls = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
controlLayout = new QHBoxLayout(controls); controls->setSpacing(4, 4);
controlLayout->setContentsMargins(11, 0, 11, 0);
labelPrefix = new QLabel(this); labelPrefix = new QLabel(this);
controlLayout->addWidget(labelPrefix); controls->addWidget(labelPrefix);
criteriaCombo = new QComboBox(this); criteriaCombo = new QComboBox(this);
// Give these things item-data so we can translate the actual user-facing strings // 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(), "type");
criteriaCombo->addItem(QString(), "subtype"); criteriaCombo->addItem(QString(), "subtype");
criteriaCombo->addItem(QString(), "cmc"); criteriaCombo->addItem(QString(), "cmc");
controlLayout->addWidget(criteriaCombo); controls->addWidget(criteriaCombo);
exactnessCombo = new QComboBox(this); exactnessCombo = new QComboBox(this);
exactnessCombo->addItem(QString(), true); // At least exactnessCombo->addItem(QString(), true); // At least
exactnessCombo->addItem(QString(), false); // Exactly exactnessCombo->addItem(QString(), false); // Exactly
controlLayout->addWidget(exactnessCombo); controls->addWidget(exactnessCombo);
quantitySpin = new QSpinBox(this); quantitySpin = new QSpinBox(this);
quantitySpin->setRange(1, 60); quantitySpin->setRange(1, 60);
controlLayout->addWidget(quantitySpin); controls->addWidget(quantitySpin);
labelMiddle = new QLabel(this); labelMiddle = new QLabel(this);
controlLayout->addWidget(labelMiddle); controls->addWidget(labelMiddle);
drawnSpin = new QSpinBox(this); drawnSpin = new QSpinBox(this);
drawnSpin->setRange(1, 60); drawnSpin->setRange(1, 60);
drawnSpin->setValue(7); drawnSpin->setValue(7);
controlLayout->addWidget(drawnSpin); controls->addWidget(drawnSpin);
labelSuffix = new QLabel(this); labelSuffix = new QLabel(this);
controlLayout->addWidget(labelSuffix); controls->addWidget(labelSuffix);
labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); labelPrefix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); labelMiddle->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); labelSuffix->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
controlLayout->addStretch(1);
layout->addWidget(controls); layout->addWidget(controls);
// Table // Table

View file

@ -1,6 +1,8 @@
#ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H #ifndef COCKATRICE_DRAW_PROBABILITY_WIDGET_H
#define 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 "../../abstract_analytics_panel_widget.h"
#include "../../deck_list_statistics_analyzer.h" #include "../../deck_list_statistics_analyzer.h"
#include "draw_probability_config.h" #include "draw_probability_config.h"
@ -31,8 +33,7 @@ private slots:
private: private:
DrawProbabilityConfig config; DrawProbabilityConfig config;
QWidget *controls; FlowWidget *controls;
QHBoxLayout *controlLayout;
QLabel *labelPrefix; QLabel *labelPrefix;
QLabel *labelMiddle; QLabel *labelMiddle;
QLabel *labelSuffix; QLabel *labelSuffix;

View file

@ -23,8 +23,8 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
layout = new QVBoxLayout(this); layout = new QVBoxLayout(this);
// Controls // Controls
controlContainer = new QWidget(this); controlContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
controlLayout = new QHBoxLayout(controlContainer); controlContainer->setSpacing(4, 4);
addButton = new QPushButton(this); addButton = new QPushButton(this);
removeButton = new QPushButton(this); removeButton = new QPushButton(this);
saveButton = new QPushButton(this); saveButton = new QPushButton(this);
@ -32,11 +32,11 @@ DeckAnalyticsWidget::DeckAnalyticsWidget(QWidget *parent, DeckListStatisticsAnal
includeSideboardCheckBox = new QCheckBox(this); includeSideboardCheckBox = new QCheckBox(this);
includeSideboardCheckBox->setChecked(false); includeSideboardCheckBox->setChecked(false);
controlLayout->addWidget(addButton); controlContainer->addWidget(addButton);
controlLayout->addWidget(removeButton); controlContainer->addWidget(removeButton);
controlLayout->addWidget(saveButton); controlContainer->addWidget(saveButton);
controlLayout->addWidget(loadButton); controlContainer->addWidget(loadButton);
controlLayout->addWidget(includeSideboardCheckBox); controlContainer->addWidget(includeSideboardCheckBox);
layout->addWidget(controlContainer); layout->addWidget(controlContainer);

View file

@ -7,6 +7,7 @@
#ifndef DECK_ANALYTICS_WIDGET_H #ifndef DECK_ANALYTICS_WIDGET_H
#define DECK_ANALYTICS_WIDGET_H #define DECK_ANALYTICS_WIDGET_H
#include "../general/layout_containers/flow_widget.h"
#include "abstract_analytics_panel_widget.h" #include "abstract_analytics_panel_widget.h"
#include "deck_list_statistics_analyzer.h" #include "deck_list_statistics_analyzer.h"
#include "resizable_panel.h" #include "resizable_panel.h"
@ -51,8 +52,7 @@ private:
void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {}); void addPanelInstance(const QString &typeId, AbstractAnalyticsPanelWidget *panel, const QJsonObject &cfg = {});
QVBoxLayout *layout; QVBoxLayout *layout;
QWidget *controlContainer; FlowWidget *controlContainer;
QHBoxLayout *controlLayout;
QPushButton *addButton; QPushButton *addButton;
QPushButton *removeButton; QPushButton *removeButton;

View file

@ -1,42 +1,52 @@
/** /**
* @file flow_widget.cpp * @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 "flow_widget.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QResizeEvent> #include <QResizeEvent>
#include <QScrollArea>
#include <QSizePolicy>
#include <QWidget> #include <QWidget>
#include <qscrollarea.h>
#include <qsizepolicy.h>
/** /**
* @brief Constructs a FlowWidget with a scrollable layout. * @brief Constructs a FlowWidget.
* *
* @param parent The parent widget of this FlowWidget. * When both scroll policies are Qt::ScrollBarAlwaysOff the scroll area is
* @param horizontalPolicy The horizontal scroll bar policy for the scroll area. * omitted entirely and the container is placed directly in the main layout.
* @param verticalPolicy The vertical scroll bar policy for the scroll area. *
* @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, FlowWidget::FlowWidget(QWidget *parent,
const Qt::Orientation _flowDirection, const Qt::Orientation _flowDirection,
const Qt::ScrollBarPolicy horizontalPolicy, const Qt::ScrollBarPolicy horizontalPolicy,
const Qt::ScrollBarPolicy verticalPolicy) const Qt::ScrollBarPolicy verticalPolicy)
: QWidget(parent), flowDirection(_flowDirection) : QWidget(parent), scrollArea(nullptr), flowDirection(_flowDirection)
{ {
// Main Widget and Layout // Top-level size policy
if (_flowDirection == Qt::Horizontal) { // Horizontal flow: expand horizontally, let height be determined by wrapping.
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum); // Vertical flow: expand vertically, let width be determined by wrapping.
setMinimumWidth(0); if (flowDirection == Qt::Horizontal) {
setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
} else { } else {
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
setMinimumHeight(0);
} }
mainLayout = new QHBoxLayout(this); mainLayout = new QHBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
setLayout(mainLayout); setLayout(mainLayout);
if (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff) { const bool useScrollArea = (horizontalPolicy != Qt::ScrollBarAlwaysOff || verticalPolicy != Qt::ScrollBarAlwaysOff);
// Scroll Area, which should expand as much as possible, since it should be the only direct child widget.
// Scroll area (optional)
if (useScrollArea) {
scrollArea = new QScrollArea(this); scrollArea = new QScrollArea(this);
scrollArea->setWidgetResizable(true); scrollArea->setWidgetResizable(true);
scrollArea->setMinimumSize(0, 0); scrollArea->setMinimumSize(0, 0);
@ -48,39 +58,28 @@ FlowWidget::FlowWidget(QWidget *parent,
scrollArea = nullptr; scrollArea = nullptr;
} }
// Flow Layout inside the scroll area // Container widget (holds the FlowLayout)
if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) { container = new QWidget(useScrollArea ? static_cast<QWidget *>(scrollArea) : this);
container = new QWidget(this);
} else { // The container should be willing to grow in both axes; its actual size is
container = new QWidget(scrollArea); // 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); flowLayout = new FlowLayout(container, flowDirection);
container->setLayout(flowLayout); 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 (useScrollArea) {
if (horizontalPolicy == Qt::ScrollBarAlwaysOff && verticalPolicy == Qt::ScrollBarAlwaysOff) {
mainLayout->addWidget(container);
} else {
scrollArea->setWidget(container); scrollArea->setWidget(container);
mainLayout->addWidget(scrollArea); mainLayout->addWidget(scrollArea);
} else {
mainLayout->addWidget(container);
} }
} }
/** /**
* @brief Adds a widget to the flow layout within the FlowWidget. * @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. * @param widget_to_add The widget to add to the flow layout.
*/ */
void FlowWidget::addWidget(QWidget *widget_to_add) const 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() void FlowWidget::clearLayout()
{ {
if (flowLayout != nullptr) { if (flowLayout) {
QLayoutItem *item; QLayoutItem *item;
while ((item = flowLayout->takeAt(0)) != nullptr) { while ((item = flowLayout->takeAt(0))) {
item->widget()->deleteLater(); // Delete the widget if (item->widget())
delete item; // Delete the layout item item->widget()->deleteLater();
delete item;
} }
} else { } else {
// Defensive fallback: recreate the layout if it was deleted externally.
flowLayout = new FlowLayout(container, flowDirection); flowLayout = new FlowLayout(container, flowDirection);
container->setLayout(flowLayout); 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. * We do NOT call adjustSize() or activate() here:
* * - adjustSize() would freeze geometry by calling setFixedSize internally.
* @param event The resize event containing the new size information. * - 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) void FlowWidget::resizeEvent(QResizeEvent *event)
{ {
QWidget::resizeEvent(event); QWidget::resizeEvent(event);
qCDebug(FlowWidgetSizeLog) << "resizeEvent:" << event->size();
qCDebug(FlowWidgetSizeLog) << event->size(); if (flowLayout) {
flowLayout->invalidate();
// 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();
} }
} }
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() 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 // Iterate over all widgets in the flow layout to find the maximum sizeHint
for (int i = 0; i < flowLayout->count(); ++i) { for (int i = 0; i < flowLayout->count(); ++i) {
if (QLayoutItem *item = flowLayout->itemAt(i)) { QLayoutItem *item = flowLayout->itemAt(i);
if (QWidget *widget = item->widget()) { if (item && item->widget()) {
// Update the max size based on the sizeHint of each widget maxSize = maxSize.expandedTo(item->widget()->sizeHint());
QSize widgetSizeHint = widget->sizeHint();
maxSize.setWidth(qMax(maxSize.width(), widgetSizeHint.width()));
maxSize.setHeight(qMax(maxSize.height(), widgetSizeHint.height()));
}
} }
} }
// Set the minimum size for all widgets to the max sizeHint // Set the minimum size for all widgets to the max sizeHint
for (int i = 0; i < flowLayout->count(); ++i) { for (int i = 0; i < flowLayout->count(); ++i) {
if (QLayoutItem *item = flowLayout->itemAt(i)) { QLayoutItem *item = flowLayout->itemAt(i);
if (QWidget *widget = item->widget()) { if (item && item->widget()) {
widget->setMinimumSize(maxSize); item->widget()->setMinimumSize(maxSize);
}
} }
} }
} }

View file

@ -1,22 +1,23 @@
/** /**
* @file flow_widget.h * @file flow_widget.h
* @ingroup UI * @ingroup UI
* @brief TODO: Document this. * @brief A QWidget that wraps a FlowLayout inside an optional QScrollArea.
*/ */
#ifndef FLOW_WIDGET_H #ifndef FLOW_WIDGET_H
#define FLOW_WIDGET_H #define FLOW_WIDGET_H
#include "../../../layouts/flow_layout.h" #include "../../../layouts/flow_layout.h"
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QLoggingCategory> #include <QLoggingCategory>
#include <QScrollArea>
#include <QWidget> #include <QWidget>
#include <qscrollarea.h>
inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget", QtInfoMsg); inline Q_LOGGING_CATEGORY(FlowWidgetLog, "flow_widget", QtInfoMsg);
inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size", QtInfoMsg); inline Q_LOGGING_CATEGORY(FlowWidgetSizeLog, "flow_widget.size", QtInfoMsg);
class FlowWidget final : public QWidget class FlowWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -25,17 +26,20 @@ public:
Qt::Orientation orientation, Qt::Orientation orientation,
Qt::ScrollBarPolicy horizontalPolicy, Qt::ScrollBarPolicy horizontalPolicy,
Qt::ScrollBarPolicy verticalPolicy); Qt::ScrollBarPolicy verticalPolicy);
void addWidget(QWidget *widget_to_add) const; void addWidget(QWidget *widget_to_add) const;
void insertWidgetAtIndex(QWidget *toInsert, int index); void insertWidgetAtIndex(QWidget *toInsert, int index);
void removeWidget(QWidget *widgetToRemove) const; void removeWidget(QWidget *widgetToRemove) const;
void clearLayout(); void clearLayout();
[[nodiscard]] int count() const; [[nodiscard]] int count() const;
[[nodiscard]] QLayoutItem *itemAt(int index) const; [[nodiscard]] QLayoutItem *itemAt(int index) const;
QScrollArea *scrollArea; QScrollArea *scrollArea; ///< Null when both scroll policies are AlwaysOff.
public slots: public slots:
void setMinimumSizeToMaxSizeHint(); void setMinimumSizeToMaxSizeHint();
void setSpacing(int hSpacing, int vSpacing);
protected: protected:
void resizeEvent(QResizeEvent *event) override; void resizeEvent(QResizeEvent *event) override;
@ -47,4 +51,4 @@ private:
QWidget *container; QWidget *container;
}; };
#endif // FLOW_WIDGET_H #endif // FLOW_WIDGET_H

View file

@ -35,19 +35,33 @@ void SettingsButtonWidget::setButtonIcon(QPixmap iconMap)
button->setIcon(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->setMinimumSize(QSize(0, 0));
button->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)); button->setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX));
button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
button->setText(buttonText); button->setText(text);
button->setFixedHeight(32); button->setFixedHeight(32);
button->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 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() void SettingsButtonWidget::togglePopup()

View file

@ -23,6 +23,11 @@ public:
void removeSettingsWidget(QWidget *toRemove) const; void removeSettingsWidget(QWidget *toRemove) const;
void setButtonIcon(QPixmap iconMap); void setButtonIcon(QPixmap iconMap);
void setButtonText(const QString &buttonText); void setButtonText(const QString &buttonText);
void setCompact(bool compact);
bool isCompact() const
{
return compact;
};
protected: protected:
void mousePressEvent(QMouseEvent *event) override; void mousePressEvent(QMouseEvent *event) override;
@ -34,6 +39,8 @@ private slots:
private: private:
QHBoxLayout *layout; QHBoxLayout *layout;
QToolButton *button; QToolButton *button;
QString buttonText;
bool compact;
public: public:
SettingsPopupWidget *popup; SettingsPopupWidget *popup;

View file

@ -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;
}

View file

@ -0,0 +1,31 @@
#ifndef COCKATRICE_COMPACT_PUSH_BUTTON_H
#define COCKATRICE_COMPACT_PUSH_BUTTON_H
#include <QPushButton>
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

View file

@ -5,16 +5,9 @@
#include <QGroupBox> #include <QGroupBox>
VisualDatabaseDisplayFilterToolbarWidget::VisualDatabaseDisplayFilterToolbarWidget(VisualDatabaseDisplayWidget *_parent) 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, connect(this, &VisualDatabaseDisplayFilterToolbarWidget::searchModelChanged, visualDatabaseDisplay,
&VisualDatabaseDisplayWidget::onSearchModelChanged); &VisualDatabaseDisplayWidget::onSearchModelChanged);
@ -131,10 +124,18 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize()
filterLayout->addWidget(quickFilterFormatLegalityWidget); filterLayout->addWidget(quickFilterFormatLegalityWidget);
// put everything into main layout // put everything into main layout
filterContainerLayout->addWidget(sortGroupBox); addWidget(sortGroupBox);
filterContainerLayout->addWidget(filterGroupBox); addWidget(filterGroupBox);
filterContainerLayout->addStretch(); auto *spacer = new QWidget(this);
filterContainerLayout->addWidget(quickFilterSaveLoadWidget); 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() void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi()
@ -155,4 +156,25 @@ void VisualDatabaseDisplayFilterToolbarWidget::retranslateUi()
quickFilterSubTypeWidget->setButtonText(tr("Sub Type")); quickFilterSubTypeWidget->setButtonText(tr("Sub Type"));
quickFilterSetWidget->setButtonText(tr("Sets")); quickFilterSetWidget->setButtonText(tr("Sets"));
quickFilterFormatLegalityWidget->setButtonText(tr("Formats")); 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<SettingsButtonWidget *> filterButtons = {
quickFilterSaveLoadWidget, quickFilterNameWidget, quickFilterMainTypeWidget,
quickFilterSubTypeWidget, quickFilterSetWidget, quickFilterFormatLegalityWidget,
};
for (auto *btn : filterButtons) {
if (btn->isCompact() != compact) // only act on transitions
btn->setCompact(compact);
}
} }

View file

@ -10,7 +10,7 @@
class VisualDatabaseDisplayWidget; class VisualDatabaseDisplayWidget;
class VisualDatabaseDisplayFilterToolbarWidget : public QWidget class VisualDatabaseDisplayFilterToolbarWidget : public FlowWidget
{ {
Q_OBJECT Q_OBJECT
@ -32,7 +32,6 @@ private:
QGroupBox *filterGroupBox; QGroupBox *filterGroupBox;
QLabel *filterByLabel; QLabel *filterByLabel;
QHBoxLayout *filterContainerLayout;
SettingsButtonWidget *quickFilterSaveLoadWidget; SettingsButtonWidget *quickFilterSaveLoadWidget;
VisualDatabaseDisplayFilterSaveLoadWidget *saveLoadWidget; VisualDatabaseDisplayFilterSaveLoadWidget *saveLoadWidget;
SettingsButtonWidget *quickFilterNameWidget; SettingsButtonWidget *quickFilterNameWidget;
@ -45,6 +44,12 @@ private:
VisualDatabaseDisplaySetFilterWidget *setFilterWidget; VisualDatabaseDisplaySetFilterWidget *setFilterWidget;
SettingsButtonWidget *quickFilterFormatLegalityWidget; SettingsButtonWidget *quickFilterFormatLegalityWidget;
VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget; VisualDatabaseDisplayFormatLegalityFilterWidget *formatLegalityWidget;
int fullWidthHint = 0;
void updateCompactMode(int availableWidth);
protected:
void resizeEvent(QResizeEvent *event) override;
}; };
#endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_TOOLBAR_WIDGET_H #endif // COCKATRICE_VISUAL_DATABASE_DISPLAY_FILTER_TOOLBAR_WIDGET_H

View file

@ -51,9 +51,7 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent,
connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance(), connect(cardSizeWidget, &CardSizeWidget::cardSizeSettingUpdated, &SettingsCache::instance(),
&SettingsCache::setVisualDatabaseDisplayCardSize); &SettingsCache::setVisualDatabaseDisplayCardSize);
searchContainer = new QWidget(this); searchContainer = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
searchLayout = new QHBoxLayout(searchContainer);
searchContainer->setLayout(searchLayout);
searchEdit = new SearchLineEdit(); searchEdit = new SearchLineEdit();
searchEdit->setObjectName("searchEdit"); searchEdit->setObjectName("searchEdit");
@ -152,10 +150,10 @@ void VisualDatabaseDisplayWidget::initialize()
filterContainer->initialize(); filterContainer->initialize();
filterContainer->setVisible(true); filterContainer->setVisible(true);
searchLayout->addWidget(colorFilterWidget); searchContainer->addWidget(colorFilterWidget);
searchLayout->addWidget(clearFilterWidget); searchContainer->addWidget(clearFilterWidget);
searchLayout->addWidget(searchEdit); searchContainer->addWidget(searchEdit);
searchLayout->addWidget(displayModeButton); searchContainer->addWidget(displayModeButton);
mainLayout->addWidget(searchContainer); mainLayout->addWidget(searchContainer);

View file

@ -91,8 +91,7 @@ protected slots:
void onDisplayModeChanged(bool checked); void onDisplayModeChanged(bool checked);
private: private:
QWidget *searchContainer; FlowWidget *searchContainer;
QHBoxLayout *searchLayout;
SearchLineEdit *searchEdit; SearchLineEdit *searchEdit;
QPushButton *displayModeButton; QPushButton *displayModeButton;
FilterTreeModel *filterModel; FilterTreeModel *filterModel;

View file

@ -72,7 +72,7 @@ VisualDeckDisplayOptionsWidget::VisualDeckDisplayOptionsWidget(QWidget *parent)
sortCriteriaButton->addSettingsWidget(sortLabel); sortCriteriaButton->addSettingsWidget(sortLabel);
sortCriteriaButton->addSettingsWidget(sortByListWidget); sortCriteriaButton->addSettingsWidget(sortByListWidget);
displayTypeButton = new QPushButton(this); displayTypeButton = new CompactPushButton(this);
connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckDisplayOptionsWidget::updateDisplayType); connect(displayTypeButton, &QPushButton::clicked, this, &VisualDeckDisplayOptionsWidget::updateDisplayType);
groupAndSortLayout->addWidget(groupByLabel); groupAndSortLayout->addWidget(groupByLabel);
@ -91,7 +91,8 @@ void VisualDeckDisplayOptionsWidget::retranslateUi()
sortByLabel->setText(tr("Sort by:")); sortByLabel->setText(tr("Sort by:"));
sortLabel->setText(tr("Click and drag to change the sort order within the groups")); 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")); 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( displayTypeButton->setToolTip(
tr("Change how cards are displayed within zones (i.e. overlapped or fully visible.)")); 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 // Update UI and emit signal
switch (currentDisplayType) { switch (currentDisplayType) {
case DisplayType::Flat: case DisplayType::Flat:
displayTypeButton->setText(tr("Toggle Layout: Flat")); displayTypeButton->setButtonText(tr("Toggle Layout: Flat"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scroll"));
break; break;
case DisplayType::Overlap: case DisplayType::Overlap:
displayTypeButton->setText(tr("Toggle Layout: Overlap")); displayTypeButton->setButtonText(tr("Toggle Layout: Overlap"));
displayTypeButton->setButtonIcon(QPixmap("theme:icons/scales"));
break; break;
} }
emit displayTypeChanged(currentDisplayType); 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);
}

View file

@ -53,6 +53,9 @@ public slots:
* Called when the application language changes. * Called when the application language changes.
*/ */
void retranslateUi(); void retranslateUi();
void updateCompactMode(bool mode);
int expandedWidth() const;
int compactWidth() const;
public: public:
/** /**
@ -108,7 +111,7 @@ private:
DisplayType currentDisplayType = DisplayType::Overlap; DisplayType currentDisplayType = DisplayType::Overlap;
/// Button used to toggle the display layout. /// Button used to toggle the display layout.
QPushButton *displayTypeButton; CompactPushButton *displayTypeButton;
/// Label for the group-by selector. /// Label for the group-by selector.
QLabel *groupByLabel; QLabel *groupByLabel;

View file

@ -9,6 +9,7 @@
#include "../general/layout_containers/flow_widget.h" #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.h"
#include "../tabs/visual_deck_editor/tab_deck_editor_visual_tab_widget.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 "visual_deck_display_options_widget.h"
#include <QCheckBox> #include <QCheckBox>
@ -69,7 +70,13 @@ VisualDeckEditorWidget::VisualDeckEditorWidget(QWidget *parent,
void VisualDeckEditorWidget::initializeSearchBarAndCompleter() 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]() { connect(searchBar, &QLineEdit::returnPressed, this, [=, this]() {
if (!searchBar->hasFocus()) if (!searchBar->hasFocus())
return; return;
@ -80,6 +87,8 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
} }
}); });
searchLayout->addWidget(searchBar);
setFocusProxy(searchBar); setFocusProxy(searchBar);
setFocusPolicy(Qt::ClickFocus); setFocusPolicy(Qt::ClickFocus);
@ -133,13 +142,16 @@ void VisualDeckEditorWidget::initializeSearchBarAndCompleter()
}); });
// Search button functionality // Search button functionality
searchPushButton = new QPushButton(this); searchPushButton = new CompactPushButton(searchContainer);
searchPushButton->setButtonIcon(QPixmap("theme:icons/search"));
connect(searchPushButton, &QPushButton::clicked, this, [=, this]() { connect(searchPushButton, &QPushButton::clicked, this, [=, this]() {
ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()}); ExactCard card = CardDatabaseManager::query()->getCard({searchBar->text()});
if (card) { if (card) {
emit cardAdditionRequested(card); emit cardAdditionRequested(card);
} }
}); });
searchLayout->addWidget(searchPushButton);
} }
void VisualDeckEditorWidget::initializeDisplayOptionsWidget() void VisualDeckEditorWidget::initializeDisplayOptionsWidget()
@ -156,18 +168,14 @@ void VisualDeckEditorWidget::initializeDisplayOptionsWidget()
void VisualDeckEditorWidget::initializeDisplayOptionsAndSearchWidget() void VisualDeckEditorWidget::initializeDisplayOptionsAndSearchWidget()
{ {
initializeSearchBarAndCompleter(); initializeSearchBarAndCompleter();
initializeDisplayOptionsWidget(); initializeDisplayOptionsWidget();
displayOptionsAndSearch = new QWidget(this); displayOptionsAndSearch = new FlowWidget(this, Qt::Horizontal, Qt::ScrollBarAlwaysOff, Qt::ScrollBarAlwaysOff);
displayOptionsAndSearchLayout = new QHBoxLayout(displayOptionsAndSearch);
displayOptionsAndSearchLayout->setContentsMargins(0, 0, 0, 0);
displayOptionsAndSearchLayout->setAlignment(Qt::AlignLeft);
displayOptionsAndSearch->setLayout(displayOptionsAndSearchLayout);
displayOptionsAndSearchLayout->addWidget(displayOptionsWidget); // We split into two sub-widgets here so that the searchBar and button wrap together. At this point, we've done
displayOptionsAndSearchLayout->addWidget(searchBar); // pretty much all we can and have reached our minimum size.
displayOptionsAndSearchLayout->addWidget(searchPushButton); displayOptionsAndSearch->addWidget(displayOptionsWidget);
displayOptionsAndSearch->addWidget(searchContainer); // Expanding — fills remainder of its row
} }
void VisualDeckEditorWidget::initializeScrollAreaAndZoneContainer() void VisualDeckEditorWidget::initializeScrollAreaAndZoneContainer()
@ -205,7 +213,7 @@ void VisualDeckEditorWidget::connectDeckListModel()
void VisualDeckEditorWidget::retranslateUi() void VisualDeckEditorWidget::retranslateUi()
{ {
searchBar->setPlaceholderText(tr("Type a card name here for suggestions from the database...")); 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 " searchPushButton->setToolTip(tr("Search for closest match in the database (with auto-suggestions) and add "
"preferred printing to the deck on pressing enter")); "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() void VisualDeckEditorWidget::updatePlaceholderVisibility()
{ {
if (placeholderWidget) { if (placeholderWidget) {

View file

@ -11,6 +11,7 @@
#include "../cards/card_size_widget.h" #include "../cards/card_size_widget.h"
#include "../general/layout_containers/overlap_control_widget.h" #include "../general/layout_containers/overlap_control_widget.h"
#include "../quick_settings/settings_button_widget.h" #include "../quick_settings/settings_button_widget.h"
#include "../utility/compact_push_button.h"
#include "visual_deck_editor_placeholder_widget.h" #include "visual_deck_editor_placeholder_widget.h"
#include <QCheckBox> #include <QCheckBox>
@ -39,6 +40,7 @@ class VisualDeckEditorWidget : public QWidget
public: public:
explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel); explicit VisualDeckEditorWidget(QWidget *parent, DeckListModel *deckListModel, QItemSelectionModel *selectionModel);
void retranslateUi(); void retranslateUi();
void updateCompactMode();
void clearAllDisplayWidgets(); void clearAllDisplayWidgets();
void setDeckList(const DeckList &_deckListModel); void setDeckList(const DeckList &_deckListModel);
@ -82,19 +84,23 @@ protected slots:
void onHover(const ExactCard &hoveredCard); void onHover(const ExactCard &hoveredCard);
void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName); void onCardClick(QMouseEvent *event, CardInfoPictureWithTextOverlayWidget *instance, QString zoneName);
void decklistModelReset(); void decklistModelReset();
void resizeEvent(QResizeEvent *event) override;
private: private:
int expandedWidthAll = -1;
int expandedWidthDisplayCompact = -1;
DeckListModel *deckListModel; DeckListModel *deckListModel;
QItemSelectionModel *selectionModel; QItemSelectionModel *selectionModel;
QVBoxLayout *mainLayout; QVBoxLayout *mainLayout;
CardDatabaseModel *cardDatabaseModel; CardDatabaseModel *cardDatabaseModel;
CardDatabaseDisplayModel *cardDatabaseDisplayModel; CardDatabaseDisplayModel *cardDatabaseDisplayModel;
CardCompleterProxyModel *proxyModel; CardCompleterProxyModel *proxyModel;
QWidget *searchContainer;
QHBoxLayout *searchLayout;
QCompleter *completer; QCompleter *completer;
QWidget *displayOptionsAndSearch; FlowWidget *displayOptionsAndSearch;
QHBoxLayout *displayOptionsAndSearchLayout;
VisualDeckDisplayOptionsWidget *displayOptionsWidget; VisualDeckDisplayOptionsWidget *displayOptionsWidget;
QPushButton *searchPushButton; CompactPushButton *searchPushButton;
QScrollArea *scrollArea; QScrollArea *scrollArea;
QWidget *zoneContainer; QWidget *zoneContainer;
QVBoxLayout *zoneContainerLayout; QVBoxLayout *zoneContainerLayout;