float the selection panel over the game world instead of a side column
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user