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

Implements REQ-UI-SELECTION-PANEL and the full-width REQ-UI-WORLD-SIZE /
REQ-UI-HEADER: MainWindow drops the 25% side column and its 75/25 math, so
the header bar and world view span the window, and the panel joins the
build button bar as a widget floating over the world.

The panel follows the bar's pattern -- an opaque sibling built after the
world view, so it sits above the vignettes, below the dim overlay, and
swallows the mouse events that would otherwise reach the world. It sizes
itself to its content within a band the window hands it (the world view
less the bar's strip, so the bar never has to move for it), right-aligned
and centered in that band, and scrolls once the content outgrows it. Its
content moved into a scroll area for that; the width is capped at 320 px
because the wrapped labels and the splitter filter lists have no natural
width of their own. With nothing selected the panel now hides entirely
rather than showing an empty box (REQ-UI-EMPTY-SELECTION).

Two defects that content sizing exposed: buildEmpty() left the selected
ids behind when the building vanished under the panel, which would have
held an empty panel on screen, and buildMulti() let a shipyard's layout
preview survive from a previous single selection (REQ-UI-MULTI-SELECTION).

Renames SelectedBuildingPanel to SelectionPanel throughout, matching the
requirements: the panel has long shown ships, stations, and debris too.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JcReq7hVk4KUPhTDKWAG7K
This commit is contained in:
2026-08-06 21:58:36 +02:00
parent 7ddb4a1bb5
commit 668ce0fcb8
13 changed files with 281 additions and 98 deletions

151
src/ui/SelectionPanel.h Normal file
View File

@@ -0,0 +1,151 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include <QPoint>
#include <QRect>
#include <QWidget>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingId.h"
#include "EntitySelectionChangedEvent.h"
#include "EventHandler.h"
#include "GameConfig.h"
#include "PlayerCommandsAppliedEvent.h"
#include "RecipesConfig.h"
#include "DebrisSelectionChangedEvent.h"
#include "SelectionChangedEvent.h"
#include "ShipLayout.h"
#include "ShipsConfig.h"
#include "Tick.h"
#include "TickAdvancedEvent.h"
class Simulation;
class FieldSelectionPanel;
class ShipLayoutPreview;
class QLabel;
class QListWidget;
class QPushButton;
class QScrollArea;
class QVBoxLayout;
// Shows the current selection. The building category (buildings and construction sites)
// is rendered by this panel itself; the field category (ships, defence stations, debris)
// is rendered by the embedded FieldSelectionPanel.
//
// The two categories are mutually exclusive (REQ-UI-SELECTION-CATEGORIES) and this panel
// 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.
//
// 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:
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;
void handleEvent(std::shared_ptr<const PlayerCommandsAppliedEvent> event) override;
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
private slots:
void onSelectRecipeClicked();
void onClearBelt();
void onSplitterFilterChanged();
private:
// Why the selection display is being refreshed. A periodic tick only needs a
// lightweight content update (e.g. a construction site's progress label),
// whereas an applied player command may have changed the configuration and
// needs a full structural rebuild.
enum class RefreshReason
{
PeriodicTick,
CommandApplied
};
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();
void buildEmpty();
void buildSingle(BuildingId id);
void buildMulti(const std::vector<BuildingId>& ids);
void refreshBuffers(const Building* b);
void refreshSiteProgress(const ConstructionSite* s);
void updateShipyardLayoutWidgets(BuildingType type,
const std::string& recipeId,
const std::optional<ShipLayoutConfig>& shipLayout);
void buildSplitterFilters(const std::optional<BeltSystem::SplitterInfo>& info);
const RecipeDef* findRecipe(const Building* b) const;
const ShipDef* findShipDef(const std::string& id) const;
std::vector<std::string> getAllItemIds() const;
Simulation* m_sim;
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;
QPushButton* m_clearBeltBtn;
QLabel* m_filterALabel;
QListWidget* m_filterAList;
QLabel* m_filterBLabel;
QListWidget* m_filterBList;
QLabel* m_buffersLabel;
ShipLayoutPreview* m_layoutPreview;
QPushButton* m_configureLayoutBtn;
std::optional<BuildingId> m_singleBuildingId;
bool m_singleIsSite = false; // selected single entity is a construction site
QPoint m_splitterTile;
std::string m_currentRecipeId;
// Renders the field selection (actors + debris) below the building content
// (REQ-UI-FIELD-MULTI-SELECTION). Hides itself while nothing field-side is selected.
FieldSelectionPanel* m_fieldSelectionPanel;
};