diff --git a/.github/workflows/documentation-build.yml b/.github/workflows/documentation-build.yml index 7306c726a..b0093d6b1 100644 --- a/.github/workflows/documentation-build.yml +++ b/.github/workflows/documentation-build.yml @@ -1,9 +1,9 @@ name: Generate Docs on: - push: - tags: - - '*' # Only re-generate docs when a new tagged version is pushed + release: + types: + - published # publishing of stable releases and pre-releases pull_request: paths: - 'doc/doxygen/**' @@ -53,11 +53,11 @@ jobs: run: doxygen Doxyfile - name: Deploy to cockatrice.github.io - if: github.event_name != 'pull_request' + if: github.event_name == 'release' || github.event_name == 'workflow_dispatch' uses: peaceiris/actions-gh-pages@v4 with: deploy_key: ${{ secrets.DOCS_DEPLOY_KEY }} external_repository: Cockatrice/cockatrice.github.io publish_branch: master publish_dir: ./docs/html - destination_dir: docs # Docs will live under https://cockatrice.github.io/docs/ + destination_dir: docs # Docs will live under https://cockatrice.github.io/docs/ 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/client/settings/shortcuts_settings.h b/cockatrice/src/client/settings/shortcuts_settings.h index d9ea494dc..fc159db48 100644 --- a/cockatrice/src/client/settings/shortcuts_settings.h +++ b/cockatrice/src/client/settings/shortcuts_settings.h @@ -537,6 +537,9 @@ private: {"Player/aSetAnnotation", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Set Annotation..."), parseSequenceString("Alt+N"), ShortcutGroup::Playing_Area)}, + {"Player/aReduceLifeByPower", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Reduce Life by Power"), + parseSequenceString("Ctrl+Shift+L"), + ShortcutGroup::Playing_Area)}, {"Player/aSelectAll", ShortcutKey(QT_TRANSLATE_NOOP("shortcutsTab", "Select All Cards in Zone"), parseSequenceString("Ctrl+A"), ShortcutGroup::Playing_Area)}, diff --git a/cockatrice/src/game/board/abstract_card_item.cpp b/cockatrice/src/game/board/abstract_card_item.cpp index 9ec6ada9a..a519fa4b6 100644 --- a/cockatrice/src/game/board/abstract_card_item.cpp +++ b/cockatrice/src/game/board/abstract_card_item.cpp @@ -85,7 +85,11 @@ const CardInfo &AbstractCardItem::getCardInfo() const void AbstractCardItem::setRealZValue(qreal _zValue) { realZValue = _zValue; - setZValue(_zValue); + // During hover, zValue is overridden to HOVERED_CARD. Layout operations + // like reorganizeCards() call setRealZValue() on all cards including the + // hovered one — skip setZValue() here to avoid clobbering the override. + if (!isHovered) + setZValue(_zValue); } QSizeF AbstractCardItem::getTranslatedSize(QPainter *painter) const @@ -213,8 +217,16 @@ void AbstractCardItem::setHovered(bool _hovered) if (isHovered == _hovered) return; - if (_hovered) + if (_hovered) { processHoverEvent(); + } else { + // Mark the hovered card's current scene footprint dirty so overlapped + // sibling zones (e.g. StackZone) repaint after the card moves away. + if (scene()) { + scene()->update(sceneBoundingRect()); + } + } + isHovered = _hovered; setZValue(_hovered ? ZValues::HOVERED_CARD : realZValue); setScale(_hovered && SettingsCache::instance().getScaleCards() ? 1.1 : 1); diff --git a/cockatrice/src/game/board/abstract_card_item.h b/cockatrice/src/game/board/abstract_card_item.h index 7d2c29cae..517b5cf28 100644 --- a/cockatrice/src/game/board/abstract_card_item.h +++ b/cockatrice/src/game/board/abstract_card_item.h @@ -1,7 +1,7 @@ /** * @file abstract_card_item.h * @ingroup GameGraphicsCards - * @brief TODO: Document this. + * @brief Base class for graphical card items, providing shared rendering, identity, and interaction logic. */ #ifndef ABSTRACTCARDITEM_H @@ -96,6 +96,10 @@ public: } void setRealZValue(qreal _zValue); void setHovered(bool _hovered); + bool getIsHovered() const + { + return isHovered; + } QString getColor() const { return color; diff --git a/cockatrice/src/game/game_scene.cpp b/cockatrice/src/game/game_scene.cpp index 323adda38..aa6f1e6f1 100644 --- a/cockatrice/src/game/game_scene.cpp +++ b/cockatrice/src/game/game_scene.cpp @@ -5,6 +5,7 @@ #include "phases_toolbar.h" #include "player/player.h" #include "player/player_graphics_item.h" +#include "zones/select_zone.h" #include "zones/view_zone.h" #include "zones/view_zone_widget.h" @@ -356,12 +357,26 @@ void GameScene::updateHover(const QPointF &scenePos) void GameScene::updateHoveredCard(CardItem *newCard) { if (hoveredCard && (newCard != hoveredCard)) - hoveredCard->setHovered(false); + endCardHover(hoveredCard); if (newCard && (newCard != hoveredCard)) - newCard->setHovered(true); + beginCardHover(newCard); hoveredCard = newCard; } +void GameScene::beginCardHover(CardItem *card) +{ + card->setHovered(true); + if (auto *zone = SelectZone::findOwningSelectZone(card)) + zone->escapeClipForHover(card); +} + +void GameScene::endCardHover(CardItem *card) +{ + if (auto *zone = SelectZone::findOwningSelectZone(card)) + zone->restoreClipAfterHover(card); + card->setHovered(false); +} + CardZone *GameScene::findTopmostZone(const QList &items) { for (QGraphicsItem *item : items) @@ -496,6 +511,8 @@ bool GameScene::event(QEvent *event) { if (event->type() == QEvent::GraphicsSceneMouseMove) updateHover(static_cast(event)->scenePos()); + else if (event->type() == QEvent::Leave) + updateHoveredCard(nullptr); return QGraphicsScene::event(event); } diff --git a/cockatrice/src/game/game_scene.h b/cockatrice/src/game/game_scene.h index 2ce92b775..6d491297c 100644 --- a/cockatrice/src/game/game_scene.h +++ b/cockatrice/src/game/game_scene.h @@ -56,6 +56,12 @@ private: */ void updateHover(const QPointF &scenePos); + /// Activates hover state and escapes the card from its clip container so hover scaling is visible beyond zone + /// bounds. + void beginCardHover(CardItem *card); + /// Deactivates hover state and restores the card to its clip container. + void endCardHover(CardItem *card); + public: /** * @brief Constructs the GameScene. diff --git a/cockatrice/src/game/player/menu/card_menu.cpp b/cockatrice/src/game/player/menu/card_menu.cpp index 34ed254fb..54403e237 100644 --- a/cockatrice/src/game/player/menu/card_menu.cpp +++ b/cockatrice/src/game/player/menu/card_menu.cpp @@ -62,6 +62,9 @@ CardMenu::CardMenu(Player *_player, const CardItem *_card, bool _shortcutsActive aSelectColumn = new QAction(this); connect(aSelectColumn, &QAction::triggered, playerActions, &PlayerActions::actSelectColumn); + aReduceLifeByPower = new QAction(this); + connect(aReduceLifeByPower, &QAction::triggered, playerActions, &PlayerActions::actReduceLifeByPower); + aPlay = new QAction(this); connect(aPlay, &QAction::triggered, playerActions, &PlayerActions::actPlay); aHide = new QAction(this); @@ -153,6 +156,8 @@ void CardMenu::createTableMenu(bool canModifyCard) addSeparator(); addAction(aClone); addSeparator(); + addAction(aReduceLifeByPower); + addSeparator(); addAction(aSelectAll); addAction(aSelectRow); addRelatedCardView(); @@ -179,6 +184,8 @@ void CardMenu::createTableMenu(bool canModifyCard) addMenu(new PtMenu(player)); addAction(aSetAnnotation); addSeparator(); + addAction(aReduceLifeByPower); + addSeparator(); addAction(aSelectAll); addAction(aSelectRow); @@ -463,6 +470,7 @@ void CardMenu::retranslateUi() aUnattach->setText(tr("Unattac&h")); aDrawArrow->setText(tr("&Draw arrow...")); aSetAnnotation->setText(tr("&Set annotation...")); + aReduceLifeByPower->setText(tr("Reduce life by power")); mCardCounters->setTitle(tr("Ca&rd counters")); @@ -497,6 +505,7 @@ void CardMenu::setShortcutsActive() aUnattach->setShortcuts(shortcuts.getShortcut("Player/aUnattach")); aDrawArrow->setShortcuts(shortcuts.getShortcut("Player/aDrawArrow")); aSetAnnotation->setShortcuts(shortcuts.getShortcut("Player/aSetAnnotation")); + aReduceLifeByPower->setShortcuts(shortcuts.getShortcut("Player/aReduceLifeByPower")); aSelectAll->setShortcuts(shortcuts.getShortcut("Player/aSelectAll")); aSelectRow->setShortcuts(shortcuts.getShortcut("Player/aSelectRow")); diff --git a/cockatrice/src/game/player/menu/card_menu.h b/cockatrice/src/game/player/menu/card_menu.h index b7f2f8241..3cff238de 100644 --- a/cockatrice/src/game/player/menu/card_menu.h +++ b/cockatrice/src/game/player/menu/card_menu.h @@ -36,6 +36,7 @@ public: QAction *aFlip, *aPeek; QAction *aAttach, *aUnattach; QAction *aSetAnnotation; + QAction *aReduceLifeByPower; QList aAddCounter, aSetCounter, aRemoveCounter; diff --git a/cockatrice/src/game/player/player.cpp b/cockatrice/src/game/player/player.cpp index e95e2b2ef..8f5067eae 100644 --- a/cockatrice/src/game/player/player.cpp +++ b/cockatrice/src/game/player/player.cpp @@ -343,6 +343,16 @@ void Player::incrementAllCardCounters() } } +AbstractCounter *Player::getLifeCounter() const +{ + for (auto counter : counters.values()) { + if (counter->getName() == "life") { + return counter; + } + } + return nullptr; +} + ArrowItem *Player::addArrow(const ServerInfo_Arrow &arrow) { const QMap &playerList = game->getPlayerManager()->getPlayers(); diff --git a/cockatrice/src/game/player/player.h b/cockatrice/src/game/player/player.h index 9b8456aaf..1e7bc928d 100644 --- a/cockatrice/src/game/player/player.h +++ b/cockatrice/src/game/player/player.h @@ -199,6 +199,11 @@ public: return counters; } + /** + * Gets the counter that represents the life total. + */ + AbstractCounter *getLifeCounter() const; + ArrowItem *addArrow(const ServerInfo_Arrow &arrow); ArrowItem *addArrow(int arrowId, CardItem *startCard, ArrowTarget *targetItem, const QColor &color); void delArrow(int arrowId); diff --git a/cockatrice/src/game/player/player_actions.cpp b/cockatrice/src/game/player/player_actions.cpp index bce8af74b..24da37c82 100644 --- a/cockatrice/src/game/player/player_actions.cpp +++ b/cockatrice/src/game/player/player_actions.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -1378,6 +1379,32 @@ void PlayerActions::actFlowT() actIncPT(-1, 1); } +void PlayerActions::actReduceLifeByPower() +{ + // find life counter + auto lifeCounter = player->getLifeCounter(); + if (!lifeCounter) { + return; + } + + // calculate total power + auto cards = player->getGameScene()->selectedCards(); + int total = 0; + for (auto card : cards) { + QVariantList parsed = CardItem::parsePT(card->getPT()); + if (!parsed.isEmpty()) { + int power = parsed.first().toInt(); // toInt will default to 0 if it's not an int + total += qMax(power, 0); + } + } + + // send cmd + Command_IncCounter cmd; + cmd.set_counter_id(lifeCounter->getId()); + cmd.set_delta(-total); + sendGameCommand(prepareGameCommand(cmd)); +} + void AnnotationDialog::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Return && event->modifiers() & Qt::ControlModifier) { diff --git a/cockatrice/src/game/player/player_actions.h b/cockatrice/src/game/player/player_actions.h index 181b1b135..ba62f1c59 100644 --- a/cockatrice/src/game/player/player_actions.h +++ b/cockatrice/src/game/player/player_actions.h @@ -145,6 +145,9 @@ public slots: void actDecPT(); void actFlowP(); void actFlowT(); + + void actReduceLifeByPower(); + void actSetAnnotation(); void actReveal(QAction *action); void actRevealHand(int revealToPlayerId); diff --git a/cockatrice/src/game/zones/card_zone.cpp b/cockatrice/src/game/zones/card_zone.cpp index 0c189cd2b..cd7ae6a56 100644 --- a/cockatrice/src/game/zones/card_zone.cpp +++ b/cockatrice/src/game/zones/card_zone.cpp @@ -19,6 +19,7 @@ CardZone::CardZone(CardZoneLogic *_logic, QGraphicsItem *parent) void CardZone::onCardAdded(CardItem *addedCard) { addedCard->setParentItem(this); + addedCard->setVisible(true); addedCard->update(); } diff --git a/cockatrice/src/game/zones/card_zone.h b/cockatrice/src/game/zones/card_zone.h index 6fe8157e4..5df48f7fb 100644 --- a/cockatrice/src/game/zones/card_zone.h +++ b/cockatrice/src/game/zones/card_zone.h @@ -1,7 +1,7 @@ /** * @file card_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Base graphics item for zones that contain cards. */ #ifndef CARDZONE_H @@ -40,7 +40,10 @@ protected: } public slots: bool showContextMenu(const QPoint &screenPos); - void onCardAdded(CardItem *addedCard); + /// @brief Called when a card is added to this zone. Default: reparents card to this item. + /// Virtual so subclasses (e.g. SelectZone) can override parenting behavior — the Qt signal + /// connection in CardZone's constructor dispatches through the vtable. + virtual void onCardAdded(CardItem *addedCard); public: enum diff --git a/cockatrice/src/game/zones/hand_zone.cpp b/cockatrice/src/game/zones/hand_zone.cpp index 7badfcca4..506a26a80 100644 --- a/cockatrice/src/game/zones/hand_zone.cpp +++ b/cockatrice/src/game/zones/hand_zone.cpp @@ -27,6 +27,10 @@ void HandZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) { + if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) { + return; + } + QPoint point = dropPoint + scenePos().toPoint(); int x = -1; if (SettingsCache::instance().getHorizontalHand()) { @@ -34,9 +38,7 @@ void HandZone::handleDropEvent(const QList &dragItems, if (point.x() < static_cast(getLogic()->getCards().at(x))->scenePos().x()) break; } else { - for (x = 0; x < getLogic()->getCards().size(); x++) - if (point.y() < static_cast(getLogic()->getCards().at(x))->scenePos().y()) - break; + x = calcDropIndexFromY(dropPoint.y()); } Command_MoveCard cmd; @@ -58,7 +60,7 @@ QRectF HandZone::boundingRect() const if (SettingsCache::instance().getHorizontalHand()) return QRectF(0, 0, width, CardDimensions::HEIGHT_F + 10); else - return QRectF(0, 0, 100, zoneHeight); + return QRectF(0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight); } void HandZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) @@ -78,35 +80,31 @@ void HandZone::reorganizeCards() qreal totalWidth = leftJustified ? boundingRect().width() - (1 * xPadding) - 5 : boundingRect().width() - 2 * xPadding; - for (int i = 0; i < cardCount; i++) { - CardItem *c = getLogic()->getCards().at(i); - // If the total width of the cards is smaller than the available width, - // the cards do not need to overlap and are displayed in the center of the area. - if (cardWidth * cardCount > totalWidth) - c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5); - else { - qreal xPosition = - leftJustified ? xPadding + ((qreal)i) * cardWidth - : xPadding + ((qreal)i) * cardWidth + (totalWidth - cardCount * cardWidth) / 2; - c->setPos(xPosition, 5); + if (cardCount == 1) { + CardItem *c = getLogic()->getCards().at(0); + qreal xPosition = leftJustified ? xPadding : xPadding + (totalWidth - cardWidth) / 2; + c->setPos(xPosition, 5); + c->setRealZValue(0); + } else { + for (int i = 0; i < cardCount; i++) { + CardItem *c = getLogic()->getCards().at(i); + // If the total width of the cards is smaller than the available width, + // the cards do not need to overlap and are displayed in the center of the area. + if (cardWidth * cardCount > totalWidth) + c->setPos(xPadding + ((qreal)i) * (totalWidth - cardWidth) / (cardCount - 1), 5); + else { + qreal xPosition = leftJustified ? xPadding + ((qreal)i) * cardWidth + : xPadding + ((qreal)i) * cardWidth + + (totalWidth - cardCount * cardWidth) / 2; + c->setPos(xPosition, 5); + } + c->setRealZValue(i); } - c->setRealZValue(i); } } else { - qreal totalWidth = boundingRect().width(); - qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); - qreal xspace = 5; - qreal x1 = xspace; - qreal x2 = totalWidth - xspace - cardWidth; - - for (int i = 0; i < cardCount; i++) { - CardItem *card = getLogic()->getCards().at(i); - qreal x = (i % 2) ? x2 : x1; - qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(), - getLogic()->getCards().at(0)->boundingRect().height()); - card->setPos(x, y); - card->setRealZValue(i); - } + // No clip container: hand cards should always be visible to the player. + const auto params = buildStackParams(); + layoutCardsVertically(params); } } update(); diff --git a/cockatrice/src/game/zones/hand_zone.h b/cockatrice/src/game/zones/hand_zone.h index 25f4148bd..586302308 100644 --- a/cockatrice/src/game/zones/hand_zone.h +++ b/cockatrice/src/game/zones/hand_zone.h @@ -1,7 +1,7 @@ /** * @file hand_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Graphical zone for the player's hand, supporting horizontal and vertical layouts. */ #ifndef HANDZONE_H @@ -14,7 +14,8 @@ class HandZone : public SelectZone { Q_OBJECT private: - qreal width, zoneHeight; + qreal width = 0.0; + qreal zoneHeight; private slots: void updateBg(); public slots: diff --git a/cockatrice/src/game/zones/select_zone.cpp b/cockatrice/src/game/zones/select_zone.cpp index 9bf5f9faf..fd733e01f 100644 --- a/cockatrice/src/game/zones/select_zone.cpp +++ b/cockatrice/src/game/zones/select_zone.cpp @@ -4,38 +4,207 @@ #include "../board/card_item.h" #include "../game_scene.h" -#include +#include #include +#include -qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse) +static qreal stackingOffset(qreal cardHeight) { - qreal cardMinOverlap = cardHeight * SettingsCache::instance().getStackCardOverlapPercent() / 100; - qreal desiredHeight = cardHeight * cardCount - cardMinOverlap * (cardCount - 1); - qreal y; - if (desiredHeight > totalHeight) { - if (reverse) { - y = index / ((totalHeight - cardHeight) / (cardCount - 1)); + const qreal overlapPercent = SettingsCache::instance().getStackCardOverlapPercent(); + return cardHeight * (100.0 - overlapPercent) / 100.0; +} + +SelectZone::ZoneLayout SelectZone::computeZoneLayout(const StackLayoutParams ¶ms) +{ + if (params.cardCount <= 0) { + return {0.0, 0.0}; + } + qreal effectiveOffset = params.desiredOffset; + if (params.cardCount > 1) { + qreal fitOffset; + if (params.totalHeight < params.cardHeight && params.minOffset > 0.0) { + // Zone is shorter than a card (e.g. minimized). Compress offsets so + // every card has at least minOffset pixels of its top visible. + fitOffset = (params.totalHeight - params.minOffset) / (params.cardCount - 1); + effectiveOffset = qMax(0.0, qMin(params.desiredOffset, fitOffset)); } else { - y = index * (totalHeight - cardHeight) / (cardCount - 1); - } - } else { - qreal start = (totalHeight - desiredHeight) / 2; - if (reverse) { - if (index <= start) { - return 0; + qreal reservedForBottomCard; + if (params.allowBottomOverflow) { + // Allow the bottom card to partially overflow in tight zones, scaling the + // overflow allowance by sqrt(cardCount-1) so offsets decrease smoothly + // as cards are added rather than dropping by 1/(n-1) each time. + // The 0.75 ratio was tuned experimentally to balance card visibility vs. overflow. + constexpr qreal bottomCardZoneRatio = 0.75; + const qreal adjustedRatio = bottomCardZoneRatio / qSqrt(static_cast(params.cardCount - 1)); + reservedForBottomCard = qMin(params.cardHeight, params.totalHeight * adjustedRatio); + } else { + // No overflow: reserve full card height for the bottom card + reservedForBottomCard = params.cardHeight; } - y = (index - start) / (cardHeight - cardMinOverlap); - } else { - y = index * (cardHeight - cardMinOverlap) + start; + fitOffset = (params.totalHeight - reservedForBottomCard) / (params.cardCount - 1); + effectiveOffset = qMax(params.minOffset, qMin(params.desiredOffset, fitOffset)); } } - return y; + qreal stackHeight = (params.cardCount - 1) * effectiveOffset + params.cardHeight; + qreal start = (stackHeight <= params.totalHeight) ? (params.totalHeight - stackHeight) / 2.0 : 0.0; + return {effectiveOffset, start}; +} + +SelectZone *SelectZone::findOwningSelectZone(const QGraphicsItem *card) +{ + QGraphicsItem *parent = card ? card->parentItem() : nullptr; + if (!parent) { + return nullptr; + } + // Card may be direct child of zone (escaped for hover) or child of clip container. + if (auto *zone = dynamic_cast(parent)) { + return zone; + } + if (auto *zone = dynamic_cast(parent->parentItem())) { + return zone; + } + return nullptr; +} + +SelectZone::StackLayoutParams SelectZone::buildStackParams(qreal minOffset) const +{ + const auto &cards = getLogic()->getCards(); + if (cards.isEmpty()) + return {0, boundingRect().height(), 0.0, 0.0, minOffset}; + const auto cardCount = static_cast(cards.size()); + const qreal cardHeight = cards.at(0)->boundingRect().height(); + const qreal offset = stackingOffset(cardHeight); + return {cardCount, boundingRect().height(), cardHeight, offset, minOffset}; +} + +int SelectZone::calcDropIndexFromY(qreal dropY, qreal minOffset) const +{ + const auto &cards = getLogic()->getCards(); + if (cards.isEmpty()) { + return 0; + } + const auto params = buildStackParams(minOffset); + auto [effectiveOffset, start] = computeZoneLayout(params); + if (effectiveOffset <= 0.0) { + return 0; + } + return qBound(0, qRound((dropY - start) / effectiveOffset), params.cardCount - 1); +} + +void SelectZone::restoreStaleEscapedCards() +{ + if (!cardClipContainer) + return; + for (auto *card : getLogic()->getCards()) { + // A card parented to the zone (instead of the clip container) should + // only occur while it is actively hovered. If hover cleanup was + // missed, reparent it back so clipping resumes. + if (card && card->parentItem() == this && !card->getIsHovered()) { + card->setParentItem(cardClipContainer); + } + } +} + +void SelectZone::layoutCardsVertically(const StackLayoutParams ¶ms) +{ + const auto &cards = getLogic()->getCards(); + if (cards.isEmpty() || params.cardCount <= 0) + return; + if (params.cardCount > cards.size()) + return; + + constexpr qreal xspace = 5; + const qreal cardWidth = cards.at(0)->boundingRect().width(); + const qreal totalWidth = boundingRect().width(); + const qreal x1 = xspace; + const qreal x2 = totalWidth - xspace - cardWidth; + const qreal xCentered = (totalWidth - cardWidth) / 2.0; + + auto [effectiveOffset, start] = computeZoneLayout(params); + for (int i = 0; i < params.cardCount; i++) { + CardItem *card = cards.at(i); + qreal y = start + i * effectiveOffset; + // Center single card; alternate left/right for multiple cards + qreal x = (params.cardCount == 1) ? xCentered : ((i % 2) ? x2 : x1); + card->setPos(x, y); + card->setRealZValue(i); + } } SelectZone::SelectZone(CardZoneLogic *_logic, QGraphicsItem *parent) : CardZone(_logic, parent) { } +SelectZone::~SelectZone() +{ + if (cardClipContainer) { + // Reparent any hover-escaped cards back to the clip container so Qt's + // parent-child tree is consistent for destruction. setParentItem() does + // not invalidate getLogic()->getCards() (it modifies the graphics tree, + // not the zone's logical card list). + for (auto *card : getLogic()->getCards()) { + if (card && card->parentItem() == this) { + card->setParentItem(cardClipContainer); + } + } + } +} + +void SelectZone::onCardAdded(CardItem *addedCard) +{ + if (cardClipContainer && addedCard) { + addedCard->setParentItem(cardClipContainer); + addedCard->setVisible(true); + addedCard->update(); + } else { + CardZone::onCardAdded(addedCard); + } +} + +void SelectZone::setupClipContainer(std::optional zValue) +{ + if (cardClipContainer) + return; + + setFlag(QGraphicsItem::ItemClipsChildrenToShape, false); + + cardClipContainer = new QGraphicsRectItem(this); // Owned by Qt parent-child tree; deleted with this zone. + cardClipContainer->setFlag(QGraphicsItem::ItemClipsChildrenToShape, true); + cardClipContainer->setPen(Qt::NoPen); + cardClipContainer->setBrush(Qt::NoBrush); + cardClipContainer->setRect(boundingRect()); + if (zValue.has_value()) { + cardClipContainer->setZValue(*zValue); + } +} + +void SelectZone::escapeClipForHover(QGraphicsItem *card) +{ + // Reparent from clip container to zone so the hover-scaled card is visible + // beyond clip bounds. Coordinates are identical because the clip container + // is at (0,0) with no transform relative to this zone. + if (cardClipContainer && card && card->parentItem() == cardClipContainer) { + card->setParentItem(this); + cardClipContainer->update(); + } +} + +void SelectZone::restoreClipAfterHover(QGraphicsItem *card) +{ + // Restore card to clip container. If card's parent is not this zone, + // a zone transition already reparented it via onCardAdded — skip. + if (cardClipContainer && card && card->parentItem() == this) { + card->setParentItem(cardClipContainer); + } +} + +void SelectZone::updateClipRect() +{ + if (cardClipContainer) { + cardClipContainer->setRect(boundingRect()); + } +} + void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (event->buttons().testFlag(Qt::LeftButton)) { @@ -56,7 +225,7 @@ void SelectZone::mouseMoveEvent(QGraphicsSceneMouseEvent *event) continue; } - bool inRect = selectionRect.intersects(card->mapRectToParent(card->boundingRect())); + bool inRect = selectionRect.intersects(card->mapRectToItem(this, card->boundingRect())); if (inRect && !cardsInSelectionRect.contains(card)) { // selection has just expanded to cover the card cardsInSelectionRect.insert(card); diff --git a/cockatrice/src/game/zones/select_zone.h b/cockatrice/src/game/zones/select_zone.h index d6fd3e10e..1da5c1988 100644 --- a/cockatrice/src/game/zones/select_zone.h +++ b/cockatrice/src/game/zones/select_zone.h @@ -1,7 +1,7 @@ /** * @file select_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Base class for zones where cards are laid out and individually interactable. */ #ifndef SELECTZONE_H @@ -10,6 +10,9 @@ #include "card_zone.h" #include +#include + +class QGraphicsRectItem; /** * A CardZone where the cards are laid out, with each card directly interactable by clicking. @@ -17,19 +20,113 @@ class SelectZone : public CardZone { Q_OBJECT +public: + /// Finds the SelectZone that owns a card, regardless of whether the card is parented + /// to the zone directly or to its clip container. Returns nullptr if not in a SelectZone. + static SelectZone *findOwningSelectZone(const QGraphicsItem *card); + + SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr); + ~SelectZone() override; + void onCardAdded(CardItem *addedCard) override; + + /// @brief Temporarily reparents a card from the clip container to this zone so hover scaling is visible beyond clip + /// bounds. Safe no-op if no clip container exists. Coordinates are preserved (clip container is at (0,0) with no + /// transform). + void escapeClipForHover(QGraphicsItem *card); + /// @brief Restores a hover-escaped card back to the clip container. Guards against zone transitions that already + /// reparented the card. + void restoreClipAfterHover(QGraphicsItem *card); + private: QPointF selectionOrigin; QSet cardsInSelectionRect; + /// Invisible clipping parent for cards; owned by Qt parent-child tree (parented to this zone). + /// Created by setupClipContainer(); null when no clip container is active. + QGraphicsRectItem *cardClipContainer = nullptr; protected: + // -- Layout computation -- + + /// Parameters describing a vertical card stack's geometry. + struct StackLayoutParams + { + int cardCount; ///< Number of cards in the stack + qreal totalHeight; ///< Available height for the stack (zone height) + qreal cardHeight; ///< Height of a single card + qreal desiredOffset; ///< Preferred vertical offset between card tops + qreal minOffset = 0.0; ///< Minimum offset to preserve (0 allows full compression) + /// When false (default), reserves full cardHeight for the bottom card, ensuring + /// all cards remain within zone bounds. When true, allows the bottom card to + /// partially overflow using sqrt-scaled allowance. Use with setupClipContainer() + /// for zones too short to fit a full card. + bool allowBottomOverflow = false; + }; + + /// Result of computing a vertical stack layout. + struct ZoneLayout + { + qreal effectiveOffset; ///< Actual offset between card tops (may be compressed) + qreal start; ///< Y coordinate of the first card's top edge + }; + + /// Minimum visible pixels of each card's top edge when stacking compresses offsets in tight zones. + static constexpr qreal MIN_CARD_VISIBLE = 10.0; + + /** + * @brief Computes layout for a vertical card stack (effective offset and start position). + * + * Three regimes: + * 1. Minimized zone (totalHeight < card height with minOffset > 0): offsets compress + * so each card retains at least minOffset visible pixels of its top edge. + * 2. Normal zone with allowBottomOverflow=false (default): the bottom card is + * guaranteed to fit within the zone boundary. Offsets compress as needed. + * 3. Normal zone with allowBottomOverflow=true: the bottom card may partially + * overflow. The overflow allowance is scaled by sqrt(cardCount-1) so that + * adding one card shifts existing cards smoothly. + * + * When the stack fits with room to spare, it is centered vertically. + */ + static ZoneLayout computeZoneLayout(const StackLayoutParams ¶ms); + + /// Builds StackLayoutParams from the current card list and zone geometry. + StackLayoutParams buildStackParams(qreal minOffset = 0.0) const; + + /// Computes the card index at a given y-coordinate within the zone's vertical layout. + /// Returns 0 if the zone has no cards or the offset is zero. + int calcDropIndexFromY(qreal dropY, qreal minOffset = 0.0) const; + + /** + * @brief Positions cards vertically with alternating left/right x-offsets. + * + * Cards alternate between left and right margins (5px padding from zone edges): + * even-indexed cards at left, odd-indexed at right. + * Cards are assigned ascending z-values. + * + * @param params Stack layout geometry parameters (use allowBottomOverflow to control overflow) + */ + void layoutCardsVertically(const StackLayoutParams ¶ms); + + // -- Clip container -- + // The clip container mechanism is available for future zones that need visual clipping + // (e.g., zones too short to fit a full card). To enable: call setupClipContainer() in the + // zone's constructor, and set allowBottomOverflow=true in layout params. + + /// Restores any cards that were hover-escaped but whose hover state was not properly cleaned up. + /// Call at the start of reorganizeCards() in zones that use a clip container. + void restoreStaleEscapedCards(); + + /// Creates a clip container child item that clips card overflow to zone bounds. + /// Cards entering this zone are reparented to this container by the onCardAdded override. + /// Disables zone-level child clipping; clipping is delegated to the container. + /// @param zValue Optional z-value for the clip container (e.g. ZValues::CARD_BASE) + void setupClipContainer(std::optional zValue = std::nullopt); + + /// Updates the clip container rect to match this zone's current boundingRect(). + void updateClipRect(); + void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; - -public: - SelectZone(CardZoneLogic *logic, QGraphicsItem *parent = nullptr); }; -qreal divideCardSpaceInZone(qreal index, int cardCount, qreal totalHeight, qreal cardHeight, bool reverse = false); - #endif diff --git a/cockatrice/src/game/zones/stack_zone.cpp b/cockatrice/src/game/zones/stack_zone.cpp index a9d649a4a..c0da0bfd2 100644 --- a/cockatrice/src/game/zones/stack_zone.cpp +++ b/cockatrice/src/game/zones/stack_zone.cpp @@ -1,10 +1,9 @@ #include "stack_zone.h" -#include "../../client/settings/cache_settings.h" #include "../../interface/theme_manager.h" -#include "../board/arrow_item.h" #include "../board/card_drag_item.h" #include "../board/card_item.h" +#include "../card_dimensions.h" #include "../player/player.h" #include "../player/player_actions.h" #include "logic/stack_zone_logic.h" @@ -27,7 +26,7 @@ void StackZone::updateBg() QRectF StackZone::boundingRect() const { - return {0, 0, 100, zoneHeight}; + return {0, 0, CardDimensions::WIDTH_F * 1.5, zoneHeight}; } void StackZone::paint(QPainter *painter, const QStyleOptionGraphicsItem * /*option*/, QWidget * /*widget*/) @@ -40,7 +39,15 @@ void StackZone::handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) { - if (startZone == nullptr || startZone->getPlayer() == nullptr) { + if (startZone == nullptr || startZone->getPlayer() == nullptr || dragItems.isEmpty()) { + return; + } + + int index = calcDropIndexFromY(dropPoint.y(), MIN_CARD_VISIBLE); + + // Same-zone no-op: don't move a card onto itself + const auto &cards = getLogic()->getCards(); + if (!cards.isEmpty() && startZone == getLogic() && cards.at(index)->getId() == dragItems.at(0)->getId()) { return; } @@ -49,36 +56,12 @@ void StackZone::handleDropEvent(const QList &dragItems, cmd.set_start_zone(startZone->getName().toStdString()); cmd.set_target_player_id(getLogic()->getPlayer()->getPlayerInfo()->getId()); cmd.set_target_zone(getLogic()->getName().toStdString()); - - int index = 0; - - if (!getLogic()->getCards().isEmpty()) { - const auto cardCount = static_cast(getLogic()->getCards().size()); - const auto &card = getLogic()->getCards().at(0); - - index = qRound(divideCardSpaceInZone(dropPoint.y(), cardCount, boundingRect().height(), - card->boundingRect().height(), true)); - - // divideCardSpaceInZone is not guaranteed to return a valid index - // currently, so clamp it to avoid crashes. - index = qBound(0, index, cardCount - 1); - - if (startZone == getLogic()) { - const auto &dragItem = dragItems.at(0); - const auto &card = getLogic()->getCards().at(index); - - if (card->getId() == dragItem->getId()) { - return; - } - } - } - cmd.set_x(index); cmd.set_y(0); - for (CardDragItem *item : dragItems) { + for (const CardDragItem *item : dragItems) { if (item) { - auto cardToMove = cmd.mutable_cards_to_move()->add_card(); + auto *cardToMove = cmd.mutable_cards_to_move()->add_card(); cardToMove->set_card_id(item->getId()); if (item->isForceFaceDown()) { cardToMove->set_face_down(true); @@ -89,24 +72,22 @@ void StackZone::handleDropEvent(const QList &dragItems, getLogic()->getPlayer()->getPlayerActions()->sendGameCommand(cmd); } +void StackZone::setHeight(qreal newHeight) +{ + if (qFuzzyCompare(1.0 + zoneHeight, 1.0 + newHeight)) { + return; + } + prepareGeometryChange(); + zoneHeight = newHeight; + reorganizeCards(); + update(); +} + void StackZone::reorganizeCards() { if (!getLogic()->getCards().isEmpty()) { - const auto cardCount = static_cast(getLogic()->getCards().size()); - qreal totalWidth = boundingRect().width(); - qreal cardWidth = getLogic()->getCards().at(0)->boundingRect().width(); - qreal xspace = 5; - qreal x1 = xspace; - qreal x2 = totalWidth - xspace - cardWidth; - - for (int i = 0; i < cardCount; i++) { - CardItem *card = getLogic()->getCards().at(i); - qreal x = (i % 2) ? x2 : x1; - qreal y = divideCardSpaceInZone(i, cardCount, boundingRect().height(), - getLogic()->getCards().at(0)->boundingRect().height()); - card->setPos(x, y); - card->setRealZValue(i); - } + const auto params = buildStackParams(MIN_CARD_VISIBLE); + layoutCardsVertically(params); } update(); } diff --git a/cockatrice/src/game/zones/stack_zone.h b/cockatrice/src/game/zones/stack_zone.h index 7c98f5128..8f5bed09d 100644 --- a/cockatrice/src/game/zones/stack_zone.h +++ b/cockatrice/src/game/zones/stack_zone.h @@ -1,7 +1,7 @@ /** * @file stack_zone.h * @ingroup GameGraphicsZones - * @brief TODO: Document this. + * @brief Graphical zone for the stack, displaying cards in a vertical pile. */ #ifndef STACKZONE_H @@ -20,6 +20,8 @@ private slots: public: StackZone(StackZoneLogic *_logic, int _zoneHeight, QGraphicsItem *parent); + /// @brief Resizes the stack zone height, e.g. when sharing vertical space with the command zone. + void setHeight(qreal newHeight); void handleDropEvent(const QList &dragItems, CardZoneLogic *startZone, const QPoint &dropPoint) override; QRectF boundingRect() const override; diff --git a/cockatrice/src/interface/deck_loader/deck_loader.cpp b/cockatrice/src/interface/deck_loader/deck_loader.cpp index d71dca24a..e616c5eb5 100644 --- a/cockatrice/src/interface/deck_loader/deck_loader.cpp +++ b/cockatrice/src/interface/deck_loader/deck_loader.cpp @@ -181,6 +181,11 @@ bool DeckLoader::saveToNewFile(LoadedDeck &deck, const QString &fileName, DeckFi */ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck) { + // text format doesn't support lastLoadedTimestamp, so there's no point in proceeding + if (deck.lastLoadInfo.fileFormat != DeckFileFormat::Cockatrice) { + return false; + } + QString fileName = deck.lastLoadInfo.fileName; QFileInfo fileInfo(fileName); @@ -201,15 +206,8 @@ bool DeckLoader::updateLastLoadedTimestamp(LoadedDeck &deck) bool result = false; // Perform file modifications - switch (deck.lastLoadInfo.fileFormat) { - case DeckFileFormat::PlainText: - result = deck.deckList.saveToFile_Plain(&file); - break; - case DeckFileFormat::Cockatrice: - deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString()); - result = deck.deckList.saveToFile_Native(&file); - break; - } + deck.deckList.setLastLoadedTimestamp(QDateTime::currentDateTime().toString()); + result = deck.deckList.saveToFile_Native(&file); file.close(); // Close the file to ensure changes are flushed @@ -429,8 +427,7 @@ void DeckLoader::saveToStream_DeckZoneCards(QTextStream &out, } if (addSetNameAndNumber) { if (!card->getCardSetShortName().isNull() && !card->getCardSetShortName().isEmpty()) { - out << " " - << "(" << card->getCardSetShortName() << ")"; + out << " " << "(" << card->getCardSetShortName() << ")"; } if (!card->getCardCollectorNumber().isNull()) { out << " " << card->getCardCollectorNumber(); 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/deck_editor/deck_editor_card_database_dock_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp index bacebe385..f2a2ab4ea 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.cpp @@ -58,7 +58,3 @@ void DeckEditorCardDatabaseDockWidget::clearAllDatabaseFilters() { databaseDisplayWidget->clearAllDatabaseFilters(); } -void DeckEditorCardDatabaseDockWidget::highlightAllSearchEdit() -{ - databaseDisplayWidget->searchEdit->setSelection(0, databaseDisplayWidget->searchEdit->text().length()); -} diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h index 6ad442075..bff9ee36f 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_card_database_dock_widget.h @@ -23,7 +23,6 @@ public: public slots: void retranslateUi(); void clearAllDatabaseFilters(); - void highlightAllSearchEdit(); private: void createDatabaseDisplayDock(AbstractTabDeckEditor *deckEditor); diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp index c625ff1d9..20eef180a 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.cpp @@ -147,11 +147,13 @@ void DeckEditorDatabaseDisplayWidget::updateCard(const QModelIndex ¤t, con void DeckEditorDatabaseDisplayWidget::actAddCardToMainDeck() { + highlightAllSearchEdit(); emit addCardToMainDeck(currentCard()); } void DeckEditorDatabaseDisplayWidget::actAddCardToSideboard() { + highlightAllSearchEdit(); emit addCardToSideboard(currentCard()); } @@ -240,4 +242,9 @@ void DeckEditorDatabaseDisplayWidget::retranslateUi() { aAddCard->setText(tr("Add card to &maindeck")); aAddCardToSideboard->setText(tr("Add card to &sideboard")); +} + +void DeckEditorDatabaseDisplayWidget::highlightAllSearchEdit() +{ + searchEdit->setSelection(0, searchEdit->text().length()); } \ No newline at end of file diff --git a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h index 16ae6e255..0f62998ef 100644 --- a/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h +++ b/cockatrice/src/interface/widgets/deck_editor/deck_editor_database_display_widget.h @@ -25,7 +25,6 @@ class DeckEditorDatabaseDisplayWidget : public QWidget public: explicit DeckEditorDatabaseDisplayWidget(QWidget *parent, AbstractTabDeckEditor *deckEditor); AbstractTabDeckEditor *deckEditor; - SearchLineEdit *searchEdit; CardDatabaseModel *databaseModel; CardDatabaseDisplayModel *databaseDisplayModel; @@ -58,10 +57,13 @@ private: KeySignals searchKeySignals; QTreeView *databaseView; QHBoxLayout *searchLayout; + SearchLineEdit *searchEdit; QAction *aAddCard, *aAddCardToSideboard; QVBoxLayout *centralFrame; QWidget *centralWidget; + void highlightAllSearchEdit(); + private slots: void retranslateUi(); void saveDbHeaderState(); 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/tabs/abstract_tab_deck_editor.cpp b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp index afc834e10..a739ac83b 100644 --- a/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp +++ b/cockatrice/src/interface/widgets/tabs/abstract_tab_deck_editor.cpp @@ -137,8 +137,6 @@ void AbstractTabDeckEditor::onDeckModified() void AbstractTabDeckEditor::addCardHelper(const ExactCard &card, const QString &zoneName) { deckStateManager->addCard(card, zoneName); - - cardDatabaseDockWidget->highlightAllSearchEdit(); } /** diff --git a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h index 66f38fb3d..4fe4d3e8f 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h +++ b/cockatrice/src/interface/widgets/tabs/tab_visual_database_display.h @@ -23,8 +23,8 @@ public: void retranslateUi() override; [[nodiscard]] QString getTabText() const override { - return visualDatabaseDisplayWidget->displayModeButton->isChecked() ? tr("Database Display") - : tr("Visual Database Display"); + return visualDatabaseDisplayWidget->isVisualDisplayMode() ? tr("Visual Database Display") + : tr("Database Display"); } }; 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 3cc1bf23b..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); @@ -101,7 +94,7 @@ void VisualDatabaseDisplayFilterToolbarWidget::initialize() filterLayout->setAlignment(Qt::AlignLeft); // create settings widgets - auto filterModel = visualDatabaseDisplay->filterModel; + auto filterModel = visualDatabaseDisplay->getFilterModel(); saveLoadWidget = new VisualDatabaseDisplayFilterSaveLoadWidget(this, filterModel); nameFilterWidget = @@ -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 b9f8dab51..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"); @@ -99,6 +97,11 @@ VisualDatabaseDisplayWidget::VisualDatabaseDisplayWidget(QWidget *parent, &DeckEditorDatabaseDisplayWidget::copyDatabaseCellContents); connect(help, &QAction::triggered, this, [this] { createSearchSyntaxHelpWindow(searchEdit); }); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToMainDeck, this, + &VisualDatabaseDisplayWidget::highlightAllSearchEdit); + connect(databaseDisplayWidget, &DeckEditorDatabaseDisplayWidget::addCardToSideboard, this, + &VisualDatabaseDisplayWidget::highlightAllSearchEdit); + databaseView = databaseDisplayWidget->getDatabaseView(); databaseView->setFocusProxy(searchEdit); databaseView->setItemDelegate(nullptr); @@ -147,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); @@ -181,6 +184,11 @@ void VisualDatabaseDisplayWidget::retranslateUi() clearFilterWidget->setToolTip(tr("Clear all filters")); } +void VisualDatabaseDisplayWidget::highlightAllSearchEdit() +{ + searchEdit->setSelection(0, searchEdit->text().length()); +} + void VisualDatabaseDisplayWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); @@ -237,6 +245,11 @@ void VisualDatabaseDisplayWidget::updateSearch(const QString &search) const QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows); } +bool VisualDatabaseDisplayWidget::isVisualDisplayMode() const +{ + return !displayModeButton->isChecked(); +} + void VisualDatabaseDisplayWidget::onSearchModelChanged() { if (flowWidget->isVisible()) { 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 3aa8d7f8e..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 @@ -62,12 +62,15 @@ public: return databaseView; } - QWidget *searchContainer; - QHBoxLayout *searchLayout; - SearchLineEdit *searchEdit; - QPushButton *displayModeButton; - FilterTreeModel *filterModel; - VisualDatabaseDisplayColorFilterWidget *colorFilterWidget; + FilterTreeModel *getFilterModel() + { + return filterModel; + } + + /** + * @return False if the widget is in database display mode and true if it's in visual display mode + */ + bool isVisualDisplayMode() const; public slots: void onSearchModelChanged(); @@ -88,6 +91,12 @@ protected slots: void onDisplayModeChanged(bool checked); private: + FlowWidget *searchContainer; + SearchLineEdit *searchEdit; + QPushButton *displayModeButton; + FilterTreeModel *filterModel; + VisualDatabaseDisplayColorFilterWidget *colorFilterWidget; + QLabel *databaseLoadIndicator; QToolButton *clearFilterWidget; @@ -112,6 +121,8 @@ private: int currentPage = 0; // Current page index int cardsPerPage = 100; // Number of cards per page + void highlightAllSearchEdit(); + protected: void resizeEvent(QResizeEvent *event) override; }; 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;