float the selection panel over the game world instead of a side column

This commit is contained in:
2026-08-06 22:06:42 +02:00
parent 3b37b0ecf8
commit 603f8063e9
14 changed files with 320 additions and 129 deletions

View File

@@ -158,7 +158,7 @@ BlueprintSelectionDialog::BlueprintSelectionDialog(BlueprintLibrary* library,
// Frameless: the dialog draws its own header row, so an OS title bar would only
// repeat it (REQ-UI-BLUEPRINT-DIALOG). Square corners rather than rounded ones --
// rounding a top-level window needs a translucent background, which is unreliable
// on Windows. The border matches the build bar and the side panel.
// on Windows. The border matches the build bar and the selection panel.
setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint);
setAttribute(Qt::WA_StyledBackground, true);
setStyleSheet(QStringLiteral(

View File

@@ -192,7 +192,7 @@ BuildButtonBar::BuildButtonBar(Simulation* sim, const GameConfig* config,
// The bar floats over the rendered world rather than sitting in a panel, so it
// brings its own opaque background to stay legible over any world content
// (REQ-UI-BUILD-BAR). Palette colors keep it consistent with the buttons it holds
// and with the side panels; this is widget chrome, not world rendering, so it is
// and with the selection panel; this is widget chrome, not world rendering, so it is
// deliberately not a visuals.toml color.
setAttribute(Qt::WA_StyledBackground, true);
setStyleSheet(QStringLiteral(
@@ -310,6 +310,11 @@ void BuildButtonBar::anchorTo(const QRect& worldViewRect)
recenter();
}
int BuildButtonBar::getStripHeightPx() const
{
return height() + kBottomMarginPx;
}
void BuildButtonBar::clearActiveButton()
{
if (m_activeIndex)

View File

@@ -51,6 +51,11 @@ public:
// re-center later driven by an unlock needs no second call from the owner.
void anchorTo(const QRect& worldViewRect);
// Height of the strip the bar occupies along the bottom of the world view: its own
// height plus the margin below it. The selection panel keeps out of this strip, and
// the bar never moves for the panel in return (REQ-UI-BUILD-BAR).
int getStripHeightPx() const;
void clearActiveButton();
private:

View File

@@ -11,7 +11,7 @@ SET(HDRS
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.h
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.h
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.h
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.h
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.h
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.h
@@ -37,7 +37,7 @@ SET(SRCS
${CMAKE_CURRENT_SOURCE_DIR}/WorldRenderer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/HeaderBar.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BuildButtonBar.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SelectedBuildingPanel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/SelectionPanel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/FieldSelectionPanel.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintLibrary.cpp
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSelectionDialog.cpp

View File

@@ -33,7 +33,7 @@ FieldSelectionPanel::FieldSelectionPanel(Simulation* sim,
, m_sim(sim)
, m_config(config)
{
// Zero margins and the same spacing as the enclosing SelectedBuildingPanel layout, so
// Zero margins and the same spacing as the enclosing SelectionPanel layout, so
// nesting the field widgets in this panel leaves their geometry unchanged.
m_layout = new QVBoxLayout(this);
m_layout->setContentsMargins(0, 0, 0, 0);

View File

@@ -23,8 +23,8 @@ class QVBoxLayout;
// summary (REQ-UI-SELECTION-CATEGORIES, REQ-UI-FIELD-MULTI-SELECTION, REQ-UI-DEBRIS-PANEL).
//
// The panel owns its own selection state and its own widgets, and nothing else. Which of
// the two selection categories owns the side panel is arbitrated by the parent
// SelectedBuildingPanel: it feeds this panel through setSelectedEntities() /
// the two selection categories owns the selection panel is arbitrated by the parent
// SelectionPanel: it feeds this panel through setSelectedEntities() /
// setSelectedDebris() / clearSelection() and asks it via hasSelection(). This panel hides
// itself whenever its selection is empty, so an inactive field category takes no space.
class FieldSelectionPanel : public QWidget,
@@ -47,7 +47,7 @@ public:
// Drops the whole field selection — used when the building category takes over.
void clearSelection();
// True while the field category has anything selected, i.e. while this panel owns
// the side panel's content.
// the selection panel's content.
bool hasSelection() const;
private:

View File

@@ -678,7 +678,7 @@ void GameWorldView::transferConfigTo(BuildingId id, const BlueprintBuilding& sou
if (source.type == BuildingType::Splitter)
{
// Operational splitters are configured by tile, sites by BuildingId (mirrors
// SelectedBuildingPanel::onSplitterFilterChanged). Locked item types are dropped
// SelectionPanel::onSplitterFilterChanged). Locked item types are dropped
// per REQ-LOCK-UI-BLUEPRINT.
const std::vector<ItemType> filterA = filterUnlockedItems(source.splitterFilterA, *m_sim);
const std::vector<ItemType> filterB = filterUnlockedItems(source.splitterFilterB, *m_sim);
@@ -1582,7 +1582,7 @@ void GameWorldView::handleEvent(std::shared_ptr<const DebugDrawToggleRequestedEv
void GameWorldView::handleEvent(std::shared_ptr<const CommandRequestedEvent> event)
{
// Other widgets (MainWindow, SelectedBuildingPanel) request commands via this
// Other widgets (MainWindow, SelectionPanel) request commands via this
// event; GameWorldView owns the CommandManager and enqueues them.
if (event->command && event->command->kind == CommandKind::Reset)
{

View File

@@ -28,7 +28,7 @@
#include "RecipeSelectionDialog.h"
#include "SchematicChoiceDialog.h"
#include "HeaderBar.h"
#include "SelectedBuildingPanel.h"
#include "SelectionPanel.h"
#include "ShipLayoutBlueprintSerializer.h"
#include "ShipLayoutDialog.h"
#include "ItemIconCache.h"
@@ -66,7 +66,7 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
const std::string iconDir = QDir::cleanPath(
QString::fromStdString(m_configDir) + "/../icons/buildings").toStdString();
// Floats over the game world rather than living in the side panel column
// Floats over the game world at its bottom center, sized to its buttons
// (REQ-UI-BUILD-BAR). Creation order is the stacking order for siblings, so
// building it after the world view puts it above the world and its vignettes,
// and before the dim overlay keeps modals dimming it too (REQ-UI-MODAL-DIM).
@@ -80,23 +80,12 @@ MainWindow::MainWindow(Simulation* sim, const std::string& configDir,
// world view because loading blueprints.toml may put a message box on screen.
m_blueprintLibrary = std::make_unique<BlueprintLibrary>(sim, &sim->getConfig(), this);
m_sidePanel = new QWidget(this);
QVBoxLayout* sideLayout = new QVBoxLayout(m_sidePanel);
sideLayout->setContentsMargins(1, 1, 1, 1);
sideLayout->setSpacing(1);
// The selected building panel is the column's only panel and fills its height
// (REQ-UI-PANEL-COLUMN).
m_selectedBuildingPanel = new SelectedBuildingPanel(sim, &sim->getConfig(), m_sidePanel);
sideLayout->addWidget(m_selectedBuildingPanel, 1);
// Draw a thin border around the side panel section. The class scoped selector keeps
// the border on the panel itself rather than cascading onto its child widgets;
// WA_StyledBackground lets the plain QWidget subclass honor the stylesheet box
// (border/background).
m_selectedBuildingPanel->setAttribute(Qt::WA_StyledBackground, true);
m_sidePanel->setStyleSheet(QStringLiteral(
"SelectedBuildingPanel { border: 1px solid palette(mid); }"));
// Floats over the game world at its right edge rather than occupying a column of
// its own, and hides itself while nothing is selected (REQ-UI-SELECTION-PANEL). Like
// the build button bar it is a sibling of the world view built after it, which is
// what puts it above the world and its vignettes and below the dim overlay. It
// brings its own chrome; its geometry comes from layoutPanels().
m_selectionPanel = new SelectionPanel(sim, &sim->getConfig(), this);
// Created last so it stacks above the other children; covers the whole window and
// dims the game behind modal dialogs/menus (REQ-UI-MODAL-DIM).
@@ -166,15 +155,20 @@ void MainWindow::layoutPanels()
const int totalH = height();
const int headerH = m_headerBar->sizeHint().height();
if (headerH <= 0) { return; }
const int mainW = totalW * 75 / 100;
const int sideW = totalW - mainW;
m_headerBar->setGeometry(0, 0, mainW, headerH);
m_gameWorldView->setGeometry(0, headerH, mainW, totalH - headerH);
m_sidePanel->setGeometry(mainW, 0, sideW, totalH);
// Header bar and game world view span the full window width; the two floating
// widgets below are the only things over the world (REQ-UI-HEADER,
// REQ-UI-WORLD-SIZE).
const QRect worldRect(0, headerH, totalW, totalH - headerH);
m_headerBar->setGeometry(0, 0, totalW, headerH);
m_gameWorldView->setGeometry(worldRect);
// Sizes itself to its buttons and centers along the bottom of the world view
// (REQ-UI-BUILD-BAR).
m_buildButtonBar->anchorTo(QRect(0, headerH, mainW, totalH - headerH));
m_buildButtonBar->anchorTo(worldRect);
// The panel confines itself to what the bar leaves free, so the bar never has to
// move for it (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
m_selectionPanel->anchorTo(
worldRect.adjusted(0, 0, 0, -m_buildButtonBar->getStripHeightPx()));
m_dimOverlay->setGeometry(0, 0, totalW, totalH);
}

View File

@@ -28,7 +28,7 @@ struct ParsedReplay;
class Simulation;
class GameWorldView;
class HeaderBar;
class SelectedBuildingPanel;
class SelectionPanel;
class BuildButtonBar;
class BlueprintLibrary;
class ItemIconCache;
@@ -94,12 +94,11 @@ private:
std::unique_ptr<ItemIconCache> m_itemIcons;
GameWorldView* m_gameWorldView;
HeaderBar* m_headerBar;
SelectedBuildingPanel* m_selectedBuildingPanel;
SelectionPanel* m_selectionPanel;
BuildButtonBar* m_buildButtonBar;
// The saved blueprints themselves; they have no widget of their own any more and
// are reached through the two modal dialogs (REQ-UI-BLUEPRINT-DIALOG).
std::unique_ptr<BlueprintLibrary> m_blueprintLibrary;
QWidget* m_sidePanel;
ModalDimOverlay* m_dimOverlay = nullptr;
std::vector<ShipLayoutBlueprint> m_layoutBlueprints;

View File

@@ -1,4 +1,4 @@
#include "SelectedBuildingPanel.h"
#include "SelectionPanel.h"
#include "FactoryQueries.h"
#include <algorithm>
@@ -10,6 +10,9 @@
#include <QLabel>
#include <QListWidget>
#include <QPushButton>
#include <QScrollArea>
#include <QScrollBar>
#include <QStyle>
#include <QVBoxLayout>
#include "BeltSystem.h"
@@ -35,6 +38,17 @@
namespace
{
// Distance kept between the panel and the edges of the band it is anchored to
// (REQ-UI-SELECTION-PANEL).
const int kMarginPx = 8;
// Upper bound on the content width. The panel is content-sized, but several of its
// widgets have no natural width of their own -- the word-wrapped buffer and summary
// labels grow without limit, and a QListWidget asks for 256 px whatever it holds -- so
// the width is capped and the labels wrap at the cap. 320 px is the width the former
// side panel column had at the default window size.
const int kMaxContentWidthPx = 320;
QString buildingTypeName(BuildingType type)
{
if (type == BuildingType::Hq)
@@ -100,29 +114,59 @@ QString rotationLabel(Rotation r)
} // namespace
SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
const GameConfig* config,
QWidget* parent)
SelectionPanel::SelectionPanel(Simulation* sim,
const GameConfig* config,
QWidget* parent)
: QWidget(parent)
, m_sim(sim)
, m_config(config)
, m_splitterTile(0, 0)
{
m_layout = new QVBoxLayout(this);
// The panel floats over the rendered world rather than sitting in a column, so it
// brings its own opaque background to stay legible over any world content
// (REQ-UI-SELECTION-PANEL). Palette colors match the build button bar's chrome; like
// it, this is widget chrome rather than world rendering, so it is deliberately not a
// visuals.toml color. The class scoped selector keeps the border on the panel itself
// rather than cascading onto its child widgets.
setAttribute(Qt::WA_StyledBackground, true);
setStyleSheet(QStringLiteral(
"SelectionPanel { background-color: palette(window);"
" border: 1px solid palette(mid); border-radius: 4px; }"));
// Content taller than the band scrolls rather than overrunning it
// (REQ-UI-SELECTION-PANEL). The viewport is transparent so the panel's own rounded
// chrome shows through, and horizontal scrolling is off because the width always
// follows the content.
m_content = new QWidget(this);
m_scrollArea = new QScrollArea(this);
m_scrollArea->setFrameShape(QFrame::NoFrame);
m_scrollArea->setWidgetResizable(true);
m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
m_scrollArea->viewport()->setAutoFillBackground(false);
m_content->setAutoFillBackground(false);
m_scrollArea->setWidget(m_content);
QVBoxLayout* outerLayout = new QVBoxLayout(this);
outerLayout->setContentsMargins(0, 0, 0, 0);
outerLayout->setSpacing(0);
outerLayout->addWidget(m_scrollArea);
m_layout = new QVBoxLayout(m_content);
m_layout->setContentsMargins(8, 8, 8, 8);
m_layout->setSpacing(4);
m_layout->setAlignment(Qt::AlignTop);
m_titleLabel = new QLabel(this);
m_recipeSelectButton = new QPushButton(this);
m_clearBeltBtn = new QPushButton(tr("Clear Items"), this);
m_filterALabel = new QLabel(this);
m_filterAList = new QListWidget(this);
m_filterBLabel = new QLabel(this);
m_filterBList = new QListWidget(this);
m_layoutPreview = new ShipLayoutPreview(this);
m_configureLayoutBtn = new QPushButton(tr("Configure Layout"), this);
m_buffersLabel = new QLabel(this);
m_titleLabel = new QLabel(m_content);
m_recipeSelectButton = new QPushButton(m_content);
m_clearBeltBtn = new QPushButton(tr("Clear Items"), m_content);
m_filterALabel = new QLabel(m_content);
m_filterAList = new QListWidget(m_content);
m_filterBLabel = new QLabel(m_content);
m_filterBList = new QListWidget(m_content);
m_layoutPreview = new ShipLayoutPreview(m_content);
m_configureLayoutBtn = new QPushButton(tr("Configure Layout"), m_content);
m_buffersLabel = new QLabel(m_content);
m_buffersLabel->setWordWrap(true);
m_filterAList->setMaximumHeight(100);
@@ -140,9 +184,9 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
m_layout->addWidget(m_buffersLabel);
connect(m_recipeSelectButton, &QPushButton::clicked,
this, &SelectedBuildingPanel::onSelectRecipeClicked);
this, &SelectionPanel::onSelectRecipeClicked);
connect(m_clearBeltBtn, &QPushButton::clicked,
this, &SelectedBuildingPanel::onClearBelt);
this, &SelectionPanel::onClearBelt);
connect(m_configureLayoutBtn, &QPushButton::clicked, this, [this]() {
if (m_singleBuildingId.has_value())
{
@@ -151,13 +195,13 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
}
});
connect(m_filterAList, &QListWidget::itemChanged,
this, &SelectedBuildingPanel::onSplitterFilterChanged);
this, &SelectionPanel::onSplitterFilterChanged);
connect(m_filterBList, &QListWidget::itemChanged,
this, &SelectedBuildingPanel::onSplitterFilterChanged);
this, &SelectionPanel::onSplitterFilterChanged);
// The field selection renders below the building content and hides itself while
// nothing field-side is selected, so it costs no space then.
m_fieldSelectionPanel = new FieldSelectionPanel(sim, config, this);
m_fieldSelectionPanel = new FieldSelectionPanel(sim, config, m_content);
m_layout->addWidget(m_fieldSelectionPanel);
buildEmpty();
@@ -165,12 +209,18 @@ SelectedBuildingPanel::SelectedBuildingPanel(Simulation* sim,
registerForEvents();
}
SelectedBuildingPanel::~SelectedBuildingPanel()
SelectionPanel::~SelectionPanel()
{
unregisterForEvents();
}
void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
void SelectionPanel::anchorTo(const QRect& bandRect)
{
m_bandRect = bandRect;
updateVisibility();
}
void SelectionPanel::onSelectionChanged(const std::vector<BuildingId>& ids)
{
m_selectedBuildingIds = ids;
if (!ids.empty())
@@ -182,17 +232,92 @@ void SelectedBuildingPanel::onSelectionChanged(const std::vector<BuildingId>& id
rebuild();
}
void SelectedBuildingPanel::yieldToFieldSelection()
void SelectionPanel::yieldToFieldSelection()
{
// The mirror image of onSelectionChanged(): a field selection — actors, debris, or
// both — supersedes any building selection (REQ-UI-SELECTION-CATEGORIES). An empty
// field selection changes nothing here: the building content, if any, keeps the panel.
if (!m_fieldSelectionPanel->hasSelection()) { return; }
m_selectedBuildingIds.clear();
// field selection leaves the content alone: the building content, if any, keeps the
// panel; it only has to be re-checked for whether anything is left to show at all.
if (!m_fieldSelectionPanel->hasSelection())
{
updateVisibility();
return;
}
buildEmpty();
}
void SelectedBuildingPanel::rebuild()
void SelectionPanel::updateVisibility()
{
// Nothing selected in either category means no panel at all rather than an empty
// one (REQ-UI-EMPTY-SELECTION), leaving the whole game world view visible. Before
// the owner has anchored the panel there is nowhere to put it either, so it stays
// hidden until then.
const bool shouldShow =
(!m_selectedBuildingIds.empty() || m_fieldSelectionPanel->hasSelection())
&& !m_bandRect.isNull();
setVisible(shouldShow);
if (shouldShow)
{
refit();
}
}
// Only ever reached through updateVisibility(), i.e. with a band to fit into.
void SelectionPanel::refit()
{
// The layout drops hidden widgets from its size hint, but only once it has been
// re-run: the rebuild paths call this straight after hide()/show(), before Qt would
// get around to it on its own.
m_content->layout()->activate();
const QRect band =
m_bandRect.adjusted(kMarginPx, kMarginPx, -kMarginPx, -kMarginPx);
// The panel's border is drawn around the scroll area rather than around the
// content, so it is added to whatever the content asks for. Spelled out here
// instead of read back from contentsMargins() because the stylesheet box is what
// sets it, and asking the style for it before the first show is unreliable.
const int borderPx = 1;
const int maxWidthPx = qMin(kMaxContentWidthPx, band.width() - 2 * borderPx);
const int maxHeightPx = band.height() - 2 * borderPx;
if (maxWidthPx <= 0 || maxHeightPx <= 0)
{
return;
}
int contentWidthPx = qMin(m_content->sizeHint().width(), maxWidthPx);
// Word-wrapped labels only know their height once the width is fixed; the layout
// reports -1 when nothing in it wraps, in which case the plain hint is exact.
int contentHeightPx = m_content->heightForWidth(contentWidthPx);
if (contentHeightPx < 0)
{
contentHeightPx = m_content->sizeHint().height();
}
if (contentHeightPx > maxHeightPx)
{
// Content taller than the band is capped there and scrolls
// (REQ-UI-SELECTION-PANEL). The scroll bar is laid out beside the content, so
// the panel widens by its width to keep the content as wide as the height was
// computed for.
contentHeightPx = maxHeightPx;
contentWidthPx = qMin(
contentWidthPx + m_scrollArea->verticalScrollBar()->sizeHint().width(),
maxWidthPx);
}
const int panelWidthPx = contentWidthPx + 2 * borderPx;
const int panelHeightPx = contentHeightPx + 2 * borderPx;
// Right-aligned in the band and centered on it vertically. The band excludes the
// build button bar's strip, so centering here never puts the panel over the bar
// (REQ-UI-SELECTION-PANEL).
setGeometry(band.right() - panelWidthPx + 1,
band.top() + (band.height() - panelHeightPx) / 2,
panelWidthPx, panelHeightPx);
}
void SelectionPanel::rebuild()
{
if (m_selectedBuildingIds.empty())
{
@@ -208,7 +333,7 @@ void SelectedBuildingPanel::rebuild()
}
}
void SelectedBuildingPanel::hideAllWidgets()
void SelectionPanel::hideAllWidgets()
{
m_titleLabel->hide();
m_recipeSelectButton->hide();
@@ -222,15 +347,20 @@ void SelectedBuildingPanel::hideAllWidgets()
m_buffersLabel->hide();
}
void SelectedBuildingPanel::buildEmpty()
void SelectionPanel::buildEmpty()
{
// Shows nothing for the building category — either because nothing is selected or
// because the field category has taken the panel over.
m_singleBuildingId = std::nullopt;
// Also reached when the selected building has gone away under the panel (it was
// deconstructed, or its site finished building): dropping the ids keeps them from
// outliving the content and holding the panel on screen (REQ-UI-EMPTY-SELECTION).
m_selectedBuildingIds.clear();
hideAllWidgets();
updateVisibility();
}
void SelectedBuildingPanel::buildSingle(BuildingId id)
void SelectionPanel::buildSingle(BuildingId id)
{
m_singleBuildingId = id;
hideAllWidgets();
@@ -341,7 +471,7 @@ void SelectedBuildingPanel::buildSingle(BuildingId id)
}
}
void SelectedBuildingPanel::refreshSiteProgress(const ConstructionSite* s)
void SelectionPanel::refreshSiteProgress(const ConstructionSite* s)
{
QString progress;
if (s->completesAt == 0)
@@ -369,9 +499,13 @@ void SelectedBuildingPanel::refreshSiteProgress(const ConstructionSite* s)
}
}
m_buffersLabel->setText(progress);
// The progress line changes width as it counts up, and the panel is sized to its
// content, so every refresh re-fits it (REQ-UI-SELECTION-PANEL).
updateVisibility();
}
void SelectedBuildingPanel::refreshBuffers(const Building* b)
void SelectionPanel::refreshBuffers(const Building* b)
{
const RecipeDef* recipe = findRecipe(b);
const ShipDef* shipDef = (b->type == BuildingType::Shipyard)
@@ -528,9 +662,13 @@ void SelectedBuildingPanel::refreshBuffers(const Building* b)
// Configure Layout button's visibility; otherwise they stay hidden until the
// building is re-selected (which re-runs buildSingle).
updateShipyardLayoutWidgets(b->type, b->recipeId, b->shipLayout);
// Buffer counts and the progress line change width as they run, and the panel is
// sized to its content, so every refresh re-fits it (REQ-UI-SELECTION-PANEL).
updateVisibility();
}
void SelectedBuildingPanel::updateShipyardLayoutWidgets(
void SelectionPanel::updateShipyardLayoutWidgets(
BuildingType type,
const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout)
@@ -570,24 +708,24 @@ void SelectedBuildingPanel::updateShipyardLayoutWidgets(
m_configureLayoutBtn->show();
}
const RecipeDef* SelectedBuildingPanel::findRecipe(const Building* b) const
const RecipeDef* SelectionPanel::findRecipe(const Building* b) const
{
if (b->recipeId.empty()) { return nullptr; }
return m_config->recipes.findRecipeDef(b->recipeId, b->type);
}
const ShipDef* SelectedBuildingPanel::findShipDef(const std::string& id) const
const ShipDef* SelectionPanel::findShipDef(const std::string& id) const
{
if (id.empty()) { return nullptr; }
return m_config->ships.findShipDef(id);
}
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
void SelectionPanel::handleEvent(std::shared_ptr<const TickAdvancedEvent> /*event*/)
{
refreshSelectionDisplay(RefreshReason::PeriodicTick);
}
void SelectedBuildingPanel::handleEvent(
void SelectionPanel::handleEvent(
std::shared_ptr<const PlayerCommandsAppliedEvent> /*event*/)
{
// Player commands (e.g. choosing a shipyard schematic) are applied by a
@@ -597,12 +735,18 @@ void SelectedBuildingPanel::handleEvent(
refreshSelectionDisplay(RefreshReason::CommandApplied);
}
void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
void SelectionPanel::refreshSelectionDisplay(RefreshReason reason)
{
// Only a single selected building has live content to refresh. While the field
// category owns the panel there is none: yieldToFieldSelection() has cleared it, so
// this returns immediately and the field panel refreshes itself off the same events.
if (!m_singleBuildingId.has_value()) { return; }
// Its content changes size as it does (a ship's HP, a debris pile's scrap), so the
// panel is re-fitted to it before returning.
if (!m_singleBuildingId.has_value())
{
updateVisibility();
return;
}
const Building* b = findBuilding(m_sim->getFactoryState(), *m_singleBuildingId);
if (b)
{
@@ -636,7 +780,7 @@ void SelectedBuildingPanel::refreshSelectionDisplay(RefreshReason reason)
buildEmpty();
}
void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
void SelectionPanel::buildMulti(const std::vector<BuildingId>& ids)
{
m_singleBuildingId = std::nullopt;
m_recipeSelectButton->hide();
@@ -646,6 +790,11 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
m_filterBLabel->hide();
m_filterBList->hide();
m_buffersLabel->hide();
// Per-building detail is not shown for a multi-selection (REQ-UI-MULTI-SELECTION),
// so a shipyard's preview must not survive from a previous single selection — it
// would both mislead and pad the content-sized panel.
m_layoutPreview->hide();
m_configureLayoutBtn->hide();
std::map<BuildingType, int> counts;
for (BuildingId id : ids)
@@ -690,9 +839,11 @@ void SelectedBuildingPanel::buildMulti(const std::vector<BuildingId>& ids)
{
m_clearBeltBtn->show();
}
updateVisibility();
}
void SelectedBuildingPanel::onSelectRecipeClicked()
void SelectionPanel::onSelectRecipeClicked()
{
if (!m_singleBuildingId.has_value())
{
@@ -709,7 +860,7 @@ void SelectedBuildingPanel::onSelectRecipeClicked()
rebuild();
}
void SelectedBuildingPanel::buildSplitterFilters(
void SelectionPanel::buildSplitterFilters(
const std::optional<BeltSystem::SplitterInfo>& info)
{
if (!info.has_value())
@@ -753,7 +904,7 @@ void SelectedBuildingPanel::buildSplitterFilters(
rotationLabel(info->outputB), info->filterB);
}
void SelectedBuildingPanel::onSplitterFilterChanged()
void SelectionPanel::onSplitterFilterChanged()
{
if (!m_singleBuildingId.has_value())
{
@@ -796,7 +947,7 @@ void SelectedBuildingPanel::onSplitterFilterChanged()
}
}
std::vector<std::string> SelectedBuildingPanel::getAllItemIds() const
std::vector<std::string> SelectionPanel::getAllItemIds() const
{
std::set<std::string> seen;
for (const RecipeDef& recipe : m_config->recipes.recipes)
@@ -813,7 +964,7 @@ std::vector<std::string> SelectedBuildingPanel::getAllItemIds() const
return std::vector<std::string>(seen.begin(), seen.end());
}
void SelectedBuildingPanel::onClearBelt()
void SelectionPanel::onClearBelt()
{
std::vector<QPoint> tiles;
for (BuildingId id : m_selectedBuildingIds)
@@ -837,18 +988,18 @@ void SelectedBuildingPanel::onClearBelt()
}
}
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event)
void SelectionPanel::handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event)
{
m_fieldSelectionPanel->setSelectedEntities(event->entities);
yieldToFieldSelection();
}
void SelectedBuildingPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
void SelectionPanel::handleEvent(std::shared_ptr<const SelectionChangedEvent> event)
{
onSelectionChanged(event->ids);
}
void SelectedBuildingPanel::handleEvent(
void SelectionPanel::handleEvent(
std::shared_ptr<const DebrisSelectionChangedEvent> event)
{
// Debris is a field object: it supersedes any building selection but coexists

View File

@@ -5,6 +5,7 @@
#include <vector>
#include <QPoint>
#include <QRect>
#include <QWidget>
#include "BeltSystem.h"
@@ -28,6 +29,7 @@ class ShipLayoutPreview;
class QLabel;
class QListWidget;
class QPushButton;
class QScrollArea;
class QVBoxLayout;
// Shows the current selection. The building category (buildings and construction sites)
@@ -38,19 +40,30 @@ class QVBoxLayout;
// is the sole arbiter of which one owns the content: it listens to all three selection
// events, forwards the field ones to the child panel, and drops the losing category's
// content. Neither panel touches the other's widgets.
class SelectedBuildingPanel : public QWidget,
public CombinedEventHandler<TickAdvancedEvent,
PlayerCommandsAppliedEvent,
EntitySelectionChangedEvent,
SelectionChangedEvent,
DebrisSelectionChangedEvent>
//
// The panel floats over the game world view rather than occupying a column of its own
// (REQ-UI-SELECTION-PANEL): it sizes itself to its content, anchors to the right edge of
// the band its owner hands it, and hides itself entirely while nothing is selected
// (REQ-UI-EMPTY-SELECTION).
class SelectionPanel : public QWidget,
public CombinedEventHandler<TickAdvancedEvent,
PlayerCommandsAppliedEvent,
EntitySelectionChangedEvent,
SelectionChangedEvent,
DebrisSelectionChangedEvent>
{
Q_OBJECT
public:
SelectedBuildingPanel(Simulation* sim, const GameConfig* config,
QWidget* parent = nullptr);
~SelectedBuildingPanel() override;
SelectionPanel(Simulation* sim, const GameConfig* config,
QWidget* parent = nullptr);
~SelectionPanel() override;
// Confines the panel to the given band of the game world view: it right-aligns
// within it and centers vertically in it. The band is the world view less the strip
// the build button bar occupies, so the two never overlap and the bar never has to
// move (REQ-UI-SELECTION-PANEL, REQ-UI-BUILD-BAR).
void anchorTo(const QRect& bandRect);
private:
void handleEvent(std::shared_ptr<const TickAdvancedEvent> event) override;
@@ -78,6 +91,13 @@ private:
void onSelectionChanged(const std::vector<BuildingId>& ids);
// Gives the panel to the field category once it has anything selected.
void yieldToFieldSelection();
// Shows the panel while either category has a selection and hides it otherwise
// (REQ-UI-EMPTY-SELECTION), re-fitting it to its current content while it is shown.
// Every path that changes the content ends here, because the content is what sizes
// the panel.
void updateVisibility();
// Re-fits the panel to its content within the anchored band.
void refit();
void refreshSelectionDisplay(RefreshReason reason);
void rebuild();
void hideAllWidgets();
@@ -98,6 +118,15 @@ private:
const GameConfig* m_config;
std::vector<BuildingId> m_selectedBuildingIds;
// The band the panel confines itself to, in the coordinates of its parent; null
// until the owner has anchored it for the first time.
QRect m_bandRect;
// Scrolls the content once it outgrows the band (REQ-UI-SELECTION-PANEL). All the
// content widgets below are children of m_content, not of the panel itself.
QScrollArea* m_scrollArea;
QWidget* m_content;
QVBoxLayout* m_layout;
QLabel* m_titleLabel;
QPushButton* m_recipeSelectButton;