split the selection panel into one card per kind of selection

This commit is contained in:
2026-08-07 18:38:55 +02:00
parent cc0ef856e3
commit 2289277e12
60 changed files with 3014 additions and 1559 deletions

View File

@@ -1,48 +1,34 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include <QPoint>
#include <QRect>
#include <QWidget>
#include "BeltSystem.h"
#include "Building.h"
#include "BuildingId.h"
#include "DebrisSelectionChangedEvent.h"
#include "DebugDrawToggledEvent.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"
#include "selection/SelectionContentFactory.h"
#include "selection/SelectionContext.h"
struct GameConfig;
struct VisualsConfig;
class BuildingIconCache;
class ItemIconCache;
class SelectionContent;
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.
// Shows the current selection. The panel itself renders nothing: it arbitrates between
// the two selection categories (REQ-UI-SELECTION-CATEGORIES), picks the card that fits
// what is selected (REQ-UI-SELECTION-CONTENT), and hosts exactly one of them at a time.
// What each card looks like lives in src/ui/selection/.
//
// 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
// (REQ-UI-SELECTION-PANEL): it sizes itself to its card, 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,
@@ -50,13 +36,17 @@ class SelectionPanel : public QWidget,
PlayerCommandsAppliedEvent,
EntitySelectionChangedEvent,
SelectionChangedEvent,
DebrisSelectionChangedEvent>
DebrisSelectionChangedEvent,
DebugDrawToggledEvent>
{
Q_OBJECT
public:
// visuals, itemIcons and buildingIcons are window-wide rendering resources the cards
// draw from; none is owned and all must outlive this widget.
SelectionPanel(Simulation* sim, const GameConfig* config,
QWidget* parent = nullptr);
const VisualsConfig* visuals, ItemIconCache* itemIcons,
BuildingIconCache* buildingIcons, QWidget* parent = nullptr);
~SelectionPanel() override;
// Confines the panel to the given band of the game world view: it right-aligns
@@ -66,86 +56,41 @@ public:
void anchorTo(const QRect& bandRect);
private:
void handleEvent(std::shared_ptr<const SelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const EntitySelectionChangedEvent> event) override;
void handleEvent(std::shared_ptr<const DebrisSelectionChangedEvent> event) override;
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;
void handleEvent(std::shared_ptr<const DebugDrawToggledEvent> 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.
// Re-reads the live values of the card on screen. When the selection now calls for a
// different card -- a construction site finishing is the case that matters -- it
// rebuilds instead, because a card never changes its own shape.
void refreshContent();
// Replaces the card with the one the current selection calls for.
void rebuildContent();
// Shows the panel while a card exists and hides it otherwise
// (REQ-UI-EMPTY-SELECTION), re-fitting it to the card while it is shown.
void updateVisibility();
// Re-fits the panel to its content within the anchored band.
// Re-fits the panel to its card 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;
SelectionContext m_context;
// Read through m_context by the cards that need it, so a toggle reaches the card on
// screen without it having to subscribe to the event itself.
bool m_debugDrawEnabled = false;
SelectionRequest m_request;
ContentKey m_contentKey;
SelectionContent* m_content = nullptr;
// 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.
// Scrolls the card once it outgrows the band (REQ-UI-SELECTION-PANEL). The card is a
// child of m_body, 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;
QWidget* m_body;
QVBoxLayout* m_bodyLayout;
};